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;