ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
Jogos educativos de Pensamento Computacional para o Ensino Fundamental I, alinhados à BNCC.
Translation pending
EducationTypeScript · HTMLGitHub @JonataOliveiraaOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Ambiente de Desenvolvimento de Jonata da Glória (games-atesteme)
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Plataforma de Jogos Educativos - BNCC Computacao EF I Plataforma web de jogos educativos para Computacao no Ensino Fundamental I, organizada por habilidades da BNCC Computacao, com arquitetura React + Phaser e comunicacao tipada entre plataforma e jogos. O escopo curricular do projeto contempla 135 jogos/fases jogaveis, derivados de 45 entradas curriculares com progressao em tres niveis: N1, N2 e N3. Cada entrada curricular possui uma experiencia com dificuldade crescente, pensada para cobrir reconhecimento, aplicacao guiada e resolucao autonoma.Read the full README ↗
Plataforma web de jogos educativos para Computacao no Ensino Fundamental I, organizada por habilidades da BNCC Computacao, com arquitetura React + Phaser e comunicacao tipada entre plataforma e jogos.
O escopo curricular do projeto contempla 135 jogos/fases jogaveis, derivados de 45 entradas curriculares com progressao em tres niveis: N1, N2 e N3. Cada entrada curricular possui uma experiencia com dificuldade crescente, pensada para cobrir reconhecimento, aplicacao guiada e resolucao autonoma.
Publico-alvo: estudantes do 1o ao 5o ano do Ensino Fundamental.
A aplicacao e uma plataforma React que lista jogos, controla progresso, pontos, vidas, bloqueios e ranking local. Cada jogo roda em Phaser 3, isolado em sua propria pasta, com cenas, dados, assets e GameConfig independente.
A arquitetura atual separa tres conceitos que antes ficavam misturados:
| Conceito | Uso |
|---|---|
id | Identidade interna estavel do jogo. Usado em progresso, vidas, bloqueios, ranking e historico. |
slug | Identidade publica da URL /jogos/:slug. Pode mudar com alias de compatibilidade. |
module | Caminho fisico do jogo em src/games/<skill>/<slug>/index.ts. |
skill | Codigo curricular BNCC, como EF03CO08. E uma tag pedagogica, nao a identidade interna. |
Regra principal: estado interno usa game.id; URL publica usa game.slug; carregamento usa game.module.
O projeto trabalha com 135 experiencias jogaveis na progressao curricular:
| Grupo curricular | Entradas | Progressao | Total jogavel |
|---|---|---|---|
| 1o ano | 7 | N1, N2, N3 | 21 |
| 2o ano | 6 | N1, N2, N3 | 18 |
| 3o ano | 9 | N1, N2, N3 | 27 |
| 4o ano | 8 | N1, N2, N3 | 24 |
| 5o ano | 11 | N1, N2, N3 | 33 |
| Transversal 1o-5o | 4 | N1, N2, N3 | 12 |
| Total | 45 | 3 niveis | 135 |
Codigos curriculares cobertos, sem listar nomes individuais dos jogos:
EF01CO01 a EF01CO07
EF02CO01 a EF02CO06
EF03CO01 a EF03CO09
EF04CO01 a EF04CO08
EF05CO01 a EF05CO11
EF15CO01 a EF15CO04
Eixos pedagogicos principais:
| Camada | Tecnologia |
|---|---|
| Plataforma | React 19 + TypeScript 6 |
| Build | Vite 8 |
| Jogos | Phaser 3.90 |
| Rotas | React Router DOM 7 |
| Estado | React Context + localStorage |
| Assets | Imports Vite em src/assets/games |
| Comunicacao | Bridge tipada por CustomEvent ou postMessage |
npm install
npm run build
npm run lint
npm run preview
Durante desenvolvimento, o projeto tambem possui:
npm run dev
Use apenas quando for testar localmente. Para manutencao de codigo e documentacao, o fluxo recomendado e editar e validar com npm run build.
src/
assets/
games/
<skill>/<slug>/ # Imagens, capas, fundos e sprites do jogo
components/ # Componentes React reutilizaveis
context/ # Estado global da plataforma
data/
catalog.ts # Fonte canonica do catalogo
gameIndex.ts # Indices, resolvers e lazy loading dos jogos
games.ts # Reexport/compatibilidade do catalogo atual
games/
<skill>/<slug>/
index.ts # Export default Phaser.Types.Core.GameConfig
types.ts # Tipos especificos do jogo
data/ # Niveis, missoes, desafios, blocos, layouts
scenes/
BootScene.ts # Preload de assets e tela de carregamento
GameScene.ts # Logica principal do jogo
UIScene.ts # HUD separado quando necessario
pages/ # Telas React da plataforma
platform/components/
GameFrame.tsx # Escolhe modo local ou iframe
GameLauncher.tsx # Monta Phaser local e envia START_GAME
IframeGameFrame.tsx # Renderiza iframe e usa postMessage
PhaserCanvas.tsx # Cria/destroi instancia Phaser.Game
shared/
bridge/ # Bridges local/iframe/runtime
contracts/ # Tipos de comandos e eventos
tutorial/ # Tutorial compartilhado para cenas Phaser
types/ # Tipos compartilhados
utils/ # Persistencia auxiliar
styles/
global.css
src/data/catalog.ts e a fonte unica de verdade dos jogos. Cada entrada deve ter:
{
id: "046",
slug: "novo-jogo",
module: "EF15CO02/novo-jogo",
skill: "EF15CO02",
years: [1, 2, 3, 4, 5],
tags: ["algoritmos"],
order: 460,
status: "published",
title: "Novo Jogo",
description: "Descricao curta para o catalogo.",
category: "Pensamento Computacional",
points: 60,
thumbnail,
}
Regras de manutencao:
id e imutavel e nao deve ser reaproveitado.slug pode mudar, mas o valor antigo deve entrar em aliases.module deve apontar para uma pasta real em src/games.order controla exibicao; nao use id para ordenar curriculo.skill agrupa por habilidade BNCC, mas nao deve ser usado como chave de progresso.src/data/gameIndex.ts cria indices por id, slug e skill, normaliza entradas legadas com resolveGameId() e carrega jogos por import.meta.glob("../games/*/*/index.ts").
/jogos/:slug.GameDetailsPage resolve o jogo com getGameBySlug(slug).loadGameConfig(game) faz lazy load de src/games/<module>/index.ts.GameFrame decide entre jogo local ou iframe.GameLauncher ou IframeGameFrame envia START_GAME.GameContext normaliza gameId e persiste pontos, vidas, bloqueios e historico.Comandos da plataforma para o jogo:
type PlatformCommand =
| { type: "START_GAME"; gameId: string; points: number; stage: number }
| { type: "PAUSE_GAME" }
| { type: "RESUME_GAME" }
| { type: "UNLOCK_GAME"; gameId: string }
Eventos do jogo para a plataforma:
type PlatformEvent =
| { type: "GAME_READY"; gameId: string }
| { type: "CHECKPOINT"; gameId: string; progress: number; score: number; stage: number; hits?: number; errors?: number }
| { type: "CORRECT_ANSWER"; gameId: string; pointsEarned: number; stage: number }
| { type: "WRONG_ANSWER"; gameId: string; pointsEarned: number; stage: number }
| { type: "GAME_OVER"; gameId: string; stage: number }
| { type: "GAME_COMPLETED"; gameId: string; stage: number }
Mensagens iframe:
type IframePlatformCommandMessage = {
channel: "platform-command"
payload: PlatformCommand
}
type IframePlatformEventMessage = {
channel: "platform-event"
payload: PlatformEvent
}
O jogo nao deve depender diretamente do React. Dentro do Phaser, use runtimeGameBridge:
runtimeGameBridge.emit({
type: "CORRECT_ANSWER",
gameId: GAME_ID,
stage: currentLevel,
pointsEarned: 20,
})
Para comandos:
this.unsubscribe = runtimeGameBridge.onCommand((command) => {
if (command.type !== "START_GAME") return
if (command.gameId !== GAME_ID) return
this.startLevel(command.stage)
})
A bridge escolhe automaticamente o transporte:
CustomEvent no mesmo documento;window.postMessage com channel e payload.A plataforma tambem aceita eventos de jogos legados que ainda emitem slug/codigo antigo e normaliza para id estavel antes de gravar estado.
Cada jogo deve exportar um GameConfig:
const Config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 1280,
height: 720,
backgroundColor: "#000000",
scene: [BootScene, GameScene, UIScene],
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
}
export default Config
Recomendações:
BootScene carrega assets e inicia GameScene.GameScene contem regra, interacao, validacao e eventos de progresso.UIScene deve ser usada para HUD persistente, timer, tutorial ou controles globais quando fizer sentido.src/shared/tutorial/createTutorial.ts para tutoriais, evitando implementacoes isoladas por jogo.Graphics simples em Phaser.O estado da plataforma inclui:
A identidade persistida deve ser sempre o id estavel. Slugs e codigos BNCC sao aceitos apenas como entrada legada e convertidos com resolveGameId().
Assets de jogos ficam em:
src/assets/games/<skill>/<slug>/
Padrao recomendado:
cover-*.png
bg-*.png
item-*.png
character-*.png
icon-*.png
ui-*.png
effect-*.png
Como os assets sao importados por TypeScript/Vite, cada BootScene deve importar os arquivos reais e registrar chaves Phaser estaveis:
import bgUrl from "../../../../assets/games/EF03CO08/estudio-multiformato/bg-format-workshop.png"
this.load.image("bg-format-workshop", bgUrl)
Desde ago/2026 sao DUAS rotas, com um comportamento cada:
| Rota | O que e |
|---|---|
/jogos/<slug> | so o canvas. Sem cabecalho, menu, titulo, capa ou botao. Existe para ser o src de um iframe. |
/iframe/<slug> | a plataforma de jogos: pontos, vidas, modais, "Iniciar" e "Instrucoes" — e o jogo desenhado num iframe apontado para a rota de cima. |
A lista de jogos leva para /iframe/<slug>. A Atesteme aponta o iframe dela
para /jogos/<slug>.
Antes /jogos/:slug era uma rota com DOIS rostos, escolhidos por ?embed=1.
Um endereco com dois comportamentos e facil de escrever e dificil de confiar:
"abri o link e veio diferente" vira investigacao toda vez, e quem integra nunca
sabe se esta vendo o que o outro lado vai ver. A query agora traz o CONTEXTO da
partida — ela ajusta a partida, nao troca a pagina.
E a MESMA montagem dos dois lados. A diferenca e o parametro:
| Quem embute | Query | Fim da partida |
|---|---|---|
| A Atesteme | ?embed=1&id=<tentativa>&returnBase=<origem> | navega para <returnBase>/approve ou /reprove |
/iframe/<slug> | ?embed=1&inline=1 | nao navega: os pontos e modais sao da pagina hospedeira, e o resultado chega por evento |
Por que fizemos isso. Enquanto o Phaser era montado ao lado da pagina, o caminho do iframe so era percorrido pelas paginas de teste — e caminho que ninguem percorre no dia a dia quebra em silencio. Agora ele e o caminho normal: se o embed quebrar, quebra primeiro aqui, para nos, e nao na apresentacao.
| Arquivo | Papel |
|---|---|
platform/components/IframeGameFrame.tsx | monta o iframe, valida a origem, espera GAME_READY e so entao manda START_GAME |
platform/embed/embedParams.ts | le inline junto com o resto da query, num lugar so |
shared/bridge/uiTunnel.ts | faz o EventBus atravessar a parede do iframe |
Os eventos da partida (GAME_READY, CHECKPOINT, GAME_COMPLETED…) ja
atravessavam: e o contrato platform-event. O que NAO atravessava eram os
sinais de interface que o jogo e a pagina trocavam pelo EventBus — exit-game,
close-game-modals, o modal de vida extra do Pixel Secreto. EventBus e um
emissor em memoria, e memoria nao passa de uma janela para outra.
O tunel espelha os dois EventBus por um terceiro canal (game-ui). O ganho
esta no que NAO mudou: a GameDetailsPage continua com o EventBus.on("exit-game")
dela intacto, e os 45 jogos continuam com o EventBus.emit("exit-game") deles.
Nenhum dos dois sabe que ha uma parede no meio — foi o que permitiu mover o jogo
para dentro do iframe sem encostar em nenhum jogo.
A lista de sinais que atravessam e FECHADA. A maior parte do trafego do
EventBus e conversa interna do jogo (timer-tick, mission-update, sparks —
mais de trinta nomes), com as duas pontas dentro do mesmo Phaser; esses
continuam funcionando sozinhos e nao tem por que virar postMessage.
game-ui nao faz parte do contrato da plataforma. Quem embute de fora pode
ignora-lo inteiro sem perder nada.
YDesligado por padrao. Y liga e desliga, e avisa qual dos dois aconteceu
("🔔 ATIVADO" / "🔕 DESATIVADO"). Ligado, platform/components/EventMonitor.tsx
da um alert() quando o jogo anuncia que ficou pronto, que venceu ou
que perdeu:
🏆 GAME_COMPLETED — VENCEU O JOGO (ultima fase)
fase 3 de 3
pontos: 120
tentativa: inline-abc
jogo: 035
Existe porque esse trafego e invisivel: quando um evento nao sai, a tela
simplesmente nao reage, e "nao reagiu" tem uma duzia de causas. Foi assim com o
jogo travando ao errar — a plataforma nunca recebia o GAME_OVER, e nada na
tela dizia isso. Um alerta que NAO aparece responde a pergunta na hora.
GAME_READY, GAME_COMPLETED e GAME_OVER. CHECKPOINT,
CORRECT_ANSWER e WRONG_ANSWER ficam de fora porque disparam a cada
jogada — um alert() por acerto tornaria o jogo injogavel. Todos os
eventos saem no console como [evento local|iframe]./jogos/<slug> e aberto
direto) e o postMessage (quando o jogo esta no iframe de /iframe/<slug>).App
roda duas vezes; sem esse corte cada evento daria dois alertas — e o Y
alternaria duas vezes, voltando ao ponto de partida (ou seja: "nao acontece
nada").keydown passa a ser entregue a janela DE DENTRO do iframe, e um ouvinte so
aqui fora para de ver a tecla — o motivo mais provavel de o Y ter falhado
na primeira versao. O ouvinte e registrado na fase de CAPTURA da window de
cada documento (a primeirissima etapa do trajeto, antes de o Phaser ver a
tecla), e uma varredura a cada 700ms religa quando o iframe e trocado.alert() sai num setTimeout(0): congelar dentro do tratamento da
mensagem seguraria o postMessage no meio do caminho.Em desenvolvimento a tecla vale sempre. Em producao, so com ?debug=1 na URL —
um alert() na cara de uma crianca no meio da atividade seria pior que o
problema que ele veio investigar.
Houve aqui um painel lateral com historico e contagem por tipo. Saiu: era mais maquina do que a pergunta pedia.
MODO_DO_JOGO no topo de GameDetailsPage.tsx aceita "iframe" ou "local".
Trocar para "local" devolve a montagem antiga inteira, sem tocar em mais nada.
Esta ali de proposito: e uma linha de recuo se o iframe der problema perto de uma
data de entrega.
getAllowedOrigins() sempre inclui window.location.origin, alem do que vier
em VITE_EMBED_ALLOWED_ORIGINS. Nao e frouxidao: paginas de mesma origem ja
podem ler e escrever uma na outra sem postMessage nenhum, entao exigir
configuracao para o site falar consigo mesmo nao protege de nada.
E cobrava caro. O .env.local fixa http://localhost:5173; num dia em que a
5173 estava ocupada o Vite subiu na 5174, e o navegador passou a descartar
TODO evento da partida com "target origin does not match the recipient window's
origin". O sintoma nao parecia em nada com a causa: o jogo travava ao errar e
a tela de derrota nunca aparecia — porque a plataforma jamais recebia o
WRONG_ANSWER nem o GAME_OVER. Trocar de porta nao pode quebrar o jogo.
A variavel continua obrigatoria para quem embute de FORA (a Atesteme): sem a origem dela na lista, nenhum evento sai e nenhum comando entra.
postMessage exige um targetOrigin que bata com a origem real da janela de
destino. Como nao da para perguntar a origem de uma janela de outro dominio, a
saida era mandar uma mensagem por origem permitida — a certa e entregue, as
outras sao descartadas com um erro no console.
Quando a janela e da mesma origem da para saber: janela.location.origin e
legivel nesse caso e lanca SecurityError em qualquer outro. origensDeDestino()
usa isso para mandar UMA mensagem. Nao muda o que chega do outro lado; muda o
console, que antes virava uma parede de "Failed to execute 'postMessage'" a cada
evento da partida, varrendo para longe qualquer erro de verdade.
Esta secao descreve o que esta IMPLEMENTADO no repositorio. A secao seguinte, sobre Flutter, e uma proposta anterior e mais ampla; onde as duas divergirem, vale esta.
A plataforma Atesteme abre um jogo dentro de um iframe, passa o contexto por
query string e recebe o resultado de volta. A Atesteme e dona de aluno,
competencia, tentativa, pontuacao oficial, estrelas e bloqueios. Este site e
dono de renderizar o jogo e relatar o que aconteceu.
<iframe
src="https://jogos.atesteme.com/jogos/sistema-operacional?embed=1&attempt=6f1c…&stage=1&points=120&returnBase=https%3A%2F%2Fedu.atesteme.com"
width="100%" height="100%" frameborder="0"
allow="autoplay; fullscreen"
></iframe>
Sem embed=1, a mesma URL continua abrindo a pagina normal do jogo, com
pontuacao, vidas e ranking. Nada do site atual muda.
| Parametro | Tipo | Obrigatorio | Uso |
|---|---|---|---|
embed | 1 | sim, para o modo embed | Liga o modo embed. |
attempt | string opaca | sim | ID da tentativa na Atesteme. Volta como eco em meta.attempt; o site nao interpreta. |
returnBase | URL absoluta | sim | Origem que recebe o resultado. Precisa estar em VITE_EMBED_ALLOWED_ORIGINS. |
stage | 1 | 2 | 3 | nao (1) | Fase inicial. Fora do intervalo cai para 1. |
points | inteiro >= 0 | nao (0) | Pontuacao inicial exibida. So display. |
lives | — | nao | Ignorado. Aceito para nao quebrar iframes ja escritos; a tolerancia a erro e do jogo (ver "Aprovado e reprovado"). |
locale | BCP-47 | nao (pt-BR) | Idioma. |
id e aceito como sinonimo legado de attempt; se os dois vierem, attempt
vence.
Duas classes de parametro, duas reacoes. stage, points e
locale sao ajustes: valor invalido vira o padrao e a partida comeca.
attempt e returnBase sao o contexto: sem eles a tela mostra um erro
legivel e nao inicia — jogar uma partida que ninguem consegue creditar e
pior que uma mensagem de erro.
Tudo isso e lido e validado em src/platform/embed/embedParams.ts, num lugar
so. Nenhum jogo le a query.
Phaser.Scale.FIT que os 45 jogos ja usam.Para ver a pagina normal do mesmo jogo, tire o embed=1 da URL. Nao ha
atalho de teclado: um segundo jeito de a tela estar, ligado por uma tecla, e o
tipo de estado que ninguem lembra na hora de investigar um problema.
Envelope { channel: "platform-event", payload }, como antes. Todo evento que
sai em modo embed carrega meta:
type EventMeta = {
attempt: string; // eco exato do parametro recebido
gameId: string; // game.id do catalogo (ex.: "037"), nunca o slug
sentAt: number;
protocolVersion: 1;
};
GAME_COMPLETED ganhou dois campos:
{
type: "GAME_COMPLETED";
gameId: string;
stage: number;
totalStages?: number; // quantas fases o jogo tem
isFinalStage?: boolean; // true so na ultima
score?: number; errors?: number; durationMs?: number;
}
Por que: GAME_COMPLETED e emitido a cada fase, e sempre foi. Sem
isFinalStage, quem esta de fora nao distingue "terminou a fase 1" de
"terminou o jogo" — e aprovaria o aluno na primeira fase.
Os campos sao opcionais no tipo porque sao opcionais para quem emite e
garantidos para quem recebe: os 45 jogos continuam emitindo o que sabem, e
src/shared/bridge/outgoingEvent.ts completa meta, totalStages e
isFinalStage num lugar so, antes de o evento sair.
Ordem esperada numa partida:
GAME_READY → (CORRECT_ANSWER | WRONG_ANSWER | CHECKPOINT)* → GAME_COMPLETED(isFinalStage) | GAME_OVER
Comandos aceitos continuam START_GAME, PAUSE_GAME, RESUME_GAME e
UNLOCK_GAME, no envelope { channel: "platform-command", payload }.
Alem do evento, o fim da partida navega o proprio iframe:
aprovado: <returnBase>/approve?id=<attempt>
reprovado: <returnBase>/reprove?id=<attempt>
GAME_COMPLETED com isFinalStage: true → /approve.GAME_OVER → /reprove. A derrota e do jogo, e de mais ninguem: todo
jogo tolera um erro so. Errou, ou o tempo acabou, ele emite WRONG_ANSWER e
em seguida GAME_OVER, e para. Ninguem de fora conta erros, e nenhum
parametro muda essa tolerancia (shared/level/vidasIniciais.ts)./reprove — bloquear o jogo por 24 horas, cobrar uma
vida ou pontos para liberar antes — e decisao dela. O prototipo deste site
(/iframe/<slug>) faz exatamente isso em src/context/GameContext.tsx./approve ela acontece ~150ms depois./reprove a tela espera ~2s. O GAME_OVER sai por postMessage
na hora, mas o jogo fica mais 1,35s na tela mostrando o erro (toque ja
cortado), depois aparece "Fim de jogo" por 0,6s, e so entao o iframe navega.
Quem reage ao postMessage e quer que a crianca veja o erro deve esperar os
mesmos 1,35s antes de cobrir o iframe (ESPERA_PARA_VER_O_ERRO_MS, em
src/shared/bridge/partida.ts).window.location.assign, nunca window.top.returnBase fora da allowlist nao navega para lugar nenhum — sem essa
checagem a query vira um redirecionamento aberto.VITE_EMBED_ALLOWED_ORIGINS (lista separada por virgula) vale para as duas
pontas:
targetOrigin
explicito. Nunca "*" — com "*", qualquer pagina que embutisse este site
receberia o desempenho do aluno;event.origin antes de o
payload ser olhado, e so entao passam pelos type guards.Lista vazia nao e "liberado": e "nada entra e nada sai". Um deploy que esqueceu a variavel falha visivel, em vez de falhar aberto.
Copie o .env.example para .env.local antes de rodar local.
Os 45 jogos sao desenhados numa tela fixa de 1280x720: cada layout.ts tem
coordenadas absolutas (o botao em x=1134, a peca em y=495). O Phaser roda em
Scale.FIT, que mantem essa tela e a ESCALA para caber no container,
preservando a proporcao 16:9.
Consequencia: caixa que nao e 16:9 ganha faixa preta. Isso nao e bug, e o preco de o jogo ter formato proprio — e o preco de nao ter e alto:
| Modo do Phaser | O que faz | Serve aqui? |
|---|---|---|
FIT (atual) | Escala mantendo 16:9, sobra faixa | Sim. Coordenadas fixas continuam validas. |
RESIZE | O canvas vira o tamanho do container e o jogo recebe o novo tamanho | Nao. Exigiria reposicionar tudo a cada resize nos 45 jogos, que hoje escrevem posicao em numero absoluto. |
ENVELOP | Preenche a caixa mantendo a proporcao, CORTANDO o que sobra | Nao. Corta as bordas — e e nelas que moram o botao (y=670) e os nomes das pecas (y=592). |
Entao a alavanca nao e o modo de escala, e o formato da caixa. Numa caixa 16:9 a faixa e zero. Recomendacao para quem embute:
/* do lado da Atesteme */
.iframe-do-jogo {
width: 100%;
aspect-ratio: 16 / 9; /* faixa preta = zero */
border: 0;
}
Se a caixa precisar ter outro formato, o jogo continua jogavel e inteiro — so aparece faixa. O que nao acontece em nenhum caso e o jogo cortar ou deformar.
Para ver isso acontecendo com numero na mao, abra
/embed-sandbox.html: ele mostra o mesmo jogo numa caixa que voce
redimensiona (com presets de 16:9, 4:3, celular em pe, ultrawide) e le, de
dentro do iframe, o tamanho real do canvas, a escala aplicada, a faixa preta em
pixels e o aproveitamento da area.
cp .env.example .env.local # ja vem com http://localhost:5173
npm run dev
Duas paginas de teste, com propositos diferentes:
/embed-harness.html — o CONTRATO. Embute o iframe, monta a query, manda
START_GAME e loga tudo que chega. Se funciona ali e nao funciona na
plataforma, a diferenca esta do outro lado. A tecla Y esconde o painel e
deixa so o jogo; o botao "Abrir sozinho" abre a mesma URL numa aba limpa,
sem harness e sem iframe.
/embed-sandbox.html — o ENQUADRAMENTO. A mesma URL numa caixa que voce
redimensiona, com a medida do canvas lida de dentro do iframe.
/embed-remoto.html — o CONTRATO CONTRA UM SITE DE OUTRA ORIGEM. Igual ao
harness, mas com o endereco do site como campo: aponte para
https://games.atesteme.com (ou para o local) e veja a partida inteira.
Faz o aperto de mao sozinho (GAME_READY → START_GAME), acompanha
progresso, acertos e erros num painel, e diz o veredito. E o unico que
exercita origem cruzada — os outros dois so conversam com o proprio site.
Ele precisa ser SERVIDO por http: aberto por file:// a origem e null,
que nunca esta em allowlist nenhuma. Com npm run dev ele fica em
http://localhost:5174/embed-remoto.html.
E a origem dele precisa estar em VITE_EMBED_ALLOWED_ORIGINS no build do
site de destino. Se nao estiver, o jogo aparece, roda, e o painel fica
vazio para sempre — sem erro visivel. Por isso ele cronometra o silencio e
acusa depois de 12 segundos, citando as duas causas possiveis.
As duas primeiras passam returnBase = window.location.origin, porque e a unica origem
que elas tem e a unica que esta na allowlist local. Entao, no fim da partida, o
iframe navega para o /approve ou /reprove deste site — e por isso as
duas rotas existem no App.tsx, em src/pages/EmbedReturnPage.tsx. Elas se
anunciam como "retorno simulado": as de verdade sao da Atesteme, e e para la
que o iframe vai em producao.
Sem essas rotas o React Router nao casava nada, renderizava null, e vencer ou
perder terminava em tela branca — o jogo certo, o contrato certo, e o fim
da partida com cara de crash.
Site estatico proprio, em dominio proprio, consumido por iframe.
| Peca | Configuracao |
|---|---|
| S3 | Bucket privado. Sem website hosting, sem ACL publica. |
| CloudFront | Origin Access Control apontando para o bucket. |
| ACM | Certificado em us-east-1 (exigencia do CloudFront). |
| SPA | Custom error responses 403 e 404 → /index.html com status 200. Sem isso, recarregar /jogos/<slug> dentro do iframe da 404. |
| Cache | assets/* com public, max-age=31536000, immutable; index.html com no-cache. |
| Vite | base: '/' — o site fica na raiz do dominio de jogos. |
Header obrigatorio na resposta:
Content-Security-Policy: frame-ancestors https://edu.atesteme.com https://*.atesteme.com;
Ele restringe quem pode embutir; nao e o que permite o embed. Um servidor
que nao manda header nenhum ja aceita ser embutido — o header existe para que
so a Atesteme consiga. Nao usar X-Frame-Options: DENY/SAMEORIGIN — ele
nao aceita lista de origens e derruba a integracao. Configure via CloudFront
Response Headers Policy.
Na AWS ele e obrigatorio por outro motivo: a Response Headers Policy padrao do
CloudFront pode injetar X-Frame-Options, e ai o iframe fica em branco de
verdade.
HTTPS e obrigatorio dos dois lados: iframe http dentro de pagina https e
bloqueado como conteudo misto.
O pipeline esta em .github/workflows/deploy.yml: npm ci → npm run lint →
npm run build → s3 sync → invalidacao do index.html. Credenciais por
OIDC, nao por chave estatica. Os assets sobem antes do index.html de
propósito: assim todo HTML publicado ja encontra o que ele pede.
A Vercel pode seguir como preview; a AWS passa a ser o endereco oficial.
Colar https://games.atesteme.com/iframe/<slug> no WhatsApp mostra a capa do
jogo, o titulo e a descricao. Vale para os 45.
A restricao que decide tudo: o robo do WhatsApp NAO executa JavaScript. Ele
baixa o HTML, procura as <meta> e vai embora. Como isto e uma SPA que serve o
mesmo index.html para toda rota, qualquer solucao que escreva as tags depois
— React Helmet e parentes — chega tarde: o robo ja foi. O HTML precisa JA estar
certo quando o servidor responde.
Dai as duas pecas:
| Peca | O que faz | Quando roda |
|---|---|---|
scripts/gerar-og.mjs | capa (1672x941, ~2MB) → public/og/<slug>.jpg (1200x630, ~130KB) | a mao, e o resultado e commitado |
scripts/gerar-paginas-og.mjs | escreve dist/iframe/<slug>.html com as <meta> daquele jogo | no npm run build |
As capas tem cerca de 2 MB. O WhatsApp desiste da previa acima de uns 300 KB — e "desiste" quer dizer que o link aparece sem imagem nenhuma, sem erro e sem aviso. Os JPEGs gerados ficam entre 98 e 182 KB.
Redimensionar exige sharp, que traz binario nativo por plataforma — a mesma
familia de dependencia que quebrou o npm ci na integracao continua. Manter
isso longe do caminho do build e deliberado.
Quando uma capa mudar:
npm install --no-save sharp && npm run og:imagens
Commite public/og/ junto. O build so copia.
Uma funcao no Vercel resolveria, mas o plano e publicar como site estatico em S3 + CloudFront, onde funcao nao existe. Gerar no build funciona nos dois, nao custa nada em tempo de resposta e nao tem o que quebrar em producao.
O roteamento continua igual: o vercel.json manda tudo para /index.html, mas
o sistema de arquivos e consultado ANTES do rewrite, entao /iframe/<slug>
encontra o arquivo gerado. Como o conteudo e o mesmo index.html (mesmos
scripts, mesmos caminhos de asset), o React sobe igual.
Cada pagina e escrita em duas formas, iframe/<slug>.html e
iframe/<slug>/index.html: um servidor procura uma, outro procura a outra, e a
forma errada cai no fallback de SPA — o link funciona, mas a previa sai com o
titulo e a imagem do site inteiro. Aconteceu no vite preview (com barra final
vinha a certa, sem barra vinha a generica), e so aparece olhando o HTML cru,
porque a tela fica identica nos dois casos.
og:image com caminho relativo nao rende imagem nenhuma — o robo nao tem como
resolver. O padrao e https://games.atesteme.com; para outro dominio:
VITE_SITE_URL=https://outro.exemplo.com npm run build
Depois de publicar, os validadores oficiais mostram o que cada um enxerga (e o do Facebook tem um botao de limpar cache, util quando a previa fica velha):
O WhatsApp cacheia a previa por bastante tempo. Se mudar a imagem depois de
alguem ja ter colado o link, acrescente ?v=2 para forcar.
A Vercel e o endereco de preview, e e de la que a plataforma vai puxar o jogo
enquanto a AWS nao estiver de pe. O vercel.json na raiz tem uma linha so, e
ela resolve o unico problema que impede o embed de abrir:
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }
Por que isso e obrigatorio. A plataforma nao navega pela home: ela poe
https://<projeto>.vercel.app/jogos/<slug>?embed=1&… direto no src do
iframe. Isso e um deep link frio, e dist/jogos/<slug>/index.html nao existe —
quem resolve jogos/:slug e o React Router, no cliente (src/App.tsx). Sem o
rewrite a Vercel devolve 404 e o iframe mostra a pagina de erro dela. E o mesmo
problema que a AWS resolve com "403 e 404 → /index.html", na tabela acima.
O rewrite nao engole os arquivos estaticos: na Vercel o sistema de arquivos e
consultado antes, entao /embed-harness.html, /embed-sandbox.html,
/assets/* e /games/* continuam sendo servidos como sempre.
A segunda peca e uma variavel de ambiente, nao um arquivo.
VITE_EMBED_ALLOWED_ORIGINS precisa conter a origem da plataforma nas
Environment Variables do projeto na Vercel. Isso nao e endurecimento opcional:
validarEmbed confere o returnBase contra essa lista antes de montar o jogo,
e lista vazia significa "nada entra e nada sai" por decisao explicita — sem a
variavel, a crianca ve "Endereco de retorno nao autorizado" no lugar do jogo.
E como o prefixo e VITE_, ela e lida no build: o valor vira texto dentro
do pacote. Mudar a lista exige um redeploy; trocar a variavel no painel e
recarregar nao muda nada.
Opcional, e so quando alguem pedir: o frame-ancestors da secao anterior vale
aqui tambem, como bloco headers no mesmo vercel.json. A Vercel nao manda
X-Frame-Options por conta propria, entao o embed funciona sem ele — o header
serve para impedir que qualquer outro site embuta os jogos.
Uma solucao tecnica viavel e hospedar a plataforma de jogos como web build separado e integra-la ao Flutter por iframe. O Flutter fica dono de autenticacao, turma, usuario, sincronizacao remota e persistencia principal. O jogo web fica responsavel por renderizar e emitir eventos.
Flutter App
├─ Estado nativo: usuario, turma, token, tentativas, sincronizacao
├─ Web container
│ └─ iframe / WebView apontando para a plataforma web
└─ Bridge Dart <-> JavaScript por postMessage
Web Game Host
├─ React shell em modo embed
├─ GameFrame / IframeGameFrame
├─ Phaser Game
└─ runtimeGameBridge
A plataforma pode expor uma rota dedicada:
https://games.exemplo.com/embed/:slug?sessionId=<uuid>&level=1&locale=pt-BR
Parametros recomendados:
| Parametro | Uso |
|---|---|
sessionId | Correlaciona mensagens entre Flutter e iframe. |
level | Nivel inicial solicitado pelo Flutter. |
locale | Idioma/localizacao. |
embed=1 | Remove navegacao da plataforma e mostra somente o jogo. |
Flutter -> iframe:
{
"channel": "platform-command",
"payload": {
"type": "START_GAME",
"gameId": "023",
"stage": 1,
"points": 120
},
"meta": {
"sessionId": "b7f0...",
"sentAt": 1780000000000
}
}
iframe -> Flutter:
{
"channel": "platform-event",
"payload": {
"type": "CHECKPOINT",
"gameId": "023",
"stage": 2,
"progress": 0.66,
"score": 80,
"hits": 4,
"errors": 1
},
"meta": {
"sessionId": "b7f0...",
"sentAt": 1780000005000
}
}
O contrato atual ja usa channel e payload. O campo meta seria uma extensao recomendada para Flutter, porque facilita rastreio, segurança e correlacão de sessoes.
Em Flutter Web, o container pode registrar um HTMLIFrameElement e ouvir window.onMessage:
// Exemplo conceitual para Flutter Web.
// A implementacao real depende da versao do Flutter e do pacote usado.
final iframe = HTMLIFrameElement()
..src = 'https://games.exemplo.com/embed/estudio-multiformato?embed=1&sessionId=$sessionId'
..style.border = '0'
..allow = 'autoplay; fullscreen';
window.onMessage.listen((event) {
if (event.origin != 'https://games.exemplo.com') return;
final data = event.data;
if (data is! Map) return;
if (data['channel'] != 'platform-event') return;
final payload = data['payload'];
// Validar schema e persistir progresso no backend Flutter.
});
void sendStartGame() {
iframe.contentWindow?.postMessage({
'channel': 'platform-command',
'payload': {
'type': 'START_GAME',
'gameId': gameId,
'stage': stage,
'points': points,
},
'meta': {
'sessionId': sessionId,
'sentAt': DateTime.now().millisecondsSinceEpoch,
},
}, 'https://games.exemplo.com');
}
Em mobile, iframe puro nao e o primitive natural. A abordagem mais robusta e usar um WebView carregando uma pagina wrapper que contem o mesmo host web do jogo (?) O protocolo permanece igual, mas a ponte passa por JavaScript channel do WebView:
Flutter Mobile
-> WebView
-> pagina wrapper web
-> iframe do jogo ou montagem direta da rota embed
Duas opcoes:
runtimeGameBridge.targetOrigin e retransmite para o iframe do jogo.Para mobile, a opcao 1 tende a ter menos pontos de falha. Para web, iframe direto e suficiente.
targetOrigin: "*" em producao.event.origin e event.source.sessionId nas mensagens.channel e payload antes de persistir.GAME_READY e START_GAME.protocolVersion: 1 em meta.1. Flutter cria sessionId e abre iframe/embed.
2. Web emite GAME_READY quando Phaser terminou preload.
3. Flutter envia START_GAME.
4. Jogo emite CORRECT_ANSWER, WRONG_ANSWER, CHECKPOINT ou GAME_COMPLETED.
5. Flutter persiste no backend.
6. Flutter pode enviar PAUSE_GAME/RESUME_GAME conforme ciclo de app.
sentAt e sessionId.src/games/<skill>/<slug>/
src/assets/games/<skill>/<slug>/
index.ts
scenes/BootScene.ts
scenes/GameScene.ts
scenes/UIScene.ts
data/levels.ts
types.ts
// src/data/catalog.ts
{
id: "046",
slug: "novo-jogo",
module: "EF01CO01/novo-jogo",
skill: "EF01CO01",
order: 15,
status: "published",
...
}
npm run build
Nao adicionar mapas manuais de slug para loader. O carregamento deve continuar passando por gameIndex.ts.
Phaser.Scale.FIT.wordWrap controlado.Graphics; evitar emoji como UI.Build local:
npm run build
Saida:
dist/
O build e estatico e pode ser servido por qualquer hosting HTTP. Para integracao iframe/Flutter, o deploy deve configurar corretamente:
Content-Security-Policy permitindo o dominio Flutter host em frame-ancestors;index.html.Exemplo de CSP para permitir embed por um app web especifico:
Content-Security-Policy: frame-ancestors 'self' https://app.exemplo.com
docs/migracao-catalogo-ids.md: detalhes da migracao para IDs estaveis.docs/analise-arquitetura-jogos.md: analise arquitetural anterior do projeto.45 habilidades BNCC, 2 jogos por habilidade (duas rodadas de concepção):
src/games/. S? de instruções, sem narração, computador e celular.| Tag | Jogo | R | Gênero / mecânica |
|---|---|---|---|
| EF01CO01 | Base dos Classificadores | R1 | Classificação por arrastar e soltar |
| EF01CO01 | Corrida dos Parecidos | R2 | Corrida (endless runner) |
| EF01CO02 | Trilha do Passo a Passo | R1 | Execução sequencial guiada |
| EF01CO02 | Ritmo da Rotina | R2 | Ritmo / música |
| EF01CO03 | Oficina dos Algoritmos | R1 | Puzzle de ordenação lógica |
| EF01CO03 | Pulo Programado | R2 | Plataforma |
| EF01CO04 | Correio Multimídia | R1 | Simulação de transmissão de informação |
| EF01CO04 | Passe da Mensagem | R2 | Esporte |
| EF01CO05 | Pixel Secreto | R1 | Color by code / decodificação visual |
| EF01CO05 | Ilha dos Códigos | R2 | Aventura / exploração |
| EF01CO06 | Desktop Digital Infantil | R1 | Simulação de interface / sandbox orientado |
| EF01CO06 | Meu Bichinho Conectado | R2 | Simulador de vida |
| EF01CO07 | Guardiões dos Dados | R1 | Tomada de decisão contextual |
| EF01CO07 | Esconde-Dados | R2 | Stealth infantil |
| EF02CO01 | Hangar dos Modelos | R1 | Modelagem por filtros e comparação |
| EF02CO01 | Detetive dos Modelos | R2 | Point-and-click / mistério |
| EF02CO02 | Desfile do Robô Repetidor | R1 | Programação por blocos com repetição definida |
| EF02CO02 | Série do Campeão | R2 | Esporte |
| EF02CO03 | Fábrica de Máquinas | R1 | Seleção de máquina e sequência de comandos |
| EF02CO03 | Vila das Máquinas | R2 | RPG leve (micro-missões) |
Anos por prefixo da tag: EF01 = 1º ano · EF02 = 2º · EF03 = 3º · EF04 = 4º · EF05 = 5º · EF15 = 1º ao 5º.
| Onde | Divergência |
|---|---|
| EF05CO07 | A planilha R1 traz "Controlador do Sistema"; catalog.ts (id 037) usa "Sistema Operacional". |
| EF05CO11 | As planilhas grafam o código como EF05CO011 (3 dígitos). O correto é EF05CO11, como está em catalog.ts. |
arena-da-logica | GameDetailsPage.tsx lista o slug com acento (arena-da-lógica) em GAMES_WITH_IN_GAME_COMPLETION_SCREEN; o catálogo usa arena-da-logica. Não casa. |
mapas-em-rede | BootScene.create() chama this.scene.launch('UIScene'), mas o index.ts registra só [BootScene, GameScene] — a UIScene foi aposentada e o arquivo scenes/UIScene.ts ficou morto. O Phaser ignora a chamada, então não quebra; é lixo a remover. |
Fonte única de verdade do catálogo em código: src/data/catalog.ts.
| EF02CO04 | Museu Vivo do Computador | R1 | Pareamento conceitual |
| EF02CO04 | Fuga do Laboratório | R2 | Escape room individual |
| EF02CO05 | Cidade das Tecnologias | R1 | Escolha contextual de ferramenta |
| EF02CO05 | Corrida do Cotidiano | R2 | Corrida (endless runner) |
| EF02CO06 | Checklist do Jogador Seguro | R1 | Configuração segura por checklist |
| EF02CO06 | Bolhas da Segurança | R2 | FPS adaptado (mira, sem violência) |
| EF03CO01 | Tribunal do Verdadeiro ou Falso | R1 | Quiz lógico contextual |
| EF03CO01 | Cabo de Guerra da Verdade | R2 | Luta como disputa (vs IA) |
| EF03CO02 | Labirinto do Enquanto | R1 | Programação condicional com laço indefinido |
| EF03CO02 | Corra Enquanto... | R2 | Corrida (endless runner) |
| EF03CO03 | Chef dos Subproblemas | R1 | Planejamento modular |
| EF03CO03 | Missão em Pedaços | R2 | Estratégia por turnos |
| EF03CO04 | Montador de Informações | R1 | Montagem estruturada de dados |
| EF03CO04 | Caverna dos Dados | R2 | Aventura / exploração |
| EF03CO05 | Formato Certo | R1 | Matching entre informação e formato |
| EF03CO05 | Torres dos Formatos | R2 | Tower defense |
| EF03CO06 | Central de Entrada e Saída | R1 | Conexão funcional de periféricos |
| EF03CO06 | Fase de Entrada e Saída | R2 | Plataforma |
| EF03CO07 | Detetives da Busca | R1 | Busca guiada com refinamento |
| EF03CO07 | A Busca do Bibliotecário | R2 | RPG leve (micro-missões) |
| EF03CO08 | Estúdio Multiformato | R1 | Criação livre orientada |
| EF03CO08 | Show de Formatos | R2 | Criação audiovisual (imagem + frase + som) |
| EF03CO09 | Investigação: Dados em Risco | R1 | Jogo investigativo por evidências |
| EF03CO09 | Ache o Suspeito | R2 | Tiro ao alvo educativo (atenção e interpretação) |
| EF04CO01 | Batalha das Coordenadas | R1 | Tabuleiro matricial por coordenadas |
| EF04CO01 | Canhão de Coordenadas | R2 | FPS adaptado (mira, sem violência) |
| EF04CO02 | Arquivo dos Registros | R1 | Consulta e filtragem de registros |
| EF04CO02 | O Cofre dos Registros | R2 | Escape room individual |
| EF04CO03 | Prédio dos Laços | R1 | Programação por blocos com laços aninhados |
| EF04CO03 | Dança Aninhada | R2 | Ritmo / música |
| EF04CO04 | Tradutor da Máquina | R1 | Conversão simbólica |
| EF04CO04 | Pista Binária | R2 | Corrida (endless runner) |
| EF04CO05 | Ateliê de Códigos Digitais | R1 | Oficinas de codificação |
| EF04CO05 | Expedição Pixel | R2 | Aventura / exploração |
| EF04CO06 | Estúdio de Produção Digital | R1 | Produção de conteúdo em editor |
| EF04CO06 | Estúdio Tycoon Mirim | R2 | City builder / gestão |
| EF04CO07 | Missão Ética Digital | R1 | Decisão ética baseada em regras |
| EF04CO07 | Guardião Ético | R2 | Estratégia por turnos |
| EF04CO08 | Caça à Fonte Confiável | R1 | Comparação crítica de fontes |
| EF04CO08 | Duelo das Fontes | R2 | Luta como disputa (vs IA) |
| EF05CO01 | Baralho das Listas | R1 | Manipulação de lista ordenada |
| EF05CO01 | Revezamento em Lista | R2 | Esporte |
| EF05CO02 | Mapas em Rede | R1 | Modelagem e navegação em grafo |
| EF05CO02 | Metrô Mirim | R2 | City builder / gestão |
| EF05CO03 | Arena da Lógica | R1 | Resolução de expressões lógicas |
| EF05CO03 | Portões Lógicos | R2 | Tower defense |
| EF05CO04 | Cidade das Decisões | R1 | Programação por blocos com seleção condicional |
| EF05CO04 | Robô Sorrateiro | R2 | Stealth infantil |
| EF05CO05 | Monte seu Computador | R1 | Montagem funcional |
| EF05CO05 | Dentro da Máquina | R2 | Plataforma |
| EF05CO06 | Missão Arquivo Seguro | R1 | Gestão de armazenamento |
| EF05CO06 | Arremesso na Nuvem | R2 | Esporte |
| EF05CO07 | Sistema Operacional | R1 | Simulação de gerenciamento de recursos |
| EF05CO07 | Maestro do Sistema | R2 | Simulador de vida |
| EF05CO08 | Radar de Confiabilidade | R1 | Avaliação crítica de conteúdo |
| EF05CO08 | Caça-Boatos | R2 | FPS adaptado (mira, sem violência) |
| EF05CO09 | Curadoria com Créditos | R1 | Curadoria e atribuição de créditos |
| EF05CO09 | A Guilda dos Criadores | R2 | RPG leve (micro-missões) |
| EF05CO10 | Futuro em Cena | R1 | Storytelling digital |
| EF05CO10 | Oficina do Amanhã | R2 | Sandbox de construção criativa |
| EF05CO11 | Escolha a Ferramenta Certa | R1 | Tomada de decisão multicritério |
| EF05CO11 | Kit do Solucionador | R2 | Estratégia por turnos |
| EF15CO01 | Museu das Estruturas | R1 | Metajogo de seleção de representação |
| EF15CO01 | O Mistério do Arquivo Vivo | R2 | Point-and-click / mistério |
| EF15CO02 | Academia dos Algoritmos | R1 | Programação em progressão curricular |
| EF15CO02 | Maratona do Algoritmo | R2 | Plataforma |
| EF15CO03 | Circuito da Verdade | R1 | Circuito lógico |
| EF15CO03 | Trunfo da Lógica | R2 | Cartas / deck-building leve |
| EF15CO04 | Arquiteto das Missões | R1 | Planejamento por decomposição |
| EF15CO04 | Canteiro de Obras | R2 | City builder / gestão |