Caputchin
Desarrollo de juegos del marketplace

El contrato de repetición

El contrato de repetición es la única superficie obligatoria que un juego reproducible acuerda con la plataforma. Se publica como @caputchin/replay-contract: un paquete deliberadamente diminuto y sin dependencias (se carga en un isolate de repetición sellado, así que no trae nada). Todo lo demás de tu juego, el engine, el renderer, el formato de la traza, es tuyo.

Un juego que publica un run conforme es reproducible y por tanto puede ejercer de gate de una clave de sitio; un juego sin uno sigue siendo incrustable, pero solo como UX (muestra No reproducible). Mira Publica un juego en el marketplace para esa distinción.

La función run

Un juego reproducible publica un módulo JS o WASM que exporta una función llamada run:

run(seed, config, trace) -> { passed, score, durationMs }
type RunFn<C = unknown> = (
  seed: Seed,
  config: C | null,
  trace: Uint8Array | string,
) => Verdict | Promise<Verdict>
ArgumentoFuenteSignificado
seedServidorLa semilla por ronda. El montaje propiedad del servidor de la ronda.
configServidorLa config de juego, nullable y opaca: la plataforma nunca la inspecciona, y se vuelve a resolver en el servidor en la repetición, nunca la afirma el cliente. null significa "usa los valores por defecto del propio run". En el MVP el servidor pasa null; la config por sitio es una fase diferida.
traceClienteEl blob opaco que el cliente del jugador emitió, que solo esta función interpreta. La plataforma nunca lo parsea ni lo tipa; son bytes en bruto o una cadena, acotados solo por un tope de tamaño y el límite de CPU del isolate.

El orden de los argumentos codifica el modelo de confianza: seed y config son el montaje del servidor de la ronda; trace es la entrada del jugador. Los valores que afectan al gate (un umbral de aprobado, vidas) son seguros de leer de config pero serían un bypass si se leyeran de la trace del cliente.

El módulo debe exportar la función bajo el nombre run (la constante RUN_EXPORT_NAME). El anfitrión de repetición la invoca y espera el resultado (la instanciación de WASM es asíncrona en la primera llamada).

La semilla

type Seed = readonly [number, number, number, number]   // 128 bits, 4 u32 words, MSW first

La semilla son los 128 bits bajos de SHA-256(sessionId : gameId : roundIndex), llevados como cuatro palabras de 32 bits sin signo, la palabra más significativa primero. El deriveSeed(sessionId, gameId, roundIndex) del paquete la computa; el servidor la deriva tanto al emitir la ronda como de nuevo al repetir, así que nunca viaja por el cable como entrada de cliente confiable.

Crucialmente, la semilla ata una traza a una sesión, juego y ronda: repetir una traza ajena o anterior bajo una semilla distinta produce passed: false. Esa atadura es cómo se defienden la inyección de trazas y los ataques de repetición, así que debes sembrar toda la aleatoriedad de ella para que el juego en vivo y la repetición del servidor concuerden.

El veredicto

interface Verdict {
  readonly passed: boolean      // drives the verification decision
  readonly score: number        // game-defined, any finite number
  readonly durationMs: number   // finite, non-negative
}

passed es el único campo que conduce la decisión del captcha; score y durationMs se llevan en el token emitido (y un futuro marcador). El parseVerdict(value) del paquete valida un valor de retorno no confiable y trata un resultado malformado como una ronda rechazada, nunca una aprobada, así que un run que lanza o devuelve basura falla cerrado.

El determinismo es el requisito entero

La única regla dura del contrato: run debe ser puro y determinista entre dos runtimes, el navegador del jugador y el isolate del servidor. Un (seed, config, trace) idéntico debe producir un veredicto idéntico en ambos. Las formas comunes de romper esto, todas atrapadas por la autocomprobación de publicación como run-not-conforming:

  • leer Date.now(), performance.now(), Math.random(), u otros globales no deterministas;
  • leer estado externo (DOM, red, almacenamiento) que el isolate de repetición no provee;
  • depender de matemática de coma flotante que difiere entre runtimes.

Cómo logres el determinismo (coma fija, floats de la spec de WASM, o IEEE-754 más un shim de neutralización) es tu elección; la plataforma solo aloja la repetición. El engine kit provee primitivas deterministas listas para usar si prefieres no hacer las tuyas.

Cómo lo usa el servidor

El artefacto que el isolate carga es tu bundle run, o el entry en vivo o el artefacto run dedicado del manifiesto.

Véase también

En esta página