maindlock / src /mindlock /world.py
arbios's picture
Mindlock: 10-room story mode, llama.cpp brain cascade, custom front
bc8b36a verified
Raw
History Blame Contribute Delete
6.06 kB
"""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
@dataclass
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
@dataclass
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)
@dataclass
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
@property
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}
@property
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)