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 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,
|
||||
|
||||
@@ -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.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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
+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.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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user