Compare commits
3 Commits
core-0.11.0
...
ui-0.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b4bc72f7e4 | |||
| 8959c3a849 | |||
| 47032378e2 |
@@ -1,9 +0,0 @@
|
||||
# Memory Index
|
||||
|
||||
## Feedback
|
||||
- [feedback_keep_structure_updated.md](feedback_keep_structure_updated.md) — Update structure memory files whenever source files are added, removed, or changed
|
||||
|
||||
## Project Structure
|
||||
- [project_structure_root.md](project_structure_root.md) — Top-level layout, modules list, VERSIONS map, navigation rules (skip `build/`, `.gradle/`, `.idea/`)
|
||||
- [project_structure_api.md](project_structure_api.md) — `modules/api`: all files and types (Board, Piece, Square, GameState, Move, ApiResponse, PlayerInfo)
|
||||
- [project_structure_core.md](project_structure_core.md) — `modules/core`: all files and types (GameContext, GameRules, MoveValidator, GameController, Parser, Renderer)
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: keep-structure-memory-updated
|
||||
description: Always update the project structure memory files when adding, removing, or changing source files
|
||||
type: feedback
|
||||
---
|
||||
|
||||
After any change that adds, removes, renames, or significantly alters a source file, update the relevant structure memory file:
|
||||
|
||||
- New/renamed/deleted file in `modules/api` → update `project_structure_api.md`
|
||||
- New/renamed/deleted file in `modules/core` → update `project_structure_core.md`
|
||||
- New module, dependency version change, or new top-level directory → update `project_structure_root.md`
|
||||
- New module added → create a new `project_structure_<module>.md` and add it to `MEMORY.md`
|
||||
|
||||
**Why:** Structure memories are the primary navigation aid. Stale entries cause wasted exploration.
|
||||
|
||||
**How to apply:** Treat the structure memory update as part of completing any implementation task — do it in the same session, not as a follow-up.
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
name: module-api-structure
|
||||
description: File and type overview for the modules/api module (shared domain types)
|
||||
type: project
|
||||
---
|
||||
|
||||
# Module: `modules/api`
|
||||
|
||||
**Purpose:** Shared domain model — pure data types with no game logic. Depended on by `modules/core`.
|
||||
|
||||
**Gradle:** `id("scala")`, no `application` plugin. No Quarkus. Uses scoverage plugin.
|
||||
|
||||
**Package root:** `de.nowchess.api`
|
||||
|
||||
## Source files (`src/main/scala/de/nowchess/api/`)
|
||||
|
||||
### `board/`
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `Board.scala` | `opaque type Board = Map[Square, Piece]` — extensions: `pieceAt`, `withMove`, `pieces`; `Board.initial` sets up start position |
|
||||
| `Color.scala` | `enum Color { White, Black }` — `.opposite`, `.label` |
|
||||
| `Piece.scala` | `case class Piece(color, pieceType)` — convenience vals `WhitePawn`…`BlackKing` |
|
||||
| `PieceType.scala` | `enum PieceType { Pawn, Knight, Bishop, Rook, Queen, King }` — `.label` |
|
||||
| `Square.scala` | `enum File { A–H }`, `enum Rank { R1–R8 }`, `case class Square(file, rank)` — `.toString` algebraic, `Square.fromAlgebraic(s)` |
|
||||
|
||||
### `game/`
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `GameState.scala` | `case class CastlingRights(kingSide, queenSide)` + `.None`/`.Both`; `enum GameResult { WhiteWins, BlackWins, Draw }`; `enum GameStatus { NotStarted, InProgress, Finished(result) }`; `case class GameState(piecePlacement, activeColor, castlingWhite, castlingBlack, enPassantTarget, halfMoveClock, fullMoveNumber, status)` — FEN-compatible snapshot |
|
||||
|
||||
### `move/`
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `Move.scala` | `enum PromotionPiece { Knight, Bishop, Rook, Queen }`; `enum MoveType { Normal, CastleKingside, CastleQueenside, EnPassant, Promotion(piece) }`; `case class Move(from, to, moveType = Normal)` |
|
||||
|
||||
### `player/`
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `PlayerInfo.scala` | `opaque type PlayerId = String`; `case class PlayerInfo(id: PlayerId, displayName: String)` |
|
||||
|
||||
### `response/`
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `ApiResponse.scala` | `sealed trait ApiResponse[+A]` → `Success[A](data)` / `Failure(errors)`; `case class ApiError(code, message, field?)`; `case class Pagination(page, pageSize, totalItems)` + `.totalPages`; `case class PagedResponse[A](items, pagination)` |
|
||||
|
||||
## Test files (`src/test/scala/de/nowchess/api/`)
|
||||
Mirror of main structure — one `*Test.scala` per source file using `AnyFunSuite with Matchers`.
|
||||
|
||||
## Notes
|
||||
- `GameState` is FEN-style but `Board` (in `core`) is a `Map[Square,Piece]` — the two are separate representations
|
||||
- `CastlingRights` is defined here in `api`; the castling logic lives in `core`
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: module-core-structure
|
||||
description: File and type overview for the modules/core module (TUI chess engine)
|
||||
type: project
|
||||
---
|
||||
|
||||
# Module: `modules/core`
|
||||
|
||||
**Purpose:** Standalone TUI chess application. All game logic, move validation, rendering. Depends on `modules/api`.
|
||||
|
||||
**Gradle:** `id("scala")` + `application` plugin. Main class: `de.nowchess.chess.Main`. Uses scoverage plugin.
|
||||
|
||||
**Package root:** `de.nowchess.chess`
|
||||
|
||||
## Source files (`src/main/scala/de/nowchess/chess/`)
|
||||
|
||||
### Root
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `Main.scala` | Entry point — prints welcome, starts `GameController.gameLoop(GameContext.initial, Color.White)` |
|
||||
|
||||
### `controller/`
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `GameController.scala` | `sealed trait MoveResult` ADT: `Quit`, `InvalidFormat`, `NoPiece`, `WrongColor`, `IllegalMove`, `Moved`, `MovedInCheck`, `Checkmate`, `Stalemate`; `object GameController` — `processMove(ctx, turn, raw): MoveResult` (pure), `gameLoop(ctx, turn)` (I/O loop), `applyRightsRevocation(...)` (castling rights bookkeeping) |
|
||||
| `Parser.scala` | `object Parser` — `parseMove(input): Option[(Square, Square)]` parses coordinate notation e.g. `"e2e4"` |
|
||||
|
||||
### `logic/`
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `GameContext.scala` | `enum CastleSide { Kingside, Queenside }`; `case class GameContext(board, whiteCastling, blackCastling)` — `.castlingFor(color)`, `.withUpdatedRights(color, rights)`; `GameContext.initial`; `extension (Board).withCastle(color, side)` moves king+rook atomically |
|
||||
| `GameRules.scala` | `enum PositionStatus { Normal, InCheck, Mated, Drawn }`; `object GameRules` — `isInCheck(board, color)`, `legalMoves(ctx, color): Set[(Square,Square)]`, `gameStatus(ctx, color): PositionStatus` |
|
||||
| `MoveValidator.scala` | `object MoveValidator` — `isLegal(board, from, to)`, `legalTargets(board, from): Set[Square]` (board-only, no castling), `legalTargets(ctx, from)` (context-aware, includes castling), `isCastle`, `castleSide`, `castlingTargets(ctx, color)` — full castling legality (empty squares, no check through transit) |
|
||||
|
||||
### `view/`
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `Renderer.scala` | `object Renderer` — `render(board): String` outputs ANSI-colored board with file/rank labels |
|
||||
| `PieceUnicode.scala` | `extension (Piece).unicode: String` maps each piece to its Unicode chess symbol |
|
||||
|
||||
## Test files (`src/test/scala/de/nowchess/chess/`)
|
||||
Mirror of main structure — one `*Test.scala` per source file using `AnyFunSuite with Matchers`.
|
||||
|
||||
## Key design notes
|
||||
- `MoveValidator` has two overloaded `legalTargets`: one takes `Board` (geometry only), one takes `GameContext` (adds castling)
|
||||
- `GameRules.legalMoves` filters by check — it calls `MoveValidator.legalTargets(ctx, from)` then simulates each move
|
||||
- Castling rights revocation is in `GameController.applyRightsRevocation`, triggered after every move
|
||||
- No `@QuarkusTest` — this module is a plain Scala application, not a Quarkus service
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
name: project-root-structure
|
||||
description: Top-level project structure, modules list, and navigation notes for NowChessSystems
|
||||
type: project
|
||||
---
|
||||
|
||||
# NowChessSystems — Root Structure
|
||||
|
||||
## Directory layout (skip `build/`, `.gradle/`, `.idea/`)
|
||||
|
||||
```
|
||||
NowChessSystems/
|
||||
├── build.gradle.kts # Root: sonarqube plugin, VERSIONS map
|
||||
├── settings.gradle.kts # include(":modules:core", ":modules:api")
|
||||
├── gradlew / gradlew.bat
|
||||
├── CLAUDE.md # Project instructions for Claude Code
|
||||
├── .claude/
|
||||
│ ├── CLAUDE.MD # Working agreement (plan/verify/unresolved)
|
||||
│ ├── settings.json
|
||||
│ └── agents/ # architect, code-reviewer, gradle-builder, scala-implementer, test-writer
|
||||
├── docs/
|
||||
│ ├── Claude-Skills.md
|
||||
│ ├── Security.md
|
||||
│ └── unresolved.md
|
||||
├── jacoco-reporter/ # Python scripts for coverage gap reporting
|
||||
└── modules/
|
||||
├── api/ # Shared domain types (no logic)
|
||||
└── core/ # TUI chess engine + game logic
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Gradle path | Purpose |
|
||||
|--------|-------------|---------|
|
||||
| `api` | `:modules:api` | Shared domain model: Board, Piece, Move, GameState, ApiResponse |
|
||||
| `core` | `:modules:core` | TUI chess app: game logic, move validation, rendering |
|
||||
|
||||
`core` depends on `api` via `implementation(project(":modules:api"))`.
|
||||
|
||||
## VERSIONS (root `build.gradle.kts`)
|
||||
|
||||
| Key | Value |
|
||||
|-----|-------|
|
||||
| `QUARKUS_SCALA3` | 1.0.0 |
|
||||
| `SCALA3` | 3.5.1 |
|
||||
| `SCALA_LIBRARY` | 2.13.18 |
|
||||
| `SCALATEST` | 3.2.19 |
|
||||
| `SCALATEST_JUNIT` | 0.1.11 |
|
||||
| `SCOVERAGE` | 2.1.1 |
|
||||
|
||||
## Navigation rules
|
||||
- **Always skip** `build/`, `.gradle/`, `.idea/` when exploring — they are generated artifacts
|
||||
- Tests use `AnyFunSuite with Matchers` (ScalaTest), not JUnit `@Test`
|
||||
- No Quarkus in current modules — Quarkus is planned for future services
|
||||
- Agent workflow: architect → scala-implementer → test-writer → gradle-builder → code-reviewer
|
||||
@@ -1,58 +1,50 @@
|
||||
# CLAUDE.md — NowChessSystems
|
||||
# Now-Chess
|
||||
|
||||
## Stack
|
||||
Scala 3.5.x · Quarkus + quarkus-scala3 · Hibernate/Jakarta · Lanterna TUI · K8s + ArgoCD + Kargo · Frontend TBD (Vite/React/Angular/Vue)
|
||||
|
||||
### Memory
|
||||
|
||||
Your memory is saved under .claude/memory/MEMORY.md.
|
||||
|
||||
## Structure
|
||||
```
|
||||
build.gradle.kts / settings.gradle.kts # root; include(":modules:<svc>") per service
|
||||
modules/<svc>/build.gradle.kts + src/
|
||||
docs/adr/ docs/api/ docs/unresolved.md
|
||||
```
|
||||
Versions in root `extra["VERSIONS"]`; modules read via `rootProject.extra["VERSIONS"] as Map<String,String>`.
|
||||
Scala 3.5.1 · Gradle 9
|
||||
|
||||
## Commands
|
||||
```bash
|
||||
./gradlew build
|
||||
./gradlew :modules:<svc>:build|test
|
||||
./gradlew :modules:<svc>:test --tests "de.nowchess.<svc>.<Class>"
|
||||
|
||||
```
|
||||
|
||||
## Workflow
|
||||
1. **Plan** — restate requirement, list files, flag risks. Proceed unless genuine ambiguity.
|
||||
2. **Tests first** — cover only new behaviour.
|
||||
3. **Implement** — no scope creep.
|
||||
4. **Verify** — check each requirement; confirm green build.
|
||||
|
||||
## Scala/Quarkus Rules
|
||||
- `given`/`using` only (no `implicit`); `Option`/`Either`/`Try` (no `null`/`.get`)
|
||||
- `jakarta.*` only; reactive I/O (`Uni`/`Multi`), no blocking on event loop
|
||||
- Always exclude `org.scala-lang:scala-library` from Quarkus BOM
|
||||
- Unit tests: `extends AnyFunSuite with Matchers` — no `@Test`, no `: Unit`
|
||||
- Integration tests: `@QuarkusTest` + JUnit 5 — `@Test` methods need explicit `: Unit`
|
||||
|
||||
## Coverage
|
||||
Line = 100% · Branch = 100% · Method = 100% · Regression tests · document exceptions
|
||||
Check: `jacoco-reporter/scoverage_coverage_gaps.py modules/{svc}/build/reports/scoverageTest/scoverage.xml`
|
||||
⚠️ Use `scoverageTest/`, NOT `scoverage/`.
|
||||
|
||||
## Bug Fixing
|
||||
Fix failures immediately without asking. After 3 failed attempts → log in `docs/unresolved.md` + surface summary.
|
||||
|
||||
## Agents (new service)
|
||||
Sequential: architect → scala-implementer → test-writer → gradle-builder → code-reviewer (review only, no self-fix)
|
||||
Parallel: only when services are fully independent (no shared contracts/state).
|
||||
|
||||
## Unresolved (`docs/unresolved.md`)
|
||||
Append only, never delete:
|
||||
```
|
||||
## [YYYY-MM-DD] <title>
|
||||
**Requirement/Bug:** **Root Cause:** **Attempted Fixes:** **Next Step:**
|
||||
./clean # Clear build dirs — only when necessary
|
||||
./compile # Compile all modules — always run
|
||||
./test # Run all tests
|
||||
./coverage # Check coverage
|
||||
```
|
||||
Try to stick to these commands for consistency.
|
||||
|
||||
## Done Checklist
|
||||
- [ ] Plan written · files created/modified · tests green · requirements verified · unresolved logged
|
||||
## Modules
|
||||
|
||||
| Module | Role | Depends on |
|
||||
|--------|------|-----------|
|
||||
| `api` | Model / shared types | (none) |
|
||||
| `core` | Primary business logic | api, rule |
|
||||
| `rule` | Game rules | api |
|
||||
| `io` | Export formats | api, core |
|
||||
| `ui` | Entrypoint & UI | core, io |
|
||||
|
||||
## Style
|
||||
|
||||
- Use immutable data and pure functions.
|
||||
- Keep functions under 30 lines. If you need "and" to describe it, split it.
|
||||
- Keep cyclomatic complexity under 15.
|
||||
- Avoid comments. Let names carry intent; comment only non-obvious algorithms.
|
||||
- Scan for duplicated logic before finishing. Extract it.
|
||||
- Follow default Sonar style for Scala.
|
||||
- Use `Option` or `Either` for fallible operations; avoid exceptions for control flow.
|
||||
- Naming: types are PascalCase, functions/values are camelCase.
|
||||
|
||||
## Code Quality
|
||||
|
||||
- **Coverage:** 100% condition coverage required in `api`, `core`, `rule`, `io` (mandatory); `ui` exempt.
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
- **Immutable state as primary model:** GameContext (api) holds board, history, player state — immutable, passed through the system. Each move creates a new GameContext, enabling undo/redo without side effects.
|
||||
- **Observer pattern for UI decoupling:** GameEngine publishes move/state events; CommandInvoker queues moves; UI listens to events, not polling. GameEngine never imports UI code.
|
||||
- **RuleSet trait encapsulates rules:** Move generation, check, castling, en passant all in RuleSet impl. GameEngine calls rules as a black box; rules don't know about the rest of core.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Tests are the spec.** Never modify tests to pass; modify requirements or code. Update tests only if requirements change.
|
||||
- Never read build folders. Ask permission if needed.
|
||||
- Keep this file up to date with any important decisions or conventions.
|
||||
+2
-1
@@ -32,7 +32,8 @@ val versions = mapOf(
|
||||
"SCOVERAGE" to "2.1.1",
|
||||
"SCALAFX" to "21.0.0-R32",
|
||||
"JAVAFX" to "21.0.1",
|
||||
"JUNIT_BOM" to "5.13.4"
|
||||
"JUNIT_BOM" to "5.13.4",
|
||||
"SCALA_PARSER_COMBINATORS" to "2.4.0"
|
||||
)
|
||||
extra["VERSIONS"] = versions
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
## [2026-03-31] Unreachable code blocking 100% statement coverage
|
||||
|
||||
**Requirement/Bug:** Reach 100% statement coverage in core module.
|
||||
|
||||
**Root Cause:** 4 remaining uncovered statements (99.6% coverage) are unreachable code:
|
||||
1. **PgnParser.scala:160** (`case _ => None` in extractPromotion) - Regex `=([QRBN])` only matches those 4 characters; fallback case can never execute
|
||||
2. **GameHistory.scala:29** (`addMove$default$4` compiler-generated method) - Method overload 3 without defaults shadows the 4-param version, making promotionPiece default accessor unreachable
|
||||
3. **GameEngine.scala:201-202** (`case _` in completePromotion) - GameController.completePromotion always returns one of 4 expected MoveResult types; catch-all is defensive code
|
||||
|
||||
**Attempted Fixes:**
|
||||
1. Added comprehensive PGN parsing tests (all 4 promotion types) - PgnParser improved from 95.8% to 99.4%
|
||||
2. Added GameHistory tests using named parameters - hit `addMove$default$3` (castleSide) but not `$default$4` (promotionPiece)
|
||||
3. Named parameter approach: `addMove(from=..., to=..., promotionPiece=...)` triggers 4-param with castleSide default ✓
|
||||
4. Positional approach: `addMove(f, t, None, None)` requires all 4 args (explicit, no defaults used) - doesn't hit $default$4
|
||||
5. Root issue: Scala's overload resolution prefers more-specific non-default overloads (2-param, 3-param) over the 4-param with defaults
|
||||
|
||||
**Recommendation:** 99.6% (1029/1033) is maximum achievable without refactoring method overloads. Unreachable code design patterns:
|
||||
- **Pattern 1 (unreachable regex fallback):** Defensive pattern match against exhaustive regex
|
||||
- **Pattern 2 (overshadowed defaults):** Method overloads shadow default parameters in parent signature
|
||||
- **Pattern 3 (defensive catch-all):** Error handling for impossible external API returns
|
||||
|
||||
@@ -16,3 +16,8 @@
|
||||
### Features
|
||||
|
||||
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
|
||||
## (2026-04-07)
|
||||
|
||||
### Features
|
||||
|
||||
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=2
|
||||
MINOR=3
|
||||
PATCH=0
|
||||
|
||||
@@ -188,3 +188,26 @@
|
||||
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
|
||||
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
|
||||
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
|
||||
## (2026-04-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
|
||||
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
|
||||
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
|
||||
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
|
||||
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
|
||||
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
|
||||
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
|
||||
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
|
||||
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
|
||||
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
|
||||
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
|
||||
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
|
||||
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
|
||||
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
|
||||
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=10
|
||||
MINOR=11
|
||||
PATCH=0
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
## (2026-04-06)
|
||||
## (2026-04-07)
|
||||
|
||||
@@ -38,6 +38,8 @@ dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
implementation("org.scala-lang.modules:scala-parser-combinators_3:${versions["SCALA_PARSER_COMBINATORS"]!!}")
|
||||
|
||||
implementation(project(":modules:api"))
|
||||
implementation(project(":modules:rule"))
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package de.nowchess.io.fen
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.io.GameContextImport
|
||||
import scala.util.parsing.combinator.RegexParsers
|
||||
|
||||
object FenParserCombinators extends RegexParsers with GameContextImport:
|
||||
|
||||
override val skipWhitespace: Boolean = false
|
||||
|
||||
// ── Piece character ──────────────────────────────────────────────────────
|
||||
|
||||
private val charToPieceType: Map[Char, PieceType] = Map(
|
||||
'p' -> PieceType.Pawn,
|
||||
'r' -> PieceType.Rook,
|
||||
'n' -> PieceType.Knight,
|
||||
'b' -> PieceType.Bishop,
|
||||
'q' -> PieceType.Queen,
|
||||
'k' -> PieceType.King
|
||||
)
|
||||
|
||||
private def pieceChar: Parser[Piece] =
|
||||
"[prnbqkPRNBQK]".r ^^ { s =>
|
||||
val c = s.head
|
||||
val color = if c.isUpper then Color.White else Color.Black
|
||||
Piece(color, charToPieceType(c.toLower))
|
||||
}
|
||||
|
||||
private def emptyCount: Parser[Int] =
|
||||
"[1-8]".r ^^ { s => s.toInt }
|
||||
|
||||
// ── Rank parser ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Parse a sequence of piece-chars and empty-counts, returning tagged tokens. */
|
||||
private sealed trait RankToken
|
||||
private case class PieceToken(piece: Piece) extends RankToken
|
||||
private case class EmptyToken(count: Int) extends RankToken
|
||||
|
||||
private def rankToken: Parser[RankToken] =
|
||||
pieceChar ^^ PieceToken.apply | emptyCount ^^ EmptyToken.apply
|
||||
|
||||
private def rankTokens: Parser[List[RankToken]] = rep1(rankToken)
|
||||
|
||||
/** Parse rank string for a given Rank, producing (Square, Piece) pairs.
|
||||
* Fails if total file count != 8 or any piece placement exceeds board bounds. */
|
||||
private def rankParser(rank: Rank): Parser[List[(Square, Piece)]] =
|
||||
rankTokens >> { tokens =>
|
||||
val result = tokens.foldLeft(Option((List.empty[(Square, Piece)], 0))):
|
||||
case (None, _) => None
|
||||
case (Some((acc, fileIdx)), PieceToken(piece)) =>
|
||||
if fileIdx > 7 then None
|
||||
else
|
||||
val sq = Square(File.values(fileIdx), rank)
|
||||
Some((acc :+ (sq -> piece), fileIdx + 1))
|
||||
case (Some((acc, fileIdx)), EmptyToken(n)) =>
|
||||
val next = fileIdx + n
|
||||
if next > 8 then None
|
||||
else Some((acc, next))
|
||||
result match
|
||||
case Some((squares, 8)) => success(squares)
|
||||
case Some((_, total)) => failure(s"Rank $rank has $total files, expected 8")
|
||||
case None => failure(s"Rank $rank exceeds board width")
|
||||
}
|
||||
|
||||
// ── Board parser ─────────────────────────────────────────────────────────
|
||||
|
||||
private def rankSep: Parser[String] = "/"
|
||||
|
||||
/** Parse all 8 rank strings separated by '/', rank 8 down to rank 1. */
|
||||
private def boardParser: Parser[Board] =
|
||||
rankParser(Rank.R8) ~
|
||||
(rankSep ~> rankParser(Rank.R7)) ~
|
||||
(rankSep ~> rankParser(Rank.R6)) ~
|
||||
(rankSep ~> rankParser(Rank.R5)) ~
|
||||
(rankSep ~> rankParser(Rank.R4)) ~
|
||||
(rankSep ~> rankParser(Rank.R3)) ~
|
||||
(rankSep ~> rankParser(Rank.R2)) ~
|
||||
(rankSep ~> rankParser(Rank.R1)) ^^ {
|
||||
case r8 ~ r7 ~ r6 ~ r5 ~ r4 ~ r3 ~ r2 ~ r1 =>
|
||||
Board((r8 ++ r7 ++ r6 ++ r5 ++ r4 ++ r3 ++ r2 ++ r1).toMap)
|
||||
}
|
||||
|
||||
// ── Color parser ─────────────────────────────────────────────────────────
|
||||
|
||||
private def colorParser: Parser[Color] =
|
||||
("w" | "b") ^^ {
|
||||
case "w" => Color.White
|
||||
case _ => Color.Black
|
||||
}
|
||||
|
||||
// ── Castling parser ──────────────────────────────────────────────────────
|
||||
|
||||
private def castlingParser: Parser[CastlingRights] =
|
||||
"-" ^^^ CastlingRights.None |
|
||||
"[KQkq]{1,4}".r ^^ { s =>
|
||||
CastlingRights(
|
||||
whiteKingSide = s.contains('K'),
|
||||
whiteQueenSide = s.contains('Q'),
|
||||
blackKingSide = s.contains('k'),
|
||||
blackQueenSide = s.contains('q')
|
||||
)
|
||||
}
|
||||
|
||||
// ── En passant parser ────────────────────────────────────────────────────
|
||||
|
||||
private def enPassantParser: Parser[Option[Square]] =
|
||||
"-" ^^^ Option.empty[Square] |
|
||||
"[a-h][1-8]".r ^^ { s => Square.fromAlgebraic(s) }
|
||||
|
||||
// ── Clock parser ─────────────────────────────────────────────────────────
|
||||
|
||||
private def clockParser: Parser[Int] =
|
||||
"""\d+""".r ^^ { _.toInt }
|
||||
|
||||
// ── Full FEN parser ──────────────────────────────────────────────────────
|
||||
|
||||
private def fenParser: Parser[GameContext] =
|
||||
boardParser ~ (" " ~> colorParser) ~ (" " ~> castlingParser) ~
|
||||
(" " ~> enPassantParser) ~ (" " ~> clockParser) ~ (" " ~> clockParser) ^^ {
|
||||
case board ~ color ~ castling ~ ep ~ halfMove ~ _ =>
|
||||
GameContext(
|
||||
board = board,
|
||||
turn = color,
|
||||
castlingRights = castling,
|
||||
enPassantSquare = ep,
|
||||
halfMoveClock = halfMove,
|
||||
moves = List.empty
|
||||
)
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
def parseFen(fen: String): Either[String, GameContext] =
|
||||
parseAll(fenParser, fen) match
|
||||
case Success(ctx, _) => Right(ctx)
|
||||
case other => Left(s"Invalid FEN: ${other.toString}")
|
||||
|
||||
def parseBoard(fen: String): Option[Board] =
|
||||
parseAll(boardParser, fen) match
|
||||
case Success(board, _) => Some(board)
|
||||
case _ => None
|
||||
|
||||
def importGameContext(input: String): Either[String, GameContext] =
|
||||
parseFen(input)
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.nowchess.io.fen
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class FenParserCombinatorsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("parseBoard parses canonical positions and supports round-trip"):
|
||||
val initial = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
||||
val empty = "8/8/8/8/8/8/8/8"
|
||||
val partial = "8/8/4k3/8/4K3/8/8/8"
|
||||
|
||||
FenParserCombinators.parseBoard(initial).map(_.pieceAt(Square(File.E, Rank.R2))) shouldBe Some(Some(Piece.WhitePawn))
|
||||
FenParserCombinators.parseBoard(initial).map(_.pieceAt(Square(File.E, Rank.R8))) shouldBe Some(Some(Piece.BlackKing))
|
||||
FenParserCombinators.parseBoard(empty).map(_.pieces.size) shouldBe Some(0)
|
||||
FenParserCombinators.parseBoard(partial).map(_.pieceAt(Square(File.E, Rank.R6))) shouldBe Some(Some(Piece.BlackKing))
|
||||
|
||||
FenParserCombinators.parseBoard(initial).map(FenExporter.boardToFen) shouldBe Some(initial)
|
||||
FenParserCombinators.parseBoard(empty).map(FenExporter.boardToFen) shouldBe Some(empty)
|
||||
|
||||
test("parseFen parses full state for common valid inputs"):
|
||||
FenParserCombinators.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.turn shouldBe Color.White
|
||||
ctx.castlingRights.whiteKingSide shouldBe true
|
||||
ctx.enPassantSquare shouldBe None
|
||||
ctx.halfMoveClock shouldBe 0
|
||||
)
|
||||
|
||||
FenParserCombinators.parseFen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.turn shouldBe Color.Black
|
||||
ctx.enPassantSquare shouldBe Some(Square(File.E, Rank.R3))
|
||||
)
|
||||
|
||||
FenParserCombinators.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.castlingRights.whiteKingSide shouldBe false
|
||||
ctx.castlingRights.blackQueenSide shouldBe false
|
||||
)
|
||||
|
||||
test("parseFen rejects invalid color and castling tokens"):
|
||||
FenParserCombinators.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1").isLeft shouldBe true
|
||||
FenParserCombinators.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w XYZ - 0 1").isLeft shouldBe true
|
||||
|
||||
test("importGameContext returns Right for valid and Left for invalid FEN"):
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||
FenParserCombinators.importGameContext(fen).isRight shouldBe true
|
||||
FenParserCombinators.importGameContext("invalid fen string").isLeft shouldBe true
|
||||
|
||||
test("parseBoard rejects malformed board shapes and invalid piece symbols"):
|
||||
FenParserCombinators.parseBoard("8/8/8/8/8/8/8") shouldBe None
|
||||
FenParserCombinators.parseBoard("9/8/8/8/8/8/8/8") shouldBe None
|
||||
FenParserCombinators.parseBoard("8p/8/8/8/8/8/8/8") shouldBe None
|
||||
FenParserCombinators.parseBoard("7/8/8/8/8/8/8/8") shouldBe None
|
||||
FenParserCombinators.parseBoard("8/8/8/8/8/8/8/7X") shouldBe None
|
||||
|
||||
test("parseBoard rejects ranks that overflow via multiple tokens"):
|
||||
// EmptyToken overflow: piece then 8 empties = 9 total
|
||||
FenParserCombinators.parseBoard("p8/8/8/8/8/8/8/8") shouldBe None
|
||||
// fold short-circuit: 8 empties followed by two pieces = 10 total, exercises the None-propagation path
|
||||
FenParserCombinators.parseBoard("8pp/8/8/8/8/8/8/8") shouldBe None
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=0
|
||||
PATCH=1
|
||||
PATCH=2
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
## (2026-04-06)
|
||||
## (2026-04-07)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=0
|
||||
PATCH=1
|
||||
PATCH=2
|
||||
|
||||
@@ -33,3 +33,11 @@
|
||||
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
|
||||
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
|
||||
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
|
||||
## (2026-04-07)
|
||||
|
||||
### Features
|
||||
|
||||
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
|
||||
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
|
||||
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
|
||||
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=5
|
||||
MINOR=6
|
||||
PATCH=0
|
||||
|
||||
Reference in New Issue
Block a user