Implemented module rules as a microservice.
## Summary
- Adds Quarkus to the `rule` module, making it independently deployable as a standalone microservice on port 8081
- Exposes all `RuleSet` methods via a REST API at `/api/rules` (candidate moves, legal moves, check/checkmate/stalemate/draw detection, apply
move)
- Introduces DTOs and a `DtoMapper` for serializing/deserializing chess types (board, moves, game context) as flat JSON strings
- Adds `JacksonConfig` for Scala module registration and an `application.yml` for the rule service
- Includes Dockerfiles for JVM, legacy-jar, native, and native-micro targets
- Full test coverage: 17 `@QuarkusTest` HTTP-level tests + 29 `DtoMapper` unit tests (100% condition coverage)
## Test plan
- [ ] `./compile` — all modules build successfully
- [ ] `./test` — all tests pass (rule module: 107 tests total)
- [ ] `./coverage` — 100% condition coverage in `rule` module
- [ ] Rule service runs standalone: `./gradlew :modules:rule:quarkusDev` starts on port 8081
- [ ] `GET /api/rules/candidate-moves` returns valid moves for initial position
- [ ] `GET /api/rules/is-check` returns `false` for initial position
Co-authored-by: LQ63 <lkhermann@web.de>
Co-authored-by: Janis <janis-e@gmx.de>
Reviewed-on: #36
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
Summary
- Added fastparse_3:3.0.2 dependency to modules/io
- Implemented FenParserFastParse as a second alternative FEN parser using FastParse, with the same public API as
FenParser and FenParserCombinators
- Parsers are built bottom-up using (using P[Any]) Scala 3 syntax with NoWhitespace.* to prevent implicit whitespace
skipping; rank sum validation uses Pass/Fail inside .flatMap
- Added FenParserFastParseTest mirroring FenParserCombinatorsTest to prove behavioural equivalence across all three
implementations
Test plan
- All existing tests pass — FenParser, FenParserCombinators, and all other modules untouched
- FenParserFastParseTest covers all cases: valid FEN, invalid color, invalid castling, invalid board shapes, en
passant, rank overflow, round-trip via FenExporter
- All parser logic branches genuinely covered — known scoverage gap documented in docs/unresolved.md (FastParse inline
macro generates synthetic proxy methods that scoverage instruments but that never execute at runtime)
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #22
Reviewed-by: Janis <janis-e@gmx.de>
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
Summary
- Added scala-parser-combinators_3:2.4.0 dependency to modules/io
- Implemented FenParserCombinators as an alternative FEN parser using RegexParsers, with the same public API as the
existing FenParser
- Parsers are built bottom-up: piece characters → rank tokens → rank → board, composed with explicit field separators
into a full FEN parser
- Added FenParserCombinatorsTest mirroring the existing FenParserTest to prove behavioural equivalence
Test plan
- All existing tests pass — FenParser and all other modules untouched
- FenParserCombinatorsTest covers all cases: valid FEN, invalid color, invalid castling, invalid board shapes, en
passant, round-trip via FenExporter
- 100% line/branch/method coverage on FenParserCombinators
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #21
Reviewed-by: Janis <janis-e@gmx.de>
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
Summary
- Curried candidateMoves, legalMoves, and applyMove in the RuleSet trait to separate (context) as the world being
operated on from the computation parameter
- Updated DefaultRules overrides and all internal call sites
- Updated all external call sites: GameEngine, PgnParser, PgnExporter, ChessBoardView, and all affected tests
Test plan
- All existing tests pass (./gradlew build)
- No behaviour changes — pure style refactoring, existing test suite is the regression guard
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #18
Reviewed-by: Janis <janis-e@gmx.de>
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
Summary
- Implements the FIDE 50-move draw rule: a player may claim a draw if no pawn move or capture has occurred in the last
50 full moves (100 half-moves)
- Draw is not automatic — the eligible player must claim it via a TUI menu shown at the start of their turn
- halfMoveClock: Int is threaded through processMove and gameLoop; resets on pawn move, capture, or en passant;
increments on all other moves
Changes
- GameController.scala: extended MoveResult.Moved and MoveResult.MovedInCheck with newHalfMoveClock: Int; added
MoveResult.DrawClaimed; added halfMoveClock parameter to processMove and gameLoop; TUI menu shown when clock ≥ 100
- Main.scala: initial gameLoop call passes halfMoveClock = 0
- GameControllerTest.scala: updated all existing pattern matches; added 10 new tests covering clock reset, clock
increment, draw claim, and TUI menu behaviour
Test plan
- processMove: 'draw' with halfMoveClock = 100 → DrawClaimed
- processMove: 'draw' with halfMoveClock = 99 → InvalidFormat
- Pawn move / capture / en passant → clock resets to 0
- Quiet piece move → clock increments by 1
- MovedInCheck carries updated clock
- TUI menu appears when clock ≥ 100; option 1 claims draw, option 2 continues
- No TUI menu when clock < 100
- All 197 tests passing
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #9
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
- Add EnPassantCalculator to derive the en passant target square from GameHistory, detect en passant captures, and
compute the captured pawn's square
- Extend MoveValidator.legalTargets to include the en passant diagonal square in pawn legal targets
- Extend GameController.processMove to remove the captured pawn from the board when an en passant capture is played
Details
En passant is derived purely from the last HistoryMove — no new state is introduced. If the last move was a double
pawn push, the target square is the square the pawn passed through. The board mutation follows the same pattern as
castling: board.withMove moves the capturing pawn, then board.removed removes the captured pawn from its actual square
(which differs from the destination square).
Test Plan
- EnPassantCalculatorTest — 14 unit tests covering target derivation, captured square calculation, and capture
detection for both colors
- MoveValidatorTest — 5 new tests: ep target included/excluded based on history, adjacency filter, both colors, case _
branch coverage
- GameControllerTest — 2 integration tests: white and black en passant capture removes pawn from board and returns
correct captured piece
- 100% scoverage (line/branch/method) confirmed
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #8
Reviewed-by: Janis <janis-e@gmx.de>
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>