refactor: NCS-22 NCS-23 reworked modules and tests (#17)
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package de.nowchess.io.fen
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class FenExporterTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def context(
|
||||
piecePlacement: String,
|
||||
turn: Color,
|
||||
castlingRights: CastlingRights,
|
||||
enPassantSquare: Option[Square],
|
||||
halfMoveClock: Int,
|
||||
moveCount: Int
|
||||
): GameContext =
|
||||
val board = FenParser.parseBoard(piecePlacement).getOrElse(
|
||||
fail(s"Invalid test board FEN: $piecePlacement")
|
||||
)
|
||||
val dummyMove = Move(Square(File.A, Rank.R2), Square(File.A, Rank.R3))
|
||||
GameContext(
|
||||
board = board,
|
||||
turn = turn,
|
||||
castlingRights = castlingRights,
|
||||
enPassantSquare = enPassantSquare,
|
||||
halfMoveClock = halfMoveClock,
|
||||
moves = List.fill(moveCount)(dummyMove)
|
||||
)
|
||||
|
||||
test("exportGameContextToFen handles initial and typical developed position"):
|
||||
FenExporter.gameContextToFen(GameContext.initial) shouldBe
|
||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||
|
||||
val gameContext = context(
|
||||
piecePlacement = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR",
|
||||
turn = Color.Black,
|
||||
castlingRights = CastlingRights.All,
|
||||
enPassantSquare = Some(Square(File.E, Rank.R3)),
|
||||
halfMoveClock = 0,
|
||||
moveCount = 0
|
||||
)
|
||||
FenExporter.gameContextToFen(gameContext) shouldBe
|
||||
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
|
||||
|
||||
test("export handles castling rights variants and en-passant with counters"):
|
||||
val noCastling = context(
|
||||
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
||||
turn = Color.White,
|
||||
castlingRights = CastlingRights.None,
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moveCount = 0
|
||||
)
|
||||
FenExporter.gameContextToFen(noCastling) shouldBe
|
||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1"
|
||||
|
||||
val partialCastling = context(
|
||||
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
||||
turn = Color.White,
|
||||
castlingRights = CastlingRights(
|
||||
whiteKingSide = true,
|
||||
whiteQueenSide = false,
|
||||
blackKingSide = false,
|
||||
blackQueenSide = true
|
||||
),
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 5,
|
||||
moveCount = 4
|
||||
)
|
||||
FenExporter.gameContextToFen(partialCastling) shouldBe
|
||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Kq - 5 3"
|
||||
|
||||
val withEnPassant = context(
|
||||
piecePlacement = "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR",
|
||||
turn = Color.White,
|
||||
castlingRights = CastlingRights.All,
|
||||
enPassantSquare = Some(Square(File.C, Rank.R6)),
|
||||
halfMoveClock = 2,
|
||||
moveCount = 4
|
||||
)
|
||||
FenExporter.gameContextToFen(withEnPassant) shouldBe
|
||||
"rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR w KQkq c6 2 3"
|
||||
|
||||
test("halfMoveClock round-trips through FEN export and import"):
|
||||
val gameContext = GameContext(
|
||||
board = Board.initial,
|
||||
turn = Color.White,
|
||||
castlingRights = CastlingRights.All,
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 42,
|
||||
moves = List.empty
|
||||
)
|
||||
val fen = FenExporter.gameContextToFen(gameContext)
|
||||
FenParser.parseFen(fen) match
|
||||
case Right(ctx) => ctx.halfMoveClock shouldBe 42
|
||||
case Left(err) => fail(s"FEN parsing failed: $err")
|
||||
|
||||
test("exportGameContext forwards to gameContextToFen"):
|
||||
val ctx = GameContext.initial
|
||||
|
||||
FenExporter.exportGameContext(ctx) shouldBe FenExporter.gameContextToFen(ctx)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.nowchess.io.fen
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class FenParserTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("parseBoard parses canonical positions and supports round-trip"):
|
||||
val initial = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
||||
val empty = "8/8/8/8/8/8/8/8"
|
||||
val partial = "8/8/4k3/8/4K3/8/8/8"
|
||||
|
||||
FenParser.parseBoard(initial).map(_.pieceAt(Square(File.E, Rank.R2))) shouldBe Some(Some(Piece.WhitePawn))
|
||||
FenParser.parseBoard(initial).map(_.pieceAt(Square(File.E, Rank.R8))) shouldBe Some(Some(Piece.BlackKing))
|
||||
FenParser.parseBoard(empty).map(_.pieces.size) shouldBe Some(0)
|
||||
FenParser.parseBoard(partial).map(_.pieceAt(Square(File.E, Rank.R6))) shouldBe Some(Some(Piece.BlackKing))
|
||||
|
||||
FenParser.parseBoard(initial).map(FenExporter.boardToFen) shouldBe Some(initial)
|
||||
FenParser.parseBoard(empty).map(FenExporter.boardToFen) shouldBe Some(empty)
|
||||
|
||||
test("parseFen parses full state for common valid inputs"):
|
||||
FenParser.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.turn shouldBe Color.White
|
||||
ctx.castlingRights.whiteKingSide shouldBe true
|
||||
ctx.enPassantSquare shouldBe None
|
||||
ctx.halfMoveClock shouldBe 0
|
||||
)
|
||||
|
||||
FenParser.parseFen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.turn shouldBe Color.Black
|
||||
ctx.enPassantSquare shouldBe Some(Square(File.E, Rank.R3))
|
||||
)
|
||||
|
||||
FenParser.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.castlingRights.whiteKingSide shouldBe false
|
||||
ctx.castlingRights.blackQueenSide shouldBe false
|
||||
)
|
||||
|
||||
test("parseFen rejects invalid color and castling tokens"):
|
||||
FenParser.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1").isLeft shouldBe true
|
||||
FenParser.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w XYZ - 0 1").isLeft shouldBe true
|
||||
|
||||
test("importGameContext returns Right for valid and Left for invalid FEN"):
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||
FenParser.importGameContext(fen).isRight shouldBe true
|
||||
FenParser.importGameContext("invalid fen string").isLeft shouldBe true
|
||||
|
||||
test("parseBoard rejects malformed board shapes and invalid piece symbols"):
|
||||
FenParser.parseBoard("8/8/8/8/8/8/8") shouldBe None
|
||||
FenParser.parseBoard("9/8/8/8/8/8/8/8") shouldBe None
|
||||
FenParser.parseBoard("8p/8/8/8/8/8/8/8") shouldBe None
|
||||
FenParser.parseBoard("7/8/8/8/8/8/8/8") shouldBe None
|
||||
FenParser.parseBoard("8/8/8/8/8/8/8/7X") shouldBe None
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package de.nowchess.io.pgn
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class PgnExporterTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("exportGame renders headers and basic move text"):
|
||||
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B")
|
||||
val emptyPgn = PgnExporter.exportGame(headers, List.empty)
|
||||
emptyPgn.contains("[Event \"Test\"]") shouldBe true
|
||||
emptyPgn.contains("[White \"A\"]") shouldBe true
|
||||
emptyPgn.contains("[Black \"B\"]") shouldBe true
|
||||
|
||||
val moves = List(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal()))
|
||||
PgnExporter.exportGame(headers, moves).contains("1. e4") shouldBe true
|
||||
|
||||
test("exportGame renders castling grouping and result markers"):
|
||||
PgnExporter.exportGame(Map("Event" -> "Test"), List(Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside))) should include("O-O")
|
||||
PgnExporter.exportGame(Map("Event" -> "Test"), List(Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside))) should include("O-O-O")
|
||||
|
||||
val seq = List(
|
||||
Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal()),
|
||||
Move(Square(File.C, Rank.R7), Square(File.C, Rank.R5), MoveType.Normal()),
|
||||
Move(Square(File.G, Rank.R1), Square(File.F, Rank.R3), MoveType.Normal())
|
||||
)
|
||||
val grouped = PgnExporter.exportGame(Map("Result" -> "1-0"), seq)
|
||||
grouped should include("1. e4 c5")
|
||||
grouped should include("2. Nf3")
|
||||
|
||||
val oneMove = List(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal()))
|
||||
PgnExporter.exportGame(Map.empty, oneMove) shouldBe "1. e4 *"
|
||||
PgnExporter.exportGame(Map("Result" -> "1/2-1/2"), oneMove) should endWith("1/2-1/2")
|
||||
|
||||
test("exportGame handles promotion suffixes and normal move formatting"):
|
||||
List(
|
||||
PromotionPiece.Queen -> "=Q",
|
||||
PromotionPiece.Rook -> "=R",
|
||||
PromotionPiece.Bishop -> "=B",
|
||||
PromotionPiece.Knight -> "=N"
|
||||
).foreach { (piece, suffix) =>
|
||||
val move = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(piece))
|
||||
PgnExporter.exportGame(Map.empty, List(move)) should include(s"e8$suffix")
|
||||
}
|
||||
|
||||
val normal = PgnExporter.exportGame(Map.empty, List(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())))
|
||||
normal should include("e4")
|
||||
normal should not include "="
|
||||
|
||||
test("exportGameContext preserves moves and default headers"):
|
||||
val moves = List(
|
||||
Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal()),
|
||||
Move(Square(File.E, Rank.R7), Square(File.E, Rank.R5), MoveType.Normal())
|
||||
)
|
||||
val withMoves = PgnExporter.exportGameContext(GameContext.initial.copy(moves = moves))
|
||||
withMoves.contains("e4") shouldBe true
|
||||
withMoves.contains("e5") shouldBe true
|
||||
|
||||
val empty = PgnExporter.exportGameContext(GameContext.initial)
|
||||
empty.contains("[Event") shouldBe true
|
||||
empty.contains("*") shouldBe true
|
||||
|
||||
private def sq(alg: String): Square =
|
||||
Square.fromAlgebraic(alg).getOrElse(fail(s"Invalid square in test: $alg"))
|
||||
|
||||
test("exportGame emits notation for all normal piece types and captures"):
|
||||
val moves = List(
|
||||
Move(sq("e2"), sq("e4")),
|
||||
Move(sq("a7"), sq("a6")),
|
||||
Move(sq("g1"), sq("f3")),
|
||||
Move(sq("b7"), sq("b6")),
|
||||
Move(sq("f1"), sq("b5"), MoveType.Normal(true)),
|
||||
Move(sq("g8"), sq("f6")),
|
||||
Move(sq("a1"), sq("a8"), MoveType.Normal(true)),
|
||||
Move(sq("c7"), sq("c6")),
|
||||
Move(sq("d1"), sq("d7"), MoveType.Normal(true)),
|
||||
Move(sq("d8"), sq("d7"), MoveType.Normal(true)),
|
||||
Move(sq("e1"), sq("e2"), MoveType.Normal(true))
|
||||
)
|
||||
|
||||
val pgn = PgnExporter.exportGame(Map("Result" -> "*"), moves)
|
||||
|
||||
pgn should include("e4")
|
||||
pgn should include("Nf3")
|
||||
pgn should include("Bxb5")
|
||||
pgn should include("Rxa8")
|
||||
pgn should include("Qxd7")
|
||||
pgn should include("Kxe2")
|
||||
|
||||
test("exportGame emits en-passant and promotion capture notation"):
|
||||
val enPassant = Move(sq("e2"), sq("d3"), MoveType.EnPassant)
|
||||
val promotionCapture = Move(sq("e7"), sq("f8"), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val pawnCapture = Move(sq("e2"), sq("d3"), MoveType.Normal(isCapture = true))
|
||||
val promotionQuietSetup = Move(sq("e8"), sq("e7"))
|
||||
val promotionQuiet = Move(sq("e2"), sq("e8"), MoveType.Promotion(PromotionPiece.Queen))
|
||||
|
||||
val pgn = PgnExporter.exportGame(Map.empty, List(enPassant, promotionCapture))
|
||||
val pawnCapturePgn = PgnExporter.exportGame(Map.empty, List(pawnCapture))
|
||||
val quietPromotionPgn = PgnExporter.exportGame(Map.empty, List(promotionQuietSetup, promotionQuiet))
|
||||
|
||||
pgn should include("exd3")
|
||||
pgn should include("exf8=Q")
|
||||
pawnCapturePgn should include("exd3")
|
||||
quietPromotionPgn should include("e8=Q")
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package de.nowchess.io.pgn
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.move.{MoveType, PromotionPiece}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.io.fen.FenParser
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class PgnParserTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("parsePgn handles headers standard sequences captures castling and skipped tokens"):
|
||||
val headerOnly = """[Event "Test Game"]
|
||||
[White "Alice"]
|
||||
[Black "Bob"]
|
||||
[Result "1-0"]"""
|
||||
val onlyHeaders = PgnParser.parsePgn(headerOnly)
|
||||
onlyHeaders.isDefined shouldBe true
|
||||
onlyHeaders.get.headers("Event") shouldBe "Test Game"
|
||||
onlyHeaders.get.headers("White") shouldBe "Alice"
|
||||
|
||||
val simple = PgnParser.parsePgn("""[Event "Test"]
|
||||
|
||||
1. e4 e5 2. Nf3 Nc6""")
|
||||
simple.map(_.moves.length) shouldBe Some(4)
|
||||
|
||||
val capture = PgnParser.parsePgn("""[Event "Test"]
|
||||
|
||||
1. Nf3 e5 2. Nxe5""")
|
||||
capture.map(_.moves.length) shouldBe Some(3)
|
||||
capture.get.moves(2).to shouldBe Square(File.E, Rank.R5)
|
||||
|
||||
val whiteKs = PgnParser.parsePgn("""[Event "Test"]
|
||||
|
||||
1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. O-O""").get.moves.last
|
||||
whiteKs.moveType shouldBe MoveType.CastleKingside
|
||||
whiteKs.from shouldBe Square(File.E, Rank.R1)
|
||||
whiteKs.to shouldBe Square(File.G, Rank.R1)
|
||||
|
||||
val whiteQs = PgnParser.parsePgn("""[Event "Test"]
|
||||
|
||||
1. d4 d5 2. Nc3 Nc6 3. Bf4 Bf5 4. Qd2 Qd7 5. O-O-O""").get.moves.last
|
||||
whiteQs.moveType shouldBe MoveType.CastleQueenside
|
||||
whiteQs.from shouldBe Square(File.E, Rank.R1)
|
||||
whiteQs.to shouldBe Square(File.C, Rank.R1)
|
||||
|
||||
val blackKs = PgnParser.parsePgn("""[Event "Test"]
|
||||
|
||||
1. e4 e5 2. Nf3 Nf6 3. Bc4 Be7 4. O-O O-O""").get.moves.last
|
||||
blackKs.moveType shouldBe MoveType.CastleKingside
|
||||
blackKs.from shouldBe Square(File.E, Rank.R8)
|
||||
|
||||
val blackQs = PgnParser.parsePgn("""[Event "Test"]
|
||||
|
||||
1. d4 d5 2. Nc3 Nc6 3. Bf4 Bf5 4. Qd2 Qd7 5. O-O-O O-O-O""").get.moves.last
|
||||
blackQs.moveType shouldBe MoveType.CastleQueenside
|
||||
blackQs.from shouldBe Square(File.E, Rank.R8)
|
||||
blackQs.to shouldBe Square(File.C, Rank.R8)
|
||||
|
||||
PgnParser.parsePgn("""[Event "Test"]
|
||||
|
||||
1. e4 e5 1-0""").map(_.moves.length) shouldBe Some(2)
|
||||
PgnParser.parsePgn("""[Event "Test"]
|
||||
|
||||
1. e4 INVALID e5""").map(_.moves.length) shouldBe Some(2)
|
||||
|
||||
test("parseAlgebraicMove resolves pawn knight king and disambiguation cases"):
|
||||
val board = Board.initial
|
||||
PgnParser.parseAlgebraicMove("e4", GameContext.initial.withBoard(board), Color.White).get.to shouldBe Square(File.E, Rank.R4)
|
||||
PgnParser.parseAlgebraicMove("Nf3", GameContext.initial.withBoard(board), Color.White).get.to shouldBe Square(File.F, Rank.R3)
|
||||
|
||||
val rookPieces: Map[Square, Piece] = Map(
|
||||
Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook),
|
||||
Square(File.H, Rank.R1) -> Piece(Color.White, PieceType.Rook),
|
||||
Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King),
|
||||
Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King)
|
||||
)
|
||||
val rankPieces: Map[Square, Piece] = Map(
|
||||
Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook),
|
||||
Square(File.A, Rank.R4) -> Piece(Color.White, PieceType.Rook),
|
||||
Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King),
|
||||
Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King)
|
||||
)
|
||||
PgnParser.parseAlgebraicMove("Rad1", GameContext.initial.withBoard(Board(rookPieces)), Color.White).get.from shouldBe Square(File.A, Rank.R1)
|
||||
PgnParser.parseAlgebraicMove("R1a3", GameContext.initial.withBoard(Board(rankPieces)), Color.White).get.from shouldBe Square(File.A, Rank.R1)
|
||||
|
||||
val kingBoard = FenParser.parseBoard("4k3/8/8/8/8/8/8/4K3").get
|
||||
val king = PgnParser.parseAlgebraicMove("Ke2", GameContext.initial.withBoard(kingBoard), Color.White)
|
||||
king.isDefined shouldBe true
|
||||
king.get.from shouldBe Square(File.E, Rank.R1)
|
||||
king.get.to shouldBe Square(File.E, Rank.R2)
|
||||
|
||||
test("parseAlgebraicMove handles all promotion targets"):
|
||||
val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
PgnParser.parseAlgebraicMove("e7e8=Q", GameContext.initial.withBoard(board), Color.White).get.moveType shouldBe MoveType.Promotion(PromotionPiece.Queen)
|
||||
PgnParser.parseAlgebraicMove("e7e8=R", GameContext.initial.withBoard(board), Color.White).get.moveType shouldBe MoveType.Promotion(PromotionPiece.Rook)
|
||||
PgnParser.parseAlgebraicMove("e7e8=B", GameContext.initial.withBoard(board), Color.White).get.moveType shouldBe MoveType.Promotion(PromotionPiece.Bishop)
|
||||
PgnParser.parseAlgebraicMove("e7e8=N", GameContext.initial.withBoard(board), Color.White).get.moveType shouldBe MoveType.Promotion(PromotionPiece.Knight)
|
||||
|
||||
test("importGameContext accepts valid and empty PGN"):
|
||||
val pgn = """[Event "Test"]
|
||||
|
||||
1. e4 e5"""
|
||||
PgnParser.importGameContext(pgn).isRight shouldBe true
|
||||
PgnParser.importGameContext("").isRight shouldBe true
|
||||
|
||||
test("parser edge cases: uppercase token hint chars and promotion mismatch handling"):
|
||||
PgnParser.parseAlgebraicMove("E5", GameContext.initial, Color.White) shouldBe None
|
||||
PgnParser.parseAlgebraicMove("N?f3", GameContext.initial, Color.White).get.to shouldBe Square(File.F, Rank.R3)
|
||||
PgnParser.extractPromotion("e7e8=X") shouldBe None
|
||||
|
||||
val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
PgnParser.parseAlgebraicMove("e8", GameContext.initial.withBoard(board), Color.White) shouldBe None
|
||||
|
||||
test("parseAlgebraicMove rejects too-short notation and invalid piece letters"):
|
||||
val initial = GameContext.initial
|
||||
|
||||
PgnParser.parseAlgebraicMove("e", initial, Color.White) shouldBe None
|
||||
PgnParser.parseAlgebraicMove("Xe5", initial, Color.White) shouldBe None
|
||||
|
||||
test("parseAlgebraicMove rejects notation with invalid promotion piece"):
|
||||
val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").getOrElse(fail("valid board expected"))
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
|
||||
PgnParser.parseAlgebraicMove("e7e8=X", context, Color.White) shouldBe None
|
||||
|
||||
test("parsePgn silently skips unknown tokens"):
|
||||
val parsed = PgnParser.parsePgn("1. e4 ??? e5")
|
||||
|
||||
parsed.map(_.moves.size) shouldBe Some(2)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package de.nowchess.io.pgn
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.move.MoveType
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class PgnValidatorTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("validatePgn accepts valid games including castling and result tokens"):
|
||||
val pgn =
|
||||
"""[Event "Test"]
|
||||
|
||||
1. e4 e5 2. Nf3 Nc6
|
||||
"""
|
||||
val valid = PgnParser.validatePgn(pgn)
|
||||
valid.isRight shouldBe true
|
||||
valid.toOption.get.moves.length shouldBe 4
|
||||
valid.toOption.get.moves.head.from shouldBe Square(File.E, Rank.R2)
|
||||
|
||||
val withResult = PgnParser.validatePgn("""[Event "Test"]
|
||||
|
||||
1. e4 e5 1-0
|
||||
""")
|
||||
withResult.map(_.moves.length) shouldBe Right(2)
|
||||
|
||||
val kCastle = PgnParser.validatePgn("""[Event "Test"]
|
||||
|
||||
1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. O-O
|
||||
""")
|
||||
kCastle.map(_.moves.last.moveType) shouldBe Right(MoveType.CastleKingside)
|
||||
|
||||
val qCastle = PgnParser.validatePgn("""[Event "Test"]
|
||||
|
||||
1. d4 d5 2. Nc3 Nc6 3. Bf4 Bf5 4. Qd2 Qd7 5. O-O-O
|
||||
""")
|
||||
qCastle.map(_.moves.last.moveType) shouldBe Right(MoveType.CastleQueenside)
|
||||
|
||||
test("validatePgn rejects impossible illegal and garbage tokens"):
|
||||
PgnParser.validatePgn("""[Event "Test"]
|
||||
|
||||
1. Qd4
|
||||
""").isLeft shouldBe true
|
||||
|
||||
PgnParser.validatePgn("""[Event "Test"]
|
||||
|
||||
1. O-O
|
||||
""").isLeft shouldBe true
|
||||
|
||||
PgnParser.validatePgn("""[Event "Test"]
|
||||
|
||||
1. e4 GARBAGE e5
|
||||
""").isLeft shouldBe true
|
||||
|
||||
test("validatePgn accepts empty move text and minimal valid header"):
|
||||
PgnParser.validatePgn("[Event \"Test\"]\n[White \"A\"]\n[Black \"B\"]\n").map(_.moves) shouldBe Right(List.empty)
|
||||
PgnParser.validatePgn("[Event \"T\"]\n\n1. e4").isRight shouldBe true
|
||||
|
||||
Reference in New Issue
Block a user