semif-api-rocm

SemIf HTTP API and rocm flake
Log | Files | Refs | README | LICENSE

self-rules.js (4795B)


      1 "use strict";
      2 
      3 /* No-rules platformer: the same three terrain encodings as game-rules.js
      4  * (prose counts, run-length segments, ASCII row) with every line of how-to-play
      5  * prose removed. The model is never told the goal, what running or jumping do,
      6  * or which outcomes fail — it only sees neutral observations (its own state,
      7  * jumps remaining, the terrain) plus the action list with accurate one-word
      8  * descriptions, and an "insufficient" escape hatch. When play stalls or a run
      9  * fails, the client asks /plan for rules and injects the answer into later
     10  * states as "Rules".
     11  *
     12  * Physics is GameRules' (this module reads GameRules.floorAt/GOAL and only
     13  * assembles observation text); SelfRules.PLAN_GOAL is the simulation's
     14  * one-line statement of the goal — the only game-specific input to /plan.
     15  */
     16 const SelfRules = (() => {
     17   const { GOAL, floorAt } = GameRules;
     18 
     19   // Terrain ahead as prose counts — identical listing to GameRules.stateText,
     20   // minus the goal/rules lines and the hint.
     21   function terrainLines(player) {
     22     const terrain = [];
     23     for (let x = player.x + 1; x < GOAL; x++) {
     24       const kind = floorAt(x) ? "ground" : "hole";
     25       const last = terrain[terrain.length - 1];
     26       if (last && last.kind === kind) last.count++;
     27       else terrain.push({ kind, count: 1 });
     28     }
     29     return terrain.map(({ kind, count }) => `${count} ${kind} space${count === 1 ? "" : "s"}`);
     30   }
     31 
     32   function proseText(player, jumpsLeft) {
     33     return [
     34       `Player: ${player.onGround ? "standing on ground" : "airborne"}, facing right`,
     35       `Jumps remaining: ${jumpsLeft}`,
     36       "",
     37       // When the flag is the very next space, the enumeration would be an
     38       // empty list under a header — say the useful thing instead.
     39       player.x + 1 === GOAL ? "The flag is right in front of you!"
     40         : "Ahead, from nearest to farthest (starting with the next space):",
     41       ...terrainLines(player),
     42       player.x < GOAL ? "" : "Flag reached",
     43     ].join("\n");
     44   }
     45 
     46   // Run-length segments — same encoding as GameRules.runLengthText, with a
     47   // neutral legend (a `#` names the terrain, it does not announce a failure).
     48   function runLengthText(player, jumpsLeft) {
     49     const terrain = [];
     50     for (let x = player.x + 1; x < GOAL; x++) {
     51       const kind = floorAt(x) ? "ground" : "hole";
     52       const last = terrain[terrain.length - 1];
     53       if (last && last.kind === kind) last.count++;
     54       else terrain.push({ kind, count: 1 });
     55     }
     56     const segs = terrain.map(({ kind, count }) => `${kind === "ground" ? "-" : "#"}${count}`);
     57     segs.push("[!]");
     58     return [
     59       "Legend: [`*`: player, `>`: facing right, `-N`: N ground tiles, `#N`: N hole tiles, `!`: flag]",
     60       `Jumps remaining: ${jumpsLeft}`,
     61       "",
     62       `[*] > ${segs.join(" | ")}`,
     63     ].join("\n");
     64   }
     65 
     66   // Whole level as one symbolic row — same glyph layout as GameRules.asciiText,
     67   // same neutral legend, no goal line and no rules. Cells stay space-separated
     68   // so every glyph is its own token.
     69   function asciiText(player, jumpsLeft) {
     70     const here = Math.floor(player.x);
     71     const row = [];
     72     for (let x = 0; x <= GOAL; x++) {
     73       if (x === here) row.push("*");
     74       else if (x === GOAL) row.push("!");
     75       else row.push(floorAt(x) ? "-" : "#");
     76     }
     77     return [
     78       "Legend: [`*`: player, `-`: ground, `#`: hole, `!`: flag]",
     79       `Jumps remaining: ${jumpsLeft}`,
     80       "",
     81       row.join(" "),
     82     ].join("\n");
     83   }
     84 
     85   // mode: "prose" | "runlength" | "ascii". There is deliberately no guided
     86   // variant: guidance was instructional prose, which this demo never sends.
     87   function stateText(player, jumpsLeft, mode = "prose") {
     88     if (mode === "runlength") return runLengthText(player, jumpsLeft);
     89     if (mode === "ascii") return asciiText(player, jumpsLeft);
     90     return proseText(player, jumpsLeft);
     91   }
     92 
     93   const QUESTION = "What should the player do now?";
     94   // Actions only, with accurate one-word descriptions, plus the escape hatch
     95   // that gates the /plan trigger (Planner.INSUFFICIENT_THRESHOLD on its prob).
     96   const OPTIONS = [
     97     { id: "run", description: "Run" },
     98     { id: "jump", description: "Jump" },
     99     { id: Planner.INSUFFICIENT_ID, description: "Insufficient evidence to decide" },
    100   ];
    101 
    102   /* The simulation's one-line statement of the goal — the only game-specific
    103    * input to /plan. Planner.context() wraps it with the trigger note and the
    104    * rules currently in effect; the system prompt demanding lean, committal
    105    * instructions is universal and lives in the backend. */
    106   const PLAN_GOAL = "Reach the flag at the far right end of the level.";
    107 
    108   return { stateText, proseText, runLengthText, asciiText, terrainLines,
    109     QUESTION, OPTIONS, PLAN_GOAL };
    110 })();
    111 
    112 if (typeof module !== "undefined") module.exports = SelfRules;