Compare commits
2 Commits
feature/at
...
feature/ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 760bfc474a | |||
| 79cbb56707 |
@@ -33,7 +33,6 @@ const TurtleGame = lazy(() => import("./atividades/programacao/turtle/TurtleGame
|
|||||||
const CriptoGame = lazy(() => import("./atividades/programacao/cripto/CriptoGame"));
|
const CriptoGame = lazy(() => import("./atividades/programacao/cripto/CriptoGame"));
|
||||||
const ExemploGame = lazy(() => import("./atividades/programacao/exemplo/ExemploGame"));
|
const ExemploGame = lazy(() => import("./atividades/programacao/exemplo/ExemploGame"));
|
||||||
const ExemploGame2 = lazy(() => import("./atividades/programacao/exemplo2/ExemploGame2"));
|
const ExemploGame2 = lazy(() => import("./atividades/programacao/exemplo2/ExemploGame2"));
|
||||||
const PadroesGame = lazy(() => import("./atividades/programacao/padroes/PadroesGame"));
|
|
||||||
|
|
||||||
const LoadingFallback = () => (
|
const LoadingFallback = () => (
|
||||||
<div
|
<div
|
||||||
@@ -104,7 +103,6 @@ function AppRoutes() {
|
|||||||
<Route path="/atividades/programacao/turtle" element={<TurtleGame />} />
|
<Route path="/atividades/programacao/turtle" element={<TurtleGame />} />
|
||||||
<Route path="/atividades/programacao/exemplo" element={<ExemploGame />} />
|
<Route path="/atividades/programacao/exemplo" element={<ExemploGame />} />
|
||||||
<Route path="/atividades/programacao/exemplo2" element={<ExemploGame2 />} />
|
<Route path="/atividades/programacao/exemplo2" element={<ExemploGame2 />} />
|
||||||
<Route path="/atividades/programacao/padroes" element={<PadroesGame />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
{/* Modal overlay routes — rendered on top of the background page */}
|
{/* Modal overlay routes — rendered on top of the background page */}
|
||||||
|
|||||||
BIN
app/src/assets/capicoda_anot.jpeg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
app/src/assets/capicoda_anot.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
app/src/assets/capicoda_end.jpeg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
app/src/assets/capicoda_end.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
app/src/assets/capicoda_prof.jpeg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
app/src/assets/capicoda_prof.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
app/src/assets/capicoda_start.jpeg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
app/src/assets/capicoda_start.png
Normal file
|
After Width: | Height: | Size: 261 KiB |
@@ -1,61 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Componente React de entrada do jogo Padrões.
|
|
||||||
* @module games.padroes.PadroesGame
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import GameBase from "../../../components/game/GameBase";
|
|
||||||
import GameEditor from "../../../components/game/GameEditor";
|
|
||||||
import BlocklyEditor from "../../../components/game/editors/BlocklyEditor";
|
|
||||||
import { createGame } from "./game";
|
|
||||||
import { gameConfig } from "./config/config";
|
|
||||||
import { generateDynamicToolbox, registerBlocks } from "./blocks/blocks";
|
|
||||||
import {
|
|
||||||
GameStateProvider,
|
|
||||||
useGameState,
|
|
||||||
} from "../../../contexts/GameStateContext";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Monta a cena e o editor do jogo Padrões.
|
|
||||||
* Registra os blocos, configura o toolbox dinâmico e injeta o `gameFactory`.
|
|
||||||
* @returns {JSX.Element} Conteúdo do jogo (editor + canvas)
|
|
||||||
*/
|
|
||||||
function PadroesContent() {
|
|
||||||
const { setFailureMessage } = useGameState();
|
|
||||||
|
|
||||||
// Registra os blocos customizados no Blockly ao montar
|
|
||||||
useEffect(() => {
|
|
||||||
registerBlocks();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Memoriza o gerador do toolbox para evitar recriações
|
|
||||||
const toolboxGenerator = useMemo(
|
|
||||||
() => (allowedBlocks) => generateDynamicToolbox(allowedBlocks),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GameBase
|
|
||||||
gameFactory={createGame}
|
|
||||||
gameConfig={gameConfig}
|
|
||||||
customFailureHandler={setFailureMessage}
|
|
||||||
failureHandler={setFailureMessage}
|
|
||||||
>
|
|
||||||
<GameEditor>
|
|
||||||
<BlocklyEditor toolboxGenerator={toolboxGenerator} />
|
|
||||||
</GameEditor>
|
|
||||||
</GameBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Componente de página que provê o contexto de estado do jogo Padrões.
|
|
||||||
* @returns {JSX.Element} Página completa do jogo
|
|
||||||
*/
|
|
||||||
export default function PadroesGame() {
|
|
||||||
return (
|
|
||||||
<GameStateProvider gameConfig={gameConfig}>
|
|
||||||
<PadroesContent />
|
|
||||||
</GameStateProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Blocos, geradores e toolbox do jogo Padrões.
|
|
||||||
*
|
|
||||||
* Blocos customizados expõem: leitura da ENTRADA, definição/leitura de
|
|
||||||
* SAÍDA, CONTADOR e LETRA, a constante ALFABETO A-Z e as operações de
|
|
||||||
* string text_charAt / text_indexOf (0-based, alinhadas ao cripto).
|
|
||||||
*
|
|
||||||
* @module games.padroes.blocks.blocks
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import * as Blockly from "blockly/core";
|
|
||||||
import "blockly/blocks";
|
|
||||||
import { javascriptGenerator } from "blockly/javascript";
|
|
||||||
import { CORES_BLOCKLY } from "@/blockly/blocklyColors";
|
|
||||||
import {
|
|
||||||
configurarGerador,
|
|
||||||
gerarExpressao,
|
|
||||||
gerarStatementComValor,
|
|
||||||
} from "@/blockly/generator";
|
|
||||||
import { gerarToolboxDeEstrutura } from "@/blockly/toolbox";
|
|
||||||
import {
|
|
||||||
criarBlocoExpressaoSimples,
|
|
||||||
criarBlocoStatementComValor,
|
|
||||||
} from "@/blockly/blockFactory";
|
|
||||||
|
|
||||||
const C = CORES_BLOCKLY; // LOGICA:210, LOOPS:120, MATEMATICA:230, TEXTO:160, VARIAVEIS:330
|
|
||||||
|
|
||||||
// Estrutura declarativa da toolbox (cada categoria filtra por allowedBlocks).
|
|
||||||
const ESTRUTURA_TOOLBOX = [
|
|
||||||
{
|
|
||||||
nome: "Entrada/Saída",
|
|
||||||
blocos: ["obter_entrada", "definir_saida", "obter_saida"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nome: "Variáveis",
|
|
||||||
blocos: [
|
|
||||||
"definir_contador",
|
|
||||||
"obter_contador",
|
|
||||||
"definir_letra",
|
|
||||||
"obter_letra",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ nome: "Repetição", blocos: ["controls_whileUntil"] },
|
|
||||||
{
|
|
||||||
nome: "Lógica",
|
|
||||||
blocos: ["controls_if", "logic_compare", "logic_operation"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nome: "Texto",
|
|
||||||
blocos: ["text", "text_charAt", "text_indexOf", "text_length", "alfabeto"],
|
|
||||||
},
|
|
||||||
{ nome: "Matemática", blocos: ["math_number", "math_arithmetic"] },
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registra todos os blocos e geradores do jogo Padrões no Blockly.
|
|
||||||
* Deve ser chamado uma vez na montagem do componente.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export const registerBlocks = () => {
|
|
||||||
defineBlocks();
|
|
||||||
defineGenerators();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gera a toolbox contendo apenas os blocos permitidos pela fase.
|
|
||||||
* @param {Array<string>} [allowedBlocks=[]] - Lista de blocos habilitados
|
|
||||||
* @returns {Object} Estrutura `categoryToolbox` para o Blockly
|
|
||||||
*/
|
|
||||||
export const generateDynamicToolbox = (allowedBlocks = []) => {
|
|
||||||
return gerarToolboxDeEstrutura(ESTRUTURA_TOOLBOX, allowedBlocks);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===================== Definições de blocos =====================
|
|
||||||
|
|
||||||
const defineBlocks = () => {
|
|
||||||
// Entrada (somente leitura — é pré-definida pela fase)
|
|
||||||
criarBlocoExpressaoSimples(
|
|
||||||
"obter_entrada",
|
|
||||||
"ENTRADA",
|
|
||||||
null,
|
|
||||||
C.VARIAVEIS,
|
|
||||||
"O texto que a fase quer analisar",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Saída
|
|
||||||
criarBlocoStatementComValor(
|
|
||||||
"definir_saida",
|
|
||||||
"definir SAÍDA como",
|
|
||||||
"VALUE",
|
|
||||||
null,
|
|
||||||
C.VARIAVEIS,
|
|
||||||
);
|
|
||||||
criarBlocoExpressaoSimples(
|
|
||||||
"obter_saida",
|
|
||||||
"SAÍDA",
|
|
||||||
null,
|
|
||||||
C.VARIAVEIS,
|
|
||||||
"O veredito atual",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Contador (índice do loop)
|
|
||||||
criarBlocoStatementComValor(
|
|
||||||
"definir_contador",
|
|
||||||
"definir CONTADOR como",
|
|
||||||
"VALUE",
|
|
||||||
null,
|
|
||||||
C.LOOPS,
|
|
||||||
);
|
|
||||||
criarBlocoExpressaoSimples(
|
|
||||||
"obter_contador",
|
|
||||||
"CONTADOR",
|
|
||||||
null,
|
|
||||||
C.LOOPS,
|
|
||||||
"Valor atual do contador",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Letra (caractere atual)
|
|
||||||
criarBlocoStatementComValor(
|
|
||||||
"definir_letra",
|
|
||||||
"definir LETRA como",
|
|
||||||
"VALUE",
|
|
||||||
null,
|
|
||||||
C.VARIAVEIS,
|
|
||||||
);
|
|
||||||
criarBlocoExpressaoSimples(
|
|
||||||
"obter_letra",
|
|
||||||
"LETRA",
|
|
||||||
null,
|
|
||||||
C.VARIAVEIS,
|
|
||||||
"Valor atual da letra",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Constante: alfabeto A-Z
|
|
||||||
criarBlocoExpressaoSimples(
|
|
||||||
"alfabeto",
|
|
||||||
"ALFABETO A-Z",
|
|
||||||
"String",
|
|
||||||
C.TEXTO,
|
|
||||||
"Retorna ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
||||||
);
|
|
||||||
|
|
||||||
// text_charAt — 0-based (sobrescreve o nativo para alinhar com CONTADOR=0)
|
|
||||||
Blockly.Blocks["text_charAt"] = {
|
|
||||||
init: function () {
|
|
||||||
this.setColour(C.TEXTO);
|
|
||||||
this.setOutput(true, "String");
|
|
||||||
this.appendValueInput("VALUE")
|
|
||||||
.setCheck("String")
|
|
||||||
.appendField("no texto");
|
|
||||||
this.appendValueInput("AT")
|
|
||||||
.setCheck("Number")
|
|
||||||
.appendField("obter letra nº");
|
|
||||||
this.setInputsInline(true);
|
|
||||||
this.setTooltip("Letra na posição informada (0 = primeira).");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// text_indexOf — 0-based (retorna -1 quando não encontra)
|
|
||||||
Blockly.Blocks["text_indexOf"] = {
|
|
||||||
init: function () {
|
|
||||||
this.setColour(C.TEXTO);
|
|
||||||
this.setOutput(true, "Number");
|
|
||||||
this.appendValueInput("VALUE")
|
|
||||||
.setCheck("String")
|
|
||||||
.appendField("no texto");
|
|
||||||
this.appendValueInput("FIND")
|
|
||||||
.setCheck("String")
|
|
||||||
.appendField("buscar a posição de");
|
|
||||||
this.setInputsInline(true);
|
|
||||||
this.setTooltip(
|
|
||||||
"Primeira posição (0-based) do trecho, ou -1 se não existir.",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===================== Geradores de código =====================
|
|
||||||
|
|
||||||
const defineGenerators = () => {
|
|
||||||
// Prefix de highlight (faz o bloco piscar na execução passo a passo)
|
|
||||||
configurarGerador();
|
|
||||||
|
|
||||||
// Statements com valor
|
|
||||||
gerarStatementComValor("definir_saida", "VALUE", (v) => `definirSaida(${v})`);
|
|
||||||
gerarStatementComValor("definir_contador", "VALUE", (v) =>
|
|
||||||
`definirContador(${v})`,
|
|
||||||
);
|
|
||||||
gerarStatementComValor("definir_letra", "VALUE", (v) => `var letra = ${v}`);
|
|
||||||
|
|
||||||
// Expressões
|
|
||||||
gerarExpressao(
|
|
||||||
"obter_entrada",
|
|
||||||
"obterEntrada()",
|
|
||||||
javascriptGenerator.ORDER_FUNCTION_CALL,
|
|
||||||
);
|
|
||||||
gerarExpressao(
|
|
||||||
"obter_saida",
|
|
||||||
"obterSaida()",
|
|
||||||
javascriptGenerator.ORDER_FUNCTION_CALL,
|
|
||||||
);
|
|
||||||
gerarExpressao(
|
|
||||||
"obter_contador",
|
|
||||||
"obterContador()",
|
|
||||||
javascriptGenerator.ORDER_FUNCTION_CALL,
|
|
||||||
);
|
|
||||||
gerarExpressao("obter_letra", "letra", javascriptGenerator.ORDER_ATOMIC);
|
|
||||||
gerarExpressao(
|
|
||||||
"alfabeto",
|
|
||||||
'"ABCDEFGHIJKLMNOPQRSTUVWXYZ"',
|
|
||||||
javascriptGenerator.ORDER_ATOMIC,
|
|
||||||
);
|
|
||||||
|
|
||||||
// text_charAt — 0-based
|
|
||||||
javascriptGenerator.forBlock["text_charAt"] = function (block) {
|
|
||||||
const text =
|
|
||||||
javascriptGenerator.valueToCode(
|
|
||||||
block,
|
|
||||||
"VALUE",
|
|
||||||
javascriptGenerator.ORDER_MEMBER,
|
|
||||||
) || "''";
|
|
||||||
const at =
|
|
||||||
javascriptGenerator.valueToCode(
|
|
||||||
block,
|
|
||||||
"AT",
|
|
||||||
javascriptGenerator.ORDER_NONE,
|
|
||||||
) || "0";
|
|
||||||
return [`${text}.charAt(${at})`, javascriptGenerator.ORDER_MEMBER];
|
|
||||||
};
|
|
||||||
|
|
||||||
// text_indexOf — 0-based
|
|
||||||
javascriptGenerator.forBlock["text_indexOf"] = function (block) {
|
|
||||||
const text =
|
|
||||||
javascriptGenerator.valueToCode(
|
|
||||||
block,
|
|
||||||
"VALUE",
|
|
||||||
javascriptGenerator.ORDER_MEMBER,
|
|
||||||
) || "''";
|
|
||||||
const search =
|
|
||||||
javascriptGenerator.valueToCode(
|
|
||||||
block,
|
|
||||||
"FIND",
|
|
||||||
javascriptGenerator.ORDER_NONE,
|
|
||||||
) || "''";
|
|
||||||
return [`${text}.indexOf(${search})`, javascriptGenerator.ORDER_MEMBER];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Configuração do jogo Padrões (reconhecimento de padrões).
|
|
||||||
*
|
|
||||||
* Conceito: o aluno escreve programas que analisam textos e identificam
|
|
||||||
* padrões (apenas letras A-Z, sequências, estruturas, etc.).
|
|
||||||
*
|
|
||||||
* A ENTRADA de cada fase é pré-definida em `entradaTeste`. O aluno lê a
|
|
||||||
* entrada com `obterEntrada()` e deve produzir um veredito em `SAÍDA`.
|
|
||||||
*
|
|
||||||
* @module games.padroes.config.config
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const gameConfig = {
|
|
||||||
gameId: "padroes",
|
|
||||||
gameName: "Padrões",
|
|
||||||
type: "blocks",
|
|
||||||
icon: "🔍",
|
|
||||||
thumbnail: "",
|
|
||||||
descricao:
|
|
||||||
"Aprenda reconhecimento de padrões criando programas que analisam textos e identificam padrões como letras válidas, sequências e estruturas.",
|
|
||||||
dificuldade: "Intermediário",
|
|
||||||
categoria: "Lógica",
|
|
||||||
tempoEstimado: "20-30 min",
|
|
||||||
conceitos: [
|
|
||||||
"Reconhecimento de padrões",
|
|
||||||
"Repetição",
|
|
||||||
"Condicionais",
|
|
||||||
"Strings",
|
|
||||||
],
|
|
||||||
route: "/atividades/programacao/padroes",
|
|
||||||
component: "PadroesGame",
|
|
||||||
objectives: [
|
|
||||||
"Identificar padrões em textos usando loops e condicionais",
|
|
||||||
"Combinar operações de string (charAt, indexOf, length) para validar entradas",
|
|
||||||
],
|
|
||||||
metadata: {
|
|
||||||
lastUpdated: "2026-06-27",
|
|
||||||
version: "0.1.0",
|
|
||||||
},
|
|
||||||
|
|
||||||
fases: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
nome: "Fase 1: Apenas letras de A a Z",
|
|
||||||
descricao:
|
|
||||||
'Crie um loop "enquanto" que percorra cada caractere da ENTRADA e verifique se TODOS pertencem ao alfabeto (A-Z). ' +
|
|
||||||
'Se algum caractere NÃO for uma letra, defina a SAÍDA como "INVÁLIDO". ' +
|
|
||||||
'Se todos forem letras, defina como "VÁLIDO". ' +
|
|
||||||
'Dica: use o bloco ALFABETO A-Z e "buscar a posição de" (indexOf) para testar cada letra — indexOf retorna -1 quando o caractere não existe no alfabeto.',
|
|
||||||
timeout: 30,
|
|
||||||
maxBlocks: 25,
|
|
||||||
// Exige um loop (while/for) com incremento do contador para evitar loop infinito
|
|
||||||
validationRegex: /(while|for)[\s\S]*definirContador[\s\S]*\+/,
|
|
||||||
// Caso de teste desta fase (fase de teste/dev — caso único)
|
|
||||||
entradaTeste: "ABC123",
|
|
||||||
expectedSaida: "INVÁLIDO",
|
|
||||||
allowedBlocks: [
|
|
||||||
"obter_entrada",
|
|
||||||
"definir_saida",
|
|
||||||
"obter_saida",
|
|
||||||
"definir_contador",
|
|
||||||
"obter_contador",
|
|
||||||
"definir_letra",
|
|
||||||
"obter_letra",
|
|
||||||
"alfabeto",
|
|
||||||
"text_charAt",
|
|
||||||
"text_indexOf",
|
|
||||||
"text_length",
|
|
||||||
"text",
|
|
||||||
"controls_whileUntil",
|
|
||||||
"controls_if",
|
|
||||||
"logic_compare",
|
|
||||||
"logic_operation",
|
|
||||||
"math_number",
|
|
||||||
"math_arithmetic",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
mensagens: {
|
|
||||||
semMovimento:
|
|
||||||
"Seu programa não executou nenhuma ação. Use os blocos para analisar a ENTRADA.",
|
|
||||||
semSaida:
|
|
||||||
'Você não definiu a SAÍDA. Use "definir SAÍDA como" com o veredito (VÁLIDO ou INVÁLIDO).',
|
|
||||||
erradoInvalido:
|
|
||||||
'Algo fugiu ao padrão. Algum caractere da entrada NÃO é uma letra de A-Z — a resposta correta seria "INVÁLIDO".',
|
|
||||||
erradoValido:
|
|
||||||
'Todos os caracteres são letras de A-Z — a resposta correta seria "VÁLIDO".',
|
|
||||||
erroEstrutura:
|
|
||||||
"Você precisa usar um loop 'enquanto' para percorrer a ENTRADA caractere a caractere.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Cena Phaser do jogo Padrões.
|
|
||||||
*
|
|
||||||
* A cena mantém o estado de execução (entrada, saída, contador) e expõe
|
|
||||||
* métodos que o interpretador chama via `setupPadroesAPI`. A ENTRADA é
|
|
||||||
* pré-carregada a partir de `configFase.entradaTeste`.
|
|
||||||
*
|
|
||||||
* @module games.padroes.game
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { BaseGameScene } from "../../../shared/BaseGameScene.js";
|
|
||||||
import { setupPadroesAPI } from "./hooks/interpreterSetup.js";
|
|
||||||
import { validationSolution } from "./validation/validators.js";
|
|
||||||
import { gameConfig } from "./config/config.js";
|
|
||||||
import { ConstantesJogo } from "./ui/constants.js";
|
|
||||||
import { inicializarLayout } from "./ui/layout.js";
|
|
||||||
|
|
||||||
class PadroesScene extends BaseGameScene {
|
|
||||||
constructor() {
|
|
||||||
super("PadroesScene");
|
|
||||||
this.entrada = "";
|
|
||||||
this.saida = "";
|
|
||||||
this.contador = 0;
|
|
||||||
this.textoEntrada = null;
|
|
||||||
this.textoSaida = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inicializa o estado da cena a partir da config da fase.
|
|
||||||
* @param {Object} data - Dados passados à cena
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
init(data) {
|
|
||||||
super.init(data);
|
|
||||||
this.limparVariaveis();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preload de assets globais (sons compartilhados).
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
preload() {
|
|
||||||
this.preloadGlobalAssets();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cria o layout visual e conecta o controlador padrão de execução.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
create() {
|
|
||||||
this.validatorFunc = (historico) =>
|
|
||||||
validationSolution(historico, this.configFase, gameConfig, this);
|
|
||||||
|
|
||||||
this.setupStandardController(
|
|
||||||
() => setupPadroesAPI(this, { animationSpeed: 150 }),
|
|
||||||
this.validatorFunc,
|
|
||||||
);
|
|
||||||
|
|
||||||
const layout = inicializarLayout(this);
|
|
||||||
this.textoEntrada = layout.textoEntrada;
|
|
||||||
this.textoSaida = layout.textoSaida;
|
|
||||||
this._atualizarDisplayEntrada();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preparações antes de executar o código do aluno.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
onBeforeRun() {
|
|
||||||
this.historico = [];
|
|
||||||
this.limparVariaveis();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restaura o estado visual ao resetar.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
onReset() {
|
|
||||||
this.limparVariaveis();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reseta as variáveis de estado e os displays visuais.
|
|
||||||
* A entrada volta a ser o caso de teste da fase.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
limparVariaveis() {
|
|
||||||
this.entrada = this.configFase?.entradaTeste ?? "";
|
|
||||||
this.saida = "";
|
|
||||||
this.contador = 0;
|
|
||||||
this._atualizarDisplayEntrada();
|
|
||||||
if (this.textoSaida) this.textoSaida.setText("");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Atualiza o texto do painel de ENTRADA.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_atualizarDisplayEntrada() {
|
|
||||||
if (this.textoEntrada) this.textoEntrada.setText(this.entrada || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== API exposta ao interpretador =====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define o veredito (SAÍDA) e atualiza o display.
|
|
||||||
* @param {string} valor - Veredito do aluno (ex.: "VÁLIDO" / "INVÁLIDO")
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
definirSaida(valor) {
|
|
||||||
this.saida = String(valor ?? "");
|
|
||||||
this.historico.push({ tipo: "definir_saida", valor: this.saida });
|
|
||||||
if (this.textoSaida) this.textoSaida.setText(this.saida);
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retorna o veredito atual.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
obterSaida() {
|
|
||||||
this.historico.push({ tipo: "obter_saida", valor: this.saida });
|
|
||||||
return this.saida;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define o valor do contador.
|
|
||||||
* @param {number} valor
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
definirContador(valor) {
|
|
||||||
this.contador = Number(valor) || 0;
|
|
||||||
this.historico.push({ tipo: "definir_contador", valor: this.contador });
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retorna o valor atual do contador.
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
obterContador() {
|
|
||||||
this.historico.push({ tipo: "obter_contador", valor: this.contador });
|
|
||||||
return this.contador;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retorna a entrada de teste da fase.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
obterEntrada() {
|
|
||||||
this.historico.push({ tipo: "obter_entrada", valor: this.entrada });
|
|
||||||
return this.entrada;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory que cria a configuração Phaser do jogo Padrões.
|
|
||||||
* Injeta configFase e gameConfig no registry antes do boot da cena.
|
|
||||||
* @param {HTMLElement} elementoPai - Container DOM
|
|
||||||
* @param {Object} configFaseAtual - Configuração da fase selecionada
|
|
||||||
* @returns {Object} Configuração do Phaser
|
|
||||||
*/
|
|
||||||
export const createGame = (elementoPai, configFaseAtual) => {
|
|
||||||
const scene = new PadroesScene();
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: Phaser.AUTO,
|
|
||||||
width: ConstantesJogo.LARGURA_TELA,
|
|
||||||
height: ConstantesJogo.ALTURA_TELA,
|
|
||||||
backgroundColor: ConstantesJogo.COR_FUNDO,
|
|
||||||
parent: elementoPai,
|
|
||||||
scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH },
|
|
||||||
scene,
|
|
||||||
callbacks: {
|
|
||||||
preBoot(game) {
|
|
||||||
game.registry.set("configFase", configFaseAtual);
|
|
||||||
game.registry.set("gameConfig", gameConfig);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Ponte entre o js-interpreter (sandbox) e a cena Phaser do
|
|
||||||
* jogo Padrões. Expõe as funções que o código gerado pelos blocos chama.
|
|
||||||
*
|
|
||||||
* @module games.padroes.hooks.interpreterSetup
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ApiHelpers } from "../../../../interpreters/ApiHelpers.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configura a API disponível ao interpretador para o jogo Padrões.
|
|
||||||
* @param {Object} scene - Instância da cena Phaser (PadroesScene)
|
|
||||||
* @param {Object} [config] - Opções (ex.: `animationSpeed`)
|
|
||||||
* @returns {Function} Função de init com assinatura (interpreter, globalScope)
|
|
||||||
*/
|
|
||||||
export const setupPadroesAPI = (scene, config = {}) => {
|
|
||||||
const animationDelay = config.animationSpeed ?? 150;
|
|
||||||
|
|
||||||
return (interpreter, globalScope) => {
|
|
||||||
// Ações (assíncronas — pausam o interpretador para feedback visual)
|
|
||||||
ApiHelpers.registerFunction(
|
|
||||||
interpreter,
|
|
||||||
globalScope,
|
|
||||||
"definirSaida",
|
|
||||||
ApiHelpers.createActionWrapper(scene, "definirSaida", animationDelay),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
ApiHelpers.registerFunction(
|
|
||||||
interpreter,
|
|
||||||
globalScope,
|
|
||||||
"definirContador",
|
|
||||||
ApiHelpers.createActionWrapper(scene, "definirContador", animationDelay),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Condições (síncronas — retornam valor imediatamente)
|
|
||||||
ApiHelpers.registerFunction(
|
|
||||||
interpreter,
|
|
||||||
globalScope,
|
|
||||||
"obterEntrada",
|
|
||||||
ApiHelpers.createConditionWrapper(scene, "obterEntrada"),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
ApiHelpers.registerFunction(
|
|
||||||
interpreter,
|
|
||||||
globalScope,
|
|
||||||
"obterSaida",
|
|
||||||
ApiHelpers.createConditionWrapper(scene, "obterSaida"),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
ApiHelpers.registerFunction(
|
|
||||||
interpreter,
|
|
||||||
globalScope,
|
|
||||||
"obterContador",
|
|
||||||
ApiHelpers.createConditionWrapper(scene, "obterContador"),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Highlight visual dos blocos durante a execução
|
|
||||||
ApiHelpers.registerFunction(
|
|
||||||
interpreter,
|
|
||||||
globalScope,
|
|
||||||
"highlightBlock",
|
|
||||||
ApiHelpers.createHighlightWrapper(scene),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Constantes visuais do jogo Padrões (reconhecimento de padrões).
|
|
||||||
* @module games.padroes.ui.constants
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const ConstantesJogo = {
|
|
||||||
LARGURA_TELA: 800,
|
|
||||||
ALTURA_TELA: 600,
|
|
||||||
COR_FUNDO: "#1b1e2e",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConstantesLayout = {
|
|
||||||
MARGEM: 40,
|
|
||||||
COR_BORDA_ENTRADA: 0x4a6cf7,
|
|
||||||
COR_BORDA_SAIDA: 0xf5a623,
|
|
||||||
ESPESSURA_BORDA: 4,
|
|
||||||
RAIO: 16,
|
|
||||||
PADDING: 24,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConstantesTexto = {
|
|
||||||
TITULO: { TAMANHO: "28px", COR: "#ffffff" },
|
|
||||||
LABEL: { TAMANHO: "16px", COR: "#9aa4c7" },
|
|
||||||
ENTRADA: { TAMANHO: "40px", COR: "#4ade80" },
|
|
||||||
SAIDA: { TAMANHO: "46px", COR: "#f5a623" },
|
|
||||||
};
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Layout do jogo Padrões: dois painéis (ENTRADA e SAÍDA).
|
|
||||||
* @module games.padroes.ui.layout
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
ConstantesJogo,
|
|
||||||
ConstantesLayout,
|
|
||||||
ConstantesTexto,
|
|
||||||
} from "./constants.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Monta o layout visual da cena: título, painel de ENTRADA e painel de SAÍDA.
|
|
||||||
* @param {Phaser.Scene} scene - Instância da cena Phaser ativa
|
|
||||||
* @returns {{ textoEntrada: Phaser.GameObjects.Text, textoSaida: Phaser.GameObjects.Text }}
|
|
||||||
*/
|
|
||||||
export function inicializarLayout(scene) {
|
|
||||||
const W = ConstantesJogo.LARGURA_TELA;
|
|
||||||
const H = ConstantesJogo.ALTURA_TELA;
|
|
||||||
const M = ConstantesLayout.MARGEM;
|
|
||||||
const raio = ConstantesLayout.RAIO;
|
|
||||||
const pad = ConstantesLayout.PADDING;
|
|
||||||
|
|
||||||
scene.add
|
|
||||||
.text(W / 2, M, "RECONHECIMENTO DE PADRÕES", {
|
|
||||||
fontSize: ConstantesTexto.TITULO.TAMANHO,
|
|
||||||
fontStyle: "bold",
|
|
||||||
fill: ConstantesTexto.TITULO.COR,
|
|
||||||
})
|
|
||||||
.setOrigin(0.5, 0);
|
|
||||||
|
|
||||||
const tituloH = 64;
|
|
||||||
const gap = 24;
|
|
||||||
const panelW = W - M * 2;
|
|
||||||
const panelTopY = M + tituloH;
|
|
||||||
const panelTopH = (H - panelTopY - M - gap) / 2;
|
|
||||||
const panelBotY = panelTopY + panelTopH + gap;
|
|
||||||
const panelBotH = panelTopH;
|
|
||||||
|
|
||||||
const graphics = scene.add.graphics();
|
|
||||||
|
|
||||||
// Painel ENTRADA
|
|
||||||
graphics.lineStyle(
|
|
||||||
ConstantesLayout.ESPESSURA_BORDA,
|
|
||||||
ConstantesLayout.COR_BORDA_ENTRADA,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
graphics.strokeRoundedRect(M, panelTopY, panelW, panelTopH, raio);
|
|
||||||
scene.add.text(M + pad, panelTopY + 10, "ENTRADA (texto a analisar)", {
|
|
||||||
fontSize: ConstantesTexto.LABEL.TAMANHO,
|
|
||||||
fill: ConstantesTexto.LABEL.COR,
|
|
||||||
fontStyle: "bold",
|
|
||||||
});
|
|
||||||
const textoEntrada = scene.add.text(M + pad, panelTopY + 42, "", {
|
|
||||||
fontSize: ConstantesTexto.ENTRADA.TAMANHO,
|
|
||||||
fill: ConstantesTexto.ENTRADA.COR,
|
|
||||||
fontStyle: "bold",
|
|
||||||
align: "left",
|
|
||||||
wordWrap: { width: panelW - pad * 2, useAdvancedWrap: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Painel SAÍDA
|
|
||||||
graphics.lineStyle(
|
|
||||||
ConstantesLayout.ESPESSURA_BORDA,
|
|
||||||
ConstantesLayout.COR_BORDA_SAIDA,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
graphics.strokeRoundedRect(M, panelBotY, panelW, panelBotH, raio);
|
|
||||||
scene.add.text(M + pad, panelBotY + 10, "SAÍDA (seu veredito)", {
|
|
||||||
fontSize: ConstantesTexto.LABEL.TAMANHO,
|
|
||||||
fill: ConstantesTexto.LABEL.COR,
|
|
||||||
fontStyle: "bold",
|
|
||||||
});
|
|
||||||
const textoSaida = scene.add.text(M + pad, panelBotY + 48, "", {
|
|
||||||
fontSize: ConstantesTexto.SAIDA.TAMANHO,
|
|
||||||
fill: ConstantesTexto.SAIDA.COR,
|
|
||||||
fontStyle: "bold",
|
|
||||||
align: "left",
|
|
||||||
wordWrap: { width: panelW - pad * 2, useAdvancedWrap: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
return { textoEntrada, textoSaida };
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Validadores do jogo Padrões.
|
|
||||||
*
|
|
||||||
* Cada fase pode declarar `expectedSaida` (veredito esperado para a
|
|
||||||
* `entradaTeste` da fase). O validador normaliza o veredito do aluno
|
|
||||||
* (maiúsculas, sem acento, sem espaços) antes de comparar.
|
|
||||||
*
|
|
||||||
* @module games.padroes.validation.validators
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BaseGameValidator } from "../../../../shared/BaseGameValidator";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normaliza um veredito para comparação: trim + uppercase + sem acentos.
|
|
||||||
* Ex.: "válido" -> "VALIDO", "Inválido" -> "INVALIDO".
|
|
||||||
* @param {string} texto
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function normalizar(texto) {
|
|
||||||
return String(texto ?? "")
|
|
||||||
.trim()
|
|
||||||
.toUpperCase()
|
|
||||||
.normalize("NFD")
|
|
||||||
.replace(/[\u0300-\u036f]/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PadroesValidator extends BaseGameValidator {
|
|
||||||
/**
|
|
||||||
* Delega para `validateFase{n}` quando disponível.
|
|
||||||
* @param {Array} history
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {Object} gameConfig
|
|
||||||
* @param {Object} sceneRef
|
|
||||||
* @returns {{success:boolean, reason?:string}}
|
|
||||||
*/
|
|
||||||
validatePhase(history, config, gameConfig, sceneRef) {
|
|
||||||
const phaseMethod = this[`validateFase${config.id}`];
|
|
||||||
if (phaseMethod) {
|
|
||||||
return phaseMethod.call(this, history, config, gameConfig, sceneRef);
|
|
||||||
}
|
|
||||||
return this.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fase 1 — Apenas letras de A a Z.
|
|
||||||
* Compara o veredito produzido pelo aluno (sceneRef.saida) com o esperado
|
|
||||||
* (config.expectedSaida) para a entrada de teste da fase.
|
|
||||||
* @returns {{success:boolean, reason?:string}}
|
|
||||||
*/
|
|
||||||
validateFase1(history, config, gameConfig, sceneRef) {
|
|
||||||
const esperado = normalizar(config.expectedSaida);
|
|
||||||
const recebido = normalizar(sceneRef.saida);
|
|
||||||
|
|
||||||
if (!recebido) {
|
|
||||||
return this.failure(
|
|
||||||
gameConfig?.mensagens?.semSaida ??
|
|
||||||
'Você não definiu a SAÍDA. Use "definir SAÍDA como" com o veredito.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recebido !== esperado) {
|
|
||||||
const dica =
|
|
||||||
esperado === "INVALIDO"
|
|
||||||
? gameConfig?.mensagens?.erradoInvalido
|
|
||||||
: gameConfig?.mensagens?.erradoValido;
|
|
||||||
return this.failure(dicaComEntrada(dica, sceneRef.entrada, recebido));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.success();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Anexa a entrada testada e o veredito recebido à mensagem de falha.
|
|
||||||
* @param {string} dica
|
|
||||||
* @param {string} entrada
|
|
||||||
* @param {string} recebido
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function dicaComEntrada(dica, entrada, recebido) {
|
|
||||||
return `${dica}\n\nEntrada: "${entrada}"\nSeu veredito: "${recebido}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Função exportada para validação de soluções do jogo Padrões.
|
|
||||||
* @param {Array} history - Histórico de ações do usuário
|
|
||||||
* @param {Object} config - Configuração da fase
|
|
||||||
* @param {Object} gameConfig - Configuração global do jogo
|
|
||||||
* @param {Object} sceneRef - Referência à cena (opcional)
|
|
||||||
* @returns {{success:boolean, reason?:string}}
|
|
||||||
*/
|
|
||||||
export function validationSolution(history, config, gameConfig, sceneRef) {
|
|
||||||
const validator = new PadroesValidator();
|
|
||||||
return validator.validate(history, config, gameConfig, sceneRef);
|
|
||||||
}
|
|
||||||
@@ -12,17 +12,14 @@ const EXPECTED_IDS = [
|
|||||||
"aspirador",
|
"aspirador",
|
||||||
"automato",
|
"automato",
|
||||||
"cripto",
|
"cripto",
|
||||||
"exemplo",
|
|
||||||
"molemash",
|
"molemash",
|
||||||
"ordenacao",
|
|
||||||
"padroes",
|
|
||||||
"puzzle",
|
"puzzle",
|
||||||
"semaforo",
|
"semaforo",
|
||||||
"turtle",
|
"turtle",
|
||||||
];
|
];
|
||||||
|
|
||||||
describe("GAMES_REGISTRY — estrutura", () => {
|
describe("GAMES_REGISTRY — estrutura", () => {
|
||||||
it("contem todos os 10 jogos esperados", () => {
|
it("contem todos os 7 jogos esperados", () => {
|
||||||
expect(Object.keys(GAMES_REGISTRY)).toEqual(expect.arrayContaining(EXPECTED_IDS));
|
expect(Object.keys(GAMES_REGISTRY)).toEqual(expect.arrayContaining(EXPECTED_IDS));
|
||||||
expect(Object.keys(GAMES_REGISTRY)).toHaveLength(EXPECTED_IDS.length);
|
expect(Object.keys(GAMES_REGISTRY)).toHaveLength(EXPECTED_IDS.length);
|
||||||
});
|
});
|
||||||
@@ -69,7 +66,7 @@ describe("getGameConfig()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getAllGames()", () => {
|
describe("getAllGames()", () => {
|
||||||
it("retorna array com todos os 10 jogos", () => {
|
it("retorna array com todos os 7 jogos", () => {
|
||||||
const games = getAllGames();
|
const games = getAllGames();
|
||||||
expect(games).toHaveLength(EXPECTED_IDS.length);
|
expect(games).toHaveLength(EXPECTED_IDS.length);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,10 +12,8 @@
|
|||||||
import { gameConfig as ASPIRADOR_GAME_CONFIG } from "../atividades/programacao/aspirador/config/config.js";
|
import { gameConfig as ASPIRADOR_GAME_CONFIG } from "../atividades/programacao/aspirador/config/config.js";
|
||||||
import { gameConfig as AUTOMATO_GAME_CONFIG } from "../atividades/programacao/automato/config/config.js";
|
import { gameConfig as AUTOMATO_GAME_CONFIG } from "../atividades/programacao/automato/config/config.js";
|
||||||
import { gameConfig as CRIPTO_GAME_CONFIG } from "../atividades/programacao/cripto/config/config.js";
|
import { gameConfig as CRIPTO_GAME_CONFIG } from "../atividades/programacao/cripto/config/config.js";
|
||||||
import { gameConfig as EXEMPLO_GAME_CONFIG } from "../atividades/programacao/exemplo/config/config.js";
|
|
||||||
import { gameConfig as MOLE_MASH_GAME_CONFIG } from "../atividades/programacao/mole-mash/config/config.js";
|
import { gameConfig as MOLE_MASH_GAME_CONFIG } from "../atividades/programacao/mole-mash/config/config.js";
|
||||||
import { gameConfig as ORDERNACAO_GAME_CONFIG } from "../atividades/programacao/ordenacao/config/config.js";
|
import { gameConfig as ORDERNACAO_GAME_CONFIG } from "../atividades/programacao/ordenacao/config/config.js";
|
||||||
import { gameConfig as PADROES_GAME_CONFIG } from "../atividades/programacao/padroes/config/config.js";
|
|
||||||
import { gameConfig as PUZZLE_GAME_CONFIG } from "../atividades/programacao/puzzle/config/config.js";
|
import { gameConfig as PUZZLE_GAME_CONFIG } from "../atividades/programacao/puzzle/config/config.js";
|
||||||
import { gameConfig as SEMAFORO_GAME_CONFIG } from "../atividades/programacao/semaforo/config/config.js";
|
import { gameConfig as SEMAFORO_GAME_CONFIG } from "../atividades/programacao/semaforo/config/config.js";
|
||||||
import { gameConfig as TURTLE_GAME_CONFIG } from "../atividades/programacao/turtle/config/config.js";
|
import { gameConfig as TURTLE_GAME_CONFIG } from "../atividades/programacao/turtle/config/config.js";
|
||||||
@@ -42,10 +40,8 @@ export const GAMES_REGISTRY = {
|
|||||||
aspirador: ASPIRADOR_GAME_CONFIG,
|
aspirador: ASPIRADOR_GAME_CONFIG,
|
||||||
automato: AUTOMATO_GAME_CONFIG,
|
automato: AUTOMATO_GAME_CONFIG,
|
||||||
cripto: CRIPTO_GAME_CONFIG,
|
cripto: CRIPTO_GAME_CONFIG,
|
||||||
exemplo: EXEMPLO_GAME_CONFIG,
|
|
||||||
molemash: MOLE_MASH_GAME_CONFIG,
|
molemash: MOLE_MASH_GAME_CONFIG,
|
||||||
ordenacao: ORDERNACAO_GAME_CONFIG,
|
ordenacao: ORDERNACAO_GAME_CONFIG,
|
||||||
padroes: PADROES_GAME_CONFIG,
|
|
||||||
puzzle: PUZZLE_GAME_CONFIG,
|
puzzle: PUZZLE_GAME_CONFIG,
|
||||||
semaforo: SEMAFORO_GAME_CONFIG,
|
semaforo: SEMAFORO_GAME_CONFIG,
|
||||||
turtle: TURTLE_GAME_CONFIG,
|
turtle: TURTLE_GAME_CONFIG,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* @module pages.HomePage.HomePage
|
* @module pages.HomePage.HomePage
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import capicodaUrl from "@/vendor/capicoda/capicoda.js?url";
|
||||||
import Navbar from "../../components/Navbar";
|
import Navbar from "../../components/Navbar";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import Hero from "./Hero";
|
import Hero from "./Hero";
|
||||||
@@ -16,6 +18,23 @@ import StudentsMaterials from "./StudentsMaterials";
|
|||||||
import TeachersMaterials from "./TeachersMaterials";
|
import TeachersMaterials from "./TeachersMaterials";
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
|
// Mascote Capicoda: injetado apenas na página inicial. O widget é um script
|
||||||
|
// autocontido que se anexa ao body; na saída da home removemos o script e o
|
||||||
|
// nó raiz (#dcs-root) para não vazar para outras rotas.
|
||||||
|
useEffect(() => {
|
||||||
|
// Estamos dentro do próprio DECODA: o widget esconde os links "Acessar o DECODA".
|
||||||
|
window.CAPICODA_ON_DECODA = true;
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = capicodaUrl;
|
||||||
|
script.defer = true;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
return () => {
|
||||||
|
script.remove();
|
||||||
|
const root = document.getElementById("dcs-root");
|
||||||
|
if (root) root.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
{/* Navegação */}
|
{/* Navegação */}
|
||||||
|
|||||||
135
app/src/vendor/capicoda/README.md
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Capicoda 🚩🦫
|
||||||
|
|
||||||
|
A **capivara comunista** do Núcleo de Tecnologia do MTST. Um widget de chat que
|
||||||
|
aparece na página do Núcleo, conversa com quem visita, identifica se a pessoa é
|
||||||
|
desenvolvedora e a direciona para **contribuir com o [DECODA](https://git.mtst.tec.br/educacao/decoda/)**.
|
||||||
|
|
||||||
|
Projeto do hackathon do Núcleo de Tecnologia do MTST.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ O que é
|
||||||
|
|
||||||
|
- Balão de chat no canto inferior direito de qualquer site.
|
||||||
|
- Conversa por **roteiro fixo** (botões) — sem IA, sem backend, sem build.
|
||||||
|
- Faz a triagem do visitante (**dev / aprendiz / curiosx**) e entrega o caminho
|
||||||
|
certo: repositório, como rodar localmente, como abrir um PR, ou como ajudar
|
||||||
|
sem programar.
|
||||||
|
- Um único arquivo: **`capicoda.js`** (CSS, SVG da capivara e diálogo embutidos).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Rodar o demo
|
||||||
|
|
||||||
|
Não precisa instalar nada. Na pasta do projeto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m http.server 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Abra **http://localhost:8000/demo.html** e clique no balão da capivara.
|
||||||
|
|
||||||
|
> Também dá pra só abrir o `demo.html` direto no navegador (duplo clique).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 Embutir em outro site
|
||||||
|
|
||||||
|
Copie `capicoda.js` para o site e adicione **uma linha** antes do `</body>`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="capicoda.js" defer></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pronto — o balão se injeta sozinho. Não conflita com o CSS da página (tudo é
|
||||||
|
prefixado com `dcs-` e isolado num `#dcs-root`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Editar a conversa
|
||||||
|
|
||||||
|
Toda a conversa vive no objeto **`TREE`** no topo do `capicoda.js`. Cada nó:
|
||||||
|
|
||||||
|
```js
|
||||||
|
nome_do_no: {
|
||||||
|
msg: "Texto do balão", // ou um array de balões em sequência
|
||||||
|
options: [
|
||||||
|
{ label: "Botão A", next: "outro_no" }, // navega para outro nó
|
||||||
|
{ label: "Abrir repo", url: URLS.repo }, // abre um link em nova aba
|
||||||
|
{ label: "Destaque", next: "x", primary: true } // botão em vermelho cheio
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Os links reais ficam no objeto `URLS` (repo, página do DECODA, página do Núcleo).
|
||||||
|
|
||||||
|
### Mapa do roteiro
|
||||||
|
|
||||||
|
```
|
||||||
|
start ──┬─ Bora! ───────────────► qualifica
|
||||||
|
└─ O que é o DECODA? ───► oque_decoda ──► qualifica
|
||||||
|
|
||||||
|
qualifica ─┬─ Sou dev ──────► area ─┬─ Front/JS ──► cta_front
|
||||||
|
│ ├─ Back/DevOps ► cta_infra
|
||||||
|
│ └─ Full/outra ─► cta_geral
|
||||||
|
├─ Tô aprendendo ► aprendiz
|
||||||
|
└─ Só curiosx ───► curioso
|
||||||
|
|
||||||
|
cta_* ─┬─ Ver repositório (link)
|
||||||
|
├─ Como rodar localmente ► setup
|
||||||
|
├─ Como mandar um PR ─────► fluxo_pr
|
||||||
|
└─ Voltar ao início ──────► start
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🦫 Poses da capivara (imagens)
|
||||||
|
|
||||||
|
O mascote é uma imagem grande (≈190px) que **substitui o botão** e **troca de pose**
|
||||||
|
conforme o estado da conversa. As 4 poses vivem embutidas como data URI (WebP) no
|
||||||
|
`capicoda.js`, então o widget continua autocontido (1 tag de script).
|
||||||
|
|
||||||
|
| Pose | Quando | Nós do diálogo |
|
||||||
|
|------|--------|----------------|
|
||||||
|
| `start` | acenando, no início | `start` |
|
||||||
|
| `prof` | explicando | `oque_decoda`, `setup`, `fluxo_pr` |
|
||||||
|
| `anot` | pedindo respostas | `qualifica`, `area` |
|
||||||
|
| `end` | encerrando | `cta_*`, `aprendiz`, `curioso` |
|
||||||
|
|
||||||
|
O mapa nó→pose é o objeto **`NODE_IMG`** no topo do `capicoda.js` (fácil de ajustar).
|
||||||
|
Todas as imagens têm o mesmo canvas (768×768), então a capivara não muda de tamanho
|
||||||
|
nem se desloca ao trocar de pose.
|
||||||
|
|
||||||
|
### Regenerar as imagens
|
||||||
|
|
||||||
|
As fontes são `app/src/assets/capicoda_{start,prof,anot,end}.jpeg`. Para remover o
|
||||||
|
fundo e reembutir no widget:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/build_images.py # requer Pillow
|
||||||
|
```
|
||||||
|
|
||||||
|
O script remove o fundo (flood-fill a partir das bordas), salva os PNGs transparentes
|
||||||
|
em `app/src/assets/capicoda_*.png` e reescreve o bloco `IMGS` entre os marcadores
|
||||||
|
`/* __IMAGES_START__ */ … /* __IMAGES_END__ */`. **Não edite o `IMGS` à mão.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Identidade
|
||||||
|
|
||||||
|
Capivara marrom de boina verde com estrela vermelha. Paleta MTST (vermelho `#c1121f`).
|
||||||
|
Tom acolhedor e militante-bem-humorado. Fallback: SVG inline desenhado à mão, usado se
|
||||||
|
as imagens não carregarem.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔮 Próximos passos (fora do escopo do hackathon)
|
||||||
|
|
||||||
|
- Modo IA opcional (Claude API) para conversa livre.
|
||||||
|
- Captura de contato de quem quer contribuir (onboarding → mutirão).
|
||||||
|
- Puxar "good first issues" dinamicamente da aba de Issues do repositório.
|
||||||
|
- Integração na página real do Núcleo e/ou sobre o app DECODA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Desenvolvido com ✊ para o DECODA · Núcleo de Tecnologia do MTST
|
||||||
399
app/src/vendor/capicoda/capicoda.js
vendored
Normal file
69
app/src/vendor/capicoda/demo.html
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Capicoda — demo · Núcleo de Tecnologia do MTST</title>
|
||||||
|
<style>
|
||||||
|
:root { --vermelho: #c1121f; }
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||||
|
color: #1d1d1f; line-height: 1.6; background: #faf6f1;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
background: var(--vermelho); color: #fff; padding: 18px 24px;
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
}
|
||||||
|
header .star { font-size: 26px; }
|
||||||
|
header h1 { font-size: 19px; font-weight: 700; }
|
||||||
|
.hero {
|
||||||
|
max-width: 760px; margin: 0 auto; padding: 64px 24px 40px; text-align: center;
|
||||||
|
}
|
||||||
|
.hero h2 { font-size: 38px; line-height: 1.2; margin-bottom: 16px; }
|
||||||
|
.hero h2 span { color: var(--vermelho); }
|
||||||
|
.hero p { font-size: 18px; color: #4a4a4a; max-width: 560px; margin: 0 auto; }
|
||||||
|
.grid {
|
||||||
|
max-width: 760px; margin: 0 auto; padding: 24px; display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px;
|
||||||
|
}
|
||||||
|
.card { background: #fff; border: 1px solid #eee; border-radius: 14px; padding: 20px; }
|
||||||
|
.card h3 { font-size: 16px; margin-bottom: 6px; }
|
||||||
|
.card p { font-size: 14px; color: #555; }
|
||||||
|
.hint {
|
||||||
|
max-width: 760px; margin: 12px auto 80px; padding: 16px 20px; border-radius: 12px;
|
||||||
|
background: #fff3cd; border: 1px solid #ffe69c; color: #664d03; font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
footer { text-align: center; padding: 24px; color: #999; font-size: 13px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<span class="star">🚩</span>
|
||||||
|
<h1>Núcleo de Tecnologia · MTST</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<h2>Aprenda a programar com o <span>DECODA</span></h2>
|
||||||
|
<p>Plataforma educacional do MTST: lógica de programação através de jogos
|
||||||
|
e blocos visuais. Tecnologia como ferramenta de transformação social.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid">
|
||||||
|
<div class="card"><h3>🎮 Jogos</h3><p>Atividades interativas para aprender programação na prática.</p></div>
|
||||||
|
<div class="card"><h3>🧩 Blocos visuais</h3><p>Programação arrastar-e-soltar com Blockly, sem decorar sintaxe.</p></div>
|
||||||
|
<div class="card"><h3>🆓 Livre e gratuito</h3><p>100% gratuito, sem cadastro, código aberto.</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="hint">
|
||||||
|
👉 Página de demonstração. O balão da capivara <b>Capicoda</b> aparece no
|
||||||
|
canto inferior direito — clique pra conversar.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>Demo do mascote Capicoda · hackathon do Núcleo de Tecnologia do MTST</footer>
|
||||||
|
|
||||||
|
<!-- É só isto que precisa ir na página real do Núcleo: -->
|
||||||
|
<script src="capicoda.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
152
app/src/vendor/capicoda/scripts/build_images.py
vendored
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Capicoda — processamento das imagens do mascote.
|
||||||
|
|
||||||
|
Para cada pose (capicoda_<estado>.jpeg em app/src/assets):
|
||||||
|
1. Remove o fundo branco (flood-fill a partir das bordas sobre a máscara de
|
||||||
|
pixels claros — preserva os brancos INTERNOS, cercados pelo contorno escuro:
|
||||||
|
olhos, papel do caderno, brilho dos óculos).
|
||||||
|
2. Salva um PNG transparente em alta (canvas quadrado preservado — todas as
|
||||||
|
poses ficam do MESMO tamanho, então a capivara não "pula" ao trocar).
|
||||||
|
3. Gera uma versão reduzida (EMB x EMB), codifica em base64 (WebP se suportado,
|
||||||
|
senão PNG) e injeta o mapa `IMGS` no capicoda.js entre os marcadores
|
||||||
|
/* __IMAGES_START__ */ ... /* __IMAGES_END__ */.
|
||||||
|
|
||||||
|
Uso: python3 scripts/build_images.py
|
||||||
|
Requisitos: Pillow.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from PIL import Image, features
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
CAPICODA_DIR = os.path.normpath(os.path.join(HERE, ".."))
|
||||||
|
ASSETS = os.path.normpath(os.path.join(HERE, "..", "..", "..", "assets"))
|
||||||
|
CAPICODA_JS = os.path.join(CAPICODA_DIR, "capicoda.js")
|
||||||
|
|
||||||
|
STATES = ["start", "prof", "anot", "end"]
|
||||||
|
WORK = 768 # resolução de processamento/saída do PNG (quadrado, uniforme)
|
||||||
|
EMB = 384 # resolução embutida no JS
|
||||||
|
LIGHT = 190 # canal mínimo p/ considerar um pixel "claro" (fundo/sombra)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_bg(img):
|
||||||
|
"""Torna transparente o fundo claro conectado às bordas."""
|
||||||
|
img = img.convert("RGBA").resize((WORK, WORK), Image.LANCZOS)
|
||||||
|
w, h = img.size
|
||||||
|
px = img.load()
|
||||||
|
|
||||||
|
# 1) máscara de pixels "claros" (fundo branco + sombra clara)
|
||||||
|
light = bytearray(w * h)
|
||||||
|
for y in range(h):
|
||||||
|
row = y * w
|
||||||
|
for x in range(w):
|
||||||
|
r, g, b, _ = px[x, y]
|
||||||
|
if r >= LIGHT and g >= LIGHT and b >= LIGHT:
|
||||||
|
light[row + x] = 1
|
||||||
|
|
||||||
|
# 2) BFS a partir dos pixels claros da borda -> só o fundo EXTERNO
|
||||||
|
visited = bytearray(w * h)
|
||||||
|
dq = deque()
|
||||||
|
|
||||||
|
def seed(x, y):
|
||||||
|
i = y * w + x
|
||||||
|
if light[i] and not visited[i]:
|
||||||
|
visited[i] = 1
|
||||||
|
dq.append((x, y))
|
||||||
|
|
||||||
|
for x in range(w):
|
||||||
|
seed(x, 0)
|
||||||
|
seed(x, h - 1)
|
||||||
|
for y in range(h):
|
||||||
|
seed(0, y)
|
||||||
|
seed(w - 1, y)
|
||||||
|
|
||||||
|
while dq:
|
||||||
|
x, y = dq.popleft()
|
||||||
|
if x > 0:
|
||||||
|
seed(x - 1, y)
|
||||||
|
if x < w - 1:
|
||||||
|
seed(x + 1, y)
|
||||||
|
if y > 0:
|
||||||
|
seed(x, y - 1)
|
||||||
|
if y < h - 1:
|
||||||
|
seed(x, y + 1)
|
||||||
|
|
||||||
|
# 3) zera o alpha do fundo externo
|
||||||
|
cleared = 0
|
||||||
|
for y in range(h):
|
||||||
|
row = y * w
|
||||||
|
for x in range(w):
|
||||||
|
if visited[row + x]:
|
||||||
|
r, g, b, _ = px[x, y]
|
||||||
|
px[x, y] = (r, g, b, 0)
|
||||||
|
cleared += 1
|
||||||
|
return img, cleared
|
||||||
|
|
||||||
|
|
||||||
|
def data_uri(img):
|
||||||
|
"""Reduz para EMB e devolve (data_uri, mime, n_bytes)."""
|
||||||
|
small = img.resize((EMB, EMB), Image.LANCZOS)
|
||||||
|
buf = io.BytesIO()
|
||||||
|
if features.check("webp"):
|
||||||
|
small.save(buf, format="WEBP", quality=90, method=6)
|
||||||
|
mime = "image/webp"
|
||||||
|
else:
|
||||||
|
small.save(buf, format="PNG", optimize=True)
|
||||||
|
mime = "image/png"
|
||||||
|
raw = buf.getvalue()
|
||||||
|
b64 = base64.b64encode(raw).decode("ascii")
|
||||||
|
return "data:%s;base64,%s" % (mime, b64), mime, len(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_js(uris):
|
||||||
|
with open(CAPICODA_JS, "r", encoding="utf-8") as f:
|
||||||
|
src = f.read()
|
||||||
|
lines = [' var IMGS = {']
|
||||||
|
for s in STATES:
|
||||||
|
lines.append(' %s: "%s",' % (s, uris[s]))
|
||||||
|
lines.append(' };')
|
||||||
|
block = "/* __IMAGES_START__ */\n" + "\n".join(lines) + "\n /* __IMAGES_END__ */"
|
||||||
|
new, n = re.subn(
|
||||||
|
r"/\* __IMAGES_START__ \*/.*?/\* __IMAGES_END__ \*/",
|
||||||
|
lambda _: block,
|
||||||
|
src,
|
||||||
|
flags=re.S,
|
||||||
|
)
|
||||||
|
if n != 1:
|
||||||
|
raise SystemExit("ERRO: marcadores __IMAGES_START/END__ não encontrados (n=%d)" % n)
|
||||||
|
with open(CAPICODA_JS, "w", encoding="utf-8") as f:
|
||||||
|
f.write(new)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
uris = {}
|
||||||
|
total = 0
|
||||||
|
for s in STATES:
|
||||||
|
src = os.path.join(ASSETS, "capicoda_%s.jpeg" % s)
|
||||||
|
if not os.path.exists(src):
|
||||||
|
raise SystemExit("ERRO: não encontrei %s" % src)
|
||||||
|
img = Image.open(src)
|
||||||
|
out, cleared = remove_bg(img)
|
||||||
|
|
||||||
|
png_path = os.path.join(ASSETS, "capicoda_%s.png" % s)
|
||||||
|
out.save(png_path, format="PNG", optimize=True)
|
||||||
|
|
||||||
|
uri, mime, nbytes = data_uri(out)
|
||||||
|
uris[s] = uri
|
||||||
|
total += nbytes
|
||||||
|
print(" %-6s -> %s (fundo: %d px) | embutido %s %d KB"
|
||||||
|
% (s, os.path.basename(png_path), cleared, mime, nbytes // 1024))
|
||||||
|
|
||||||
|
patch_js(uris)
|
||||||
|
print("PNGs transparentes (%dx%d) em %s" % (WORK, WORK, ASSETS))
|
||||||
|
print("IMGS embutido no capicoda.js — total embutido: %d KB" % (total // 1024))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||