Esta é a spec entregue a todas as execuções do bench — o contrato que cada combinação de harness e modelo teve que cumprir, sem poder alterá-lo. As diferenças entre os resultados vêm inteiramente de como cada um interpretou e implementou este mesmo documento.
Contrato funcional do jogo. Este arquivo é protegido: não pode ser alterado pelo agente. Toda regra aqui descrita é verificada por docs/tests/e2e/ e/ou pela avaliação humana (RUBRIC.md).
Um mini JRPG completo em Phaser 4, com:
O jogo deve ser jogável de ponta a ponta com teclado, com interface completa (menus, barras de HP/MP, log de mensagens) e assets próprios (ver §8).
Resolução 1920×1080. O jogo deve ser visualmente impressionante — apresentação é objetivo de primeira classe, não acabamento opcional, e vale 30 dos 100 pontos da avaliação. Você tem liberdade artística completa (§8.2). Não há persistência: recarregar a página reinicia tudo; "Novo Jogo" sempre parte de estado limpo.
| Cena | Entrada | Saída |
|---|---|---|
title | carregamento da página; sair do game over | confirm em "Novo Jogo" → map |
map | novo jogo; vitória; fuga | contato com inimigo → battle |
battle | contato no mapa | vitória/fuga → map; derrota → gameover |
gameover | HP do jogador chegar a 0 | confirm → title |
A tela de título deve ter ao menos a opção "Novo Jogo" com cursor navegável.
Teclado: setas e WASD para mover/navegar; Enter ou Z = confirmar; Esc ou X = cancelar/voltar. A Debug API (§7) injeta as mesmas ações pelo mesmo caminho de input do teclado.
docs/data/maps/map01.json. O campo tileSize (32) é a unidade de autoria do dado; a escala de renderização e o enquadramento em 1920×1080 são decisão sua (§8.2). Coordenadas de jogo (player.x, player.y) são sempre em tiles, nunca em pixels.tiles[y][x]: 0 = chão (andável), 1 = parede (bloqueia).map01.json.docs/data/player.json → start.group do inimigo do mapa; stats em docs/data/enemies.json).docs/data/player.json. Skills: docs/data/skills.json. Itens: docs/data/items.json.golpe_forte: custa mp, dano com power da skill. Sem MP suficiente = ação inválida.pocao: cura heal (limitado ao maxHp), consome 1 unidade. Sem unidades = ação inválida.max(1, ceil(dano / 2)).O cursor do menu começa em "Atacar". Na seleção de alvo, o cursor começa no inimigo de índice 0. Ação inválida (sem MP, sem item, alvo morto): nada acontece, o round não avança, uma mensagem entra no log.
Defender e Fugir do jogador resolvem antes de qualquer outra ação.spd decrescente. Empate jogador × inimigo: jogador primeiro. Empate entre inimigos: ordem do array battle.enemies.variacao(r) = 0.9 + r * 0.2 // r ∈ [0,1) vem do RNG (§6)
danoAtaque = max(1, floor((atk * 2 - defAlvo) * variacao(r)))
danoSkill(power) = max(1, floor((atk * power - defAlvo) * variacao(r)))
danoDefendendo = max(1, ceil(danoNormal / 2)) // aplica após a fórmula acima
cura(pocao) = min(maxHp, hp + heal)
clamp : HP e MP nunca ficam negativos (mínimo 0)
xp de todos os inimigos derrotados ao XP total do jogador; volta ao mapa.xp total >= xp da próxima linha de levelTable (nível máximo: 3). Aplica os bônus da tabela e cura HP e MP totalmente. Pode subir mais de um nível de uma vez.gameover; confirm volta ao título.Único gerador aleatório do jogo: mulberry32, exatamente:
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
Date.now()). __game.setSeed(n) substitui o gerador por mulberry32(n).Assim que o jogo carregar, deve existir window.__game:
window.__game = {
version: 1,
// Retorna uma CÓPIA PROFUNDA do estado (mutar o retorno não afeta o jogo).
getState(): {
scene: 'title' | 'map' | 'battle' | 'gameover',
player: {
x, y, level, xp, hp, maxHp, mp, maxMp, atk, def, spd,
items: [{ id, qty }], skills: [string]
},
map: null | { id, width, height, enemies: [{ id, x, y, group }] }, // não-nulo em 'map'
battle: null | { round, defending, log: [string], // não-nulo em 'battle'
enemies: [{ id, name, hp, maxHp, atk, def, spd }] }
},
// Injeta input pelo MESMO caminho do teclado.
// Resolve quando todo o processamento decorrente terminar (inclusive o round de batalha completo).
input(action: 'up'|'down'|'left'|'right'|'confirm'|'cancel'): Promise<void>,
// Atalho programático de batalha (equivalente a navegar o menu).
// opts: { target?: number, id?: string } — target: índice em battle.enemies (default 0)
// cmd: 'attack' | 'skill' | 'item' | 'defend' | 'flee'
// Resolve ao fim do round (ou da transição de cena). Ação inválida: resolve sem efeito (não rejeita).
selectCommand(cmd, opts?): Promise<void>,
setSeed(n: number): void
}
Requisitos: promises resolvem em ≤ 2s (animações podem ser encurtadas quando a ação vem da Debug API); getState() sempre disponível em qualquer cena.
index.html é protegido e já contém <canvas id="game" width="1920" height="1080">. O jogo deve reusar esse canvas, passando-o em canvas: no GameConfig — não crie um segundo canvas.
Consequência já verificada neste ambiente: ao receber um canvas próprio, o Phaser 4 entra em modo de ambiente customizado e rejeita Phaser.AUTO, falhando o boot com Must set explicit renderType in custom environment. Declare o renderer explicitamente.
Prefira type: Phaser.WEBGL. Phaser.CANVAS também boota, mas filtros e post-processing são WebGL-only — no renderer Canvas você perde bloom, glow, vignette, blur e afins.
Cuide também do dimensionamento: 1920×1080 raramente cabe inteiro na janela de quem avalia. Configure scale (Scale.FIT + autoCenter) para reduzir sem distorcer.
Performance já medida neste ambiente (Chromium headless do Playwright, sem GPU, renderização por software): ~40 fps com ~18 sprites animados + partículas + 1 filtro; ~24 fps numa carga abusiva de 240 sprites. Ou seja, há folga para caprichar. O limite real não é framerate, é o orçamento de 2s da Debug API (§7): quando a ação vier da Debug API, encurte ou pule animações.
O objetivo é um jogo visualmente impressionante, não um protótipo funcional pintado.
Você tem liberdade artística completa: escolha o estilo (pixel art, vetorial, neon, aquarela procedural, minimalista, o que você quiser), a paleta, a tipografia, a composição e os efeitos. Não existe estilo prescrito, e o index.html não força mais image-rendering: pixelated — se quiser pixel art, ligue pixelArt: true; se quiser suavizado, deixe desligado.
O que é avaliado (RUBRIC — 30 dos 100 pontos):
Graphics, generateTexture(), DynamicTexture/RenderTexture, filtros, partículas) ou arquivos de imagem criados durante a sessão. Proibido baixar assets externos, incluindo webfonts remotas. Fontes já instaladas no sistema, via add.text, são permitidas — não são download.ASSETS_REPORT.md deve listar: caminho esperado, dimensões, descrição do que inserir.npm test com 100% dos testes passando.npm run build sem erros.ASSETS_REPORT.md na raiz — sempre obrigatório, mesmo com tudo gerado. Formato:# Assets Report
## Gerados
- herói — procedural (src/assets/sprites.js), 32×32, pixel art 3 cores
- ...
## Placeholders (se houver)
- fundo de batalha — INSERIR ARQUIVO em src/assets/battle-bg.png, 1920×1080, sugestão: planície ao entardecer
SUMMARY.md na raiz: o que foi feito, decisões de arquitetura, limitações conhecidas.phaser@^4.2.1, já declarado em package.json) é a única dependência de runtime permitida. Não adicione nenhuma outra dependência, de runtime ou de dev. Nenhum outro framework. Sem TypeScript — JavaScript puro em ES Modules.docs/** (esta spec, os dados e os testes), PROTOCOL.md, RUBRIC.md, PROMPT.md, AGENTS.md, scripts/**, playwright.config.js, index.html, package.json, .agents/**, .claude/**, skills-lock.json.src/ (e assets em src/assets/ se forem arquivos).src/ é decisão sua. A rubrica avalia separação de responsabilidades (cena / regras / render) e legibilidade, não uma estrutura de pastas prescrita.Verificado pelo avaliador jogando de verdade, sem Debug API (RUBRIC — 1 ponto por item):
Sessão de 10 minutos, em tela cheia (PROTOCOL §5):
título → mapa → 3 batalhas (incluindo a dupla) → level up → fuga → derrota proposital → voltar ao título.
Capturar screenshots das 4 cenas em resolução nativa.