4.9 KiB
Chess Check / Checkmate / Stalemate — Design Spec
Date: 2026-03-23 Status: Approved
Scope
Implement check detection, checkmate (win condition), and stalemate (draw) on top of the existing normal-move rules. En passant, castling, and pawn promotion are out of scope for this iteration.
Architecture
New: GameRules object
File: modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala
Owns all check-aware game logic. MoveValidator retains its documented geometric-only contract ("ignoring check/pin").
GameRules
isInCheck(board, color): Boolean
legalMoves(board, color): Set[(Square, Square)]
gameStatus(board, color): PositionStatus
isInCheck(board, color)
Finds the king square for color, then checks whether any enemy piece's MoveValidator.legalTargets contains that square. Returns true if the king is under attack.
legalMoves(board, color)
For every piece of color on the board, collect MoveValidator.legalTargets. Filter each candidate move by applying it to the board (board.withMove) and verifying isInCheck is false on the resulting board. Returns the full set of (from, to) pairs that are truly legal.
gameStatus(board, color)
Returns a PositionStatus enum value:
Checkmate—legalMovesis empty and king is in check → the side to move losesStalemate—legalMovesis empty and king is not in check → drawInCheck—legalMovesis non-empty and king is in check → game continues under checkNormal— otherwise
Local PositionStatus enum
Defined inside GameRules.scala (or its companion package):
enum PositionStatus:
case Normal, InCheck, Checkmate, Stalemate
Modified: MoveResult (in GameController.scala)
Two new variants are added; existing variants are unchanged:
| Variant | When used |
|---|---|
MovedInCheck(newBoard, captured, newTurn) |
Move was legal; opponent is now in check |
Checkmate(winner: Color) |
Move was legal; opponent has no legal reply → winner is the side that just moved |
Stalemate |
Move was legal; opponent has no legal reply and is not in check → draw |
Moved continues to be used for all other successful moves.
Modified: GameController.processMove
After computing (newBoard, captured) from board.withMove:
- Call
GameRules.gameStatus(newBoard, newTurn). - Map the result to the appropriate
MoveResultvariant.
Normal → Moved(newBoard, captured, newTurn)
InCheck → MovedInCheck(newBoard, captured, newTurn)
Checkmate → Checkmate(turn) // turn = the side that just moved
Stalemate → Stalemate
Modified: GameController.gameLoop
Two new terminal branches:
Checkmate(winner)→ print"Checkmate! {winner} wins.", then recurse with(Board.initial, Color.White)Stalemate→ print"Stalemate! The game is a draw.", then recurse with(Board.initial, Color.White)MovedInCheck→ print"{newTurn} is in check!", then recurse normally with the new board and turn
Test Strategy
All tests are unit tests extending AnyFunSuite with Matchers with JUnitSuiteLike.
GameRulesTest
| Scenario | Method under test |
|---|---|
| King is attacked by an enemy rook | isInCheck → true |
| King is not attacked | isInCheck → false |
| Move that exposes king is filtered out | legalMoves excludes it |
| Checkmate position (e.g. back-rank mate) | gameStatus → Checkmate |
| Stalemate position | gameStatus → Stalemate |
| In-check position with at least one escape | gameStatus → InCheck |
| Normal position | gameStatus → Normal |
GameControllerTest additions
| Scenario | Expected MoveResult |
|---|---|
| Move leaves opponent in check | MovedInCheck |
| Move results in checkmate | Checkmate(winner) |
| Move results in stalemate | Stalemate |
Development Workflow (TDD)
- Create
GameRuleswith empty/stub method bodies (compile but return placeholder values). - Write all
GameRulesTesttests — they should fail. - Implement
GameRuleslogic until tests pass. - Add new
MoveResultvariants and stubprocessMovechanges. - Write new
GameControllerTestcases — they should fail. - Implement
processMoveandgameLoopchanges until tests pass. - Run
./gradlew :modules:core:test— full green build required.
Files Changed
| File | Change |
|---|---|
modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala |
New |
modules/core/src/test/scala/de/nowchess/chess/logic/GameRulesTest.scala |
New |
modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala |
Add MoveResult variants; update processMove and gameLoop |
modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala |
Add new test cases |
No changes to modules/api or MoveValidator.