feat: add castling rights revocation to processMove
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
Introduces applyRightsRevocation helper that revokes rights on castle moves, king/rook departures from home squares, and enemy captures on rook home squares. Six new tests verify all revocation paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
package de.nowchess.chess.controller
|
||||
|
||||
import scala.io.StdIn
|
||||
import de.nowchess.api.board.{Board, Color, Piece}
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, Rank, Square}
|
||||
import de.nowchess.api.game.CastlingRights
|
||||
import de.nowchess.chess.logic.{GameContext, MoveValidator, GameRules, PositionStatus, CastleSide, withCastle}
|
||||
import de.nowchess.chess.view.Renderer
|
||||
|
||||
@@ -54,13 +55,43 @@ object GameController:
|
||||
val (newBoard, captured) = castleOpt match
|
||||
case Some(side) => (ctx.board.withCastle(turn, side), None)
|
||||
case None => ctx.board.withMove(from, to)
|
||||
val newCtx = ctx.copy(board = newBoard)
|
||||
val newCtx = applyRightsRevocation(
|
||||
ctx.copy(board = newBoard), turn, from, to, castleOpt
|
||||
)
|
||||
GameRules.gameStatus(newCtx, turn.opposite) match
|
||||
case PositionStatus.Normal => MoveResult.Moved(newCtx, captured, turn.opposite)
|
||||
case PositionStatus.InCheck => MoveResult.MovedInCheck(newCtx, captured, turn.opposite)
|
||||
case PositionStatus.Mated => MoveResult.Checkmate(turn)
|
||||
case PositionStatus.Drawn => MoveResult.Stalemate
|
||||
|
||||
private def applyRightsRevocation(
|
||||
ctx: GameContext,
|
||||
turn: Color,
|
||||
from: Square,
|
||||
to: Square,
|
||||
castle: Option[CastleSide]
|
||||
): GameContext =
|
||||
// Step 1: Revoke all rights for a castling move (idempotent with step 2)
|
||||
val ctx0 = castle.fold(ctx)(_ => ctx.withUpdatedRights(turn, CastlingRights.None))
|
||||
|
||||
// Step 2: Source-square revocation
|
||||
val ctx1 = from match
|
||||
case Square(File.E, Rank.R1) => ctx0.withUpdatedRights(Color.White, CastlingRights.None)
|
||||
case Square(File.E, Rank.R8) => ctx0.withUpdatedRights(Color.Black, CastlingRights.None)
|
||||
case Square(File.A, Rank.R1) => ctx0.withUpdatedRights(Color.White, ctx0.whiteCastling.copy(queenSide = false))
|
||||
case Square(File.H, Rank.R1) => ctx0.withUpdatedRights(Color.White, ctx0.whiteCastling.copy(kingSide = false))
|
||||
case Square(File.A, Rank.R8) => ctx0.withUpdatedRights(Color.Black, ctx0.blackCastling.copy(queenSide = false))
|
||||
case Square(File.H, Rank.R8) => ctx0.withUpdatedRights(Color.Black, ctx0.blackCastling.copy(kingSide = false))
|
||||
case _ => ctx0
|
||||
|
||||
// Step 3: Destination-square revocation (enemy captures a rook on its home square)
|
||||
to match
|
||||
case Square(File.A, Rank.R1) => ctx1.withUpdatedRights(Color.White, ctx1.whiteCastling.copy(queenSide = false))
|
||||
case Square(File.H, Rank.R1) => ctx1.withUpdatedRights(Color.White, ctx1.whiteCastling.copy(kingSide = false))
|
||||
case Square(File.A, Rank.R8) => ctx1.withUpdatedRights(Color.Black, ctx1.blackCastling.copy(queenSide = false))
|
||||
case Square(File.H, Rank.R8) => ctx1.withUpdatedRights(Color.Black, ctx1.blackCastling.copy(kingSide = false))
|
||||
case _ => ctx1
|
||||
|
||||
/** Thin I/O shell: renders the board, reads a line, delegates to processMove,
|
||||
* prints the outcome, and recurses until the game ends.
|
||||
*/
|
||||
|
||||
@@ -241,3 +241,167 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
newCtx.board.pieceAt(sq(File.C, Rank.R1)) shouldBe Some(Piece.WhiteKing)
|
||||
newCtx.board.pieceAt(sq(File.D, Rank.R1)) shouldBe Some(Piece.WhiteRook)
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
|
||||
// ──── rights revocation ──────────────────────────────────────────────
|
||||
|
||||
test("processMove: e1g1 revokes both white castling rights"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.Both,
|
||||
blackCastling = CastlingRights.None
|
||||
)
|
||||
GameController.processMove(ctx, Color.White, "e1g1") match
|
||||
case MoveResult.Moved(newCtx, _, _) =>
|
||||
newCtx.whiteCastling shouldBe CastlingRights.None
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
|
||||
test("processMove: moving rook from h1 revokes white kingside right"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.Both,
|
||||
blackCastling = CastlingRights.None
|
||||
)
|
||||
GameController.processMove(ctx, Color.White, "h1h4") match
|
||||
case MoveResult.Moved(newCtx, _, _) =>
|
||||
newCtx.whiteCastling.kingSide shouldBe false
|
||||
newCtx.whiteCastling.queenSide shouldBe true
|
||||
case MoveResult.MovedInCheck(newCtx, _, _) =>
|
||||
newCtx.whiteCastling.kingSide shouldBe false
|
||||
newCtx.whiteCastling.queenSide shouldBe true
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
|
||||
test("processMove: moving king from e1 revokes both white rights"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.Both,
|
||||
blackCastling = CastlingRights.None
|
||||
)
|
||||
GameController.processMove(ctx, Color.White, "e1e2") match
|
||||
case MoveResult.Moved(newCtx, _, _) =>
|
||||
newCtx.whiteCastling shouldBe CastlingRights.None
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
|
||||
test("processMove: enemy capture on h1 revokes white kingside right"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.H, Rank.R2) -> Piece.BlackRook,
|
||||
sq(File.A, Rank.R8) -> Piece.BlackKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.Both,
|
||||
blackCastling = CastlingRights.None
|
||||
)
|
||||
GameController.processMove(ctx, Color.Black, "h2h1") match
|
||||
case MoveResult.Moved(newCtx, _, _) =>
|
||||
newCtx.whiteCastling.kingSide shouldBe false
|
||||
case MoveResult.MovedInCheck(newCtx, _, _) =>
|
||||
newCtx.whiteCastling.kingSide shouldBe false
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
|
||||
test("processMove: castle attempt when rights revoked returns IllegalMove"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.None,
|
||||
blackCastling = CastlingRights.None
|
||||
)
|
||||
GameController.processMove(ctx, Color.White, "e1g1") shouldBe MoveResult.IllegalMove
|
||||
|
||||
test("processMove: castle attempt when rook not on home square returns IllegalMove"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.G, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.Both,
|
||||
blackCastling = CastlingRights.None
|
||||
)
|
||||
GameController.processMove(ctx, Color.White, "e1g1") shouldBe MoveResult.IllegalMove
|
||||
|
||||
test("processMove: moving king from e8 revokes both black rights"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R8) -> Piece.BlackKing,
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.None,
|
||||
blackCastling = CastlingRights.Both
|
||||
)
|
||||
GameController.processMove(ctx, Color.Black, "e8e7") match
|
||||
case MoveResult.Moved(newCtx, _, _) =>
|
||||
newCtx.blackCastling shouldBe CastlingRights.None
|
||||
case MoveResult.MovedInCheck(newCtx, _, _) =>
|
||||
newCtx.blackCastling shouldBe CastlingRights.None
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
|
||||
test("processMove: moving rook from a8 revokes black queenside right"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R8) -> Piece.BlackKing,
|
||||
sq(File.A, Rank.R8) -> Piece.BlackRook,
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.None,
|
||||
blackCastling = CastlingRights.Both
|
||||
)
|
||||
GameController.processMove(ctx, Color.Black, "a8a7") match
|
||||
case MoveResult.Moved(newCtx, _, _) =>
|
||||
newCtx.blackCastling.queenSide shouldBe false
|
||||
newCtx.blackCastling.kingSide shouldBe true
|
||||
case MoveResult.MovedInCheck(newCtx, _, _) =>
|
||||
newCtx.blackCastling.queenSide shouldBe false
|
||||
newCtx.blackCastling.kingSide shouldBe true
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
|
||||
test("processMove: moving rook from h8 revokes black kingside right"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R8) -> Piece.BlackKing,
|
||||
sq(File.H, Rank.R8) -> Piece.BlackRook,
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.None,
|
||||
blackCastling = CastlingRights.Both
|
||||
)
|
||||
GameController.processMove(ctx, Color.Black, "h8h7") match
|
||||
case MoveResult.Moved(newCtx, _, _) =>
|
||||
newCtx.blackCastling.kingSide shouldBe false
|
||||
newCtx.blackCastling.queenSide shouldBe true
|
||||
case MoveResult.MovedInCheck(newCtx, _, _) =>
|
||||
newCtx.blackCastling.kingSide shouldBe false
|
||||
newCtx.blackCastling.queenSide shouldBe true
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
|
||||
test("processMove: enemy capture on a1 revokes white queenside right"):
|
||||
val ctx = GameContext(
|
||||
board = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.A, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.A, Rank.R2) -> Piece.BlackRook,
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
)),
|
||||
whiteCastling = CastlingRights.Both,
|
||||
blackCastling = CastlingRights.None
|
||||
)
|
||||
GameController.processMove(ctx, Color.Black, "a2a1") match
|
||||
case MoveResult.Moved(newCtx, _, _) =>
|
||||
newCtx.whiteCastling.queenSide shouldBe false
|
||||
case MoveResult.MovedInCheck(newCtx, _, _) =>
|
||||
newCtx.whiteCastling.queenSide shouldBe false
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
|
||||
Reference in New Issue
Block a user