| """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 |
| |
| |
| |
| 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 |
| consecutive_praise: int = 0 |
| romance_active: bool = False |
|
|
|
|
| @dataclass |
| class GameState: |
| session_id: str = "" |
| phase: str = "title" |
| 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 |
| crises_since_salary: int = 0 |
| fallback_count: int = 0 |
| game_over: bool = False |
| review: dict | None = None |
| |
| chats_this_gap: dict[str, int] = field(default_factory=dict) |
| idle_done_this_gap: bool = False |
| chat_session: dict | None = None |
| 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()}, |
| |
| "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() |
|
|