feat: NCS-25 Add linters to keep quality up (#27)
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
Reviewed-on: #27 Reviewed-by: Leon Hermann <lq@blackhole.local> Co-authored-by: Janis <janis.e.20@gmx.de> Co-committed-by: Janis <janis.e.20@gmx.de>
This commit was merged in pull request #27.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package de.nowchess.io
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.board.{Square, File, Rank}
|
||||
import de.nowchess.api.board.{File, Rank, Square}
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.io.json.{JsonExporter, JsonParser}
|
||||
import java.nio.file.{Files, Paths}
|
||||
@@ -15,37 +15,35 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
|
||||
val tmpFile = Files.createTempFile("chess_test_", ".json")
|
||||
try
|
||||
val context = GameContext.initial
|
||||
val result = FileSystemGameService.saveGameToFile(context, tmpFile, JsonExporter)
|
||||
|
||||
val result = FileSystemGameService.saveGameToFile(context, tmpFile, JsonExporter)
|
||||
|
||||
assert(result.isRight)
|
||||
assert(Files.exists(tmpFile))
|
||||
assert(Files.size(tmpFile) > 0)
|
||||
finally
|
||||
Files.deleteIfExists(tmpFile)
|
||||
finally Files.deleteIfExists(tmpFile)
|
||||
}
|
||||
|
||||
test("loadGameFromFile: reads JSON file successfully") {
|
||||
val tmpFile = Files.createTempFile("chess_test_", ".json")
|
||||
try
|
||||
val originalContext = GameContext.initial
|
||||
|
||||
|
||||
// Save
|
||||
FileSystemGameService.saveGameToFile(originalContext, tmpFile, JsonExporter)
|
||||
|
||||
|
||||
// Load
|
||||
val result = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser)
|
||||
|
||||
|
||||
assert(result.isRight)
|
||||
val loaded = result.getOrElse(GameContext.initial)
|
||||
assert(loaded == originalContext)
|
||||
finally
|
||||
Files.deleteIfExists(tmpFile)
|
||||
finally Files.deleteIfExists(tmpFile)
|
||||
}
|
||||
|
||||
test("loadGameFromFile: returns error on missing file") {
|
||||
val nonExistentFile = Paths.get("/tmp/nonexistent_chess_game_file_12345.json")
|
||||
val result = FileSystemGameService.loadGameFromFile(nonExistentFile, JsonParser)
|
||||
|
||||
val result = FileSystemGameService.loadGameFromFile(nonExistentFile, JsonParser)
|
||||
|
||||
assert(result.isLeft)
|
||||
}
|
||||
|
||||
@@ -57,16 +55,15 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
|
||||
val context = GameContext.initial
|
||||
.withMove(move1)
|
||||
.withMove(move2)
|
||||
|
||||
|
||||
val saveResult = FileSystemGameService.saveGameToFile(context, tmpFile, JsonExporter)
|
||||
assert(saveResult.isRight)
|
||||
|
||||
|
||||
val loadResult = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser)
|
||||
assert(loadResult.isRight)
|
||||
val loaded = loadResult.getOrElse(GameContext.initial)
|
||||
assert(loaded.moves.length == 2)
|
||||
finally
|
||||
Files.deleteIfExists(tmpFile)
|
||||
finally Files.deleteIfExists(tmpFile)
|
||||
}
|
||||
|
||||
test("saveGameToFile: overwrites existing file") {
|
||||
@@ -76,18 +73,17 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
|
||||
val context1 = GameContext.initial
|
||||
FileSystemGameService.saveGameToFile(context1, tmpFile, JsonExporter)
|
||||
val size1 = Files.size(tmpFile)
|
||||
|
||||
|
||||
// Write second file (should overwrite)
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val context2 = GameContext.initial.withMove(move)
|
||||
FileSystemGameService.saveGameToFile(context2, tmpFile, JsonExporter)
|
||||
|
||||
|
||||
val loadResult = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser)
|
||||
assert(loadResult.isRight)
|
||||
val loaded = loadResult.getOrElse(GameContext.initial)
|
||||
assert(loaded.moves.length == 1)
|
||||
finally
|
||||
Files.deleteIfExists(tmpFile)
|
||||
finally Files.deleteIfExists(tmpFile)
|
||||
}
|
||||
|
||||
test("loadGameFromFile: handles invalid JSON in file") {
|
||||
@@ -95,10 +91,9 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
|
||||
try
|
||||
Files.write(tmpFile, "{ invalid json}".getBytes())
|
||||
val result = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser)
|
||||
|
||||
|
||||
assert(result.isLeft)
|
||||
finally
|
||||
Files.deleteIfExists(tmpFile)
|
||||
finally Files.deleteIfExists(tmpFile)
|
||||
}
|
||||
|
||||
test("round-trip: save and load preserves game state") {
|
||||
@@ -110,16 +105,15 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
|
||||
.withMove(move1)
|
||||
.withMove(move2)
|
||||
.withHalfMoveClock(3)
|
||||
|
||||
|
||||
FileSystemGameService.saveGameToFile(original, tmpFile, JsonExporter)
|
||||
val loadResult = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser)
|
||||
|
||||
|
||||
assert(loadResult.isRight)
|
||||
val loaded = loadResult.getOrElse(GameContext.initial)
|
||||
assert(loaded.moves.length == 2)
|
||||
assert(loaded.halfMoveClock == 3)
|
||||
finally
|
||||
Files.deleteIfExists(tmpFile)
|
||||
finally Files.deleteIfExists(tmpFile)
|
||||
}
|
||||
|
||||
test("saveGameToFile: handles exporter that throws exception") {
|
||||
@@ -127,13 +121,12 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
|
||||
try
|
||||
val context = GameContext.initial
|
||||
val faultyExporter = new GameContextExport {
|
||||
def exportGameContext(c: GameContext): String =
|
||||
def exportGameContext(c: GameContext): String =
|
||||
throw new RuntimeException("Export failed")
|
||||
}
|
||||
|
||||
|
||||
val result = FileSystemGameService.saveGameToFile(context, tmpFile, faultyExporter)
|
||||
assert(result.isLeft)
|
||||
assert(result.left.toOption.get.contains("Failed to save file"))
|
||||
finally
|
||||
Files.deleteIfExists(tmpFile)
|
||||
finally Files.deleteIfExists(tmpFile)
|
||||
}
|
||||
|
||||
@@ -9,16 +9,18 @@ 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
|
||||
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 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,
|
||||
@@ -26,7 +28,7 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
||||
castlingRights = castlingRights,
|
||||
enPassantSquare = enPassantSquare,
|
||||
halfMoveClock = halfMoveClock,
|
||||
moves = List.fill(moveCount)(dummyMove)
|
||||
moves = List.fill(moveCount)(dummyMove),
|
||||
)
|
||||
|
||||
test("exportGameContextToFen handles initial and typical developed position"):
|
||||
@@ -39,7 +41,7 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
||||
castlingRights = CastlingRights.All,
|
||||
enPassantSquare = Some(Square(File.E, Rank.R3)),
|
||||
halfMoveClock = 0,
|
||||
moveCount = 0
|
||||
moveCount = 0,
|
||||
)
|
||||
FenExporter.gameContextToFen(gameContext) shouldBe
|
||||
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
|
||||
@@ -51,7 +53,7 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
||||
castlingRights = CastlingRights.None,
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moveCount = 0
|
||||
moveCount = 0,
|
||||
)
|
||||
FenExporter.gameContextToFen(noCastling) shouldBe
|
||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1"
|
||||
@@ -63,11 +65,11 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
||||
whiteKingSide = true,
|
||||
whiteQueenSide = false,
|
||||
blackKingSide = false,
|
||||
blackQueenSide = true
|
||||
blackQueenSide = true,
|
||||
),
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 5,
|
||||
moveCount = 4
|
||||
moveCount = 4,
|
||||
)
|
||||
FenExporter.gameContextToFen(partialCastling) shouldBe
|
||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Kq - 5 3"
|
||||
@@ -78,7 +80,7 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
||||
castlingRights = CastlingRights.All,
|
||||
enPassantSquare = Some(Square(File.C, Rank.R6)),
|
||||
halfMoveClock = 2,
|
||||
moveCount = 4
|
||||
moveCount = 4,
|
||||
)
|
||||
FenExporter.gameContextToFen(withEnPassant) shouldBe
|
||||
"rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR w KQkq c6 2 3"
|
||||
@@ -90,7 +92,7 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
||||
castlingRights = CastlingRights.All,
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 42,
|
||||
moves = List.empty
|
||||
moves = List.empty,
|
||||
)
|
||||
val fen = FenExporter.gameContextToFen(gameContext)
|
||||
FenParser.parseFen(fen) match
|
||||
@@ -101,4 +103,3 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
||||
val ctx = GameContext.initial
|
||||
|
||||
FenExporter.exportGameContext(ctx) shouldBe FenExporter.gameContextToFen(ctx)
|
||||
|
||||
|
||||
@@ -8,34 +8,52 @@ class FenParserCombinatorsTest 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 empty = "8/8/8/8/8/8/8/8"
|
||||
val partial = "8/8/4k3/8/4K3/8/8/8"
|
||||
|
||||
FenParserCombinators.parseBoard(initial).map(_.pieceAt(Square(File.E, Rank.R2))) shouldBe Some(Some(Piece.WhitePawn))
|
||||
FenParserCombinators.parseBoard(initial).map(_.pieceAt(Square(File.E, Rank.R8))) shouldBe Some(Some(Piece.BlackKing))
|
||||
FenParserCombinators.parseBoard(initial).map(_.pieceAt(Square(File.E, Rank.R2))) shouldBe Some(
|
||||
Some(Piece.WhitePawn),
|
||||
)
|
||||
FenParserCombinators.parseBoard(initial).map(_.pieceAt(Square(File.E, Rank.R8))) shouldBe Some(
|
||||
Some(Piece.BlackKing),
|
||||
)
|
||||
FenParserCombinators.parseBoard(empty).map(_.pieces.size) shouldBe Some(0)
|
||||
FenParserCombinators.parseBoard(partial).map(_.pieceAt(Square(File.E, Rank.R6))) shouldBe Some(Some(Piece.BlackKing))
|
||||
FenParserCombinators.parseBoard(partial).map(_.pieceAt(Square(File.E, Rank.R6))) shouldBe Some(
|
||||
Some(Piece.BlackKing),
|
||||
)
|
||||
|
||||
FenParserCombinators.parseBoard(initial).map(FenExporter.boardToFen) shouldBe Some(initial)
|
||||
FenParserCombinators.parseBoard(empty).map(FenExporter.boardToFen) shouldBe Some(empty)
|
||||
|
||||
test("parseFen parses full state for common valid inputs"):
|
||||
FenParserCombinators.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
|
||||
)
|
||||
FenParserCombinators
|
||||
.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,
|
||||
)
|
||||
|
||||
FenParserCombinators.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))
|
||||
)
|
||||
FenParserCombinators
|
||||
.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)),
|
||||
)
|
||||
|
||||
FenParserCombinators.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
|
||||
)
|
||||
FenParserCombinators
|
||||
.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"):
|
||||
FenParserCombinators.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1").isLeft shouldBe true
|
||||
|
||||
@@ -8,7 +8,7 @@ class FenParserFastParseTest 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 empty = "8/8/8/8/8/8/8/8"
|
||||
val partial = "8/8/4k3/8/4K3/8/8/8"
|
||||
|
||||
FenParserFastParse.parseBoard(initial).map(_.pieceAt(Square(File.E, Rank.R2))) shouldBe Some(Some(Piece.WhitePawn))
|
||||
@@ -20,22 +20,34 @@ class FenParserFastParseTest extends AnyFunSuite with Matchers:
|
||||
FenParserFastParse.parseBoard(empty).map(FenExporter.boardToFen) shouldBe Some(empty)
|
||||
|
||||
test("parseFen parses full state for common valid inputs"):
|
||||
FenParserFastParse.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
|
||||
)
|
||||
FenParserFastParse
|
||||
.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,
|
||||
)
|
||||
|
||||
FenParserFastParse.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))
|
||||
)
|
||||
FenParserFastParse
|
||||
.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)),
|
||||
)
|
||||
|
||||
FenParserFastParse.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
|
||||
)
|
||||
FenParserFastParse
|
||||
.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"):
|
||||
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1").isLeft shouldBe true
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 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))
|
||||
@@ -20,22 +20,34 @@ class FenParserTest extends AnyFunSuite with Matchers:
|
||||
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/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/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
|
||||
)
|
||||
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
|
||||
@@ -52,4 +64,3 @@ class FenParserTest extends AnyFunSuite with Matchers:
|
||||
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
|
||||
|
||||
|
||||
+22
-22
@@ -1,7 +1,7 @@
|
||||
package de.nowchess.io.json
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.board.{Square, File, Rank, Board, Color, CastlingRights, Piece, PieceType}
|
||||
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -13,71 +13,71 @@ class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
|
||||
(PromotionPiece.Queen, "queen"),
|
||||
(PromotionPiece.Rook, "rook"),
|
||||
(PromotionPiece.Bishop, "bishop"),
|
||||
(PromotionPiece.Knight, "knight")
|
||||
(PromotionPiece.Knight, "knight"),
|
||||
)
|
||||
|
||||
for ((piece, expectedName) <- promotions) do
|
||||
for (piece, expectedName) <- promotions do
|
||||
val move = Move(Square(File.A, Rank.R7), Square(File.A, Rank.R8), MoveType.Promotion(piece))
|
||||
// Empty boards can cause issues in PgnExporter, using initial
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
// try-catch to ignore PgnExporter errors but cover convertMoveType
|
||||
try {
|
||||
val json = JsonExporter.exportGameContext(ctx)
|
||||
json should include (s""""$expectedName"""")
|
||||
json should include(s""""$expectedName"""")
|
||||
} catch { case _: Exception => }
|
||||
}
|
||||
|
||||
test("export normal non-capture move") {
|
||||
val quietMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal(false))
|
||||
val ctx = GameContext.initial.copy(moves = List(quietMove))
|
||||
val json = JsonExporter.exportGameContext(ctx)
|
||||
json should include ("\"normal\"")
|
||||
val ctx = GameContext.initial.copy(moves = List(quietMove))
|
||||
val json = JsonExporter.exportGameContext(ctx)
|
||||
json should include("\"normal\"")
|
||||
}
|
||||
|
||||
test("export normal capture move manually") {
|
||||
val move = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true))
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
try {
|
||||
val json = JsonExporter.exportGameContext(ctx)
|
||||
json should include ("\"normal\"")
|
||||
json should include ("\"isCapture\": true")
|
||||
json should include("\"normal\"")
|
||||
json should include("\"isCapture\": true")
|
||||
} catch { case _: Exception => }
|
||||
}
|
||||
|
||||
test("export all move type categories") {
|
||||
val move = Move(Square(File.D, Rank.R2), Square(File.D, Rank.R4))
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
val json = JsonExporter.exportGameContext(ctx)
|
||||
|
||||
json should include ("\"moves\"")
|
||||
json should include ("\"from\"")
|
||||
json should include ("\"to\"")
|
||||
|
||||
json should include("\"moves\"")
|
||||
json should include("\"from\"")
|
||||
json should include("\"to\"")
|
||||
}
|
||||
|
||||
test("export castle queenside move") {
|
||||
val move = Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside)
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
try {
|
||||
val json = JsonExporter.exportGameContext(ctx)
|
||||
json should include ("\"castleQueenside\"")
|
||||
json should include("\"castleQueenside\"")
|
||||
} catch { case _: Exception => }
|
||||
}
|
||||
|
||||
test("export castle kingside move") {
|
||||
val move = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
try {
|
||||
val json = JsonExporter.exportGameContext(ctx)
|
||||
json should include ("\"castleKingside\"")
|
||||
json should include("\"castleKingside\"")
|
||||
} catch { case _: Exception => }
|
||||
}
|
||||
|
||||
test("export en passant move manually") {
|
||||
val move = Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant)
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
val ctx = GameContext.initial.copy(moves = List(move))
|
||||
try {
|
||||
val json = JsonExporter.exportGameContext(ctx)
|
||||
json should include ("\"enPassant\"")
|
||||
json should include ("\"isCapture\": true")
|
||||
json should include("\"enPassant\"")
|
||||
json should include("\"isCapture\": true")
|
||||
} catch { case _: Exception => }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.nowchess.io.json
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.board.{Board, Square, Piece, Color, PieceType, File, Rank, CastlingRights}
|
||||
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -10,8 +10,8 @@ class JsonExporterSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("exportGameContext: exports initial position") {
|
||||
val context = GameContext.initial
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"metadata\"")
|
||||
json should include("\"gameState\"")
|
||||
json should include("\"moveHistory\"")
|
||||
@@ -21,8 +21,8 @@ class JsonExporterSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("exportGameContext: includes board pieces") {
|
||||
val context = GameContext.initial
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"a1\"")
|
||||
json should include("\"Rook\"")
|
||||
json should include("\"White\"")
|
||||
@@ -30,24 +30,24 @@ class JsonExporterSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("exportGameContext: includes turn information") {
|
||||
val context = GameContext.initial
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"turn\": \"White\"")
|
||||
}
|
||||
|
||||
test("exportGameContext: includes castling rights") {
|
||||
val context = GameContext.initial
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"whiteKingSide\": true")
|
||||
json should include("\"whiteQueenSide\": true")
|
||||
}
|
||||
|
||||
test("exportGameContext: exports with moves") {
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val context = GameContext.initial.withMove(move)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"moves\"")
|
||||
json should include("\"from\"")
|
||||
json should include("\"to\"")
|
||||
@@ -57,8 +57,8 @@ class JsonExporterSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("exportGameContext: valid JSON structure") {
|
||||
val context = GameContext.initial
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should startWith("{")
|
||||
json should endWith("}")
|
||||
json should include("\"metadata\": {")
|
||||
@@ -67,47 +67,47 @@ class JsonExporterSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("exportGameContext: empty move history for initial position") {
|
||||
val context = GameContext.initial
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"moves\": []")
|
||||
}
|
||||
|
||||
test("exportGameContext: exports en passant square") {
|
||||
val epSquare = Some(Square(File.E, Rank.R3))
|
||||
val context = GameContext.initial.copy(enPassantSquare = epSquare)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val context = GameContext.initial.copy(enPassantSquare = epSquare)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"enPassantSquare\": \"e3\"")
|
||||
}
|
||||
|
||||
test("exportGameContext: exports null en passant square") {
|
||||
val context = GameContext.initial.copy(enPassantSquare = None)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"enPassantSquare\": null")
|
||||
}
|
||||
|
||||
test("exportGameContext: exports different move destinations") {
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val context = GameContext.initial.withMove(move)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"moves\"")
|
||||
}
|
||||
|
||||
test("exportGameContext: exports empty board") {
|
||||
val emptyBoard = Board(Map.empty)
|
||||
val context = GameContext.initial.copy(board = emptyBoard)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val context = GameContext.initial.copy(board = emptyBoard)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"board\": []")
|
||||
}
|
||||
|
||||
test("exportGameContext: exports all castling rights disabled") {
|
||||
val noCastling = CastlingRights(false, false, false, false)
|
||||
val context = GameContext.initial.withCastlingRights(noCastling)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
val context = GameContext.initial.withCastlingRights(noCastling)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
|
||||
json should include("\"whiteKingSide\": false")
|
||||
json should include("\"whiteQueenSide\": false")
|
||||
json should include("\"blackKingSide\": false")
|
||||
|
||||
@@ -40,7 +40,7 @@ class JsonModelExtraTestSuite extends AnyFunSuite with Matchers:
|
||||
Some("White"),
|
||||
Some(JsonCastlingRights()),
|
||||
Some("e3"),
|
||||
Some(5)
|
||||
Some(5),
|
||||
)
|
||||
assert(gs.board.contains(Nil))
|
||||
assert(gs.halfMoveClock.contains(5))
|
||||
@@ -88,7 +88,7 @@ class JsonModelExtraTestSuite extends AnyFunSuite with Matchers:
|
||||
Some(""),
|
||||
Some(Nil),
|
||||
Some(JsonCapturedPieces()),
|
||||
Some("2026-04-08T00:00:00Z")
|
||||
Some("2026-04-08T00:00:00Z"),
|
||||
)
|
||||
assert(record.metadata.nonEmpty)
|
||||
assert(record.timestamp.nonEmpty)
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.scalatest.matchers.should.Matchers
|
||||
class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("parse invalid turn color returns error") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {"turn": "Invalid", "board": []},
|
||||
"moves": []
|
||||
@@ -19,7 +19,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse invalid piece type filters it out") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {
|
||||
"turn": "White",
|
||||
@@ -36,7 +36,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse invalid color in board filters piece") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {
|
||||
"turn": "White",
|
||||
@@ -53,7 +53,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse with missing turn uses default") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {"board": []},
|
||||
"moves": []
|
||||
@@ -65,7 +65,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse with missing board uses empty") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {"turn": "White"},
|
||||
"moves": []
|
||||
@@ -77,7 +77,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse with missing moves uses empty list") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {"turn": "White", "board": []}
|
||||
}"""
|
||||
@@ -88,7 +88,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse invalid square in board filters it") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {
|
||||
"turn": "White",
|
||||
@@ -105,7 +105,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse all valid piece types") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {
|
||||
"turn": "White",
|
||||
@@ -124,11 +124,16 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
|
||||
assert(result.isRight)
|
||||
val ctx = result.toOption.get
|
||||
assert(ctx.board.pieces.size == 6)
|
||||
assert(ctx.board.pieceAt(de.nowchess.api.board.Square(de.nowchess.api.board.File.A, de.nowchess.api.board.Rank.R1)).get.pieceType == PieceType.Pawn)
|
||||
assert(
|
||||
ctx.board
|
||||
.pieceAt(de.nowchess.api.board.Square(de.nowchess.api.board.File.A, de.nowchess.api.board.Rank.R1))
|
||||
.get
|
||||
.pieceType == PieceType.Pawn,
|
||||
)
|
||||
}
|
||||
|
||||
test("parse with all castling rights false") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {
|
||||
"turn": "White",
|
||||
|
||||
@@ -8,7 +8,7 @@ class JsonParserErrorHandlingSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("parse completely invalid JSON returns error") {
|
||||
val invalidJson = "{ this is not valid json at all }"
|
||||
val result = JsonParser.importGameContext(invalidJson)
|
||||
val result = JsonParser.importGameContext(invalidJson)
|
||||
assert(result.isLeft)
|
||||
assert(result.left.toOption.get.contains("JSON parsing error"))
|
||||
}
|
||||
@@ -26,26 +26,26 @@ class JsonParserErrorHandlingSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("parse malformed JSON object returns error") {
|
||||
val malformed = """{"metadata": {"unclosed": """
|
||||
val result = JsonParser.importGameContext(malformed)
|
||||
val result = JsonParser.importGameContext(malformed)
|
||||
assert(result.isLeft)
|
||||
assert(result.left.toOption.get.contains("JSON parsing error"))
|
||||
}
|
||||
|
||||
test("parse invalid JSON array returns error") {
|
||||
val invalidArray = "[1, 2, 3"
|
||||
val result = JsonParser.importGameContext(invalidArray)
|
||||
val result = JsonParser.importGameContext(invalidArray)
|
||||
assert(result.isLeft)
|
||||
}
|
||||
|
||||
test("parse JSON with missing required fields") {
|
||||
val json = """{"metadata": {}}"""
|
||||
val json = """{"metadata": {}}"""
|
||||
val result = JsonParser.importGameContext(json)
|
||||
// Should still succeed because all fields have defaults
|
||||
assert(result.isRight)
|
||||
}
|
||||
|
||||
test("parse valid JSON with invalid turn falls back to default") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {"turn": "White", "board": []},
|
||||
"moves": []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.nowchess.io.json
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.board.{Color, PieceType, Piece, Square, File, Rank}
|
||||
import de.nowchess.api.board.{Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -9,7 +9,7 @@ import org.scalatest.matchers.should.Matchers
|
||||
class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("parse all move type variations") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {"event": "Game", "result": "*"},
|
||||
"gameState": {"turn": "White", "board": []},
|
||||
"moves": [
|
||||
@@ -34,7 +34,7 @@ class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse invalid move type defaults to None") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {"event": "Game"},
|
||||
"gameState": {"turn": "White", "board": []},
|
||||
"moves": [{"from": "e2", "to": "e4", "type": {"type": "unknown"}}]
|
||||
@@ -45,7 +45,7 @@ class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse promotion with default piece") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {"turn": "White", "board": []},
|
||||
"moves": [{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "invalid"}}]
|
||||
@@ -56,7 +56,7 @@ class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse move with missing from/to skips it") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {"turn": "White", "board": []},
|
||||
"moves": [{"from": "e2", "to": "invalid", "type": {"type": "normal"}}]
|
||||
@@ -69,26 +69,26 @@ class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("parse with invalid JSON returns error") {
|
||||
val json = """{"invalid json"""
|
||||
val json = """{"invalid json"""
|
||||
val result = JsonParser.importGameContext(json)
|
||||
assert(result.isLeft)
|
||||
}
|
||||
|
||||
test("parse normal move with isCapture true") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {"turn": "White", "board": []},
|
||||
"moves": [{"from": "e4", "to": "d5", "type": {"type": "normal", "isCapture": true}}]
|
||||
}"""
|
||||
val result = JsonParser.importGameContext(json)
|
||||
assert(result.isRight)
|
||||
val ctx = result.toOption.get
|
||||
val ctx = result.toOption.get
|
||||
val move = ctx.moves.head
|
||||
assert(move.moveType == MoveType.Normal(true))
|
||||
}
|
||||
|
||||
test("parse board with invalid pieces filters them") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {},
|
||||
"gameState": {
|
||||
"turn": "White",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.nowchess.io.json
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.board.{Color, File, Rank, Square, CastlingRights}
|
||||
import de.nowchess.api.board.{CastlingRights, Color, File, Rank, Square}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -9,39 +9,39 @@ import org.scalatest.matchers.should.Matchers
|
||||
class JsonParserSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("importGameContext: parses valid JSON") {
|
||||
val json = JsonExporter.exportGameContext(GameContext.initial)
|
||||
val json = JsonExporter.exportGameContext(GameContext.initial)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
|
||||
assert(result.isRight)
|
||||
}
|
||||
|
||||
test("importGameContext: restores board state") {
|
||||
val context = GameContext.initial
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result == Right(context))
|
||||
}
|
||||
|
||||
test("importGameContext: restores turn") {
|
||||
val context = GameContext.initial.withTurn(Color.Black)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result.map(_.turn) == Right(Color.Black))
|
||||
}
|
||||
|
||||
test("importGameContext: restores moves") {
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val context = GameContext.initial.withMove(move)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result.map(_.moves.length) == Right(1))
|
||||
}
|
||||
|
||||
test("importGameContext: handles empty board") {
|
||||
val json = """{
|
||||
val json = """{
|
||||
"metadata": {"event": "Game", "players": {"white": "A", "black": "B"}, "date": "2026-04-06", "result": "*"},
|
||||
"gameState": {
|
||||
"board": [],
|
||||
@@ -56,30 +56,31 @@ class JsonParserSuite extends AnyFunSuite with Matchers:
|
||||
"timestamp": "2026-04-06T00:00:00Z"
|
||||
}"""
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
|
||||
assert(result.isRight)
|
||||
assert(result.map(_.board.pieces.isEmpty) == Right(true))
|
||||
}
|
||||
|
||||
test("importGameContext: returns error on invalid JSON") {
|
||||
val result = JsonParser.importGameContext("not valid json {{{")
|
||||
|
||||
|
||||
assert(result.isLeft)
|
||||
}
|
||||
|
||||
test("importGameContext: handles missing fields with defaults") {
|
||||
val json = "{\"metadata\": {}, \"gameState\": {\"board\": [], \"turn\": \"White\", \"castlingRights\": {\"whiteKingSide\": true, \"whiteQueenSide\": true, \"blackKingSide\": true, \"blackQueenSide\": true}, \"enPassantSquare\": null, \"halfMoveClock\": 0}, \"moves\": [], \"moveHistory\": \"\", \"capturedPieces\": {\"byWhite\": [], \"byBlack\": []}, \"timestamp\": \"2026-01-01T00:00:00Z\"}"
|
||||
val json =
|
||||
"{\"metadata\": {}, \"gameState\": {\"board\": [], \"turn\": \"White\", \"castlingRights\": {\"whiteKingSide\": true, \"whiteQueenSide\": true, \"blackKingSide\": true, \"blackQueenSide\": true}, \"enPassantSquare\": null, \"halfMoveClock\": 0}, \"moves\": [], \"moveHistory\": \"\", \"capturedPieces\": {\"byWhite\": [], \"byBlack\": []}, \"timestamp\": \"2026-01-01T00:00:00Z\"}"
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
|
||||
assert(result.isRight)
|
||||
}
|
||||
|
||||
test("importGameContext: handles castling rights") {
|
||||
val newCastling = GameContext.initial.castlingRights.copy(whiteKingSide = false)
|
||||
val context = GameContext.initial.withCastlingRights(newCastling)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val context = GameContext.initial.withCastlingRights(newCastling)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result.map(_.castlingRights.whiteKingSide) == Right(false))
|
||||
}
|
||||
|
||||
@@ -91,7 +92,7 @@ class JsonParserSuite extends AnyFunSuite with Matchers:
|
||||
.withMove(move2)
|
||||
.withTurn(Color.White)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val restored = JsonParser.importGameContext(json)
|
||||
|
||||
assert(restored.map(_.moves.length) == Right(2))
|
||||
@@ -100,55 +101,55 @@ class JsonParserSuite extends AnyFunSuite with Matchers:
|
||||
|
||||
test("importGameContext: handles half-move clock") {
|
||||
val context = GameContext.initial.withHalfMoveClock(5)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result.map(_.halfMoveClock) == Right(5))
|
||||
}
|
||||
|
||||
test("importGameContext: parses en passant square") {
|
||||
// Create a context with en passant square
|
||||
val epSquare = Some(Square(File.E, Rank.R3))
|
||||
val context = GameContext.initial.copy(enPassantSquare = epSquare)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val context = GameContext.initial.copy(enPassantSquare = epSquare)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result.map(_.enPassantSquare) == Right(epSquare))
|
||||
}
|
||||
|
||||
test("importGameContext: handles black turn") {
|
||||
val context = GameContext.initial.withTurn(Color.Black)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result.map(_.turn) == Right(Color.Black))
|
||||
}
|
||||
|
||||
test("importGameContext: preserves basic moves in JSON round-trip") {
|
||||
// Use simple move without explicit moveType to let system handle it
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val context = GameContext.initial.withMove(move)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result.isRight)
|
||||
assert(result.map(_.moves.length) == Right(1))
|
||||
}
|
||||
|
||||
test("importGameContext: handles all castling rights disabled") {
|
||||
val noCastling = CastlingRights(false, false, false, false)
|
||||
val context = GameContext.initial.withCastlingRights(noCastling)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val context = GameContext.initial.withCastlingRights(noCastling)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result.map(_.castlingRights) == Right(noCastling))
|
||||
}
|
||||
|
||||
test("importGameContext: handles mixed castling rights") {
|
||||
val mixed = CastlingRights(true, false, false, true)
|
||||
val mixed = CastlingRights(true, false, false, true)
|
||||
val context = GameContext.initial.withCastlingRights(mixed)
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
val json = JsonExporter.exportGameContext(context)
|
||||
val result = JsonParser.importGameContext(json)
|
||||
|
||||
assert(result.map(_.castlingRights) == Right(mixed))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 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
|
||||
@@ -19,13 +19,19 @@ class PgnExporterTest extends AnyFunSuite with Matchers:
|
||||
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")
|
||||
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())
|
||||
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")
|
||||
@@ -37,23 +43,24 @@ class PgnExporterTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("exportGame handles promotion suffixes and normal move formatting"):
|
||||
List(
|
||||
PromotionPiece.Queen -> "=Q",
|
||||
PromotionPiece.Rook -> "=R",
|
||||
PromotionPiece.Queen -> "=Q",
|
||||
PromotionPiece.Rook -> "=R",
|
||||
PromotionPiece.Bishop -> "=B",
|
||||
PromotionPiece.Knight -> "=N"
|
||||
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())))
|
||||
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())
|
||||
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
|
||||
@@ -78,7 +85,7 @@ class PgnExporterTest extends AnyFunSuite with Matchers:
|
||||
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))
|
||||
Move(sq("e1"), sq("e2"), MoveType.Normal(true)),
|
||||
)
|
||||
|
||||
val pgn = PgnExporter.exportGame(Map("Result" -> "*"), moves)
|
||||
@@ -91,18 +98,17 @@ class PgnExporterTest extends AnyFunSuite with Matchers:
|
||||
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 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 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 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")
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ 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"]
|
||||
val headerOnly = """[Event "Test Game"]
|
||||
[White "Alice"]
|
||||
[Black "Bob"]
|
||||
[Result "1-0"]"""
|
||||
@@ -30,72 +30,116 @@ class PgnParserTest extends AnyFunSuite with Matchers:
|
||||
capture.map(_.moves.length) shouldBe Some(3)
|
||||
capture.get.moves(2).to shouldBe Square(File.E, Rank.R5)
|
||||
|
||||
val whiteKs = PgnParser.parsePgn("""[Event "Test"]
|
||||
val whiteKs = PgnParser
|
||||
.parsePgn("""[Event "Test"]
|
||||
|
||||
1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. O-O""").get.moves.last
|
||||
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"]
|
||||
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
|
||||
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"]
|
||||
val blackKs = PgnParser
|
||||
.parsePgn("""[Event "Test"]
|
||||
|
||||
1. e4 e5 2. Nf3 Nf6 3. Bc4 Be7 4. O-O O-O""").get.moves.last
|
||||
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"]
|
||||
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
|
||||
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"]
|
||||
PgnParser
|
||||
.parsePgn("""[Event "Test"]
|
||||
|
||||
1. e4 e5 1-0""").map(_.moves.length) shouldBe Some(2)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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"]
|
||||
@@ -119,7 +163,7 @@ class PgnParserTest extends AnyFunSuite with Matchers:
|
||||
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 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
|
||||
@@ -128,4 +172,3 @@ class PgnParserTest extends AnyFunSuite with Matchers:
|
||||
val parsed = PgnParser.parsePgn("1. e4 ??? e5")
|
||||
|
||||
parsed.map(_.moves.size) shouldBe Some(2)
|
||||
|
||||
|
||||
@@ -37,17 +37,20 @@ class PgnValidatorTest extends AnyFunSuite with Matchers:
|
||||
qCastle.map(_.moves.last.moveType) shouldBe Right(MoveType.CastleQueenside)
|
||||
|
||||
test("validatePgn rejects impossible illegal and garbage tokens"):
|
||||
PgnParser.validatePgn("""[Event "Test"]
|
||||
PgnParser
|
||||
.validatePgn("""[Event "Test"]
|
||||
|
||||
1. Qd4
|
||||
""").isLeft shouldBe true
|
||||
|
||||
PgnParser.validatePgn("""[Event "Test"]
|
||||
PgnParser
|
||||
.validatePgn("""[Event "Test"]
|
||||
|
||||
1. O-O
|
||||
""").isLeft shouldBe true
|
||||
|
||||
PgnParser.validatePgn("""[Event "Test"]
|
||||
PgnParser
|
||||
.validatePgn("""[Event "Test"]
|
||||
|
||||
1. e4 GARBAGE e5
|
||||
""").isLeft shouldBe true
|
||||
@@ -55,4 +58,3 @@ class PgnValidatorTest extends AnyFunSuite with Matchers:
|
||||
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