"""Render game data to themed, animated HTML. The suspect's hidden ``internal`` state is never rendered; only ``spoken`` text and a few derived in-world cues reach the player. The stage and notebook are deterministic CSS-animated graphics - no model runs to draw them. """ from __future__ import annotations import html from ..engine.game_state import GameState from ..projections.player_view import PlayerCaseView from ..schemas.accusation import Verdict from ..schemas.case import CaseFile def _esc(text: str) -> str: return html.escape(text or "") def how_to_play_html() -> str: steps = [ ("1", "INTERROGATE", "Pick a suspect and ask anything. They remember, and they lie."), ("2", "INVESTIGATE", "Search the rooms to collect physical evidence."), ("3", "CONFRONT", "Present a clue that contradicts a suspect - watch their alibi crack."), ("4", "ACCUSE", "Name the killer and cite your proof. One of them did it."), ] cards = "".join( f"
{n}" f"{title}
{body}
" for n, title, body in steps ) return ( "
HOW TO PLAY
" f"
{cards}
" "
Press NEW CASE to open a fresh file, " "then question your suspects.
" ) def briefing_html(view: PlayerCaseView) -> str: return ( f"
{_esc(view.title)}

{_esc(view.briefing)}" f"

Victim: {_esc(view.victim.name)} ({_esc(view.victim.role)}) - " f"{_esc(view.victim.cause_of_death)}.
" ) def stage_html(scene_uri: str, sprite_uri: str, name: str, room_name: str = "") -> str: room = f"
{_esc(room_name)}
" if room_name else "" return ( "
" f"
" f"
{_esc(name)}
{room}" f"
" "
" ) def dialogue_html(name: str, line: str, streaming: bool = False) -> str: caret = "" if streaming else "" body = _esc(line) if line else "..." who = _esc(name.upper()) if name else " " return f"
{who}
{body}{caret}
" def scene_panel_html(scene_uri: str, caption: str) -> str: return ( f"
" f"
{_esc(caption)}
" ) def evidence_html(items: list[tuple[str, str, str]]) -> str: """items: (name, reveal, image_uri).""" if not items: return "
No evidence yet. Search the rooms.
" # Each card is a
: the description is clamped to 3 lines, click to expand the # full text (native HTML, no JS needed). The "+"/"-" corner marker shows it is toggleable. slots = [ "
" f"" f"
{_esc(name)}
" f"
{_esc(reveal[:300])}
" "
" for name, reveal, uri in items ] return f"
{''.join(slots)}
" def notebook_html(state: GameState, case: CaseFile) -> str: if not state.notebook: return "
The page is blank. Question them and search the rooms.
" rows = [] for entry in reversed(state.notebook): cls = entry.kind.value rows.append(f"
{entry.kind.value.upper()} · {_esc(entry.text)}
") return f"
{''.join(rows)}
" def verdict_html(case: CaseFile, verdict: Verdict) -> str: head_color = "#7bbf73" if verdict.solved else "#d2603f" head = "CASE SOLVED" if verdict.solved else "CASE UNSOLVED" parts = [f"
{head}
", f"
Score: {verdict.score}/100
", f"
{_esc(verdict.rationale)}
", "
Director's Cut:
    "] parts += [f"
  • {_esc(step)}
  • " for step in verdict.deduction_chain] parts.append("
") return "".join(parts)