refactor(core): migrate GameState to GameContext and update FEN handling
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
2026-04-04 17:55:08 +02:00
parent 6283db85c0
commit c08e5f8c62
12 changed files with 196 additions and 193 deletions
@@ -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
@@ -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.