refactor(core): migrate GameState to GameContext and update FEN handling
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -8,6 +8,7 @@ import de.nowchess.api.board.{Color, Square}
|
|||||||
* @param kingSide king-side castling still legally available
|
* @param kingSide king-side castling still legally available
|
||||||
* @param queenSide queen-side castling still legally available
|
* @param queenSide queen-side castling still legally available
|
||||||
*/
|
*/
|
||||||
|
@deprecated("Use de.nowchess.api.board.CastlingRights via GameContext.", "NCS-22")
|
||||||
final case class CastlingRights(kingSide: Boolean, queenSide: Boolean)
|
final case class CastlingRights(kingSide: Boolean, queenSide: Boolean)
|
||||||
|
|
||||||
object CastlingRights:
|
object CastlingRights:
|
||||||
@@ -15,12 +16,14 @@ object CastlingRights:
|
|||||||
val Both: CastlingRights = CastlingRights(kingSide = true, queenSide = true)
|
val Both: CastlingRights = CastlingRights(kingSide = true, queenSide = true)
|
||||||
|
|
||||||
/** Outcome of a finished game. */
|
/** Outcome of a finished game. */
|
||||||
|
@deprecated("Use GameContext and derive game lifecycle from rules/engine state.", "NCS-22")
|
||||||
enum GameResult:
|
enum GameResult:
|
||||||
case WhiteWins
|
case WhiteWins
|
||||||
case BlackWins
|
case BlackWins
|
||||||
case Draw
|
case Draw
|
||||||
|
|
||||||
/** Lifecycle state of a game. */
|
/** Lifecycle state of a game. */
|
||||||
|
@deprecated("Use GameContext and engine events for lifecycle handling.", "NCS-22")
|
||||||
enum GameStatus:
|
enum GameStatus:
|
||||||
case NotStarted
|
case NotStarted
|
||||||
case InProgress
|
case InProgress
|
||||||
@@ -42,6 +45,7 @@ enum GameStatus:
|
|||||||
* @param fullMoveNumber increments after Black's move, starts at 1
|
* @param fullMoveNumber increments after Black's move, starts at 1
|
||||||
* @param status current lifecycle status of the game
|
* @param status current lifecycle status of the game
|
||||||
*/
|
*/
|
||||||
|
@deprecated("Use GameContext for runtime state; keep GameState only for legacy compatibility.", "NCS-22")
|
||||||
final case class GameState(
|
final case class GameState(
|
||||||
piecePlacement: String,
|
piecePlacement: String,
|
||||||
activeColor: Color,
|
activeColor: Color,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package de.nowchess.api.game
|
||||||
|
|
||||||
|
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Rank, Square}
|
||||||
|
import de.nowchess.api.move.Move
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class GameContextTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
test("GameContext.initial has initial board"):
|
||||||
|
GameContext.initial.board shouldBe Board.initial
|
||||||
|
|
||||||
|
test("GameContext.initial active color is White"):
|
||||||
|
GameContext.initial.turn shouldBe Color.White
|
||||||
|
|
||||||
|
test("GameContext.initial has full castling rights"):
|
||||||
|
GameContext.initial.castlingRights shouldBe CastlingRights.Initial
|
||||||
|
|
||||||
|
test("GameContext.initial en-passant square is None"):
|
||||||
|
GameContext.initial.enPassantSquare shouldBe None
|
||||||
|
|
||||||
|
test("GameContext.initial half-move clock is 0"):
|
||||||
|
GameContext.initial.halfMoveClock shouldBe 0
|
||||||
|
|
||||||
|
test("GameContext.initial move history is empty"):
|
||||||
|
GameContext.initial.moves shouldBe List.empty
|
||||||
|
|
||||||
|
test("withBoard updates only board"):
|
||||||
|
val square = Square(File.E, Rank.R4)
|
||||||
|
val updatedBoard = Board.initial.updated(square, de.nowchess.api.board.Piece.WhiteQueen)
|
||||||
|
val updated = GameContext.initial.withBoard(updatedBoard)
|
||||||
|
updated.board shouldBe updatedBoard
|
||||||
|
updated.turn shouldBe GameContext.initial.turn
|
||||||
|
updated.castlingRights shouldBe GameContext.initial.castlingRights
|
||||||
|
updated.enPassantSquare shouldBe GameContext.initial.enPassantSquare
|
||||||
|
updated.halfMoveClock shouldBe GameContext.initial.halfMoveClock
|
||||||
|
updated.moves shouldBe GameContext.initial.moves
|
||||||
|
|
||||||
|
test("withTurn updates only turn"):
|
||||||
|
val updated = GameContext.initial.withTurn(Color.Black)
|
||||||
|
updated.turn shouldBe Color.Black
|
||||||
|
updated.board shouldBe GameContext.initial.board
|
||||||
|
|
||||||
|
test("withCastlingRights updates castling rights"):
|
||||||
|
val rights = CastlingRights(
|
||||||
|
whiteKingSide = true,
|
||||||
|
whiteQueenSide = false,
|
||||||
|
blackKingSide = false,
|
||||||
|
blackQueenSide = true
|
||||||
|
)
|
||||||
|
GameContext.initial.withCastlingRights(rights).castlingRights shouldBe rights
|
||||||
|
|
||||||
|
test("withEnPassantSquare updates en-passant square"):
|
||||||
|
val square = Some(Square(File.E, Rank.R3))
|
||||||
|
GameContext.initial.withEnPassantSquare(square).enPassantSquare shouldBe square
|
||||||
|
|
||||||
|
test("withHalfMoveClock updates half-move clock"):
|
||||||
|
GameContext.initial.withHalfMoveClock(17).halfMoveClock shouldBe 17
|
||||||
|
|
||||||
|
test("withMove appends move to history"):
|
||||||
|
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||||
|
GameContext.initial.withMove(move).moves shouldBe List(move)
|
||||||
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package de.nowchess.api.game
|
|
||||||
|
|
||||||
import de.nowchess.api.board.Color
|
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
|
||||||
import org.scalatest.matchers.should.Matchers
|
|
||||||
|
|
||||||
class GameStateTest extends AnyFunSuite with Matchers:
|
|
||||||
|
|
||||||
test("CastlingRights.None has both flags false") {
|
|
||||||
CastlingRights.None.kingSide shouldBe false
|
|
||||||
CastlingRights.None.queenSide shouldBe false
|
|
||||||
}
|
|
||||||
|
|
||||||
test("CastlingRights.Both has both flags true") {
|
|
||||||
CastlingRights.Both.kingSide shouldBe true
|
|
||||||
CastlingRights.Both.queenSide shouldBe true
|
|
||||||
}
|
|
||||||
|
|
||||||
test("CastlingRights constructor sets fields") {
|
|
||||||
val cr = CastlingRights(kingSide = true, queenSide = false)
|
|
||||||
cr.kingSide shouldBe true
|
|
||||||
cr.queenSide shouldBe false
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameResult cases exist") {
|
|
||||||
GameResult.WhiteWins shouldBe GameResult.WhiteWins
|
|
||||||
GameResult.BlackWins shouldBe GameResult.BlackWins
|
|
||||||
GameResult.Draw shouldBe GameResult.Draw
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameStatus.NotStarted") {
|
|
||||||
GameStatus.NotStarted shouldBe GameStatus.NotStarted
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameStatus.InProgress") {
|
|
||||||
GameStatus.InProgress shouldBe GameStatus.InProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameStatus.Finished carries result") {
|
|
||||||
val status = GameStatus.Finished(GameResult.Draw)
|
|
||||||
status shouldBe GameStatus.Finished(GameResult.Draw)
|
|
||||||
status match
|
|
||||||
case GameStatus.Finished(r) => r shouldBe GameResult.Draw
|
|
||||||
case _ => fail("expected Finished")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameState.initial has standard FEN piece placement") {
|
|
||||||
GameState.initial.piecePlacement shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameState.initial active color is White") {
|
|
||||||
GameState.initial.activeColor shouldBe Color.White
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameState.initial white has full castling rights") {
|
|
||||||
GameState.initial.castlingWhite shouldBe CastlingRights.Both
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameState.initial black has full castling rights") {
|
|
||||||
GameState.initial.castlingBlack shouldBe CastlingRights.Both
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameState.initial en-passant target is None") {
|
|
||||||
GameState.initial.enPassantTarget shouldBe None
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameState.initial half-move clock is 0") {
|
|
||||||
GameState.initial.halfMoveClock shouldBe 0
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameState.initial full-move number is 1") {
|
|
||||||
GameState.initial.fullMoveNumber shouldBe 1
|
|
||||||
}
|
|
||||||
|
|
||||||
test("GameState.initial status is InProgress") {
|
|
||||||
GameState.initial.status shouldBe GameStatus.InProgress
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,8 @@ 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.chess.notation.{PgnExporter, PgnParser}
|
import de.nowchess.chess.notation.{PgnExporter, PgnParser}
|
||||||
import de.nowchess.rules.{RuleSet, StandardRules}
|
import de.nowchess.rules.RuleSet
|
||||||
|
import de.nowchess.rules.sets.StandardRules
|
||||||
|
|
||||||
/** Pure game engine that manages game state and notifies observers of state changes.
|
/** Pure game engine that manages game state and notifies observers of state changes.
|
||||||
* All rule queries delegate to the injected RuleSet.
|
* All rule queries delegate to the injected RuleSet.
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.chess.notation
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.game.{CastlingRights, GameState}
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.board.Color
|
|
||||||
|
|
||||||
object FenExporter:
|
object FenExporter:
|
||||||
|
|
||||||
@@ -31,20 +30,21 @@ object FenExporter:
|
|||||||
if emptyCount > 0 then rankChars += emptyCount.toString.charAt(0)
|
if emptyCount > 0 then rankChars += emptyCount.toString.charAt(0)
|
||||||
rankChars.mkString
|
rankChars.mkString
|
||||||
|
|
||||||
/** Convert a GameState to a complete FEN string. */
|
/** Convert a GameContext to a complete FEN string. */
|
||||||
def gameStateToFen(state: GameState): String =
|
def gameContextToFen(context: GameContext): String =
|
||||||
val piecePlacement = state.piecePlacement
|
val piecePlacement = boardToFen(context.board)
|
||||||
val activeColor = if state.activeColor == Color.White then "w" else "b"
|
val activeColor = if context.turn == Color.White then "w" else "b"
|
||||||
val castling = castlingString(state.castlingWhite, state.castlingBlack)
|
val castling = castlingString(context.castlingRights)
|
||||||
val enPassant = state.enPassantTarget.map(_.toString).getOrElse("-")
|
val enPassant = context.enPassantSquare.map(_.toString).getOrElse("-")
|
||||||
s"$piecePlacement $activeColor $castling $enPassant ${state.halfMoveClock} ${state.fullMoveNumber}"
|
val fullMoveNumber = 1 + (context.moves.length / 2)
|
||||||
|
s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber"
|
||||||
|
|
||||||
/** Convert castling rights to FEN notation. */
|
/** Convert castling rights to FEN notation. */
|
||||||
private def castlingString(white: CastlingRights, black: CastlingRights): String =
|
private def castlingString(rights: CastlingRights): String =
|
||||||
val wk = if white.kingSide then "K" else ""
|
val wk = if rights.whiteKingSide then "K" else ""
|
||||||
val wq = if white.queenSide then "Q" else ""
|
val wq = if rights.whiteQueenSide then "Q" else ""
|
||||||
val bk = if black.kingSide then "k" else ""
|
val bk = if rights.blackKingSide then "k" else ""
|
||||||
val bq = if black.queenSide then "q" else ""
|
val bq = if rights.blackQueenSide then "q" else ""
|
||||||
val result = s"$wk$wq$bk$bq"
|
val result = s"$wk$wq$bk$bq"
|
||||||
if result.isEmpty then "-" else result
|
if result.isEmpty then "-" else result
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.chess.notation
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.game.{CastlingRights, GameState, GameStatus}
|
import de.nowchess.api.game.GameContext
|
||||||
|
|
||||||
object FenParser:
|
object FenParser:
|
||||||
|
|
||||||
/** Parse a complete FEN string into a GameState.
|
/** Parse a complete FEN string into a GameContext.
|
||||||
* Returns None if the format is invalid. */
|
* Returns None if the format is invalid. */
|
||||||
def parseFen(fen: String): Option[GameState] =
|
def parseFen(fen: String): Option[GameContext] =
|
||||||
val parts = fen.trim.split("\\s+")
|
val parts = fen.trim.split("\\s+")
|
||||||
Option.when(parts.length == 6)(parts).flatMap: parts =>
|
Option.when(parts.length == 6)(parts).flatMap: parts =>
|
||||||
for
|
for
|
||||||
_ <- parseBoard(parts(0))
|
board <- parseBoard(parts(0))
|
||||||
activeColor <- parseColor(parts(1))
|
activeColor <- parseColor(parts(1))
|
||||||
castlingRights <- parseCastling(parts(2))
|
castlingRights <- parseCastling(parts(2))
|
||||||
enPassant <- parseEnPassant(parts(3))
|
enPassant <- parseEnPassant(parts(3))
|
||||||
halfMoveClock <- parts(4).toIntOption
|
halfMoveClock <- parts(4).toIntOption
|
||||||
fullMoveNumber <- parts(5).toIntOption
|
fullMoveNumber <- parts(5).toIntOption
|
||||||
if halfMoveClock >= 0 && fullMoveNumber >= 1
|
if halfMoveClock >= 0 && fullMoveNumber >= 1
|
||||||
yield GameState(
|
yield GameContext(
|
||||||
piecePlacement = parts(0),
|
board = board,
|
||||||
activeColor = activeColor,
|
turn = activeColor,
|
||||||
castlingWhite = castlingRights._1,
|
castlingRights = castlingRights,
|
||||||
castlingBlack = castlingRights._2,
|
enPassantSquare = enPassant,
|
||||||
enPassantTarget = enPassant,
|
|
||||||
halfMoveClock = halfMoveClock,
|
halfMoveClock = halfMoveClock,
|
||||||
fullMoveNumber = fullMoveNumber,
|
moves = List.empty
|
||||||
status = GameStatus.InProgress
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Parse active color ("w" or "b"). */
|
/** Parse active color ("w" or "b"). */
|
||||||
@@ -35,14 +33,17 @@ object FenParser:
|
|||||||
else if s == "b" then Some(Color.Black)
|
else if s == "b" then Some(Color.Black)
|
||||||
else None
|
else None
|
||||||
|
|
||||||
/** Parse castling rights string (e.g. "KQkq", "K", "-") into rights for White and Black. */
|
/** Parse castling rights string (e.g. "KQkq", "K", "-") into unified castling rights. */
|
||||||
private def parseCastling(s: String): Option[(CastlingRights, CastlingRights)] =
|
private def parseCastling(s: String): Option[CastlingRights] =
|
||||||
if s == "-" then
|
if s == "-" then
|
||||||
Some((CastlingRights.None, CastlingRights.None))
|
Some(CastlingRights.None)
|
||||||
else if s.length <= 4 && s.forall(c => "KQkq".contains(c)) then
|
else if s.length <= 4 && s.forall(c => "KQkq".contains(c)) then
|
||||||
val white = CastlingRights(kingSide = s.contains('K'), queenSide = s.contains('Q'))
|
Some(CastlingRights(
|
||||||
val black = CastlingRights(kingSide = s.contains('k'), queenSide = s.contains('q'))
|
whiteKingSide = s.contains('K'),
|
||||||
Some((white, black))
|
whiteQueenSide = s.contains('Q'),
|
||||||
|
blackKingSide = s.contains('k'),
|
||||||
|
blackQueenSide = s.contains('q')
|
||||||
|
))
|
||||||
else
|
else
|
||||||
None
|
None
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package de.nowchess.chess.notation
|
|||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.api.game.{GameContext, HistoryMove}
|
import de.nowchess.api.game.{GameContext, HistoryMove}
|
||||||
import de.nowchess.rules.StandardRules
|
import de.nowchess.rules.sets.StandardRules
|
||||||
|
|
||||||
/** A parsed PGN game containing headers and the resolved move list. */
|
/** A parsed PGN game containing headers and the resolved move list. */
|
||||||
case class PgnGame(
|
case class PgnGame(
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ 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.notation.FenParser
|
import de.nowchess.chess.notation.FenParser
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.rules.{RuleSet, StandardRules}
|
import de.nowchess.rules.RuleSet
|
||||||
|
import de.nowchess.rules.sets.StandardRules
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +1,101 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.chess.notation
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.game.{CastlingRights, GameState, GameStatus}
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.board.Color
|
import de.nowchess.api.move.Move
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
class FenExporterTest extends AnyFunSuite with Matchers:
|
class FenExporterTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
private def context(
|
||||||
|
piecePlacement: String,
|
||||||
|
turn: Color,
|
||||||
|
castlingRights: CastlingRights,
|
||||||
|
enPassantSquare: Option[Square],
|
||||||
|
halfMoveClock: Int,
|
||||||
|
moveCount: Int
|
||||||
|
): GameContext =
|
||||||
|
val board = FenParser.parseBoard(piecePlacement).getOrElse(
|
||||||
|
fail(s"Invalid test board FEN: $piecePlacement")
|
||||||
|
)
|
||||||
|
val dummyMove = Move(Square(File.A, Rank.R2), Square(File.A, Rank.R3))
|
||||||
|
GameContext(
|
||||||
|
board = board,
|
||||||
|
turn = turn,
|
||||||
|
castlingRights = castlingRights,
|
||||||
|
enPassantSquare = enPassantSquare,
|
||||||
|
halfMoveClock = halfMoveClock,
|
||||||
|
moves = List.fill(moveCount)(dummyMove)
|
||||||
|
)
|
||||||
|
|
||||||
test("export initial position to FEN"):
|
test("export initial position to FEN"):
|
||||||
val gameState = GameState.initial
|
val fen = FenExporter.gameContextToFen(GameContext.initial)
|
||||||
val fen = FenExporter.gameStateToFen(gameState)
|
|
||||||
fen shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
fen shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||||
|
|
||||||
test("export position after e4"):
|
test("export position after e4"):
|
||||||
val gameState = GameState(
|
val gameContext = context(
|
||||||
piecePlacement = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR",
|
piecePlacement = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR",
|
||||||
activeColor = Color.Black,
|
turn = Color.Black,
|
||||||
castlingWhite = CastlingRights.Both,
|
castlingRights = CastlingRights.All,
|
||||||
castlingBlack = CastlingRights.Both,
|
enPassantSquare = Some(Square(File.E, Rank.R3)),
|
||||||
enPassantTarget = Some(Square(File.E, Rank.R3)),
|
|
||||||
halfMoveClock = 0,
|
halfMoveClock = 0,
|
||||||
fullMoveNumber = 1,
|
moveCount = 0
|
||||||
status = GameStatus.InProgress
|
|
||||||
)
|
)
|
||||||
val fen = FenExporter.gameStateToFen(gameState)
|
val fen = FenExporter.gameContextToFen(gameContext)
|
||||||
fen shouldBe "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
|
fen shouldBe "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
|
||||||
|
|
||||||
test("export position with no castling"):
|
test("export position with no castling"):
|
||||||
val gameState = GameState(
|
val gameContext = context(
|
||||||
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
||||||
activeColor = Color.White,
|
turn = Color.White,
|
||||||
castlingWhite = CastlingRights.None,
|
castlingRights = CastlingRights.None,
|
||||||
castlingBlack = CastlingRights.None,
|
enPassantSquare = None,
|
||||||
enPassantTarget = None,
|
|
||||||
halfMoveClock = 0,
|
halfMoveClock = 0,
|
||||||
fullMoveNumber = 1,
|
moveCount = 0
|
||||||
status = GameStatus.InProgress
|
|
||||||
)
|
)
|
||||||
val fen = FenExporter.gameStateToFen(gameState)
|
val fen = FenExporter.gameContextToFen(gameContext)
|
||||||
fen shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1"
|
fen shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1"
|
||||||
|
|
||||||
test("export position with partial castling"):
|
test("export position with partial castling"):
|
||||||
val gameState = GameState(
|
val gameContext = context(
|
||||||
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
||||||
activeColor = Color.White,
|
turn = Color.White,
|
||||||
castlingWhite = CastlingRights(kingSide = true, queenSide = false),
|
castlingRights = CastlingRights(
|
||||||
castlingBlack = CastlingRights(kingSide = false, queenSide = true),
|
whiteKingSide = true,
|
||||||
enPassantTarget = None,
|
whiteQueenSide = false,
|
||||||
|
blackKingSide = false,
|
||||||
|
blackQueenSide = true
|
||||||
|
),
|
||||||
|
enPassantSquare = None,
|
||||||
halfMoveClock = 5,
|
halfMoveClock = 5,
|
||||||
fullMoveNumber = 3,
|
moveCount = 4
|
||||||
status = GameStatus.InProgress
|
|
||||||
)
|
)
|
||||||
val fen = FenExporter.gameStateToFen(gameState)
|
val fen = FenExporter.gameContextToFen(gameContext)
|
||||||
fen shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Kq - 5 3"
|
fen shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Kq - 5 3"
|
||||||
|
|
||||||
test("export position with en passant and move counts"):
|
test("export position with en passant and move counts"):
|
||||||
val gameState = GameState(
|
val gameContext = context(
|
||||||
piecePlacement = "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR",
|
piecePlacement = "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR",
|
||||||
activeColor = Color.White,
|
turn = Color.White,
|
||||||
castlingWhite = CastlingRights.Both,
|
castlingRights = CastlingRights.All,
|
||||||
castlingBlack = CastlingRights.Both,
|
enPassantSquare = Some(Square(File.C, Rank.R6)),
|
||||||
enPassantTarget = Some(Square(File.C, Rank.R6)),
|
|
||||||
halfMoveClock = 2,
|
halfMoveClock = 2,
|
||||||
fullMoveNumber = 3,
|
moveCount = 4
|
||||||
status = GameStatus.InProgress
|
|
||||||
)
|
)
|
||||||
val fen = FenExporter.gameStateToFen(gameState)
|
val fen = FenExporter.gameContextToFen(gameContext)
|
||||||
fen shouldBe "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR w KQkq c6 2 3"
|
fen shouldBe "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR w KQkq c6 2 3"
|
||||||
|
|
||||||
test("halfMoveClock round-trips through FEN export and import"):
|
test("halfMoveClock round-trips through FEN export and import"):
|
||||||
import de.nowchess.api.game.GameHistory
|
val gameContext = GameContext(
|
||||||
import de.nowchess.chess.notation.FenParser
|
board = Board.initial,
|
||||||
val history = GameHistory(halfMoveClock = 42)
|
turn = Color.White,
|
||||||
val gameState = GameState(
|
castlingRights = CastlingRights.All,
|
||||||
piecePlacement = FenExporter.boardToFen(de.nowchess.api.board.Board.initial),
|
enPassantSquare = None,
|
||||||
activeColor = Color.White,
|
halfMoveClock = 42,
|
||||||
castlingWhite = CastlingRights.Both,
|
moves = List.empty
|
||||||
castlingBlack = CastlingRights.Both,
|
|
||||||
enPassantTarget = None,
|
|
||||||
halfMoveClock = history.halfMoveClock,
|
|
||||||
fullMoveNumber = 1,
|
|
||||||
status = GameStatus.InProgress
|
|
||||||
)
|
)
|
||||||
val fen = FenExporter.gameStateToFen(gameState)
|
val fen = FenExporter.gameContextToFen(gameContext)
|
||||||
FenParser.parseFen(fen) match
|
FenParser.parseFen(fen) match
|
||||||
case Some(gs) => gs.halfMoveClock shouldBe 42
|
case Some(ctx) => ctx.halfMoveClock shouldBe 42
|
||||||
case None => fail("FEN parsing failed")
|
case None => fail("FEN parsing failed")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.chess.notation
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.game.*
|
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
@@ -65,52 +64,51 @@ class FenParserTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
test("parse full FEN - initial position"):
|
test("parse full FEN - initial position"):
|
||||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||||
val gameState = FenParser.parseFen(fen)
|
val context = FenParser.parseFen(fen)
|
||||||
|
|
||||||
gameState.isDefined shouldBe true
|
context.isDefined shouldBe true
|
||||||
gameState.get.activeColor shouldBe Color.White
|
context.get.turn shouldBe Color.White
|
||||||
gameState.get.castlingWhite.kingSide shouldBe true
|
context.get.castlingRights.whiteKingSide shouldBe true
|
||||||
gameState.get.castlingWhite.queenSide shouldBe true
|
context.get.castlingRights.whiteQueenSide shouldBe true
|
||||||
gameState.get.castlingBlack.kingSide shouldBe true
|
context.get.castlingRights.blackKingSide shouldBe true
|
||||||
gameState.get.castlingBlack.queenSide shouldBe true
|
context.get.castlingRights.blackQueenSide shouldBe true
|
||||||
gameState.get.enPassantTarget shouldBe None
|
context.get.enPassantSquare shouldBe None
|
||||||
gameState.get.halfMoveClock shouldBe 0
|
context.get.halfMoveClock shouldBe 0
|
||||||
gameState.get.fullMoveNumber shouldBe 1
|
|
||||||
|
|
||||||
test("parse full FEN - after e4"):
|
test("parse full FEN - after e4"):
|
||||||
val fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
|
val fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
|
||||||
val gameState = FenParser.parseFen(fen)
|
val context = FenParser.parseFen(fen)
|
||||||
|
|
||||||
gameState.get.activeColor shouldBe Color.Black
|
context.get.turn shouldBe Color.Black
|
||||||
gameState.get.enPassantTarget shouldBe Some(Square(File.E, Rank.R3))
|
context.get.enPassantSquare shouldBe Some(Square(File.E, Rank.R3))
|
||||||
|
|
||||||
test("parse full FEN - invalid parts count"):
|
test("parse full FEN - invalid parts count"):
|
||||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq"
|
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq"
|
||||||
val gameState = FenParser.parseFen(fen)
|
val context = FenParser.parseFen(fen)
|
||||||
|
|
||||||
gameState.isDefined shouldBe false
|
context.isDefined shouldBe false
|
||||||
|
|
||||||
test("parse full FEN - invalid color"):
|
test("parse full FEN - invalid color"):
|
||||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1"
|
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1"
|
||||||
val gameState = FenParser.parseFen(fen)
|
val context = FenParser.parseFen(fen)
|
||||||
|
|
||||||
gameState.isDefined shouldBe false
|
context.isDefined shouldBe false
|
||||||
|
|
||||||
test("parse full FEN - invalid castling"):
|
test("parse full FEN - invalid castling"):
|
||||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w XYZ - 0 1"
|
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w XYZ - 0 1"
|
||||||
val gameState = FenParser.parseFen(fen)
|
val context = FenParser.parseFen(fen)
|
||||||
|
|
||||||
gameState.isDefined shouldBe false
|
context.isDefined shouldBe false
|
||||||
|
|
||||||
test("parseFen: castling '-' produces CastlingRights.None for both sides"):
|
test("parseFen: castling '-' produces no castling rights"):
|
||||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1"
|
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1"
|
||||||
val gameState = FenParser.parseFen(fen)
|
val context = FenParser.parseFen(fen)
|
||||||
|
|
||||||
gameState.isDefined shouldBe true
|
context.isDefined shouldBe true
|
||||||
gameState.get.castlingWhite.kingSide shouldBe false
|
context.get.castlingRights.whiteKingSide shouldBe false
|
||||||
gameState.get.castlingWhite.queenSide shouldBe false
|
context.get.castlingRights.whiteQueenSide shouldBe false
|
||||||
gameState.get.castlingBlack.kingSide shouldBe false
|
context.get.castlingRights.blackKingSide shouldBe false
|
||||||
gameState.get.castlingBlack.queenSide shouldBe false
|
context.get.castlingRights.blackQueenSide shouldBe false
|
||||||
|
|
||||||
test("parseBoard: returns None when a rank has too many files (overflow beyond 8)"):
|
test("parseBoard: returns None when a rank has too many files (overflow beyond 8)"):
|
||||||
// "9" alone would advance fileIdx to 9, exceeding 8 → None
|
// "9" alone would advance fileIdx to 9, exceeding 8 → None
|
||||||
|
|||||||
+4
-2
@@ -1,8 +1,10 @@
|
|||||||
package de.nowchess.rules
|
package de.nowchess.rules.sets
|
||||||
|
|
||||||
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Rank, Square, PieceType, Piece}
|
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
|
import de.nowchess.rules.RuleSet
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
|
|
||||||
/** Standard chess rules implementation.
|
/** Standard chess rules implementation.
|
||||||
@@ -10,10 +10,8 @@ import scalafx.scene.shape.Rectangle
|
|||||||
import scalafx.scene.text.{Font, Text}
|
import scalafx.scene.text.{Font, Text}
|
||||||
import scalafx.stage.Stage
|
import scalafx.stage.Stage
|
||||||
import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square, File, Rank}
|
import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square, File, Rank}
|
||||||
import de.nowchess.api.game.{CastlingRights, GameState, GameStatus, GameHistory}
|
|
||||||
import de.nowchess.api.move.PromotionPiece
|
import de.nowchess.api.move.PromotionPiece
|
||||||
import de.nowchess.chess.engine.GameEngine
|
import de.nowchess.chess.engine.GameEngine
|
||||||
import de.nowchess.chess.notation.{FenExporter, FenParser, PgnExporter, PgnParser}
|
|
||||||
|
|
||||||
/** ScalaFX chess board view that displays the game state.
|
/** ScalaFX chess board view that displays the game state.
|
||||||
* Uses chess sprites and color palette.
|
* Uses chess sprites and color palette.
|
||||||
|
|||||||
Reference in New Issue
Block a user