diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterMoveSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterMoveSuite.scala new file mode 100644 index 0000000..39ba45e --- /dev/null +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterMoveSuite.scala @@ -0,0 +1,108 @@ +package de.nowchess.io.json + +import de.nowchess.api.game.GameContext +import de.nowchess.api.board.{Piece, Square, File, Rank, Board, CastlingRights, Color, PieceType} +import de.nowchess.api.move.{Move, MoveType, PromotionPiece} +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class JsonExporterMoveSuite extends AnyFunSuite with Matchers: + + test("export initial position has empty moves") { + val context = GameContext.initial + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"moves\": []")) + } + + test("export move using withMove applies it validly") { + val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) + val context = GameContext.initial.withMove(move) + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"e2\"")) + assert(json.contains("\"e4\"")) + } + + test("export castling rights after kingside castling") { + val move = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside) + // Note: We can't apply kingside castling without proper board state, so test structure + val cr = CastlingRights(false, false, true, true) + val context = GameContext.initial.copy(castlingRights = cr) + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"whiteKingSide\": false")) + assert(json.contains("\"blackKingSide\": true")) + } + + test("export en passant square if set") { + val epSquare = Some(Square(File.E, Rank.R3)) + val context = GameContext.initial.copy(enPassantSquare = epSquare) + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"enPassantSquare\": \"e3\"")) + } + + test("export null en passant square") { + val context = GameContext.initial.copy(enPassantSquare = None) + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"enPassantSquare\": null")) + } + + test("export move JSON structure contains required fields") { + val move = Move(Square(File.D, Rank.R2), Square(File.D, Rank.R4)) + val context = GameContext.initial.withMove(move) + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"d2\"")) + assert(json.contains("\"d4\"")) + assert(json.contains("\"moves\"")) + } + + test("export complex board with custom pieces") { + val board = Board(Map( + Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook), + Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King), + Square(File.H, Rank.R1) -> Piece(Color.White, PieceType.Rook), + Square(File.A, Rank.R8) -> Piece(Color.Black, PieceType.Rook), + Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King), + Square(File.H, Rank.R8) -> Piece(Color.Black, PieceType.Rook) + )) + val context = GameContext.initial.copy(board = board) + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"a1\"")) + assert(json.contains("\"e1\"")) + assert(json.contains("\"h1\"")) + assert(json.contains("\"Rook\"")) + } + + test("export with no castling rights") { + val cr = CastlingRights(false, false, false, false) + val context = GameContext.initial.copy(castlingRights = cr) + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"whiteKingSide\": false")) + assert(json.contains("\"whiteQueenSide\": false")) + assert(json.contains("\"blackKingSide\": false")) + assert(json.contains("\"blackQueenSide\": false")) + } + + test("export with partial castling rights") { + val cr = CastlingRights(true, false, true, false) + val context = GameContext.initial.copy(castlingRights = cr) + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"whiteKingSide\": true")) + assert(json.contains("\"whiteQueenSide\": false")) + } + + test("export normal move type") { + val normalMove = Move(Square(File.C, Rank.R2), Square(File.C, Rank.R4), MoveType.Normal(false)) + val context = GameContext.initial.withMove(normalMove) + val json = JsonExporter.exportGameContext(context) + + assert(json.contains("\"c2\"")) + assert(json.contains("\"c4\"")) + } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala new file mode 100644 index 0000000..b14ea20 --- /dev/null +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala @@ -0,0 +1,122 @@ +package de.nowchess.io.json + +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class JsonModelExtraTestSuite extends AnyFunSuite with Matchers: + + test("JsonMetadata with all fields") { + val meta = JsonMetadata(Some("Event"), Some(Map("a" -> "b")), Some("2026-04-08"), Some("1-0")) + assert(meta.event.contains("Event")) + assert(meta.players.exists(_.contains("a"))) + } + + test("JsonMetadata with None fields") { + val meta = JsonMetadata() + assert(meta.event.isEmpty) + assert(meta.players.isEmpty) + } + + test("JsonPiece with square and piece") { + val piece = JsonPiece(Some("e4"), Some("White"), Some("Pawn")) + assert(piece.square.contains("e4")) + assert(piece.color.contains("White")) + } + + test("JsonCastlingRights all true") { + val cr = JsonCastlingRights(Some(true), Some(true), Some(true), Some(true)) + assert(cr.whiteKingSide.contains(true)) + assert(cr.blackQueenSide.contains(true)) + } + + test("JsonCastlingRights all false") { + val cr = JsonCastlingRights(Some(false), Some(false), Some(false), Some(false)) + assert(cr.whiteKingSide.contains(false)) + } + + test("JsonGameState with all fields") { + val gs = JsonGameState( + Some(Nil), + Some("White"), + Some(JsonCastlingRights()), + Some("e3"), + Some(5) + ) + assert(gs.board.contains(Nil)) + assert(gs.halfMoveClock.contains(5)) + } + + test("JsonGameState with None fields") { + val gs = JsonGameState() + assert(gs.board.isEmpty) + assert(gs.halfMoveClock.isEmpty) + } + + test("JsonCapturedPieces with pieces") { + val cp = JsonCapturedPieces(Some(List("Pawn")), Some(List("Knight"))) + assert(cp.byWhite.exists(_.contains("Pawn"))) + assert(cp.byBlack.exists(_.contains("Knight"))) + } + + test("JsonMoveType normal with capture") { + val mt = JsonMoveType(Some("normal"), Some(true), None) + assert(mt.`type`.contains("normal")) + assert(mt.isCapture.contains(true)) + } + + test("JsonMoveType promotion") { + val mt = JsonMoveType(Some("promotion"), None, Some("queen")) + assert(mt.`type`.contains("promotion")) + assert(mt.promotionPiece.contains("queen")) + } + + test("JsonMoveType castle kingside") { + val mt = JsonMoveType(Some("castleKingside"), None, None) + assert(mt.`type`.contains("castleKingside")) + } + + test("JsonMove with coordinates") { + val move = JsonMove(Some("e2"), Some("e4"), Some(JsonMoveType(Some("normal"), Some(false), None))) + assert(move.from.contains("e2")) + assert(move.to.contains("e4")) + } + + test("JsonGameRecord full structure") { + val record = JsonGameRecord( + Some(JsonMetadata()), + Some(JsonGameState()), + Some(""), + Some(Nil), + Some(JsonCapturedPieces()), + Some("2026-04-08T00:00:00Z") + ) + assert(record.metadata.nonEmpty) + assert(record.timestamp.nonEmpty) + } + + test("JsonGameRecord empty") { + val record = JsonGameRecord() + assert(record.metadata.isEmpty) + assert(record.moves.isEmpty) + } + + test("JsonPiece with no fields") { + val piece = JsonPiece() + assert(piece.square.isEmpty) + assert(piece.color.isEmpty) + assert(piece.piece.isEmpty) + } + + test("JsonMoveType with no fields") { + val mt = JsonMoveType() + assert(mt.`type`.isEmpty) + assert(mt.isCapture.isEmpty) + assert(mt.promotionPiece.isEmpty) + } + + test("JsonMove with empty fields") { + val move = JsonMove() + assert(move.from.isEmpty) + assert(move.to.isEmpty) + assert(move.`type`.isEmpty) + } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala new file mode 100644 index 0000000..d36ca21 --- /dev/null +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala @@ -0,0 +1,107 @@ +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.move.{Move, MoveType, PromotionPiece} +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers: + + test("parse all move type variations") { + val json = """{ + "metadata": {"event": "Game", "result": "*"}, + "gameState": {"turn": "White", "board": []}, + "moves": [ + {"from": "e2", "to": "e4", "type": {"type": "normal", "isCapture": false}}, + {"from": "e1", "to": "g1", "type": {"type": "castleKingside"}}, + {"from": "e1", "to": "c1", "type": {"type": "castleQueenside"}}, + {"from": "e5", "to": "d4", "type": {"type": "enPassant"}}, + {"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "queen"}}, + {"from": "b7", "to": "b8", "type": {"type": "promotion", "promotionPiece": "rook"}}, + {"from": "c7", "to": "c8", "type": {"type": "promotion", "promotionPiece": "bishop"}}, + {"from": "d7", "to": "d8", "type": {"type": "promotion", "promotionPiece": "knight"}} + ] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.moves.length == 8) + assert(ctx.moves(0).moveType == MoveType.Normal(false)) + assert(ctx.moves(1).moveType == MoveType.CastleKingside) + assert(ctx.moves(2).moveType == MoveType.CastleQueenside) + assert(ctx.moves(3).moveType == MoveType.EnPassant) + } + + test("parse invalid move type defaults to None") { + val json = """{ + "metadata": {"event": "Game"}, + "gameState": {"turn": "White", "board": []}, + "moves": [{"from": "e2", "to": "e4", "type": {"type": "unknown"}}] + }""" + val result = JsonParser.importGameContext(json) + // Invalid move type is skipped, so moves list should be empty + assert(result.isRight) + } + + test("parse promotion with default piece") { + val json = """{ + "metadata": {}, + "gameState": {"turn": "White", "board": []}, + "moves": [{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "invalid"}}] + }""" + val result = JsonParser.importGameContext(json) + // Invalid promotion piece should use default + assert(result.isRight) + } + + test("parse move with missing from/to skips it") { + val json = """{ + "metadata": {}, + "gameState": {"turn": "White", "board": []}, + "moves": [{"from": "e2", "to": "invalid", "type": {"type": "normal"}}] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + // Invalid square should be filtered out + assert(ctx.moves.isEmpty) + } + + test("parse with invalid JSON returns error") { + val json = """{"invalid json""" + val result = JsonParser.importGameContext(json) + assert(result.isLeft) + } + + test("parse normal move with isCapture true") { + 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 move = ctx.moves.head + assert(move.moveType == MoveType.Normal(true)) + } + + test("parse board with invalid pieces filters them") { + val json = """{ + "metadata": {}, + "gameState": { + "turn": "White", + "board": [ + {"square": "a1", "color": "White", "piece": "Rook"}, + {"square": "invalid", "color": "White", "piece": "King"}, + {"square": "a2", "color": "Invalid", "piece": "Pawn"} + ] + } + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + // Only valid piece should be in board + assert(ctx.board.pieces.size == 1) + }