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
|
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.api.move.PromotionPiece
|
||||||
import de.nowchess.chess.logic.*
|
import de.nowchess.chess.logic.*
|
||||||
|
|
||||||
@@ -76,3 +76,28 @@ object GameController:
|
|||||||
case PositionStatus.InCheck => MoveResult.MovedInCheck(newBoard, newHistory, captured, turn.opposite)
|
case PositionStatus.InCheck => MoveResult.MovedInCheck(newBoard, newHistory, captured, turn.opposite)
|
||||||
case PositionStatus.Mated => MoveResult.Checkmate(turn)
|
case PositionStatus.Mated => MoveResult.Checkmate(turn)
|
||||||
case PositionStatus.Drawn => MoveResult.Stalemate
|
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.board.*
|
||||||
import de.nowchess.api.game.CastlingRights
|
import de.nowchess.api.game.CastlingRights
|
||||||
|
import de.nowchess.api.move.PromotionPiece
|
||||||
import de.nowchess.chess.logic.{CastleSide, GameHistory}
|
import de.nowchess.chess.logic.{CastleSide, GameHistory}
|
||||||
import de.nowchess.chess.notation.FenParser
|
import de.nowchess.chess.notation.FenParser
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
@@ -327,3 +328,112 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
|||||||
case MoveResult.PromotionRequired(_, _, _, _, captured, _) =>
|
case MoveResult.PromotionRequired(_, _, _, _, captured, _) =>
|
||||||
captured should be (Some(Piece(Color.Black, PieceType.Queen)))
|
captured should be (Some(Piece(Color.Black, PieceType.Queen)))
|
||||||
case _ => fail("Expected PromotionRequired")
|
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