feat: add completePromotion to GameController to finalize promotion moves
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
package de.nowchess.chess.controller
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, Rank, Square}
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.move.PromotionPiece
|
||||
import de.nowchess.chess.logic.*
|
||||
|
||||
@@ -76,3 +76,28 @@ object GameController:
|
||||
case PositionStatus.InCheck => MoveResult.MovedInCheck(newBoard, newHistory, captured, turn.opposite)
|
||||
case PositionStatus.Mated => MoveResult.Checkmate(turn)
|
||||
case PositionStatus.Drawn => MoveResult.Stalemate
|
||||
|
||||
/** Apply a previously detected promotion move with the chosen piece.
|
||||
* Called after processMove returned PromotionRequired.
|
||||
*/
|
||||
def completePromotion(
|
||||
board: Board,
|
||||
history: GameHistory,
|
||||
from: Square,
|
||||
to: Square,
|
||||
piece: PromotionPiece,
|
||||
turn: Color
|
||||
): MoveResult =
|
||||
val (boardAfterMove, captured) = board.withMove(from, to)
|
||||
val promotedPieceType = piece match
|
||||
case PromotionPiece.Queen => PieceType.Queen
|
||||
case PromotionPiece.Rook => PieceType.Rook
|
||||
case PromotionPiece.Bishop => PieceType.Bishop
|
||||
case PromotionPiece.Knight => PieceType.Knight
|
||||
val newBoard = boardAfterMove.updated(to, Piece(turn, promotedPieceType))
|
||||
val newHistory = history.addMove(from, to, None, Some(piece))
|
||||
GameRules.gameStatus(newBoard, newHistory, turn.opposite) match
|
||||
case PositionStatus.Normal => MoveResult.Moved(newBoard, newHistory, captured, turn.opposite)
|
||||
case PositionStatus.InCheck => MoveResult.MovedInCheck(newBoard, newHistory, captured, turn.opposite)
|
||||
case PositionStatus.Mated => MoveResult.Checkmate(turn)
|
||||
case PositionStatus.Drawn => MoveResult.Stalemate
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.nowchess.chess.controller
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.game.CastlingRights
|
||||
import de.nowchess.api.move.PromotionPiece
|
||||
import de.nowchess.chess.logic.{CastleSide, GameHistory}
|
||||
import de.nowchess.chess.notation.FenParser
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
@@ -327,3 +328,112 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
case MoveResult.PromotionRequired(_, _, _, _, captured, _) =>
|
||||
captured should be (Some(Piece(Color.Black, PieceType.Queen)))
|
||||
case _ => fail("Expected PromotionRequired")
|
||||
|
||||
// ──── completePromotion ──────────────────────────────────────────────────
|
||||
|
||||
test("completePromotion applies move and places queen"):
|
||||
// Black king on h1: not attacked by queen on e8 (different file, rank, and diagonals)
|
||||
val board = FenParser.parseBoard("8/4P3/8/8/8/8/8/7k").get
|
||||
val result = GameController.completePromotion(
|
||||
board, GameHistory.empty,
|
||||
sq(File.E, Rank.R7), sq(File.E, Rank.R8),
|
||||
PromotionPiece.Queen, Color.White
|
||||
)
|
||||
result should matchPattern { case _: MoveResult.Moved => }
|
||||
result match
|
||||
case MoveResult.Moved(newBoard, newHistory, _, _) =>
|
||||
newBoard.pieceAt(sq(File.E, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Queen)))
|
||||
newBoard.pieceAt(sq(File.E, Rank.R7)) should be (None)
|
||||
newHistory.moves should have length 1
|
||||
newHistory.moves.head.promotionPiece should be (Some(PromotionPiece.Queen))
|
||||
case _ => fail("Expected Moved")
|
||||
|
||||
test("completePromotion with rook underpromotion"):
|
||||
// Black king on h1: not attacked by rook on e8 (different file and rank)
|
||||
val board = FenParser.parseBoard("8/4P3/8/8/8/8/8/7k").get
|
||||
val result = GameController.completePromotion(
|
||||
board, GameHistory.empty,
|
||||
sq(File.E, Rank.R7), sq(File.E, Rank.R8),
|
||||
PromotionPiece.Rook, Color.White
|
||||
)
|
||||
result match
|
||||
case MoveResult.Moved(newBoard, newHistory, _, _) =>
|
||||
newBoard.pieceAt(sq(File.E, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Rook)))
|
||||
newHistory.moves.head.promotionPiece should be (Some(PromotionPiece.Rook))
|
||||
case _ => fail("Expected Moved with Rook")
|
||||
|
||||
test("completePromotion with bishop underpromotion"):
|
||||
val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
val result = GameController.completePromotion(
|
||||
board, GameHistory.empty,
|
||||
sq(File.E, Rank.R7), sq(File.E, Rank.R8),
|
||||
PromotionPiece.Bishop, Color.White
|
||||
)
|
||||
result match
|
||||
case MoveResult.Moved(newBoard, newHistory, _, _) =>
|
||||
newBoard.pieceAt(sq(File.E, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Bishop)))
|
||||
newHistory.moves.head.promotionPiece should be (Some(PromotionPiece.Bishop))
|
||||
case _ => fail("Expected Moved with Bishop")
|
||||
|
||||
test("completePromotion with knight underpromotion"):
|
||||
val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
val result = GameController.completePromotion(
|
||||
board, GameHistory.empty,
|
||||
sq(File.E, Rank.R7), sq(File.E, Rank.R8),
|
||||
PromotionPiece.Knight, Color.White
|
||||
)
|
||||
result match
|
||||
case MoveResult.Moved(newBoard, newHistory, _, _) =>
|
||||
newBoard.pieceAt(sq(File.E, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Knight)))
|
||||
newHistory.moves.head.promotionPiece should be (Some(PromotionPiece.Knight))
|
||||
case _ => fail("Expected Moved with Knight")
|
||||
|
||||
test("completePromotion captures opponent piece"):
|
||||
// Black king on h1: after white queen captures d8 queen, h1 king is safe (queen on d8 does not attack h1)
|
||||
val board = FenParser.parseBoard("3q4/4P3/8/8/8/8/8/7k").get
|
||||
val result = GameController.completePromotion(
|
||||
board, GameHistory.empty,
|
||||
sq(File.E, Rank.R7), sq(File.D, Rank.R8),
|
||||
PromotionPiece.Queen, Color.White
|
||||
)
|
||||
result match
|
||||
case MoveResult.Moved(newBoard, _, captured, _) =>
|
||||
newBoard.pieceAt(sq(File.D, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Queen)))
|
||||
captured should be (Some(Piece(Color.Black, PieceType.Queen)))
|
||||
case _ => fail("Expected Moved with captured piece")
|
||||
|
||||
test("completePromotion for black pawn to R1"):
|
||||
val board = FenParser.parseBoard("8/8/8/8/4K3/8/4p3/8").get
|
||||
val result = GameController.completePromotion(
|
||||
board, GameHistory.empty,
|
||||
sq(File.E, Rank.R2), sq(File.E, Rank.R1),
|
||||
PromotionPiece.Knight, Color.Black
|
||||
)
|
||||
result match
|
||||
case MoveResult.Moved(newBoard, newHistory, _, _) =>
|
||||
newBoard.pieceAt(sq(File.E, Rank.R1)) should be (Some(Piece(Color.Black, PieceType.Knight)))
|
||||
newHistory.moves.head.promotionPiece should be (Some(PromotionPiece.Knight))
|
||||
case _ => fail("Expected Moved")
|
||||
|
||||
test("completePromotion evaluates check after promotion"):
|
||||
val board = FenParser.parseBoard("3k4/4P3/8/8/8/8/8/8").get
|
||||
val result = GameController.completePromotion(
|
||||
board, GameHistory.empty,
|
||||
sq(File.E, Rank.R7), sq(File.E, Rank.R8),
|
||||
PromotionPiece.Queen, Color.White
|
||||
)
|
||||
result should matchPattern { case _: MoveResult.MovedInCheck => }
|
||||
|
||||
test("completePromotion full round-trip via processMove then completePromotion"):
|
||||
// Black king on h1: not attacked by queen on e8
|
||||
val board = FenParser.parseBoard("8/4P3/8/8/8/8/8/7k").get
|
||||
GameController.processMove(board, GameHistory.empty, Color.White, "e7e8") match
|
||||
case MoveResult.PromotionRequired(from, to, boardBefore, histBefore, _, turn) =>
|
||||
val result = GameController.completePromotion(boardBefore, histBefore, from, to, PromotionPiece.Queen, turn)
|
||||
result should matchPattern { case _: MoveResult.Moved => }
|
||||
result match
|
||||
case MoveResult.Moved(finalBoard, finalHistory, _, _) =>
|
||||
finalBoard.pieceAt(sq(File.E, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Queen)))
|
||||
finalHistory.moves.head.promotionPiece should be (Some(PromotionPiece.Queen))
|
||||
case _ => fail("Expected Moved")
|
||||
case _ => fail("Expected PromotionRequired")
|
||||
|
||||
Reference in New Issue
Block a user