Merge pull request 'add templates' (#5) from feature/template into main
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -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 = () => (
|
||||
<div
|
||||
@@ -99,6 +101,8 @@ function AppRoutes() {
|
||||
<Route path="/atividades/programacao/semaforo" element={<SemaforoGame />} />
|
||||
<Route path="/atividades/programacao/molemash" element={<MoleMashGame />} />
|
||||
<Route path="/atividades/programacao/turtle" element={<TurtleGame />} />
|
||||
<Route path="/atividades/programacao/exemplo" element={<ExemploGame />} />
|
||||
<Route path="/atividades/programacao/exemplo2" element={<ExemploGame2 />} />
|
||||
</Routes>
|
||||
|
||||
{/* Modal overlay routes — rendered on top of the background page */}
|
||||
|
||||
47
app/src/atividades/programacao/exemplo/ExemploGame.jsx
Normal file
47
app/src/atividades/programacao/exemplo/ExemploGame.jsx
Normal file
@@ -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 (
|
||||
<GameBase
|
||||
gameFactory={createGame}
|
||||
gameConfig={gameConfig}
|
||||
customFailureHandler={setFailureMessage}
|
||||
failureHandler={setFailureMessage}
|
||||
>
|
||||
<GameEditor>
|
||||
<BlocklyEditor toolboxGenerator={toolboxGenerator} />
|
||||
</GameEditor>
|
||||
</GameBase>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ExemploGame() {
|
||||
return (
|
||||
<GameStateProvider gameConfig={gameConfig}>
|
||||
<ExemploContent />
|
||||
</GameStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
ExemploContent.propTypes = {};
|
||||
ExemploGame.propTypes = {};
|
||||
58
app/src/atividades/programacao/exemplo/blocks/blocks.js
Normal file
58
app/src/atividades/programacao/exemplo/blocks/blocks.js
Normal file
@@ -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");
|
||||
};
|
||||
51
app/src/atividades/programacao/exemplo/config/config.js
Normal file
51
app/src/atividades/programacao/exemplo/config/config.js
Normal file
@@ -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!",
|
||||
},
|
||||
};
|
||||
194
app/src/atividades/programacao/exemplo/game.js
Normal file
194
app/src/atividades/programacao/exemplo/game.js
Normal file
@@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
};
|
||||
19
app/src/atividades/programacao/exemplo/ui/constants.js
Normal file
19
app/src/atividades/programacao/exemplo/ui/constants.js
Normal file
@@ -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,
|
||||
};
|
||||
66
app/src/atividades/programacao/exemplo/ui/layout.js
Normal file
66
app/src/atividades/programacao/exemplo/ui/layout.js
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
45
app/src/atividades/programacao/exemplo2/ExemploGame2.jsx
Normal file
45
app/src/atividades/programacao/exemplo2/ExemploGame2.jsx
Normal file
@@ -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 (
|
||||
<GameBase
|
||||
gameFactory={createGame}
|
||||
gameConfig={gameConfig}
|
||||
customFailureHandler={setFailureMessage}
|
||||
failureHandler={setFailureMessage}
|
||||
>
|
||||
<GameEditor>
|
||||
<BlocklyEditor toolboxGenerator={toolboxGenerator} />
|
||||
</GameEditor>
|
||||
</GameBase>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ExemploGame2() {
|
||||
return (
|
||||
<GameStateProvider gameConfig={gameConfig}>
|
||||
<ExemploContent2 />
|
||||
</GameStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
ExemploContent2.propTypes = {};
|
||||
ExemploGame2.propTypes = {};
|
||||
54
app/src/atividades/programacao/exemplo2/blocks/blocks.js
Normal file
54
app/src/atividades/programacao/exemplo2/blocks/blocks.js
Normal file
@@ -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})`
|
||||
);
|
||||
};
|
||||
43
app/src/atividades/programacao/exemplo2/config/config.js
Normal file
43
app/src/atividades/programacao/exemplo2/config/config.js
Normal file
@@ -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}".`,
|
||||
},
|
||||
};
|
||||
117
app/src/atividades/programacao/exemplo2/game.js
Normal file
117
app/src/atividades/programacao/exemplo2/game.js
Normal file
@@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
};
|
||||
17
app/src/atividades/programacao/exemplo2/ui/constants.js
Normal file
17
app/src/atividades/programacao/exemplo2/ui/constants.js
Normal file
@@ -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" },
|
||||
};
|
||||
34
app/src/atividades/programacao/exemplo2/ui/layout.js
Normal file
34
app/src/atividades/programacao/exemplo2/ui/layout.js
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
14
docs/docs/crie-suas-atividades/_category_.json
Normal file
14
docs/docs/crie-suas-atividades/_category_.json
Normal file
@@ -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."
|
||||
}
|
||||
}
|
||||
216
docs/docs/crie-suas-atividades/game-design.md
Normal file
216
docs/docs/crie-suas-atividades/game-design.md
Normal file
@@ -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 |
|
||||
73
docs/docs/crie-suas-atividades/intro.md
Normal file
73
docs/docs/crie-suas-atividades/intro.md
Normal file
@@ -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.
|
||||
465
docs/docs/crie-suas-atividades/passo-a-passo.md
Normal file
465
docs/docs/crie-suas-atividades/passo-a-passo.md
Normal file
@@ -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/<nome-da-atividade>/`:
|
||||
|
||||
```
|
||||
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 (
|
||||
<GameBase
|
||||
gameFactory={createGame}
|
||||
gameConfig={gameConfig}
|
||||
customFailureHandler={setFailureMessage}
|
||||
failureHandler={setFailureMessage}
|
||||
>
|
||||
<GameEditor>
|
||||
<BlocklyEditor toolboxGenerator={toolboxGenerator} />
|
||||
</GameEditor>
|
||||
</GameBase>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MeuJogoGame() {
|
||||
return (
|
||||
<GameStateProvider gameConfig={gameConfig}>
|
||||
<MeuJogoContent />
|
||||
</GameStateProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 <Routes>:
|
||||
<Route path="/atividades/programacao/meu-jogo" element={<MeuJogoGame />} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
108
docs/docs/crie-suas-atividades/usando-os-exemplos.md
Normal file
108
docs/docs/crie-suas-atividades/usando-os-exemplos.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user