# IO Interface Refactor Design Date: 2026-04-05 ## Goal Make `GameEngine` accept any IO format (FEN, PGN, future formats) through a uniform interface, so callers never depend on format-specific classes directly. ## Problem `GameContextImport` returns `Option[GameContext]`, losing error messages. `PgnParser` and `PgnExporter` do not implement either interface — the engine imports `PgnParser` directly in `loadPgn`. This breaks the abstraction the interfaces are meant to provide. ## Interface Changes ### `GameContextImport` (modules/io) Change return type from `Option` to `Either`: ```scala trait GameContextImport: def importGameContext(input: String): Either[String, GameContext] ``` ### `GameContextExport` (modules/io) Unchanged: ```scala trait GameContextExport: def exportGameContext(context: GameContext): String ``` ## Implementations | Class | Trait | Behaviour | |---|---|---| | `FenParser` | `GameContextImport` | `parseFen` → `Right(ctx)` or `Left("Invalid FEN: …")` | | `FenExporter` | `GameContextExport` | unchanged — delegates to `gameContextToFen` | | `PgnParser` | `GameContextImport` | calls `validatePgn`; maps `Right(game)` to final `GameContext` with `moves` populated via `DefaultRules.applyMove`; passes through `Left(err)` | | `PgnExporter` | `GameContextExport` | generates PGN from `ctx.moves` with default headers | `PgnParser` retains `parsePgn`, `validatePgn`, and `parseAlgebraicMove` as its own public API. `importGameContext` is the additional uniform entry point. ## GameEngine Changes (modules/core) Remove `loadPgn(pgn: String)`. Add: ```scala def loadGame(importer: GameContextImport, input: String): Either[String, Unit] ``` Logic inside `loadGame`: 1. Call `importer.importGameContext(input)` 2. On `Left(err)` → return `Left(err)` 3. On `Right(ctx)`: - `ctx.moves.nonEmpty` → replay each move through `handleParsedMove` + `completePromotion`, then notify `PgnLoadedEvent` - `ctx.moves.isEmpty` → call `loadPosition(ctx)` Add symmetric export: ```scala def exportGame(exporter: GameContextExport): String = exporter.exportGameContext(context) ``` `loadPosition` is kept unchanged for direct `GameContext` injection (tests, GUI, reset). Callers: ```scala engine.loadGame(PgnParser, pgn) // game with history → replay engine.loadGame(FenParser, fen) // position snapshot → set position engine.exportGame(FenExporter) engine.exportGame(PgnExporter) ``` ## Testing ### Updates to existing tests - `FenParserTest` — update assertions from `Option` to `Either` - `FenExporterTest` — no changes expected - `GameEngineLoadPgnTest` — replace `engine.loadPgn(pgn)` with `engine.loadGame(PgnParser, pgn)` ### New test cases (in existing test files) - `PgnParserTest` — `importGameContext` returns `Right(ctx)` with correct final position and `ctx.moves` populated; returns `Left(err)` on invalid PGN - `PgnExporterTest` — `exportGameContext(ctx)` generates valid PGN from a context with moves - `GameEngineLoadPgnTest` / `GameEngineTest` — `loadGame(FenParser, fen)` sets position without replay; `loadGame(PgnParser, pgn)` replays moves and enables undo/redo; `exportGame` delegates correctly to both exporters ## Out of Scope - Adding new formats (no new parsers/exporters in this change) - PGN header customisation on export (default headers only for now) - Changes to `GameHistory` (already deprecated, not touched)