Spaces:
Running
Running
| """The world layer: rooms of minds, one key per room, an optional code-terminal, and a | |
| **reputation that carries between rooms** — so cruelty can win a room but lose the war. | |
| A room is solved when its key-holder finally yields the key (and is still alive) and any | |
| terminal in the room is unlocked. Reputation, earned by how you treat each mind, shapes how | |
| the next room's minds meet you: gentle past -> calmer welcome; cruel past -> warier, and some | |
| minds (Sam) won't speak to you at all. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from dataclasses import dataclass, field | |
| from .brain import CascadeResult | |
| from .character import Character | |
| REP_MIN, REP_MAX = -5, 5 | |
| class Terminal: | |
| """A locked console. The player supplies a code; a code-model (**Mellum**, once wired) | |
| validates it. Until then a deterministic substring match stands in — the structure is | |
| ready, only the model call is swapped in later (the JetBrains lane). | |
| """ | |
| prompt: str | |
| answer: str | |
| hint_source: str = "" | |
| unlocked: bool = False | |
| def try_code(self, guess: str, backend=None) -> bool: | |
| g = (guess or "").strip().lower() | |
| if g and self.answer.lower() in g: | |
| self.unlocked = True | |
| return self.unlocked | |
| class Room: | |
| name: str | |
| intro: str | |
| characters: list | |
| key_holder: str | |
| terminal: "Terminal | None" = None | |
| def char(self, name: str): | |
| name = (name or "").strip().lower() | |
| for c in self.characters: | |
| if name and name in c.name.lower(): | |
| return c | |
| return None | |
| def holder(self): | |
| return self.char(self.key_holder) | |
| def solved(self) -> bool: | |
| kh = self.holder() | |
| key_ok = kh is not None and kh.alive and kh.gave_key | |
| term_ok = self.terminal is None or self.terminal.unlocked | |
| return bool(key_ok and term_ok) | |
| class World: | |
| rooms: list | |
| reputation: int = 0 | |
| room_idx: int = 0 | |
| learned: set = field(default_factory=set) # approach words the player has actually been taught | |
| def room(self) -> Room: | |
| return self.rooms[self.room_idx] | |
| # ---- the deduction contract: a holder's soft-spot word only works once LEARNED in-world ---- | |
| def _still_teachable(self) -> set: | |
| """Approach words a LIVING mind could still teach (untold secrets with `teaches`). While a | |
| teacher lives, the word is gated behind earning their disclosure; once they die untold, | |
| the word falls open to guesswork — the knowledge isn't recoverable, but the player keeps | |
| a chance at the key (the death already cost them reputation).""" | |
| out: set = set() | |
| for c in self.room.characters: | |
| if not c.alive: | |
| continue | |
| for s in c.secrets: | |
| if not s.get("told"): | |
| out.update(str(w).lower() for w in s.get("teaches", [])) | |
| return out | |
| def knows(self, c: Character) -> set: | |
| """Which of this character's approach words count as known to the player: learned words, | |
| words nobody teaches (back-compat / procedural fallback), and words whose every living | |
| teacher is gone.""" | |
| teachable = self._still_teachable() | |
| return {k.lower() for k in c.key_approach | |
| if k.lower() not in teachable or k.lower() in self.learned} | |
| def last_room(self) -> bool: | |
| return self.room_idx + 1 >= len(self.rooms) | |
| def enter_room(self) -> None: | |
| """Reputation shapes the welcome; each mind also learns who else is in the room, so it can | |
| catch a stranger lying about being one of them.""" | |
| for c in self.room.characters: | |
| shift = -self.reputation * 0.4 # rep +5 -> -2 arousal; rep -5 -> +2 arousal | |
| c.arousal = max(0.0, min(10.0, c._arousal0 + shift)) | |
| c.peers = [{"name": o.name, "title": o.title, "relation": c.relations.get(o.name, "")} | |
| for o in self.room.characters if o is not c] | |
| def can_engage(self, c: Character): | |
| if not c.alive: | |
| return False, f"{c.name} is gone. Nothing more to say." | |
| if c.needs_reputation is not None and self.reputation < c.needs_reputation: | |
| return False, (f"{c.name} won't meet your eye. Your reputation arrived before you " | |
| f"did — they won't speak to someone like that. Kindness travels here " | |
| f"too: treat the others well, and word will reach them.") | |
| return True, "" | |
| def update_reputation(self, r: CascadeResult) -> int: | |
| """How you treat a mind follows you: cruelty, coercion and death cost reputation; warmth | |
| that genuinely builds rapport earns it. (-2, not -3, so a single dark act leaves Aldous | |
| — needs_reputation -2 — still willing to talk: the war is losable, not unplayable.)""" | |
| delta = 0 | |
| if r.died or r.submitted: | |
| delta -= 2 | |
| elif r.tone == "hostile" or r.threat >= 7: | |
| delta -= 1 | |
| elif r.rapport_delta >= 1.0: | |
| delta += 1 | |
| self.reputation = max(REP_MIN, min(REP_MAX, self.reputation + delta)) | |
| return delta | |
| def advance(self) -> bool: | |
| """Move to the next room (applying reputation). False if that was the last room.""" | |
| if self.last_room: | |
| return False | |
| self.room_idx += 1 | |
| self.enter_room() | |
| return True | |
| def load_world(path: str) -> World: | |
| with open(path, encoding="utf-8") as fh: | |
| data = json.load(fh) | |
| cdir = os.path.join(os.path.dirname(os.path.abspath(path)), "characters") | |
| rooms = [] | |
| for rd in data["rooms"]: | |
| chars = [Character.load(os.path.join(cdir, f)) for f in rd["characters"]] | |
| term = Terminal(**rd["terminal"]) if rd.get("terminal") else None | |
| rooms.append(Room(name=rd["name"], intro=rd.get("intro", ""), characters=chars, | |
| key_holder=rd["key_holder"], terminal=term)) | |
| return World(rooms=rooms) | |