feat: NCS-11 implement 50-move rule with player claim via TUI menu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,5 +8,5 @@ import de.nowchess.chess.logic.GameHistory
|
||||
object Main {
|
||||
def main(args: Array[String]): Unit =
|
||||
println("NowChess TUI — type moves in coordinate notation (e.g. e2e4). Type 'quit' to exit.")
|
||||
GameController.gameLoop(Board.initial, GameHistory.empty, Color.White)
|
||||
GameController.gameLoop(Board.initial, GameHistory.empty, Color.White, 0)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.nowchess.chess.controller
|
||||
|
||||
import scala.io.StdIn
|
||||
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.chess.logic.*
|
||||
import de.nowchess.chess.view.Renderer
|
||||
|
||||
@@ -11,15 +11,16 @@ import de.nowchess.chess.view.Renderer
|
||||
|
||||
sealed trait MoveResult
|
||||
object MoveResult:
|
||||
case object Quit extends MoveResult
|
||||
case class InvalidFormat(raw: String) extends MoveResult
|
||||
case object NoPiece extends MoveResult
|
||||
case object WrongColor extends MoveResult
|
||||
case object IllegalMove extends MoveResult
|
||||
case class Moved(newBoard: Board, newHistory: GameHistory, captured: Option[Piece], newTurn: Color) extends MoveResult
|
||||
case class MovedInCheck(newBoard: Board, newHistory: GameHistory, captured: Option[Piece], newTurn: Color) extends MoveResult
|
||||
case class Checkmate(winner: Color) extends MoveResult
|
||||
case object Stalemate extends MoveResult
|
||||
case object Quit extends MoveResult
|
||||
case class InvalidFormat(raw: String) extends MoveResult
|
||||
case object NoPiece extends MoveResult
|
||||
case object WrongColor extends MoveResult
|
||||
case object IllegalMove extends MoveResult
|
||||
case class Moved(newBoard: Board, newHistory: GameHistory, captured: Option[Piece], newHalfMoveClock: Int, newTurn: Color) extends MoveResult
|
||||
case class MovedInCheck(newBoard: Board, newHistory: GameHistory, captured: Option[Piece], newHalfMoveClock: Int, newTurn: Color) extends MoveResult
|
||||
case class Checkmate(winner: Color) extends MoveResult
|
||||
case object Stalemate extends MoveResult
|
||||
case object DrawClaimed extends MoveResult
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller
|
||||
@@ -30,10 +31,13 @@ object GameController:
|
||||
/** Pure function: interprets one raw input line against the current game context.
|
||||
* Has no I/O side effects — all output must be handled by the caller.
|
||||
*/
|
||||
def processMove(board: Board, history: GameHistory, turn: Color, raw: String): MoveResult =
|
||||
def processMove(board: Board, history: GameHistory, turn: Color, halfMoveClock: Int, raw: String): MoveResult =
|
||||
raw.trim match
|
||||
case "quit" | "q" =>
|
||||
MoveResult.Quit
|
||||
case "draw" =>
|
||||
if halfMoveClock >= 50 then MoveResult.DrawClaimed
|
||||
else MoveResult.InvalidFormat("draw")
|
||||
case trimmed =>
|
||||
Parser.parseMove(trimmed) match
|
||||
case None =>
|
||||
@@ -44,7 +48,7 @@ object GameController:
|
||||
MoveResult.NoPiece
|
||||
case Some(piece) if piece.color != turn =>
|
||||
MoveResult.WrongColor
|
||||
case Some(_) =>
|
||||
case Some(piece) =>
|
||||
if !MoveValidator.isLegal(board, history, from, to) then
|
||||
MoveResult.IllegalMove
|
||||
else
|
||||
@@ -60,52 +64,68 @@ object GameController:
|
||||
val capturedSq = EnPassantCalculator.capturedPawnSquare(to, turn)
|
||||
(b.removed(capturedSq), board.pieceAt(capturedSq))
|
||||
else (b, cap)
|
||||
val isReset = piece.pieceType == PieceType.Pawn || captured.isDefined || isEP
|
||||
val newClock = if isReset then 0 else halfMoveClock + 1
|
||||
val newHistory = history.addMove(from, to, castleOpt)
|
||||
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.Normal => MoveResult.Moved(newBoard, newHistory, captured, newClock, turn.opposite)
|
||||
case PositionStatus.InCheck => MoveResult.MovedInCheck(newBoard, newHistory, captured, newClock, turn.opposite)
|
||||
case PositionStatus.Mated => MoveResult.Checkmate(turn)
|
||||
case PositionStatus.Drawn => MoveResult.Stalemate
|
||||
|
||||
/** Thin I/O shell: renders the board, reads a line, delegates to processMove,
|
||||
* prints the outcome, and recurses until the game ends.
|
||||
*/
|
||||
def gameLoop(board: Board, history: GameHistory, turn: Color): Unit =
|
||||
def gameLoop(board: Board, history: GameHistory, turn: Color, halfMoveClock: Int): Unit =
|
||||
println()
|
||||
print(Renderer.render(board))
|
||||
println(s"${turn.label}'s turn. Enter move: ")
|
||||
val input = Option(StdIn.readLine()).getOrElse("quit").trim
|
||||
processMove(board, history, turn, input) match
|
||||
val input =
|
||||
if halfMoveClock >= 50 then
|
||||
println(s"[50-move rule] ${turn.label} may claim a draw, or continue playing.")
|
||||
println(" 1. Claim draw")
|
||||
println(" 2. Continue")
|
||||
Option(StdIn.readLine()).getOrElse("2").trim match
|
||||
case "1" => "draw"
|
||||
case _ =>
|
||||
println(s"${turn.label}'s turn. Enter move: ")
|
||||
Option(StdIn.readLine()).getOrElse("quit").trim
|
||||
else
|
||||
println(s"${turn.label}'s turn. Enter move: ")
|
||||
Option(StdIn.readLine()).getOrElse("quit").trim
|
||||
processMove(board, history, turn, halfMoveClock, input) match
|
||||
case MoveResult.Quit =>
|
||||
println("Game over. Goodbye!")
|
||||
case MoveResult.DrawClaimed =>
|
||||
println("Draw claimed by 50-move rule.")
|
||||
gameLoop(Board.initial, GameHistory.empty, Color.White, 0)
|
||||
case MoveResult.InvalidFormat(raw) =>
|
||||
println(s"Invalid move format '$raw'. Use coordinate notation, e.g. e2e4.")
|
||||
gameLoop(board, history, turn)
|
||||
gameLoop(board, history, turn, halfMoveClock)
|
||||
case MoveResult.NoPiece =>
|
||||
println(s"No piece on ${Parser.parseMove(input).map(_._1).fold("?")(_.toString)}.")
|
||||
gameLoop(board, history, turn)
|
||||
gameLoop(board, history, turn, halfMoveClock)
|
||||
case MoveResult.WrongColor =>
|
||||
println(s"That is not your piece.")
|
||||
gameLoop(board, history, turn)
|
||||
gameLoop(board, history, turn, halfMoveClock)
|
||||
case MoveResult.IllegalMove =>
|
||||
println(s"Illegal move.")
|
||||
gameLoop(board, history, turn)
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, newTurn) =>
|
||||
gameLoop(board, history, turn, halfMoveClock)
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, newClock, newTurn) =>
|
||||
val prevTurn = newTurn.opposite
|
||||
captured.foreach: cap =>
|
||||
val toSq = Parser.parseMove(input).map(_._2).fold("?")(_.toString)
|
||||
println(s"${prevTurn.label} captures ${cap.color.label} ${cap.pieceType.label} on $toSq")
|
||||
gameLoop(newBoard, newHistory, newTurn)
|
||||
case MoveResult.MovedInCheck(newBoard, newHistory, captured, newTurn) =>
|
||||
gameLoop(newBoard, newHistory, newTurn, newClock)
|
||||
case MoveResult.MovedInCheck(newBoard, newHistory, captured, newClock, newTurn) =>
|
||||
val prevTurn = newTurn.opposite
|
||||
captured.foreach: cap =>
|
||||
val toSq = Parser.parseMove(input).map(_._2).fold("?")(_.toString)
|
||||
println(s"${prevTurn.label} captures ${cap.color.label} ${cap.pieceType.label} on $toSq")
|
||||
println(s"${newTurn.label} is in check!")
|
||||
gameLoop(newBoard, newHistory, newTurn)
|
||||
gameLoop(newBoard, newHistory, newTurn, newClock)
|
||||
case MoveResult.Checkmate(winner) =>
|
||||
println(s"Checkmate! ${winner.label} wins.")
|
||||
gameLoop(Board.initial, GameHistory.empty, Color.White)
|
||||
gameLoop(Board.initial, GameHistory.empty, Color.White, 0)
|
||||
case MoveResult.Stalemate =>
|
||||
println("Stalemate! The game is a draw.")
|
||||
gameLoop(Board.initial, GameHistory.empty, Color.White)
|
||||
gameLoop(Board.initial, GameHistory.empty, Color.White, 0)
|
||||
|
||||
+123
-27
@@ -11,11 +11,11 @@ import java.io.ByteArrayInputStream
|
||||
class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def sq(f: File, r: Rank): Square = Square(f, r)
|
||||
private def processMove(board: Board, history: GameHistory, turn: Color, raw: String): MoveResult =
|
||||
GameController.processMove(board, history, turn, raw)
|
||||
private def processMove(board: Board, history: GameHistory, turn: Color, raw: String, halfMoveClock: Int = 0): MoveResult =
|
||||
GameController.processMove(board, history, turn, halfMoveClock, raw)
|
||||
|
||||
private def gameLoop(board: Board, history: GameHistory, turn: Color): Unit =
|
||||
GameController.gameLoop(board, history, turn)
|
||||
private def gameLoop(board: Board, history: GameHistory, turn: Color, halfMoveClock: Int = 0): Unit =
|
||||
GameController.gameLoop(board, history, turn, halfMoveClock)
|
||||
|
||||
private def castlingRights(history: GameHistory, color: Color): CastlingRights =
|
||||
de.nowchess.chess.logic.CastlingRightsCalculator.deriveCastlingRights(history, color)
|
||||
@@ -48,7 +48,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("processMove: legal pawn move returns Moved with updated board and flipped turn"):
|
||||
processMove(Board.initial, GameHistory.empty, Color.White, "e2e4") match
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, newTurn) =>
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, _, newTurn) =>
|
||||
newBoard.pieceAt(sq(File.E, Rank.R4)) shouldBe Some(Piece.WhitePawn)
|
||||
newBoard.pieceAt(sq(File.E, Rank.R2)) shouldBe None
|
||||
captured shouldBe None
|
||||
@@ -63,7 +63,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R8) -> Piece.WhiteKing
|
||||
))
|
||||
processMove(board, GameHistory.empty, Color.White, "e5d6") match
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, newTurn) =>
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, _, newTurn) =>
|
||||
captured shouldBe Some(Piece.BlackPawn)
|
||||
newBoard.pieceAt(sq(File.D, Rank.R6)) shouldBe Some(Piece.WhitePawn)
|
||||
newTurn shouldBe Color.Black
|
||||
@@ -133,7 +133,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.White, "a1a8") match
|
||||
case MoveResult.MovedInCheck(_, _, _, newTurn) => newTurn shouldBe Color.Black
|
||||
case MoveResult.MovedInCheck(_, _, _, _, newTurn) => newTurn shouldBe Color.Black
|
||||
case other => fail(s"Expected MovedInCheck, got $other")
|
||||
|
||||
test("processMove: legal move that results in checkmate returns Checkmate"):
|
||||
@@ -220,7 +220,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.White, "e1g1") match
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, newTurn) =>
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, _, newTurn) =>
|
||||
newBoard.pieceAt(sq(File.G, Rank.R1)) shouldBe Some(Piece.WhiteKing)
|
||||
newBoard.pieceAt(sq(File.F, Rank.R1)) shouldBe Some(Piece.WhiteRook)
|
||||
newBoard.pieceAt(sq(File.E, Rank.R1)) shouldBe None
|
||||
@@ -236,7 +236,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.White, "e1c1") match
|
||||
case MoveResult.Moved(newBoard, _, _, _) =>
|
||||
case MoveResult.Moved(newBoard, _, _, _, _) =>
|
||||
newBoard.pieceAt(sq(File.C, Rank.R1)) shouldBe Some(Piece.WhiteKing)
|
||||
newBoard.pieceAt(sq(File.D, Rank.R1)) shouldBe Some(Piece.WhiteRook)
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
@@ -250,7 +250,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.White, "e1g1") match
|
||||
case MoveResult.Moved(_, newHistory, _, _) =>
|
||||
case MoveResult.Moved(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.White) shouldBe CastlingRights.None
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
|
||||
@@ -261,10 +261,10 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.White, "h1h4") match
|
||||
case MoveResult.Moved(_, newHistory, _, _) =>
|
||||
case MoveResult.Moved(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.White).kingSide shouldBe false
|
||||
castlingRights(newHistory, Color.White).queenSide shouldBe true
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _) =>
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.White).kingSide shouldBe false
|
||||
castlingRights(newHistory, Color.White).queenSide shouldBe true
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
@@ -275,7 +275,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.White, "e1e2") match
|
||||
case MoveResult.Moved(_, newHistory, _, _) =>
|
||||
case MoveResult.Moved(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.White) shouldBe CastlingRights.None
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
|
||||
@@ -287,9 +287,9 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.A, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.Black, "h2h1") match
|
||||
case MoveResult.Moved(_, newHistory, _, _) =>
|
||||
case MoveResult.Moved(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.White).kingSide shouldBe false
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _) =>
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.White).kingSide shouldBe false
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
|
||||
@@ -316,9 +316,9 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.Black, "e8e7") match
|
||||
case MoveResult.Moved(_, newHistory, _, _) =>
|
||||
case MoveResult.Moved(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.Black) shouldBe CastlingRights.None
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _) =>
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.Black) shouldBe CastlingRights.None
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
|
||||
@@ -329,10 +329,10 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.Black, "a8a1") match
|
||||
case MoveResult.Moved(_, newHistory, _, _) =>
|
||||
case MoveResult.Moved(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.Black).queenSide shouldBe false
|
||||
castlingRights(newHistory, Color.Black).kingSide shouldBe true
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _) =>
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.Black).queenSide shouldBe false
|
||||
castlingRights(newHistory, Color.Black).kingSide shouldBe true
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
@@ -344,10 +344,10 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.A, Rank.R1) -> Piece.WhiteKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.Black, "h8h4") match
|
||||
case MoveResult.Moved(_, newHistory, _, _) =>
|
||||
case MoveResult.Moved(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.Black).kingSide shouldBe false
|
||||
castlingRights(newHistory, Color.Black).queenSide shouldBe true
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _) =>
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.Black).kingSide shouldBe false
|
||||
castlingRights(newHistory, Color.Black).queenSide shouldBe true
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
@@ -360,9 +360,9 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
processMove(b, GameHistory.empty, Color.Black, "a2a1") match
|
||||
case MoveResult.Moved(_, newHistory, _, _) =>
|
||||
case MoveResult.Moved(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.White).queenSide shouldBe false
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _) =>
|
||||
case MoveResult.MovedInCheck(_, newHistory, _, _, _) =>
|
||||
castlingRights(newHistory, Color.White).queenSide shouldBe false
|
||||
case other => fail(s"Expected Moved or MovedInCheck, got $other")
|
||||
|
||||
@@ -377,9 +377,9 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
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")
|
||||
val result = GameController.processMove(b, h, Color.White, 0, "e5d6")
|
||||
result match
|
||||
case MoveResult.Moved(newBoard, _, captured, _) =>
|
||||
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)
|
||||
@@ -394,10 +394,106 @@ class GameControllerTest extends AnyFunSuite with Matchers:
|
||||
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")
|
||||
val result = GameController.processMove(b, h, Color.Black, 0, "d4e3")
|
||||
result match
|
||||
case MoveResult.Moved(newBoard, _, captured, _) =>
|
||||
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")
|
||||
|
||||
// ──── processMove: 50-move rule draw claim ───────────────────────────────
|
||||
|
||||
test("processMove: 'draw' with halfMoveClock = 50 returns DrawClaimed"):
|
||||
val b = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.E, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
GameController.processMove(b, GameHistory.empty, Color.White, 50, "draw") shouldBe MoveResult.DrawClaimed
|
||||
|
||||
test("processMove: 'draw' with halfMoveClock = 49 returns InvalidFormat"):
|
||||
val b = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.E, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
GameController.processMove(b, GameHistory.empty, Color.White, 49, "draw") shouldBe MoveResult.InvalidFormat("draw")
|
||||
|
||||
// ──── processMove: halfMoveClock update ──────────────────────────────────
|
||||
|
||||
test("processMove: pawn move resets halfMoveClock to 0"):
|
||||
GameController.processMove(Board.initial, GameHistory.empty, Color.White, 10, "e2e4") match
|
||||
case MoveResult.Moved(_, _, _, newClock, _) => newClock shouldBe 0
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
|
||||
test("processMove: capture resets halfMoveClock to 0"):
|
||||
val b = Board(Map(
|
||||
sq(File.A, Rank.R5) -> Piece.WhiteRook,
|
||||
sq(File.D, Rank.R5) -> Piece.BlackPawn,
|
||||
sq(File.H, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
GameController.processMove(b, GameHistory.empty, Color.White, 15, "a5d5") match
|
||||
case MoveResult.Moved(_, _, _, newClock, _) => newClock shouldBe 0
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
|
||||
test("processMove: en passant capture resets halfMoveClock to 0"):
|
||||
val b = Board(Map(
|
||||
sq(File.E, Rank.R5) -> Piece.WhitePawn,
|
||||
sq(File.D, Rank.R5) -> Piece.BlackPawn,
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.E, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
val h = GameHistory.empty.addMove(sq(File.D, Rank.R7), sq(File.D, Rank.R5))
|
||||
GameController.processMove(b, h, Color.White, 20, "e5d6") match
|
||||
case MoveResult.Moved(_, _, _, newClock, _) => newClock shouldBe 0
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
|
||||
test("processMove: quiet piece move increments halfMoveClock"):
|
||||
val b = Board(Map(
|
||||
sq(File.A, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.E, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
GameController.processMove(b, GameHistory.empty, Color.White, 10, "a1a5") match
|
||||
case MoveResult.Moved(_, _, _, newClock, _) => newClock shouldBe 11
|
||||
case other => fail(s"Expected Moved, got $other")
|
||||
|
||||
test("processMove: MovedInCheck carries updated halfMoveClock"):
|
||||
val b = Board(Map(
|
||||
sq(File.A, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.C, Rank.R3) -> Piece.WhiteKing,
|
||||
sq(File.H, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
GameController.processMove(b, GameHistory.empty, Color.White, 7, "a1a8") match
|
||||
case MoveResult.MovedInCheck(_, _, _, newClock, _) => newClock shouldBe 8
|
||||
case other => fail(s"Expected MovedInCheck, got $other")
|
||||
|
||||
// ──── gameLoop: 50-move rule menu ────────────────────────────────────────
|
||||
|
||||
test("gameLoop: shows 50-move rule menu when halfMoveClock >= 50 and draw claimed"):
|
||||
val b = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.E, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
val output = captureOutput:
|
||||
withInput("1\nquit\n"):
|
||||
GameController.gameLoop(b, GameHistory.empty, Color.White, 50)
|
||||
output should include("50-move rule")
|
||||
output should include("Draw claimed by 50-move rule.")
|
||||
|
||||
test("gameLoop: shows 50-move rule menu when halfMoveClock >= 50 and player continues"):
|
||||
val b = Board(Map(
|
||||
sq(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
sq(File.E, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
val output = captureOutput:
|
||||
withInput("2\nquit\n"):
|
||||
GameController.gameLoop(b, GameHistory.empty, Color.White, 50)
|
||||
output should include("50-move rule")
|
||||
output should include("White's turn")
|
||||
|
||||
test("gameLoop: no 50-move rule menu when halfMoveClock < 50"):
|
||||
val output = captureOutput:
|
||||
withInput("quit\n"):
|
||||
GameController.gameLoop(Board.initial, GameHistory.empty, Color.White, 49)
|
||||
output should not include "50-move rule"
|
||||
|
||||
Reference in New Issue
Block a user