fix: used external lib
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
shahdlala66
2026-04-07 15:21:48 +02:00
parent 33c0260b75
commit ef9bbcfe85
6 changed files with 427 additions and 420 deletions
+6
View File
@@ -41,6 +41,12 @@ dependencies {
implementation(project(":modules:api"))
implementation(project(":modules:rule"))
// Jackson for JSON serialization/deserialization
implementation("com.fasterxml.jackson.core:jackson-databind:${versions["JACKSON"]!!}")
implementation("com.fasterxml.jackson.core:jackson-core:${versions["JACKSON"]!!}")
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${versions["JACKSON"]!!}")
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
@@ -1,14 +1,16 @@
package de.nowchess.io.json
import com.fasterxml.jackson.databind.{ObjectMapper, SerializationFeature}
import com.fasterxml.jackson.core.util.{DefaultIndenter, DefaultPrettyPrinter}
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.*
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.game.GameContext
import de.nowchess.io.GameContextExport
import de.nowchess.io.pgn.PgnExporter
import java.time.Instant
import scala.collection.mutable
import java.time.{LocalDate, ZonedDateTime, ZoneId}
/** Exports a GameContext to a comprehensive JSON format.
/** Exports a GameContext to a comprehensive JSON format using Jackson.
*
* The JSON includes:
* - Game metadata (players, event, date, result)
@@ -19,145 +21,114 @@ import scala.collection.mutable
* - Timestamp for record-keeping
*/
object JsonExporter extends GameContextExport:
private val mapper = createMapper()
private def createMapper(): ObjectMapper =
val mapper = new ObjectMapper()
.registerModule(DefaultScalaModule)
// Configure pretty printer with custom spacing to match test expectations
val indenter = new DefaultIndenter(" ", "\n")
val printer = new DefaultPrettyPrinter()
printer.indentArraysWith(indenter)
printer.indentObjectsWith(indenter)
mapper.setDefaultPrettyPrinter(printer)
mapper.enable(SerializationFeature.INDENT_OUTPUT)
mapper
def exportGameContext(context: GameContext): String =
val metadata = buildMetadata()
val gameState = buildGameState(context)
val pgnMoves = PgnExporter.exportGameContext(context)
val moves = exportMoves(context.moves)
val captured = exportCapturedPieces(context.board)
val timestamp = buildTimestamp()
val record = buildGameRecord(context)
formatJson(mapper.writeValueAsString(record))
s"""{
"metadata": $metadata,
"gameState": $gameState,
"moveHistory": "${escapePgn(pgnMoves)}",
"moves": $moves,
"capturedPieces": $captured,
"timestamp": "$timestamp"
}"""
private def buildGameRecord(context: GameContext): JsonGameRecord =
JsonGameRecord(
metadata = Some(buildMetadata()),
gameState = Some(buildGameState(context)),
moveHistory = Some(PgnExporter.exportGameContext(context)),
moves = Some(buildMoves(context.moves)),
capturedPieces = Some(buildCapturedPieces(context.board)),
timestamp = Some(ZonedDateTime.now(ZoneId.of("UTC")).toString)
)
/** Build metadata section. */
private def buildMetadata(): String =
s"""{
"event": "Game",
"players": {
"white": "White Player",
"black": "Black Player"
},
"date": "${java.time.LocalDate.now}",
"result": "*"
}"""
private def buildMetadata(): JsonMetadata =
JsonMetadata(
event = Some("Game"),
players = Some(Map("white" -> "White Player", "black" -> "Black Player")),
date = Some(LocalDate.now().toString),
result = Some("*")
)
/** Build gameState section. */
private def buildGameState(context: GameContext): String =
s"""{
"board": ${exportBoard(context.board)},
"turn": "${context.turn.label}",
"castlingRights": ${exportCastlingRights(context.castlingRights)},
"enPassantSquare": ${exportEnPassant(context.enPassantSquare)},
"halfMoveClock": ${context.halfMoveClock}
}"""
private def buildGameState(context: GameContext): JsonGameState =
JsonGameState(
board = Some(buildBoardPieces(context.board)),
turn = Some(context.turn.label),
castlingRights = Some(buildCastlingRights(context.castlingRights)),
enPassantSquare = context.enPassantSquare.map(_.toString),
halfMoveClock = Some(context.halfMoveClock)
)
/** Build ISO 8601 timestamp. */
private def buildTimestamp(): String =
java.time.ZonedDateTime.now(java.time.ZoneId.of("UTC")).toString
/** Export board as a list of piece placements. */
private def exportBoard(board: Board): String =
val pieces = board.pieces.toList.map { case (square, piece) =>
s"""{"square": "${square}", "color": "${piece.color.label}", "piece": "${piece.pieceType.label}"}"""
}
if pieces.isEmpty then "[]" else s"[${pieces.mkString(", ")}]"
/** Export castling rights. */
private def exportCastlingRights(rights: CastlingRights): String =
s"""{
"whiteKingSide": ${rights.whiteKingSide},
"whiteQueenSide": ${rights.whiteQueenSide},
"blackKingSide": ${rights.blackKingSide},
"blackQueenSide": ${rights.blackQueenSide}
}"""
/** Export en passant square. */
private def exportEnPassant(epSquare: Option[Square]): String =
epSquare match
case Some(sq) => s""""${sq.toString}""""
case None => "null"
/** Export captured pieces. */
private def exportCapturedPieces(board: Board): String =
val initialBoard = Board.initial
val allSquares = Square.all
// Find captured pieces by comparing with initial position
var whiteCaptured = List.empty[String]
var blackCaptured = List.empty[String]
for square <- allSquares do
initialBoard.pieceAt(square) match
case Some(initialPiece) =>
board.pieceAt(square) match
case None =>
// Piece was captured
val pieceName = initialPiece.pieceType.label
if initialPiece.color == Color.White then
whiteCaptured = whiteCaptured :+ pieceName
else
blackCaptured = blackCaptured :+ pieceName
case Some(_) => // Piece still there or promoted
case None => // Square was empty initially
val whiteList = if whiteCaptured.isEmpty then "[]"
else s"[${whiteCaptured.map(p => s""""$p"""").mkString(", ")}]"
val blackList = if blackCaptured.isEmpty then "[]"
else s"[${blackCaptured.map(p => s""""$p"""").mkString(", ")}]"
s"""{
"byWhite": $blackList,
"byBlack": $whiteList
}"""
/** Export moves as an array of move objects. */
private def exportMoves(moves: List[Move]): String =
val moveObjects = moves.map { move =>
val from = move.from.toString
val to = move.to.toString
val moveTypeJson = exportMoveType(move.moveType)
s""" {
"from": "$from",
"to": "$to",
"type": $moveTypeJson
}"""
private def buildBoardPieces(board: Board): List[JsonPiece] =
board.pieces.toList.map { case (sq, p) =>
JsonPiece(Some(sq.toString), Some(p.color.label), Some(p.pieceType.label))
}
if moveObjects.isEmpty then "[]"
else s"[\n${moveObjects.mkString(",\n")}\n ]"
private def buildCastlingRights(rights: CastlingRights): JsonCastlingRights =
JsonCastlingRights(
Some(rights.whiteKingSide),
Some(rights.whiteQueenSide),
Some(rights.blackKingSide),
Some(rights.blackQueenSide)
)
/** Export move type as a JSON object. */
private def exportMoveType(moveType: MoveType): String =
moveType match
private def buildMoves(moves: List[Move]): List[JsonMove] =
moves.map { m =>
val moveType = convertMoveType(m.moveType)
JsonMove(Some(m.from.toString), Some(m.to.toString), moveType)
}
private def convertMoveType(moveType: MoveType): Option[JsonMoveType] =
val (tpe, isC, pp) = moveType match {
case MoveType.Normal(isCapture) =>
s"""{"type": "normal", "isCapture": $isCapture}"""
(Some("normal"), Some(isCapture), None)
case MoveType.CastleKingside =>
s"""{"type": "castleKingside"}"""
(Some("castleKingside"), None, None)
case MoveType.CastleQueenside =>
s"""{"type": "castleQueenside"}"""
(Some("castleQueenside"), None, None)
case MoveType.EnPassant =>
s"""{"type": "enPassant"}"""
(Some("enPassant"), None, None)
case MoveType.Promotion(piece) =>
val pieceName = piece match
val pName = piece match {
case PromotionPiece.Queen => "queen"
case PromotionPiece.Rook => "rook"
case PromotionPiece.Bishop => "bishop"
case PromotionPiece.Knight => "knight"
s"""{"type": "promotion", "promotionPiece": "$pieceName"}"""
}
(Some("promotion"), None, Some(pName))
}
Some(JsonMoveType(tpe, isC, pp))
/** Escape PGN text for JSON string. */
private def escapePgn(pgn: String): String =
pgn.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
private def buildCapturedPieces(board: Board): JsonCapturedPieces =
val (byWhite, byBlack) = getCapturedPieces(board)
JsonCapturedPieces(Some(byWhite), Some(byBlack))
private def formatJson(json: String): String =
json
.replace(" : ", ": ")
.replaceAll("\\[\\s*\\]", "[]")
.replaceAll("\\{\\s*\\}", "{}")
private def getCapturedPieces(board: Board): (List[String], List[String]) =
val initialBoard = Board.initial
val captured = Square.all.flatMap { square =>
initialBoard.pieceAt(square).flatMap { initialPiece =>
board.pieceAt(square) match
case None => Some(initialPiece)
case Some(_) => None
}
}
val whiteCaptured = captured.filter(_.color == Color.White).map(_.pieceType.label).toList
val blackCaptured = captured.filter(_.color == Color.Black).map(_.pieceType.label).toList
(blackCaptured, whiteCaptured)
@@ -0,0 +1,55 @@
package de.nowchess.io.json
case class JsonMetadata(
event: Option[String] = None,
players: Option[Map[String, String]] = None,
date: Option[String] = None,
result: Option[String] = None
)
case class JsonPiece(
square: Option[String] = None,
color: Option[String] = None,
piece: Option[String] = None
)
case class JsonCastlingRights(
whiteKingSide: Option[Boolean] = None,
whiteQueenSide: Option[Boolean] = None,
blackKingSide: Option[Boolean] = None,
blackQueenSide: Option[Boolean] = None
)
case class JsonGameState(
board: Option[List[JsonPiece]] = None,
turn: Option[String] = None,
castlingRights: Option[JsonCastlingRights] = None,
enPassantSquare: Option[String] = None,
halfMoveClock: Option[Int] = None
)
case class JsonCapturedPieces(
byWhite: Option[List[String]] = None,
byBlack: Option[List[String]] = None
)
case class JsonMoveType(
`type`: Option[String] = None,
isCapture: Option[Boolean] = None,
promotionPiece: Option[String] = None
)
case class JsonMove(
from: Option[String] = None,
to: Option[String] = None,
`type`: Option[JsonMoveType] = None
)
case class JsonGameRecord(
metadata: Option[JsonMetadata] = None,
gameState: Option[JsonGameState] = None,
moveHistory: Option[String] = None,
moves: Option[List[JsonMove]] = None,
capturedPieces: Option[JsonCapturedPieces] = None,
timestamp: Option[String] = None
)
@@ -1,12 +1,14 @@
package de.nowchess.io.json
import com.fasterxml.jackson.databind.{ObjectMapper, DeserializationFeature}
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.*
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.game.GameContext
import de.nowchess.io.GameContextImport
import scala.util.{Try, Success, Failure}
import scala.util.Try
/** Imports a GameContext from JSON format.
/** Imports a GameContext from JSON format using Jackson.
*
* Parses JSON exported by JsonExporter and reconstructs the GameContext including:
* - Board state
@@ -20,281 +22,57 @@ import scala.util.{Try, Success, Failure}
*/
object JsonParser extends GameContextImport:
private val mapper = new ObjectMapper()
.registerModule(DefaultScalaModule)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
def importGameContext(input: String): Either[String, GameContext] =
for
data <- parseRootObject(input)
gameStateJson <- extractGameState(data)
gameStateData <- parseGameStateObject(gameStateJson)
context <- assembleGameContext(gameStateData, data)
yield context
Try(mapper.readValue(input, classOf[JsonGameRecord])).toEither
.left.map(e => "JSON parsing error: " + e.getMessage)
.flatMap { data =>
val gs = data.gameState.getOrElse(JsonGameState())
val rawBoard = gs.board.getOrElse(Nil)
val rawTurn = gs.turn.getOrElse("White")
val rawCr = gs.castlingRights.getOrElse(JsonCastlingRights())
val rawHmc = gs.halfMoveClock.getOrElse(0)
val rawMoves = data.moves.getOrElse(Nil)
/** Parse the root JSON object. */
private def parseRootObject(input: String): Either[String, Map[String, String]] =
Try {
parseJsonObject(input.trim)
}.toEither.left.map("JSON parsing error: " + _.getMessage)
for
board <- parseBoard(rawBoard)
turn <- parseTurn(rawTurn)
castlingRights = parseCastlingRights(rawCr)
enPassantSquare = gs.enPassantSquare.flatMap(s => Square.fromAlgebraic(s))
moves <- parseMoves(rawMoves)
yield GameContext(
board = board,
turn = turn,
castlingRights = castlingRights,
enPassantSquare = enPassantSquare,
halfMoveClock = rawHmc,
moves = moves
)
}
/** Extract gameState field (new format) or use old format. */
private def extractGameState(data: Map[String, String]): Either[String, String] =
Right(data.getOrElse("gameState", "{\"board\": [], \"turn\": \"White\", \"castlingRights\": {}, \"enPassantSquare\": \"null\", \"halfMoveClock\": \"0\"}"))
private def parseBoard(pieces: List[JsonPiece]): Either[String, Board] =
val parsedPieces = pieces.flatMap { p =>
for
sq <- p.square.flatMap(Square.fromAlgebraic)
color <- p.color.flatMap(parseColor)
pt <- p.piece.flatMap(parsePieceType)
yield (sq, Piece(color, pt))
}
Right(Board(parsedPieces.toMap))
/** Parse the gameState object. */
private def parseGameStateObject(json: String): Either[String, Map[String, String]] =
Try {
parseJsonObject(json)
}.toEither.left.map("Failed to parse gameState: " + _.getMessage)
private def parseTurn(color: String): Either[String, Color] =
parseColor(color).toRight(s"Invalid turn color: $color")
/** Assemble the complete GameContext from parsed data. */
private def assembleGameContext(gameStateData: Map[String, String], rootData: Map[String, String]): Either[String, GameContext] =
for
board <- parseBoard(gameStateData.getOrElse("board", "[]"))
turn <- parseTurn(gameStateData.getOrElse("turn", "White"))
castling <- parseCastlingRights(gameStateData.getOrElse("castlingRights", "{}"))
enPassant <- parseEnPassant(gameStateData.getOrElse("enPassantSquare", "null"))
halfMoveClock <- Try(gameStateData.getOrElse("halfMoveClock", "0").toInt)
.toEither.left.map("Invalid halfMoveClock: " + _.getMessage)
moves <- parseMoveHistory(rootData.getOrElse("moves", "[]"))
yield GameContext(
board = board,
turn = turn,
castlingRights = castling,
enPassantSquare = enPassant,
halfMoveClock = halfMoveClock,
moves = moves
)
/** Parse a simple JSON object into a Map of field names to JSON values. */
private def parseJsonObject(json: String): Map[String, String] =
val startIdx = json.indexOf('{')
if startIdx == -1 then Map.empty
else
val (obj, _) = extractJsonObject(json.substring(startIdx))
if obj.length < 2 then Map.empty
else
val content = obj.substring(1, obj.length - 1).trim
parseFields(content, Map.empty)
/** Recursively parse fields from JSON object content. */
private def parseFields(remaining: String, acc: Map[String, String]): Map[String, String] =
if remaining.isEmpty then acc
else
val colonIdx = remaining.indexOf(':')
if colonIdx == -1 then acc
else
val beforeColon = remaining.substring(0, colonIdx).trim
val key = beforeColon.replaceAll("""^[,\s"]+|["\s]+$""", "")
val afterColon = remaining.substring(colonIdx + 1).trim
val (value, rest) = extractJsonValue(afterColon)
if key.nonEmpty && value.nonEmpty then
parseFields(rest, acc + (key -> value))
else
parseFields(rest, acc)
/** Extract the next JSON value and return (value, remainder). */
private def extractJsonValue(json: String): (String, String) =
if json.startsWith("{") then extractJsonObject(json)
else if json.startsWith("[") then extractJsonArray(json)
else if json.startsWith("\"") then extractJsonString(json)
else extractJsonLiteral(json)
/** Extract a JSON object {...}. */
private def extractJsonObject(json: String): (String, String) =
var depth = 0
var i = 0
var inString = false
var escape = false
while i < json.length do
val ch = json.charAt(i)
if escape then
escape = false
else if ch == '\\' then
escape = true
else if ch == '"' then
inString = !inString
else if !inString then
if ch == '{' then depth += 1
else if ch == '}' then
depth -= 1
if depth == 0 then
return (json.substring(0, i + 1), json.substring(i + 1).dropWhile(c => c == ',' || c.isWhitespace))
i += 1
(json, "")
/** Extract a JSON array [...]. */
private def extractJsonArray(json: String): (String, String) =
var depth = 0
var i = 0
var inString = false
var escape = false
while i < json.length do
val ch = json.charAt(i)
if escape then
escape = false
else if ch == '\\' then
escape = true
else if ch == '"' then
inString = !inString
else if !inString then
if ch == '[' then depth += 1
else if ch == ']' then
depth -= 1
if depth == 0 then
return (json.substring(0, i + 1), json.substring(i + 1).dropWhile(c => c == ',' || c.isWhitespace))
i += 1
(json, "")
/** Extract a JSON string "...". */
private def extractJsonString(json: String): (String, String) =
var i = 1
var escape = false
while i < json.length do
val ch = json.charAt(i)
if escape then
escape = false
else if ch == '\\' then
escape = true
else if ch == '"' then
return (json.substring(0, i + 1), json.substring(i + 1).dropWhile(c => c == ',' || c.isWhitespace))
i += 1
(json, "")
/** Extract a JSON literal (number, true, false, null). */
private def extractJsonLiteral(json: String): (String, String) =
val endIdx = json.indexWhere(c => c == ',' || c == '}' || c == ']' || c.isWhitespace)
if endIdx == -1 then
(json, "")
else
(json.substring(0, endIdx), json.substring(endIdx).dropWhile(c => c == ',' || c.isWhitespace))
/** Parse board array. */
private def parseBoard(json: String): Either[String, Board] =
Try {
val clean = json.trim.drop(1).dropRight(1) // Remove [ and ]
if clean.isEmpty then Board(Map.empty)
else
val pieces = scala.collection.mutable.Map[Square, Piece]()
var remaining = clean
while remaining.contains("{") do
val startIdx = remaining.indexOf("{")
val (obj, rest) = extractJsonObject(remaining.substring(startIdx))
val fields = parseJsonObject(obj)
for
squareStr <- fields.get("square")
colorStr <- fields.get("color")
pieceStr <- fields.get("piece")
square <- Square.fromAlgebraic(unquote(squareStr))
color <- parseColorValueOption(unquote(colorStr))
pieceType <- parsePieceTypeValueOption(unquote(pieceStr))
do
pieces(square) = Piece(color, pieceType)
remaining = rest
Board(pieces.toMap)
}.toEither.left.map("Failed to parse board: " + _.getMessage)
/** Parse turn field. */
private def parseTurn(json: String): Either[String, Color] =
Try {
val color = unquote(json.trim)
if color == "White" then Color.White
else if color == "Black" then Color.Black
else throw new Exception(s"Invalid turn color: $color")
}.toEither.left.map("Failed to parse turn: " + _.getMessage)
/** Parse castling rights object. */
private def parseCastlingRights(json: String): Either[String, CastlingRights] =
Try {
val fields = parseJsonObject(json)
val wk = fields.get("whiteKingSide").map(_.toLowerCase == "true").getOrElse(false)
val wq = fields.get("whiteQueenSide").map(_.toLowerCase == "true").getOrElse(false)
val bk = fields.get("blackKingSide").map(_.toLowerCase == "true").getOrElse(false)
val bq = fields.get("blackQueenSide").map(_.toLowerCase == "true").getOrElse(false)
CastlingRights(wk, wq, bk, bq)
}.toEither.left.map("Failed to parse castling rights: " + _.getMessage)
/** Parse en passant square (string or null). */
private def parseEnPassant(json: String): Either[String, Option[Square]] =
Try {
val trimmed = json.trim
if trimmed == "null" then None: Option[Square]
else
val squareStr = unquote(trimmed)
Square.fromAlgebraic(squareStr)
}.toEither.left.map("Failed to parse en passant: " + _.getMessage)
/** Parse move history array. */
private def parseMoveHistory(json: String): Either[String, List[Move]] =
Try {
val clean = json.trim.drop(1).dropRight(1) // Remove [ and ]
if clean.isEmpty then List.empty
else
val moves = scala.collection.mutable.ListBuffer[Move]()
var remaining = clean
while remaining.contains("{") do
val startIdx = remaining.indexOf("{")
val (obj, rest) = extractJsonObject(remaining.substring(startIdx))
val fields = parseJsonObject(obj)
for
fromStr <- fields.get("from")
toStr <- fields.get("to")
typeJson <- fields.get("type")
from <- Square.fromAlgebraic(unquote(fromStr))
to <- Square.fromAlgebraic(unquote(toStr))
moveType <- parseMoveTypeUnsafe(typeJson)
do
moves += Move(from, to, moveType)
remaining = rest
moves.toList
}.toEither.left.map("Failed to parse move history: " + _.getMessage)
/** Parse move type object (unsafe version that throws on error for use in Try). */
private def parseMoveTypeUnsafe(json: String): Option[MoveType] =
val fields = parseJsonObject(json)
fields.get("type").map(unquote(_)) match
case Some("normal") =>
val isCapture = fields.get("isCapture").map(_.toLowerCase == "true").getOrElse(false)
Some(MoveType.Normal(isCapture))
case Some("castleKingside") =>
Some(MoveType.CastleKingside)
case Some("castleQueenside") =>
Some(MoveType.CastleQueenside)
case Some("enPassant") =>
Some(MoveType.EnPassant)
case Some("promotion") =>
val piece = fields.get("promotionPiece").map(unquote(_)) match
case Some("queen") => PromotionPiece.Queen
case Some("rook") => PromotionPiece.Rook
case Some("bishop") => PromotionPiece.Bishop
case Some("knight") => PromotionPiece.Knight
case _ => PromotionPiece.Queen
Some(MoveType.Promotion(piece))
case Some(unknown) =>
None
case None =>
None
/** Parse a color value (Option version for use in for-comprehension). */
private def parseColorValueOption(colorStr: String): Option[Color] =
if colorStr == "White" then Some(Color.White)
else if colorStr == "Black" then Some(Color.Black)
private def parseColor(color: String): Option[Color] =
if color == "White" then Some(Color.White)
else if color == "Black" then Some(Color.Black)
else None
/** Parse a piece type value (Option version for use in for-comprehension). */
private def parsePieceTypeValueOption(pieceStr: String): Option[PieceType] =
pieceStr match
private def parsePieceType(pt: String): Option[PieceType] =
pt match
case "Pawn" => Some(PieceType.Pawn)
case "Knight" => Some(PieceType.Knight)
case "Bishop" => Some(PieceType.Bishop)
@@ -303,6 +81,39 @@ object JsonParser extends GameContextImport:
case "King" => Some(PieceType.King)
case _ => None
/** Remove surrounding quotes from a JSON string value. */
private def unquote(s: String): String =
s.trim.dropWhile(_ == '"').dropWhile(_ == '\'').takeWhile(_ != '"').takeWhile(_ != '\'')
private def parseCastlingRights(cr: JsonCastlingRights): CastlingRights =
CastlingRights(
cr.whiteKingSide.getOrElse(false),
cr.whiteQueenSide.getOrElse(false),
cr.blackKingSide.getOrElse(false),
cr.blackQueenSide.getOrElse(false)
)
private def parseMoves(moves: List[JsonMove]): Either[String, List[Move]] =
Right(moves.flatMap { m =>
for
from <- m.from.flatMap(Square.fromAlgebraic)
to <- m.to.flatMap(Square.fromAlgebraic)
moveType <- m.`type`.flatMap(parseMoveType)
yield Move(from, to, moveType)
})
private def parseMoveType(mt: JsonMoveType): Option[MoveType] =
mt.`type` match
case Some("normal") =>
Some(MoveType.Normal(mt.isCapture.getOrElse(false)))
case Some("castleKingside") =>
Some(MoveType.CastleKingside)
case Some("castleQueenside") =>
Some(MoveType.CastleQueenside)
case Some("enPassant") =>
Some(MoveType.EnPassant)
case Some("promotion") =>
val piece = mt.promotionPiece match
case Some("queen") => PromotionPiece.Queen
case Some("rook") => PromotionPiece.Rook
case Some("bishop") => PromotionPiece.Bishop
case Some("knight") => PromotionPiece.Knight
case _ => PromotionPiece.Queen // default
Some(MoveType.Promotion(piece))
case _ => None