From 7c42260d148b708d4f9e46cc727ca13a803e4429 Mon Sep 17 00:00:00 2001 From: Janis Date: Sun, 5 Apr 2026 13:14:25 +0200 Subject: [PATCH] feat(io): implement PgnParser.importGameContext with move replay Implement the importGameContext method to validate PGN input via validatePgn() and replay each move using DefaultRules.applyMove to populate GameContext.moves. Returns final GameContext with all moves applied or error message on failure. Co-Authored-By: Claude Sonnet 4.6 --- .../scala/de/nowchess/io/pgn/PgnParser.scala | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala index 20ffe5d..136de4a 100644 --- a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala +++ b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala @@ -23,6 +23,35 @@ object PgnParser: val moveText = rest.mkString(" ") validateMovesText(moveText).map(moves => PgnGame(headers, moves)) + /** Import a PGN text into a GameContext by validating and replaying all moves. + * Returns Right(GameContext) with all moves applied and .moves populated. + * Returns Left(error message) if validation fails or move replay encounters an issue. */ + def importGameContext(input: String): Either[String, GameContext] = + validatePgn(input).flatMap { game => + // Replay moves to populate GameContext.moves via DefaultRules.applyMove + val (finalCtx, errors) = game.moves.foldLeft((GameContext.initial, Option.empty[String])) { + case ((ctx, Some(err)), _) => (ctx, Some(err)) // Already failed, stop + case ((ctx, None), histMove) => + val moveOpt = parseAlgebraicMove( + s"${histMove.from}${histMove.to}", + ctx, + ctx.turn + ) + moveOpt match + case None => (ctx, Some(s"Failed to parse move ${histMove.from}${histMove.to}")) + case Some(move) => + val nextCtx = DefaultRules.applyMove(ctx, move) + (nextCtx, None) + } + errors match + case Some(err) => Left(err) + case None => + if finalCtx.moves.isEmpty && game.moves.nonEmpty then + Left("No moves were parsed from the PGN") + else + Right(finalCtx) + } + /** Parse a complete PGN text into a PgnGame with headers and moves. * Always succeeds (returns Some); malformed tokens are silently skipped. */ def parsePgn(pgn: String): Option[PgnGame] =