diff --git a/modules/api/src/main/scala/de/nowchess/api/game/GameState.scala b/modules/api/src/main/scala/de/nowchess/api/game/GameState.scala index 7f57b19..b6b49e8 100644 --- a/modules/api/src/main/scala/de/nowchess/api/game/GameState.scala +++ b/modules/api/src/main/scala/de/nowchess/api/game/GameState.scala @@ -8,6 +8,7 @@ import de.nowchess.api.board.{Color, Square} * @param kingSide king-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) object CastlingRights: @@ -15,12 +16,14 @@ object CastlingRights: val Both: CastlingRights = CastlingRights(kingSide = true, queenSide = true) /** Outcome of a finished game. */ +@deprecated("Use GameContext and derive game lifecycle from rules/engine state.", "NCS-22") enum GameResult: case WhiteWins case BlackWins case Draw /** Lifecycle state of a game. */ +@deprecated("Use GameContext and engine events for lifecycle handling.", "NCS-22") enum GameStatus: case NotStarted case InProgress @@ -42,6 +45,7 @@ enum GameStatus: * @param fullMoveNumber increments after Black's move, starts at 1 * @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( piecePlacement: String, activeColor: Color, diff --git a/modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala b/modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala new file mode 100644 index 0000000..685ce27 --- /dev/null +++ b/modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala @@ -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) + diff --git a/modules/api/src/test/scala/de/nowchess/api/game/GameStateTest.scala b/modules/api/src/test/scala/de/nowchess/api/game/GameStateTest.scala deleted file mode 100644 index 374638c..0000000 --- a/modules/api/src/test/scala/de/nowchess/api/game/GameStateTest.scala +++ /dev/null @@ -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 - } diff --git a/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala b/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala index b045938..7c8b6fd 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala @@ -7,7 +7,8 @@ import de.nowchess.chess.controller.Parser import de.nowchess.chess.observer.* import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult} 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. * All rule queries delegate to the injected RuleSet. diff --git a/modules/core/src/main/scala/de/nowchess/chess/notation/FenExporter.scala b/modules/core/src/main/scala/de/nowchess/chess/notation/FenExporter.scala index e300dd1..50c8108 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/notation/FenExporter.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/notation/FenExporter.scala @@ -1,8 +1,7 @@ package de.nowchess.chess.notation import de.nowchess.api.board.* -import de.nowchess.api.game.{CastlingRights, GameState} -import de.nowchess.api.board.Color +import de.nowchess.api.game.GameContext object FenExporter: @@ -31,20 +30,21 @@ object FenExporter: if emptyCount > 0 then rankChars += emptyCount.toString.charAt(0) rankChars.mkString - /** Convert a GameState to a complete FEN string. */ - def gameStateToFen(state: GameState): String = - val piecePlacement = state.piecePlacement - val activeColor = if state.activeColor == Color.White then "w" else "b" - val castling = castlingString(state.castlingWhite, state.castlingBlack) - val enPassant = state.enPassantTarget.map(_.toString).getOrElse("-") - s"$piecePlacement $activeColor $castling $enPassant ${state.halfMoveClock} ${state.fullMoveNumber}" + /** 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" /** Convert castling rights to FEN notation. */ - private def castlingString(white: CastlingRights, black: CastlingRights): String = - val wk = if white.kingSide then "K" else "" - val wq = if white.queenSide then "Q" else "" - val bk = if black.kingSide then "k" else "" - val bq = if black.queenSide then "q" else "" + 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 diff --git a/modules/core/src/main/scala/de/nowchess/chess/notation/FenParser.scala b/modules/core/src/main/scala/de/nowchess/chess/notation/FenParser.scala index 94b7244..5b20243 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/notation/FenParser.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/notation/FenParser.scala @@ -1,32 +1,30 @@ package de.nowchess.chess.notation import de.nowchess.api.board.* -import de.nowchess.api.game.{CastlingRights, GameState, GameStatus} +import de.nowchess.api.game.GameContext 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. */ - def parseFen(fen: String): Option[GameState] = + def parseFen(fen: String): Option[GameContext] = val parts = fen.trim.split("\\s+") Option.when(parts.length == 6)(parts).flatMap: parts => for - _ <- parseBoard(parts(0)) + board <- parseBoard(parts(0)) activeColor <- parseColor(parts(1)) castlingRights <- parseCastling(parts(2)) enPassant <- parseEnPassant(parts(3)) halfMoveClock <- parts(4).toIntOption fullMoveNumber <- parts(5).toIntOption if halfMoveClock >= 0 && fullMoveNumber >= 1 - yield GameState( - piecePlacement = parts(0), - activeColor = activeColor, - castlingWhite = castlingRights._1, - castlingBlack = castlingRights._2, - enPassantTarget = enPassant, + yield GameContext( + board = board, + turn = activeColor, + castlingRights = castlingRights, + enPassantSquare = enPassant, halfMoveClock = halfMoveClock, - fullMoveNumber = fullMoveNumber, - status = GameStatus.InProgress + moves = List.empty ) /** Parse active color ("w" or "b"). */ @@ -35,14 +33,17 @@ object FenParser: else if s == "b" then Some(Color.Black) else None - /** Parse castling rights string (e.g. "KQkq", "K", "-") into rights for White and Black. */ - private def parseCastling(s: String): Option[(CastlingRights, CastlingRights)] = + /** Parse castling rights string (e.g. "KQkq", "K", "-") into unified castling rights. */ + private def parseCastling(s: String): Option[CastlingRights] = if s == "-" then - Some((CastlingRights.None, CastlingRights.None)) + Some(CastlingRights.None) else if s.length <= 4 && s.forall(c => "KQkq".contains(c)) then - val white = CastlingRights(kingSide = s.contains('K'), queenSide = s.contains('Q')) - val black = CastlingRights(kingSide = s.contains('k'), queenSide = s.contains('q')) - Some((white, black)) + Some(CastlingRights( + whiteKingSide = s.contains('K'), + whiteQueenSide = s.contains('Q'), + blackKingSide = s.contains('k'), + blackQueenSide = s.contains('q') + )) else None diff --git a/modules/core/src/main/scala/de/nowchess/chess/notation/PgnParser.scala b/modules/core/src/main/scala/de/nowchess/chess/notation/PgnParser.scala index a22c29e..5152a98 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/notation/PgnParser.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/notation/PgnParser.scala @@ -3,7 +3,7 @@ package de.nowchess.chess.notation import de.nowchess.api.board.* import de.nowchess.api.move.{Move, MoveType, PromotionPiece} 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. */ case class PgnGame( diff --git a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala index 14d6771..62e274d 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala @@ -5,7 +5,8 @@ import de.nowchess.api.game.GameContext import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.chess.notation.FenParser 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.matchers.should.Matchers diff --git a/modules/core/src/test/scala/de/nowchess/chess/notation/FenExporterTest.scala b/modules/core/src/test/scala/de/nowchess/chess/notation/FenExporterTest.scala index 7b4d11a..ea572fa 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/notation/FenExporterTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/notation/FenExporterTest.scala @@ -1,89 +1,101 @@ package de.nowchess.chess.notation import de.nowchess.api.board.* -import de.nowchess.api.game.{CastlingRights, GameState, GameStatus} -import de.nowchess.api.board.Color +import de.nowchess.api.game.GameContext +import de.nowchess.api.move.Move import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.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"): - val gameState = GameState.initial - val fen = FenExporter.gameStateToFen(gameState) + val fen = FenExporter.gameContextToFen(GameContext.initial) fen shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" test("export position after e4"): - val gameState = GameState( + val gameContext = context( piecePlacement = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR", - activeColor = Color.Black, - castlingWhite = CastlingRights.Both, - castlingBlack = CastlingRights.Both, - enPassantTarget = Some(Square(File.E, Rank.R3)), + turn = Color.Black, + castlingRights = CastlingRights.All, + enPassantSquare = Some(Square(File.E, Rank.R3)), halfMoveClock = 0, - fullMoveNumber = 1, - status = GameStatus.InProgress + moveCount = 0 ) - 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" test("export position with no castling"): - val gameState = GameState( + val gameContext = context( piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR", - activeColor = Color.White, - castlingWhite = CastlingRights.None, - castlingBlack = CastlingRights.None, - enPassantTarget = None, + turn = Color.White, + castlingRights = CastlingRights.None, + enPassantSquare = None, halfMoveClock = 0, - fullMoveNumber = 1, - status = GameStatus.InProgress + moveCount = 0 ) - val fen = FenExporter.gameStateToFen(gameState) + val fen = FenExporter.gameContextToFen(gameContext) fen shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1" test("export position with partial castling"): - val gameState = GameState( + val gameContext = context( piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR", - activeColor = Color.White, - castlingWhite = CastlingRights(kingSide = true, queenSide = false), - castlingBlack = CastlingRights(kingSide = false, queenSide = true), - enPassantTarget = None, + turn = Color.White, + castlingRights = CastlingRights( + whiteKingSide = true, + whiteQueenSide = false, + blackKingSide = false, + blackQueenSide = true + ), + enPassantSquare = None, halfMoveClock = 5, - fullMoveNumber = 3, - status = GameStatus.InProgress + moveCount = 4 ) - val fen = FenExporter.gameStateToFen(gameState) + val fen = FenExporter.gameContextToFen(gameContext) fen shouldBe "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Kq - 5 3" test("export position with en passant and move counts"): - val gameState = GameState( + val gameContext = context( piecePlacement = "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR", - activeColor = Color.White, - castlingWhite = CastlingRights.Both, - castlingBlack = CastlingRights.Both, - enPassantTarget = Some(Square(File.C, Rank.R6)), + turn = Color.White, + castlingRights = CastlingRights.All, + enPassantSquare = Some(Square(File.C, Rank.R6)), halfMoveClock = 2, - fullMoveNumber = 3, - status = GameStatus.InProgress + moveCount = 4 ) - 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" test("halfMoveClock round-trips through FEN export and import"): - import de.nowchess.api.game.GameHistory - import de.nowchess.chess.notation.FenParser - val history = GameHistory(halfMoveClock = 42) - val gameState = GameState( - piecePlacement = FenExporter.boardToFen(de.nowchess.api.board.Board.initial), - activeColor = Color.White, - castlingWhite = CastlingRights.Both, - castlingBlack = CastlingRights.Both, - enPassantTarget = None, - halfMoveClock = history.halfMoveClock, - fullMoveNumber = 1, - status = GameStatus.InProgress + val gameContext = GameContext( + board = Board.initial, + turn = Color.White, + castlingRights = CastlingRights.All, + enPassantSquare = None, + halfMoveClock = 42, + moves = List.empty ) - val fen = FenExporter.gameStateToFen(gameState) + val fen = FenExporter.gameContextToFen(gameContext) FenParser.parseFen(fen) match - case Some(gs) => gs.halfMoveClock shouldBe 42 + case Some(ctx) => ctx.halfMoveClock shouldBe 42 case None => fail("FEN parsing failed") diff --git a/modules/core/src/test/scala/de/nowchess/chess/notation/FenParserTest.scala b/modules/core/src/test/scala/de/nowchess/chess/notation/FenParserTest.scala index 47716df..b4752b6 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/notation/FenParserTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/notation/FenParserTest.scala @@ -1,7 +1,6 @@ package de.nowchess.chess.notation import de.nowchess.api.board.* -import de.nowchess.api.game.* import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -65,52 +64,51 @@ class FenParserTest extends AnyFunSuite with Matchers: test("parse full FEN - initial position"): 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 - gameState.get.activeColor shouldBe Color.White - gameState.get.castlingWhite.kingSide shouldBe true - gameState.get.castlingWhite.queenSide shouldBe true - gameState.get.castlingBlack.kingSide shouldBe true - gameState.get.castlingBlack.queenSide shouldBe true - gameState.get.enPassantTarget shouldBe None - gameState.get.halfMoveClock shouldBe 0 - gameState.get.fullMoveNumber shouldBe 1 + context.isDefined shouldBe true + context.get.turn shouldBe Color.White + context.get.castlingRights.whiteKingSide shouldBe true + context.get.castlingRights.whiteQueenSide shouldBe true + context.get.castlingRights.blackKingSide shouldBe true + context.get.castlingRights.blackQueenSide shouldBe true + context.get.enPassantSquare shouldBe None + context.get.halfMoveClock shouldBe 0 test("parse full FEN - after e4"): 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 - gameState.get.enPassantTarget shouldBe Some(Square(File.E, Rank.R3)) + context.get.turn shouldBe Color.Black + context.get.enPassantSquare shouldBe Some(Square(File.E, Rank.R3)) test("parse full FEN - invalid parts count"): 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"): 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"): 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 gameState = FenParser.parseFen(fen) + val context = FenParser.parseFen(fen) - gameState.isDefined shouldBe true - gameState.get.castlingWhite.kingSide shouldBe false - gameState.get.castlingWhite.queenSide shouldBe false - gameState.get.castlingBlack.kingSide shouldBe false - gameState.get.castlingBlack.queenSide shouldBe false + context.isDefined shouldBe true + context.get.castlingRights.whiteKingSide shouldBe false + context.get.castlingRights.whiteQueenSide shouldBe false + context.get.castlingRights.blackKingSide shouldBe false + context.get.castlingRights.blackQueenSide shouldBe false test("parseBoard: returns None when a rank has too many files (overflow beyond 8)"): // "9" alone would advance fileIdx to 9, exceeding 8 → None diff --git a/modules/rule/src/main/scala/de/nowchess/rules/StandardRules.scala b/modules/rule/src/main/scala/de/nowchess/rules/sets/StandardRules.scala similarity index 99% rename from modules/rule/src/main/scala/de/nowchess/rules/StandardRules.scala rename to modules/rule/src/main/scala/de/nowchess/rules/sets/StandardRules.scala index c78b9b4..cb51c1c 100644 --- a/modules/rule/src/main/scala/de/nowchess/rules/StandardRules.scala +++ b/modules/rule/src/main/scala/de/nowchess/rules/sets/StandardRules.scala @@ -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.board.{Board, CastlingRights, Color, File, Rank, Square, PieceType, Piece} import de.nowchess.api.move.{Move, MoveType, PromotionPiece} +import de.nowchess.rules.RuleSet + import scala.annotation.tailrec /** Standard chess rules implementation. diff --git a/modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala b/modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala index a8f40e0..82ef8cf 100644 --- a/modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala +++ b/modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala @@ -10,10 +10,8 @@ import scalafx.scene.shape.Rectangle import scalafx.scene.text.{Font, Text} import scalafx.stage.Stage 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.chess.engine.GameEngine -import de.nowchess.chess.notation.{FenExporter, FenParser, PgnExporter, PgnParser} /** ScalaFX chess board view that displays the game state. * Uses chess sprites and color palette.