Files
NowChessSystems/docs/superpowers/specs/2026-04-05-io-interface-refactor-design.md
T
2026-04-05 09:33:50 +02:00

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 parseFenRight(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:

  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:

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 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)

  • PgnParserTestimportGameContext returns Right(ctx) with correct final position and ctx.moves populated; returns Left(err) on invalid PGN
  • PgnExporterTestexportGameContext(ctx) generates valid PGN from a context with moves
  • GameEngineLoadPgnTest / GameEngineTestloadGame(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)