Aller au contenu
quest'n'click

CréerTutorielPour auteursS'applique de 4.1.17

Remix

This page in EnglishCette page sur le wiki GitHub

Remix permet à un même jeu de produire plusieurs parties vraiment différentes : où se trouve un objet, où un personnage commence et quelle tournée il fait, un code et son indice, l'ordre de deux groupes d'énigmes, une réplique dite autrement. L'histoire ne devient jamais un contenu aléatoire. Chaque variation est déclarée par l'auteur, finie, déterministe à partir d'une seed, et comprise par le validateur, le solveur, les sauvegardes, le replay, le Studio et les outils de speedrun. La conception est l'ADR 0018 ; les décisions sont D25 à D28 (docs/dev/DECISIONS.md). Remix est arrivé en 4.1.15.

La première moitié de cette page est un tutoriel (déplacer une clé entre trois cachettes), la seconde une référence du manifeste, des seeds, des outils et de ce que voit le joueur.

Ce que vous allez construire

La clé du jeu d'exemple, que le vendeur remet sur son étal dans l'histoire, cachée selon le monde à l'un de trois endroits du marché : sur l'étal, sous les oranges, dans la lanterne. Une seed comme WS-0000-02DZ nomme un monde ; la même seed donne le même monde sur toutes les machines et tous les navigateurs. À la fin, npm run remix affiche le monde que fait une seed et npm run verify:variants prouve que chaque monde se termine.

L'exemple complet est dans le dépôt : games/demo/game.ts (le manifeste) et games/demo/rooms/market.ts (les ancres et les branches).

Ce qu'il vous faut

  • Le dépôt du moteur, installé (Installer depuis le dépôt), avec votre jeu ou le jeu demo sélectionné (GAME=<id> ou npm run game).
  • Un jeu qui se termine déjà : npm run validate et npm run solve au vert (Vérifier son jeu).
  • Vingt minutes.

Étapes

  1. Taguer les emplacements. Dans la salle, déclarez les ancres où l'objet peut être posé. anchors est un dictionnaire indexé par l'id de l'ancre ; at nomme le prop ou le hotspot sur lequel elle se trouve :

    // games/<id>/rooms/market.ts
    anchors: {
      stall: { at: 'stall', reachableBy: 'bouquet_given', phase: 'deposit' },
      oranges: { at: 'oranges', reachableBy: 'bouquet_given', phase: 'deposit' },
      lantern: { at: 'lantern', reachableBy: 'bouquet_given', phase: 'deposit' },
    },

    npm run validate vérifie désormais que chaque ancre repose sur quelque chose et qu'on peut l'atteindre.

  1. Déclarer le manifeste. Dans game.ts, remix liste les modes et les dimensions. Ici, une dimension logique déplace la clé et une dimension de présentation change une réplique :

    // games/<id>/game.ts
    remix: {
      schema: 1, algorithm: 'web-scumm-remix-1',
      modes: [
        { id: 'story', strategy: 'catalogue', dimensions: [] },
        { id: 'remix', strategy: 'catalogue', dimensions: ['key-spot', 'oranges-line'] },
      ],
      dimensions: [
        { id: 'key-spot', kind: 'item-placement', item: 'key', logical: true,
          anchors: [{ room: 'market', anchor: 'stall' }, { room: 'market', anchor: 'oranges' }, { room: 'market', anchor: 'lantern' }],
          story: { room: 'market', anchor: 'stall' } },
        { id: 'oranges-line', kind: 'presentation', target: 'line:market.take-oranges.l-oranges-are-not', logical: false, story: 0,
          values: [{ en: 'Oranges are not sardines…', fr: '…' }, { en: 'An orange. Round, bright…', fr: '…' }] },
      ],
      constraints: [{ kind: 'not-behind', item: 'key', action: 'house.use-key-pantry' }],
    },
  1. Écrire les branches. Une valeur logique différente de sa valeur histoire écrit le drapeau réservé remix.<dimension> au début de la partie (remix.key-spot = market.oranges). Le contenu le lit comme n'importe quel drapeau :

    // games/<id>/rooms/market.ts, la condition d'une règle
    if: { all: ['bouquet_given', { flag: 'remix.key-spot', eq: 'market.oranges' }, '!hidden_key_found'] },

    Le monde histoire n'écrit aucun drapeau : on écrit une condition pour chacune des autres valeurs, et l'histoire garde son texte. La démo remet la clé dans l'histoire, dit où elle est cachée sinon, et laisse Pixel la prendre sous les oranges ou dans la lanterne. npm run validate avertit d'une valeur qu'aucune condition ne lit.

  1. Regarder un monde.

    # dans le dépôt du moteur, votre jeu sélectionné
    npm run remix -- --seed WS-0000-02DZ
    npm run remix -- --preview WS-0000-02DZ

    La première affiche la valeur de chaque dimension et l'empreinte du monde ; la seconde y ajoute validate et un témoin du solveur sur ce monde.

  1. Prouver chaque monde.

    # dans le dépôt du moteur, votre jeu sélectionné
    npm run verify:variants -- --prove

    Chaque mode est vérifié : dans un catalogue, chaque monde logique est validé et prouvé sans blocage. La commande sort en 1 quand un monde échoue, et le nomme.

Ce que vous avez appris

Pour aller plus loin

Ce qui varie, et ce qui ne varie jamais

Une dimension fait varier une chose parmi des valeurs que l'auteur énumère, chacune avec sa valeur histoire (le monde tel qu'il est écrit) :

kindCe qu'elle fait varierSes champs
item-placementun objet sur l'une de plusieurs ancres taguéesitem, anchors ({room, anchor}), story (une ancre)
actor-startla salle où commence un personnageactor, rooms, story (une salle)
actor-routele script qui sert de tournéeactor, routes (ids de scripts), story
coupledun code et son indice, tirés ensemblepairs ({hint, answer}), story (un indice)
puzzle-orderun ordre de groupes d'énigmes qui garde les arêtes de l'auteurgroups (1 à 8), graph ([a, b] : a avant b), story
presentationune réplique, une image, une palette ou un paramètre de minijeutarget, values, story (un indice)

Chaque dimension a un id et logical: true, sauf presentation, qui est logical: false. Remix ne réordonne jamais les règles (leur ordre peut porter un sens), n'écrit jamais un texte, n'invente jamais une dépendance : l'auteur écrit chaque branche avec les conditions que le DSL a déjà.

Déclarer un manifeste

Le manifeste est remix dans game.ts (GameDef.remix, type VariationManifest) :

  • schema: 1 et algorithm: 'web-scumm-remix-1', obligatoires tels quels.
  • modes : chacun { id, strategy: 'catalogue' | 'generator', dimensions, mask? }. Les dimensions qu'un mode ne nomme pas gardent leur valeur histoire. mask: true cache ce que révèle la seed jusqu'à la fin (un run Mystery).
  • dimensions (tableau ci-dessus) et constraints (plus bas).
  • daily (facultatif) : { kid, publicKey, mode }, la clé du Bridge qui signe le défi du jour (Ed25519, brute, base64url), pour que le joueur vérifie la seed du jour hors ligne.

Les drapeaux qu'écrit une dimension logique sont remix.<dimension> ; un ordre d'énigmes écrit remix.<dimension>.<groupe> (la position du groupe). On les lit avec { flag: 'remix.key-spot', eq: 'market.oranges' } ; la valeur d'un placement d'objet est <salle>.<ancre>.

Champs inconnus. Le manifeste est vérifié contre un schéma strict (src/engine/core/remix/manifest.ts) : une clé inconnue, un champ manquant ou un littéral faux est refusé, avec le chemin de l'erreur. Un manifeste qui ne peut produire aucun monde (un domaine vidé par ses contraintes, par exemple) est une erreur de build qui liste toutes les raisons.

Ancres et placement d'objets

Une ancre est un emplacement tagué d'une salle, anchors dans rooms/<salle>.ts :

ChampSensPar défaut
atle prop ou le hotspot sur lequel elle se trouveobligatoire
visiblequand elle se montre (une condition)toujours
reachableByce qu'il faut pour l'atteindre (une condition)rien
capacitycombien d'objets elle tient à la fois1 : deux objets ne la partagent jamais sauf si elle le dit
phaseune étiquette par laquelle le Studio regroupe, et que verify:variants compteaucune

Le validateur refuse une ancre posée sur rien, une ancre dans une salle que le départ ne peut atteindre, et une ancre qu'on n'atteint qu'avec l'objet qui y est posé. Trois contraintes lient les dimensions :

  • not-behind ({ item, action }) : retire les ancres situées derrière une règle qui exige l'objet ;
  • exclusive ({ dimensions }, deux au moins) : deux d'entre elles ne prennent jamais la même valeur ;
  • requires ({ a, b }, chacun <dimension>=<valeur>) : quand a vaut, b vaut.

Personnages, codes et ordres

  • actor-start change la salle de départ du personnage. Chaque salle candidate doit déclarer un acteur pour lui ; le solveur prouve que le personnage est là où chaque monde a besoin de lui.
  • actor-route nomme des scripts : le while de chacun lit le drapeau (dans games/reference/game.ts, seller_rounds et seller_rounds_late).
  • coupled associe un indice (un texte, ou un par langue) à une réponse. {code:<id>} dans n'importe quel texte devient la réponse et {hint:<id>} l'indice, dans toutes les langues à la fois, après la traduction. Le contenu accepte la réponse par le drapeau : voir le mot de passe de la fête dans games/reference/rooms/hall.ts et la devinette de Grand-mère dans kitchen.ts.
  • puzzle-order écrit la position de chaque groupe : remix.festival-order.board = 0 quand le panneau vient d'abord.

Variance de présentation

La cible (target) d'une dimension presentation est l'une de :

  • line:<id de réplique>
  • prop-img:<salle>.<prop>
  • palette:<personnage>
  • minigame:<id de règle>:<paramètre> : un texte ou un décor de minijeu seulement. Un paramètre qui pourrait décider d'une victoire, comme les réponses d'une roue ou une difficulté, est refusé, puisqu'une preuve est indexée par le monde logique.

Elle tire du flux cosmetic, n'écrit aucun drapeau, ne prend aucune contrainte, et reste hors de l'espace du solveur (D27) : deux mondes qui ne diffèrent que par la présentation partagent leur preuve. Ajouter une dimension de présentation ne déplace aucun tirage logique.

Seeds et mondes

Une seed est story ou un code WS-XXXX-XXXX : sept symboles en base32 de Crockford et un symbole de contrôle. La saisie pardonne la casse, I et L (lus 1), O (lu 0), les espaces et les tirets ; un symbole faux est une erreur explicite, jamais un autre monde.

Les poids du symbole de contrôle sont impairs : un symbole faux n'importe où dans le code le change aussi.

Le même jeu, le même manifeste, la même seed et la même version d'algorithme donnent le même WorldVariant : la seed, l'algorithme et sa version, l'empreinte du manifeste, le mode, une valeur par dimension, et un SHA-256 du tout. Les tirages viennent du seul générateur à seed, un flux par dimension ; Math.random est interdit dans src/engine/core par le linter et par un test. Que ce soit le même dans Node est testé à chaque exécution ; npm run e2e:remix compare les mondes et les roues de code de 50 seeds (500 dans la nightly et le candidat de release) dans Node, Chromium, WebKit et Firefox, dans le job multi-runtime de la CI. Un code ne dit rien du joueur (docs/dev/threat-models/remix-seed.md) : c'est le nom d'un monde, pas un secret.

Stratégies par mode

Chaque mode dit ce qui soutient ses mondes (D25) :

  • Un catalogue énumère chaque monde logique (10 000 au plus, CATALOGUE_MAX ; au-delà, il est refusé) et chacun est validé et résolu : chacun est un gate de release.
  • Un générateur construit un monde par construction ; ses invariants sont testés par propriétés et son échantillon publié de seeds est résolu. On ne dit jamais que toutes les seeds sont prouvées.

Les jeux livrés n'utilisent que des catalogues.

Le chemin générateur est exercé par une fixture de test (tests/fixtures/remix-extension.ts, 527 280 mondes logiques) : 10 000 seeds donnent des mondes valides (un test par propriété) ; aucun n'est déclaré prouvé.

Outils

Tous dans le dépôt du moteur, pour le jeu sélectionné :

CommandeFait
npm run remix -- --seed <code>affiche un monde et son empreinte ; --json le WorldVariant entier ; --mode=<id> (remix par défaut) ; --version=<n> une version d'algorithme
npm run remix -- --preview <code>le même monde, puis validate et un témoin du solveur dessus (--max=<états>, 20 000 par défaut)
npm run remix -- --record=20les seeds de playtest : les N premières seeds de l'échantillon publié, chacune résolue et son témoin écrit en session dans games/<id>/playtests/remix-<seed>.session.json, que npm run playtests rejoue dans son propre monde
npm run verify:variantschaque mode : chaque monde d'un catalogue, l'échantillon d'un générateur (--sample=50), avec la couverture par dimension et par couple, les valeurs jamais choisies, les dominantes sur 1 000 seeds (--draws), un certificat par monde logique dans .cache/proofs/variants/ ; --prove la preuve exhaustive, --max=<états> (20 000 par défaut), --out=<fichier> le rapport que publie la release. Sortie 1 quand un monde échoue
npm run e2e:remixles mondes et les roues de code de 50 seeds comparés entre Node, Chromium, WebKit et Firefox (--seeds=, --browsers= ; sortie 3 quand un navigateur ne se lance pas, 0 avec --allow-skip)
npm run code-wheel -- --game <id> --format svg|pdfla roue de code imprimable (plus bas)

Un jeu sans manifeste remix n'a qu'un monde, l'histoire : npm run remix le dit et sort en 1.

Sauvegardes, replays et speedruns

Une sauvegarde est l'enveloppe v4 (SaveEnvelopeV4) : l'enveloppe v3 et le monde. Une sauvegarde faite avant 4.1.15 reçoit le monde histoire. Une sauvegarde n'est jamais chargée dans un autre monde : SaveWorldMismatch nomme le monde à reconstruire.

La session enregistre le monde (Session.variant), et un replay le reconstruit depuis l'affectation stockée, jamais depuis la seed avec un générateur plus récent. Les catégories de speedrun sont Story, Fixed (une seed publiée), Random (tirée au départ et montrée), Mystery et Daily ; Fixed et Daily classent chaque seed à part (leaderboardKey), et worldVerdict vérifie le monde d'un run contre ce que sa catégorie permet : voir Speedrun pour les auteurs. Les formats de fichiers sont dans Formats de fichiers.

Le défi du jour et Mystery

Le module quotidien du Bridge (bridge/src/daily.ts) signe la seed et les règles du jour pour 24 heures (GET /v1/daily) ; le joueur vérifie le jeton hors ligne avec la clé publique de remix.daily. Mystery est un engagement : POST /v1/commit publie le hash signé d'une seed et d'un nonce avant le run, GET /v1/reveal/:id la seed après. La seed du jour ne dépend que du jour, et une seed Mystery est fixée avant que son engagement soit signé : le Bridge ne choisit jamais une seed après avoir vu des actions (D26).

bridge serve le monte depuis la section daily d'une configuration (4.1.16 ; voir Installer et lancer le Bridge), et le menu Remix du lecteur a une entrée Défi du jour et (4.1.17) une entrée Monde mystère quand le jeu nomme son Bridge.

La roue de code

« The Extremely Legitimate Pirate Check » est un minijeu, code-wheel : deux disques tirés des personnages, symboles et réponses du jeu ; on tourne le petit disque jusqu'à placer un symbole sous un personnage, et on lit sa fenêtre. La seed la tire du flux copy-protection : la même seed donne la même roue partout. Le validateur vérifie que chaque roue a une solution ; le solveur la traite comme gagnable.

Modes (mode) : parody (le défaut : après trois mauvaises réponses, elle laisse passer), story, strict (ne laisse jamais passer, ne se passe pas), cosmetic (toute réponse est acceptée), disabled, daily.

Elle tourne avec des boutons, les flèches et une manette, annonce chaque tour, liste la roue entière en texte, et ne s'anime pas sous mouvement réduit. Elle se joue entièrement au clavier (les flèches la tournent, Tab et Entrée répondent, Échap passe) et à la manette (gauche et droite tournent, haut et bas choisissent une réponse, A confirme, B passe), et le focus revient au jeu à sa fermeture.

# dans le dépôt du moteur
npm run code-wheel -- --game <id> --format svg|pdf [--seed <code>|story] [--out <dir>]

imprime les disques, les repères de découpe, le trou central et un livret, en couleur et en version économique. Les pages SVG sont toujours écrites ; le PDF demande Pillow (npm run doctor dit s'il est là ; sans lui, la commande le dit et garde le SVG). C'est une reconstitution ludique, jamais un DRM ; la direction artistique est celle du jeu.

Son issue compte (4.1.16). La roue finit won, passed (la clémence de la parodie), skipped, failed (une roue story perdue après ses essais : l'histoire continue avec) ou disabled. Le moteur enregistre le résultat dans la session (le mg de l'entrée, réinjecté au rejeu comme un choix) et l'écrit dans le drapeau réservé minigame.code-wheel, que l'histoire lit avec { flag: 'minigame.code-wheel', eq: 'failed' } et que le journal rapporte en flagChanged. Une commande ne pose jamais un drapeau minigame.* (le validateur le refuse). Chaque minijeu dit comment il a fini dans le lecteur (passé, sinon gagné) ; celui qui ne dit rien (le solveur, le rejeu d'une session 4.1.15) n'enregistre rien.

Une catégorie de speedrun dit ce qu'elle permet de la roue dans world.codeWheel : skip: false la veut gagnée, enabled: false la veut non jouée (passée ou désactivée), medium vaut digital, physical ou either, et proof: 'transcript' (4.1.17) exige les réponses qui donnent le résultat. Voir Speedrun pour les auteurs.

Le joueur et le Studio

L'écran titre gagne Remix (après Nouvelle partie et Continuer) : l'histoire, un nouveau monde, une seed tapée ou le défi du jour. La page redémarre dans le monde choisi. Le menu pause montre le code à copier, ou « caché jusqu'à la fin » dans un mode masqué. Les options d'accessibilité ne touchent jamais la seed. Un lien nomme un monde (?seed=, ?daily=, ?world=).

L'onglet Remix du Studio liste les dimensions, prévisualise une seed, verrouille des dimensions et relance les autres, compare deux mondes, dessine un ordre d'énigmes, compte couverture et biais, liste les ancres et en ajoute une depuis la sélection de l'onglet Salles, et joue ou exporte un monde : voir Remix dans le Studio.