forge-lcdl

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_id values (never invented moves).
  • Orchestration: who acts when, plugging run_task(...), injecting fake_chat in tests, etc.
  • Adapters: mapping PlayerView + legal_moves into governed task payloads (see forge_lcdl.games.adapters when 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.runner
  • forge_lcdl.transport
  • forge_lcdl.generic
  • forge_lcdl.messages

ensures:

  1. Clear dependency direction: core rules never depend on how chat is fetched or parsed.
  2. Extractability: rules can ship as forge-lcdl-game-engine without pulling HTTP or gateway helpers.
  3. 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

  1. state = game.setup(seed=..., …) — build initial authoritative state (optional RNG seed folded into setup as needed).
  2. view = game.player_view(state, player_id) — expose only what this player may know.
  3. moves = game.legal_moves(state, player_id) — engine enumerates options (sorted / stable convention per implementation).
  4. move_choice — human, script, or LLM constrained to moves (via catalog tasks); the choice is a Move (or validated move_id).
  5. result = game.apply_move(state, move) — if result.ok, successor state is result.state; result.events carries TransitionEvent entries; on failure result.ok is false and result.error explains the rejection.

Repeat from step 2 until game.is_terminal(state).

  1. scores = game.score(state) — interpret outcomes (ScoreResult.by_player, optional notes).

Governed prompts and JSON contracts should include:

  • player_view (or equivalent) constructed only through game.player_view(...).
  • legal_moves as a finite list (Move or {move_id, public_summary}) produced only through game.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.