Spaces:
Running
Running
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55 | """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) | |