Compare commits
1 Commits
feature/at
...
feature/so
| Author | SHA1 | Date | |
|---|---|---|---|
| f7108d50bb |
@@ -33,7 +33,6 @@ const TurtleGame = lazy(() => import("./atividades/programacao/turtle/TurtleGame
|
||||
const CriptoGame = lazy(() => import("./atividades/programacao/cripto/CriptoGame"));
|
||||
const ExemploGame = lazy(() => import("./atividades/programacao/exemplo/ExemploGame"));
|
||||
const ExemploGame2 = lazy(() => import("./atividades/programacao/exemplo2/ExemploGame2"));
|
||||
const PadroesGame = lazy(() => import("./atividades/programacao/padroes/PadroesGame"));
|
||||
|
||||
const LoadingFallback = () => (
|
||||
<div
|
||||
@@ -104,7 +103,6 @@ function AppRoutes() {
|
||||
<Route path="/atividades/programacao/turtle" element={<TurtleGame />} />
|
||||
<Route path="/atividades/programacao/exemplo" element={<ExemploGame />} />
|
||||
<Route path="/atividades/programacao/exemplo2" element={<ExemploGame2 />} />
|
||||
<Route path="/atividades/programacao/padroes" element={<PadroesGame />} />
|
||||
</Routes>
|
||||
|
||||
{/* Modal overlay routes — rendered on top of the background page */}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { getCategoryIcon } from "./editors/toolboxIcons"
|
||||
import { useVolume } from "../../hooks/useVolume";
|
||||
|
||||
export default function GameFooter({
|
||||
gameConfig,
|
||||
@@ -17,6 +19,8 @@ export default function GameFooter({
|
||||
const totalPhases = gameConfig.fases.length;
|
||||
const displayPhase = currentPhase ?? currentPhase;
|
||||
|
||||
const { isMuted, toggleVolume } = useVolume();
|
||||
|
||||
const ajuda = () => {
|
||||
if (onHelpClick) {
|
||||
onHelpClick();
|
||||
@@ -25,11 +29,12 @@ export default function GameFooter({
|
||||
alert("Recurso de ajuda será implementado em breve!");
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900">
|
||||
<div className="flex items-center justify-between px-6 py-3">
|
||||
{/* Lado esquerdo - Botão de Ajuda (desativado temporariamente, tour será reimplementado) */}
|
||||
<div className="flex items-center invisible">
|
||||
{/*<div className="flex items-center invisible">
|
||||
<button
|
||||
onClick={ajuda}
|
||||
data-tour="help-button"
|
||||
@@ -38,7 +43,19 @@ export default function GameFooter({
|
||||
>
|
||||
Ajuda
|
||||
</button>
|
||||
</div>*/}
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={toggleVolume}
|
||||
data-tour="volume-button"
|
||||
title={isMuted ? "Ligar som" : "Desligar som"}
|
||||
aria-label={isMuted ? "Ligar som" : "Desligar som"}
|
||||
className="bg-green-100 text-black font-medium py-2 px-6 lg:py-3 lg:px-9 text-sm lg:text-base rounded-full transition-all duration-200 hover:bg-green-200 hover:scale-105 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-green-300"
|
||||
>
|
||||
<i className={isMuted ? getCategoryIcon("Volume Desligado") : getCategoryIcon("Volume Ligado")} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Centro - Indicador de Fase Atual/Total */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="phase-indicator">
|
||||
|
||||
@@ -42,6 +42,10 @@ export function getCategoryIcon(name) {
|
||||
return "fa fa-database";
|
||||
case "Caneta":
|
||||
return "fa fa-pencil-alt";
|
||||
case "Volume Desligado":
|
||||
return "fa-solid fa-volume-xmark";
|
||||
case "Volume Ligado":
|
||||
return "fa-solid fa-volume-high";
|
||||
default:
|
||||
return "fa fa-cube";
|
||||
}
|
||||
|
||||
@@ -12,17 +12,14 @@ const EXPECTED_IDS = [
|
||||
"aspirador",
|
||||
"automato",
|
||||
"cripto",
|
||||
"exemplo",
|
||||
"molemash",
|
||||
"ordenacao",
|
||||
"padroes",
|
||||
"puzzle",
|
||||
"semaforo",
|
||||
"turtle",
|
||||
];
|
||||
|
||||
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)).toHaveLength(EXPECTED_IDS.length);
|
||||
});
|
||||
@@ -69,7 +66,7 @@ describe("getGameConfig()", () => {
|
||||
});
|
||||
|
||||
describe("getAllGames()", () => {
|
||||
it("retorna array com todos os 10 jogos", () => {
|
||||
it("retorna array com todos os 7 jogos", () => {
|
||||
const games = getAllGames();
|
||||
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 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 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 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 SEMAFORO_GAME_CONFIG } from "../atividades/programacao/semaforo/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,
|
||||
automato: AUTOMATO_GAME_CONFIG,
|
||||
cripto: CRIPTO_GAME_CONFIG,
|
||||
exemplo: EXEMPLO_GAME_CONFIG,
|
||||
molemash: MOLE_MASH_GAME_CONFIG,
|
||||
ordenacao: ORDERNACAO_GAME_CONFIG,
|
||||
padroes: PADROES_GAME_CONFIG,
|
||||
puzzle: PUZZLE_GAME_CONFIG,
|
||||
semaforo: SEMAFORO_GAME_CONFIG,
|
||||
turtle: TURTLE_GAME_CONFIG,
|
||||
|
||||
23
app/src/hooks/useVolume.js
Normal file
23
app/src/hooks/useVolume.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// hooks/useVolume.js
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { gameEventBus } from "../utils/gameEvents";
|
||||
|
||||
const STORAGE_KEY = "decoda-volume-muted";
|
||||
|
||||
export function useVolume() {
|
||||
const [isMuted, setIsMuted] = useState(() => localStorage.getItem(STORAGE_KEY) === "true");
|
||||
|
||||
useEffect(() => {
|
||||
gameEventBus.setVolumeMuted(isMuted);
|
||||
}, [isMuted]);
|
||||
|
||||
const toggleVolume = useCallback(() => {
|
||||
setIsMuted((prev) => {
|
||||
const next = !prev;
|
||||
localStorage.setItem(STORAGE_KEY, String(next));
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { isMuted, toggleVolume };
|
||||
}
|
||||
@@ -49,14 +49,26 @@ export function setupGameController(
|
||||
}
|
||||
};
|
||||
|
||||
const setVolumeMutedHandler = (event) => {
|
||||
if (scene.game?.sound) {
|
||||
scene.game.sound.mute = event.detail.muted;
|
||||
}
|
||||
};
|
||||
|
||||
gameEventBus.addEventListener("executeCode", executeCodeHandler);
|
||||
gameEventBus.addEventListener("resetGame", resetGameHandler);
|
||||
gameEventBus.addEventListener("stopExecution", stopExecutionHandler);
|
||||
gameEventBus.addEventListener("setVolumeMuted", setVolumeMutedHandler);
|
||||
|
||||
if (scene.game?.sound) {
|
||||
scene.game.sound.mute = localStorage.getItem("decoda-volume-muted") === "true";
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
gameEventBus.removeEventListener("executeCode", executeCodeHandler);
|
||||
gameEventBus.removeEventListener("resetGame", resetGameHandler);
|
||||
gameEventBus.removeEventListener("stopExecution", stopExecutionHandler);
|
||||
gameEventBus.removeEventListener("setVolumeMuted", setVolumeMutedHandler);
|
||||
};
|
||||
|
||||
scene._cleanupController = cleanup;
|
||||
|
||||
@@ -86,6 +86,15 @@ describe("gameEventBus", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setVolumeMuted", () => {
|
||||
it("dispatches setVolumeMuted with muted flag in detail", async () => {
|
||||
const p = listenOnce("setVolumeMuted");
|
||||
gameEventBus.setVolumeMuted(true);
|
||||
const e = await p;
|
||||
expect(e.detail).toEqual({ muted: true });
|
||||
});
|
||||
});
|
||||
|
||||
it("is an EventTarget", () => {
|
||||
expect(gameEventBus).toBeInstanceOf(EventTarget);
|
||||
});
|
||||
|
||||
@@ -103,6 +103,18 @@ class GameEventBus extends EventTarget {
|
||||
stopExecution() {
|
||||
this.dispatchEvent(new CustomEvent("stopExecution"));
|
||||
}
|
||||
|
||||
/**
|
||||
* React define mute global do áudio no Phaser.
|
||||
*
|
||||
* @function setVolumeMuted
|
||||
* @param {boolean} muted - `true` para silenciar, `false` para restaurar
|
||||
* @returns {void}
|
||||
* @fires setVolumeMuted - CustomEvent com detail: { muted }
|
||||
*/
|
||||
setVolumeMuted(muted) {
|
||||
this.dispatchEvent(new CustomEvent("setVolumeMuted", { detail: { muted } }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user