Le contrat de rejeu
Le contrat de rejeu est l'unique surface obligatoire sur laquelle un jeu rejouable s'accorde avec la plateforme. Il est publié sous @caputchin/replay-contract : un paquet délibérément minuscule et sans dépendance (il est chargé dans un isolat de rejeu scellé, donc il n'entraîne rien). Tout le reste de ton jeu, le moteur, le renderer, le format de trace, est à toi.
Un jeu qui livre un run conforme est rejouable et peut donc gater une clé de site ; un jeu sans cela est toujours intégrable, mais seulement comme UX (il s'affiche Non rejouable). Vois Livre un jeu au marketplace pour cette distinction.
La fonction run
Un jeu rejouable livre un module JS ou WASM exportant une fonction nommée run :
run(seed, config, trace) -> { passed, score, durationMs }type RunFn<C = unknown> = (
seed: Seed,
config: C | null,
trace: Uint8Array | string,
) => Verdict | Promise<Verdict>| Argument | Source | Signification |
|---|---|---|
seed | Serveur | La graine par manche. La mise en place de la manche, propriété du serveur. |
config | Serveur | La config de gameplay, nullable et opaque : la plateforme ne l'inspecte jamais, et elle est re-résolue côté serveur au rejeu, jamais affirmée par le client. null signifie « utilise les propres défauts du run ». Au MVP, le serveur passe null ; la config par site est une phase différée. |
trace | Client | Le blob opaque que le client du joueur a émis, que cette fonction seule interprète. La plateforme ne le parse ni ne le type jamais ; ce sont des octets bruts ou une chaîne, bornés seulement par un plafond de taille et la limite CPU de l'isolat. |
L'ordre des arguments encode le modèle de confiance : seed et config sont la mise en place serveur de la manche ; trace est l'entrée du joueur. Les valeurs qui affectent le gate (un seuil de réussite, des vies) sont sûres à lire depuis config mais seraient un contournement si lues depuis le trace du client.
Le module doit exporter la fonction sous le nom run (la constante RUN_EXPORT_NAME). L'hôte de rejeu l'invoque et attend le résultat (l'instanciation WASM est asynchrone au premier appel).
La graine
type Seed = readonly [number, number, number, number] // 128 bits, 4 u32 words, MSW firstLa graine est les 128 bits de poids faible de SHA-256(sessionId : gameId : roundIndex), portés comme quatre mots non signés de 32 bits, mot le plus significatif en premier. Le deriveSeed(sessionId, gameId, roundIndex) du paquet la calcule ; le serveur la dérive à la fois en émettant la manche et de nouveau en rejouant, donc elle ne voyage jamais sur le fil comme entrée client de confiance.
De façon cruciale, la graine lie une trace à une session, un jeu et une manche : rejouer une trace étrangère ou antérieure sous une graine différente donne passed: false. Cette liaison est la façon dont l'injection de trace et les attaques par rejeu sont défendues, donc tu dois ensemencer tout l'aléatoire à partir d'elle pour que le jeu en direct et le rejeu serveur s'accordent.
Le verdict
interface Verdict {
readonly passed: boolean // drives the verification decision
readonly score: number // game-defined, any finite number
readonly durationMs: number // finite, non-negative
}passed est le seul champ qui pilote la décision du captcha ; score et durationMs sont portés dans le token émis (et un futur tableau des scores). Le parseVerdict(value) du paquet valide une valeur de retour non fiable et traite un résultat malformé comme une manche rejetée, jamais réussie, donc un run qui lève ou renvoie n'importe quoi échoue en mode fermé.
Le déterminisme est toute l'exigence
L'unique règle stricte du contrat : run doit être pur et déterministe sur deux runtimes, le navigateur du joueur et l'isolat du serveur. Un (seed, config, trace) identique doit donner un verdict identique dans les deux. Les façons courantes de casser ça, toutes attrapées par l'auto-vérification de publication comme run-not-conforming :
- lire
Date.now(),performance.now(),Math.random(), ou d'autres globaux non déterministes ; - lire un état externe (DOM, réseau, stockage) que l'isolat de rejeu ne fournit pas ;
- s'appuyer sur des maths à virgule flottante qui diffèrent entre runtimes.
Comment tu atteins le déterminisme (virgule fixe, flottants spec-WASM, ou IEEE-754 plus un shim de neutralisation) est ton choix ; la plateforme n'héberge que le rejeu. Le kit moteur fournit des primitives déterministes prêtes à l'emploi si tu préfères ne pas rouler les tiennes.
Comment le serveur l'utilise
L'artefact que l'isolat charge est ton bundle run, soit l'entry en direct, soit l'artefact run dédié du manifeste.
Voir aussi
- L'engine-kit : produire un
runconforme à partir d'un simple reducer, avec le déterminisme géré pour toi. - Le manifeste caputchin.json : déclarer l'artefact
runet ses modules. - Bâtir un jeu pour le marketplace : enregistrer la trace que ce contrat rejoue.
- Référence des erreurs de publication : le résultat d'auto-vérification
run-not-conforming.