Spaces:
Running
Running
File size: 4,763 Bytes
414dc55 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | """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"<div class='cz-step'><span class='cz-step-n'>{n}</span>"
f"<b>{title}</b><div class='cz-muted'>{body}</div></div>"
for n, title, body in steps
)
return (
"<div class='cz-howto'><div class='cz-howto-h'>HOW TO PLAY</div>"
f"<div class='cz-steps'>{cards}</div>"
"<div class='cz-howto-cta'>Press <b>NEW CASE</b> to open a fresh file, "
"then question your suspects.</div></div>"
)
def briefing_html(view: PlayerCaseView) -> str:
return (
f"<div class='cz-briefing'><b>{_esc(view.title)}</b><br><br>{_esc(view.briefing)}"
f"<br><br><i>Victim:</i> {_esc(view.victim.name)} ({_esc(view.victim.role)}) - "
f"{_esc(view.victim.cause_of_death)}.</div>"
)
def stage_html(scene_uri: str, sprite_uri: str, name: str, room_name: str = "") -> str:
room = f"<div class='cz-roomlabel'>{_esc(room_name)}</div>" if room_name else ""
return (
"<div class='cz-stage'>"
f"<div class='cz-room' style=\"background-image:url('{scene_uri}')\"></div>"
f"<div class='cz-nameplate'>{_esc(name)}</div>{room}"
f"<div class='cz-actor'><div id='cz-sprite' class='cz-sprite' "
f"style=\"background-image:url('{sprite_uri}')\"></div></div>"
"</div>"
)
def dialogue_html(name: str, line: str, streaming: bool = False) -> str:
caret = "<span class='cz-caret'>█</span>" if streaming else ""
body = _esc(line) if line else "<span class='cz-muted'>...</span>"
who = _esc(name.upper()) if name else " "
return f"<div class='cz-dialogue'><div class='who'>{who}</div><div class='line'>{body}{caret}</div></div>"
def scene_panel_html(scene_uri: str, caption: str) -> str:
return (
f"<div class='cz-scene'><img src='{scene_uri}' alt=''>"
f"<div class='cap'>{_esc(caption)}</div></div>"
)
def evidence_html(items: list[tuple[str, str, str]]) -> str:
"""items: (name, reveal, image_uri)."""
if not items:
return "<div class='cz-muted'>No evidence yet. Search the rooms.</div>"
# Each card is a <details>: 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 = [
"<details class='cz-slot'><summary>"
f"<img src='{uri}' alt=''>"
f"<div class='bd'><div class='nm'>{_esc(name)}</div>"
f"<div class='ds'>{_esc(reveal[:300])}</div></div>"
"</summary></details>"
for name, reveal, uri in items
]
return f"<div class='cz-evi'>{''.join(slots)}</div>"
def notebook_html(state: GameState, case: CaseFile) -> str:
if not state.notebook:
return "<div class='cz-notebook'><div class='nb cz-muted'>The page is blank. Question them and search the rooms.</div></div>"
rows = []
for entry in reversed(state.notebook):
cls = entry.kind.value
rows.append(f"<div class='nb {cls}'><b>{entry.kind.value.upper()}</b> · {_esc(entry.text)}</div>")
return f"<div class='cz-notebook'>{''.join(rows)}</div>"
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"<div class='cz-verdict'><div style='color:{head_color};font-size:1.2rem'>{head}</div>",
f"<div style='margin:8px 0'>Score: <b style='color:#e0ab4a'>{verdict.score}/100</b></div>",
f"<div style='margin-bottom:8px'>{_esc(verdict.rationale)}</div>",
"<div class='cz-muted'><b>Director's Cut:</b><ul>"]
parts += [f"<li>{_esc(step)}</li>" for step in verdict.deduction_chain]
parts.append("</ul></div></div>")
return "".join(parts)
|