qFelix's picture
Brad Did Something - Gradio/FastAPI Space on HF
78c0f6e
Raw
History Blame Contribute Delete
5.31 kB
"""GameState dataclass, in-memory session store, and the client-safe snapshot.
The snapshot NEVER includes morale, relationship scores, or constraint flags —
only npc_moods strings and an ambient band (AGENTS.md non-negotiables).
"""
from __future__ import annotations
import threading
import uuid
from collections import OrderedDict
from dataclasses import dataclass, field
from .schemas import NPC_IDS
TARGET = 1_000_000
# small opening buffer so early losses register on the meter instead of
# silently flooring at zero (the board reads the applied figure, not the
# model's claim — see validator). ~15% of target keeps difficulty intact.
START_REVENUE = 150_000
START_BUDGET = 50_000
START_POCKET = 3_000
START_MORALE = 65
START_RELATIONSHIP = 50
TOTAL_CRISES = 15
PRESENTATION_EVENTS = (4, 8, 15)
MAX_SESSIONS = 200
@dataclass
class NpcState:
relationship: int = START_RELATIONSHIP
gifts_received: int = 0
mood: str = "normal"
incident_count: int = 0
personal_situation: str | None = None
recent_events: list[str] = field(default_factory=list)
last_gift_event: int = -10 # crisis number of last gift, for half-value rule
consecutive_praise: int = 0
romance_active: bool = False # the player is dating this NPC
@dataclass
class GameState:
session_id: str = ""
phase: str = "title" # title | free_roam | crisis | presentation | review
crisis_number: int = 0
revenue: int = START_REVENUE
target: int = TARGET
company_budget: int = START_BUDGET
bonuses_issued: int = 0
total_bonus_spend: int = 0
pocket_money: int = START_POCKET
bribes_accepted: int = 0
morale: int = START_MORALE
boss_title: str = "Head of Sales and Partnerships"
npcs: dict[str, NpcState] = field(
default_factory=lambda: {n: NpcState() for n in NPC_IDS})
board_scrutiny: str = "low"
scrutiny_high_streak: int = 0
consecutive_harsh: int = 0
consecutive_fine_whatever: int = 0
hr_alert: bool = False
newspaper_count: int = 0
event_log: list[str] = field(default_factory=list)
paper_trail: list[dict] = field(default_factory=list)
current_event: dict | None = None
special_drought: int = 0
queued_special: str | None = None
pending_bribe: int = 0
presentation: dict | None = None # {round, transcript, board_tone, ...}
crises_since_salary: int = 0
fallback_count: int = 0
game_over: bool = False
review: dict | None = None
# idle-activity budgets, reset every roam gap (events.next_event)
chats_this_gap: dict[str, int] = field(default_factory=dict)
idle_done_this_gap: bool = False
chat_session: dict | None = None # {npc_id, turns}
pending_email: dict | None = None
def npc(self, npc_id: str) -> NpcState:
return self.npcs[npc_id]
def log(self, text: str) -> None:
self.event_log.append(text)
def trail(self, npc: str, text: str, delta: int) -> None:
self.paper_trail.append({"npc": npc, "text": text, "delta": delta})
@property
def morale_band(self) -> str:
m = self.morale
if m >= 70:
return "high"
if m >= 50:
return "normal"
if m >= 30:
return "tired"
if m >= 20:
return "low"
return "critical"
def snapshot(self) -> dict:
"""Client-safe view. No hidden numbers ever leave this function."""
return {
"revenue": self.revenue,
"target": self.target,
"pocket_money": self.pocket_money,
"boss_title": self.boss_title,
"crisis_number": self.crisis_number,
"total_crises": TOTAL_CRISES,
"paper_trail": self.paper_trail[-30:],
"npc_moods": {n: s.mood for n, s in self.npcs.items()},
# coarse romance state only — never the raw relationship number
"npc_romance": {
n: ("active" if s.romance_active
else "available" if s.relationship >= 65 else "none")
for n, s in self.npcs.items()
},
"gift_available": self.pocket_money >= 50,
"bonus_available": self.company_budget >= 5_000,
"hr_alert": self.hr_alert,
"email_waiting": self.pending_email is not None,
"ambient": "gloomy" if self.morale < 20 else "normal",
"newspaper_on_floor": self.newspaper_count > 0,
"phase": self.phase,
"game_over": self.game_over,
}
class SessionStore:
def __init__(self, cap: int = MAX_SESSIONS):
self._cap = cap
self._sessions: OrderedDict[str, GameState] = OrderedDict()
self._lock = threading.Lock()
def create(self) -> GameState:
with self._lock:
sid = uuid.uuid4().hex
state = GameState(session_id=sid)
self._sessions[sid] = state
while len(self._sessions) > self._cap:
self._sessions.popitem(last=False)
return state
def get(self, session_id: str) -> GameState | None:
with self._lock:
state = self._sessions.get(session_id)
if state is not None:
self._sessions.move_to_end(session_id)
return state
STORE = SessionStore()