From 61203a9ec4d497f10f54286dcc7350b40010b799 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Sun, 29 Mar 2026 15:40:39 +0200 Subject: [PATCH] feat: NCS-9 apply en passant board mutation in GameController Implement explicit removal of the captured pawn in en passant captures. The pawn is located at a different square than the destination (same file but one rank closer to the player). Use EnPassantCalculator to compute the capture square and remove it from the board after withMove. Co-Authored-By: Claude Sonnet 4.6 --- .../chess/controller/GameController.scala | 8 ++++- .../chess/controller/GameControllerTest.scala | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala b/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala index acb7d17..4717430 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala @@ -51,9 +51,15 @@ object GameController: val castleOpt = if MoveValidator.isCastle(board, from, to) then Some(MoveValidator.castleSide(from, to)) else None + val isEP = EnPassantCalculator.isEnPassant(board, history, from, to) val (newBoard, captured) = castleOpt match case Some(side) => (board.withCastle(turn, side), None) - case None => board.withMove(from, to) + case None => + val (b, cap) = board.withMove(from, to) + if isEP then + val capturedSq = EnPassantCalculator.capturedPawnSquare(to, turn) + (b.removed(capturedSq), board.pieceAt(capturedSq)) + else (b, cap) val newHistory = history.addMove(from, to, castleOpt) GameRules.gameStatus(newBoard, newHistory, turn.opposite) match case PositionStatus.Normal => MoveResult.Moved(newBoard, newHistory, captured, turn.opposite) diff --git a/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala b/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala index 5e1a71e..8124005 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala @@ -365,3 +365,39 @@ class GameControllerTest extends AnyFunSuite with Matchers: case MoveResult.MovedInCheck(_, newHistory, _, _) => castlingRights(newHistory, Color.White).queenSide shouldBe false case other => fail(s"Expected Moved or MovedInCheck, got $other") + + // ──── en passant ──────────────────────────────────────────────────────── + + test("en passant capture removes the captured pawn from the board"): + // Setup: white pawn e5, black pawn just double-pushed to d5 (ep target = d6) + val b = Board(Map( + Square(File.E, Rank.R5) -> Piece.WhitePawn, + Square(File.D, Rank.R5) -> Piece.BlackPawn, + Square(File.E, Rank.R1) -> Piece.WhiteKing, + Square(File.E, Rank.R8) -> Piece.BlackKing + )) + val h = GameHistory.empty.addMove(Square(File.D, Rank.R7), Square(File.D, Rank.R5)) + val result = GameController.processMove(b, h, Color.White, "e5d6") + result match + case MoveResult.Moved(newBoard, _, captured, _) => + newBoard.pieceAt(Square(File.D, Rank.R5)) shouldBe None // captured pawn removed + newBoard.pieceAt(Square(File.D, Rank.R6)) shouldBe Some(Piece.WhitePawn) // capturing pawn placed + captured shouldBe Some(Piece.BlackPawn) + case other => fail(s"Expected Moved but got $other") + + test("en passant capture by black removes the captured white pawn"): + // Setup: black pawn d4, white pawn just double-pushed to e4 (ep target = e3) + val b = Board(Map( + Square(File.D, Rank.R4) -> Piece.BlackPawn, + Square(File.E, Rank.R4) -> Piece.WhitePawn, + Square(File.E, Rank.R8) -> Piece.BlackKing, + Square(File.E, Rank.R1) -> Piece.WhiteKing + )) + val h = GameHistory.empty.addMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) + val result = GameController.processMove(b, h, Color.Black, "d4e3") + result match + case MoveResult.Moved(newBoard, _, captured, _) => + newBoard.pieceAt(Square(File.E, Rank.R4)) shouldBe None // captured pawn removed + newBoard.pieceAt(Square(File.E, Rank.R3)) shouldBe Some(Piece.BlackPawn) // capturing pawn placed + captured shouldBe Some(Piece.WhitePawn) + case other => fail(s"Expected Moved but got $other")