O contrato de replay
O contrato de replay é a única superfície obrigatória que um jogo reproduzível acorda com a plataforma. Ele é publicado como @caputchin/replay-contract: um pacote deliberadamente minúsculo e sem dependências (ele é carregado em um isolate de replay selado, então não puxa nada). Todo o resto sobre seu jogo, a engine, o renderizador, o formato do trace, é seu.
Um jogo que envia um run conforme é reproduzível e por isso pode proteger uma chave de site; um jogo sem um ainda é embutível, mas só como UX (ele mostra Não reproduzível). Veja Publique um jogo no marketplace para essa distinção.
A função run
Um jogo reproduzível envia um módulo JS ou WASM exportando uma função chamada run:
run(seed, config, trace) -> { passed, score, durationMs }type RunFn<C = unknown> = (
seed: Seed,
config: C | null,
trace: Uint8Array | string,
) => Verdict | Promise<Verdict>| Argumento | Origem | Significado |
|---|---|---|
seed | Servidor | A semente por rodada. A montagem da rodada, de posse do servidor. |
config | Servidor | A config de jogabilidade, anulável e opaca: a plataforma nunca a inspeciona, e ela é re-resolvida no servidor no replay, nunca afirmada pelo cliente. null significa "use os padrões do próprio run". No MVP o servidor passa null; config por site é uma fase adiada. |
trace | Cliente | O blob opaco que o cliente do jogador emitiu, que só esta função interpreta. A plataforma nunca o faz parse nem o tipa; são bytes crus ou uma string, limitados só por um teto de tamanho e o limite de CPU do isolate. |
A ordem dos argumentos codifica o modelo de confiança: seed e config são a montagem da rodada no servidor; trace é a entrada do jogador. Valores que afetam o portão (um limiar de aprovação, vidas) são seguros para ler de config mas seriam um contorno se lidos do trace do cliente.
O módulo precisa exportar a função sob o nome run (a constante RUN_EXPORT_NAME). O anfitrião de replay a invoca e aguarda o resultado (a instanciação de WASM é assíncrona na primeira chamada).
A semente
type Seed = readonly [number, number, number, number] // 128 bits, 4 u32 words, MSW firstA semente são os 128 bits baixos de SHA-256(sessionId : gameId : roundIndex), carregada como quatro palavras de 32 bits sem sinal, a palavra mais significativa primeiro. O deriveSeed(sessionId, gameId, roundIndex) do pacote a computa; o servidor a deriva tanto ao emitir a rodada quanto de novo ao reexecutar, então ela nunca anda pela rede como entrada de cliente confiável.
Crucialmente, a semente liga um trace a uma sessão, jogo e rodada: reexecutar um trace estranho ou anterior sob uma semente diferente produz passed: false. Essa ligação é como a injeção de trace e os ataques de replay são defendidos, então você precisa semear toda aleatoriedade dela para o jogo ativo e o replay do servidor concordarem.
O veredito
interface Verdict {
readonly passed: boolean // drives the verification decision
readonly score: number // game-defined, any finite number
readonly durationMs: number // finite, non-negative
}passed é o único campo que conduz a decisão do captcha; score e durationMs são carregados no token emitido (e em um placar futuro). O parseVerdict(value) do pacote valida um valor de retorno não confiável e trata um resultado malformado como uma rodada rejeitada, nunca uma de aprovação, então um run que lança ou retorna lixo falha de forma fechada.
O determinismo é o requisito inteiro
A única regra rígida do contrato: run precisa ser puro e determinístico em dois runtimes, o navegador do jogador e o isolate do servidor. (seed, config, trace) idênticos precisam produzir um veredito idêntico nos dois. As formas comuns de quebrar isto, todas pegas pela autoverificação de publicação como run-not-conforming:
- ler
Date.now(),performance.now(),Math.random(), ou outros globais não determinísticos; - ler estado externo (DOM, rede, armazenamento) que o isolate de replay não fornece;
- depender de matemática de ponto flutuante que difere entre runtimes.
Como você alcança o determinismo (ponto fixo, floats spec-WASM, ou IEEE-754 mais um shim de neutralização) é sua escolha; a plataforma só hospeda o replay. O kit de engine fornece primitivas determinísticas prontas se você preferir não fazer a sua.
Como o servidor o usa
O artefato que o isolate carrega é o seu bundle run, ou o entry ativo ou o artefato run dedicado do manifesto.
Veja também
- O engine-kit: produza um
runconforme a partir de um reducer simples, com o determinismo tratado por você. - O manifesto caputchin.json: declarar o artefato
rune seus módulos. - Construir um jogo de marketplace: registrar o trace que este contrato reexecuta.
- Referência de erros de publicação: o resultado
run-not-conformingda autoverificação.