Imported from Felps03/nexo-puzzle (
AGENTS.md). Install upstream withnpx skills add Felps03/nexo-puzzle. Copyright stays with the author.
This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ (resolved from this file's directory; in monorepos the next package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by next dev — verify at node_modules/next/dist/server/lib/generate-agent-files.js. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
NEXO — guia para agentes
Jogo de puzzle de caminho hamiltoniano com checkpoints ordenados. Documentação
de produto no README.md; fórmula de dificuldade em
docs/difficulty.md. Este arquivo cobre só o que não é
óbvio lendo o código.
Comandos
npm run dev # desenvolvimento
npm test # 199 testes (Vitest, ambiente node)
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run verify # typecheck + lint + testes + build ← rode antes de concluir
Sempre rode npm run verify antes de dar uma tarefa por encerrada. Não afirme
que algo funciona sem ter executado o teste correspondente.
Regra de ouro da arquitetura
src/lib/nexo domínio puro → NUNCA importa React, DOM ou next/*
src/features regras de app → funções puras e reducers, sem JSX
src/hooks ponte React → traduz eventos em ações do domínio
src/components só renderização → NUNCA contém regra de jogo
Se você está prestes a escrever if sobre uma regra do jogo dentro de um
componente, o lugar certo é src/features/game/gameMachine.ts. Se está prestes
a importar react dentro de src/lib/nexo, pare — a divisão é intencional e é
o que mantém a suíte rodando em Node puro, sem jsdom.
Invariantes que não podem ser quebrados
Determinismo. Nada no domínio pode usar Math.random(), Date.now() ou
qualquer entrada não determinística. Toda aleatoriedade passa por createRng /
deriveSeed (src/lib/nexo/rng.ts). A mesma seed precisa produzir exatamente o
mesmo desafio — é disso que dependem o desafio diário, os ids regeneráveis e a
reprodutibilidade dos testes.
Solução única. Só publicamos puzzles com exatamente uma solução. O gerador
nunca troca corretude por dificuldade: se não achar nada na banda pedida, ele
devolve o melhor candidato válido e único com fallbackUsed: true. Não
adicione um caminho de fallback que devolva puzzle ambíguo.
A solução nunca vai para o cliente. toPayload()
(src/server/challengeService.ts) só inclui debugSolution se
NODE_ENV !== 'production' e NEXO_EXPOSE_SOLUTION === 'true'. Dica e
validação de conclusão são calculadas no servidor. Não mova o solver para o
cliente "para economizar uma requisição".
Paredes são obrigatórias. Sem elas, um caminho hamiltoniano aleatório
precisa de ~1 checkpoint a cada 2 células para ter solução única (um 7×7 exigia
24 checkpoints). Se alguém sugerir remover GridShape.walls para "simplificar",
isso quebra o jogo, não só o visual.
Armadilhas já pagas
Cada item abaixo custou um bug real. Não desfaça essas decisões achando que são complexidade desnecessária.
| Local | O que parece supérfluo | Por que existe |
|---|---|---|
grid.ts → Graph.epoch |
Um contador mutável no grafo | O buffer scratchSeen é carimbado com um epoch em vez de ser limpo. Vários solve() compartilham o mesmo Graph; um epoch que reiniciasse colidiria com carimbos antigos e corromperia o flood-fill — o sintoma era publicar puzzle com 2 soluções. |
pathGenerator.ts → const candidates dentro de walk |
Alocar array a cada nível | Um array compartilhado entre níveis da recursão é mutado pela chamada recursiva durante o for que o percorre. O sintoma eram caminhos com saltos não adjacentes. |
grid.ts → analyseParity |
Checagem "extra" antes de buscar | O grid é bipartido: as contagens de cor podem diferir no máximo em 1 e, quando diferem, ambos os extremos ficam na cor majoritária. Sem isso, 13% das sementes em tabuleiros de área ímpar travavam a busca. |
pathGenerator.ts → laço de reinício |
"Bastaria aumentar maxSteps" |
Reiniciar de outra célula é muito mais eficiente que insistir na subárvore. Levou 8×8 de 2,4 s (7 falhas em 60) para 50 ms (0 falhas em 60). |
usePointerPath.ts → mirror |
Espelho do estado que o React já tem | pointermove é evento contínuo; o React pode adiar o commit e o próximo passo seria planejado a partir de uma trilha desatualizada, perdendo movimentos no arrasto rápido. |
useGame.ts → recordedState |
Guardar o objeto em vez do challenge.id |
Passive effects rodam depois do commit; um carregamento em voo pode resolver no meio e liberar a guarda, registrando a partida anterior de novo (modal fantasma "00:00 — Novo recorde"). Identidade de objeto é exata e imune à corrida. |
gameMachine.ts → graphFor com WeakMap |
Cache por id seria mais simples | Chavear pelo objeto evita colisão entre dois desafios com o mesmo id e deixa o grafo ser coletado. |
Board.tsx → handlers no div da grade |
Poderiam ficar no contêiner externo | usePointerPath mapeia coordenadas linearmente sobre o retângulo do elemento; o contêiner externo tem padding, então as fronteiras de célula não bateriam com o que é renderizado. |
Ao mexer no gerador, solver ou dificuldade
As faixas de normalização em difficultyEngine.ts foram medidas, não
estimadas: vêm da saída real do gerador em tabuleiros de 5×5 a 11×11. Se você
alterar o gerador, o solver ou a colocação de checkpoints, os scores saem do
lugar e as bandas param de ser atingidas.
Nesse caso: gere amostras por dificuldade, confira onde os scores caem e
reajuste as faixas e os presets. O teste
lands inside the requested difficulty band most of the time é o alarme —
aferição atual: 49 de 50 desafios dentro da banda.
Os pesos somam exatamente 1.0 e há teste garantindo isso. Se adicionar uma
característica, redistribua os pesos e atualize docs/difficulty.md.
Como escrever testes aqui
Não construa afirmações sobre puzzles à mão. Durante o desenvolvimento, várias premissas "óbvias" sobre caminhos impossíveis estavam erradas — o tabuleiro admitia a solução que eu jurava não existir, e o teste falhava por culpa do teste. Um deles só passava por causa do bug do epoch.
Formas confiáveis de montar cenários:
- para um caminho comprovadamente impossível, pegue a solução única de um
desafio gerado e desvie dela — qualquer desvio é beco sem saída por
construção (veja
firstDetouremhint.test.ts); - para verificar corretude, use o solver como oráculo, não raciocínio manual;
- prefira asserções de propriedade (100 sementes, todas únicas) a exemplos isolados.
Testes ficam colocalizados: src/lib/nexo/__tests__/, src/features/*/*.test.ts,
src/hooks/__tests__/, src/repositories/__tests__/.
Restrições de ambiente
O ambiente roda Node 20.19.5. Isso define escolhas que parecem desatualizadas mas não são:
- Vitest 3, porque o Vitest 5 exige Node
>= 22.12 - sem jsdom nem @testing-library, porque as versões atuais exigem Node
>= 22 - por isso não há teste de componente React — a renderização é validada manualmente no navegador; as regras de jogo estão cobertas no reducer
Em Node 22+ dá para atualizar e adicionar testes de componente sem mudar o código de produção. Até lá, não introduza dependência que exija Node 22.
Convenções
- TypeScript
strict; semanydesnecessário e sem@ts-ignore - Comentários em inglês no código, documentação em português
- Comentário explica por quê, não o quê; se descreve o óbvio, remova
- Textos de interface em pt-BR
- Sem dependências novas sem necessidade real — o projeto tem 4 dependências de runtime e isso é intencional