diff --git a/app/src/App.jsx b/app/src/App.jsx
index 1675cac..c49e35f 100644
--- a/app/src/App.jsx
+++ b/app/src/App.jsx
@@ -31,6 +31,8 @@ const OrdenacaoGame = lazy(() => import("./atividades/programacao/ordenacao/Orde
const PuzzleGame = lazy(() => import("./atividades/programacao/puzzle/PuzzleGame"));
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 LoadingFallback = () => (
} />
} />
} />
+ } />
+ } />
{/* Modal overlay routes — rendered on top of the background page */}
diff --git a/app/src/atividades/programacao/exemplo/ExemploGame.jsx b/app/src/atividades/programacao/exemplo/ExemploGame.jsx
new file mode 100644
index 0000000..4a45e0e
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo/ExemploGame.jsx
@@ -0,0 +1,47 @@
+import React, { useEffect, useMemo } from "react";
+import PropTypes from "prop-types";
+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";
+
+function ExemploContent() {
+ const { setFailureMessage, isDebugMode } = useGameState();
+
+ // Registra os blocos customizados no Blockly ao montar o componente
+ useEffect(() => {
+ registerBlocks();
+ }, []);
+
+ // Memoriza o gerador para evitar recriações desnecessárias do toolbox
+ const toolboxGenerator = useMemo(() => {
+ return (allowedBlocks) => generateDynamicToolbox(allowedBlocks);
+ }, []);
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default function ExemploGame() {
+ return (
+
+
+
+ );
+}
+
+ExemploContent.propTypes = {};
+ExemploGame.propTypes = {};
diff --git a/app/src/atividades/programacao/exemplo/blocks/blocks.js b/app/src/atividades/programacao/exemplo/blocks/blocks.js
new file mode 100644
index 0000000..10e6187
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo/blocks/blocks.js
@@ -0,0 +1,58 @@
+"use strict";
+
+import "blockly/blocks";
+import { CORES_CUSTOMIZADAS } from "@/blockly/blocklyColors";
+import { configurarGerador, gerarStatement } from "@/blockly/generator";
+import { gerarToolboxDeEstrutura } from "@/blockly/toolbox";
+import { criarBlocoStatementSimples } from "@/blockly/blockFactory";
+
+// controls_repeat_ext e math_number são blocos nativos do Blockly (não precisam de defineBlocks)
+const ESTRUTURA_TOOLBOX = [
+ {
+ nome: "Repetição",
+ cssContainer: "cat_repeticao",
+ blocos: ["controls_repeat_ext"],
+ },
+ {
+ nome: "Movimento",
+ cor: CORES_CUSTOMIZADAS.MOVIMENTO,
+ cssContainer: "cat_movimento",
+ blocos: ["exemplo_mover_direita", "exemplo_mover_baixo"],
+ },
+ {
+ nome: "Matemática",
+ cssContainer: "cat_matematica",
+ blocos: ["math_number"],
+ },
+];
+
+export const registerBlocks = () => {
+ defineBlocks();
+ defineGenerators();
+};
+
+export const generateDynamicToolbox = (allowedBlocks = []) => {
+ return gerarToolboxDeEstrutura(ESTRUTURA_TOOLBOX, allowedBlocks);
+};
+
+const defineBlocks = () => {
+ criarBlocoStatementSimples(
+ "exemplo_mover_direita",
+ "mover para DIREITA",
+ CORES_CUSTOMIZADAS.MOVIMENTO
+ );
+
+ criarBlocoStatementSimples(
+ "exemplo_mover_baixo",
+ "mover para BAIXO",
+ CORES_CUSTOMIZADAS.MOVIMENTO
+ );
+};
+
+const defineGenerators = () => {
+ // Ativa o prefix de highlight no gerador (necessário para feedback visual dos blocos)
+ configurarGerador();
+
+ gerarStatement("exemplo_mover_direita", "moverDireita");
+ gerarStatement("exemplo_mover_baixo", "moverBaixo");
+};
diff --git a/app/src/atividades/programacao/exemplo/config/config.js b/app/src/atividades/programacao/exemplo/config/config.js
new file mode 100644
index 0000000..f913a9d
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo/config/config.js
@@ -0,0 +1,51 @@
+export const gameConfig = {
+ gameId: "exemplo",
+ gameName: "Exemplo",
+ type: "blocks",
+ icon: "🎯",
+ thumbnail: "/images/atividades/programacao/exemplo-thumbnail.png",
+ descricao:
+ "Atividade de demonstração arquitetural. Guie o personagem até o alvo usando blocos de movimento e repetição.",
+ dificuldade: "Iniciante",
+ categoria: "Lógica",
+ tempoEstimado: "5 min",
+ conceitos: ["Sequenciamento", "Repetição"],
+ route: "/atividades/programacao/exemplo",
+ component: "ExemploGame",
+ objectives: [
+ "Demonstrar o fluxo completo de instanciação, execução e validação da plataforma Decoda",
+ ],
+ metadata: {
+ lastUpdated: "2026-06-24",
+ version: "1.1.0",
+ },
+
+ fases: [
+ {
+ id: 1,
+ nome: "Fase 1: Chegue ao Alvo",
+ descricao:
+ "Use 'repetir N vezes' com 'mover para DIREITA' e 'mover para BAIXO' para guiar o personagem até o alvo verde. Dica: a solução ótima usa apenas 6 blocos!",
+ timeout: 15,
+ // Limite pedagógico: força o aluno a usar laços em vez de repetir blocos manualmente
+ maxBlocks: 6,
+ allowedBlocks: [
+ "controls_repeat_ext",
+ "math_number",
+ "exemplo_mover_direita",
+ "exemplo_mover_baixo",
+ ],
+ // Grade 5×5; jogador em (0,0), alvo em (4,4)
+ cols: 5,
+ rows: 5,
+ jogadorInicio: { col: 0, row: 0 },
+ alvo: { col: 4, row: 4 },
+ },
+ ],
+
+ mensagens: {
+ semMovimento: "O personagem não se mexeu! Use os blocos de movimento.",
+ saiu: "O personagem saiu da tela! Cuidado com os limites da grade.",
+ naoAlcancou: "O personagem não chegou ao alvo. Tente de novo!",
+ },
+};
diff --git a/app/src/atividades/programacao/exemplo/game.js b/app/src/atividades/programacao/exemplo/game.js
new file mode 100644
index 0000000..edcb1f5
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo/game.js
@@ -0,0 +1,194 @@
+import Phaser from "phaser";
+import { BaseGameScene } from "../../../shared/BaseGameScene.js";
+import { setupExemploAPI } from "./hooks/setupExemploAPI.js";
+import { validationSolution } from "./validation/validators.js";
+import { gameConfig } from "./config/config.js";
+import { Assets, Constantes } from "./ui/constants.js";
+import { montarGrade, criarAlvo, criarJogador } from "./ui/layout.js";
+
+export class ExemploScene extends BaseGameScene {
+ constructor() {
+ super("ExemploScene");
+ this.jogadorLogico = { col: 0, row: 0 };
+ this.jogadorSprite = null;
+ this.alvoSprite = null;
+ this._gridGraphics = null;
+ this.executionStopped = false;
+ }
+
+ preload() {
+ this.preloadGlobalAssets();
+ // Carrega o logo via URL pública — assets em /public não precisam de import de módulo
+ this.load.image(Assets.CHAVES.LOGO, Assets.PATHS.LOGO);
+ }
+
+ create() {
+ this.validatorFunc = (historico) =>
+ validationSolution(historico, this.configFase, gameConfig, this);
+
+ // Registra a API do interpreter e os handlers de run/reset via BaseGameScene
+ this.setupStandardController(
+ () => setupExemploAPI(this, { animationSpeed: 200 }),
+ this.validatorFunc
+ );
+
+ this.montarFase();
+ }
+
+ onBeforeRun() {
+ this.isRunning = true;
+ this.historico = [];
+ this.executionStopped = false;
+ }
+
+ onReset() {
+ this.isRunning = false;
+ this.executionStopped = true;
+ this.montarFase();
+ }
+
+ async onSuccess() {
+ this.isRunning = false;
+ return new Promise((resolve) => {
+ this.tweens.add({
+ targets: this.jogadorSprite,
+ scaleX: 1.6,
+ scaleY: 1.6,
+ duration: 180,
+ yoyo: true,
+ repeat: 2,
+ ease: "Back.easeOut",
+ onComplete: resolve,
+ });
+ });
+ }
+
+ async onFailure() {
+ this.isRunning = false;
+ return new Promise((resolve) => {
+ this.tweens.add({
+ targets: this.jogadorSprite,
+ x: "+=6",
+ yoyo: true,
+ repeat: 8,
+ duration: 40,
+ onComplete: resolve,
+ });
+ });
+ }
+
+ montarFase() {
+ if (this._gridGraphics) this._gridGraphics.destroy();
+ if (this.alvoSprite) this.alvoSprite.destroy();
+ if (this.jogadorSprite) this.jogadorSprite.destroy();
+
+ const cfg = this.configFase;
+ const cols = cfg?.cols || Constantes.COLS;
+ const rows = cfg?.rows || Constantes.ROWS;
+ const inicio = cfg?.jogadorInicio || { col: 0, row: 0 };
+ const alvo = cfg?.alvo || { col: 4, row: 4 };
+
+ this._gridGraphics = montarGrade(this, cols, rows);
+ this.alvoSprite = criarAlvo(this, alvo.col, alvo.row);
+ this.jogadorLogico = { col: inicio.col, row: inicio.row };
+ this.jogadorSprite = criarJogador(this, inicio.col, inicio.row);
+ this.executionStopped = false;
+ }
+
+ // --- API exposta ao js-interpreter via setupExemploAPI ---
+
+ moverDireita() {
+ if (this.executionStopped) return Promise.resolve();
+
+ const novaCol = this.jogadorLogico.col + 1;
+ if (novaCol >= (this.configFase?.cols || Constantes.COLS)) {
+ return this._falharSaida();
+ }
+
+ this.jogadorLogico.col = novaCol;
+ this.historico.push({ tipo: "mover", direcao: "DIREITA", col: novaCol, row: this.jogadorLogico.row });
+ return this._animarJogador(() => this._checarAlvo());
+ }
+
+ moverBaixo() {
+ if (this.executionStopped) return Promise.resolve();
+
+ const novaRow = this.jogadorLogico.row + 1;
+ if (novaRow >= (this.configFase?.rows || Constantes.ROWS)) {
+ return this._falharSaida();
+ }
+
+ this.jogadorLogico.row = novaRow;
+ this.historico.push({ tipo: "mover", direcao: "BAIXO", col: this.jogadorLogico.col, row: novaRow });
+ return this._animarJogador(() => this._checarAlvo());
+ }
+
+ // Para o interpreter e agenda handleFailure quando o jogador sai da grade
+ _falharSaida() {
+ this.executionStopped = true;
+ this.gameInterpreter.stopInternal();
+ this.time.delayedCall(100, () =>
+ this.handleFailure(this.gameConfig?.mensagens?.saiu || "Saiu da tela!")
+ );
+ return Promise.resolve();
+ }
+
+ // Anima o sprite até a posição lógica atual; executa onComplete ao término do tween
+ _animarJogador(onComplete) {
+ const { CELL_SIZE } = Constantes;
+ return new Promise((resolve) => {
+ this.tweens.add({
+ targets: this.jogadorSprite,
+ x: this.jogadorLogico.col * CELL_SIZE + CELL_SIZE / 2,
+ y: this.jogadorLogico.row * CELL_SIZE + CELL_SIZE / 2,
+ duration: 150,
+ ease: "Power1",
+ onComplete: () => {
+ if (onComplete) onComplete();
+ resolve();
+ },
+ });
+ });
+ }
+
+ // Verifica se o jogador atingiu o alvo após cada movimento bem-sucedido
+ _checarAlvo() {
+ if (this.executionStopped) return;
+ const alvo = this.configFase?.alvo || { col: 4, row: 4 };
+ if (this.jogadorLogico.col !== alvo.col || this.jogadorLogico.row !== alvo.row) return;
+
+ this.executionStopped = true;
+ // Para o interpreter sem marcar como parado pelo usuário — validação ainda ocorre
+ this.gameInterpreter.stopInternal();
+ this.time.delayedCall(300, () => {
+ if (this.validatorFunc) this.handleValidation(this.validatorFunc);
+ });
+ }
+}
+
+/**
+ * Factory para criar a configuração Phaser do jogo Exemplo.
+ * Injeta configFase e gameConfig no registry antes do boot da cena.
+ */
+export const createGame = (elementoPai, configFaseAtual) => {
+ const scene = new ExemploScene();
+ const { CELL_SIZE, COLS, ROWS } = Constantes;
+ const cols = configFaseAtual?.cols || COLS;
+ const rows = configFaseAtual?.rows || ROWS;
+
+ return {
+ type: Phaser.AUTO,
+ width: cols * CELL_SIZE,
+ height: rows * CELL_SIZE,
+ backgroundColor: "#1a1a2e",
+ 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/exemplo/hooks/setupExemploAPI.js b/app/src/atividades/programacao/exemplo/hooks/setupExemploAPI.js
new file mode 100644
index 0000000..641854a
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo/hooks/setupExemploAPI.js
@@ -0,0 +1,42 @@
+import { ApiHelpers } from "../../../../interpreters/ApiHelpers.js";
+
+/**
+ * Retorna a função de init do js-interpreter para o jogo Exemplo.
+ * É aqui que a ponte entre o interpretador sandboxed e a cena Phaser é construída:
+ * cada função registrada expõe um método da cena como uma chamada segura para o aluno.
+ *
+ * @param {ExemploScene} scene - Instância da cena Phaser ativa
+ * @param {Object} config - Configurações opcionais (ex: animationSpeed)
+ * @returns {Function} Função de init com assinatura (interpreter, globalScope)
+ */
+export const setupExemploAPI = (scene, config) => {
+ const delay = (config && config.animationSpeed) || 200;
+
+ return function (interpreter, globalScope) {
+ // Ações assíncronas: createAsyncFunction aguarda o callback para avançar o interpreter
+ ApiHelpers.registerFunction(
+ interpreter,
+ globalScope,
+ "moverDireita",
+ ApiHelpers.createActionWrapper(scene, "moverDireita", delay),
+ true // isAsync
+ );
+
+ ApiHelpers.registerFunction(
+ interpreter,
+ globalScope,
+ "moverBaixo",
+ ApiHelpers.createActionWrapper(scene, "moverBaixo", delay),
+ true // isAsync
+ );
+
+ // Necessário para o highlight visual dos blocos durante a execução
+ ApiHelpers.registerFunction(
+ interpreter,
+ globalScope,
+ "highlightBlock",
+ ApiHelpers.createHighlightWrapper(scene),
+ false
+ );
+ };
+};
diff --git a/app/src/atividades/programacao/exemplo/ui/constants.js b/app/src/atividades/programacao/exemplo/ui/constants.js
new file mode 100644
index 0000000..32cee87
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo/ui/constants.js
@@ -0,0 +1,19 @@
+// Assets da pasta public/ são referenciados por URL direta, sem import de módulo.
+// Esta é a forma correta em Vite para arquivos em /public.
+export const Assets = {
+ CHAVES: { LOGO: "exemplo_logo" },
+ PATHS: { LOGO: "/img/logo.png" },
+};
+
+export const Constantes = {
+ CELL_SIZE: 80,
+ COLS: 5,
+ ROWS: 5,
+};
+
+// Cores no formato Phaser (0xRRGGBB)
+export const Cores = {
+ FUNDO: 0x1a1a2e,
+ GRADE: 0x2d2d4e,
+ ALVO: 0x69f0ae,
+};
diff --git a/app/src/atividades/programacao/exemplo/ui/layout.js b/app/src/atividades/programacao/exemplo/ui/layout.js
new file mode 100644
index 0000000..c9e4652
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo/ui/layout.js
@@ -0,0 +1,66 @@
+import { Assets, Constantes, Cores } from "./constants.js";
+
+/**
+ * Desenha a grade de fundo da cena.
+ * @param {Phaser.Scene} scene
+ * @param {number} cols
+ * @param {number} rows
+ * @returns {Phaser.GameObjects.Graphics}
+ */
+export function montarGrade(scene, cols, rows) {
+ const { CELL_SIZE } = Constantes;
+ const g = scene.add.graphics();
+
+ g.fillStyle(Cores.FUNDO);
+ g.fillRect(0, 0, cols * CELL_SIZE, rows * CELL_SIZE);
+ g.lineStyle(1, Cores.GRADE, 1);
+
+ for (let c = 0; c <= cols; c++) {
+ g.lineBetween(c * CELL_SIZE, 0, c * CELL_SIZE, rows * CELL_SIZE);
+ }
+ for (let r = 0; r <= rows; r++) {
+ g.lineBetween(0, r * CELL_SIZE, cols * CELL_SIZE, r * CELL_SIZE);
+ }
+
+ return g;
+}
+
+/**
+ * Cria o retângulo visual do alvo.
+ * @param {Phaser.Scene} scene
+ * @param {number} col
+ * @param {number} row
+ * @returns {Phaser.GameObjects.Rectangle}
+ */
+export function criarAlvo(scene, col, row) {
+ const { CELL_SIZE } = Constantes;
+ return scene.add
+ .rectangle(
+ col * CELL_SIZE + CELL_SIZE / 2,
+ row * CELL_SIZE + CELL_SIZE / 2,
+ CELL_SIZE - 8,
+ CELL_SIZE - 8,
+ Cores.ALVO
+ )
+ .setDepth(1);
+}
+
+/**
+ * Cria o sprite do jogador usando o logo do projeto.
+ * O asset é carregado em ExemploScene.preload() via this.load.image().
+ * @param {Phaser.Scene} scene
+ * @param {number} col
+ * @param {number} row
+ * @returns {Phaser.GameObjects.Image}
+ */
+export function criarJogador(scene, col, row) {
+ const { CELL_SIZE } = Constantes;
+ return scene.add
+ .image(
+ col * CELL_SIZE + CELL_SIZE / 2,
+ row * CELL_SIZE + CELL_SIZE / 2,
+ Assets.CHAVES.LOGO
+ )
+ .setDisplaySize(CELL_SIZE - 10, CELL_SIZE - 10)
+ .setDepth(10);
+}
diff --git a/app/src/atividades/programacao/exemplo/validation/validators.js b/app/src/atividades/programacao/exemplo/validation/validators.js
new file mode 100644
index 0000000..b219bab
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo/validation/validators.js
@@ -0,0 +1,22 @@
+import { BaseGameValidator } from "../../../../shared/BaseGameValidator";
+
+export class ExemploValidator extends BaseGameValidator {
+ validatePhase(history, config, gameConfig, sceneRef) {
+ // Lê a posição lógica final do jogador diretamente da cena (fonte de verdade)
+ const { jogadorLogico } = sceneRef;
+ const alvo = config?.alvo;
+
+ if (jogadorLogico.col === alvo.col && jogadorLogico.row === alvo.row) {
+ return this.success();
+ }
+
+ return this.failure(
+ gameConfig?.mensagens?.naoAlcancou || "O personagem não chegou ao alvo. Tente de novo!"
+ );
+ }
+}
+
+export function validationSolution(history, config, gameConfig, sceneRef) {
+ const validator = new ExemploValidator();
+ return validator.validate(history, config, gameConfig, sceneRef);
+}
diff --git a/app/src/atividades/programacao/exemplo2/ExemploGame2.jsx b/app/src/atividades/programacao/exemplo2/ExemploGame2.jsx
new file mode 100644
index 0000000..77fe0dc
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo2/ExemploGame2.jsx
@@ -0,0 +1,45 @@
+import React, { useEffect, useMemo } from "react";
+import PropTypes from "prop-types";
+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";
+
+function ExemploContent2() {
+ const { setFailureMessage } = useGameState();
+
+ useEffect(() => {
+ registerBlocks();
+ }, []);
+
+ const toolboxGenerator = useMemo(() => {
+ return (allowedBlocks) => generateDynamicToolbox(allowedBlocks);
+ }, []);
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default function ExemploGame2() {
+ return (
+
+
+
+ );
+}
+
+ExemploContent2.propTypes = {};
+ExemploGame2.propTypes = {};
diff --git a/app/src/atividades/programacao/exemplo2/blocks/blocks.js b/app/src/atividades/programacao/exemplo2/blocks/blocks.js
new file mode 100644
index 0000000..12a0214
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo2/blocks/blocks.js
@@ -0,0 +1,54 @@
+"use strict";
+
+import "blockly/blocks";
+import { CORES_BLOCKLY } from "@/blockly/blocklyColors";
+import { configurarGerador, gerarStatementComValor } from "@/blockly/generator";
+import { gerarToolboxDeEstrutura } from "@/blockly/toolbox";
+import { criarBlocoStatementComValor } from "@/blockly/blockFactory";
+
+// text e text_join são blocos nativos do Blockly; não precisam de defineBlocks
+const ESTRUTURA_TOOLBOX = [
+ {
+ nome: "Funções",
+ cor: CORES_BLOCKLY.VARIAVEIS,
+ cssContainer: "cat_saida",
+ blocos: ["exemplo2_imprimir"],
+ },
+ {
+ nome: "Texto",
+ cor: CORES_BLOCKLY.TEXTO,
+ cssContainer: "cat_texto",
+ blocos: ["text", "text_join"],
+ },
+];
+
+export const registerBlocks = () => {
+ defineBlocks();
+ defineGenerators();
+};
+
+export const generateDynamicToolbox = (allowedBlocks = []) => {
+ return gerarToolboxDeEstrutura(ESTRUTURA_TOOLBOX, allowedBlocks);
+};
+
+const defineBlocks = () => {
+ // Bloco statement que aceita qualquer valor de texto como input
+ criarBlocoStatementComValor(
+ "exemplo2_imprimir",
+ "imprimir",
+ "TEXTO",
+ null, // aceita qualquer tipo (String ou texto concatenado)
+ CORES_BLOCKLY.VARIAVEIS
+ );
+};
+
+const defineGenerators = () => {
+ configurarGerador();
+
+ // gerarStatementComValor lê o input "TEXTO" e usa o template para gerar o código
+ gerarStatementComValor(
+ "exemplo2_imprimir",
+ "TEXTO",
+ (valor) => `imprimir(${valor})`
+ );
+};
diff --git a/app/src/atividades/programacao/exemplo2/config/config.js b/app/src/atividades/programacao/exemplo2/config/config.js
new file mode 100644
index 0000000..34f613f
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo2/config/config.js
@@ -0,0 +1,43 @@
+export const gameConfig = {
+ gameId: "exemplo2",
+ gameName: "Exemplo 2: Texto",
+ type: "blocks",
+ icon: "💬",
+ thumbnail: "/images/atividades/programacao/exemplo2-thumbnail.png",
+ descricao:
+ "Atividade de demonstração com texto. Use o bloco 'imprimir' para exibir a frase correta.",
+ dificuldade: "Iniciante",
+ categoria: "Lógica",
+ tempoEstimado: "5 min",
+ conceitos: ["Texto", "Concatenação"],
+ route: "/atividades/programacao/exemplo2",
+ component: "ExemploGame2",
+ objectives: [
+ "Demonstrar como trabalhar com strings e concatenação de texto na plataforma Decoda",
+ ],
+ metadata: {
+ lastUpdated: "2026-06-24",
+ version: "1.0.0",
+ },
+
+ fases: [
+ {
+ id: 1,
+ nome: "Fase 1: Escreva a Frase",
+ descricao:
+ 'Use o bloco "imprimir" com o texto correto. Dica: você pode escrever tudo em um bloco de texto ou juntar duas palavras!',
+ timeout: 10,
+ // 4 blocos permite: imprimir + juntar + "SOBERANIA" + " DIGITAL"
+ // 2 blocos resolve na forma simples: imprimir + "SOBERANIA DIGITAL"
+ maxBlocks: 4,
+ allowedBlocks: ["exemplo2_imprimir", "text", "text_join"],
+ textoEsperado: "SOBERANIA DIGITAL",
+ },
+ ],
+
+ mensagens: {
+ semMovimento: "Use o bloco 'imprimir' para mostrar um texto!",
+ textoErrado: (atual, esperado) =>
+ `Texto incorreto: "${atual}". O esperado é "${esperado}".`,
+ },
+};
diff --git a/app/src/atividades/programacao/exemplo2/game.js b/app/src/atividades/programacao/exemplo2/game.js
new file mode 100644
index 0000000..15fea84
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo2/game.js
@@ -0,0 +1,117 @@
+import Phaser from "phaser";
+import { BaseGameScene } from "../../../shared/BaseGameScene.js";
+import { setupExemplo2API } from "./hooks/setupExemplo2API.js";
+import { validationSolution } from "./validation/validators.js";
+import { gameConfig } from "./config/config.js";
+import { Constantes } from "./ui/constants.js";
+import { montarDisplay } from "./ui/layout.js";
+
+export class Exemplo2Scene extends BaseGameScene {
+ constructor() {
+ super("Exemplo2Scene");
+ this.textoAtual = "";
+ this.textoDisplay = null;
+ }
+
+ preload() {
+ this.preloadGlobalAssets();
+ }
+
+ create() {
+ this.validatorFunc = (historico) =>
+ validationSolution(historico, this.configFase, gameConfig, this);
+
+ // Registra a API do interpreter e os handlers de run/reset via BaseGameScene
+ this.setupStandardController(
+ () => setupExemplo2API(this),
+ this.validatorFunc
+ );
+
+ this.montarFase();
+ }
+
+ onBeforeRun() {
+ this.isRunning = true;
+ this.historico = [];
+ this.textoAtual = "";
+ this.textoDisplay.setText("");
+ this.textoDisplay.setColor("#e0e0ff");
+ }
+
+ onReset() {
+ this.isRunning = false;
+ this.textoAtual = "";
+ this.textoDisplay.setText("");
+ this.textoDisplay.setColor("#e0e0ff");
+ }
+
+ async onSuccess() {
+ this.isRunning = false;
+ this.textoDisplay.setColor("#69f0ae");
+ return new Promise((resolve) => {
+ this.tweens.add({
+ targets: this.textoDisplay,
+ scaleX: 1.1,
+ scaleY: 1.1,
+ duration: 150,
+ yoyo: true,
+ repeat: 2,
+ onComplete: () => {
+ this.textoDisplay.setColor("#e0e0ff");
+ resolve();
+ },
+ });
+ });
+ }
+
+ async onFailure() {
+ this.isRunning = false;
+ this.textoDisplay.setColor("#ff4444");
+ return new Promise((resolve) => {
+ this.time.delayedCall(500, () => {
+ this.textoDisplay.setColor("#e0e0ff");
+ resolve();
+ });
+ });
+ }
+
+ montarFase() {
+ if (this.textoDisplay) this.textoDisplay.destroy();
+ this.textoAtual = "";
+ // montarDisplay cria os objetos visuais e retorna a referência ao texto de saída
+ this.textoDisplay = montarDisplay(this);
+ }
+
+ // --- API exposta ao js-interpreter via setupExemplo2API ---
+
+ imprimir(texto) {
+ this.textoAtual = texto;
+ this.textoDisplay.setText(texto);
+ this.historico.push({ tipo: "imprimir", texto });
+ }
+}
+
+/**
+ * Factory para criar a configuração Phaser do jogo Exemplo2.
+ * Injeta configFase e gameConfig no registry antes do boot da cena.
+ */
+export const createGame = (elementoPai, configFaseAtual) => {
+ const scene = new Exemplo2Scene();
+ const { LARGURA, ALTURA } = Constantes;
+
+ return {
+ type: Phaser.AUTO,
+ width: LARGURA,
+ height: ALTURA,
+ backgroundColor: "#0a0a1a",
+ 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/exemplo2/hooks/setupExemplo2API.js b/app/src/atividades/programacao/exemplo2/hooks/setupExemplo2API.js
new file mode 100644
index 0000000..794fc61
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo2/hooks/setupExemplo2API.js
@@ -0,0 +1,31 @@
+import { ApiHelpers } from "../../../../interpreters/ApiHelpers.js";
+
+/**
+ * Retorna a função de init do js-interpreter para o jogo Exemplo2.
+ *
+ * @param {Exemplo2Scene} scene
+ * @returns {Function} Função de init com assinatura (interpreter, globalScope)
+ */
+export const setupExemplo2API = (scene) => {
+ return function (interpreter, globalScope) {
+ // imprimir(texto) é síncrona: apenas atualiza o estado da cena, sem aguardar animação.
+ // Por isso usa createNativeFunction em vez de createAsyncFunction.
+ const imprimirWrapper = interpreter.createNativeFunction((textoRaw) => {
+ // js-interpreter pode passar primitivos diretamente ou encapsulados em {data: ...}
+ const texto =
+ textoRaw !== null && typeof textoRaw === "object" && textoRaw.data !== undefined
+ ? String(textoRaw.data)
+ : String(textoRaw ?? "");
+ scene.imprimir(texto);
+ });
+ interpreter.setProperty(globalScope, "imprimir", imprimirWrapper);
+
+ ApiHelpers.registerFunction(
+ interpreter,
+ globalScope,
+ "highlightBlock",
+ ApiHelpers.createHighlightWrapper(scene),
+ false
+ );
+ };
+};
diff --git a/app/src/atividades/programacao/exemplo2/ui/constants.js b/app/src/atividades/programacao/exemplo2/ui/constants.js
new file mode 100644
index 0000000..1f5f968
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo2/ui/constants.js
@@ -0,0 +1,17 @@
+export const Constantes = {
+ LARGURA: 480,
+ ALTURA: 220,
+};
+
+// Cores no formato Phaser (0xRRGGBB)
+export const Cores = {
+ FUNDO: 0x0a0a1a,
+ PAINEL: 0x111128,
+ BORDA: 0x3a3a6e,
+};
+
+// Estilos de texto Phaser
+export const Fontes = {
+ SAIDA: { fontSize: "22px", fontFamily: "monospace", color: "#e0e0ff" },
+ LABEL: { fontSize: "13px", fontFamily: "monospace", color: "#555577" },
+};
diff --git a/app/src/atividades/programacao/exemplo2/ui/layout.js b/app/src/atividades/programacao/exemplo2/ui/layout.js
new file mode 100644
index 0000000..46ae46b
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo2/ui/layout.js
@@ -0,0 +1,34 @@
+import { Constantes, Cores, Fontes } from "./constants.js";
+
+/**
+ * Cria o display de texto da cena.
+ * Retorna a referência ao Phaser.Text de saída para ser atualizado pela cena.
+ *
+ * @param {Phaser.Scene} scene
+ * @returns {Phaser.GameObjects.Text}
+ */
+export function montarDisplay(scene) {
+ const { LARGURA, ALTURA } = Constantes;
+ const cx = LARGURA / 2;
+ const cy = ALTURA / 2;
+
+ // Fundo escuro
+ scene.add.rectangle(cx, cy, LARGURA, ALTURA, Cores.FUNDO).setDepth(0);
+
+ // Painel com borda sutil
+ scene.add
+ .rectangle(cx, cy, LARGURA - 40, ALTURA - 70, Cores.PAINEL)
+ .setStrokeStyle(1, Cores.BORDA)
+ .setDepth(1);
+
+ // Label "Saída:"
+ scene.add.text(30, 18, "Saída:", Fontes.LABEL).setDepth(2);
+
+ // Texto de saída — começa vazio, atualizado por imprimir()
+ const textoDisplay = scene.add
+ .text(cx, cy, "", { ...Fontes.SAIDA, align: "center" })
+ .setOrigin(0.5, 0.5)
+ .setDepth(2);
+
+ return textoDisplay;
+}
diff --git a/app/src/atividades/programacao/exemplo2/validation/validators.js b/app/src/atividades/programacao/exemplo2/validation/validators.js
new file mode 100644
index 0000000..4cefbb7
--- /dev/null
+++ b/app/src/atividades/programacao/exemplo2/validation/validators.js
@@ -0,0 +1,22 @@
+import { BaseGameValidator } from "../../../../shared/BaseGameValidator";
+
+export class Exemplo2Validator extends BaseGameValidator {
+ validatePhase(history, config, gameConfig, sceneRef) {
+ const esperado = config?.textoEsperado || "SOBERANIA DIGITAL";
+ const atual = sceneRef?.textoAtual ?? "";
+
+ if (atual === esperado) return this.success();
+
+ const msgErro =
+ atual && typeof gameConfig?.mensagens?.textoErrado === "function"
+ ? gameConfig.mensagens.textoErrado(atual, esperado)
+ : `Texto incorreto: "${atual}". O esperado é "${esperado}".`;
+
+ return this.failure(msgErro);
+ }
+}
+
+export function validationSolution(history, config, gameConfig, sceneRef) {
+ const validator = new Exemplo2Validator();
+ return validator.validate(history, config, gameConfig, sceneRef);
+}