From b210ed4f92cd9ffde315da5781edb51093f2ef00 Mon Sep 17 00:00:00 2001 From: "rui.moraes" Date: Wed, 24 Jun 2026 21:00:19 -0300 Subject: [PATCH 1/2] add templates --- app/src/App.jsx | 4 + .../programacao/exemplo/ExemploGame.jsx | 47 +++++ .../programacao/exemplo/blocks/blocks.js | 58 ++++++ .../programacao/exemplo/config/config.js | 51 +++++ .../atividades/programacao/exemplo/game.js | 194 ++++++++++++++++++ .../exemplo/hooks/setupExemploAPI.js | 42 ++++ .../programacao/exemplo/ui/constants.js | 19 ++ .../programacao/exemplo/ui/layout.js | 66 ++++++ .../exemplo/validation/validators.js | 22 ++ .../programacao/exemplo2/ExemploGame2.jsx | 45 ++++ .../programacao/exemplo2/blocks/blocks.js | 54 +++++ .../programacao/exemplo2/config/config.js | 43 ++++ .../atividades/programacao/exemplo2/game.js | 117 +++++++++++ .../exemplo2/hooks/setupExemplo2API.js | 31 +++ .../programacao/exemplo2/ui/constants.js | 17 ++ .../programacao/exemplo2/ui/layout.js | 34 +++ .../exemplo2/validation/validators.js | 22 ++ 17 files changed, 866 insertions(+) create mode 100644 app/src/atividades/programacao/exemplo/ExemploGame.jsx create mode 100644 app/src/atividades/programacao/exemplo/blocks/blocks.js create mode 100644 app/src/atividades/programacao/exemplo/config/config.js create mode 100644 app/src/atividades/programacao/exemplo/game.js create mode 100644 app/src/atividades/programacao/exemplo/hooks/setupExemploAPI.js create mode 100644 app/src/atividades/programacao/exemplo/ui/constants.js create mode 100644 app/src/atividades/programacao/exemplo/ui/layout.js create mode 100644 app/src/atividades/programacao/exemplo/validation/validators.js create mode 100644 app/src/atividades/programacao/exemplo2/ExemploGame2.jsx create mode 100644 app/src/atividades/programacao/exemplo2/blocks/blocks.js create mode 100644 app/src/atividades/programacao/exemplo2/config/config.js create mode 100644 app/src/atividades/programacao/exemplo2/game.js create mode 100644 app/src/atividades/programacao/exemplo2/hooks/setupExemplo2API.js create mode 100644 app/src/atividades/programacao/exemplo2/ui/constants.js create mode 100644 app/src/atividades/programacao/exemplo2/ui/layout.js create mode 100644 app/src/atividades/programacao/exemplo2/validation/validators.js 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); +} From 592a5a17e4f17df247b9cf649570888c293e34cb Mon Sep 17 00:00:00 2001 From: "rui.moraes" Date: Wed, 24 Jun 2026 21:26:29 -0300 Subject: [PATCH 2/2] update doc --- .../docs/crie-suas-atividades/_category_.json | 14 + docs/docs/crie-suas-atividades/game-design.md | 216 ++++++++ docs/docs/crie-suas-atividades/intro.md | 73 +++ .../crie-suas-atividades/passo-a-passo.md | 465 ++++++++++++++++++ .../usando-os-exemplos.md | 108 ++++ 5 files changed, 876 insertions(+) create mode 100644 docs/docs/crie-suas-atividades/_category_.json create mode 100644 docs/docs/crie-suas-atividades/game-design.md create mode 100644 docs/docs/crie-suas-atividades/intro.md create mode 100644 docs/docs/crie-suas-atividades/passo-a-passo.md create mode 100644 docs/docs/crie-suas-atividades/usando-os-exemplos.md diff --git a/docs/docs/crie-suas-atividades/_category_.json b/docs/docs/crie-suas-atividades/_category_.json new file mode 100644 index 0000000..845d51f --- /dev/null +++ b/docs/docs/crie-suas-atividades/_category_.json @@ -0,0 +1,14 @@ +{ + "position": 4, + "label": "Crie suas Atividades", + "collapsible": true, + "collapsed": false, + "className": "red", + "link": { + "type": "generated-index", + "title": "Crie suas Atividades" + }, + "customProps": { + "description": "Guia prático para criar novas atividades de programação na plataforma Decoda, do zero ao deploy." + } +} \ No newline at end of file diff --git a/docs/docs/crie-suas-atividades/game-design.md b/docs/docs/crie-suas-atividades/game-design.md new file mode 100644 index 0000000..86bd342 --- /dev/null +++ b/docs/docs/crie-suas-atividades/game-design.md @@ -0,0 +1,216 @@ +--- +sidebar_position: 4 +title: "Game Design Pedagógico" +--- + +Esta página não é sobre tecnologia — é sobre **como pensar antes de escrever código**. Uma atividade tecnicamente perfeita pode ser um fracasso pedagógico se o aluno não aprender nada ou desistir frustrado antes de terminar. + +:::warning +**Premissa inegociável**: Toda atividade da Decoda existe para **ensinar ou reforçar um conceito de programação**. Entretenimento é um meio, não o fim. Se a atividade for divertida mas o aluno não aprender nada de programação, ela não pertence à plataforma. +::: + +--- + +## Princípio 1 — Uma fase, um conceito + +Cada fase deve ter **um único objetivo de aprendizagem**. Não "aprender loops e condicionais ao mesmo tempo" — ou loops, ou condicionais. + +``` +✅ Fase 1: O aluno aprende a usar o bloco "repetir N vezes" +✅ Fase 2: O aluno aprende a combinar repetição com condicionais +❌ Fase 1: O aluno aprende repetição, condicionais, variáveis e sensores +``` + +Isso não significa que a atividade deve ser trivial. Significa que a **novidade cognitiva** introduzida por fase deve ser uma coisa de cada vez. + +**Como implementar:** o campo `allowedBlocks` em `config.js` é o seu mecanismo. Libere blocos novos progressivamente conforme as fases avançam. + +```javascript +// Fase 1 — só movimento +allowedBlocks: ["mover_direita", "mover_baixo"] + +// Fase 2 — adiciona repetição +allowedBlocks: ["controls_repeat_ext", "math_number", "mover_direita", "mover_baixo"] + +// Fase 3 — adiciona condicionais +allowedBlocks: ["controls_whileUntil", "robo_if", "sensor_frente", "mover_direita", "mover_baixo"] +``` + +--- + +## Princípio 2 — A curva de dificuldade + +A progressão de dificuldade deve seguir uma curva suave: o aluno nunca deve sentir um salto abrupto entre duas fases consecutivas. + +``` +Dificuldade + │ + 5 │ ╭──── Fase 5 + 4 │ ╭───────╯ + 3 │ ╭────────╯ + 2 │ ╭────────╯ + 1 │───╯ + └──────────────────────────────── Fase + 1 2 3 4 5 +``` + +Se a curva tiver um degrau vertical, o aluno vai travar. Isso gera frustração, não aprendizado. + +**Sinais de que a curva está errada:** +- Alunos em teste completam a Fase N facilmente e travam na Fase N+1 por mais de 5 minutos sem progresso. +- A Fase N+1 exige um conceito que nunca foi apresentado antes. +- O `maxBlocks` da Fase N+1 é incompatível com a solução esperada. + +**Regra prática:** a Fase N+1 deve ser solucionável por alguém que acabou de completar a Fase N sem ajuda. + +--- + +## Princípio 3 — O `maxBlocks` como professor, não como punição + +O limite de blocos é uma **diretriz pedagógica**, não uma punição. Seu papel é impedir que o aluno resolva o problema "na força bruta" sem aprender o conceito esperado. + +```javascript +// ❌ Errado: maxBlocks tão alto que qualquer solução serve +{ maxBlocks: 50, allowedBlocks: ["controls_repeat_ext", "mover_direita"] } +// → O aluno coloca 10 blocos "mover_direita" sem usar o loop. Não aprende nada. + +// ✅ Correto: maxBlocks força o loop +{ maxBlocks: 4, allowedBlocks: ["controls_repeat_ext", "math_number", "mover_direita"] } +// → Solução: repetir(4) { moverDireita() } ← exatamente 3 blocos. Ensina loops. +``` + +**Como calcular o `maxBlocks` ideal:** +1. Escreva a solução ótima (que usa o conceito da fase). +2. Conte os blocos dessa solução. +3. Some 1 ou 2 blocos de margem para pequenas variações. +4. Esse é o seu `maxBlocks`. + +--- + +## Princípio 4 — Mensagens de erro que ensinam + +A mensagem de falha é um **momento de aprendizado**. Ela não pode ser genérica, técnica ou intimidadora. + +```javascript +// ❌ Errado +mensagens: { + falha: "Erro: validação falhou.", + timeout: "Execution timeout exceeded." +} + +// ✅ Correto +mensagens: { + semMovimento: "Seu robô está parado! Use o bloco 'mover' para sair do lugar.", + saiu: "Ops! O personagem saiu da tela. Verifique quantos passos ele deu.", + timeoutExcedido: "Tempo esgotado! Seu programa parece estar em loop infinito. Verifique a condição de parada.", + naoAlcancou: "Quase lá! O personagem não chegou ao destino. Tente ajustar o número de passos." +} +``` + +**Checklist de mensagem de erro:** +- [ ] Está em português, sem jargão técnico +- [ ] Diz **o que aconteceu** (não apenas "erro") +- [ ] Dá uma **dica** do que verificar (sem entregar a resposta) +- [ ] Usa linguagem de encorajamento ("quase lá", "tente de novo") + +--- + +## Princípio 5 — O ciclo de feedback imediato + +O aluno deve receber feedback **visual e imediato** para cada ação que executa. Isso é o que diferencia um jogo educativo de uma tarefa de casa. + +```mermaid +flowchart LR + A[Aluno executa blocos] --> B[Personagem anima] + B --> C{Condição de fim?} + C -->|Sucesso| D[Animação de vitória\n+ Modal de parabéns] + C -->|Falha| E[Animação de erro\n+ Modal com dica] + C -->|Continua| B +``` + +**Na prática:** + +1. **Toda ação deve ter animação.** Se o personagem "teletransporta" sem transição, o aluno perde a referência do que aconteceu. + +2. **`onSuccess()` deve celebrar.** Use tweens, partículas, mudança de cor — qualquer efeito que transmita conquista. O modal de sucesso aparece *depois* da animação. + +3. **`onFailure()` deve ser empático, não assustador.** Um leve tremor, uma cor vermelha por um instante. Nada que desanime o aluno. + +4. **O highlight de blocos é obrigatório.** Sempre chame `configurarGerador()` em `blocks.js`. Ver o bloco iluminado enquanto executa ajuda o aluno a conectar o bloco visual com a ação no jogo. + +--- + +## Princípio 6 — Projetar contra a frustração + +A frustração acontece quando o aluno **não sabe o que fazer a seguir**. Projete a atividade para eliminar esse estado. + +### O que causa frustração + +| Causa | Como evitar | +|---|---| +| Fase impossível sem conhecimento prévio | Introduza o conceito na fase anterior, ou no texto da fase | +| `timeout` curto demais | Calcule o tempo da execução correta e multiplique por 3 | +| Mensagem de erro genérica | Escreva uma mensagem por condição de falha possível | +| `maxBlocks` abaixo da solução ótima | Sempre teste a solução antes de publicar | +| Fase sem saída (loop inevitável) | Teste com `timeout` ativado; se travar, redesenhe o mapa | +| Salto de dificuldade abrupto | Adicione uma fase de transição | + +### A regra dos 5 minutos + +Se um aluno iniciante trava em uma fase por mais de 5 minutos sem nenhum progresso visível, a fase está mal projetada. Não é o aluno que está errado. + +--- + +## Princípio 7 — O jogo deve ser jogável sem o enunciado + +O texto de `descricao` em `config.js` aparece na interface. Mas o jogo deve fazer sentido mesmo sem que o aluno o leia. O layout visual, os blocos disponíveis e o cenário devem comunicar o objetivo por si mesmos. + +``` +✅ Um robô no canto superior esquerdo de uma grade, com sujeira espalhada + e o cursor piscando no bloco "mover" → o objetivo é óbvio. + +❌ Uma tela escura com um ponto e texto dizendo + "mova o objeto para a posição de destino" → nada é óbvio. +``` + +**Dicas de design visual:** +- Use **verde** para o alvo/destino (convenção universal de "vá aqui"). +- Use **vermelho** apenas para erro ou perigo. +- A posição inicial do personagem deve estar **visivelmente separada** do alvo. +- O tamanho do personagem deve ser proporcional à grade (não pequeno demais). + +--- + +## Checklist de Game Design + +Antes de finalizar uma atividade, valide: + +**Pedagogia** +- [ ] Cada fase tem exatamente um conceito principal a ensinar +- [ ] Os blocos disponíveis são os mínimos necessários para o conceito da fase +- [ ] A progressão entre fases é gradual (sem saltos abruptos) +- [ ] A solução ótima usa o conceito que a fase pretende ensinar + +**Jogabilidade** +- [ ] `timeout` é pelo menos 3× o tempo da execução da solução correta +- [ ] `maxBlocks` é suficiente para a solução ótima + 1-2 blocos de margem +- [ ] `onSuccess()` tem uma animação de celebração +- [ ] `onFailure()` tem feedback visual empático (tremor, cor) +- [ ] O highlight de blocos está funcionando durante a execução + +**Comunicação com o aluno** +- [ ] Cada condição de falha tem sua própria mensagem específica +- [ ] As mensagens usam linguagem de encorajamento +- [ ] O objetivo da fase é visualmente óbvio no cenário +- [ ] O texto de `descricao` é curto, motivador e sem jargão técnico + +--- + +## Referências de boas práticas nas atividades existentes + +| Atividade | O que observar | +|---|---| +| **Aspirador** | Progressão pedagógica exemplar: 10 fases com curva gradual de sequência → loops → condicionais → variáveis → algoritmos | +| **Aspirador Fase 1** | `maxBlocks: 3` força o aluno a usar `while` logo na primeira fase | +| **Semáforo** | Uso de `expectedSequence` em `config.js` para validação por sequência exata de ações | +| **Puzzle** | Exemplo de atividade onde a validação ocorre por estado final, não por sequência | \ No newline at end of file diff --git a/docs/docs/crie-suas-atividades/intro.md b/docs/docs/crie-suas-atividades/intro.md new file mode 100644 index 0000000..8fbab35 --- /dev/null +++ b/docs/docs/crie-suas-atividades/intro.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 1 +title: "Visão Geral" +--- +Esta seção é o ponto de partida para quem quer **criar ou adaptar uma atividade de programação** na plataforma Decoda. + +A plataforma já entrega toda a infraestrutura: editor Blockly, interpretador seguro (JS-Interpreter), motor de física e renderização (Phaser), ciclo de sucesso/falha e controles de execução. Você cuida apenas da **lógica do jogo, dos blocos customizados e das regras de validação**. + +## O que você vai encontrar aqui + +| Página | O que cobre | +|---|---| +| [Usando os Exemplos](./usando-os-exemplos) | Como rodar e interpretar as atividades `Exemplo` e `Exemplo 2`, que foram criadas como código de referência | +| [Passo a Passo](./passo-a-passo) | Roteiro completo: da cópia do template ao registro da rota | +| [Game Design Pedagógico](./game-design) | Princípios para criar atividades que ensinam sem frustrar | + +## Pré-requisitos + +Antes de criar uma atividade, certifique-se de conhecer: + +- A [estrutura de atividades de programação](../plataforma/atividades_programacao/estrutura-de-jogo) da plataforma. +- O papel do [`BaseGameScene`](../plataforma/atividades_programacao/base-game-scene) no ciclo de execução. +- Como o [Blockly](../plataforma/atividades_programacao/blockly) gera código JavaScript. + +## Arquitetura em uma visão + +Cada atividade é composta por **6 camadas** que se comunicam de forma bem definida: + +```mermaid +flowchart TD + JSX["🎮 XGame.jsx\nComponente React + GameStateProvider"] + CONFIG["⚙️ config/config.js\nFases, maxBlocks, mensagens"] + BLOCKS["🧩 blocks/blocks.js\nBlocos customizados + toolbox"] + API["🔌 hooks/setupXAPI.js\nPonte JS-Interpreter ↔ Phaser"] + GAME["🖼️ game.js\nCena Phaser + createGame factory"] + VALID["✅ validation/validators.js\nRegras de sucesso e falha"] + UI["🎨 ui/\nconstants.js + layout.js"] + + JSX --> CONFIG + JSX --> BLOCKS + JSX --> GAME + GAME --> API + GAME --> VALID + GAME --> UI +``` + +## Fluxo de execução resumido + +```mermaid +sequenceDiagram + participant Aluno + participant Blockly + participant Interpreter as JS-Interpreter + participant Phaser + participant Validator + + Aluno->>Blockly: Monta blocos + Blockly->>Interpreter: Código JS gerado + Interpreter->>Phaser: Chama API da cena (ex: moverDireita()) + Phaser->>Phaser: Anima sprite, atualiza estado lógico + Interpreter-->>Validator: Execução concluída + Validator->>Aluno: Sucesso ou Falha com mensagem +``` + +## Dois caminhos para começar + +**Caminho rápido** — copie o `Exemplo` ou o `Exemplo 2` e adapte: +``` +app/src/atividades/programacao/exemplo/ ← visual (grade + sprite) +app/src/atividades/programacao/exemplo2/ ← textual (display + imprimir) +``` + +**Caminho estruturado** — siga o [Passo a Passo](./passo-a-passo) do zero, usando os exemplos como referência de cada decisão. \ No newline at end of file diff --git a/docs/docs/crie-suas-atividades/passo-a-passo.md b/docs/docs/crie-suas-atividades/passo-a-passo.md new file mode 100644 index 0000000..9f70de9 --- /dev/null +++ b/docs/docs/crie-suas-atividades/passo-a-passo.md @@ -0,0 +1,465 @@ +--- +sidebar_position: 3 +title: "Passo a Passo" +--- + +Roteiro completo para criar uma nova atividade de programação do zero. Use a atividade `Exemplo` como gabarito de cada etapa. + +:::tip +Se sua atividade for visual (sprite em grade/mapa), copie `exemplo/`. Se for baseada em texto ou saída de dados, copie `exemplo2/`. Depois siga este guia para entender o que adaptar. +::: + +--- + +## Passo 1 — Estrutura de pastas + +Crie a pasta da atividade em `app/src/atividades/programacao//`: + +``` +meu-jogo/ +├── MeuJogoGame.jsx ← componente de entrada (React) +├── game.js ← cena Phaser + factory createGame +├── blocks/ +│ └── blocks.js ← definição e geradores dos blocos +├── config/ +│ └── config.js ← fases, maxBlocks, mensagens +├── hooks/ +│ └── setupMeuJogoAPI.js ← ponte JS-Interpreter ↔ Phaser +├── validation/ +│ └── validators.js ← regras de sucesso e falha +└── ui/ + ├── constants.js ← assets, cores, dimensões + └── layout.js ← funções de construção visual +``` + +--- + +## Passo 2 — `config/config.js` + +O config é a **espinha dorsal pedagógica** da atividade. Define todas as fases e seus parâmetros. + +```javascript +export const gameConfig = { + gameId: "meu-jogo", + gameName: "Meu Jogo", + route: "/atividades/programacao/meu-jogo", + component: "MeuJogoGame", + + fases: [ + { + id: 1, + nome: "Fase 1: Introdução", + descricao: "Descrição curta e motivadora do desafio desta fase.", + timeout: 15, // segundos — protege contra loops infinitos + maxBlocks: 4, // limite pedagógico (não técnico) + allowedBlocks: ["meu_bloco_a", "meu_bloco_b"], + + // Dados específicos do jogo (lidos em montarFase) + // ex: posição inicial, mapa, obstáculos... + }, + ], + + mensagens: { + semMovimento: "Você não executou nenhuma ação!", + // Mensagens específicas da sua atividade... + }, +}; +``` + +**Checklist do config:** + +- [ ] `allowedBlocks` contém apenas os blocos necessários para **esta fase** +- [ ] `maxBlocks` força a solução elegante (não a solução de força bruta) +- [ ] `timeout` é generoso o suficiente para o aluno ter tempo de ver a execução +- [ ] Mensagens de erro são em linguagem acessível (sem jargão técnico) + +--- + +## Passo 3 — `blocks/blocks.js` + +Define os blocos visuais e seus geradores de código JavaScript. + +```javascript +"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"; + +const ESTRUTURA_TOOLBOX = [ + { + nome: "Movimento", + cor: CORES_CUSTOMIZADAS.MOVIMENTO, + cssContainer: "cat_movimento", + blocos: ["meu_bloco_a"], + }, +]; + +export const registerBlocks = () => { + defineBlocks(); + defineGenerators(); +}; + +export const generateDynamicToolbox = (allowedBlocks = []) => + gerarToolboxDeEstrutura(ESTRUTURA_TOOLBOX, allowedBlocks); + +const defineBlocks = () => { + criarBlocoStatementSimples("meu_bloco_a", "executar ação A", CORES_CUSTOMIZADAS.MOVIMENTO); +}; + +const defineGenerators = () => { + configurarGerador(); // ← sempre chamar primeiro (ativa highlight) + gerarStatement("meu_bloco_a", "executarAcaoA"); +}; +``` + +### Factories de bloco disponíveis (`@/blockly/blockFactory`) + +| Factory | Quando usar | +|---|---| +| `criarBlocoStatementSimples` | Ação sem parâmetros: `mover()`, `girar()` | +| `criarBlocoStatementComDropdown` | Ação com opção: `virar('DIREITA' \| 'ESQUERDA')` | +| `criarBlocoStatementComValor` | Ação com input de valor: `imprimir(texto)`, `repetir(n)` | +| `criarBlocoExpressaoSimples` | Condição sem parâmetros: `aindaTemSujeira()` | +| `criarBlocoExpressaoComDropdown` | Condição com opção: `caminhoBloqueado('FRENTE')` | +| `criarBlocoCondicional` | Bloco Se/Faça | +| `criarBlocoNegacao` | Bloco NÃO | + +### Blocos nativos do Blockly (não precisam de `defineBlocks`) + +Estes blocos estão disponíveis após `import "blockly/blocks"` — adicione-os direto em `ESTRUTURA_TOOLBOX` e em `allowedBlocks`: + +| ID | Bloco | +|---|---| +| `controls_repeat_ext` | Repetir N vezes | +| `controls_whileUntil` | Enquanto / Até | +| `math_number` | Número literal | +| `text` | Texto literal (string) | +| `text_join` | Juntar textos | +| `robo_if` / `robo_if_else` | Se / Se-Senão (customizados da plataforma) | + +--- + +## Passo 4 — `ui/constants.js` e `ui/layout.js` + +Mantenha **todo código visual fora do `game.js`**. A cena deve ser legível — quem a lê não deve precisar vasculhar criação de objetos Phaser. + +**`ui/constants.js`** — valores que nunca mudam: + +```javascript +// Assets em /public — URL direta, sem import de módulo +export const Assets = { + CHAVES: { PERSONAGEM: "meu_personagem" }, + PATHS: { PERSONAGEM: "/img/meu-personagem.png" }, +}; + +export const Constantes = { CELL_SIZE: 80, COLS: 6, ROWS: 6 }; + +export const Cores = { FUNDO: 0x1a1a2e, ALVO: 0x69f0ae }; +``` + +:::info +**Assets em `/public` vs. `src/`** + +Arquivos em `public/` (ex: `public/img/logo.png`) são servidos diretamente pelo servidor web. Referencie-os com a string da URL: `"/img/logo.png"`. +Arquivos em `src/atividades/.../assets/` devem ser importados como módulos: `import img from "./assets/img.png"`. O Vite os processa e otimiza durante o build. +::: + +**`ui/layout.js`** — funções que constroem a cena: + +```javascript +import { Assets, Constantes, Cores } from "./constants.js"; + +export function montarCenario(scene, cols, rows) { /* ... */ } +export function criarPersonagem(scene, col, row) { /* ... */ } +export function criarAlvo(scene, col, row) { /* ... */ } +``` + +--- + +## Passo 5 — `hooks/setupMeuJogoAPI.js` + +A **ponte** entre o código do aluno (rodando no JS-Interpreter) e a cena Phaser. + +```javascript +import { ApiHelpers } from "../../../../interpreters/ApiHelpers.js"; + +export const setupMeuJogoAPI = (scene, config) => { + const delay = config?.animationSpeed || 200; + + // Retorna a função de init do js-interpreter + return function(interpreter, globalScope) { + + // Ações com animação → createAsyncFunction (aguarda callback para continuar) + ApiHelpers.registerFunction( + interpreter, globalScope, + "executarAcaoA", + ApiHelpers.createActionWrapper(scene, "executarAcaoA", delay), + true // isAsync = true + ); + + // Condições síncronas → createNativeFunction (retorna valor imediatamente) + ApiHelpers.registerFunction( + interpreter, globalScope, + "verificarCondicao", + ApiHelpers.createConditionWrapper(scene, "verificarCondicao"), + false // isAsync = false + ); + + // Obrigatório para o highlight de blocos durante a execução + ApiHelpers.registerFunction( + interpreter, globalScope, + "highlightBlock", + ApiHelpers.createHighlightWrapper(scene), + false + ); + }; +}; +``` + +**Regra de ouro:** +- Funções que **animam algo** → `createAsyncFunction` (`isAsync: true`) +- Funções que **retornam um valor** (sensores, condições) → `createNativeFunction` (`isAsync: false`) + +--- + +## Passo 6 — `game.js` + +A cena Phaser. Herda `BaseGameScene` e implementa os hooks do ciclo de execução. + +```javascript +import Phaser from "phaser"; +import { BaseGameScene } from "../../../shared/BaseGameScene.js"; +import { setupMeuJogoAPI } from "./hooks/setupMeuJogoAPI.js"; +import { validationSolution } from "./validation/validators.js"; +import { gameConfig } from "./config/config.js"; +import { Assets, Constantes } from "./ui/constants.js"; +import { montarCenario, criarPersonagem, criarAlvo } from "./ui/layout.js"; + +export class MeuJogoScene extends BaseGameScene { + constructor() { + super("MeuJogoScene"); + // Estado lógico do jogo (NÃO posição visual — isso é do Phaser) + this.posicaoLogica = { x: 0, y: 0 }; + this.executionStopped = false; + } + + preload() { + this.preloadGlobalAssets(); // sons globais de sucesso/falha + this.load.image(Assets.CHAVES.PERSONAGEM, Assets.PATHS.PERSONAGEM); + } + + create() { + // 1. Cria o validador com referência à cena (para ler estado em tempo de validação) + this.validatorFunc = (historico) => + validationSolution(historico, this.configFase, gameConfig, this); + + // 2. Conecta o interpreter e os handlers de run/reset ao gameEventBus + this.setupStandardController( + () => setupMeuJogoAPI(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; + // Animação de vitória antes do modal aparecer + return new Promise(resolve => { + this.tweens.add({ targets: this.personagemSprite, /* ... */, onComplete: resolve }); + }); + } + + async onFailure() { + this.isRunning = false; + // Animação de falha antes do modal aparecer + return new Promise(resolve => { + this.tweens.add({ targets: this.personagemSprite, /* ... */, onComplete: resolve }); + }); + } + + montarFase() { + if (this.personagemSprite) this.personagemSprite.destroy(); + // ... destroy outros sprites + this.personagemSprite = criarPersonagem(this, 0, 0); + this.executionStopped = false; + } + + // --- API exposta ao interpreter --- + + executarAcaoA() { + if (this.executionStopped) return Promise.resolve(); + // Lógica da ação, registro no historico, animação... + this.historico.push({ tipo: "acao_a" }); + return this._animarPersonagem(() => this._checarCondicaoFim()); + } + + _checarCondicaoFim() { + if (this.executionStopped) return; + // ... verifica sucesso ou falha ... + // Sucesso detectado mid-execution: + this.executionStopped = true; + this.gameInterpreter.stopInternal(); // para sem marcar como parado pelo usuário + this.time.delayedCall(300, () => this.handleValidation(this.validatorFunc)); + // Falha detectada mid-execution: + // this.executionStopped = true; + // this.gameInterpreter.stopInternal(); + // this.time.delayedCall(100, () => this.handleFailure("Mensagem de falha")); + } +} + +export const createGame = (elementoPai, configFaseAtual) => { + const scene = new MeuJogoScene(); + return { + type: Phaser.AUTO, + width: /* largura */, + height: /* altura */, + parent: elementoPai, + scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, + scene, + callbacks: { + // Injeta configs no registry antes do boot → BaseGameScene.init() as lê + preBoot: (game) => { + game.registry.set("configFase", configFaseAtual); + game.registry.set("gameConfig", gameConfig); + }, + }, + }; +}; +``` + +### Quando usar `stopInternal()` vs. deixar o interpreter terminar + +| Cenário | Abordagem | +|---|---| +| Condição de fim detectada **durante** uma ação (ex: chegou ao alvo) | `stopInternal()` + `handleValidation()` com delay | +| Condição de falha detectada **durante** uma ação (ex: saiu da tela) | `stopInternal()` + `handleFailure()` com delay | +| Execução termina naturalmente (todos os blocos rodaram) | Nada — `BaseGameScene` agenda `handleValidation()` automaticamente | + +--- + +## Passo 7 — `validation/validators.js` + +```javascript +import { BaseGameValidator } from "../../../../shared/BaseGameValidator"; + +export class MeuJogoValidator extends BaseGameValidator { + validatePhase(history, config, gameConfig, sceneRef) { + // sceneRef é a cena Phaser — leia o estado lógico direto dela + const { posicaoLogica } = sceneRef; + const alvo = config?.alvo; + + if (posicaoLogica.x === alvo.x && posicaoLogica.y === alvo.y) { + return this.success(); + } + + return this.failure( + gameConfig?.mensagens?.naoAlcancou || "Não chegou ao destino. Tente de novo!" + ); + } +} + +export function validationSolution(history, config, gameConfig, sceneRef) { + return new MeuJogoValidator().validate(history, config, gameConfig, sceneRef); +} +``` + +`BaseGameValidator.validate()` já executa antes do seu código: +1. Verifica se a config da fase existe. +2. Verifica se o `historico` tem ao menos uma ação (evita validação de tela em branco). +3. (Opcional) Verifica sequência exata via `configFase.expectedSequence`. + +Implemente apenas as **regras específicas** do seu jogo em `validatePhase()`. + +--- + +## Passo 8 — `MeuJogoGame.jsx` + +O componente React de entrada segue sempre o mesmo padrão: + +```jsx +import React, { 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"; + +function MeuJogoContent() { + const { setFailureMessage } = useGameState(); + + useEffect(() => { registerBlocks(); }, []); + + const toolboxGenerator = useMemo( + () => (allowedBlocks) => generateDynamicToolbox(allowedBlocks), + [] + ); + + return ( + + + + + + ); +} + +export default function MeuJogoGame() { + return ( + + + + ); +} +``` + +--- + +## Passo 9 — Registrar a rota em `App.jsx` + +```jsx +// app/src/App.jsx + +// 1. Adicionar o lazy import com os outros jogos: +const MeuJogoGame = lazy(() => import("./atividades/programacao/meu-jogo/MeuJogoGame")); + +// 2. Adicionar a rota dentro de : +} /> +``` + +--- + +## Checklist final + +Antes de considerar a atividade pronta, verifique: + +- [ ] A atividade roda sem erros no console do navegador +- [ ] O fluxo de **Sucesso** dispara o modal corretamente +- [ ] O fluxo de **Falha** dispara o modal com a mensagem certa +- [ ] O botão **Reset** restaura o estado visual e lógico da fase +- [ ] O `timeout` da fase é suficiente para o aluno executar a solução correta +- [ ] O `maxBlocks` é suficiente para a solução ótima, mas não para a solução "preguiçosa" +- [ ] As mensagens de erro são compreensíveis para um aluno iniciante +- [ ] A rota está registrada em `App.jsx` +- [ ] O código visual (criação de sprites, cores, fontes) está em `ui/`, não em `game.js` \ No newline at end of file diff --git a/docs/docs/crie-suas-atividades/usando-os-exemplos.md b/docs/docs/crie-suas-atividades/usando-os-exemplos.md new file mode 100644 index 0000000..eee30cb --- /dev/null +++ b/docs/docs/crie-suas-atividades/usando-os-exemplos.md @@ -0,0 +1,108 @@ +--- +sidebar_position: 2 +title: "Usando os Exemplos" +--- + +As atividades `Exemplo` e `Exemplo 2` foram criadas especificamente como **código de referência comentado** para novos desenvolvedores. Elas implementam o fluxo arquitetural completo na sua forma mais simples possível. + +## Atividade Exemplo — movimento em grade + +**Rota:** `/atividades/programacao/exemplo` +**Código:** `app/src/atividades/programacao/exemplo/` + +O aluno move o logo da Decoda em uma grade 5×5 até um alvo verde usando blocos de movimento e repetição. + +``` +Conceito ensinado: Sequenciamento + Laços de Repetição +Blocos: mover para DIREITA, mover para BAIXO, repetir N vezes, número +Limite: 6 blocos → força o uso de loops em vez de repetição manual +Sucesso: sprite atinge a célula (4,4) +Falha: sprite sai dos limites da grade +``` + +### O que cada arquivo demonstra + +| Arquivo | O que ensina ao desenvolvedor | +|---|---| +| `ui/constants.js` | Como definir assets, cores e dimensões separados do código de jogo | +| `ui/layout.js` | Como criar grade, alvo e sprite em funções isoladas (o `game.js` fica limpo) | +| `game.js` — `preload()` | Como carregar um asset de `/public` via URL: `this.load.image(chave, "/img/logo.png")` | +| `game.js` — `_checarAlvo()` | Como detectar condição de sucesso **durante** a execução e usar `stopInternal()` | +| `game.js` — `_falharSaida()` | Como detectar condição de falha e acionar `handleFailure()` antes que o interpreter termine | +| `hooks/setupExemploAPI.js` | Como registrar funções **assíncronas** no JS-Interpreter via `createAsyncFunction` | +| `validation/validators.js` | Como estender `BaseGameValidator` e ler estado da cena via `sceneRef` | + +### Solução de referência + +```javascript +// Solução ótima — exatamente 6 blocos +repetir(4) { moverDireita(); } +repetir(4) { moverBaixo(); } +``` + +--- + +## Atividade Exemplo 2 — texto + +**Rota:** `/atividades/programacao/exemplo2` +**Código:** `app/src/atividades/programacao/exemplo2/` + +O aluno usa o bloco `imprimir` com texto (literal ou concatenado) para exibir exatamente `"SOBERANIA DIGITAL"` no display. + +``` +Conceito ensinado: Strings e Concatenação de texto +Blocos: imprimir, texto (literal), juntar texto +Limite: 4 blocos → comporta tanto a solução simples quanto a com concatenação +Sucesso: scene.textoAtual === "SOBERANIA DIGITAL" +Falha: qualquer outro texto impresso +``` + +### O que cada arquivo demonstra + +| Arquivo | O que ensina ao desenvolvedor | +|---|---| +| `ui/constants.js` | Estilos de texto Phaser centralizados (fontes, cores) | +| `ui/layout.js` | Como criar um display e **retornar a referência** para a cena atualizá-la | +| `hooks/setupExemplo2API.js` | Como registrar funções **síncronas** via `createNativeFunction`, e como extrair valores de string do JS-Interpreter | +| `game.js` — `imprimir()` | Exemplo de API que atualiza estado interno (`textoAtual`) e visual (`textoDisplay`) ao mesmo tempo | +| `validation/validators.js` | Validação por comparação direta de valor de estado (sem percorrer `historico`) | + +### Soluções de referência + +```javascript +// Solução simples — 2 blocos +imprimir("SOBERANIA DIGITAL"); + +// Solução com concatenação — 4 blocos (ensina text_join) +imprimir(juntar("SOBERANIA", " DIGITAL")); +``` + +--- + +## Como usar os exemplos como base + +### Opção A — copiar e renomear + +1. Duplique a pasta `exemplo/` ou `exemplo2/` com o nome da sua atividade. +2. Renomeie os arquivos (ex: `ExemploGame.jsx` → `MeuJogoGame.jsx`). +3. Faça um `Find & Replace` de `"exemplo"` → `"meu-jogo"` e `ExemploScene` → `MeuJogoScene` em todos os arquivos. +4. Ajuste `config.js` com os dados da sua atividade. +5. Registre a rota em `app/src/App.jsx`. + +### Opção B — seguir o passo a passo + +Use os exemplos como **resposta de gabarito** enquanto segue o [Passo a Passo](./passo-a-passo). Cada etapa do guia aponta para o arquivo correspondente no exemplo. + +--- + +## Arquivos que NÃO precisam mudar + +Estes fazem parte da infraestrutura da plataforma e são herdados automaticamente: + +- `BaseGameScene` — ciclo de execução completo +- `BaseGameValidator` — checks genéricos (histórico vazio, sequência esperada) +- `GameInterpreter` — execução segura do código do aluno +- `ApiHelpers` — utilitários para registrar funções no interpreter +- `GameBase`, `GameEditor`, `BlocklyEditor` — componentes React do shell do jogo +- `GameStateProvider` / `useGameState` — contexto de estado (fases, debug, falhas) +- `gameEventBus` — canal de comunicação Phaser → React \ No newline at end of file