Spaces:
Running
Running
| """The Outcome Oracle's spine β CODE-owned, deterministic, model-free. | |
| The duel's verdict: the model must NEVER decide whether a gamble succeeds, or the | |
| roguelike isn't fair and the "why small models" story is decorative. So pass/fail | |
| and every reward magnitude are computed here from a seeded RNG + the TIER_TABLE. | |
| The router model only writes the *flavor sentence* for an outcome already decided. | |
| Deterministic by (seed, turn): the same dream seed always replays the same dice, | |
| which is what makes the Dream Journal card's "beat my run: seed abc123" honest. | |
| """ | |
| from __future__ import annotations | |
| import random | |
| from dataclasses import dataclass | |
| from .world import TIER_TABLE, WorldState, COURAGE_MAX | |
| class Outcome: | |
| tier: str | |
| result: str # "success" | "partial" | "fail" | |
| roll: int # 1..100, shown as the visible die | |
| fail_threshold: int # roll <= this = fail; shown on the HUD | |
| lucidity_cost: int | |
| progress_reward: int | |
| courage_gain: int | |
| menace_delta: int | |
| def won_roll(self) -> bool: | |
| return self.result != "fail" | |
| def _rng(seed: str, turn: int) -> random.Random: | |
| """Stable per-(seed, turn) stream β no global Math.random/Date dependence.""" | |
| return random.Random(f"{seed}:{turn}") | |
| def resolve(tier: str, seed: str, turn: int) -> Outcome: | |
| """Roll one gambit. Pure: identical (tier, seed, turn) -> identical Outcome.""" | |
| spec = TIER_TABLE.get(tier, TIER_TABLE["safe"]) | |
| rng = _rng(seed, turn) | |
| roll = rng.randint(1, 100) | |
| fail_threshold = int(spec["fail_chance"] * 100) | |
| if roll <= fail_threshold: | |
| result = "fail" | |
| elif roll <= fail_threshold + 20: # a 20-pt band above the line = grazed it | |
| result = "partial" | |
| else: | |
| result = "success" | |
| # Reward math, all code-owned. KEY DESIGN: the story ALWAYS proceeds toward the | |
| # goal β every gambit gives *some* progress, so the dream never stalls. The dice | |
| # decides the COST, not whether anything happens: | |
| # success β big leap forward, the Nightmare eases, Hobbes grows braver | |
| # partial β solid step forward, small cost | |
| # fail β you still stumble forward, BUT the Nightmare surges (extra lucidity | |
| # drain + menace) β that's the real, legible risk of a bold bet. | |
| reward = int(spec["progress_reward"]) | |
| cost = int(spec["lucidity_cost"]) | |
| if result == "fail": | |
| return Outcome(tier, result, roll, fail_threshold, | |
| lucidity_cost=cost + 1, # the dream destabilizes | |
| progress_reward=max(3, reward // 3), # still inch forward | |
| courage_gain=0, | |
| menace_delta=int(spec["menace_delta"]) + 2) # Nightmare surges | |
| if result == "partial": | |
| return Outcome(tier, result, roll, fail_threshold, | |
| lucidity_cost=cost, | |
| progress_reward=(reward * 2) // 3, | |
| courage_gain=0, | |
| menace_delta=int(spec["menace_delta"])) | |
| return Outcome(tier, result, roll, fail_threshold, | |
| lucidity_cost=cost, | |
| progress_reward=reward, | |
| courage_gain=int(spec["courage_gain"]), | |
| menace_delta=max(0, int(spec["menace_delta"]) - 1)) # a win eases pressure | |
| def apply(state: WorldState, out: Outcome) -> None: | |
| """Mutate world-state by an Outcome β the ONLY place gamble math touches state. | |
| One clean tension: PROGRESS climbs to 100 (wake with the prize) while LUCIDITY | |
| drains to 0 (lost in the dream). Every gambit advances progress; bolder gambits | |
| leap further but cost more lucidity, and a fail costs extra. Menace is the | |
| Nightmare's narrative pressure (drives the Nightmare beat), not a hidden drain β | |
| so the only way to lose stays legible: run out of lucidity. | |
| """ | |
| state.lucidity = max(0, state.lucidity - out.lucidity_cost) | |
| state.progress = min(100, state.progress + out.progress_reward) | |
| state.courage = min(COURAGE_MAX, state.courage + out.courage_gain) | |
| state.menace = max(0, state.menace + out.menace_delta) | |
| state.peak_menace = max(state.peak_menace, state.menace) | |
| if out.result == "fail": | |
| state.scars.append(f"a {out.tier} bet gone wrong (turn {state.turn})") | |
| if state.progress >= 100: | |
| # Reaching the prize on the same beat that drains you = out just in time. | |
| state.complete = True | |
| state.progress = 100 | |
| elif state.lucidity <= 0: | |
| state.lost = True | |