docs: remove outdated JUnitSuiteLike issue from unresolved.md
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
# Design: Add ScalaTest + Replace JaCoCo with Scoverage
|
||||
|
||||
**Date:** 2026-03-22
|
||||
**Status:** Approved
|
||||
|
||||
## Summary
|
||||
|
||||
Replace the current JUnit-only test setup and JaCoCo coverage with ScalaTest (via its JUnit 5 bridge) and Scoverage across both `modules/core` and `modules/api`.
|
||||
|
||||
## Motivation
|
||||
|
||||
- The CLAUDE.md working agreement prescribes `AnyFunSuite with Matchers with JUnitSuiteLike` as the unit test style, which requires ScalaTest.
|
||||
- Scoverage is the standard Scala code coverage tool and understands Scala semantics; JaCoCo's JVM bytecode instrumentation is less accurate for Scala code.
|
||||
|
||||
## Scope
|
||||
|
||||
Two modules are affected: `modules/core` and `modules/api`. The root `build.gradle.kts` is updated for shared dependency versions only.
|
||||
|
||||
## Changes
|
||||
|
||||
### Root `build.gradle.kts`
|
||||
|
||||
Add to the `versions` map (dependency versions only — plugin version is hardcoded per module, see note below):
|
||||
- `SCALATEST` → `3.2.19`
|
||||
- `SCALATESTPLUS_JUNIT5` → `3.2.19.1`
|
||||
|
||||
> **Note on plugin versioning:** Gradle resolves the `plugins {}` block before `rootProject.extra` is available, so the Scoverage plugin version (`8.1`) must be declared inline in each module's `plugins {}` block. It cannot be read from the root versions map.
|
||||
|
||||
### `modules/core/build.gradle.kts` and `modules/api/build.gradle.kts`
|
||||
|
||||
Both modules require the same set of changes. Both currently have **two separate `tasks.test {}` blocks** that must be merged into one.
|
||||
|
||||
**Plugins block:**
|
||||
- Remove `jacoco`
|
||||
- Add `id("org.scoverage") version "8.1"`
|
||||
|
||||
**Dependencies block:**
|
||||
- Remove `testImplementation(platform("org.junit:junit-bom:5.10.0"))`
|
||||
- Remove `testImplementation("org.junit.jupiter:junit-jupiter")`
|
||||
- Remove `testRuntimeOnly("org.junit.platform:junit-platform-launcher")`
|
||||
- Add `testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")`
|
||||
- Add `testImplementation("org.scalatestplus:junit-5-11_3:${versions["SCALATESTPLUS_JUNIT5"]!!}")`
|
||||
|
||||
**Task wiring — merge both `tasks.test {}` blocks into one and replace jacoco wiring:**
|
||||
|
||||
Both `modules/core` and `modules/api` currently have two `tasks.test {}` blocks. Delete both and replace with the following single merged block placed **after** the `dependencies {}` block (conventional position):
|
||||
|
||||
```kotlin
|
||||
tasks.test {
|
||||
useJUnitPlatform() // required — scalatestplus JUnit 5 bridge relies on this
|
||||
finalizedBy(tasks.reportScoverage)
|
||||
}
|
||||
tasks.reportScoverage {
|
||||
dependsOn(tasks.test)
|
||||
}
|
||||
```
|
||||
|
||||
> Note: `modules/api` does not currently have `useJUnitPlatform()` — it must be **added** (not just kept) in the merged block.
|
||||
|
||||
Remove the `jacocoTestReport` task block entirely from both modules.
|
||||
|
||||
**Task name confirmation:** The Scoverage Gradle plugin 8.1 registers `reportScoverage` as the HTML report task.
|
||||
|
||||
## Versions
|
||||
|
||||
| Artifact | Version | Notes |
|
||||
|---|---|---|
|
||||
| `org.scalatest:scalatest_3` | 3.2.19 | Core ScalaTest for Scala 3 |
|
||||
| `org.scalatestplus:junit-5-11_3` | 3.2.19.1 | JUnit 5.11 runner bridge; `.1` = build 1 |
|
||||
| Scoverage Gradle plugin | 8.1 | Hardcoded inline in `plugins {}` block |
|
||||
|
||||
## Testing the Change
|
||||
|
||||
After applying:
|
||||
1. `./gradlew :modules:core:test` and `./gradlew :modules:api:test` must pass (green, even with zero test files).
|
||||
2. `./gradlew :modules:core:reportScoverage` must produce a coverage report.
|
||||
3. `./gradlew build` must be fully green.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `build.gradle.kts` (root) — add two version entries
|
||||
- `modules/core/build.gradle.kts` — plugin, deps, merge two `tasks.test` blocks, replace jacoco wiring
|
||||
- `modules/api/build.gradle.kts` — plugin, deps, merge two `tasks.test` blocks, add `useJUnitPlatform()`, replace jacoco wiring
|
||||
|
||||
No new source files are created.
|
||||
@@ -1,169 +0,0 @@
|
||||
# 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` by scanning `board.pieces` for a `Piece(color, PieceType.King)`. If no king is found (constructed/test boards), returns `false`.
|
||||
|
||||
Then checks whether any enemy piece's `MoveValidator.legalTargets` contains that square. This works correctly for all piece types, including the king: `kingTargets` returns the squares the king can move to, which are identical to the squares the king attacks, so using `legalTargets` for attack detection is correct by design.
|
||||
|
||||
Returns `true` if the king square is covered by at least one enemy piece.
|
||||
|
||||
|
||||
#### `legalMoves(board, color)`
|
||||
|
||||
1. Filter `board.pieces` to entries where `piece.color == color`.
|
||||
2. For each such `(from, piece)`, call `MoveValidator.legalTargets(board, from)` to get geometric candidates.
|
||||
3. For each candidate `to`, apply `board.withMove(from, to)` to get `newBoard`.
|
||||
4. Keep only moves where `isInCheck(newBoard, color)` is `false` (i.e., the move does not leave own king in check).
|
||||
5. Return the full set of `(from, to)` pairs that survive this filter.
|
||||
|
||||
#### `gameStatus(board, color)`
|
||||
|
||||
Returns a `PositionStatus` enum value based on `legalMoves(board, color)` and `isInCheck(board, color)`:
|
||||
|
||||
- `Mated` — `legalMoves` is empty **and** king is in check → the side to move has been checkmated
|
||||
- `Drawn` — `legalMoves` is empty **and** king is **not** in check → stalemate (draw)
|
||||
- `InCheck` — `legalMoves` is non-empty **and** king is in check → game continues under check
|
||||
- `Normal` — otherwise
|
||||
|
||||
#### Local `PositionStatus` enum
|
||||
|
||||
Defined in `GameRules.scala`. Names are intentionally distinct from `MoveResult` variants to avoid unqualified-name collisions in `GameController.scala`:
|
||||
|
||||
```scala
|
||||
enum PositionStatus:
|
||||
case Normal, InCheck, Mated, Drawn
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Modified: `MoveResult` (in `GameController.scala`)
|
||||
|
||||
Three new variants; existing variants are unchanged:
|
||||
|
||||
| Variant | When used |
|
||||
|---|---|
|
||||
| `MovedInCheck(newBoard, captured, newTurn)` | Move was legal; opponent is now in check but has legal replies |
|
||||
| `Checkmate(winner: Color)` | Move was legal; opponent is `Mated` → `winner` is the side that just moved |
|
||||
| `Stalemate` | Move was legal; opponent is `Drawn` (no legal reply, not in check) |
|
||||
|
||||
`Moved` continues to be used when `gameStatus` returns `Normal`.
|
||||
|
||||
---
|
||||
|
||||
### Modified: `GameController.processMove`
|
||||
|
||||
After computing `(newBoard, captured)` from `board.withMove`:
|
||||
|
||||
1. Call `GameRules.gameStatus(newBoard, newTurn)`.
|
||||
2. Map to the appropriate `MoveResult`:
|
||||
|
||||
```
|
||||
PositionStatus.Normal → Moved(newBoard, captured, newTurn)
|
||||
PositionStatus.InCheck → MovedInCheck(newBoard, captured, newTurn)
|
||||
PositionStatus.Mated → Checkmate(turn) // turn = the side that just moved
|
||||
PositionStatus.Drawn → Stalemate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Modified: `GameController.gameLoop`
|
||||
|
||||
**New terminal branches** (both print a message then restart):
|
||||
|
||||
- `Checkmate(winner)` → print `"Checkmate! {winner.label} wins."`, then recurse with `(Board.initial, Color.White)`
|
||||
- `Stalemate` → print `"Stalemate! The game is a draw."`, then recurse with `(Board.initial, Color.White)`
|
||||
|
||||
**New non-terminal branch:**
|
||||
|
||||
- `MovedInCheck(newBoard, captured, newTurn)` → print the same optional capture message as `Moved` (when `captured.isDefined`), then print `"{newTurn.label} is in check!"`, then recurse with `(newBoard, newTurn)`
|
||||
|
||||
**Restart vs. exit:** Checkmate and stalemate restart the game automatically (no prompt). This is intentionally asymmetric with `Quit`, which exits. `Quit` is an explicit user request to stop; Checkmate/Stalemate are natural game endings that should roll into a new game.
|
||||
|
||||
---
|
||||
|
||||
## Test Strategy
|
||||
|
||||
All tests are unit tests extending `AnyFunSuite with Matchers with JUnitSuiteLike`.
|
||||
|
||||
### `GameRulesTest` — new file
|
||||
|
||||
| Scenario | Method | Expected |
|
||||
|---|---|---|
|
||||
| King attacked by enemy rook on same rank | `isInCheck` | `true` |
|
||||
| King not attacked (only own pieces nearby) | `isInCheck` | `false` |
|
||||
| No king on board (constructed board) | `isInCheck` | `false` |
|
||||
| Move that exposes own king to rook is excluded | `legalMoves` | does not contain that move |
|
||||
| Move that blocks check is included | `legalMoves` | contains the blocking move |
|
||||
| Checkmate: White Qh8, Ka6; Black Ka8 — Black king is in check (Qh8 along rank 8), cannot escape to a7 (Ka6), b7 (Ka6), or b8 (Qh8) | `gameStatus` | `Mated` |
|
||||
| Stalemate: White Qb6, Kc6; Black Ka8 — Black king has no legal moves (a7/b7/b8 all controlled by Qb6), not in check | `gameStatus` | `Drawn` |
|
||||
| King in check with at least one escape square | `gameStatus` | `InCheck` |
|
||||
| Normal midgame position, not in check, has moves | `gameStatus` | `Normal` |
|
||||
|
||||
### `GameControllerTest` additions — new `processMove` cases
|
||||
|
||||
| Scenario | Expected `MoveResult` |
|
||||
|---|---|
|
||||
| Move leaves opponent in check (has escape) | `MovedInCheck` |
|
||||
| Move results in checkmate | `Checkmate(winner)` where winner is the side that moved |
|
||||
| Move results in stalemate | `Stalemate` |
|
||||
|
||||
### `GameControllerTest` additions — new `gameLoop` cases
|
||||
|
||||
| Scenario | Expected output / behavior |
|
||||
|---|---|
|
||||
| `gameLoop` receives `Checkmate(White)` | Prints "Checkmate! White wins." and continues (new game) |
|
||||
| `gameLoop` receives `Stalemate` | Prints "Stalemate! The game is a draw." and continues (new game) |
|
||||
| `gameLoop` receives `MovedInCheck` with a capture | Prints capture message AND check message |
|
||||
| `gameLoop` receives `MovedInCheck` without a capture | Prints check message only |
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow (TDD)
|
||||
|
||||
1. Create `GameRules.scala` with empty/stub method bodies that compile but return placeholder values (`false`, `Set.empty`, `PositionStatus.Normal`).
|
||||
2. Write all `GameRulesTest` tests — they should **fail**.
|
||||
3. Implement `GameRules` logic until `GameRulesTest` is green.
|
||||
4. Add new `MoveResult` variants to `GameController.scala`; update `processMove` to call `GameRules.gameStatus` (stub the match arms initially).
|
||||
5. Write new `GameControllerTest` cases — they should **fail**.
|
||||
6. Implement `processMove` match arms and `gameLoop` new branches until all tests pass.
|
||||
7. 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`.
|
||||
@@ -1,255 +0,0 @@
|
||||
# Castling Implementation Design
|
||||
|
||||
**Date:** 2026-03-24
|
||||
**Status:** Approved (rev 2)
|
||||
**Branch:** castling
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The NowChessSystems chess engine currently operates on a raw `Board` (opaque `Map[Square, Piece]`) paired with a `Color` for turn tracking. Castling requires tracking whether the king and rooks have previously moved — state that does not exist in the current engine layer. The `CastlingRights` and `MoveType.Castle*` types are already defined in the `api` module but are not wired into the engine.
|
||||
|
||||
---
|
||||
|
||||
## Approach: `GameContext` Wrapper (Option B)
|
||||
|
||||
Introduce a thin `GameContext` wrapper in `modules/core` that bundles `Board` with castling rights for both sides. This is the single seam through which the engine learns about castling availability without pulling in the full FEN-structured `GameState` type.
|
||||
|
||||
---
|
||||
|
||||
## Section 1 — `GameContext` Type
|
||||
|
||||
**Location:** `modules/core/src/main/scala/de/nowchess/chess/logic/GameContext.scala`
|
||||
|
||||
```scala
|
||||
case class GameContext(
|
||||
board: Board,
|
||||
whiteCastling: CastlingRights,
|
||||
blackCastling: CastlingRights
|
||||
):
|
||||
def castlingFor(color: Color): CastlingRights =
|
||||
if color == Color.White then whiteCastling else blackCastling
|
||||
|
||||
def withUpdatedRights(color: Color, rights: CastlingRights): GameContext =
|
||||
if color == Color.White then copy(whiteCastling = rights)
|
||||
else copy(blackCastling = rights)
|
||||
```
|
||||
|
||||
`GameContext.initial` wraps `Board.initial` with `CastlingRights.Both` for both sides.
|
||||
|
||||
`gameLoop` and `processMove` replace `(board: Board, turn: Color)` with `(ctx: GameContext, turn: Color)`. All `MoveResult` variants that previously carried `newBoard: Board` now carry `newCtx: GameContext`. The `gameLoop` render call becomes `Renderer.render(ctx.board)`, and all `gameLoop` pattern match arms that destructure `MoveResult.Moved(newBoard, ...)` or `MoveResult.MovedInCheck(newBoard, ...)` must be updated to destructure `newCtx` and pass it to the recursive `gameLoop` call.
|
||||
|
||||
---
|
||||
|
||||
## Section 2 — `CastleSide` and Board Extension for Castle Moves
|
||||
|
||||
### `CastleSide` enum
|
||||
|
||||
`CastleSide` is a two-value engine-internal enum defined in `core` (not in `api`). It is co-located in `GameContext.scala` — there is no separate `CastleSide.scala` file.
|
||||
|
||||
```scala
|
||||
enum CastleSide:
|
||||
case Kingside, Queenside
|
||||
```
|
||||
|
||||
### `withCastle` extension
|
||||
|
||||
`Board.withMove(from, to)` moves a single piece. Castling moves two pieces atomically. To avoid a circular dependency (`api` must not import from `core`), `withCastle` is **not** added to `Board` in the `api` module. Instead it is defined as an extension method in `core`, co-located with `GameContext`:
|
||||
|
||||
```scala
|
||||
// inside GameContext.scala or a BoardCastleOps.scala in core
|
||||
extension (b: Board)
|
||||
def withCastle(color: Color, side: CastleSide): Board = ...
|
||||
```
|
||||
|
||||
Post-castle square assignments:
|
||||
- **Kingside White:** King e1→g1, Rook h1→f1
|
||||
- **Queenside White:** King e1→c1, Rook a1→d1
|
||||
- **Kingside Black:** King e8→g8, Rook h8→f8
|
||||
- **Queenside Black:** King e8→c8, Rook a8→d8
|
||||
|
||||
---
|
||||
|
||||
## Section 3 — `MoveValidator` Castling Logic
|
||||
|
||||
### Signature change
|
||||
|
||||
`legalTargets` and `isLegal` are extended to accept `GameContext` when the caller has full game context. To avoid breaking `GameRules.isInCheck` (which uses `legalTargets` with only a `Board` for attacked-square detection), the implementation retains a **board-only private helper** for sliding/jump/normal king targets, and a **public overload** that additionally unions castling targets when a `GameContext` is provided:
|
||||
|
||||
```scala
|
||||
// board-only (used internally by isInCheck)
|
||||
def legalTargets(board: Board, from: Square): Set[Square]
|
||||
|
||||
// context-aware (used by legalMoves and processMove)
|
||||
def legalTargets(ctx: GameContext, from: Square): Set[Square]
|
||||
```
|
||||
|
||||
The `GameContext` overload delegates to the `Board` overload for all piece types except King, where it additionally unions `castlingTargets(ctx, color)`.
|
||||
|
||||
`isLegal` is likewise overloaded:
|
||||
|
||||
```scala
|
||||
// board-only (retained for callers that have no castling context)
|
||||
def isLegal(board: Board, from: Square, to: Square): Boolean
|
||||
|
||||
// context-aware (used by processMove)
|
||||
def isLegal(ctx: GameContext, from: Square, to: Square): Boolean
|
||||
```
|
||||
|
||||
The context-aware `isLegal(ctx, from, to)` calls `legalTargets(ctx, from).contains(to)` — using the context-aware overload — so castling targets are included in the legality check.
|
||||
|
||||
### `castlingTargets` method
|
||||
|
||||
```scala
|
||||
def castlingTargets(ctx: GameContext, color: Color): Set[Square]
|
||||
```
|
||||
|
||||
For each side (kingside, queenside), checks all six conditions in order (failing fast):
|
||||
|
||||
1. `CastlingRights` flag is `true` for that side (`ctx.castlingFor(color)`)
|
||||
2. King is on its home square (e1 for White, e8 for Black)
|
||||
3. Relevant rook is on its home square (h-file for kingside, a-file for queenside)
|
||||
4. All squares between king and rook are empty
|
||||
5. King is **not currently in check** — calls `GameRules.isInCheck(ctx.board, color)` using the board-only path (no castling recursion)
|
||||
6. Each square the king **passes through and lands on** is not attacked — checks that no enemy `legalTargets(board, enemySq)` (board-only) covers those squares
|
||||
|
||||
Transit and landing squares:
|
||||
- **Kingside:** f-file, g-file (White: f1, g1; Black: f8, g8)
|
||||
- **Queenside:** d-file, c-file (White: d1, c1; Black: d8, c8). Note: b1/b8 must be empty (condition 4) but the king does not pass through them, so they are not checked for attacks.
|
||||
|
||||
---
|
||||
|
||||
## Section 4 — `GameRules` Changes
|
||||
|
||||
`GameRules.legalMoves` must accept `GameContext` (not just `Board`) so it can enumerate castling moves as part of the legal move set. This is required for correct stalemate and checkmate detection — a position where the only legal move is to castle must not be evaluated as stalemate.
|
||||
|
||||
```scala
|
||||
def legalMoves(ctx: GameContext, color: Color): Set[(Square, Square)]
|
||||
```
|
||||
|
||||
Internally it calls `MoveValidator.legalTargets(ctx, from)` (the context-aware overload) for all pieces of `color`, then filters to moves that do not leave the king in check.
|
||||
|
||||
`isInCheck` retains its `(board: Board, color: Color)` signature — it does not need castling context.
|
||||
|
||||
`gameStatus` is updated to accept `GameContext`:
|
||||
|
||||
```scala
|
||||
def gameStatus(ctx: GameContext, color: Color): PositionStatus
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Section 5 — `GameController` Changes
|
||||
|
||||
### Move detection and execution
|
||||
|
||||
`processMove` identifies a castling move by the king occupying its home square and moving exactly two files laterally:
|
||||
- White: e1→g1 (kingside) or e1→c1 (queenside)
|
||||
- Black: e8→g8 (kingside) or e8→c8 (queenside)
|
||||
|
||||
Legality is confirmed via `MoveValidator.isLegal(ctx, from, to)` (the context-aware overload, which includes castling targets). When a castling move is legal and executed:
|
||||
1. Call `ctx.board.withCastle(color, side)` to move both pieces atomically.
|
||||
2. Revoke **both** castling rights for the moving color in the new `GameContext`.
|
||||
|
||||
### Rights revocation rules (applied on every move)
|
||||
|
||||
After every move `(from, to)` is applied, revoke rights based on both the **source square** and the **destination square**. Both tables are checked independently and all triggered revocations are applied.
|
||||
|
||||
**Source square → revocation** (piece leaves its home square):
|
||||
|
||||
| Source square | Rights revoked |
|
||||
|---------------|---------------|
|
||||
| `e1` | Both White castling rights |
|
||||
| `e8` | Both Black castling rights |
|
||||
| `a1` | White queenside |
|
||||
| `h1` | White kingside |
|
||||
| `a8` | Black queenside |
|
||||
| `h8` | Black kingside |
|
||||
|
||||
**Destination square → revocation** (a piece — including an enemy piece — arrives on a rook home square, meaning a capture removed the rook):
|
||||
|
||||
| Destination square | Rights revoked |
|
||||
|--------------------|---------------|
|
||||
| `a1` | White queenside |
|
||||
| `h1` | White kingside |
|
||||
| `a8` | Black queenside |
|
||||
| `h8` | Black kingside |
|
||||
|
||||
This covers the following cases:
|
||||
- **King normal move** — source square e1/e8 fires; both rights revoked.
|
||||
- **King castle move** — the castle-specific step 2 revokes both rights for the moving color. Additionally, the source-square table fires (king departs e1/e8), revoking the same rights a second time. This double-revocation is idempotent and harmless. The king's destination (g1/c1/g8/c8) does not appear in the destination table, so no extra revocation fires there.
|
||||
- **Own rook move** — source square a1/h1/a8/h8 fires.
|
||||
- **Enemy capture on a rook home square** — destination square a1/h1/a8/h8 fires, revoking the side that lost the rook.
|
||||
|
||||
`processMove` also calls `GameRules.gameStatus(newCtx, turn.opposite)` — note this call passes the full `GameContext`, not just a `Board`, because `gameStatus` now accepts `GameContext`.
|
||||
|
||||
The revocation is applied to the `GameContext` that results from the move, before it is returned in `MoveResult`.
|
||||
|
||||
### Signatures
|
||||
|
||||
```scala
|
||||
def processMove(ctx: GameContext, turn: Color, raw: String): MoveResult
|
||||
def gameLoop(ctx: GameContext, turn: Color): Unit
|
||||
```
|
||||
|
||||
`MoveResult.Moved` and `MoveResult.MovedInCheck` carry `newCtx: GameContext` instead of `newBoard: Board`. All `gameLoop` pattern match arms are updated to use `newCtx`. The render call uses `newCtx.board`.
|
||||
|
||||
On checkmate/stalemate reset, `GameContext.initial` is used.
|
||||
|
||||
---
|
||||
|
||||
## Section 6 — Move Notation
|
||||
|
||||
The player types standard coordinate notation:
|
||||
- `e1g1` → White kingside castle
|
||||
- `e1c1` → White queenside castle
|
||||
- `e8g8` → Black kingside castle
|
||||
- `e8c8` → Black queenside castle
|
||||
|
||||
No parser changes required. The controller identifies castling by the king moving 2 files from the home square.
|
||||
|
||||
---
|
||||
|
||||
## Section 7 — Testing
|
||||
|
||||
### `MoveValidatorTest`
|
||||
- Castling target (g1) is returned when all kingside conditions are met (White)
|
||||
- Castling target (c1) is returned when all queenside conditions are met (White)
|
||||
- Castling targets returned for Black kingside (g8) and queenside (c8)
|
||||
- Castling blocked when transit square is occupied (piece between king and rook)
|
||||
- Castling blocked when king is in check (condition 5)
|
||||
- Castling blocked when **transit square** is attacked (e.g., f1 attacked for White kingside)
|
||||
- Castling blocked when **landing square** is attacked (e.g., g1 attacked for White kingside)
|
||||
- Castling blocked when `kingSide = false` in `CastlingRights`
|
||||
- Castling blocked when `queenSide = false` in `CastlingRights`
|
||||
- Castling blocked when relevant rook is not on its home square
|
||||
|
||||
### `GameControllerTest`
|
||||
- `processMove` with `e1g1` returns `Moved` with king on g1, rook on f1, and both White castling rights revoked in `newCtx`
|
||||
- `processMove` with `e1c1` returns `Moved` with king on c1, rook on d1, and both White castling rights revoked in `newCtx`
|
||||
- `processMove` castle attempt after king has moved returns `IllegalMove`
|
||||
- `processMove` castle attempt after rook has moved returns `IllegalMove`
|
||||
- Normal rook move from h1 revokes White kingside right in the returned `newCtx`
|
||||
- Normal king move from e1 revokes both White rights in the returned `newCtx`
|
||||
- Enemy capture on h1 (e.g., Black rook captures White rook on h1) revokes White kingside right in the returned `newCtx`
|
||||
|
||||
### `GameRulesTest`
|
||||
- `legalMoves` includes castling destinations when available
|
||||
- `legalMoves` excludes castling when king is in check
|
||||
- `gameStatus` returns `Normal` (not `Drawn`) when the only legal move available is to castle — verifying that the `GameContext` signature change correctly prevents a false stalemate
|
||||
|
||||
---
|
||||
|
||||
## Files to Create / Modify
|
||||
|
||||
| Action | File |
|
||||
|--------|------|
|
||||
| **Create** | `modules/core/src/main/scala/de/nowchess/chess/logic/GameContext.scala` — includes `CastleSide` enum and `withCastle` Board extension |
|
||||
| **Modify** | `modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala` — add `castlingTargets`, board-only + context-aware `legalTargets`/`isLegal` overloads |
|
||||
| **Modify** | `modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala` — update `legalMoves` and `gameStatus` to accept `GameContext` |
|
||||
| **Modify** | `modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala` — use `GameContext`; castling detection, execution, rights revocation |
|
||||
| **Modify** | `modules/core/src/main/scala/de/nowchess/chess/Main.scala` — use `GameContext.initial` |
|
||||
| **Modify** | `modules/core/src/test/scala/de/nowchess/chess/logic/MoveValidatorTest.scala` — new castling tests |
|
||||
| **Modify** | `modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala` — update signatures + new castling tests |
|
||||
| **Modify** | `modules/core/src/test/scala/de/nowchess/chess/logic/GameRulesTest.scala` — update signatures + new castling tests |
|
||||
Reference in New Issue
Block a user