From c66bb6a9a8dec0c67ebe630eb85fb6f8aca524ef Mon Sep 17 00:00:00 2001 From: willow Date: Sat, 27 Jun 2026 18:30:53 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20adiciona=20atividade=20Padr=C3=B5es=20d?= =?UTF-8?q?e=20reconhecimento=20de=20padr=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nova atividade em app/src/atividades/programacao/padroes/, seguindo o padrão arquitetural das demais (BaseGameScene + Blockly + interpretador): - Cena Phaser (PadroesScene), blocos customizados, API do interpretador, validador e UI com painéis ENTRADA/SAÍDA. - Fase 1 (teste): loop "enquanto" que verifica se a ENTRADA contém apenas letras de A-Z (caso "ABC123" -> "INVÁLIDO"). - Registro em gameRegistry.js, rota em App.jsx e ajuste do teste do registry (EXPECTED_IDS -> 10 jogos). --- app/src/App.jsx | 2 + .../programacao/padroes/PadroesGame.jsx | 61 +++++ .../programacao/padroes/blocks/blocks.js | 249 ++++++++++++++++++ .../programacao/padroes/config/config.js | 92 +++++++ .../atividades/programacao/padroes/game.js | 182 +++++++++++++ .../padroes/hooks/interpreterSetup.js | 68 +++++ .../programacao/padroes/ui/constants.js | 26 ++ .../programacao/padroes/ui/layout.js | 83 ++++++ .../padroes/validation/validators.js | 95 +++++++ app/src/config/__tests__/gameRegistry.test.js | 7 +- app/src/config/gameRegistry.js | 4 + 11 files changed, 867 insertions(+), 2 deletions(-) create mode 100644 app/src/atividades/programacao/padroes/PadroesGame.jsx create mode 100644 app/src/atividades/programacao/padroes/blocks/blocks.js create mode 100644 app/src/atividades/programacao/padroes/config/config.js create mode 100644 app/src/atividades/programacao/padroes/game.js create mode 100644 app/src/atividades/programacao/padroes/hooks/interpreterSetup.js create mode 100644 app/src/atividades/programacao/padroes/ui/constants.js create mode 100644 app/src/atividades/programacao/padroes/ui/layout.js create mode 100644 app/src/atividades/programacao/padroes/validation/validators.js diff --git a/app/src/App.jsx b/app/src/App.jsx index c49e35f..2857939 100644 --- a/app/src/App.jsx +++ b/app/src/App.jsx @@ -33,6 +33,7 @@ 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 = () => (
} /> } /> } /> + } /> {/* Modal overlay routes — rendered on top of the background page */} diff --git a/app/src/atividades/programacao/padroes/PadroesGame.jsx b/app/src/atividades/programacao/padroes/PadroesGame.jsx new file mode 100644 index 0000000..699f44d --- /dev/null +++ b/app/src/atividades/programacao/padroes/PadroesGame.jsx @@ -0,0 +1,61 @@ +/** + * @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 ( + + + + + + ); +} + +/** + * 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 ( + + + + ); +} diff --git a/app/src/atividades/programacao/padroes/blocks/blocks.js b/app/src/atividades/programacao/padroes/blocks/blocks.js new file mode 100644 index 0000000..7d14800 --- /dev/null +++ b/app/src/atividades/programacao/padroes/blocks/blocks.js @@ -0,0 +1,249 @@ +/** + * @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} [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]; + }; +}; diff --git a/app/src/atividades/programacao/padroes/config/config.js b/app/src/atividades/programacao/padroes/config/config.js new file mode 100644 index 0000000..38dc5ef --- /dev/null +++ b/app/src/atividades/programacao/padroes/config/config.js @@ -0,0 +1,92 @@ +/** + * @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.", + }, +}; diff --git a/app/src/atividades/programacao/padroes/game.js b/app/src/atividades/programacao/padroes/game.js new file mode 100644 index 0000000..7290a83 --- /dev/null +++ b/app/src/atividades/programacao/padroes/game.js @@ -0,0 +1,182 @@ +/** + * @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} + */ + 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} + */ + 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); + }, + }, + }; +}; diff --git a/app/src/atividades/programacao/padroes/hooks/interpreterSetup.js b/app/src/atividades/programacao/padroes/hooks/interpreterSetup.js new file mode 100644 index 0000000..d2ec09b --- /dev/null +++ b/app/src/atividades/programacao/padroes/hooks/interpreterSetup.js @@ -0,0 +1,68 @@ +/** + * @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, + ); + }; +}; diff --git a/app/src/atividades/programacao/padroes/ui/constants.js b/app/src/atividades/programacao/padroes/ui/constants.js new file mode 100644 index 0000000..19f2bc1 --- /dev/null +++ b/app/src/atividades/programacao/padroes/ui/constants.js @@ -0,0 +1,26 @@ +/** + * @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" }, +}; diff --git a/app/src/atividades/programacao/padroes/ui/layout.js b/app/src/atividades/programacao/padroes/ui/layout.js new file mode 100644 index 0000000..03fca0f --- /dev/null +++ b/app/src/atividades/programacao/padroes/ui/layout.js @@ -0,0 +1,83 @@ +/** + * @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 }; +} diff --git a/app/src/atividades/programacao/padroes/validation/validators.js b/app/src/atividades/programacao/padroes/validation/validators.js new file mode 100644 index 0000000..f50a2c5 --- /dev/null +++ b/app/src/atividades/programacao/padroes/validation/validators.js @@ -0,0 +1,95 @@ +/** + * @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); +} diff --git a/app/src/config/__tests__/gameRegistry.test.js b/app/src/config/__tests__/gameRegistry.test.js index 937abc7..b221a58 100644 --- a/app/src/config/__tests__/gameRegistry.test.js +++ b/app/src/config/__tests__/gameRegistry.test.js @@ -12,14 +12,17 @@ const EXPECTED_IDS = [ "aspirador", "automato", "cripto", + "exemplo", "molemash", + "ordenacao", + "padroes", "puzzle", "semaforo", "turtle", ]; describe("GAMES_REGISTRY — estrutura", () => { - it("contem todos os 7 jogos esperados", () => { + it("contem todos os 10 jogos esperados", () => { expect(Object.keys(GAMES_REGISTRY)).toEqual(expect.arrayContaining(EXPECTED_IDS)); expect(Object.keys(GAMES_REGISTRY)).toHaveLength(EXPECTED_IDS.length); }); @@ -66,7 +69,7 @@ describe("getGameConfig()", () => { }); describe("getAllGames()", () => { - it("retorna array com todos os 7 jogos", () => { + it("retorna array com todos os 10 jogos", () => { const games = getAllGames(); expect(games).toHaveLength(EXPECTED_IDS.length); }); diff --git a/app/src/config/gameRegistry.js b/app/src/config/gameRegistry.js index 53734e6..0822fad 100644 --- a/app/src/config/gameRegistry.js +++ b/app/src/config/gameRegistry.js @@ -12,8 +12,10 @@ 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"; @@ -40,8 +42,10 @@ 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,