feat: refactor IO module to API structure and add FEN export functionality
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
package de.nowchess.api.dto
|
||||||
|
|
||||||
|
case class ImportFenRequest(fen: String)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.nowchess.api.dto
|
||||||
|
|
||||||
|
case class ImportPgnRequest(pgn: String)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package de.nowchess.api.io
|
||||||
|
|
||||||
|
import de.nowchess.api.board.*
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
|
||||||
|
object FenExporter extends GameContextExport:
|
||||||
|
|
||||||
|
def boardToFen(board: Board): String =
|
||||||
|
Rank.values.reverse
|
||||||
|
.map(rank => buildRankString(board, rank))
|
||||||
|
.mkString("/")
|
||||||
|
|
||||||
|
def gameContextToFen(context: GameContext): String =
|
||||||
|
val piecePlacement = boardToFen(context.board)
|
||||||
|
val activeColor = if context.turn == Color.White then "w" else "b"
|
||||||
|
val castling = castlingString(context.castlingRights)
|
||||||
|
val enPassant = context.enPassantSquare.map(_.toString).getOrElse("-")
|
||||||
|
val fullMoveNumber = 1 + (context.moves.length / 2)
|
||||||
|
s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber"
|
||||||
|
|
||||||
|
def exportGameContext(context: GameContext): String = gameContextToFen(context)
|
||||||
|
|
||||||
|
private def buildRankString(board: Board, rank: Rank): String =
|
||||||
|
val rankSquares = File.values.map(file => Square(file, rank))
|
||||||
|
val (result, emptyCount) = rankSquares.foldLeft(("", 0)):
|
||||||
|
case ((acc, empty), square) =>
|
||||||
|
board.pieceAt(square) match
|
||||||
|
case Some(piece) =>
|
||||||
|
val flushed = if empty > 0 then acc + empty.toString else acc
|
||||||
|
(flushed + pieceToFenChar(piece), 0)
|
||||||
|
case None =>
|
||||||
|
(acc, empty + 1)
|
||||||
|
if emptyCount > 0 then result + emptyCount.toString else result
|
||||||
|
|
||||||
|
private def castlingString(rights: CastlingRights): String =
|
||||||
|
val wk = if rights.whiteKingSide then "K" else ""
|
||||||
|
val wq = if rights.whiteQueenSide then "Q" else ""
|
||||||
|
val bk = if rights.blackKingSide then "k" else ""
|
||||||
|
val bq = if rights.blackQueenSide then "q" else ""
|
||||||
|
val result = s"$wk$wq$bk$bq"
|
||||||
|
if result.isEmpty then "-" else result
|
||||||
|
|
||||||
|
private def pieceToFenChar(piece: Piece): Char =
|
||||||
|
val base = piece.pieceType match
|
||||||
|
case PieceType.Pawn => 'p'
|
||||||
|
case PieceType.Knight => 'n'
|
||||||
|
case PieceType.Bishop => 'b'
|
||||||
|
case PieceType.Rook => 'r'
|
||||||
|
case PieceType.Queen => 'q'
|
||||||
|
case PieceType.King => 'k'
|
||||||
|
if piece.color == Color.White then base.toUpper else base
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package de.nowchess.api.io
|
||||||
|
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
|
||||||
|
trait GameContextExport:
|
||||||
|
def exportGameContext(context: GameContext): String
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package de.nowchess.api.io
|
||||||
|
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
|
||||||
|
trait GameContextImport:
|
||||||
|
def importGameContext(input: String): Either[String, GameContext]
|
||||||
@@ -48,7 +48,6 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
implementation(project(":modules:api"))
|
implementation(project(":modules:api"))
|
||||||
implementation(project(":modules:io"))
|
|
||||||
implementation(project(":modules:rule"))
|
implementation(project(":modules:rule"))
|
||||||
implementation(project(":modules:bot"))
|
implementation(project(":modules:bot"))
|
||||||
|
|
||||||
@@ -69,6 +68,8 @@ dependencies {
|
|||||||
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
|
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
|
||||||
|
|
||||||
|
|
||||||
|
testImplementation(project(":modules:io"))
|
||||||
|
|
||||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package de.nowchess.chess.client
|
package de.nowchess.chess.client
|
||||||
|
|
||||||
|
import de.nowchess.api.dto.{ImportFenRequest, ImportPgnRequest}
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest}
|
|
||||||
import jakarta.ws.rs.*
|
import jakarta.ws.rs.*
|
||||||
import jakarta.ws.rs.core.MediaType
|
import jakarta.ws.rs.core.MediaType
|
||||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||||
import de.nowchess.api.board.Square
|
import de.nowchess.api.board.Square
|
||||||
import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer}
|
|
||||||
import io.quarkus.jackson.ObjectMapperCustomizer
|
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||||
import jakarta.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.nowchess.chess.config
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.{DeserializationContext, KeyDeserializer}
|
||||||
|
import de.nowchess.api.board.Square
|
||||||
|
|
||||||
|
class SquareKeyDeserializer extends KeyDeserializer:
|
||||||
|
override def deserializeKey(key: String, ctx: DeserializationContext): AnyRef =
|
||||||
|
Square.fromAlgebraic(key).orNull
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.nowchess.chess.config
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
|
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
|
||||||
|
import de.nowchess.api.board.Square
|
||||||
|
|
||||||
|
class SquareKeySerializer extends JsonSerializer[Square]:
|
||||||
|
override def serialize(value: Square, gen: JsonGenerator, provider: SerializerProvider): Unit =
|
||||||
|
gen.writeFieldName(value.toString)
|
||||||
@@ -7,7 +7,7 @@ import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
|||||||
import de.nowchess.chess.controller.Parser
|
import de.nowchess.chess.controller.Parser
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult}
|
import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult}
|
||||||
import de.nowchess.io.{GameContextExport, GameContextImport}
|
import de.nowchess.api.io.{GameContextExport, GameContextImport}
|
||||||
import de.nowchess.rules.RuleSet
|
import de.nowchess.rules.RuleSet
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.nowchess.api.board.Square
|
import de.nowchess.api.board.Square
|
||||||
import de.nowchess.api.dto.*
|
import de.nowchess.api.dto.*
|
||||||
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
||||||
|
import de.nowchess.api.io.FenExporter
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||||
import de.nowchess.chess.client.IoServiceClient
|
import de.nowchess.chess.client.IoServiceClient
|
||||||
@@ -12,9 +13,7 @@ import de.nowchess.chess.engine.GameEngine
|
|||||||
import de.nowchess.chess.exception.{BadRequestException, GameNotFoundException}
|
import de.nowchess.chess.exception.{BadRequestException, GameNotFoundException}
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
|
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
|
||||||
import de.nowchess.io.fen.FenExporter
|
import de.nowchess.rules.pgn.PgnExporter
|
||||||
import de.nowchess.io.pgn.PgnExporter
|
|
||||||
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest}
|
|
||||||
import io.smallrye.mutiny.Multi
|
import io.smallrye.mutiny.Multi
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import de.nowchess.api.board.{Board, Color, File, PieceType, Rank, Square}
|
|||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, InvalidMoveReason, MoveRedoneEvent, Observer}
|
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, InvalidMoveReason, MoveRedoneEvent, Observer}
|
||||||
import de.nowchess.io.GameContextImport
|
import de.nowchess.api.io.GameContextImport
|
||||||
import de.nowchess.rules.RuleSet
|
import de.nowchess.rules.RuleSet
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import scala.collection.mutable
|
|||||||
import de.nowchess.api.board.{Board, Color}
|
import de.nowchess.api.board.{Board, Color}
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.chess.observer.{GameEvent, Observer, PgnLoadedEvent}
|
import de.nowchess.chess.observer.{GameEvent, Observer, PgnLoadedEvent}
|
||||||
import de.nowchess.io.pgn.PgnParser
|
import de.nowchess.api.io.FenExporter
|
||||||
import de.nowchess.io.fen.FenParser
|
import de.nowchess.io.fen.FenParser
|
||||||
import de.nowchess.io.pgn.PgnExporter
|
import de.nowchess.io.pgn.PgnParser
|
||||||
import de.nowchess.io.fen.FenExporter
|
import de.nowchess.rules.pgn.PgnExporter
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import de.nowchess.api.dto.*
|
|||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.chess.client.IoServiceClient
|
import de.nowchess.chess.client.IoServiceClient
|
||||||
import de.nowchess.chess.exception.BadRequestException
|
import de.nowchess.chess.exception.BadRequestException
|
||||||
import de.nowchess.io.fen.FenExporter
|
import de.nowchess.api.io.FenExporter
|
||||||
import de.nowchess.io.pgn.PgnParser
|
import de.nowchess.io.pgn.PgnParser
|
||||||
import io.quarkus.test.InjectMock
|
import io.quarkus.test.InjectMock
|
||||||
import io.quarkus.test.junit.QuarkusTest
|
import io.quarkus.test.junit.QuarkusTest
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
package de.nowchess.io
|
package de.nowchess.io
|
||||||
|
|
||||||
import de.nowchess.api.game.GameContext
|
type GameContextExport = de.nowchess.api.io.GameContextExport
|
||||||
|
|
||||||
trait GameContextExport:
|
|
||||||
|
|
||||||
def exportGameContext(context: GameContext): String
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
package de.nowchess.io
|
package de.nowchess.io
|
||||||
|
|
||||||
import de.nowchess.api.game.GameContext
|
type GameContextImport = de.nowchess.api.io.GameContextImport
|
||||||
|
|
||||||
trait GameContextImport:
|
|
||||||
|
|
||||||
def importGameContext(input: String): Either[String, GameContext]
|
|
||||||
|
|||||||
@@ -1,57 +1,11 @@
|
|||||||
package de.nowchess.io.fen
|
package de.nowchess.io.fen
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.Board
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.api.io.{FenExporter => ApiFenExporter}
|
||||||
import de.nowchess.io.GameContextExport
|
import de.nowchess.io.GameContextExport
|
||||||
|
|
||||||
object FenExporter extends GameContextExport:
|
object FenExporter extends GameContextExport:
|
||||||
|
def boardToFen(board: Board): String = ApiFenExporter.boardToFen(board)
|
||||||
/** Convert a Board to FEN piece-placement string (rank 8 to rank 1, separated by '/'). */
|
def gameContextToFen(context: GameContext): String = ApiFenExporter.gameContextToFen(context)
|
||||||
def boardToFen(board: Board): String =
|
def exportGameContext(context: GameContext): String = ApiFenExporter.exportGameContext(context)
|
||||||
Rank.values.reverse
|
|
||||||
.map(rank => buildRankString(board, rank))
|
|
||||||
.mkString("/")
|
|
||||||
|
|
||||||
/** Build the FEN representation for a single rank. */
|
|
||||||
private def buildRankString(board: Board, rank: Rank): String =
|
|
||||||
val rankSquares = File.values.map(file => Square(file, rank))
|
|
||||||
val (result, emptyCount) = rankSquares.foldLeft(("", 0)):
|
|
||||||
case ((acc, empty), square) =>
|
|
||||||
board.pieceAt(square) match
|
|
||||||
case Some(piece) =>
|
|
||||||
val flushed = if empty > 0 then acc + empty.toString else acc
|
|
||||||
(flushed + pieceToFenChar(piece), 0)
|
|
||||||
case None =>
|
|
||||||
(acc, empty + 1)
|
|
||||||
if emptyCount > 0 then result + emptyCount.toString else result
|
|
||||||
|
|
||||||
/** Convert a GameContext to a complete FEN string. */
|
|
||||||
def gameContextToFen(context: GameContext): String =
|
|
||||||
val piecePlacement = boardToFen(context.board)
|
|
||||||
val activeColor = if context.turn == Color.White then "w" else "b"
|
|
||||||
val castling = castlingString(context.castlingRights)
|
|
||||||
val enPassant = context.enPassantSquare.map(_.toString).getOrElse("-")
|
|
||||||
val fullMoveNumber = 1 + (context.moves.length / 2)
|
|
||||||
s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber"
|
|
||||||
|
|
||||||
def exportGameContext(context: GameContext): String = gameContextToFen(context)
|
|
||||||
|
|
||||||
/** Convert castling rights to FEN notation. */
|
|
||||||
private def castlingString(rights: CastlingRights): String =
|
|
||||||
val wk = if rights.whiteKingSide then "K" else ""
|
|
||||||
val wq = if rights.whiteQueenSide then "Q" else ""
|
|
||||||
val bk = if rights.blackKingSide then "k" else ""
|
|
||||||
val bq = if rights.blackQueenSide then "q" else ""
|
|
||||||
val result = s"$wk$wq$bk$bq"
|
|
||||||
if result.isEmpty then "-" else result
|
|
||||||
|
|
||||||
/** Convert a Piece to its FEN character (uppercase = White, lowercase = Black). */
|
|
||||||
private def pieceToFenChar(piece: Piece): Char =
|
|
||||||
val base = piece.pieceType match
|
|
||||||
case PieceType.Pawn => 'p'
|
|
||||||
case PieceType.Knight => 'n'
|
|
||||||
case PieceType.Bishop => 'b'
|
|
||||||
case PieceType.Rook => 'r'
|
|
||||||
case PieceType.Queen => 'q'
|
|
||||||
case PieceType.King => 'k'
|
|
||||||
if piece.color == Color.White then base.toUpper else base
|
|
||||||
|
|||||||
@@ -1,77 +1,10 @@
|
|||||||
package de.nowchess.io.pgn
|
package de.nowchess.io.pgn
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.api.move.Move
|
||||||
import de.nowchess.io.GameContextExport
|
import de.nowchess.io.GameContextExport
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.pgn.{PgnExporter => RulesPgnExporter}
|
||||||
|
|
||||||
object PgnExporter extends GameContextExport:
|
object PgnExporter extends GameContextExport:
|
||||||
|
def exportGameContext(context: GameContext): String = RulesPgnExporter.exportGameContext(context)
|
||||||
/** Export a GameContext to PGN format. */
|
def exportGame(headers: Map[String, String], moves: List[Move]): String = RulesPgnExporter.exportGame(headers, moves)
|
||||||
def exportGameContext(context: GameContext): String =
|
|
||||||
val headers = Map(
|
|
||||||
"Event" -> "?",
|
|
||||||
"White" -> "?",
|
|
||||||
"Black" -> "?",
|
|
||||||
"Result" -> "*",
|
|
||||||
)
|
|
||||||
|
|
||||||
exportGame(headers, context.moves)
|
|
||||||
|
|
||||||
/** Export a game with headers and moves to PGN format. */
|
|
||||||
def exportGame(headers: Map[String, String], moves: List[Move]): String =
|
|
||||||
val headerLines = headers
|
|
||||||
.map { case (key, value) =>
|
|
||||||
s"""[$key "$value"]"""
|
|
||||||
}
|
|
||||||
.mkString("\n")
|
|
||||||
|
|
||||||
val moveText =
|
|
||||||
if moves.isEmpty then ""
|
|
||||||
else
|
|
||||||
val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move))
|
|
||||||
val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) }
|
|
||||||
|
|
||||||
val groupedMoves = sanMoves.zipWithIndex.groupBy(_._2 / 2)
|
|
||||||
val moveLines = for (moveNumber, movePairs) <- groupedMoves.toList.sortBy(_._1) yield
|
|
||||||
val moveNum = moveNumber + 1
|
|
||||||
val whiteMoveStr = movePairs.find(_._2 % 2 == 0).map(_._1).getOrElse("")
|
|
||||||
val blackMoveStr = movePairs.find(_._2 % 2 == 1).map(_._1).getOrElse("")
|
|
||||||
if blackMoveStr.isEmpty then s"$moveNum. $whiteMoveStr"
|
|
||||||
else s"$moveNum. $whiteMoveStr $blackMoveStr"
|
|
||||||
|
|
||||||
val termination = headers.getOrElse("Result", "*")
|
|
||||||
moveLines.mkString(" ") + s" $termination"
|
|
||||||
|
|
||||||
if headerLines.isEmpty then moveText
|
|
||||||
else if moveText.isEmpty then headerLines
|
|
||||||
else s"$headerLines\n\n$moveText"
|
|
||||||
|
|
||||||
/** Convert a Move to Standard Algebraic Notation using the board state before the move. */
|
|
||||||
private def moveToAlgebraic(move: Move, boardBefore: Board): String =
|
|
||||||
move.moveType match
|
|
||||||
case MoveType.CastleKingside => "O-O"
|
|
||||||
case MoveType.CastleQueenside => "O-O-O"
|
|
||||||
case MoveType.EnPassant => s"${move.from.file.toString.toLowerCase}x${move.to}"
|
|
||||||
case MoveType.Promotion(pp) =>
|
|
||||||
val promSuffix = pp match
|
|
||||||
case PromotionPiece.Queen => "=Q"
|
|
||||||
case PromotionPiece.Rook => "=R"
|
|
||||||
case PromotionPiece.Bishop => "=B"
|
|
||||||
case PromotionPiece.Knight => "=N"
|
|
||||||
val isCapture = boardBefore.pieceAt(move.to).isDefined
|
|
||||||
if isCapture then s"${move.from.file.toString.toLowerCase}x${move.to}$promSuffix"
|
|
||||||
else s"${move.to}$promSuffix"
|
|
||||||
case MoveType.Normal(isCapture) =>
|
|
||||||
val dest = move.to.toString
|
|
||||||
val capStr = if isCapture then "x" else ""
|
|
||||||
boardBefore.pieceAt(move.from).map(_.pieceType).getOrElse(PieceType.Pawn) match
|
|
||||||
case PieceType.Pawn =>
|
|
||||||
if isCapture then s"${move.from.file.toString.toLowerCase}x$dest"
|
|
||||||
else dest
|
|
||||||
case PieceType.Knight => s"N$capStr$dest"
|
|
||||||
case PieceType.Bishop => s"B$capStr$dest"
|
|
||||||
case PieceType.Rook => s"R$capStr$dest"
|
|
||||||
case PieceType.Queen => s"Q$capStr$dest"
|
|
||||||
case PieceType.King => s"K$capStr$dest"
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package de.nowchess.rules.pgn
|
||||||
|
|
||||||
|
import de.nowchess.api.board.*
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.api.io.GameContextExport
|
||||||
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
|
|
||||||
|
object PgnExporter extends GameContextExport:
|
||||||
|
|
||||||
|
def exportGameContext(context: GameContext): String =
|
||||||
|
exportGame(
|
||||||
|
Map("Event" -> "?", "White" -> "?", "Black" -> "?", "Result" -> "*"),
|
||||||
|
context.moves,
|
||||||
|
)
|
||||||
|
|
||||||
|
def exportGame(headers: Map[String, String], moves: List[Move]): String =
|
||||||
|
val headerLines = headers.map { case (k, v) => s"""[$k "$v"]""" }.mkString("\n")
|
||||||
|
val moveText = if moves.isEmpty then "" else buildMoveText(headers, moves)
|
||||||
|
if headerLines.isEmpty then moveText
|
||||||
|
else if moveText.isEmpty then headerLines
|
||||||
|
else s"$headerLines\n\n$moveText"
|
||||||
|
|
||||||
|
private def buildMoveText(headers: Map[String, String], moves: List[Move]): String =
|
||||||
|
val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move))
|
||||||
|
val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) }
|
||||||
|
val grouped = sanMoves.zipWithIndex.groupBy(_._2 / 2)
|
||||||
|
val moveLines = grouped.toList.sortBy(_._1).map { case (n, pairs) =>
|
||||||
|
val w = pairs.find(_._2 % 2 == 0).map(_._1).getOrElse("")
|
||||||
|
val b = pairs.find(_._2 % 2 == 1).map(_._1).getOrElse("")
|
||||||
|
if b.isEmpty then s"${n + 1}. $w" else s"${n + 1}. $w $b"
|
||||||
|
}
|
||||||
|
moveLines.mkString(" ") + s" ${headers.getOrElse("Result", "*")}"
|
||||||
|
|
||||||
|
private def moveToAlgebraic(move: Move, boardBefore: Board): String =
|
||||||
|
move.moveType match
|
||||||
|
case MoveType.CastleKingside => "O-O"
|
||||||
|
case MoveType.CastleQueenside => "O-O-O"
|
||||||
|
case MoveType.EnPassant => s"${move.from.file.toString.toLowerCase}x${move.to}"
|
||||||
|
case MoveType.Promotion(pp) =>
|
||||||
|
val suffix = pp match
|
||||||
|
case PromotionPiece.Queen => "=Q"
|
||||||
|
case PromotionPiece.Rook => "=R"
|
||||||
|
case PromotionPiece.Bishop => "=B"
|
||||||
|
case PromotionPiece.Knight => "=N"
|
||||||
|
val isCapture = boardBefore.pieceAt(move.to).isDefined
|
||||||
|
if isCapture then s"${move.from.file.toString.toLowerCase}x${move.to}$suffix"
|
||||||
|
else s"${move.to}$suffix"
|
||||||
|
case MoveType.Normal(isCapture) =>
|
||||||
|
val dest = move.to.toString
|
||||||
|
val capStr = if isCapture then "x" else ""
|
||||||
|
boardBefore.pieceAt(move.from).map(_.pieceType).getOrElse(PieceType.Pawn) match
|
||||||
|
case PieceType.Pawn => if isCapture then s"${move.from.file.toString.toLowerCase}x$dest" else dest
|
||||||
|
case PieceType.Knight => s"N$capStr$dest"
|
||||||
|
case PieceType.Bishop => s"B$capStr$dest"
|
||||||
|
case PieceType.Rook => s"R$capStr$dest"
|
||||||
|
case PieceType.Queen => s"Q$capStr$dest"
|
||||||
|
case PieceType.King => s"K$capStr$dest"
|
||||||
Reference in New Issue
Block a user