# 030 — Architecture and clean-code boundaries

## Core design

One shared modern modal shell and launch card; two explicit game adapters/engines. Story is composition, not inheritance from the large Millionaire controller. The modal owns window/focus/navigation; an adapter owns its engine and runtime work; content prepares a whole pack; a pure view maps state to properties.

```text
Existing entry + Subject + source context
  -> existing modal shell + shared title/settings/mode choice
       -> Millionaire adapter -> existing controller/source/view
       -> Story adapter -> Story engine + Story pack source + Story view

Story AI source -> shared AIProvider -> existing selected provider client
Story demo source -> authored Matrix JSON -> same pack validator
Both -> existing textures/UI primitives and optional voice providers
```

## Implemented file ownership

The following files now exist. Reconcile them with any later refactor; preserve these responsibilities and avoid competing entry modules.

```text
wcs/games/story/
  __init__.py       small public exports; no second title picker or router
  engine.py         pure state transitions, lives, counters, terminal precedence
  content.py        pack types, validation, normalization and demo loading
  ai.py             one-shot generation through AIProvider, one transient session
  prompts.py        narrator/whole-pack instructions and selected-work context
  adapter.py        shell-facing intent handling, loading worker, voice, property publication
  view.py           pure Story state -> property values

resources/demo/story/matrix/
  easy.json         fixed ten-scene pack
  medium.json       fixed fifteen-scene pack
  hard.json         fixed twenty-scene pack

scripts/game_dialog/story_screens.py   Story assembly using existing generator primitives/tokens
```

Keep the demo loader in `content.py` initially; do not create separate source registries, plugin interfaces, schemas packages, or repository layers for three fixture files. Split a module only when its actual responsibilities warrant it.

## What to reuse

| Concern | Owner / reuse rule |
|---|---|
| Selected work and URL entry | Existing `Subject` and canonical `wcs.games.millionaire` entry |
| Modal lifecycle, Back confirmation, settings overlay | Existing `MilionarGameModal`, with narrow adapter-owned hooks |
| XML geometry, textures, button focus, labels, autoscroll | Existing generator primitives; extract once if necessary |
| Settings read/write/localized value helpers | Existing `game_settings`; add explicit mode-scoped row selection |
| LLM transport, model selection, HTTP errors, accounting | Existing `AIProvider`; no duplicate API-key check or cost tracking |
| Voice settings/providers/stop behavior | Existing `TtsVoice` / `SilentVoice`; Story passes narrator text, no duplicate voice class |
| Kodi stubs, modal driver, live capture, probe | Existing harness; add Story scenarios and snapshots |
| Letter redistribution | Story shuffles normalized option indexes locally; Millionaire's existing placement algorithm stays unchanged |

Do not make Story implement `welcome/question/feedback/tip` merely to fit the existing round source. A whole-pack `prepare(subject, difficulty)` interface is enough. Demo returns the same validated pack shape; AI returns a fresh one.

## Pure engine

The engine must import neither Kodi nor provider clients. It owns the validated active pack, difficulty, scene index, remaining lives, answered/correct counters, current selected answer/result, and terminal outcome.

Suggested engine states: `question`, `feedback`, `won`, `lost`, `closed`. The adapter/shell handles `title`, `rules`, `settings`, `loading`, and `error`. Keep the current final scene's reveal accessible on terminal screens without an artificial delay or a duplicate evaluation.

| Input | From | Effect |
|---|---|---|
| Start with validated pack | New attempt | Reset all counters/lives; show first scene |
| Answer A–D | Question only | Evaluate once, count answered, decrement on error, set feedback or terminal state |
| Continue | Nonterminal feedback only | Advance exactly one scene; show question |
| Answer while feedback/terminal | Any non-question state | Ignore |
| Continue while question/terminal | Wrong state | Ignore |
| Close | Any state | Retire attempt and drop active pack |

Evaluation order: record the single answer -> update counters/lives -> if lives are zero, lose -> else if the final scene was answered, win -> else feedback. Derive chapter and chapter progress from the scene positions and total count; do not maintain redundant mutable chapter counters.

Distinguish `answered_count` from `correct_count`; derive mistakes from their difference and progress from answered scenes. Avoid a second score subsystem.

## Shell-facing adapter seam

The common shell should use a small concrete interface shared by the existing and Story adapters, such as `start()`, `act(intent)`, `screen()`, `focus_target()`, `retire()`, and `cleanup()`. This is a duck-typed seam, not a new inheritance framework.

- Move the current Millionaire-specific retirement steps into its existing adapter, preserving order/behavior.
- Replace shell access to Millionaire's controller state with the adapter's screen/focus result.
- Instantiate exactly one active adapter. Retire it fully before a title-mode switch or return-to-title replacement.
- Do not send Story intents to Millionaire's CHOICES table, money view, timers, or lifelines.
- Keep existing entry signatures/imports usable. `milionar_game` remains the public action; its historic name is not a reason to duplicate routing.

## Generation and cancellation

Story has one preparation worker per new attempt, not a thread per scene. Use the existing project's threading approach and a request generation token.

1. Capture an immutable subject, difficulty, source context, and generation number.
2. Set Loading and disable Start/repeated inputs.
3. Prepare and validate the complete pack off the UI thread.
4. Publish only if the modal/adapter is still active and the generation number still matches.
5. On Back, close, retry, source/mode change, or replay, invalidate the previous request and release its result.

Do not block closing by joining a provider HTTP request. Transport timeouts belong to the provider clients. Cancellation may suppress publication even when it cannot abort an in-flight request or its charge; do not silently launch replacement requests.

The single-answer guard must take effect synchronously before property/TTS publication. Generation responses arriving late never reactivate a closed attempt or decrement lives.

## Voice and cleanup

Reuse `SilentVoice` for offline text checks. Bind Story setup/question/options/reveal to the existing TTS capabilities without introducing a moderator persona. Narration text must never include the hidden correct answer before evaluation.

The player can Continue before speech finishes. Stop/invalidate obsolete game speech when advancing or leaving, using the existing `stop_tts()` boundary; check for late TTS workers as well as late LLM workers. Do not add a five-second wait or new global voice defaults. Ensure an old game voice cannot start over the next scene; preserve unrelated video playback.

On retirement: invalidate callbacks/request tokens, prevent further publication, stop this game's speech, detach probe where appropriate, clear this adapter's properties, and release the pack. Cleanup is idempotent.

## Mandatory quality review

- No copied `MillionaireGameController`, AI client, TTS transport, XML builder, or metadata fetcher.
- No mode checks scattered through every Millionaire rule. Mode choice belongs at launcher/adapter/view boundaries.
- No speculative framework, dynamic registry, global mutable Story singleton, or `except: pass` hiding generation errors.
- One authoritative content schema/limits; one life/terminal transition; one owner per property.
- Pure engine/view are inspectable independently; meaningful behavior tests cover them.
- Shared extraction preserves existing default function output. Do not refactor unrelated classic/legacy code to make the diff look cleaner.
- Use existing repository style and stdlib where sufficient. New package dependencies require an actual need.
