Handbook
Games architecture (LCDL vs deterministic engine)
This document describes forge_lcdl.games and how it relates to the rest of forge-lcdl. For a concise inventory (modules, tasks, gaps, backlog), see GAMES-ALPHA-STATUS.md. Task and API details: GAMES-ENGINE-API.md…
Thin engine responsibilities
The deterministic engine owns:
- Authoritative game state (including any committed randomness needed for reproducibility).
- Legal moves for the acting player, as stable identifiable options (e.g.
Move.move_id). apply_move: state transitions, terminal detection, scoring rules.player_view: JSON-serializable, privacy-preserving projection per player (hidden information stripped or summarized safely).
Engines must remain pure and testable: no network calls, no LLM transports, no prompt plumbing.
Thick LCDL layer responsibilities
The LCDL / games-integration layer owns:
- Contracts for LLM outputs (explain, coach, rank, negotiate in prose)—always downstream of engine legality.
- Parsing and validation: model-chosen identifiers must match engine-supplied
move_idvalues (never invented moves). - Orchestration: who acts when, plugging
run_task(...), injectingfake_chatin tests, etc. - Adapters: mapping
PlayerView+legal_movesinto governed task payloads (seeforge_lcdl.games.adapterswhen implemented).
The thick layer must not be the source of truth for rules; if the LLM contradicts legality, the engine rejects or validation fails before mutating authoritative state.
Why the engine must not import LLM / task / transport modules
Keeping forge_lcdl.games.engine (and sibling pure rules code) free of imports such as:
forge_lcdl.runnerforge_lcdl.transportforge_lcdl.genericforge_lcdl.messages
ensures:
- Clear dependency direction: core rules never depend on how chat is fetched or parsed.
- Extractability: rules can ship as
forge-lcdl-game-enginewithout pulling HTTP or gateway helpers. - Safer testing: boundary tests can statically verify engine sources omit forbidden imports.
Task code and adapters may import both games.engine and forge_lcdl.tasks / generic—that dependency arrow points inward toward the core, not outward from it.
Canonical play loop
state = game.setup(seed=..., …)— build initial authoritative state (optional RNG seed folded into setup as needed).view = game.player_view(state, player_id)— expose only what this player may know.moves = game.legal_moves(state, player_id)— engine enumerates options (sorted / stable convention per implementation).move_choice— human, script, or LLM constrained tomoves(via catalog tasks); the choice is aMove(or validatedmove_id).result = game.apply_move(state, move)— ifresult.ok, successor state isresult.state;result.eventscarriesTransitionEvententries; on failureresult.okis false andresult.errorexplains the rejection.
Repeat from step 2 until game.is_terminal(state).
scores = game.score(state)— interpret outcomes (ScoreResult.by_player, optionalnotes).
Rule: LLM tasks receive PlayerView + legal moves—not full hidden state
Governed prompts and JSON contracts should include:
player_view(or equivalent) constructed only throughgame.player_view(...).legal_movesas a finite list (Moveor{move_id, public_summary}) produced only throughgame.legal_moves(...).
They must not receive undisclosed opponents’ secrets (hands, unseen tiles, etc.). If the orchestrator mistakenly passes hidden fields, HiddenInfoLeakError documents the intent to catch violations in reviews or asserts.
Initial package layout (this repo)
| Path | Responsibility |
|---|---|
forge_lcdl.games |
Umbrella namespace (GAMES-ALPHA-STATUS.md); links companion docs (GAMES-ENGINE-API, …). |
forge_lcdl.games.errors |
Shared exceptions (IllegalMoveError, …). |
forge_lcdl.games.engine |
Game protocol and core models (stdlib-oriented). |
forge_lcdl.games.agents |
Policies and play orchestration hooks (hooks for LLM/human loops). |
forge_lcdl.games.tasks |
Helpers beside forge_lcdl.tasks for catalog wiring; schemas for typed JSON shapes. |
forge_lcdl.games.adapters |
web_json envelopes for PlayerView, legal_moves, apply_move outcomes. |
forge_lcdl.games.engine.mechanics |
Grid/graph topology utilities (SquareGrid, GraphBoard, path and line helpers). |
forge_lcdl.games.engine.reference |
Minimal rulesets + REFERENCE_GAMES_META / new_reference_game. |
forge_lcdl.games.engine.loop |
Dev/test harness (play_random_legal_game). |
forge_lcdl.games.engine.log / replay / rng |
Canonical hashing, deterministic replay auditing, seeded RNG helper. |
forge_lcdl.games.engine.serialization |
Canonical JSON facade + ensure_json_serializable. |
forge_lcdl.games.engine.cli |
python -m forge_lcdl.games.engine: list-reference-games, replay, random-play. |
Governed tasks (LCDL) for moves: game_move_parse, game_move_explain, game_move_rank (v1) under forge_lcdl.tasks; contracts in contracts/game_move_*/v1/. Validation ensures LLM move_id values are subsets of caller-supplied legal ids.
Existing forge_lcdl.game_engine is the current reference implementations registry (minimal rulesets). Sprint 0 adds forge_lcdl.games as the long-term façade and layout for new work; a later migration may wrap or port game_engine behind games.engine Game implementations.
Future split option
Rough packaging split:
| Package | Contents |
|---|---|
forge-lcdl-game-engine |
games.engine, core models, implementations that stay transport-free (and forge_lcdl.games.errors or a renamed sibling). |
forge-lcdl-games |
games.agents, games.tasks, games.adapters, optional presets that depend only on forge-lcdl APIs. |
forge-lcdl (core) |
Today’s governed tasks (forge_lcdl.tasks), generic, operators, TaskRunner—consumers optionally add forge-lcdl-games for richer board-game ergonomics |
Until that split happens, developing under forge_lcdl/games in-tree preserves the boundaries above.