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] =