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,