Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.3 KiB
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:
trait GameContextImport:
def importGameContext(input: String): Either[String, GameContext]
GameContextExport (modules/io)
Unchanged:
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:
def loadGame(importer: GameContextImport, input: String): Either[String, Unit]
Logic inside loadGame:
- Call
importer.importGameContext(input) - On
Left(err)→ returnLeft(err) - On
Right(ctx):ctx.moves.nonEmpty→ replay each move throughhandleParsedMove+completePromotion, then notifyPgnLoadedEventctx.moves.isEmpty→ callloadPosition(ctx)
Add symmetric export:
def exportGame(exporter: GameContextExport): String =
exporter.exportGameContext(context)
loadPosition is kept unchanged for direct GameContext injection (tests, GUI, reset).
Callers:
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 fromOptiontoEitherFenExporterTest— no changes expectedGameEngineLoadPgnTest— replaceengine.loadPgn(pgn)withengine.loadGame(PgnParser, pgn)
New test cases (in existing test files)
PgnParserTest—importGameContextreturnsRight(ctx)with correct final position andctx.movespopulated; returnsLeft(err)on invalid PGNPgnExporterTest—exportGameContext(ctx)generates valid PGN from a context with movesGameEngineLoadPgnTest/GameEngineTest—loadGame(FenParser, fen)sets position without replay;loadGame(PgnParser, pgn)replays moves and enables undo/redo;exportGamedelegates 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)