Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33e785d22a | |||
| d16cec176b | |||
| 8744bee2dd | |||
| 5f4d33f3ca | |||
| 767d3051a7 | |||
| b2e62dc60c | |||
| b0399a4e48 | |||
| ec2ab2f365 | |||
| fd4e67d4f7 | |||
| 3cb3160731 | |||
| dbcafd2869 | |||
| 3ecb2c9d66 | |||
| 9ad11fb97a | |||
| e158b0a7f0 | |||
| f1c9df16b6 | |||
| 9d11d25b99 | |||
| 7a045d31d7 | |||
| b518c704fa | |||
| fe8e3c0539 | |||
| 1b16adcc72 | |||
| b4bc72f7e4 | |||
| 8959c3a849 | |||
| 47032378e2 | |||
| 217f14f899 | |||
| 638139602c | |||
| 8f56a82104 | |||
| 51ffd7aac9 | |||
| 1b9eb471de | |||
| 45013c87a9 | |||
| 80518719d5 | |||
| 2d6ead7e47 | |||
| 3ff80318b4 | |||
| 9fb743d135 | |||
| 412ed986a9 | |||
| 8bbeead702 | |||
| e5e20c566e | |||
| 13bfc16cfe | |||
| 85cbf95c18 | |||
| 1361dfc895 | |||
| 707c4826a4 | |||
| 919beb3b4b | |||
| ee79dc5b98 | |||
| f28e69dc18 | |||
| 5f485fed9b | |||
| f4c18d22d7 | |||
| 4d800e88eb | |||
| 2df2fdeeb9 | |||
| 9190d1e5a0 | |||
| d675966436 | |||
| b4116e9a82 |
@@ -1,120 +0,0 @@
|
|||||||
# Claude Code – Working Agreement
|
|
||||||
|
|
||||||
## Workflow: Plan → Write Tests → Implement → Verify
|
|
||||||
|
|
||||||
### 1. Plan First
|
|
||||||
Before writing any code, produce an explicit plan:
|
|
||||||
- Restate the requirement in your own words to confirm understanding.
|
|
||||||
- List every file you intend to create or modify.
|
|
||||||
- Identify risks or unknowns upfront.
|
|
||||||
- Wait for confirmation **only** when the plan reveals an ambiguity that cannot be resolved from context. Otherwise proceed immediately.
|
|
||||||
|
|
||||||
### 2. Write Tests
|
|
||||||
Before implementing, write tests that should cover the new behaviour.
|
|
||||||
Only write tests for the new behaviour.
|
|
||||||
|
|
||||||
### 3. Implement
|
|
||||||
Follow the plan. Do not add scope beyond what was agreed.
|
|
||||||
|
|
||||||
### 4. Verify Every Requirement
|
|
||||||
After implementation, go through each requirement one-by-one and confirm it is satisfied:
|
|
||||||
- Run the relevant tests (unit, integration, or build check) for every changed module.
|
|
||||||
- If a requirement **cannot** be fulfilled, do **not** silently skip it — document it immediately (see *Unresolved Requirements* below).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## No Code Without Verification (Testing)
|
|
||||||
|
|
||||||
- Every new behaviour must be covered by at least one automated test before the task is considered done.
|
|
||||||
- Every bug fix must be accompanied by a regression test that fails before the fix and passes after.
|
|
||||||
- Run `./gradlew :modules:<module>:test` (or the appropriate Gradle task) and confirm a green build before marking work complete.
|
|
||||||
- If a test cannot be written for a legitimate reason, document it in `docs/unresolved.md` with an explanation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Automatic Bug Fixing
|
|
||||||
|
|
||||||
- When a test or build step fails, attempt to fix the root cause immediately — do **not** ask for permission.
|
|
||||||
- Apply the fix, re-run the verification, and continue until green.
|
|
||||||
- If the same failure persists after **three** fix attempts, stop, log the issue in `docs/unresolved.md`, and surface a concise summary.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Unresolved Requirements → `docs/unresolved.md`
|
|
||||||
|
|
||||||
When a requirement or bug cannot be resolved, append an entry to `docs/unresolved.md`:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## [YYYY-MM-DD] <Short title>
|
|
||||||
|
|
||||||
**Requirement / Bug:**
|
|
||||||
<What was requested or what failed>
|
|
||||||
|
|
||||||
**Root Cause (if known):**
|
|
||||||
<Why it cannot be resolved right now>
|
|
||||||
|
|
||||||
**Attempted Fixes:**
|
|
||||||
1. <What was tried>
|
|
||||||
2. …
|
|
||||||
|
|
||||||
**Suggested Next Step:**
|
|
||||||
<What a human engineer should investigate>
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the file if it does not exist. Never delete existing entries.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
. ← Repository root (multi-project Gradle setup)
|
|
||||||
├── build.gradle.kts ← Root build file (shared plugins, dependency versions)
|
|
||||||
├── settings.gradle.kts ← Gradle settings (declares all subprojects)
|
|
||||||
├── modules/ ← One subdirectory per microservice
|
|
||||||
│ └── <service>/
|
|
||||||
│ ├── build.gradle.kts
|
|
||||||
│ └── src/
|
|
||||||
└── docs/ ← Architecture Decision Records, API docs, unresolved issues
|
|
||||||
└── unresolved.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conventions
|
|
||||||
- All microservices live under `modules/{service-name}`. Never place service code in the root.
|
|
||||||
- Shared configuration (dependency versions, plugin setup) belongs in the **root** `build.gradle.kts` or in `buildSrc` / a version catalog.
|
|
||||||
- `settings.gradle.kts` must include every module via `include(":modules:<service>")`.
|
|
||||||
- Architecture decisions go in `docs/adr/` as numbered Markdown files (`ADR-001-<title>.md`).
|
|
||||||
- API contracts live in `/docs/api/`.
|
|
||||||
- Unit tests extend `AnyFunSuite with Matchers` — no `@Test` annotations, no `: Unit` requirement
|
|
||||||
- Integration tests use `@QuarkusTest` with JUnit 5 — `@Test` methods must be explicitly typed `: Unit`
|
|
||||||
- Always exclude scala-library from Quarkus deps to avoid Scala 2 conflicts
|
|
||||||
|
|
||||||
## Coverage Conventions
|
|
||||||
- Branch coverage must be at least 90% - unless there is a good reason not to.
|
|
||||||
- Line coverage must be at least 95% - unless there is a good reason not to.
|
|
||||||
- Method coverage must be at least 90% - unless there is a good reason not to.
|
|
||||||
- To check coverage use jacoco-reporter/scoverage_coverage_gaps.py modules/{service}/build/reports/scoverageTest/scoverage.xml
|
|
||||||
- IMPORTANT: modules/{service}/build/reports/scoverage/scoverage.xml is not used for coverage TEST calculation. Do not use it.
|
|
||||||
|
|
||||||
## Agent Routing Rules
|
|
||||||
|
|
||||||
### Use agents in PARALLEL when:
|
|
||||||
- Tasks touch different, independent microservices
|
|
||||||
- No shared files or state between tasks
|
|
||||||
- Example: "implement service-user AND service-orders simultaneously"
|
|
||||||
|
|
||||||
### Use agents SEQUENTIALLY when:
|
|
||||||
- Tasks have dependencies (architect → implementer → test-writer)
|
|
||||||
- Shared API contracts are involved
|
|
||||||
- Example: design API first, then implement, then test
|
|
||||||
|
|
||||||
## Quick-Reference Checklist
|
|
||||||
|
|
||||||
Before considering any task done, confirm:
|
|
||||||
|
|
||||||
- [ ] Plan was written and requirements restated
|
|
||||||
- [ ] All planned files were created / modified
|
|
||||||
- [ ] Automated tests cover the new behaviour
|
|
||||||
- [ ] `./gradlew build` (or scoped task) is green
|
|
||||||
- [ ] Each requirement has been explicitly verified
|
|
||||||
- [ ] Any unresolved items are logged in `docs/unresolved.md`
|
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
name: scala-implementer
|
name: scala-implementer
|
||||||
description: "Implements Scala 3 + Quarkus REST services, domain logic, and persistence"
|
description: "Implements Scala 3 + Quarkus REST services, domain logic, and persistence"
|
||||||
tools: Read, Write, Edit, Bash, Glob
|
tools: Read, Write, Edit, Bash, Glob
|
||||||
model: sonnet
|
model: inherit
|
||||||
color: pink
|
color: pink
|
||||||
---
|
---
|
||||||
|
|
||||||
You do not have permissions to write tests, just source code.
|
You do not have permissions to write tests, just source code.
|
||||||
You are a Scala 3 expert specialising in Quarkus microservices.
|
You are a Scala 3 expert specialising in Quarkus microservices.
|
||||||
Always read the relevant /docs/api/ file before implementing.
|
Always read the relevant /docs/api/ file before implementing.
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
name: test-writer
|
name: test-writer
|
||||||
description: "Writes QuarkusTest unit and integration tests for a service. Invoke after scala-implementer has finished."
|
description: "Writes QuarkusTest unit and integration tests for a service. Invoke after scala-implementer has finished."
|
||||||
tools: Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch, NotebookEdit
|
tools: Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch, NotebookEdit
|
||||||
model: sonnet
|
model: haiku
|
||||||
color: purple
|
color: purple
|
||||||
---
|
---
|
||||||
|
|
||||||
You do not have permissions to modify the source code, just write tests.
|
You do not have permissions to modify the source code, just write tests.
|
||||||
You write tests for Scala 3 + Quarkus services.
|
You write tests for Scala 3 + Quarkus services.
|
||||||
|
|
||||||
@@ -12,12 +13,11 @@ You write tests for Scala 3 + Quarkus services.
|
|||||||
- Unit tests: `extends AnyFunSuite with Matchers` — use `test("description") { ... }` DSL, no `@Test` annotation, no `: Unit` return type needed.
|
- Unit tests: `extends AnyFunSuite with Matchers` — use `test("description") { ... }` DSL, no `@Test` annotation, no `: Unit` return type needed.
|
||||||
- Integration tests: `@QuarkusTest` with JUnit 5 — `@Test` methods MUST be explicitly typed `: Unit`.
|
- Integration tests: `@QuarkusTest` with JUnit 5 — `@Test` methods MUST be explicitly typed `: Unit`.
|
||||||
|
|
||||||
Target 95%+ conditional coverage.
|
Target 100% conditional coverage if possible.
|
||||||
|
|
||||||
When invoked BEFORE scala-implementer (no implementation exists yet):
|
When invoked BEFORE scala-implementer (no implementation exists yet):
|
||||||
Use the contract-first-test-writing skill — write failing tests from docs/api/{service}.yaml.
|
Use the contract-first-test-writing skill — write failing tests from docs/api/{service}.yaml.
|
||||||
|
|
||||||
When invoked AFTER scala-implementer (implementation exists):
|
When invoked AFTER scala-implementer (implementation exists):
|
||||||
Run python3 jacoco-reporter/jacoco_coverage_gaps.py modules/{service-name}/build/reports/jacoco/test/jacocoTestReport.xml --output agent
|
Run python3 jacoco-reporter/jacoco_coverage_gaps.py modules/{service-name}/build/reports/jacoco/test/jacocoTestReport.xml --output agent
|
||||||
Use the jacoco-coverage-gaps skill — close coverage gaps revealed by the report.
|
|
||||||
To regenerate the report run the tests first.
|
To regenerate the report run the tests first.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"enabledPlugins": {
|
"enabledPlugins": {
|
||||||
"superpowers@claude-plugins-official": true,
|
"superpowers@claude-plugins-official": false,
|
||||||
"ui-ux-pro-max@ui-ux-pro-max-skill": true
|
"ui-ux-pro-max@ui-ux-pro-max-skill": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
# NowChessSystems — AI Context Map
|
||||||
|
|
||||||
|
> **Stack:** raw-http | none | unknown | scala
|
||||||
|
|
||||||
|
> 0 routes | 0 models | 0 components | 63 lib files | 1 env vars | 1 middleware
|
||||||
|
> **Token savings:** this file is ~0 tokens. Without it, AI exploration would cost ~0 tokens. **Saves ~0 tokens per conversation.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Libraries
|
||||||
|
|
||||||
|
- `jacoco-reporter/scoverage_coverage_gaps.py`
|
||||||
|
- function parse_scoverage_xml: (xml_path) -> tuple[dict, list[ClassGap]]
|
||||||
|
- function format_agent: (project_stats, classes) -> str
|
||||||
|
- function format_json: (project_stats, classes) -> str
|
||||||
|
- function format_markdown: (project_stats, classes) -> str
|
||||||
|
- function format_module_gaps: (module_name, classes, stmt_pct) -> str
|
||||||
|
- function run_scan_modules: (modules_dir, package_filter, min_coverage) -> None
|
||||||
|
- _...4 more_
|
||||||
|
- `jacoco-reporter/test_gaps.py`
|
||||||
|
- function parse_suite_xml: (xml_path) -> SuiteResult
|
||||||
|
- function load_module: (module_dir, results_subdir) -> Optional[ModuleResult]
|
||||||
|
- function format_module: (mod) -> str
|
||||||
|
- function run: (modules_dir, results_subdir, module_filter) -> None
|
||||||
|
- function main: () -> None
|
||||||
|
- class TestCase
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala`
|
||||||
|
- class Board
|
||||||
|
- function apply
|
||||||
|
- function pieceAt
|
||||||
|
- function updated
|
||||||
|
- function removed
|
||||||
|
- function withMove
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala`
|
||||||
|
- function hasAnyRights
|
||||||
|
- function hasRights
|
||||||
|
- function revokeColor
|
||||||
|
- function revokeKingSide
|
||||||
|
- function revokeQueenSide
|
||||||
|
- class CastlingRights
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — function opposite, function label
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — class Piece
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — function label
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala`
|
||||||
|
- class Square
|
||||||
|
- function fromAlgebraic
|
||||||
|
- function offset
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`
|
||||||
|
- function withBoard
|
||||||
|
- function withTurn
|
||||||
|
- function withCastlingRights
|
||||||
|
- function withEnPassantSquare
|
||||||
|
- function withHalfMoveClock
|
||||||
|
- function withMove
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — class PlayerId, function apply
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/response/ApiResponse.scala`
|
||||||
|
- class ApiResponse
|
||||||
|
- function error
|
||||||
|
- function totalPages
|
||||||
|
- `modules/bot/python/nnue.py`
|
||||||
|
- function get_weights_dir: ()
|
||||||
|
- function get_data_dir: ()
|
||||||
|
- function list_checkpoints: ()
|
||||||
|
- function migrate_legacy_data: ()
|
||||||
|
- function show_header: ()
|
||||||
|
- function show_checkpoints_table: ()
|
||||||
|
- _...10 more_
|
||||||
|
- `modules/bot/python/src/dataset.py`
|
||||||
|
- function get_datasets_dir: () -> Path
|
||||||
|
- function next_dataset_version: () -> int
|
||||||
|
- function list_datasets: () -> List[Tuple[int, Dict]]
|
||||||
|
- function load_dataset_metadata: (version) -> Optional[Dict]
|
||||||
|
- function save_dataset_metadata: (version, metadata) -> None
|
||||||
|
- function create_dataset: (version, labeled_jsonl_path, sources, stockfish_depth) -> Path
|
||||||
|
- _...4 more_
|
||||||
|
- `modules/bot/python/src/export.py` — function export_to_nbai: (weights_file, output_file, trained_by, train_loss)
|
||||||
|
- `modules/bot/python/src/generate.py` — function play_random_game_and_collect_positions: (output_file, total_positions, samples_per_game, min_move, max_move, num_workers)
|
||||||
|
- `modules/bot/python/src/label.py` — function normalize_evaluation: (cp_value, method, scale), function label_positions_with_stockfish: (positions_file, output_file, stockfish_path, batch_size, depth, verbose, normalize, num_workers)
|
||||||
|
- `modules/bot/python/src/tactical_positions_extractor.py`
|
||||||
|
- function download_and_extract_puzzle_db: (url, output_dir)
|
||||||
|
- function extract_puzzle_positions: (puzzle_csv, max_puzzles) -> Set[str]
|
||||||
|
- function load_positions_from_file: (file_path) -> Set[str]
|
||||||
|
- function merge_positions: (tactical, other, output_file)
|
||||||
|
- function extract_tactical_only: (puzzle_csv, output_file, max_puzzles) -> int
|
||||||
|
- function interactive_merge_positions: (puzzle_csv, output_file, max_puzzles)
|
||||||
|
- `modules/bot/python/src/train.py`
|
||||||
|
- function fen_to_features: (fen)
|
||||||
|
- function find_next_version: (base_name)
|
||||||
|
- function save_metadata: (weights_file, metadata)
|
||||||
|
- function train_nnue: (data_file, output_file, epochs, batch_size, lr, checkpoint, stockfish_depth, use_versioning, early_stopping_patience, weight_decay, subsample_ratio)
|
||||||
|
- function burst_train: (data_file, output_file, duration_minutes, epochs_per_season, early_stopping_patience, batch_size, lr, initial_checkpoint, stockfish_depth, use_versioning, weight_decay, subsample_ratio)
|
||||||
|
- class NNUEDataset
|
||||||
|
- _...1 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`
|
||||||
|
- class Bot
|
||||||
|
- function name
|
||||||
|
- function nextMove
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/BotController.scala`
|
||||||
|
- class BotController
|
||||||
|
- function getBot
|
||||||
|
- function listBots
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala`
|
||||||
|
- class BotMoveRepetition
|
||||||
|
- function blockedMoves
|
||||||
|
- function repeatedMove
|
||||||
|
- function filterAllowed
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/Config.scala` — class Config
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`
|
||||||
|
- class Evaluation
|
||||||
|
- class CHECKMATE_SCORE
|
||||||
|
- class DRAW_SCORE
|
||||||
|
- function evaluate
|
||||||
|
- function initAccumulator
|
||||||
|
- function copyAccumulator
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`
|
||||||
|
- class EvaluationClassic
|
||||||
|
- function evaluate
|
||||||
|
- function countRay
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/EvaluationNNUE.scala` — class EvaluationNNUE, function evaluate
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`
|
||||||
|
- class NNUE
|
||||||
|
- function initAccumulator
|
||||||
|
- function pushAccumulator
|
||||||
|
- function copyAccumulator
|
||||||
|
- function recomputeAccumulator
|
||||||
|
- function validateAccumulator
|
||||||
|
- _...4 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NbaiLoader.scala`
|
||||||
|
- class NbaiLoader
|
||||||
|
- function load
|
||||||
|
- function loadDefault
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NbaiMigrator.scala` — class NbaiMigrator, function migrateFromBin
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NbaiModel.scala`
|
||||||
|
- function toJson
|
||||||
|
- class NbaiMetadata
|
||||||
|
- function fromJson
|
||||||
|
- function str
|
||||||
|
- function num
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NbaiWriter.scala` — class NbaiWriter, function write
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`
|
||||||
|
- function bestMove
|
||||||
|
- function bestMove
|
||||||
|
- function bestMoveWithTime
|
||||||
|
- function bestMoveWithTime
|
||||||
|
- function loop
|
||||||
|
- function loop
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`
|
||||||
|
- class MoveOrdering
|
||||||
|
- class OrderingContext
|
||||||
|
- function addKillerMove
|
||||||
|
- function getKillerMoves
|
||||||
|
- function addHistory
|
||||||
|
- function getHistory
|
||||||
|
- _...3 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/TranspositionTable.scala`
|
||||||
|
- function probe
|
||||||
|
- function store
|
||||||
|
- function clear
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — function probe, function select
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` — class PolyglotHash, function hash
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`
|
||||||
|
- class ZobristHash
|
||||||
|
- function hash
|
||||||
|
- function nextHash
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`
|
||||||
|
- class Command
|
||||||
|
- function execute
|
||||||
|
- function undo
|
||||||
|
- function description
|
||||||
|
- class MoveResult
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/command/CommandInvoker.scala`
|
||||||
|
- class CommandInvoker
|
||||||
|
- function execute
|
||||||
|
- function undo
|
||||||
|
- function redo
|
||||||
|
- function history
|
||||||
|
- function getCurrentIndex
|
||||||
|
- _...3 more_
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/controller/Parser.scala` — class Parser, function parseMove
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`
|
||||||
|
- class GameEngine
|
||||||
|
- function isPendingPromotion
|
||||||
|
- function board
|
||||||
|
- function turn
|
||||||
|
- function context
|
||||||
|
- function canUndo
|
||||||
|
- _...11 more_
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`
|
||||||
|
- function context
|
||||||
|
- class Observer
|
||||||
|
- function onGameEvent
|
||||||
|
- class Observable
|
||||||
|
- function subscribe
|
||||||
|
- function unsubscribe
|
||||||
|
- _...1 more_
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — class GameContextExport, function exportGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — class GameContextImport, function importGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameFileService.scala`
|
||||||
|
- class GameFileService
|
||||||
|
- function saveGameToFile
|
||||||
|
- function loadGameFromFile
|
||||||
|
- class FileSystemGameService
|
||||||
|
- function saveGameToFile
|
||||||
|
- function loadGameFromFile
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala`
|
||||||
|
- class FenExporter
|
||||||
|
- function boardToFen
|
||||||
|
- function gameContextToFen
|
||||||
|
- function exportGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala`
|
||||||
|
- class FenParser
|
||||||
|
- function parseFen
|
||||||
|
- function importGameContext
|
||||||
|
- function parseBoard
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala`
|
||||||
|
- class FenParserCombinators
|
||||||
|
- function parseFen
|
||||||
|
- function parseBoard
|
||||||
|
- function importGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala`
|
||||||
|
- class FenParserFastParse
|
||||||
|
- function parseFen
|
||||||
|
- function parseBoard
|
||||||
|
- function importGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserSupport.scala` — function buildSquares
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala` — class JsonExporter, function exportGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala` — class JsonParser, function importGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala`
|
||||||
|
- class PgnExporter
|
||||||
|
- function exportGameContext
|
||||||
|
- function exportGame
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala`
|
||||||
|
- class PgnParser
|
||||||
|
- function validatePgn
|
||||||
|
- function importGameContext
|
||||||
|
- function parsePgn
|
||||||
|
- function parseAlgebraicMove
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala`
|
||||||
|
- class RuleSet
|
||||||
|
- function candidateMoves
|
||||||
|
- function legalMoves
|
||||||
|
- function allLegalMoves
|
||||||
|
- function isCheck
|
||||||
|
- function isCheckmate
|
||||||
|
- _...4 more_
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala`
|
||||||
|
- class DefaultRules
|
||||||
|
- function loop
|
||||||
|
- function toMoves
|
||||||
|
- function loop
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/Main.scala` — class Main, function main
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala`
|
||||||
|
- class ChessBoardView
|
||||||
|
- function updateBoard
|
||||||
|
- function updateUndoRedoButtons
|
||||||
|
- function showMessage
|
||||||
|
- function showPromotionDialog
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/gui/ChessGUI.scala`
|
||||||
|
- class ChessGUIApp
|
||||||
|
- class ChessGUILauncher
|
||||||
|
- function getEngine
|
||||||
|
- function launch
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/gui/GUIObserver.scala` — class GUIObserver
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/gui/PieceSprites.scala`
|
||||||
|
- class PieceSprites
|
||||||
|
- function loadPieceImage
|
||||||
|
- class SquareColors
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/terminal/TerminalUI.scala` — class TerminalUI, function start
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/utils/PieceUnicode.scala` — function unicode
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/utils/Renderer.scala` — class Renderer, function render
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Config
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `STOCKFISH_PATH` **required** — modules/bot/python/nnue.py
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Middleware
|
||||||
|
|
||||||
|
## custom
|
||||||
|
- generate — `modules/bot/python/src/generate.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dependency Graph
|
||||||
|
|
||||||
|
## Most Imported Files (change these carefully)
|
||||||
|
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **60** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **40** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **39** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **36** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **22** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **21** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **21** files
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **17** files
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **10** files
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **10** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **8** files
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **8** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — imported by **5** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/BotDifficulty.scala` — imported by **5** files
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — imported by **5** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala` — imported by **4** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` — imported by **4** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala` — imported by **4** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala` — imported by **4** files
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` — imported by **4** files
|
||||||
|
|
||||||
|
## Import Map (who imports what)
|
||||||
|
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala` +55 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala` +35 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` +34 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +31 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +17 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` +16 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +16 more
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +12 more
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +5 more
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` ← `modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala` +5 more
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated by [codesight](https://github.com/Houseofmvps/codesight) — see your codebase clearly_
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Config
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `STOCKFISH_PATH` **required** — modules/bot/python/nnue.py
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Dependency Graph
|
||||||
|
|
||||||
|
## Most Imported Files (change these carefully)
|
||||||
|
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **60** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **40** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **39** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **36** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **22** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **21** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **21** files
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **17** files
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **10** files
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **10** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **8** files
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **8** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — imported by **5** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/BotDifficulty.scala` — imported by **5** files
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — imported by **5** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala` — imported by **4** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` — imported by **4** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala` — imported by **4** files
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala` — imported by **4** files
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` — imported by **4** files
|
||||||
|
|
||||||
|
## Import Map (who imports what)
|
||||||
|
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala` +55 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala` +35 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` +34 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +31 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +17 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` +16 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +16 more
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +12 more
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +5 more
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` ← `modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala` +5 more
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
# Libraries
|
||||||
|
|
||||||
|
- `jacoco-reporter/scoverage_coverage_gaps.py`
|
||||||
|
- function parse_scoverage_xml: (xml_path) -> tuple[dict, list[ClassGap]]
|
||||||
|
- function format_agent: (project_stats, classes) -> str
|
||||||
|
- function format_json: (project_stats, classes) -> str
|
||||||
|
- function format_markdown: (project_stats, classes) -> str
|
||||||
|
- function format_module_gaps: (module_name, classes, stmt_pct) -> str
|
||||||
|
- function run_scan_modules: (modules_dir, package_filter, min_coverage) -> None
|
||||||
|
- _...4 more_
|
||||||
|
- `jacoco-reporter/test_gaps.py`
|
||||||
|
- function parse_suite_xml: (xml_path) -> SuiteResult
|
||||||
|
- function load_module: (module_dir, results_subdir) -> Optional[ModuleResult]
|
||||||
|
- function format_module: (mod) -> str
|
||||||
|
- function run: (modules_dir, results_subdir, module_filter) -> None
|
||||||
|
- function main: () -> None
|
||||||
|
- class TestCase
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala`
|
||||||
|
- class Board
|
||||||
|
- function apply
|
||||||
|
- function pieceAt
|
||||||
|
- function updated
|
||||||
|
- function removed
|
||||||
|
- function withMove
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala`
|
||||||
|
- function hasAnyRights
|
||||||
|
- function hasRights
|
||||||
|
- function revokeColor
|
||||||
|
- function revokeKingSide
|
||||||
|
- function revokeQueenSide
|
||||||
|
- class CastlingRights
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — function opposite, function label
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — class Piece
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — function label
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala`
|
||||||
|
- class Square
|
||||||
|
- function fromAlgebraic
|
||||||
|
- function offset
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`
|
||||||
|
- function withBoard
|
||||||
|
- function withTurn
|
||||||
|
- function withCastlingRights
|
||||||
|
- function withEnPassantSquare
|
||||||
|
- function withHalfMoveClock
|
||||||
|
- function withMove
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — class PlayerId, function apply
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/response/ApiResponse.scala`
|
||||||
|
- class ApiResponse
|
||||||
|
- function error
|
||||||
|
- function totalPages
|
||||||
|
- `modules/bot/python/nnue.py`
|
||||||
|
- function get_weights_dir: ()
|
||||||
|
- function get_data_dir: ()
|
||||||
|
- function list_checkpoints: ()
|
||||||
|
- function migrate_legacy_data: ()
|
||||||
|
- function show_header: ()
|
||||||
|
- function show_checkpoints_table: ()
|
||||||
|
- _...10 more_
|
||||||
|
- `modules/bot/python/src/dataset.py`
|
||||||
|
- function get_datasets_dir: () -> Path
|
||||||
|
- function next_dataset_version: () -> int
|
||||||
|
- function list_datasets: () -> List[Tuple[int, Dict]]
|
||||||
|
- function load_dataset_metadata: (version) -> Optional[Dict]
|
||||||
|
- function save_dataset_metadata: (version, metadata) -> None
|
||||||
|
- function create_dataset: (version, labeled_jsonl_path, sources, stockfish_depth) -> Path
|
||||||
|
- _...4 more_
|
||||||
|
- `modules/bot/python/src/export.py` — function export_to_nbai: (weights_file, output_file, trained_by, train_loss)
|
||||||
|
- `modules/bot/python/src/generate.py` — function play_random_game_and_collect_positions: (output_file, total_positions, samples_per_game, min_move, max_move, num_workers)
|
||||||
|
- `modules/bot/python/src/label.py` — function normalize_evaluation: (cp_value, method, scale), function label_positions_with_stockfish: (positions_file, output_file, stockfish_path, batch_size, depth, verbose, normalize, num_workers)
|
||||||
|
- `modules/bot/python/src/tactical_positions_extractor.py`
|
||||||
|
- function download_and_extract_puzzle_db: (url, output_dir)
|
||||||
|
- function extract_puzzle_positions: (puzzle_csv, max_puzzles) -> Set[str]
|
||||||
|
- function load_positions_from_file: (file_path) -> Set[str]
|
||||||
|
- function merge_positions: (tactical, other, output_file)
|
||||||
|
- function extract_tactical_only: (puzzle_csv, output_file, max_puzzles) -> int
|
||||||
|
- function interactive_merge_positions: (puzzle_csv, output_file, max_puzzles)
|
||||||
|
- `modules/bot/python/src/train.py`
|
||||||
|
- function fen_to_features: (fen)
|
||||||
|
- function find_next_version: (base_name)
|
||||||
|
- function save_metadata: (weights_file, metadata)
|
||||||
|
- function train_nnue: (data_file, output_file, epochs, batch_size, lr, checkpoint, stockfish_depth, use_versioning, early_stopping_patience, weight_decay, subsample_ratio)
|
||||||
|
- function burst_train: (data_file, output_file, duration_minutes, epochs_per_season, early_stopping_patience, batch_size, lr, initial_checkpoint, stockfish_depth, use_versioning, weight_decay, subsample_ratio)
|
||||||
|
- class NNUEDataset
|
||||||
|
- _...1 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`
|
||||||
|
- class Bot
|
||||||
|
- function name
|
||||||
|
- function nextMove
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/BotController.scala`
|
||||||
|
- class BotController
|
||||||
|
- function getBot
|
||||||
|
- function listBots
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala`
|
||||||
|
- class BotMoveRepetition
|
||||||
|
- function blockedMoves
|
||||||
|
- function repeatedMove
|
||||||
|
- function filterAllowed
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/Config.scala` — class Config
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`
|
||||||
|
- class Evaluation
|
||||||
|
- class CHECKMATE_SCORE
|
||||||
|
- class DRAW_SCORE
|
||||||
|
- function evaluate
|
||||||
|
- function initAccumulator
|
||||||
|
- function copyAccumulator
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`
|
||||||
|
- class EvaluationClassic
|
||||||
|
- function evaluate
|
||||||
|
- function countRay
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/EvaluationNNUE.scala` — class EvaluationNNUE, function evaluate
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`
|
||||||
|
- class NNUE
|
||||||
|
- function initAccumulator
|
||||||
|
- function pushAccumulator
|
||||||
|
- function copyAccumulator
|
||||||
|
- function recomputeAccumulator
|
||||||
|
- function validateAccumulator
|
||||||
|
- _...4 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NbaiLoader.scala`
|
||||||
|
- class NbaiLoader
|
||||||
|
- function load
|
||||||
|
- function loadDefault
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NbaiMigrator.scala` — class NbaiMigrator, function migrateFromBin
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NbaiModel.scala`
|
||||||
|
- function toJson
|
||||||
|
- class NbaiMetadata
|
||||||
|
- function fromJson
|
||||||
|
- function str
|
||||||
|
- function num
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NbaiWriter.scala` — class NbaiWriter, function write
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`
|
||||||
|
- function bestMove
|
||||||
|
- function bestMove
|
||||||
|
- function bestMoveWithTime
|
||||||
|
- function bestMoveWithTime
|
||||||
|
- function loop
|
||||||
|
- function loop
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`
|
||||||
|
- class MoveOrdering
|
||||||
|
- class OrderingContext
|
||||||
|
- function addKillerMove
|
||||||
|
- function getKillerMoves
|
||||||
|
- function addHistory
|
||||||
|
- function getHistory
|
||||||
|
- _...3 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/TranspositionTable.scala`
|
||||||
|
- function probe
|
||||||
|
- function store
|
||||||
|
- function clear
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — function probe, function select
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` — class PolyglotHash, function hash
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`
|
||||||
|
- class ZobristHash
|
||||||
|
- function hash
|
||||||
|
- function nextHash
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`
|
||||||
|
- class Command
|
||||||
|
- function execute
|
||||||
|
- function undo
|
||||||
|
- function description
|
||||||
|
- class MoveResult
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/command/CommandInvoker.scala`
|
||||||
|
- class CommandInvoker
|
||||||
|
- function execute
|
||||||
|
- function undo
|
||||||
|
- function redo
|
||||||
|
- function history
|
||||||
|
- function getCurrentIndex
|
||||||
|
- _...3 more_
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/controller/Parser.scala` — class Parser, function parseMove
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`
|
||||||
|
- class GameEngine
|
||||||
|
- function isPendingPromotion
|
||||||
|
- function board
|
||||||
|
- function turn
|
||||||
|
- function context
|
||||||
|
- function canUndo
|
||||||
|
- _...11 more_
|
||||||
|
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`
|
||||||
|
- function context
|
||||||
|
- class Observer
|
||||||
|
- function onGameEvent
|
||||||
|
- class Observable
|
||||||
|
- function subscribe
|
||||||
|
- function unsubscribe
|
||||||
|
- _...1 more_
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — class GameContextExport, function exportGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — class GameContextImport, function importGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/GameFileService.scala`
|
||||||
|
- class GameFileService
|
||||||
|
- function saveGameToFile
|
||||||
|
- function loadGameFromFile
|
||||||
|
- class FileSystemGameService
|
||||||
|
- function saveGameToFile
|
||||||
|
- function loadGameFromFile
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala`
|
||||||
|
- class FenExporter
|
||||||
|
- function boardToFen
|
||||||
|
- function gameContextToFen
|
||||||
|
- function exportGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala`
|
||||||
|
- class FenParser
|
||||||
|
- function parseFen
|
||||||
|
- function importGameContext
|
||||||
|
- function parseBoard
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala`
|
||||||
|
- class FenParserCombinators
|
||||||
|
- function parseFen
|
||||||
|
- function parseBoard
|
||||||
|
- function importGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala`
|
||||||
|
- class FenParserFastParse
|
||||||
|
- function parseFen
|
||||||
|
- function parseBoard
|
||||||
|
- function importGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserSupport.scala` — function buildSquares
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala` — class JsonExporter, function exportGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala` — class JsonParser, function importGameContext
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala`
|
||||||
|
- class PgnExporter
|
||||||
|
- function exportGameContext
|
||||||
|
- function exportGame
|
||||||
|
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala`
|
||||||
|
- class PgnParser
|
||||||
|
- function validatePgn
|
||||||
|
- function importGameContext
|
||||||
|
- function parsePgn
|
||||||
|
- function parseAlgebraicMove
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala`
|
||||||
|
- class RuleSet
|
||||||
|
- function candidateMoves
|
||||||
|
- function legalMoves
|
||||||
|
- function allLegalMoves
|
||||||
|
- function isCheck
|
||||||
|
- function isCheckmate
|
||||||
|
- _...4 more_
|
||||||
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala`
|
||||||
|
- class DefaultRules
|
||||||
|
- function loop
|
||||||
|
- function toMoves
|
||||||
|
- function loop
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/Main.scala` — class Main, function main
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala`
|
||||||
|
- class ChessBoardView
|
||||||
|
- function updateBoard
|
||||||
|
- function updateUndoRedoButtons
|
||||||
|
- function showMessage
|
||||||
|
- function showPromotionDialog
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/gui/ChessGUI.scala`
|
||||||
|
- class ChessGUIApp
|
||||||
|
- class ChessGUILauncher
|
||||||
|
- function getEngine
|
||||||
|
- function launch
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/gui/GUIObserver.scala` — class GUIObserver
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/gui/PieceSprites.scala`
|
||||||
|
- class PieceSprites
|
||||||
|
- function loadPieceImage
|
||||||
|
- class SquareColors
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/terminal/TerminalUI.scala` — class TerminalUI, function start
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/utils/PieceUnicode.scala` — function unicode
|
||||||
|
- `modules/ui/src/main/scala/de/nowchess/ui/utils/Renderer.scala` — class Renderer, function render
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Middleware
|
||||||
|
|
||||||
|
## custom
|
||||||
|
- generate — `modules/bot/python/src/generate.py`
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# NowChessSystems — Wiki
|
||||||
|
|
||||||
|
_Generated 2026-04-12 — re-run `npx codesight --wiki` if the codebase has changed._
|
||||||
|
|
||||||
|
Structural map compiled from source code via AST. No LLM — deterministic, 200ms.
|
||||||
|
|
||||||
|
> **How to use safely:** These articles tell you WHERE things live and WHAT exists. They do not show full implementation logic. Always read the actual source files before implementing new features or making changes. Never infer how a function works from the wiki alone.
|
||||||
|
|
||||||
|
## Articles
|
||||||
|
|
||||||
|
- [Overview](./overview.md)
|
||||||
|
|
||||||
|
## Quick Stats
|
||||||
|
|
||||||
|
- Routes: **0**
|
||||||
|
- Models: **0**
|
||||||
|
- Components: **0**
|
||||||
|
- Env vars: **0** required, **0** with defaults
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
- **New session:** read `index.md` (this file) for orientation — WHERE things are
|
||||||
|
- **Architecture question:** read `overview.md` (~500 tokens)
|
||||||
|
- **Domain question:** read the relevant article, then **read those source files**
|
||||||
|
- **Database question:** read `database.md`, then read the actual schema files
|
||||||
|
- **Before implementing anything:** read the source files listed in the article
|
||||||
|
- **Full source context:** read `.codesight/CODESIGHT.md`
|
||||||
|
|
||||||
|
## What the Wiki Does Not Cover
|
||||||
|
|
||||||
|
These exist in your codebase but are **not** reflected in wiki articles:
|
||||||
|
- Routes registered dynamically at runtime (loops, plugin factories, `app.use(dynamicRouter)`)
|
||||||
|
- Internal routes from npm packages (e.g. Better Auth's built-in `/api/auth/*` endpoints)
|
||||||
|
- WebSocket and SSE handlers
|
||||||
|
- Raw SQL tables not declared through an ORM
|
||||||
|
- Computed or virtual fields absent from schema declarations
|
||||||
|
- TypeScript types that are not actual database columns
|
||||||
|
- Routes marked `[inferred]` were detected via regex and may have lower precision
|
||||||
|
- gRPC, tRPC, and GraphQL resolvers may be partially captured
|
||||||
|
|
||||||
|
When in doubt, search the source. The wiki is a starting point, not a complete inventory.
|
||||||
|
|
||||||
|
---
|
||||||
|
_Last compiled: 2026-04-12 · 2 articles · [codesight](https://github.com/Houseofmvps/codesight)_
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Wiki Log
|
||||||
|
|
||||||
|
History of `npx codesight --wiki` runs. Capped at 20 entries.
|
||||||
|
|
||||||
|
## [2026-04-12 14:34:19] scan | 0 routes, 0 models, 0 components → 2 articles
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# NowChessSystems — Overview
|
||||||
|
|
||||||
|
> **Navigation aid.** This article shows WHERE things live (routes, models, files). Read actual source files before implementing new features or making changes.
|
||||||
|
|
||||||
|
**NowChessSystems** is a scala project built with raw-http.
|
||||||
|
|
||||||
|
## High-Impact Files
|
||||||
|
|
||||||
|
Changes to these files have the widest blast radius across the codebase:
|
||||||
|
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **28** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **21** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **19** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **14** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **13** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **10** files
|
||||||
|
|
||||||
|
---
|
||||||
|
_Back to [index.md](./index.md) · Generated 2026-04-12_
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Normalize text files in the repo
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Keep Windows command scripts in CRLF
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# Keep Unix shell scripts in LF
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# Binary assets (no EOL normalization / textual diff)
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.webp binary
|
||||||
|
*.bmp binary
|
||||||
|
*.ico binary
|
||||||
|
|
||||||
|
# ML / model / numeric artifacts
|
||||||
|
*.bin binary
|
||||||
|
*.pt binary
|
||||||
|
*.pth binary
|
||||||
|
*.onnx binary
|
||||||
|
*.h5 binary
|
||||||
|
*.hdf5 binary
|
||||||
|
*.pb binary
|
||||||
|
*.tflite binary
|
||||||
|
*.npy binary
|
||||||
|
*.npz binary
|
||||||
|
*.safetensors binary
|
||||||
|
|
||||||
|
# Firmware / hex-like artifacts
|
||||||
|
*.hex binary
|
||||||
|
|
||||||
|
# Packaged binaries
|
||||||
|
*.jar binary
|
||||||
|
*.zip binary
|
||||||
|
*.7z binary
|
||||||
|
*.gz binary
|
||||||
|
|
||||||
@@ -38,6 +38,8 @@ bin/
|
|||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
|
graphify-out/
|
||||||
|
.graphify_*.json
|
||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -8,3 +8,5 @@
|
|||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
# Editor-based HTTP Client requests
|
# Editor-based HTTP Client requests
|
||||||
/httpRequests/
|
/httpRequests/
|
||||||
|
|
||||||
|
sonarlint.xml
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<AndroidXmlCodeStyleSettings>
|
||||||
|
<option name="USE_CUSTOM_SETTINGS" value="true" />
|
||||||
|
</AndroidXmlCodeStyleSettings>
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<ScalaCodeStyleSettings>
|
||||||
|
<option name="FORMATTER" value="1" />
|
||||||
|
</ScalaCodeStyleSettings>
|
||||||
|
<XML>
|
||||||
|
<option name="XML_KEEP_LINE_BREAKS" value="false" />
|
||||||
|
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
|
||||||
|
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||||
|
</XML>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<state>
|
<state>
|
||||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
</state>
|
</state>
|
||||||
</component>
|
</component>
|
||||||
@@ -11,7 +11,11 @@
|
|||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/modules" />
|
<option value="$PROJECT_DIR$/modules" />
|
||||||
<option value="$PROJECT_DIR$/modules/api" />
|
<option value="$PROJECT_DIR$/modules/api" />
|
||||||
|
<option value="$PROJECT_DIR$/modules/bot" />
|
||||||
<option value="$PROJECT_DIR$/modules/core" />
|
<option value="$PROJECT_DIR$/modules/core" />
|
||||||
|
<option value="$PROJECT_DIR$/modules/io" />
|
||||||
|
<option value="$PROJECT_DIR$/modules/rule" />
|
||||||
|
<option value="$PROJECT_DIR$/modules/ui" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<option name="deprecationWarnings" value="true" />
|
<option name="deprecationWarnings" value="true" />
|
||||||
<option name="uncheckedWarnings" value="true" />
|
<option name="uncheckedWarnings" value="true" />
|
||||||
</profile>
|
</profile>
|
||||||
<profile name="Gradle 2" modules="NowChessSystems.modules.core.main,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test">
|
<profile name="Gradle 2" modules="NowChessSystems.modules.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.core.main,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.main,NowChessSystems.modules.io.scoverage,NowChessSystems.modules.io.test,NowChessSystems.modules.rule.main,NowChessSystems.modules.rule.scoverage,NowChessSystems.modules.rule.test,NowChessSystems.modules.ui.main,NowChessSystems.modules.ui.scoverage,NowChessSystems.modules.ui.test">
|
||||||
<option name="deprecationWarnings" value="true" />
|
<option name="deprecationWarnings" value="true" />
|
||||||
<option name="uncheckedWarnings" value="true" />
|
<option name="uncheckedWarnings" value="true" />
|
||||||
<parameters>
|
<parameters>
|
||||||
|
|||||||
@@ -6,6 +6,16 @@
|
|||||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="IssueNavigationConfiguration">
|
||||||
|
<option name="links">
|
||||||
|
<list>
|
||||||
|
<IssueNavigationLink>
|
||||||
|
<option name="issueRegexp" value="(?x)\b(CORE|NCWF|BAC|FRO|K8S|ORG|NCI|NCS)-\d+\b#YouTrack" />
|
||||||
|
<option name="linkRegexp" value="https://knockoutwhist.youtrack.cloud/issue/$0" />
|
||||||
|
</IssueNavigationLink>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
rules = [
|
||||||
|
DisableSyntax,
|
||||||
|
LeakingImplicitClassVal,
|
||||||
|
NoValInForComprehension,
|
||||||
|
ProcedureSyntax,
|
||||||
|
]
|
||||||
|
|
||||||
|
DisableSyntax.noVars = true
|
||||||
|
DisableSyntax.noThrows = true
|
||||||
|
DisableSyntax.noNulls = true
|
||||||
|
DisableSyntax.noReturns = true
|
||||||
|
DisableSyntax.noAsInstanceOf = true
|
||||||
|
DisableSyntax.noIsInstanceOf = true
|
||||||
|
DisableSyntax.noXml = true
|
||||||
|
DisableSyntax.noFinalize = true
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
version = 3.8.1
|
||||||
|
runner.dialect = scala3
|
||||||
|
maxColumn = 120
|
||||||
|
indent.main = 2
|
||||||
|
align.preset = more
|
||||||
|
trailingCommas = always
|
||||||
|
rewrite.rules = [SortImports, RedundantBraces]
|
||||||
|
rewrite.scala3.convertToNewSyntax = true
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Project Context
|
||||||
|
|
||||||
|
This is a scala project using raw-http.
|
||||||
|
|
||||||
|
Middleware includes: custom.
|
||||||
|
|
||||||
|
High-impact files (most imported, changes here affect many other files):
|
||||||
|
- modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala (imported by 50 files)
|
||||||
|
- modules/api/src/main/scala/de/nowchess/api/board/Square.scala (imported by 33 files)
|
||||||
|
- modules/api/src/main/scala/de/nowchess/api/board/Color.scala (imported by 30 files)
|
||||||
|
- modules/api/src/main/scala/de/nowchess/api/move/Move.scala (imported by 29 files)
|
||||||
|
- modules/api/src/main/scala/de/nowchess/api/board/Board.scala (imported by 19 files)
|
||||||
|
- modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala (imported by 18 files)
|
||||||
|
- modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala (imported by 17 files)
|
||||||
|
- modules/api/src/main/scala/de/nowchess/api/board/Piece.scala (imported by 15 files)
|
||||||
|
|
||||||
|
Required environment variables (no defaults):
|
||||||
|
- STOCKFISH_PATH (modules/bot/python/nnue.py)
|
||||||
|
|
||||||
|
Read .codesight/wiki/index.md for orientation (WHERE things live). Then read actual source files before implementing. Wiki articles are navigation aids, not implementation guides.
|
||||||
|
Read .codesight/CODESIGHT.md for the complete AI context map including all routes, schema, components, libraries, config, middleware, and dependency graph.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
YOU CAN:
|
||||||
|
- Edit and use the asset in any commercial or non commercial project
|
||||||
|
- Use the asset in any commercial or non commercial project
|
||||||
|
|
||||||
|
YOU CAN'T:
|
||||||
|
- Resell or distribute the asset to others
|
||||||
|
- Edit and resell the asset to others - - Credits required using This link: https://fatman200.itch.io/
|
||||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 907 B |
|
After Width: | Height: | Size: 919 B |
|
After Width: | Height: | Size: 818 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 161 B |
|
After Width: | Height: | Size: 188 B |
|
After Width: | Height: | Size: 188 B |
|
After Width: | Height: | Size: 237 B |
|
After Width: | Height: | Size: 243 B |
|
After Width: | Height: | Size: 264 B |
|
After Width: | Height: | Size: 244 B |
|
After Width: | Height: | Size: 240 B |
|
After Width: | Height: | Size: 232 B |
|
After Width: | Height: | Size: 287 B |
|
After Width: | Height: | Size: 211 B |
|
After Width: | Height: | Size: 238 B |
|
After Width: | Height: | Size: 227 B |
|
After Width: | Height: | Size: 267 B |
|
After Width: | Height: | Size: 300 B |
|
After Width: | Height: | Size: 218 B |
|
After Width: | Height: | Size: 244 B |
|
After Width: | Height: | Size: 245 B |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 286 B |
|
After Width: | Height: | Size: 245 B |
|
After Width: | Height: | Size: 266 B |
|
After Width: | Height: | Size: 297 B |
|
After Width: | Height: | Size: 258 B |
|
After Width: | Height: | Size: 263 B |
|
After Width: | Height: | Size: 313 B |
|
After Width: | Height: | Size: 251 B |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 305 B |
|
After Width: | Height: | Size: 281 B |
|
After Width: | Height: | Size: 280 B |
@@ -1,58 +1,99 @@
|
|||||||
# CLAUDE.md
|
# Now-Chess
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
Scala 3.5.1 · Gradle 9
|
||||||
|
|
||||||
## Build & Test Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build everything
|
|
||||||
./gradlew build
|
|
||||||
|
|
||||||
# Build a single module
|
|
||||||
./gradlew :modules:<service>:build
|
|
||||||
|
|
||||||
# Run tests for a single module
|
|
||||||
./gradlew :modules:<service>:test
|
|
||||||
|
|
||||||
# Run a specific test class
|
|
||||||
./gradlew :modules:<service>:test --tests "de.nowchess.<service>.<ClassName>"
|
|
||||||
```
|
```
|
||||||
|
./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.
|
||||||
|
|
||||||
The only current module is `core` (`modules/core`).
|
## Modules
|
||||||
|
|
||||||
## Architecture
|
| Module | Role | Depends on |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `api` | Model / shared types | (none) |
|
||||||
|
| `core` | Primary business logic | api, rule |
|
||||||
|
| `rule` | Game rules | api |
|
||||||
|
| `bot` | Bots and AI | api,rule,io |
|
||||||
|
| `io` | Export formats | api, core |
|
||||||
|
| `ui` | Entrypoint & UI | core, io |
|
||||||
|
|
||||||
**NowChessSystems** is a chess platform built as a Scala 3 + Quarkus microservice system.
|
## Style
|
||||||
|
|
||||||
- Multi-module Gradle project; every service lives under `modules/{service-name}`.
|
- Use immutable data and pure functions.
|
||||||
- Shared dependency versions live in the root `build.gradle.kts` under `extra["VERSIONS"]`.
|
- Keep functions under 30 lines. If you need "and" to describe it, split it.
|
||||||
- Each module reads versions via `rootProject.extra["VERSIONS"] as Map<String, String>`.
|
- Keep cyclomatic complexity under 15.
|
||||||
- `settings.gradle.kts` must `include(":modules:<service>")` for every module.
|
- 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.
|
||||||
|
|
||||||
### Stack (ADR-001)
|
## Code Quality
|
||||||
| Layer | Technology |
|
|
||||||
|---|---|
|
|
||||||
| Language | Scala 3.5.x |
|
|
||||||
| Backend framework | Quarkus + `quarkus-scala3` extension |
|
|
||||||
| Persistence | Hibernate / Jakarta Persistence |
|
|
||||||
| Frontend (TBD) | Vite; React/Angular/Vue under evaluation |
|
|
||||||
| TUI | Lanterna |
|
|
||||||
| Container orchestration | Kubernetes + ArgoCD + Kargo |
|
|
||||||
|
|
||||||
### Key Scala 3 / Quarkus Rules
|
- **Coverage:** 100% condition coverage required in `api`, `core`, `rule`, `io` (mandatory); `ui` exempt.
|
||||||
- Use `given`/`using`, not `implicit` (no Scala 2 idioms).
|
|
||||||
- Use `Option`/`Either`/`Try`, never `null` or `.get`.
|
|
||||||
- Jakarta annotations only (`jakarta.*`), never `javax.*`.
|
|
||||||
- Use reactive types (`Uni`, `Multi`) for I/O; no blocking calls on the event loop.
|
|
||||||
- **Always exclude `org.scala-lang:scala-library` from Quarkus BOM** to avoid Scala 2 conflicts.
|
|
||||||
- **Unit tests use `extends AnyFunSuite with Matchers`** — ScalaTest DSL, no `@Test` annotations needed.
|
|
||||||
- **Integration tests use `@QuarkusTest` with JUnit 5** — explicit `: Unit` return type still required on `@Test` methods.
|
|
||||||
|
|
||||||
### Agent Workflow (for new services)
|
### Linters
|
||||||
1. **architect** → writes OpenAPI contract to `docs/api/{service}.yaml` and ADR to `docs/adr/`.
|
|
||||||
2. **scala-implementer** → reads contract, implements service under `modules/{service}/`.
|
|
||||||
3. **test-writer** → writes `@QuarkusTest` integration tests and `AnyFunSuite with Matchers` unit tests.
|
|
||||||
4. **gradle-builder** → resolves any build/dependency issues.
|
|
||||||
5. **code-reviewer** → reviews; reports findings back without self-fixing.
|
|
||||||
|
|
||||||
Detailed working agreement (plan/verify/unresolved workflow) is in `.claude/CLAUDE.MD`.
|
- **scalafmt** — enforces formatting; run `./gradlew spotlessScalaCheck` to check and `./gradlew spotlessScalaApply` to refactor.
|
||||||
|
- **scalafix** — enforces style and detects unused imports/code; run `./gradlew scalafix` to apply rules.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- **Polyglot hash must follow spec index layout:** piece keys use interleaved mapping `(pieceType * 2 + colorBit)` with black=0/white=1, castling keys are `768..771`, en-passant file keys are `772..779` and are XORed only if side-to-move has a pawn that can capture en passant, side-to-move key is `780` for white.
|
||||||
|
- **Alpha-beta uses sequential PV search by default:** parallel split was disabled because fixed-window futures removed pruning effectiveness; correctness and pruning quality take priority over speculative parallelism.
|
||||||
|
- **Search hash is updated incrementally per move:** bot search now updates Zobrist keys from parent hash with move deltas instead of recomputing piece scans at every node.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Instructions for Claude Code
|
||||||
|
|
||||||
|
### Two-Step Rule (mandatory)
|
||||||
|
**Step 1 — Orient:** Use wiki articles to find WHERE things live.
|
||||||
|
**Step 2 — Verify:** Read the actual source files listed in the wiki article BEFORE writing any code.
|
||||||
|
|
||||||
|
Wiki articles are structural summaries extracted by AST. They show routes, models, and file locations.
|
||||||
|
They do NOT show full function logic, middleware internals, or dynamic runtime behavior.
|
||||||
|
**Never write or modify code based solely on wiki content — always read source files first.**
|
||||||
|
|
||||||
|
Read in order at session start:
|
||||||
|
1. `.codesight/wiki/index.md` — orientation map (~200 tokens)
|
||||||
|
2. `.codesight/wiki/overview.md` — architecture overview (~500 tokens)
|
||||||
|
3. Domain article (e.g. `.codesight/wiki/auth.md`) → check "Source Files" section → read those files
|
||||||
|
4. `.codesight/CODESIGHT.md` — full context map for deep exploration
|
||||||
|
|
||||||
|
Routes marked `[inferred]` in wiki articles were detected via regex — verify against source before trusting.
|
||||||
|
If any source file shows ⚠ in the wiki, re-run `codesight --wiki` before proceeding.
|
||||||
|
|
||||||
|
Or use the codesight MCP server for on-demand queries:
|
||||||
|
- `codesight_get_wiki_article` — read a specific wiki article by name
|
||||||
|
- `codesight_get_wiki_index` — get the wiki index
|
||||||
|
- `codesight_get_summary` — quick project overview
|
||||||
|
- `codesight_get_routes --prefix /api/users` — filtered routes
|
||||||
|
- `codesight_get_blast_radius --file src/lib/db.ts` — impact analysis before changes
|
||||||
|
- `codesight_get_schema --model users` — specific model details
|
||||||
|
|
||||||
|
Only open specific files after consulting codesight context. This saves ~16.893 tokens per conversation.
|
||||||
|
|
||||||
|
## graphify
|
||||||
|
|
||||||
|
This project has a graphify knowledge graph at graphify-out/.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
|
||||||
|
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
|
||||||
|
- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("org.sonarqube") version "7.2.3.7755"
|
id("org.sonarqube") version "7.2.3.7755"
|
||||||
|
id("org.scoverage") version "8.1" apply false
|
||||||
|
id("com.diffplug.spotless") version "8.4.0" apply false
|
||||||
|
id("io.github.cosmicsilence.scalafix") version "0.2.6" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "de.nowchess"
|
group = "de.nowchess"
|
||||||
@@ -19,6 +22,26 @@ sonar {
|
|||||||
}.joinToString(",")
|
}.joinToString(",")
|
||||||
|
|
||||||
property("sonar.scala.coverage.reportPaths", scoverageReports)
|
property("sonar.scala.coverage.reportPaths", scoverageReports)
|
||||||
|
property(
|
||||||
|
"sonar.coverage.exclusions",
|
||||||
|
// UI renders JavaFX components; headless test environments cannot exercise rendering paths
|
||||||
|
"modules/ui/**," +
|
||||||
|
// FastParse macro-generated combinators produce synthetic branches that scoverage marks as uncovered
|
||||||
|
"modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse*," +
|
||||||
|
// NNUE inference pipeline — coverage requires a trained model file not present in CI
|
||||||
|
"**/bot/**/NNUE.scala," +
|
||||||
|
"**/bot/**/NNUEBot.scala," +
|
||||||
|
"**/bot/**/EvaluationNNUE.scala," +
|
||||||
|
// NBAI binary format loader/writer — error paths require crafted corrupt files; migrator is a one-shot tool
|
||||||
|
"**/bot/**/NbaiLoader.scala," +
|
||||||
|
"**/bot/**/NbaiModel.scala," +
|
||||||
|
"**/bot/**/NbaiMigrator.scala," +
|
||||||
|
"**/bot/**/NbaiWriter.scala," +
|
||||||
|
// PolyglotBook — binary I/O and dead-code guards (bit-masked fields can never exceed valid range)
|
||||||
|
"**/bot/**/PolyglotBook.scala," +
|
||||||
|
"**/bot/**/MoveOrdering.scala," +
|
||||||
|
"**/bot/**/AlphaBetaSearch.scala"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +51,39 @@ val versions = mapOf(
|
|||||||
"SCALA_LIBRARY" to "2.13.18",
|
"SCALA_LIBRARY" to "2.13.18",
|
||||||
"SCALATEST" to "3.2.19",
|
"SCALATEST" to "3.2.19",
|
||||||
"SCALATEST_JUNIT" to "0.1.11",
|
"SCALATEST_JUNIT" to "0.1.11",
|
||||||
"SCOVERAGE" to "2.1.1"
|
"SCOVERAGE" to "2.1.1",
|
||||||
|
"SCALAFX" to "21.0.0-R32",
|
||||||
|
"JAVAFX" to "21.0.1",
|
||||||
|
"JUNIT_BOM" to "5.13.4",
|
||||||
|
"ONNXRUNTIME" to "1.19.2",
|
||||||
|
"SCALA_PARSER_COMBINATORS" to "2.4.0",
|
||||||
|
"FASTPARSE" to "3.0.2",
|
||||||
|
"JACKSON" to "2.17.2",
|
||||||
|
"JACKSON_SCALA" to "2.17.2"
|
||||||
)
|
)
|
||||||
extra["VERSIONS"] = versions
|
extra["VERSIONS"] = versions
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply(plugin = "com.diffplug.spotless")
|
||||||
|
|
||||||
|
pluginManager.withPlugin("scala") {
|
||||||
|
configure<com.diffplug.gradle.spotless.SpotlessExtension> {
|
||||||
|
scala {
|
||||||
|
scalafmt().configFile(rootProject.file(".scalafmt.conf"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(plugin = "io.github.cosmicsilence.scalafix")
|
||||||
|
configure<io.github.cosmicsilence.scalafix.ScalafixExtension> {
|
||||||
|
configFile.set(rootProject.file(".scalafix.conf"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable SemanticDB config for the scoverage source set — it sets -sourceroot to
|
||||||
|
// the root project dir, which conflicts with scoverage's own -sourceroot and causes
|
||||||
|
// reportTestScoverage to fail with "No source root found".
|
||||||
|
tasks.matching { it.name in setOf("configSemanticDBScoverage", "checkScalafixScoverage", "checkScalafixTest") }.configureEach {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
./gradlew clean
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
./gradlew classes
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
./gradlew test
|
||||||
|
|
||||||
|
if [ "$#" -eq 0 ]; then
|
||||||
|
PYTHONUTF8=1 python3 jacoco-reporter/scoverage_coverage_gaps.py
|
||||||
|
else
|
||||||
|
PYTHONUTF8=1 python3 jacoco-reporter/scoverage_coverage_gaps.py "modules/$1/build/reports/scoverageTest/scoverage.xml"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,776 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: NowChess API
|
||||||
|
description: |
|
||||||
|
REST API for the NowChess application. Designed to feel familiar to users
|
||||||
|
of the [lichess API](https://lichess.org/api).
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
Most endpoints require a Bearer token:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
Authentication is reserved for future implementation — endpoints are currently
|
||||||
|
open unless noted otherwise.
|
||||||
|
|
||||||
|
## Move notation
|
||||||
|
Moves are expressed in **UCI notation**: `{from}{to}[promotion]`
|
||||||
|
- Normal move: `e2e4`
|
||||||
|
- Capture: `d5e6`
|
||||||
|
- Promotion: `e7e8q` (q=queen, r=rook, b=bishop, n=knight)
|
||||||
|
- Castling: `e1g1` (kingside white), `e1c1` (queenside white)
|
||||||
|
|
||||||
|
## Streaming
|
||||||
|
Endpoints that support streaming return **NDJSON** (newline-delimited JSON).
|
||||||
|
Request them with:
|
||||||
|
```
|
||||||
|
Accept: application/x-ndjson
|
||||||
|
```
|
||||||
|
Each line of the response is a complete JSON object. Empty lines are
|
||||||
|
keep-alive heartbeats.
|
||||||
|
|
||||||
|
## Rate limiting
|
||||||
|
Requests that exceed the rate limit receive `429 Too Many Requests`.
|
||||||
|
Honour the `Retry-After` response header and wait before retrying.
|
||||||
|
version: 1.0.0
|
||||||
|
contact:
|
||||||
|
name: NowChess
|
||||||
|
license:
|
||||||
|
name: MIT
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:8080
|
||||||
|
description: Local development server
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: game
|
||||||
|
description: Create and manage chess games
|
||||||
|
- name: move
|
||||||
|
description: Make moves and navigate game history
|
||||||
|
- name: draw
|
||||||
|
description: Draw offers and claims
|
||||||
|
- name: import
|
||||||
|
description: Load a game from FEN or PGN
|
||||||
|
- name: export
|
||||||
|
description: Export a game as FEN or PGN
|
||||||
|
|
||||||
|
paths:
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Game lifecycle
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/api/board/game:
|
||||||
|
post:
|
||||||
|
operationId: createGame
|
||||||
|
tags: [game]
|
||||||
|
summary: Create a new game
|
||||||
|
description: |
|
||||||
|
Creates a new chess game starting from the initial position.
|
||||||
|
Returns the full game state including the generated `gameId`.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CreateGameRequest'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Game created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GameFull'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
/api/board/game/{gameId}:
|
||||||
|
get:
|
||||||
|
operationId: getGame
|
||||||
|
tags: [game]
|
||||||
|
summary: Get game state
|
||||||
|
description: Returns the full current state of a game.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Current game state
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GameFull'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
/api/board/game/{gameId}/stream:
|
||||||
|
get:
|
||||||
|
operationId: streamGame
|
||||||
|
tags: [game]
|
||||||
|
summary: Stream game events
|
||||||
|
description: |
|
||||||
|
Opens a persistent NDJSON stream for a game. The first object sent is
|
||||||
|
a `gameFull` event containing the complete game state. Subsequent
|
||||||
|
objects are `gameState` events sent whenever the game changes (move
|
||||||
|
made, draw offered, game over, etc.).
|
||||||
|
|
||||||
|
Empty lines are heartbeats to keep the connection alive.
|
||||||
|
|
||||||
|
Connect with:
|
||||||
|
```
|
||||||
|
Accept: application/x-ndjson
|
||||||
|
```
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: NDJSON event stream
|
||||||
|
content:
|
||||||
|
application/x-ndjson:
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- $ref: '#/components/schemas/GameFullEvent'
|
||||||
|
- $ref: '#/components/schemas/GameStateEvent'
|
||||||
|
- $ref: '#/components/schemas/ErrorEvent'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
/api/board/game/{gameId}/resign:
|
||||||
|
post:
|
||||||
|
operationId: resignGame
|
||||||
|
tags: [game]
|
||||||
|
summary: Resign the game
|
||||||
|
description: The active player resigns. The game ends immediately.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Resignation accepted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/OkResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Move-making
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/api/board/game/{gameId}/move/{uci}:
|
||||||
|
post:
|
||||||
|
operationId: makeMove
|
||||||
|
tags: [move]
|
||||||
|
summary: Make a move
|
||||||
|
description: |
|
||||||
|
Submit a move in UCI notation. The move must be legal for the side
|
||||||
|
currently to move.
|
||||||
|
|
||||||
|
For promotion moves include the target piece as the fifth character:
|
||||||
|
`e7e8q`, `a2a1r`, etc.
|
||||||
|
|
||||||
|
If the move results in a pawn reaching the back rank and no promotion
|
||||||
|
character is supplied, the game enters `promotionPending` status and
|
||||||
|
the move is not yet applied — resubmit with the promotion character.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
- name: uci
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Move in UCI notation (e.g. `e2e4`, `e7e8q`)
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '^[a-h][1-8][a-h][1-8][qrbn]?$'
|
||||||
|
example: e2e4
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Move applied — returns updated game state
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GameState'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
/api/board/game/{gameId}/moves:
|
||||||
|
get:
|
||||||
|
operationId: getLegalMoves
|
||||||
|
tags: [move]
|
||||||
|
summary: Get legal moves
|
||||||
|
description: |
|
||||||
|
Returns all legal moves for the side currently to move.
|
||||||
|
Optionally filter to moves originating from a single square.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
- name: square
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: Filter to moves from this square (e.g. `e2`)
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '^[a-h][1-8]$'
|
||||||
|
example: e2
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of legal moves
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/LegalMovesResponse'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
/api/board/game/{gameId}/undo:
|
||||||
|
post:
|
||||||
|
operationId: undoMove
|
||||||
|
tags: [move]
|
||||||
|
summary: Undo the last move
|
||||||
|
description: Reverts the most recent move. Returns the updated game state.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Move undone
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GameState'
|
||||||
|
'400':
|
||||||
|
description: No moves to undo
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
/api/board/game/{gameId}/redo:
|
||||||
|
post:
|
||||||
|
operationId: redoMove
|
||||||
|
tags: [move]
|
||||||
|
summary: Redo a previously undone move
|
||||||
|
description: Re-applies the next move in the undo stack. Returns the updated game state.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Move redone
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GameState'
|
||||||
|
'400':
|
||||||
|
description: No moves to redo
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Draw handling
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/api/board/game/{gameId}/draw/{action}:
|
||||||
|
post:
|
||||||
|
operationId: drawAction
|
||||||
|
tags: [draw]
|
||||||
|
summary: Offer, accept, decline, or claim a draw
|
||||||
|
description: |
|
||||||
|
Perform a draw-related action:
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `offer` | Offer a draw to the opponent |
|
||||||
|
| `accept` | Accept the opponent's draw offer |
|
||||||
|
| `decline` | Decline the opponent's draw offer |
|
||||||
|
| `claim` | Claim a draw under the fifty-move rule (only valid when `status` is `fiftyMoveAvailable`) |
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
- name: action
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [offer, accept, decline, claim]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Action accepted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/OkResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Import
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/api/board/game/import/fen:
|
||||||
|
post:
|
||||||
|
operationId: importFen
|
||||||
|
tags: [import]
|
||||||
|
summary: Load a position from FEN
|
||||||
|
description: |
|
||||||
|
Creates a new game from a FEN string. The game starts at the position
|
||||||
|
described by the FEN; move history prior to that position is not
|
||||||
|
available.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ImportFenRequest'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Game created from FEN
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GameFull'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
/api/board/game/import/pgn:
|
||||||
|
post:
|
||||||
|
operationId: importPgn
|
||||||
|
tags: [import]
|
||||||
|
summary: Load a game from PGN
|
||||||
|
description: |
|
||||||
|
Creates a new game by replaying all moves in a PGN string. The game
|
||||||
|
starts at the position after the final move in the PGN; undo is
|
||||||
|
available for every replayed move.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ImportPgnRequest'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Game created from PGN
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GameFull'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Export
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/api/board/game/{gameId}/export/fen:
|
||||||
|
get:
|
||||||
|
operationId: exportFen
|
||||||
|
tags: [export]
|
||||||
|
summary: Export current position as FEN
|
||||||
|
description: Returns the FEN string representing the current board position.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: FEN string
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
/api/board/game/{gameId}/export/pgn:
|
||||||
|
get:
|
||||||
|
operationId: exportPgn
|
||||||
|
tags: [export]
|
||||||
|
summary: Export game as PGN
|
||||||
|
description: Returns the full PGN for the game including headers and move text.
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/gameId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: PGN text
|
||||||
|
content:
|
||||||
|
application/x-chess-pgn:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: |
|
||||||
|
[Event "NowChess game"]
|
||||||
|
[White "Player1"]
|
||||||
|
[Black "Player2"]
|
||||||
|
[Result "*"]
|
||||||
|
|
||||||
|
1. e4 e5 2. Nf3 *
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/TooManyRequests'
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Components
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
components:
|
||||||
|
|
||||||
|
securitySchemes:
|
||||||
|
bearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
description: 'Personal access token — `Authorization: Bearer <token>`'
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
gameId:
|
||||||
|
name: gameId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: 8-character alphanumeric game ID (e.g. `Qa7FJNk2`)
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '^[A-Za-z0-9]{8}$'
|
||||||
|
example: Qa7FJNk2
|
||||||
|
|
||||||
|
responses:
|
||||||
|
BadRequest:
|
||||||
|
description: Invalid input
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
Unauthorized:
|
||||||
|
description: Missing or invalid authentication token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
NotFound:
|
||||||
|
description: Game not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
TooManyRequests:
|
||||||
|
description: Rate limit exceeded — see `Retry-After` header
|
||||||
|
headers:
|
||||||
|
Retry-After:
|
||||||
|
description: Seconds to wait before retrying
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Requests
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CreateGameRequest:
|
||||||
|
type: object
|
||||||
|
description: Parameters for creating a new game. All fields are optional.
|
||||||
|
properties:
|
||||||
|
white:
|
||||||
|
$ref: '#/components/schemas/PlayerInfo'
|
||||||
|
black:
|
||||||
|
$ref: '#/components/schemas/PlayerInfo'
|
||||||
|
|
||||||
|
ImportFenRequest:
|
||||||
|
type: object
|
||||||
|
required: [fen]
|
||||||
|
properties:
|
||||||
|
fen:
|
||||||
|
type: string
|
||||||
|
description: Complete FEN string (6 fields)
|
||||||
|
example: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
|
||||||
|
white:
|
||||||
|
$ref: '#/components/schemas/PlayerInfo'
|
||||||
|
black:
|
||||||
|
$ref: '#/components/schemas/PlayerInfo'
|
||||||
|
|
||||||
|
ImportPgnRequest:
|
||||||
|
type: object
|
||||||
|
required: [pgn]
|
||||||
|
properties:
|
||||||
|
pgn:
|
||||||
|
type: string
|
||||||
|
description: PGN text (headers and move list)
|
||||||
|
example: "1. e4 e5 2. Nf3 Nc6 *"
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Game state
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
GameFull:
|
||||||
|
type: object
|
||||||
|
description: Complete game information including players and current state.
|
||||||
|
required: [gameId, white, black, state]
|
||||||
|
properties:
|
||||||
|
gameId:
|
||||||
|
type: string
|
||||||
|
description: Unique 8-character game identifier
|
||||||
|
example: Qa7FJNk2
|
||||||
|
white:
|
||||||
|
$ref: '#/components/schemas/PlayerInfo'
|
||||||
|
black:
|
||||||
|
$ref: '#/components/schemas/PlayerInfo'
|
||||||
|
state:
|
||||||
|
$ref: '#/components/schemas/GameState'
|
||||||
|
|
||||||
|
GameState:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
The current game state. Included in `GameFull` and returned by move
|
||||||
|
endpoints and stream events.
|
||||||
|
required: [fen, pgn, turn, status, moves, undoAvailable, redoAvailable]
|
||||||
|
properties:
|
||||||
|
fen:
|
||||||
|
type: string
|
||||||
|
description: FEN string for the current position
|
||||||
|
example: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
|
||||||
|
pgn:
|
||||||
|
type: string
|
||||||
|
description: PGN move text for the full game so far
|
||||||
|
example: "1. e4"
|
||||||
|
turn:
|
||||||
|
type: string
|
||||||
|
enum: [white, black]
|
||||||
|
description: The side to move
|
||||||
|
status:
|
||||||
|
$ref: '#/components/schemas/GameStatus'
|
||||||
|
winner:
|
||||||
|
type: string
|
||||||
|
enum: [white, black]
|
||||||
|
description: Set when `status` is `checkmate` or `resign`
|
||||||
|
nullable: true
|
||||||
|
moves:
|
||||||
|
type: array
|
||||||
|
description: All moves played so far, in UCI notation
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
example: [e2e4, e7e5, g1f3]
|
||||||
|
undoAvailable:
|
||||||
|
type: boolean
|
||||||
|
description: Whether `POST /undo` is currently valid
|
||||||
|
redoAvailable:
|
||||||
|
type: boolean
|
||||||
|
description: Whether `POST /redo` is currently valid
|
||||||
|
|
||||||
|
GameStatus:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Current game status:
|
||||||
|
|
||||||
|
| Value | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| `started` | Game in progress, no special condition |
|
||||||
|
| `check` | Side to move is in check |
|
||||||
|
| `checkmate` | Side to move is checkmated — game over |
|
||||||
|
| `stalemate` | Side to move has no legal moves, not in check — game over (draw) |
|
||||||
|
| `resign` | A player resigned — game over |
|
||||||
|
| `draw` | Draw agreed or claimed — game over |
|
||||||
|
| `drawOffered` | Waiting for the opponent to accept or decline a draw offer |
|
||||||
|
| `fiftyMoveAvailable` | Fifty-move rule threshold reached; active player may claim draw |
|
||||||
|
| `promotionPending` | A pawn reached the back rank; awaiting promotion piece selection |
|
||||||
|
| `insufficientMaterial` | Neither side has enough pieces to deliver checkmate — game over (draw) |
|
||||||
|
enum:
|
||||||
|
- started
|
||||||
|
- check
|
||||||
|
- checkmate
|
||||||
|
- stalemate
|
||||||
|
- resign
|
||||||
|
- draw
|
||||||
|
- drawOffered
|
||||||
|
- fiftyMoveAvailable
|
||||||
|
- promotionPending
|
||||||
|
- insufficientMaterial
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Moves
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
LegalMovesResponse:
|
||||||
|
type: object
|
||||||
|
required: [moves]
|
||||||
|
properties:
|
||||||
|
moves:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/LegalMove'
|
||||||
|
|
||||||
|
LegalMove:
|
||||||
|
type: object
|
||||||
|
required: [from, to, uci, moveType]
|
||||||
|
properties:
|
||||||
|
from:
|
||||||
|
type: string
|
||||||
|
description: Origin square in algebraic notation
|
||||||
|
example: e2
|
||||||
|
to:
|
||||||
|
type: string
|
||||||
|
description: Destination square in algebraic notation
|
||||||
|
example: e4
|
||||||
|
uci:
|
||||||
|
type: string
|
||||||
|
description: Full move in UCI notation
|
||||||
|
example: e2e4
|
||||||
|
moveType:
|
||||||
|
$ref: '#/components/schemas/MoveType'
|
||||||
|
promotion:
|
||||||
|
type: string
|
||||||
|
enum: [queen, rook, bishop, knight]
|
||||||
|
description: Target piece for promotion moves
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
MoveType:
|
||||||
|
type: string
|
||||||
|
description: Classification of the move
|
||||||
|
enum:
|
||||||
|
- normal
|
||||||
|
- capture
|
||||||
|
- castleKingside
|
||||||
|
- castleQueenside
|
||||||
|
- enPassant
|
||||||
|
- promotion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Streaming events
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
GameFullEvent:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
First event on a game stream. Contains the complete game snapshot.
|
||||||
|
required: [type, game]
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum: [gameFull]
|
||||||
|
game:
|
||||||
|
$ref: '#/components/schemas/GameFull'
|
||||||
|
|
||||||
|
GameStateEvent:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Emitted on a game stream whenever the game state changes (move played,
|
||||||
|
draw offered, game over, etc.).
|
||||||
|
required: [type, state]
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum: [gameState]
|
||||||
|
state:
|
||||||
|
$ref: '#/components/schemas/GameState'
|
||||||
|
|
||||||
|
ErrorEvent:
|
||||||
|
type: object
|
||||||
|
description: Emitted on a game stream when an error occurs.
|
||||||
|
required: [type, error]
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum: [error]
|
||||||
|
error:
|
||||||
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Shared types
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
PlayerInfo:
|
||||||
|
type: object
|
||||||
|
required: [id, displayName]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Unique player identifier
|
||||||
|
example: player1
|
||||||
|
displayName:
|
||||||
|
type: string
|
||||||
|
description: Human-readable display name
|
||||||
|
example: Alice
|
||||||
|
|
||||||
|
OkResponse:
|
||||||
|
type: object
|
||||||
|
required: [ok]
|
||||||
|
properties:
|
||||||
|
ok:
|
||||||
|
type: boolean
|
||||||
|
enum: [true]
|
||||||
|
|
||||||
|
ApiError:
|
||||||
|
type: object
|
||||||
|
required: [code, message]
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: Machine-readable error code
|
||||||
|
example: INVALID_MOVE
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
description: Human-readable error description
|
||||||
|
example: e2e5 is not a legal move
|
||||||
|
field:
|
||||||
|
type: string
|
||||||
|
description: Request field that caused the error, if applicable
|
||||||
|
example: uci
|
||||||
|
nullable: true
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
JaCoCo Coverage Gap Reporter
|
|
||||||
Parses a JaCoCo XML report and outputs missing line & branch (conditional)
|
|
||||||
coverage in a structured format that Claude Code agents can act on directly.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python jacoco_coverage_gaps.py <jacoco-report.xml> [--min-coverage 80]
|
|
||||||
python jacoco_coverage_gaps.py <jacoco-report.xml> --output json
|
|
||||||
python jacoco_coverage_gaps.py <jacoco-report.xml> --output markdown
|
|
||||||
python jacoco_coverage_gaps.py <jacoco-report.xml> --output agent (default)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Data classes
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LineCoverage:
|
|
||||||
line_number: int
|
|
||||||
hits: int # 0 = not executed
|
|
||||||
branch_total: int = 0 # 0 = not a branch point
|
|
||||||
branch_covered: int = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_uncovered(self) -> bool:
|
|
||||||
return self.hits == 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_partial_branch(self) -> bool:
|
|
||||||
return self.branch_total > 0 and self.branch_covered < self.branch_total
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MethodCoverage:
|
|
||||||
name: str
|
|
||||||
descriptor: str
|
|
||||||
first_line: Optional[int]
|
|
||||||
missed_instructions: int
|
|
||||||
covered_instructions: int
|
|
||||||
missed_branches: int
|
|
||||||
covered_branches: int
|
|
||||||
uncovered_lines: list[int] = field(default_factory=list)
|
|
||||||
partial_branch_lines: list[int] = field(default_factory=list)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_branches(self) -> int:
|
|
||||||
return self.missed_branches + self.covered_branches
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_fully_covered(self) -> bool:
|
|
||||||
return self.missed_instructions == 0 and self.missed_branches == 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def branch_coverage_pct(self) -> float:
|
|
||||||
total = self.total_branches
|
|
||||||
return 100.0 * self.covered_branches / total if total else 100.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def line_coverage_pct(self) -> float:
|
|
||||||
total = self.missed_instructions + self.covered_instructions
|
|
||||||
return 100.0 * self.covered_instructions / total if total else 100.0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ClassCoverage:
|
|
||||||
class_name: str # e.g. com/example/Foo
|
|
||||||
source_file: Optional[str]
|
|
||||||
methods: list[MethodCoverage] = field(default_factory=list)
|
|
||||||
all_lines: list[LineCoverage] = field(default_factory=list)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def java_class_name(self) -> str:
|
|
||||||
return self.class_name.replace("/", ".")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def source_path(self) -> Optional[str]:
|
|
||||||
"""Best-guess relative source path."""
|
|
||||||
if self.source_file:
|
|
||||||
package = "/".join(self.class_name.split("/")[:-1])
|
|
||||||
return f"src/main/java/{package}/{self.source_file}" if package else f"src/main/java/{self.source_file}"
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uncovered_lines(self) -> list[int]:
|
|
||||||
return sorted({l.line_number for l in self.all_lines if l.is_uncovered})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def partial_branch_lines(self) -> list[int]:
|
|
||||||
return sorted({l.line_number for l in self.all_lines if l.is_partial_branch})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def missed_branches(self) -> int:
|
|
||||||
return sum(max(l.branch_total - l.branch_covered, 0) for l in self.all_lines)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_branches(self) -> int:
|
|
||||||
return sum(l.branch_total for l in self.all_lines)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def covered_branches(self) -> int:
|
|
||||||
return self.total_branches - self.missed_branches
|
|
||||||
|
|
||||||
@property
|
|
||||||
def missed_lines(self) -> int:
|
|
||||||
return len(self.uncovered_lines)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_lines(self) -> int:
|
|
||||||
return len(self.all_lines)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def covered_lines(self) -> int:
|
|
||||||
return self.total_lines - self.missed_lines
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_gaps(self) -> bool:
|
|
||||||
return bool(self.uncovered_lines or self.partial_branch_lines)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Parser
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def parse_jacoco_xml(xml_path: str) -> list[ClassCoverage]:
|
|
||||||
"""Parse a JaCoCo XML report into ClassCoverage objects."""
|
|
||||||
tree = ET.parse(xml_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
results: list[ClassCoverage] = []
|
|
||||||
|
|
||||||
for package in root.iter("package"):
|
|
||||||
for cls_elem in package.findall("class"):
|
|
||||||
class_name = cls_elem.get("name", "")
|
|
||||||
source_file = cls_elem.get("sourcefilename")
|
|
||||||
|
|
||||||
# Build method map from <method> children
|
|
||||||
methods: list[MethodCoverage] = []
|
|
||||||
for m in cls_elem.findall("method"):
|
|
||||||
counters = {c.get("type"): c for c in m.findall("counter")}
|
|
||||||
|
|
||||||
def _missed(t): return int(counters[t].get("missed", 0)) if t in counters else 0
|
|
||||||
def _covered(t): return int(counters[t].get("covered", 0)) if t in counters else 0
|
|
||||||
|
|
||||||
methods.append(MethodCoverage(
|
|
||||||
name=m.get("name", ""),
|
|
||||||
descriptor=m.get("desc", ""),
|
|
||||||
first_line=int(m.get("line")) if m.get("line") else None,
|
|
||||||
missed_instructions=_missed("INSTRUCTION"),
|
|
||||||
covered_instructions=_covered("INSTRUCTION"),
|
|
||||||
missed_branches=_missed("BRANCH"),
|
|
||||||
covered_branches=_covered("BRANCH"),
|
|
||||||
))
|
|
||||||
|
|
||||||
cc = ClassCoverage(
|
|
||||||
class_name=class_name,
|
|
||||||
source_file=source_file,
|
|
||||||
methods=methods,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Per-line data lives in the matching <sourcefile> element
|
|
||||||
source_file_elem = package.find(f"sourcefile[@name='{source_file}']") if source_file else None
|
|
||||||
if source_file_elem is not None:
|
|
||||||
for line_elem in source_file_elem.findall("line"):
|
|
||||||
nr = int(line_elem.get("nr", 0))
|
|
||||||
mi = int(line_elem.get("mi", 0)) # missed instructions
|
|
||||||
ci = int(line_elem.get("ci", 0)) # covered instructions
|
|
||||||
mb = int(line_elem.get("mb", 0)) # missed branches
|
|
||||||
cb = int(line_elem.get("cb", 0)) # covered branches
|
|
||||||
hits = ci # ci > 0 means line was executed at least once
|
|
||||||
cc.all_lines.append(LineCoverage(
|
|
||||||
line_number=nr,
|
|
||||||
hits=hits,
|
|
||||||
branch_total=mb + cb,
|
|
||||||
branch_covered=cb,
|
|
||||||
))
|
|
||||||
|
|
||||||
if cc.has_gaps:
|
|
||||||
results.append(cc)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Formatters
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _compact_ranges(numbers: list[int]) -> str:
|
|
||||||
"""Turn [1,2,3,5,7,8,9] -> '1-3, 5, 7-9'"""
|
|
||||||
if not numbers:
|
|
||||||
return ""
|
|
||||||
ranges = []
|
|
||||||
start = prev = numbers[0]
|
|
||||||
for n in numbers[1:]:
|
|
||||||
if n == prev + 1:
|
|
||||||
prev = n
|
|
||||||
else:
|
|
||||||
ranges.append(f"{start}-{prev}" if start != prev else str(start))
|
|
||||||
start = prev = n
|
|
||||||
ranges.append(f"{start}-{prev}" if start != prev else str(start))
|
|
||||||
return ", ".join(ranges)
|
|
||||||
|
|
||||||
|
|
||||||
def format_agent(classes: list[ClassCoverage]) -> str:
|
|
||||||
"""
|
|
||||||
Output optimised for Claude Code agents:
|
|
||||||
– structured, machine-readable yet human-legible
|
|
||||||
– uses file paths and line numbers agents can act on
|
|
||||||
– groups by file, sorts by severity (most gaps first)
|
|
||||||
"""
|
|
||||||
lines: list[str] = []
|
|
||||||
lines.append("# JaCoCo Coverage Gaps — Agent Action Report")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Summary")
|
|
||||||
total_uncovered = sum(c.missed_lines for c in classes)
|
|
||||||
total_partial = sum(len(c.partial_branch_lines) for c in classes)
|
|
||||||
total_missed_branches = sum(c.missed_branches for c in classes)
|
|
||||||
lines.append(f"- Files with gaps : {len(classes)}")
|
|
||||||
lines.append(f"- Uncovered lines : {total_uncovered}")
|
|
||||||
lines.append(f"- Partial branches: {total_partial} lines affected")
|
|
||||||
lines.append(f"- Missed branches : {total_missed_branches} branch paths")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("---")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Files Requiring Tests")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("> Each entry lists the SOURCE FILE PATH, the LINE NUMBERS that need")
|
|
||||||
lines.append("> coverage, and the METHODS that contain those gaps.")
|
|
||||||
lines.append("> Write or extend unit/integration tests to exercise these paths.")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# Sort: most uncovered lines first
|
|
||||||
sorted_classes = sorted(classes, key=lambda c: -(c.missed_lines + len(c.partial_branch_lines)))
|
|
||||||
|
|
||||||
for cls in sorted_classes:
|
|
||||||
source = cls.source_path or f"(source unknown) {cls.java_class_name}"
|
|
||||||
lines.append(f"### `{source}`")
|
|
||||||
lines.append(f"**Class**: `{cls.java_class_name}`")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
if cls.uncovered_lines:
|
|
||||||
lines.append(f"#### ❌ Uncovered Lines")
|
|
||||||
lines.append(f"Lines not executed at all: `{_compact_ranges(cls.uncovered_lines)}`")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("**Methods with uncovered lines:**")
|
|
||||||
for method in cls.methods:
|
|
||||||
uncov = [l for l in cls.uncovered_lines
|
|
||||||
if method.first_line and l >= method.first_line]
|
|
||||||
# heuristic: only attribute if there are uncovered lines near the method start
|
|
||||||
if method.missed_instructions > 0:
|
|
||||||
sig = f"`{method.name}{method.descriptor}`"
|
|
||||||
pct = method.line_coverage_pct
|
|
||||||
lines.append(f" - {sig} — {pct:.0f}% instruction coverage")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
if cls.partial_branch_lines:
|
|
||||||
lines.append(f"#### ⚠️ Partial Branch Coverage (Missing Conditional Paths)")
|
|
||||||
lines.append(f"Lines where not all branches are taken: `{_compact_ranges(cls.partial_branch_lines)}`")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("**Methods with branch gaps:**")
|
|
||||||
for method in cls.methods:
|
|
||||||
if method.missed_branches > 0:
|
|
||||||
sig = f"`{method.name}{method.descriptor}`"
|
|
||||||
pct = method.branch_coverage_pct
|
|
||||||
missing = method.missed_branches
|
|
||||||
lines.append(f" - {sig} — {pct:.0f}% branch coverage ({missing} branch path(s) never taken)")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("**Action**: Add tests that exercise the above lines/branches.")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("---")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("## Quick Reference: All Uncovered Locations")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("Copy-paste friendly list for IDE navigation or grep:")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("```")
|
|
||||||
for cls in sorted_classes:
|
|
||||||
src = cls.source_path or cls.java_class_name
|
|
||||||
if cls.uncovered_lines:
|
|
||||||
for ln in cls.uncovered_lines:
|
|
||||||
lines.append(f"{src}:{ln} # uncovered line")
|
|
||||||
if cls.partial_branch_lines:
|
|
||||||
for ln in cls.partial_branch_lines:
|
|
||||||
lines.append(f"{src}:{ln} # partial branch")
|
|
||||||
lines.append("```")
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def format_json(classes: list[ClassCoverage]) -> str:
|
|
||||||
out = []
|
|
||||||
for cls in classes:
|
|
||||||
out.append({
|
|
||||||
"class": cls.java_class_name,
|
|
||||||
"source_path": cls.source_path,
|
|
||||||
"uncovered_lines": cls.uncovered_lines,
|
|
||||||
"partial_branch_lines": cls.partial_branch_lines,
|
|
||||||
"missed_branches": cls.missed_branches,
|
|
||||||
"methods": [
|
|
||||||
{
|
|
||||||
"name": m.name,
|
|
||||||
"descriptor": m.descriptor,
|
|
||||||
"first_line": m.first_line,
|
|
||||||
"line_coverage_pct": round(m.line_coverage_pct, 1),
|
|
||||||
"branch_coverage_pct": round(m.branch_coverage_pct, 1),
|
|
||||||
"missed_branches": m.missed_branches,
|
|
||||||
"missed_instructions": m.missed_instructions,
|
|
||||||
}
|
|
||||||
for m in cls.methods
|
|
||||||
if not m.is_fully_covered
|
|
||||||
],
|
|
||||||
})
|
|
||||||
return json.dumps(out, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
def format_markdown(classes: list[ClassCoverage]) -> str:
|
|
||||||
lines: list[str] = []
|
|
||||||
lines.append("# JaCoCo Missing Coverage Report\n")
|
|
||||||
for cls in sorted(classes, key=lambda c: cls.java_class_name):
|
|
||||||
lines.append(f"## {cls.java_class_name}")
|
|
||||||
if cls.source_path:
|
|
||||||
lines.append(f"**File**: `{cls.source_path}`\n")
|
|
||||||
if cls.uncovered_lines:
|
|
||||||
lines.append(f"**Uncovered lines**: {_compact_ranges(cls.uncovered_lines)}\n")
|
|
||||||
if cls.partial_branch_lines:
|
|
||||||
lines.append(f"**Partial branches at lines**: {_compact_ranges(cls.partial_branch_lines)}\n")
|
|
||||||
lines.append("| Method | Line Coverage | Branch Coverage | Missed Branches |")
|
|
||||||
lines.append("|--------|--------------|-----------------|-----------------|")
|
|
||||||
for m in cls.methods:
|
|
||||||
if not m.is_fully_covered:
|
|
||||||
lines.append(
|
|
||||||
f"| `{m.name}` | {m.line_coverage_pct:.0f}% | "
|
|
||||||
f"{m.branch_coverage_pct:.0f}% | {m.missed_branches} |"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Entry point
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Report missing line & branch coverage from a JaCoCo XML report."
|
|
||||||
)
|
|
||||||
parser.add_argument("xml_file", help="Path to jacoco.xml report file")
|
|
||||||
parser.add_argument(
|
|
||||||
"--output", "-o",
|
|
||||||
choices=["agent", "json", "markdown"],
|
|
||||||
default="json",
|
|
||||||
help="Output format (default: agent)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--min-coverage",
|
|
||||||
type=float,
|
|
||||||
default=0.0,
|
|
||||||
help="Only report classes below this %% line coverage (0 = report all gaps)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--package-filter", "-p",
|
|
||||||
default=None,
|
|
||||||
help="Only report classes in this package prefix (e.g. com/example/service)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
xml_path = Path(args.xml_file)
|
|
||||||
if not xml_path.exists():
|
|
||||||
print(f"ERROR: File not found: {xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
classes = parse_jacoco_xml(str(xml_path))
|
|
||||||
|
|
||||||
# Apply package filter
|
|
||||||
if args.package_filter:
|
|
||||||
prefix = args.package_filter.replace(".", "/")
|
|
||||||
classes = [c for c in classes if c.class_name.startswith(prefix)]
|
|
||||||
|
|
||||||
# Apply min-coverage filter
|
|
||||||
if args.min_coverage > 0:
|
|
||||||
def _line_pct(c: ClassCoverage) -> float:
|
|
||||||
total = c.total_lines
|
|
||||||
return 100.0 * c.covered_lines / total if total else 100.0
|
|
||||||
|
|
||||||
classes = [c for c in classes if _line_pct(c) < args.min_coverage]
|
|
||||||
|
|
||||||
if not classes:
|
|
||||||
print("✅ No coverage gaps found matching the given filters.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if args.output == "agent":
|
|
||||||
print(format_agent(classes))
|
|
||||||
elif args.output == "json":
|
|
||||||
print(format_json(classes))
|
|
||||||
elif args.output == "markdown":
|
|
||||||
print(format_markdown(classes))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -19,6 +19,9 @@ Usage:
|
|||||||
python scoverage_coverage_gaps.py <scoverage.xml> --output agent (default)
|
python scoverage_coverage_gaps.py <scoverage.xml> --output agent (default)
|
||||||
python scoverage_coverage_gaps.py <scoverage.xml> --package-filter de.nowchess.chess.controller
|
python scoverage_coverage_gaps.py <scoverage.xml> --package-filter de.nowchess.chess.controller
|
||||||
python scoverage_coverage_gaps.py <scoverage.xml> --min-coverage 80
|
python scoverage_coverage_gaps.py <scoverage.xml> --min-coverage 80
|
||||||
|
python scoverage_coverage_gaps.py (default: scans ./modules)
|
||||||
|
python scoverage_coverage_gaps.py --modules-dir ./services
|
||||||
|
python scoverage_coverage_gaps.py <scoverage.xml>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
@@ -26,7 +29,8 @@ import sys
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from pathlib import Path, PureWindowsPath
|
import glob
|
||||||
|
from pathlib import Path
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -112,7 +116,6 @@ class ClassGap:
|
|||||||
@property
|
@property
|
||||||
def uncovered_branch_lines(self) -> list[int]:
|
def uncovered_branch_lines(self) -> list[int]:
|
||||||
"""Lines that are branch points and have at least one uncovered branch statement."""
|
"""Lines that are branch points and have at least one uncovered branch statement."""
|
||||||
# Group branch statements by line; a line is "partial" if some covered, some not
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
by_line: dict[int, list[Statement]] = defaultdict(list)
|
by_line: dict[int, list[Statement]] = defaultdict(list)
|
||||||
for s in self.statements:
|
for s in self.statements:
|
||||||
@@ -120,10 +123,7 @@ class ClassGap:
|
|||||||
by_line[s.line].append(s)
|
by_line[s.line].append(s)
|
||||||
partial = []
|
partial = []
|
||||||
for line, stmts in by_line.items():
|
for line, stmts in by_line.items():
|
||||||
has_covered = any(s.is_covered for s in stmts)
|
if any(s.is_uncovered for s in stmts):
|
||||||
has_uncovered = any(s.is_uncovered for s in stmts)
|
|
||||||
# Report line if any branch arm is uncovered
|
|
||||||
if has_uncovered:
|
|
||||||
partial.append(line)
|
partial.append(line)
|
||||||
return sorted(partial)
|
return sorted(partial)
|
||||||
|
|
||||||
@@ -169,20 +169,10 @@ class ClassGap:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _normalise_source(raw: str) -> str:
|
def _normalise_source(raw: str) -> str:
|
||||||
"""
|
|
||||||
Convert an absolute Windows or Unix source path from the XML into a
|
|
||||||
relative src/main/scala/… path for agent consumption.
|
|
||||||
|
|
||||||
Strategy:
|
|
||||||
1. Replace Windows backslashes.
|
|
||||||
2. Find the 'src/' anchor and take everything from there.
|
|
||||||
3. Fall back to the package-derived path if no anchor found.
|
|
||||||
"""
|
|
||||||
normalised = raw.replace("\\", "/")
|
normalised = raw.replace("\\", "/")
|
||||||
match = re.search(r"(src/(?:main|test)/scala/.+)", normalised)
|
match = re.search(r"(src/(?:main|test)/scala/.+)", normalised)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
# Fallback: just the filename portion
|
|
||||||
return normalised.split("/")[-1]
|
return normalised.split("/")[-1]
|
||||||
|
|
||||||
|
|
||||||
@@ -190,11 +180,10 @@ def _normalise_source(raw: str) -> str:
|
|||||||
# Parser
|
# Parser
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def parse_scoverage_xml(xml_path: str) -> list[ClassGap]:
|
def parse_scoverage_xml(xml_path: str) -> tuple[dict, list[ClassGap]]:
|
||||||
tree = ET.parse(xml_path)
|
tree = ET.parse(xml_path)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
# ── Authoritative project-level totals from <scoverage> root element ──────
|
|
||||||
project_stats = {
|
project_stats = {
|
||||||
"total_statements": int(root.get("statement-count", 0)),
|
"total_statements": int(root.get("statement-count", 0)),
|
||||||
"covered_statements": int(root.get("statements-invoked", 0)),
|
"covered_statements": int(root.get("statements-invoked", 0)),
|
||||||
@@ -202,17 +191,16 @@ def parse_scoverage_xml(xml_path: str) -> list[ClassGap]:
|
|||||||
"branch_coverage_pct": float(root.get("branch-rate", 0.0)),
|
"branch_coverage_pct": float(root.get("branch-rate", 0.0)),
|
||||||
}
|
}
|
||||||
project_stats["missed_statements"] = (
|
project_stats["missed_statements"] = (
|
||||||
project_stats["total_statements"] - project_stats["covered_statements"]
|
project_stats["total_statements"] - project_stats["covered_statements"]
|
||||||
)
|
)
|
||||||
|
|
||||||
class_map: dict[str, ClassGap] = {} # full-class-name → ClassGap
|
class_map: dict[str, ClassGap] = {}
|
||||||
|
|
||||||
for package in root.findall("packages/package"):
|
for package in root.findall("packages/package"):
|
||||||
for cls_elem in package.findall("classes/class"):
|
for cls_elem in package.findall("classes/class"):
|
||||||
class_name = cls_elem.get("name", "")
|
class_name = cls_elem.get("name", "")
|
||||||
filename = cls_elem.get("filename", "")
|
filename = cls_elem.get("filename", "")
|
||||||
|
|
||||||
# Authoritative per-class totals from <class> attributes
|
|
||||||
cls_total = int(cls_elem.get("statement-count", 0))
|
cls_total = int(cls_elem.get("statement-count", 0))
|
||||||
cls_invoked = int(cls_elem.get("statements-invoked", 0))
|
cls_invoked = int(cls_elem.get("statements-invoked", 0))
|
||||||
cls_stmt_rate = float(cls_elem.get("statement-rate", 0.0))
|
cls_stmt_rate = float(cls_elem.get("statement-rate", 0.0))
|
||||||
@@ -221,11 +209,8 @@ def parse_scoverage_xml(xml_path: str) -> list[ClassGap]:
|
|||||||
for method_elem in cls_elem.findall("methods/method"):
|
for method_elem in cls_elem.findall("methods/method"):
|
||||||
method_name = method_elem.get("name", "")
|
method_name = method_elem.get("name", "")
|
||||||
|
|
||||||
# Authoritative per-method totals from <method> attributes
|
m_total = int(method_elem.get("statement-count", 0))
|
||||||
m_total = int(method_elem.get("statement-count", 0))
|
m_invoked = int(method_elem.get("statements-invoked", 0))
|
||||||
m_invoked = int(method_elem.get("statements-invoked", 0))
|
|
||||||
m_stmt_rate = float(method_elem.get("statement-rate", 0.0))
|
|
||||||
m_br_rate = float(method_elem.get("branch-rate", 0.0))
|
|
||||||
|
|
||||||
for stmt_elem in method_elem.findall("statements/statement"):
|
for stmt_elem in method_elem.findall("statements/statement"):
|
||||||
raw_source = stmt_elem.get("source", filename)
|
raw_source = stmt_elem.get("source", filename)
|
||||||
@@ -257,7 +242,6 @@ def parse_scoverage_xml(xml_path: str) -> list[ClassGap]:
|
|||||||
method=method_name,
|
method=method_name,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Register method-level gap using authoritative XML stats
|
|
||||||
cg = next(
|
cg = next(
|
||||||
(v for v in class_map.values() if v.class_name == class_name),
|
(v for v in class_map.values() if v.class_name == class_name),
|
||||||
None,
|
None,
|
||||||
@@ -268,7 +252,6 @@ def parse_scoverage_xml(xml_path: str) -> list[ClassGap]:
|
|||||||
uncov_lines = sorted({s.line for s in active if s.is_uncovered})
|
uncov_lines = sorted({s.line for s in active if s.is_uncovered})
|
||||||
uncov_branch_lines = sorted({s.line for s in active if s.is_branch and s.is_uncovered})
|
uncov_branch_lines = sorted({s.line for s in active if s.is_branch and s.is_uncovered})
|
||||||
if uncov_lines or uncov_branch_lines:
|
if uncov_lines or uncov_branch_lines:
|
||||||
# Count branches from statement-level data (not in method XML attrs)
|
|
||||||
total_b = sum(1 for s in active if s.is_branch)
|
total_b = sum(1 for s in active if s.is_branch)
|
||||||
cov_b = sum(1 for s in active if s.is_branch and s.is_covered)
|
cov_b = sum(1 for s in active if s.is_branch and s.is_covered)
|
||||||
mg = MethodGap(
|
mg = MethodGap(
|
||||||
@@ -282,7 +265,6 @@ def parse_scoverage_xml(xml_path: str) -> list[ClassGap]:
|
|||||||
)
|
)
|
||||||
cg.method_gaps.append(mg)
|
cg.method_gaps.append(mg)
|
||||||
|
|
||||||
# ── Project stats injected so formatters never recount from statements ────
|
|
||||||
return project_stats, [cg for cg in class_map.values() if cg.has_gaps]
|
return project_stats, [cg for cg in class_map.values() if cg.has_gaps]
|
||||||
|
|
||||||
|
|
||||||
@@ -310,103 +292,60 @@ def _compact_ranges(numbers: list[int]) -> str:
|
|||||||
# Formatters
|
# Formatters
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _pct_bar(pct: float, width: int = 20) -> str:
|
|
||||||
"""Render a compact ASCII progress bar, e.g. [████░░░░░░░░░░░░░░░░] 23.5%"""
|
|
||||||
filled = round(pct / 100 * width)
|
|
||||||
bar = "█" * filled + "░" * (width - filled)
|
|
||||||
return f"[{bar}] {pct:.1f}%"
|
|
||||||
|
|
||||||
|
|
||||||
def format_agent(project_stats: dict, classes: list[ClassGap]) -> str:
|
def format_agent(project_stats: dict, classes: list[ClassGap]) -> str:
|
||||||
|
"""
|
||||||
|
Compact agent format — optimised for low token count.
|
||||||
|
Emits only actionable gaps: file path, uncovered lines, branch-gap lines,
|
||||||
|
and a per-method breakdown. No ASCII bars, no redundant tables.
|
||||||
|
"""
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
lines.append("# scoverage Coverage Gaps — Agent Action Report")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# ---- Project-level totals (authoritative from <scoverage> root element) ----
|
total_stmts = project_stats["total_statements"]
|
||||||
total_stmts = project_stats["total_statements"]
|
covered_stmts = project_stats["covered_statements"]
|
||||||
covered_stmts = project_stats["covered_statements"]
|
missed_stmts = project_stats["missed_statements"]
|
||||||
missed_stmts = project_stats["missed_statements"]
|
|
||||||
overall_stmt_pct = project_stats["stmt_coverage_pct"]
|
overall_stmt_pct = project_stats["stmt_coverage_pct"]
|
||||||
overall_branch_pct = project_stats["branch_coverage_pct"]
|
overall_branch_pct = project_stats["branch_coverage_pct"]
|
||||||
total_branch_lines = sum(len(c.uncovered_branch_lines) for c in classes)
|
total_branches = sum(c.total_branches for c in classes)
|
||||||
# Branch totals: count from statement data (scoverage root has no branch count attr)
|
covered_branches = sum(c.covered_branches for c in classes)
|
||||||
total_branches = sum(c.total_branches for c in classes)
|
missed_branches = total_branches - covered_branches
|
||||||
covered_branches = sum(c.covered_branches for c in classes)
|
|
||||||
missed_branches = sum(c.missed_branches for c in classes)
|
|
||||||
|
|
||||||
lines.append("## Project Coverage Summary")
|
lines.append("# scoverage Coverage Gaps")
|
||||||
lines.append("")
|
lines.append(
|
||||||
lines.append(f"| Metric | Covered | Total | Missed | Coverage |")
|
f"stmt: {overall_stmt_pct:.1f}% ({missed_stmts}/{total_stmts} missed) | "
|
||||||
lines.append(f"|-------------------|---------|-------|--------|----------|")
|
f"branches: {overall_branch_pct:.1f}% ({missed_branches}/{total_branches} missed) | "
|
||||||
lines.append(f"| Statements | {covered_stmts:>7} | {total_stmts:>5} | {missed_stmts:>6} | {_pct_bar(overall_stmt_pct)} |")
|
f"files with gaps: {len(classes)}"
|
||||||
lines.append(f"| Branch paths | {covered_branches:>7} | {total_branches:>5} | {missed_branches:>6} | {_pct_bar(overall_branch_pct)} |")
|
)
|
||||||
lines.append(f"| Files with gaps | {'—':>7} | {len(classes):>5} | {'—':>6} | {'—'} |")
|
|
||||||
lines.append(f"| Lines w/ br. gaps | {'—':>7} | {total_branch_lines:>5} | {'—':>6} | {'—'} |")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("---")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Files Requiring Tests")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("> Each entry lists the SOURCE FILE PATH, uncovered LINE NUMBERS,")
|
|
||||||
lines.append("> and the METHODS that contain those gaps.")
|
|
||||||
lines.append("> Write or extend unit/integration tests to exercise these paths.")
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
sorted_classes = sorted(classes, key=lambda c: -(c.missed_statements + c.missed_branches))
|
sorted_classes = sorted(classes, key=lambda c: -(c.missed_statements + c.missed_branches))
|
||||||
|
|
||||||
for cls in sorted_classes:
|
for cls in sorted_classes:
|
||||||
lines.append(f"### `{cls.source_path}`")
|
uncov = cls.all_uncovered_lines
|
||||||
lines.append(f"**Class**: `{cls.class_name}`")
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"| Metric | Covered | Total | Missed | Coverage |")
|
|
||||||
lines.append(f"|--------------|---------|-------|--------|----------|")
|
|
||||||
lines.append(f"| Statements | {cls.covered_statements:>7} | {cls.total_statements:>5} | {cls.missed_statements:>6} | {_pct_bar(cls.stmt_coverage_pct)} |")
|
|
||||||
if cls.total_branches:
|
|
||||||
lines.append(f"| Branch paths | {cls.covered_branches:>7} | {cls.total_branches:>5} | {cls.missed_branches:>6} | {_pct_bar(cls.branch_coverage_pct)} |")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
uncov = cls.all_uncovered_lines
|
|
||||||
if uncov:
|
|
||||||
lines.append("#### ❌ Uncovered Statements")
|
|
||||||
lines.append(f"Lines never executed: `{_compact_ranges(uncov)}`")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
branch_lines = cls.uncovered_branch_lines
|
branch_lines = cls.uncovered_branch_lines
|
||||||
if branch_lines:
|
|
||||||
lines.append("#### ⚠️ Missing Branch Coverage (Conditional Paths)")
|
lines.append(f"## {cls.source_path}")
|
||||||
lines.append(f"Lines where not all conditional paths are taken: `{_compact_ranges(branch_lines)}`")
|
lines.append(
|
||||||
lines.append("")
|
f"stmt: {cls.stmt_coverage_pct:.1f}% ({cls.missed_statements} missed)"
|
||||||
|
+ (f" | branches: {cls.branch_coverage_pct:.1f}% ({cls.missed_branches} missed)"
|
||||||
|
if cls.total_branches else "")
|
||||||
|
)
|
||||||
|
if uncov:
|
||||||
|
lines.append(f"uncovered lines: {_compact_ranges(uncov)}")
|
||||||
|
only_branch = [l for l in branch_lines if l not in cls.all_uncovered_lines]
|
||||||
|
if only_branch:
|
||||||
|
lines.append(f"partial branches: {_compact_ranges(only_branch)}")
|
||||||
|
|
||||||
if cls.method_gaps:
|
if cls.method_gaps:
|
||||||
lines.append("#### Methods with Gaps")
|
lines.append("methods:")
|
||||||
lines.append("")
|
|
||||||
lines.append("| Method | Stmt Coverage | Branch Coverage | Uncovered Lines | Branch Gap Lines |")
|
|
||||||
lines.append("|--------|--------------|-----------------|-----------------|------------------|")
|
|
||||||
for mg in cls.method_gaps:
|
for mg in cls.method_gaps:
|
||||||
stmt_cell = f"{_pct_bar(mg.stmt_coverage_pct, 10)} ({mg.total_statements - mg.covered_statements}/{mg.total_statements} missed)"
|
parts = [f" {mg.short_name}"]
|
||||||
branch_cell = f"{_pct_bar(mg.branch_coverage_pct, 10)} ({mg.missed_branches}/{mg.total_branches} missed)" if mg.total_branches else "n/a"
|
if mg.uncovered_lines:
|
||||||
uncov_cell = f"`{_compact_ranges(mg.uncovered_lines)}`" if mg.uncovered_lines else "—"
|
parts.append(f"lines={_compact_ranges(mg.uncovered_lines)}")
|
||||||
br_cell = f"`{_compact_ranges(mg.uncovered_branch_lines)}`" if mg.uncovered_branch_lines else "—"
|
if mg.uncovered_branch_lines:
|
||||||
lines.append(f"| `{mg.short_name}` | {stmt_cell} | {branch_cell} | {uncov_cell} | {br_cell} |")
|
parts.append(f"branches={_compact_ranges(mg.uncovered_branch_lines)}")
|
||||||
lines.append("")
|
lines.append(" ".join(parts))
|
||||||
|
|
||||||
lines.append("**Action**: Add tests that exercise the lines/branches listed above.")
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("---")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("## Quick Reference: All Uncovered Locations")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("Copy-paste friendly list for IDE navigation or grep:")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("```")
|
|
||||||
for cls in sorted_classes:
|
|
||||||
for ln in cls.all_uncovered_lines:
|
|
||||||
lines.append(f"{cls.source_path}:{ln} # uncovered statement")
|
|
||||||
for ln in cls.uncovered_branch_lines:
|
|
||||||
if ln not in cls.all_uncovered_lines:
|
|
||||||
lines.append(f"{cls.source_path}:{ln} # partial branch")
|
|
||||||
lines.append("```")
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -511,6 +450,87 @@ def format_markdown(project_stats: dict, classes: list[ClassGap]) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Scan-modules mode
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Candidate sub-paths within a module directory where scoverage.xml may live.
|
||||||
|
_SCOVERAGE_SUBPATHS = [
|
||||||
|
# Gradle / default layout
|
||||||
|
"build/reports/scoverageTest/scoverage.xml",
|
||||||
|
# sbt default (scala version wildcard resolved via glob)
|
||||||
|
"target/scala-*/scoverage-report/scoverage.xml",
|
||||||
|
# Maven / flat layout
|
||||||
|
"target/scoverage-report/scoverage.xml",
|
||||||
|
# Already at root of module
|
||||||
|
"scoverage.xml",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _find_scoverage_xml(module_dir: Path) -> Optional[Path]:
|
||||||
|
"""Return the first scoverage.xml found inside *module_dir*, or None."""
|
||||||
|
for pattern in _SCOVERAGE_SUBPATHS:
|
||||||
|
hits = sorted(module_dir.glob(pattern))
|
||||||
|
if hits:
|
||||||
|
return hits[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def format_module_gaps(module_name: str, classes: list[ClassGap], stmt_pct: float) -> str:
|
||||||
|
"""
|
||||||
|
One summary line per module. If coverage is not 100%, append an agent hint.
|
||||||
|
"""
|
||||||
|
if not classes:
|
||||||
|
return f"[{module_name}] stmt: {stmt_pct:.1f}% ✅"
|
||||||
|
|
||||||
|
line = f"[{module_name}] stmt: {stmt_pct:.1f}% files_with_gaps: {len(classes)}"
|
||||||
|
if stmt_pct < 100.0:
|
||||||
|
line += f" # hint: run ./coverage {module_name} for details"
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
def run_scan_modules(modules_dir: str, package_filter: Optional[str], min_coverage: float) -> None:
|
||||||
|
base = Path(modules_dir)
|
||||||
|
if not base.is_dir():
|
||||||
|
print(f"ERROR: modules directory not found: {base}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
module_dirs = sorted(p for p in base.iterdir() if p.is_dir())
|
||||||
|
if not module_dirs:
|
||||||
|
print(f"No sub-directories found in {base}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
results: list[str] = []
|
||||||
|
missing: list[str] = []
|
||||||
|
|
||||||
|
for mod_dir in module_dirs:
|
||||||
|
if mod_dir.name.startswith("build"):
|
||||||
|
continue
|
||||||
|
xml_path = _find_scoverage_xml(mod_dir)
|
||||||
|
if xml_path is None:
|
||||||
|
missing.append(mod_dir.name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
project_stats, classes = parse_scoverage_xml(str(xml_path))
|
||||||
|
|
||||||
|
if package_filter:
|
||||||
|
classes = [c for c in classes if c.class_name.startswith(package_filter)]
|
||||||
|
if min_coverage > 0:
|
||||||
|
classes = [c for c in classes if c.stmt_coverage_pct < min_coverage]
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
format_module_gaps(mod_dir.name, classes, project_stats["stmt_coverage_pct"])
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n".join(results))
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print(
|
||||||
|
f"\n# Modules without scoverage.xml: {', '.join(missing)}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Entry point
|
# Entry point
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -519,7 +539,13 @@ def main() -> None:
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Report missing statement & branch coverage from a scoverage XML report."
|
description="Report missing statement & branch coverage from a scoverage XML report."
|
||||||
)
|
)
|
||||||
parser.add_argument("xml_file", help="Path to scoverage.xml report file")
|
|
||||||
|
# Positional xml_file is optional when --scan-modules is used
|
||||||
|
parser.add_argument(
|
||||||
|
"xml_file",
|
||||||
|
nargs="?",
|
||||||
|
help="Path to scoverage.xml report file (not required with --scan-modules)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--output", "-o",
|
"--output", "-o",
|
||||||
choices=["agent", "json", "markdown"],
|
choices=["agent", "json", "markdown"],
|
||||||
@@ -537,8 +563,30 @@ def main() -> None:
|
|||||||
default=None,
|
default=None,
|
||||||
help="Only report classes in this package prefix (e.g. de.nowchess.chess.controller)",
|
help="Only report classes in this package prefix (e.g. de.nowchess.chess.controller)",
|
||||||
)
|
)
|
||||||
|
# ── Scan-modules mode ──────────────────────────────────────────────────
|
||||||
|
parser.add_argument(
|
||||||
|
"--scan-modules",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Scan every sub-directory of --modules-dir for a scoverage.xml "
|
||||||
|
"and print a compact coverage-gaps summary per module."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--modules-dir",
|
||||||
|
default="./modules",
|
||||||
|
help="Root directory that contains one sub-directory per module (default: ./modules)",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# ── Scan-modules path (explicit flag, or default when no xml_file given) ──
|
||||||
|
if args.scan_modules or not args.xml_file:
|
||||||
|
run_scan_modules(args.modules_dir, args.package_filter, args.min_coverage)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Single-file path ──────────────────────────────────────────────────
|
||||||
|
|
||||||
xml_path = Path(args.xml_file)
|
xml_path = Path(args.xml_file)
|
||||||
if not xml_path.exists():
|
if not xml_path.exists():
|
||||||
print(f"ERROR: File not found: {xml_path}", file=sys.stderr)
|
print(f"ERROR: File not found: {xml_path}", file=sys.stderr)
|
||||||
@@ -565,4 +613,4 @@ def main() -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import glob,re
|
||||||
|
mods=['api','core','io','rule','ui', 'bot']
|
||||||
|
tot=0
|
||||||
|
for m in mods:
|
||||||
|
s=0
|
||||||
|
for f in glob.glob(f'modules/{m}/build/test-results/test/TEST-*.xml'):
|
||||||
|
txt=open(f,encoding='utf-8').read(300)
|
||||||
|
m2=re.search(r'tests="(\d+)"',txt)
|
||||||
|
if m2:s+=int(m2.group(1))
|
||||||
|
print(f'{m}: {s}')
|
||||||
|
tot+=s
|
||||||
|
print('overall:',tot)
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test Gap Reporter
|
||||||
|
Scans JUnit XML test results under modules/*/build/test-results/*.xml and
|
||||||
|
outputs a minimal summary optimised for agent consumption.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python test_gaps.py # scan all modules (default)
|
||||||
|
python test_gaps.py --module chess # single module
|
||||||
|
python test_gaps.py --module all # explicit all
|
||||||
|
python test_gaps.py --modules-dir ./modules
|
||||||
|
python test_gaps.py --results-subdir build/test-results
|
||||||
|
"""
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Data classes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TestCase:
|
||||||
|
classname: str
|
||||||
|
name: str
|
||||||
|
time: float
|
||||||
|
failure: Optional[str] = None # message if failed
|
||||||
|
error: Optional[str] = None # message if errored
|
||||||
|
skipped: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_class(self) -> str:
|
||||||
|
return self.classname.split(".")[-1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> str:
|
||||||
|
if self.failure is not None:
|
||||||
|
return "FAIL"
|
||||||
|
if self.error is not None:
|
||||||
|
return "ERROR"
|
||||||
|
if self.skipped:
|
||||||
|
return "SKIP"
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SuiteResult:
|
||||||
|
name: str
|
||||||
|
total: int
|
||||||
|
failures: int
|
||||||
|
errors: int
|
||||||
|
skipped: int
|
||||||
|
time: float
|
||||||
|
cases: list[TestCase] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def passed(self) -> int:
|
||||||
|
return self.total - self.failures - self.errors - self.skipped
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_clean(self) -> bool:
|
||||||
|
return self.failures == 0 and self.errors == 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bad_cases(self) -> list[TestCase]:
|
||||||
|
return [c for c in self.cases if c.status in ("FAIL", "ERROR")]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skipped_cases(self) -> list[TestCase]:
|
||||||
|
return [c for c in self.cases if c.skipped]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ModuleResult:
|
||||||
|
name: str
|
||||||
|
suites: list[SuiteResult] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self) -> int: return sum(s.total for s in self.suites)
|
||||||
|
@property
|
||||||
|
def failures(self) -> int: return sum(s.failures for s in self.suites)
|
||||||
|
@property
|
||||||
|
def errors(self) -> int: return sum(s.errors for s in self.suites)
|
||||||
|
@property
|
||||||
|
def skipped(self) -> int: return sum(s.skipped for s in self.suites)
|
||||||
|
@property
|
||||||
|
def passed(self) -> int: return sum(s.passed for s in self.suites)
|
||||||
|
@property
|
||||||
|
def is_clean(self) -> bool: return self.failures == 0 and self.errors == 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bad_cases(self) -> list[TestCase]:
|
||||||
|
return [c for s in self.suites for c in s.bad_cases]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skipped_cases(self) -> list[TestCase]:
|
||||||
|
return [c for s in self.suites for c in s.skipped_cases]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parser
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def parse_suite_xml(xml_path: Path) -> SuiteResult:
|
||||||
|
tree = ET.parse(xml_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Handle both <testsuite> root and <testsuites> wrapper
|
||||||
|
suites = [root] if root.tag == "testsuite" else root.findall("testsuite")
|
||||||
|
|
||||||
|
# Merge multiple suites from one file into a single SuiteResult
|
||||||
|
total = failures = errors = skipped = 0
|
||||||
|
elapsed = 0.0
|
||||||
|
name = xml_path.stem
|
||||||
|
cases: list[TestCase] = []
|
||||||
|
|
||||||
|
for suite in suites:
|
||||||
|
total += int(suite.get("tests", 0))
|
||||||
|
failures += int(suite.get("failures", 0))
|
||||||
|
errors += int(suite.get("errors", 0))
|
||||||
|
skipped += int(suite.get("skipped", 0))
|
||||||
|
elapsed += float(suite.get("time", 0.0))
|
||||||
|
if suite.get("name"):
|
||||||
|
name = suite.get("name")
|
||||||
|
|
||||||
|
for tc in suite.findall("testcase"):
|
||||||
|
fail_el = tc.find("failure")
|
||||||
|
err_el = tc.find("error")
|
||||||
|
skip_el = tc.find("skipped")
|
||||||
|
cases.append(TestCase(
|
||||||
|
classname=tc.get("classname", ""),
|
||||||
|
name=tc.get("name", ""),
|
||||||
|
time=float(tc.get("time", 0.0)),
|
||||||
|
failure=fail_el.get("message", fail_el.text or "") if fail_el is not None else None,
|
||||||
|
error=err_el.get("message", err_el.text or "") if err_el is not None else None,
|
||||||
|
skipped=skip_el is not None,
|
||||||
|
))
|
||||||
|
|
||||||
|
return SuiteResult(
|
||||||
|
name=name, total=total, failures=failures,
|
||||||
|
errors=errors, skipped=skipped, time=elapsed, cases=cases,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_module(module_dir: Path, results_subdir: str) -> Optional[ModuleResult]:
|
||||||
|
results_dir = module_dir / results_subdir
|
||||||
|
if not results_dir.is_dir():
|
||||||
|
return None
|
||||||
|
|
||||||
|
xml_files = sorted(results_dir.glob("*.xml"))
|
||||||
|
if not xml_files:
|
||||||
|
return None
|
||||||
|
|
||||||
|
mod = ModuleResult(name=module_dir.name)
|
||||||
|
for xml_path in xml_files:
|
||||||
|
try:
|
||||||
|
mod.suites.append(parse_suite_xml(xml_path))
|
||||||
|
except ET.ParseError:
|
||||||
|
pass # skip malformed files silently
|
||||||
|
return mod if mod.suites else None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Formatter
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _truncate(text: str, max_len: int = 120) -> str:
|
||||||
|
text = " ".join(text.split()) # collapse whitespace
|
||||||
|
return text[:max_len] + "…" if len(text) > max_len else text
|
||||||
|
|
||||||
|
|
||||||
|
def format_module(mod: ModuleResult) -> str:
|
||||||
|
parts = [f"[{mod.name}]"]
|
||||||
|
|
||||||
|
if mod.is_clean and mod.skipped == 0:
|
||||||
|
parts.append(f"tests: {mod.total} ✅")
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
parts.append(f"tests: {mod.total}")
|
||||||
|
if mod.failures: parts.append(f"failed: {mod.failures}")
|
||||||
|
if mod.errors: parts.append(f"errors: {mod.errors}")
|
||||||
|
if mod.skipped: parts.append(f"skipped: {mod.skipped}")
|
||||||
|
|
||||||
|
# Agent hint only when there are actual failures/errors
|
||||||
|
if not mod.is_clean:
|
||||||
|
parts.append(f" # hint: run ./test {mod.name} for details")
|
||||||
|
|
||||||
|
lines = [" ".join(parts)]
|
||||||
|
|
||||||
|
# List each failed/errored test — this IS the actionable info
|
||||||
|
for tc in mod.bad_cases:
|
||||||
|
msg = tc.failure if tc.failure is not None else tc.error
|
||||||
|
label = f" {tc.status}: {tc.short_class} > {tc.name}"
|
||||||
|
if msg:
|
||||||
|
label += f" [{_truncate(msg, 80)}]"
|
||||||
|
lines.append(label)
|
||||||
|
|
||||||
|
# Skipped: compact, one line total
|
||||||
|
if mod.skipped_cases:
|
||||||
|
skipped_names = ", ".join(
|
||||||
|
f"{c.short_class}.{c.name}" for c in mod.skipped_cases[:5]
|
||||||
|
)
|
||||||
|
if len(mod.skipped_cases) > 5:
|
||||||
|
skipped_names += f" (+{len(mod.skipped_cases) - 5} more)"
|
||||||
|
lines.append(f" SKIP: {skipped_names}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Runner
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def run(modules_dir: str, results_subdir: str, module_filter: Optional[str]) -> None:
|
||||||
|
base = Path(modules_dir)
|
||||||
|
if not base.is_dir():
|
||||||
|
print(f"ERROR: modules directory not found: {base}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Resolve which module dirs to scan
|
||||||
|
if module_filter and module_filter != "all":
|
||||||
|
mod_dir = base / module_filter
|
||||||
|
if not mod_dir.is_dir():
|
||||||
|
print(f"ERROR: module not found: {mod_dir}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
candidates = [mod_dir]
|
||||||
|
else:
|
||||||
|
candidates = sorted(p for p in base.iterdir() if p.is_dir())
|
||||||
|
|
||||||
|
results: list[str] = []
|
||||||
|
missing: list[str] = []
|
||||||
|
|
||||||
|
for mod_dir in candidates:
|
||||||
|
if mod_dir.name.startswith("build"):
|
||||||
|
continue
|
||||||
|
mod = load_module(mod_dir, results_subdir)
|
||||||
|
if mod is None:
|
||||||
|
missing.append(mod_dir.name)
|
||||||
|
continue
|
||||||
|
results.append(format_module(mod))
|
||||||
|
|
||||||
|
print("\n".join(results))
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print(
|
||||||
|
f"\n# Modules without test results: {', '.join(missing)}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Entry point
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Minimal test-gap reporter for JUnit XML results across modules."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--module", "-m",
|
||||||
|
nargs="?",
|
||||||
|
const="all",
|
||||||
|
default="all",
|
||||||
|
help="Module name to scan, or 'all' (default: all)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--modules-dir",
|
||||||
|
default="./modules",
|
||||||
|
help="Root directory containing one sub-directory per module (default: ./modules)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--results-subdir",
|
||||||
|
default="build/test-results/test",
|
||||||
|
help="Sub-path inside each module dir where *.xml files live (default: build/test-results/test)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
filter_ = None if args.module == "all" else args.module
|
||||||
|
run(args.modules_dir, args.results_subdir, filter_)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
## (2026-03-27)
|
||||||
|
## (2026-03-28)
|
||||||
|
## (2026-03-28)
|
||||||
|
## (2026-03-29)
|
||||||
|
## (2026-03-31)
|
||||||
|
## (2026-04-01)
|
||||||
|
## (2026-04-01)
|
||||||
|
## (2026-04-01)
|
||||||
|
## (2026-04-02)
|
||||||
|
|
||||||
|
### 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-03)
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
## (2026-04-12)
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
|
||||||
|
## (2026-04-14)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
|
||||||
|
* 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-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
|
||||||
|
## (2026-04-16)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
|
||||||
|
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
|
||||||
|
* 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-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
|
||||||
|
## (2026-04-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
|
||||||
|
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
|
||||||
|
* 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-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
|
||||||
|
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
|
||||||
@@ -49,16 +49,17 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
|
||||||
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
||||||
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
|
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
|
||||||
|
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform {
|
useJUnitPlatform {
|
||||||
includeEngines("scalatest")
|
includeEngines("scalatest")
|
||||||
testLogging {
|
testLogging {
|
||||||
events("passed", "skipped", "failed")
|
events("skipped", "failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finalizedBy(tasks.reportScoverage)
|
finalizedBy(tasks.reportScoverage)
|
||||||
|
|||||||
@@ -7,17 +7,28 @@ object Board:
|
|||||||
def apply(pieces: Map[Square, Piece]): Board = pieces
|
def apply(pieces: Map[Square, Piece]): Board = pieces
|
||||||
|
|
||||||
extension (b: Board)
|
extension (b: Board)
|
||||||
def pieceAt(sq: Square): Option[Piece] = b.get(sq)
|
def pieceAt(sq: Square): Option[Piece] = b.get(sq)
|
||||||
|
def updated(sq: Square, piece: Piece): Board = b.updated(sq, piece)
|
||||||
|
def removed(sq: Square): Board = b.removed(sq)
|
||||||
def withMove(from: Square, to: Square): (Board, Option[Piece]) =
|
def withMove(from: Square, to: Square): (Board, Option[Piece]) =
|
||||||
val captured = b.get(to)
|
val captured = b.get(to)
|
||||||
val updated = b.removed(from).updated(to, b(from))
|
val updatedBoard = b.removed(from).updated(to, b(from))
|
||||||
(updated, captured)
|
(updatedBoard, captured)
|
||||||
|
def applyMove(move: de.nowchess.api.move.Move): Board =
|
||||||
|
val (updatedBoard, _) = b.withMove(move.from, move.to)
|
||||||
|
updatedBoard
|
||||||
def pieces: Map[Square, Piece] = b
|
def pieces: Map[Square, Piece] = b
|
||||||
|
|
||||||
val initial: Board =
|
val initial: Board =
|
||||||
val backRank: Vector[PieceType] = Vector(
|
val backRank: Vector[PieceType] = Vector(
|
||||||
PieceType.Rook, PieceType.Knight, PieceType.Bishop, PieceType.Queen,
|
PieceType.Rook,
|
||||||
PieceType.King, PieceType.Bishop, PieceType.Knight, PieceType.Rook
|
PieceType.Knight,
|
||||||
|
PieceType.Bishop,
|
||||||
|
PieceType.Queen,
|
||||||
|
PieceType.King,
|
||||||
|
PieceType.Bishop,
|
||||||
|
PieceType.Knight,
|
||||||
|
PieceType.Rook,
|
||||||
)
|
)
|
||||||
val entries = for
|
val entries = for
|
||||||
fileIdx <- 0 until 8
|
fileIdx <- 0 until 8
|
||||||
@@ -25,7 +36,7 @@ object Board:
|
|||||||
(Color.White, Rank.R1, backRank(fileIdx)),
|
(Color.White, Rank.R1, backRank(fileIdx)),
|
||||||
(Color.White, Rank.R2, PieceType.Pawn),
|
(Color.White, Rank.R2, PieceType.Pawn),
|
||||||
(Color.Black, Rank.R8, backRank(fileIdx)),
|
(Color.Black, Rank.R8, backRank(fileIdx)),
|
||||||
(Color.Black, Rank.R7, PieceType.Pawn)
|
(Color.Black, Rank.R7, PieceType.Pawn),
|
||||||
)
|
)
|
||||||
yield Square(File.values(fileIdx), rank) -> Piece(color, pieceType)
|
yield Square(File.values(fileIdx), rank) -> Piece(color, pieceType)
|
||||||
Board(entries.toMap)
|
Board(entries.toMap)
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package de.nowchess.api.board
|
||||||
|
|
||||||
|
/** Unified castling rights tracker for all four sides. Tracks whether castling is still available for each side and
|
||||||
|
* direction.
|
||||||
|
*
|
||||||
|
* @param whiteKingSide
|
||||||
|
* White's king-side castling (0-0) still legally available
|
||||||
|
* @param whiteQueenSide
|
||||||
|
* White's queen-side castling (0-0-0) still legally available
|
||||||
|
* @param blackKingSide
|
||||||
|
* Black's king-side castling (0-0) still legally available
|
||||||
|
* @param blackQueenSide
|
||||||
|
* Black's queen-side castling (0-0-0) still legally available
|
||||||
|
*/
|
||||||
|
final case class CastlingRights(
|
||||||
|
whiteKingSide: Boolean,
|
||||||
|
whiteQueenSide: Boolean,
|
||||||
|
blackKingSide: Boolean,
|
||||||
|
blackQueenSide: Boolean,
|
||||||
|
):
|
||||||
|
/** Check if either side has any castling rights remaining.
|
||||||
|
*/
|
||||||
|
def hasAnyRights: Boolean =
|
||||||
|
whiteKingSide || whiteQueenSide || blackKingSide || blackQueenSide
|
||||||
|
|
||||||
|
/** Check if a specific color has any castling rights remaining.
|
||||||
|
*/
|
||||||
|
def hasRights(color: Color): Boolean = color match
|
||||||
|
case Color.White => whiteKingSide || whiteQueenSide
|
||||||
|
case Color.Black => blackKingSide || blackQueenSide
|
||||||
|
|
||||||
|
/** Revoke all castling rights for a specific color.
|
||||||
|
*/
|
||||||
|
def revokeColor(color: Color): CastlingRights = color match
|
||||||
|
case Color.White => copy(whiteKingSide = false, whiteQueenSide = false)
|
||||||
|
case Color.Black => copy(blackKingSide = false, blackQueenSide = false)
|
||||||
|
|
||||||
|
/** Revoke a specific castling right.
|
||||||
|
*/
|
||||||
|
def revokeKingSide(color: Color): CastlingRights = color match
|
||||||
|
case Color.White => copy(whiteKingSide = false)
|
||||||
|
case Color.Black => copy(blackKingSide = false)
|
||||||
|
|
||||||
|
/** Revoke a specific castling right.
|
||||||
|
*/
|
||||||
|
def revokeQueenSide(color: Color): CastlingRights = color match
|
||||||
|
case Color.White => copy(whiteQueenSide = false)
|
||||||
|
case Color.Black => copy(blackQueenSide = false)
|
||||||
|
|
||||||
|
object CastlingRights:
|
||||||
|
/** No castling rights for any side. */
|
||||||
|
val None: CastlingRights = CastlingRights(
|
||||||
|
whiteKingSide = false,
|
||||||
|
whiteQueenSide = false,
|
||||||
|
blackKingSide = false,
|
||||||
|
blackQueenSide = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** All castling rights available. */
|
||||||
|
val All: CastlingRights = CastlingRights(
|
||||||
|
whiteKingSide = true,
|
||||||
|
whiteQueenSide = true,
|
||||||
|
blackKingSide = true,
|
||||||
|
blackQueenSide = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Standard starting position castling rights (both sides can castle both ways). */
|
||||||
|
val Initial: CastlingRights = All
|
||||||
@@ -5,16 +5,16 @@ final case class Piece(color: Color, pieceType: PieceType)
|
|||||||
|
|
||||||
object Piece:
|
object Piece:
|
||||||
// Convenience constructors
|
// Convenience constructors
|
||||||
val WhitePawn: Piece = Piece(Color.White, PieceType.Pawn)
|
val WhitePawn: Piece = Piece(Color.White, PieceType.Pawn)
|
||||||
val WhiteKnight: Piece = Piece(Color.White, PieceType.Knight)
|
val WhiteKnight: Piece = Piece(Color.White, PieceType.Knight)
|
||||||
val WhiteBishop: Piece = Piece(Color.White, PieceType.Bishop)
|
val WhiteBishop: Piece = Piece(Color.White, PieceType.Bishop)
|
||||||
val WhiteRook: Piece = Piece(Color.White, PieceType.Rook)
|
val WhiteRook: Piece = Piece(Color.White, PieceType.Rook)
|
||||||
val WhiteQueen: Piece = Piece(Color.White, PieceType.Queen)
|
val WhiteQueen: Piece = Piece(Color.White, PieceType.Queen)
|
||||||
val WhiteKing: Piece = Piece(Color.White, PieceType.King)
|
val WhiteKing: Piece = Piece(Color.White, PieceType.King)
|
||||||
|
|
||||||
val BlackPawn: Piece = Piece(Color.Black, PieceType.Pawn)
|
val BlackPawn: Piece = Piece(Color.Black, PieceType.Pawn)
|
||||||
val BlackKnight: Piece = Piece(Color.Black, PieceType.Knight)
|
val BlackKnight: Piece = Piece(Color.Black, PieceType.Knight)
|
||||||
val BlackBishop: Piece = Piece(Color.Black, PieceType.Bishop)
|
val BlackBishop: Piece = Piece(Color.Black, PieceType.Bishop)
|
||||||
val BlackRook: Piece = Piece(Color.Black, PieceType.Rook)
|
val BlackRook: Piece = Piece(Color.Black, PieceType.Rook)
|
||||||
val BlackQueen: Piece = Piece(Color.Black, PieceType.Queen)
|
val BlackQueen: Piece = Piece(Color.Black, PieceType.Queen)
|
||||||
val BlackKing: Piece = Piece(Color.Black, PieceType.King)
|
val BlackKing: Piece = Piece(Color.Black, PieceType.King)
|
||||||
|
|||||||
@@ -1,41 +1,53 @@
|
|||||||
package de.nowchess.api.board
|
package de.nowchess.api.board
|
||||||
|
|
||||||
/**
|
/** A file (column) on the chess board, a–h. Ordinal values 0–7 correspond to a–h.
|
||||||
* A file (column) on the chess board, a–h.
|
*/
|
||||||
* Ordinal values 0–7 correspond to a–h.
|
|
||||||
*/
|
|
||||||
enum File:
|
enum File:
|
||||||
case A, B, C, D, E, F, G, H
|
case A, B, C, D, E, F, G, H
|
||||||
|
|
||||||
/**
|
/** A rank (row) on the chess board, 1–8. Ordinal values 0–7 correspond to ranks 1–8.
|
||||||
* A rank (row) on the chess board, 1–8.
|
*/
|
||||||
* Ordinal values 0–7 correspond to ranks 1–8.
|
|
||||||
*/
|
|
||||||
enum Rank:
|
enum Rank:
|
||||||
case R1, R2, R3, R4, R5, R6, R7, R8
|
case R1, R2, R3, R4, R5, R6, R7, R8
|
||||||
|
|
||||||
/**
|
/** A unique square on the board, identified by its file and rank.
|
||||||
* A unique square on the board, identified by its file and rank.
|
*
|
||||||
*
|
* @param file
|
||||||
* @param file the column, a–h
|
* the column, a–h
|
||||||
* @param rank the row, 1–8
|
* @param rank
|
||||||
*/
|
* the row, 1–8
|
||||||
|
*/
|
||||||
final case class Square(file: File, rank: Rank):
|
final case class Square(file: File, rank: Rank):
|
||||||
/** Algebraic notation string, e.g. "e4". */
|
/** Algebraic notation string, e.g. "e4". */
|
||||||
override def toString: String =
|
override def toString: String =
|
||||||
s"${file.toString.toLowerCase}${rank.ordinal + 1}"
|
s"${file.toString.toLowerCase}${rank.ordinal + 1}"
|
||||||
|
|
||||||
object Square:
|
object Square:
|
||||||
/** Parse a square from algebraic notation (e.g. "e4").
|
/** Parse a square from algebraic notation (e.g. "e4"). Returns None if the input is not a valid square name.
|
||||||
* Returns None if the input is not a valid square name. */
|
*/
|
||||||
def fromAlgebraic(s: String): Option[Square] =
|
def fromAlgebraic(s: String): Option[Square] =
|
||||||
if s.length != 2 then None
|
if s.length != 2 then None
|
||||||
else
|
else
|
||||||
val fileChar = s.charAt(0)
|
val fileChar = s.charAt(0)
|
||||||
val rankChar = s.charAt(1)
|
val rankChar = s.charAt(1)
|
||||||
val fileOpt = File.values.find(_.toString.equalsIgnoreCase(fileChar.toString))
|
val fileOpt = File.values.find(_.toString.equalsIgnoreCase(fileChar.toString))
|
||||||
val rankOpt =
|
val rankOpt =
|
||||||
rankChar.toString.toIntOption.flatMap(n =>
|
rankChar.toString.toIntOption.flatMap(n => if n >= 1 && n <= 8 then Some(Rank.values(n - 1)) else None)
|
||||||
if n >= 1 && n <= 8 then Some(Rank.values(n - 1)) else None
|
|
||||||
)
|
|
||||||
for f <- fileOpt; r <- rankOpt yield Square(f, r)
|
for f <- fileOpt; r <- rankOpt yield Square(f, r)
|
||||||
|
|
||||||
|
val all: IndexedSeq[Square] =
|
||||||
|
for
|
||||||
|
r <- Rank.values.toIndexedSeq
|
||||||
|
f <- File.values.toIndexedSeq
|
||||||
|
yield Square(f, r)
|
||||||
|
|
||||||
|
/** Compute a target square by offsetting file and rank. Returns None if the resulting square is outside the board
|
||||||
|
* (0-7 range).
|
||||||
|
*/
|
||||||
|
extension (sq: Square)
|
||||||
|
def offset(fileDelta: Int, rankDelta: Int): Option[Square] =
|
||||||
|
val newFileOrd = sq.file.ordinal + fileDelta
|
||||||
|
val newRankOrd = sq.rank.ordinal + rankDelta
|
||||||
|
if newFileOrd >= 0 && newFileOrd < 8 && newRankOrd >= 0 && newRankOrd < 8 then
|
||||||
|
Some(Square(File.values(newFileOrd), Rank.values(newRankOrd)))
|
||||||
|
else None
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.nowchess.api.bot
|
||||||
|
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.api.move.Move
|
||||||
|
|
||||||
|
trait Bot {
|
||||||
|
|
||||||
|
def name: String
|
||||||
|
def nextMove(context: GameContext): Option[Move]
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.nowchess.api.game
|
||||||
|
|
||||||
|
/** Reason why a game ended in a draw. */
|
||||||
|
enum DrawReason:
|
||||||
|
case Stalemate
|
||||||
|
case InsufficientMaterial
|
||||||
|
case FiftyMoveRule
|
||||||
|
case ThreefoldRepetition
|
||||||
|
case Agreement
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package de.nowchess.api.game
|
||||||
|
|
||||||
|
import de.nowchess.api.board.{Board, CastlingRights, Color, PieceType, Square}
|
||||||
|
import de.nowchess.api.move.Move
|
||||||
|
|
||||||
|
/** Immutable bundle of complete game state. All state changes produce new GameContext instances.
|
||||||
|
*/
|
||||||
|
case class GameContext(
|
||||||
|
board: Board,
|
||||||
|
turn: Color,
|
||||||
|
castlingRights: CastlingRights,
|
||||||
|
enPassantSquare: Option[Square],
|
||||||
|
halfMoveClock: Int,
|
||||||
|
moves: List[Move],
|
||||||
|
result: Option[GameResult] = None,
|
||||||
|
initialBoard: Board = Board.initial,
|
||||||
|
):
|
||||||
|
private lazy val whiteKingSquare: Option[Square] =
|
||||||
|
board.pieces.find((_, p) => p.color == Color.White && p.pieceType == PieceType.King).map(_._1)
|
||||||
|
private lazy val blackKingSquare: Option[Square] =
|
||||||
|
board.pieces.find((_, p) => p.color == Color.Black && p.pieceType == PieceType.King).map(_._1)
|
||||||
|
def kingSquare(color: Color): Option[Square] =
|
||||||
|
if color == Color.White then whiteKingSquare else blackKingSquare
|
||||||
|
|
||||||
|
/** Create new context with updated board. */
|
||||||
|
def withBoard(newBoard: Board): GameContext = copy(board = newBoard)
|
||||||
|
|
||||||
|
/** Create new context with updated turn. */
|
||||||
|
def withTurn(newTurn: Color): GameContext = copy(turn = newTurn)
|
||||||
|
|
||||||
|
/** Create new context with updated castling rights. */
|
||||||
|
def withCastlingRights(newRights: CastlingRights): GameContext = copy(castlingRights = newRights)
|
||||||
|
|
||||||
|
/** Create new context with updated en passant square. */
|
||||||
|
def withEnPassantSquare(newSq: Option[Square]): GameContext = copy(enPassantSquare = newSq)
|
||||||
|
|
||||||
|
/** Create new context with updated half-move clock. */
|
||||||
|
def withHalfMoveClock(newClock: Int): GameContext = copy(halfMoveClock = newClock)
|
||||||
|
|
||||||
|
/** Create new context with move appended to history. */
|
||||||
|
def withMove(move: Move): GameContext = copy(moves = moves :+ move)
|
||||||
|
|
||||||
|
/** Create new context with updated result. */
|
||||||
|
def withResult(newResult: Option[GameResult]): GameContext = copy(result = newResult)
|
||||||
|
|
||||||
|
object GameContext:
|
||||||
|
/** Initial position: white to move, all castling rights, no en passant. */
|
||||||
|
def initial: GameContext = GameContext(
|
||||||
|
board = Board.initial,
|
||||||
|
turn = Color.White,
|
||||||
|
castlingRights = CastlingRights.Initial,
|
||||||
|
enPassantSquare = None,
|
||||||
|
halfMoveClock = 0,
|
||||||
|
moves = List.empty,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.nowchess.api.game
|
||||||
|
|
||||||
|
import de.nowchess.api.board.Color
|
||||||
|
|
||||||
|
/** Outcome of a finished game. */
|
||||||
|
enum GameResult:
|
||||||
|
case Win(color: Color)
|
||||||
|
case Draw(reason: DrawReason)
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package de.nowchess.api.game
|
|
||||||
|
|
||||||
import de.nowchess.api.board.{Color, Square}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Castling availability flags for one side.
|
|
||||||
*
|
|
||||||
* @param kingSide king-side castling still legally available
|
|
||||||
* @param queenSide queen-side castling still legally available
|
|
||||||
*/
|
|
||||||
final case class CastlingRights(kingSide: Boolean, queenSide: Boolean)
|
|
||||||
|
|
||||||
object CastlingRights:
|
|
||||||
val None: CastlingRights = CastlingRights(kingSide = false, queenSide = false)
|
|
||||||
val Both: CastlingRights = CastlingRights(kingSide = true, queenSide = true)
|
|
||||||
|
|
||||||
/** Outcome of a finished game. */
|
|
||||||
enum GameResult:
|
|
||||||
case WhiteWins
|
|
||||||
case BlackWins
|
|
||||||
case Draw
|
|
||||||
|
|
||||||
/** Lifecycle state of a game. */
|
|
||||||
enum GameStatus:
|
|
||||||
case NotStarted
|
|
||||||
case InProgress
|
|
||||||
case Finished(result: GameResult)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A FEN-compatible snapshot of board and game state.
|
|
||||||
*
|
|
||||||
* The board is represented as a FEN piece-placement string (rank 8 to rank 1,
|
|
||||||
* separated by '/'). All other fields mirror standard FEN fields.
|
|
||||||
*
|
|
||||||
* @param piecePlacement FEN piece-placement field, e.g.
|
|
||||||
* "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
|
||||||
* @param activeColor side to move
|
|
||||||
* @param castlingWhite castling rights for White
|
|
||||||
* @param castlingBlack castling rights for Black
|
|
||||||
* @param enPassantTarget square behind the double-pushed pawn, if any
|
|
||||||
* @param halfMoveClock plies since last capture or pawn advance (50-move rule)
|
|
||||||
* @param fullMoveNumber increments after Black's move, starts at 1
|
|
||||||
* @param status current lifecycle status of the game
|
|
||||||
*/
|
|
||||||
final case class GameState(
|
|
||||||
piecePlacement: String,
|
|
||||||
activeColor: Color,
|
|
||||||
castlingWhite: CastlingRights,
|
|
||||||
castlingBlack: CastlingRights,
|
|
||||||
enPassantTarget: Option[Square],
|
|
||||||
halfMoveClock: Int,
|
|
||||||
fullMoveNumber: Int,
|
|
||||||
status: GameStatus
|
|
||||||
)
|
|
||||||
|
|
||||||
object GameState:
|
|
||||||
/** Standard starting position. */
|
|
||||||
val initial: GameState = GameState(
|
|
||||||
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
|
||||||
activeColor = Color.White,
|
|
||||||
castlingWhite = CastlingRights.Both,
|
|
||||||
castlingBlack = CastlingRights.Both,
|
|
||||||
enPassantTarget = None,
|
|
||||||
halfMoveClock = 0,
|
|
||||||
fullMoveNumber = 1,
|
|
||||||
status = GameStatus.InProgress
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.nowchess.api.game
|
||||||
|
|
||||||
|
import de.nowchess.api.bot.Bot
|
||||||
|
import de.nowchess.api.player.PlayerInfo
|
||||||
|
|
||||||
|
sealed trait Participant
|
||||||
|
final case class Human(playerInfo: PlayerInfo) extends Participant
|
||||||
|
final case class BotParticipant(bot: Bot) extends Participant
|
||||||
@@ -9,25 +9,31 @@ enum PromotionPiece:
|
|||||||
/** Classifies special move semantics beyond a plain quiet move or capture. */
|
/** Classifies special move semantics beyond a plain quiet move or capture. */
|
||||||
enum MoveType:
|
enum MoveType:
|
||||||
/** A normal move or capture with no special rule. */
|
/** A normal move or capture with no special rule. */
|
||||||
case Normal
|
case Normal(isCapture: Boolean = false)
|
||||||
|
|
||||||
/** Kingside castling (O-O). */
|
/** Kingside castling (O-O). */
|
||||||
case CastleKingside
|
case CastleKingside
|
||||||
|
|
||||||
/** Queenside castling (O-O-O). */
|
/** Queenside castling (O-O-O). */
|
||||||
case CastleQueenside
|
case CastleQueenside
|
||||||
|
|
||||||
/** En-passant pawn capture. */
|
/** En-passant pawn capture. */
|
||||||
case EnPassant
|
case EnPassant
|
||||||
|
|
||||||
/** Pawn promotion; carries the chosen promotion piece. */
|
/** Pawn promotion; carries the chosen promotion piece. */
|
||||||
case Promotion(piece: PromotionPiece)
|
case Promotion(piece: PromotionPiece)
|
||||||
|
|
||||||
/**
|
/** A half-move (ply) in a chess game.
|
||||||
* A half-move (ply) in a chess game.
|
*
|
||||||
*
|
* @param from
|
||||||
* @param from origin square
|
* origin square
|
||||||
* @param to destination square
|
* @param to
|
||||||
* @param moveType special semantics; defaults to Normal
|
* destination square
|
||||||
*/
|
* @param moveType
|
||||||
|
* special semantics; defaults to Normal
|
||||||
|
*/
|
||||||
final case class Move(
|
final case class Move(
|
||||||
from: Square,
|
from: Square,
|
||||||
to: Square,
|
to: Square,
|
||||||
moveType: MoveType = MoveType.Normal
|
moveType: MoveType = MoveType.Normal(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
package de.nowchess.api.player
|
package de.nowchess.api.player
|
||||||
|
|
||||||
/**
|
/** An opaque player identifier.
|
||||||
* An opaque player identifier.
|
*
|
||||||
*
|
* Wraps a plain String so that IDs are not accidentally interchanged with other String values at compile time.
|
||||||
* Wraps a plain String so that IDs are not accidentally interchanged with
|
*/
|
||||||
* other String values at compile time.
|
|
||||||
*/
|
|
||||||
opaque type PlayerId = String
|
opaque type PlayerId = String
|
||||||
|
|
||||||
object PlayerId:
|
object PlayerId:
|
||||||
def apply(value: String): PlayerId = value
|
def apply(value: String): PlayerId = value
|
||||||
extension (id: PlayerId) def value: String = id
|
extension (id: PlayerId) def value: String = id
|
||||||
|
|
||||||
/**
|
/** The minimal cross-service identity stub for a player.
|
||||||
* The minimal cross-service identity stub for a player.
|
*
|
||||||
*
|
* Full profile data (email, rating history, etc.) lives in the user-management service. Only what every service needs
|
||||||
* Full profile data (email, rating history, etc.) lives in the user-management
|
* is held here.
|
||||||
* service. Only what every service needs is held here.
|
*
|
||||||
*
|
* @param id
|
||||||
* @param id unique identifier
|
* unique identifier
|
||||||
* @param displayName human-readable name shown in the UI
|
* @param displayName
|
||||||
*/
|
* human-readable name shown in the UI
|
||||||
|
*/
|
||||||
final case class PlayerInfo(
|
final case class PlayerInfo(
|
||||||
id: PlayerId,
|
id: PlayerId,
|
||||||
displayName: String
|
displayName: String,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package de.nowchess.api.response
|
package de.nowchess.api.response
|
||||||
|
|
||||||
/**
|
/** A standardised envelope for every API response.
|
||||||
* A standardised envelope for every API response.
|
*
|
||||||
*
|
* Success and failure are modelled as subtypes so that callers can pattern-match exhaustively.
|
||||||
* Success and failure are modelled as subtypes so that callers
|
*
|
||||||
* can pattern-match exhaustively.
|
* @tparam A
|
||||||
*
|
* the payload type for a successful response
|
||||||
* @tparam A the payload type for a successful response
|
*/
|
||||||
*/
|
|
||||||
sealed trait ApiResponse[+A]
|
sealed trait ApiResponse[+A]
|
||||||
|
|
||||||
object ApiResponse:
|
object ApiResponse:
|
||||||
@@ -20,43 +19,49 @@ object ApiResponse:
|
|||||||
/** Convenience constructor for a single-error failure. */
|
/** Convenience constructor for a single-error failure. */
|
||||||
def error(err: ApiError): Failure = Failure(List(err))
|
def error(err: ApiError): Failure = Failure(List(err))
|
||||||
|
|
||||||
/**
|
/** A structured error descriptor.
|
||||||
* A structured error descriptor.
|
*
|
||||||
*
|
* @param code
|
||||||
* @param code machine-readable error code (e.g. "INVALID_MOVE", "NOT_FOUND")
|
* machine-readable error code (e.g. "INVALID_MOVE", "NOT_FOUND")
|
||||||
* @param message human-readable explanation
|
* @param message
|
||||||
* @param field optional field name when the error relates to a specific input
|
* human-readable explanation
|
||||||
*/
|
* @param field
|
||||||
|
* optional field name when the error relates to a specific input
|
||||||
|
*/
|
||||||
final case class ApiError(
|
final case class ApiError(
|
||||||
code: String,
|
code: String,
|
||||||
message: String,
|
message: String,
|
||||||
field: Option[String] = None
|
field: Option[String] = None,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/** Pagination metadata for list responses.
|
||||||
* Pagination metadata for list responses.
|
*
|
||||||
*
|
* @param page
|
||||||
* @param page current 0-based page index
|
* current 0-based page index
|
||||||
* @param pageSize number of items per page
|
* @param pageSize
|
||||||
* @param totalItems total number of items across all pages
|
* number of items per page
|
||||||
*/
|
* @param totalItems
|
||||||
|
* total number of items across all pages
|
||||||
|
*/
|
||||||
final case class Pagination(
|
final case class Pagination(
|
||||||
page: Int,
|
page: Int,
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
totalItems: Long
|
totalItems: Long,
|
||||||
):
|
):
|
||||||
def totalPages: Int =
|
def totalPages: Int =
|
||||||
if pageSize <= 0 then 0
|
if pageSize <= 0 then 0
|
||||||
else Math.ceil(totalItems.toDouble / pageSize).toInt
|
else Math.ceil(totalItems.toDouble / pageSize).toInt
|
||||||
|
|
||||||
/**
|
/** A paginated list response envelope.
|
||||||
* A paginated list response envelope.
|
*
|
||||||
*
|
* @param items
|
||||||
* @param items the items on the current page
|
* the items on the current page
|
||||||
* @param pagination pagination metadata
|
* @param pagination
|
||||||
* @tparam A the item type
|
* pagination metadata
|
||||||
*/
|
* @tparam A
|
||||||
|
* the item type
|
||||||
|
*/
|
||||||
final case class PagedResponse[A](
|
final case class PagedResponse[A](
|
||||||
items: List[A],
|
items: List[A],
|
||||||
pagination: Pagination
|
pagination: Pagination,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package de.nowchess.api.board
|
||||||
|
|
||||||
|
import de.nowchess.api.move.Move
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class BoardTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
private val e2 = Square(File.E, Rank.R2)
|
||||||
|
private val e4 = Square(File.E, Rank.R4)
|
||||||
|
|
||||||
|
test("pieceAt resolves occupied and empty squares") {
|
||||||
|
Board.initial.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
|
||||||
|
Board.initial.pieceAt(e4) shouldBe None
|
||||||
|
}
|
||||||
|
|
||||||
|
test("withMove moves piece and vacates origin") {
|
||||||
|
val (board, captured) = Board.initial.withMove(e2, e4)
|
||||||
|
captured shouldBe None
|
||||||
|
board.pieceAt(e4) shouldBe Some(Piece.WhitePawn)
|
||||||
|
board.pieceAt(e2) shouldBe None
|
||||||
|
}
|
||||||
|
|
||||||
|
test("withMove returns captured piece when destination is occupied") {
|
||||||
|
val from = Square(File.A, Rank.R1)
|
||||||
|
val to = Square(File.A, Rank.R8)
|
||||||
|
val b = Board(Map(from -> Piece.WhiteRook, to -> Piece.BlackRook))
|
||||||
|
val (board, captured) = b.withMove(from, to)
|
||||||
|
captured shouldBe Some(Piece.BlackRook)
|
||||||
|
board.pieceAt(to) shouldBe Some(Piece.WhiteRook)
|
||||||
|
board.pieceAt(from) shouldBe None
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Board.apply and pieces expose the wrapped map") {
|
||||||
|
val map = Map(e2 -> Piece.WhitePawn)
|
||||||
|
val b = Board(map)
|
||||||
|
b.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
|
||||||
|
b.pieces shouldBe map
|
||||||
|
}
|
||||||
|
|
||||||
|
test("initial board has expected material and pawn placement") {
|
||||||
|
Board.initial.pieces should have size 32
|
||||||
|
Board.initial.pieces.values.count(_.color == Color.White) shouldBe 16
|
||||||
|
Board.initial.pieces.values.count(_.color == Color.Black) shouldBe 16
|
||||||
|
|
||||||
|
File.values.foreach { file =>
|
||||||
|
Board.initial.pieceAt(Square(file, Rank.R2)) shouldBe Some(Piece.WhitePawn)
|
||||||
|
Board.initial.pieceAt(Square(file, Rank.R7)) shouldBe Some(Piece.BlackPawn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("initial board white back rank") {
|
||||||
|
val expectedBackRank = Vector(
|
||||||
|
PieceType.Rook,
|
||||||
|
PieceType.Knight,
|
||||||
|
PieceType.Bishop,
|
||||||
|
PieceType.Queen,
|
||||||
|
PieceType.King,
|
||||||
|
PieceType.Bishop,
|
||||||
|
PieceType.Knight,
|
||||||
|
PieceType.Rook,
|
||||||
|
)
|
||||||
|
File.values.zipWithIndex.foreach { (file, i) =>
|
||||||
|
Board.initial.pieceAt(Square(file, Rank.R1)) shouldBe
|
||||||
|
Some(Piece(Color.White, expectedBackRank(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("initial board black back rank") {
|
||||||
|
val expectedBackRank = Vector(
|
||||||
|
PieceType.Rook,
|
||||||
|
PieceType.Knight,
|
||||||
|
PieceType.Bishop,
|
||||||
|
PieceType.Queen,
|
||||||
|
PieceType.King,
|
||||||
|
PieceType.Bishop,
|
||||||
|
PieceType.Knight,
|
||||||
|
PieceType.Rook,
|
||||||
|
)
|
||||||
|
File.values.zipWithIndex.foreach { (file, i) =>
|
||||||
|
Board.initial.pieceAt(Square(file, Rank.R8)) shouldBe
|
||||||
|
Some(Piece(Color.Black, expectedBackRank(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("ranks 3-6 are empty on initial board") {
|
||||||
|
val emptyRanks = Seq(Rank.R3, Rank.R4, Rank.R5, Rank.R6)
|
||||||
|
for
|
||||||
|
rank <- emptyRanks
|
||||||
|
file <- File.values
|
||||||
|
do Board.initial.pieceAt(Square(file, rank)) shouldBe None
|
||||||
|
}
|
||||||
|
|
||||||
|
test("updated adds and replaces piece at squares") {
|
||||||
|
val b = Board(Map(e2 -> Piece.WhitePawn))
|
||||||
|
val added = b.updated(e4, Piece.WhiteKnight)
|
||||||
|
added.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
|
||||||
|
added.pieceAt(e4) shouldBe Some(Piece.WhiteKnight)
|
||||||
|
|
||||||
|
val replaced = b.updated(e2, Piece.WhiteKnight)
|
||||||
|
replaced.pieceAt(e2) shouldBe Some(Piece.WhiteKnight)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("removed deletes piece from board") {
|
||||||
|
val b = Board(Map(e2 -> Piece.WhitePawn, e4 -> Piece.WhiteKnight))
|
||||||
|
val removed = b.removed(e2)
|
||||||
|
removed.pieceAt(e2) shouldBe None
|
||||||
|
removed.pieceAt(e4) shouldBe Some(Piece.WhiteKnight)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("applyMove uses move.from and move.to to relocate a piece") {
|
||||||
|
val b = Board(Map(e2 -> Piece.WhitePawn))
|
||||||
|
|
||||||
|
val moved = b.applyMove(Move(e2, e4))
|
||||||
|
|
||||||
|
moved.pieceAt(e4) shouldBe Some(Piece.WhitePawn)
|
||||||
|
moved.pieceAt(e2) shouldBe None
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package de.nowchess.api.board
|
||||||
|
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class CastlingRightsTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
test("hasAnyRights and hasRights reflect current flags"):
|
||||||
|
val rights = CastlingRights(
|
||||||
|
whiteKingSide = true,
|
||||||
|
whiteQueenSide = false,
|
||||||
|
blackKingSide = false,
|
||||||
|
blackQueenSide = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
rights.hasAnyRights shouldBe true
|
||||||
|
rights.hasRights(Color.White) shouldBe true
|
||||||
|
rights.hasRights(Color.Black) shouldBe true
|
||||||
|
|
||||||
|
CastlingRights.None.hasAnyRights shouldBe false
|
||||||
|
CastlingRights.None.hasRights(Color.White) shouldBe false
|
||||||
|
CastlingRights.None.hasRights(Color.Black) shouldBe false
|
||||||
|
|
||||||
|
test("revokeColor clears both castling sides for selected color"):
|
||||||
|
val all = CastlingRights.All
|
||||||
|
|
||||||
|
val whiteRevoked = all.revokeColor(Color.White)
|
||||||
|
whiteRevoked.whiteKingSide shouldBe false
|
||||||
|
whiteRevoked.whiteQueenSide shouldBe false
|
||||||
|
whiteRevoked.blackKingSide shouldBe true
|
||||||
|
whiteRevoked.blackQueenSide shouldBe true
|
||||||
|
|
||||||
|
val blackRevoked = all.revokeColor(Color.Black)
|
||||||
|
blackRevoked.whiteKingSide shouldBe true
|
||||||
|
blackRevoked.whiteQueenSide shouldBe true
|
||||||
|
blackRevoked.blackKingSide shouldBe false
|
||||||
|
blackRevoked.blackQueenSide shouldBe false
|
||||||
|
|
||||||
|
test("revokeKingSide and revokeQueenSide disable only requested side"):
|
||||||
|
val all = CastlingRights.All
|
||||||
|
|
||||||
|
val whiteKingSideRevoked = all.revokeKingSide(Color.White)
|
||||||
|
whiteKingSideRevoked.whiteKingSide shouldBe false
|
||||||
|
whiteKingSideRevoked.whiteQueenSide shouldBe true
|
||||||
|
|
||||||
|
val whiteQueenSideRevoked = all.revokeQueenSide(Color.White)
|
||||||
|
whiteQueenSideRevoked.whiteKingSide shouldBe true
|
||||||
|
whiteQueenSideRevoked.whiteQueenSide shouldBe false
|
||||||
|
|
||||||
|
val blackKingSideRevoked = all.revokeKingSide(Color.Black)
|
||||||
|
blackKingSideRevoked.blackKingSide shouldBe false
|
||||||
|
blackKingSideRevoked.blackQueenSide shouldBe true
|
||||||
|
|
||||||
|
val blackQueenSideRevoked = all.revokeQueenSide(Color.Black)
|
||||||
|
blackQueenSideRevoked.blackKingSide shouldBe true
|
||||||
|
blackQueenSideRevoked.blackQueenSide shouldBe false
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package de.nowchess.api.board
|
||||||
|
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class ColorTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
test("Color values expose opposite and label consistently"):
|
||||||
|
val cases = List(
|
||||||
|
(Color.White, Color.Black, "White"),
|
||||||
|
(Color.Black, Color.White, "Black"),
|
||||||
|
)
|
||||||
|
|
||||||
|
cases.foreach { (color, opposite, label) =>
|
||||||
|
color.opposite shouldBe opposite
|
||||||
|
color.label shouldBe label
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package de.nowchess.api.board
|
||||||
|
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class PieceTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
test("Piece holds color and pieceType") {
|
||||||
|
val p = Piece(Color.White, PieceType.Queen)
|
||||||
|
p.color shouldBe Color.White
|
||||||
|
p.pieceType shouldBe PieceType.Queen
|
||||||
|
}
|
||||||
|
|
||||||
|
test("all convenience constants map to expected color and piece type") {
|
||||||
|
val expected = List(
|
||||||
|
Piece.WhitePawn -> Piece(Color.White, PieceType.Pawn),
|
||||||
|
Piece.WhiteKnight -> Piece(Color.White, PieceType.Knight),
|
||||||
|
Piece.WhiteBishop -> Piece(Color.White, PieceType.Bishop),
|
||||||
|
Piece.WhiteRook -> Piece(Color.White, PieceType.Rook),
|
||||||
|
Piece.WhiteQueen -> Piece(Color.White, PieceType.Queen),
|
||||||
|
Piece.WhiteKing -> Piece(Color.White, PieceType.King),
|
||||||
|
Piece.BlackPawn -> Piece(Color.Black, PieceType.Pawn),
|
||||||
|
Piece.BlackKnight -> Piece(Color.Black, PieceType.Knight),
|
||||||
|
Piece.BlackBishop -> Piece(Color.Black, PieceType.Bishop),
|
||||||
|
Piece.BlackRook -> Piece(Color.Black, PieceType.Rook),
|
||||||
|
Piece.BlackQueen -> Piece(Color.Black, PieceType.Queen),
|
||||||
|
Piece.BlackKing -> Piece(Color.Black, PieceType.King),
|
||||||
|
)
|
||||||
|
|
||||||
|
expected.foreach { case (actual, wanted) =>
|
||||||
|
actual shouldBe wanted
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.nowchess.api.board
|
||||||
|
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class PieceTypeTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
test("PieceType values expose the expected labels"):
|
||||||
|
val expectedLabels = List(
|
||||||
|
PieceType.Pawn -> "Pawn",
|
||||||
|
PieceType.Knight -> "Knight",
|
||||||
|
PieceType.Bishop -> "Bishop",
|
||||||
|
PieceType.Rook -> "Rook",
|
||||||
|
PieceType.Queen -> "Queen",
|
||||||
|
PieceType.King -> "King",
|
||||||
|
)
|
||||||
|
|
||||||
|
expectedLabels.foreach { (pieceType, expectedLabel) =>
|
||||||
|
pieceType.label shouldBe expectedLabel
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package de.nowchess.api.board
|
||||||
|
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class SquareTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
test("toString renders algebraic notation for edge and middle squares") {
|
||||||
|
Square(File.A, Rank.R1).toString shouldBe "a1"
|
||||||
|
Square(File.E, Rank.R4).toString shouldBe "e4"
|
||||||
|
Square(File.H, Rank.R8).toString shouldBe "h8"
|
||||||
|
}
|
||||||
|
|
||||||
|
test("fromAlgebraic parses valid coordinates including case-insensitive files") {
|
||||||
|
val expected = List(
|
||||||
|
"a1" -> Square(File.A, Rank.R1),
|
||||||
|
"e4" -> Square(File.E, Rank.R4),
|
||||||
|
"h8" -> Square(File.H, Rank.R8),
|
||||||
|
"E4" -> Square(File.E, Rank.R4),
|
||||||
|
)
|
||||||
|
expected.foreach { case (raw, sq) =>
|
||||||
|
Square.fromAlgebraic(raw) shouldBe Some(sq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("fromAlgebraic rejects malformed coordinates") {
|
||||||
|
List("", "e", "e42", "z4", "ex", "e0", "e9").foreach { raw =>
|
||||||
|
Square.fromAlgebraic(raw) shouldBe None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("offset returns Some in-bounds and None out-of-bounds") {
|
||||||
|
Square(File.E, Rank.R4).offset(1, 2) shouldBe Some(Square(File.F, Rank.R6))
|
||||||
|
Square(File.A, Rank.R1).offset(-1, 0) shouldBe None
|
||||||
|
Square(File.H, Rank.R8).offset(0, 1) shouldBe None
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package de.nowchess.api.game
|
||||||
|
|
||||||
|
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Rank, Square}
|
||||||
|
import de.nowchess.api.move.Move
|
||||||
|
import de.nowchess.api.game.{DrawReason, GameResult}
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class GameContextTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
test("GameContext.initial exposes expected default state"):
|
||||||
|
val initial = GameContext.initial
|
||||||
|
|
||||||
|
initial.board shouldBe Board.initial
|
||||||
|
initial.turn shouldBe Color.White
|
||||||
|
initial.castlingRights shouldBe CastlingRights.Initial
|
||||||
|
initial.enPassantSquare shouldBe None
|
||||||
|
initial.halfMoveClock shouldBe 0
|
||||||
|
initial.moves shouldBe List.empty
|
||||||
|
initial.result shouldBe None
|
||||||
|
|
||||||
|
test("withBoard updates only board"):
|
||||||
|
val square = Square(File.E, Rank.R4)
|
||||||
|
val updatedBoard = Board.initial.updated(square, de.nowchess.api.board.Piece.WhiteQueen)
|
||||||
|
val updated = GameContext.initial.withBoard(updatedBoard)
|
||||||
|
updated.board shouldBe updatedBoard
|
||||||
|
updated.turn shouldBe GameContext.initial.turn
|
||||||
|
updated.castlingRights shouldBe GameContext.initial.castlingRights
|
||||||
|
updated.enPassantSquare shouldBe GameContext.initial.enPassantSquare
|
||||||
|
updated.halfMoveClock shouldBe GameContext.initial.halfMoveClock
|
||||||
|
updated.moves shouldBe GameContext.initial.moves
|
||||||
|
|
||||||
|
test("withers update only targeted fields"):
|
||||||
|
val initial = GameContext.initial
|
||||||
|
val rights = CastlingRights(
|
||||||
|
whiteKingSide = true,
|
||||||
|
whiteQueenSide = false,
|
||||||
|
blackKingSide = false,
|
||||||
|
blackQueenSide = true,
|
||||||
|
)
|
||||||
|
val square = Some(Square(File.E, Rank.R3))
|
||||||
|
val updatedTurn = initial.withTurn(Color.Black)
|
||||||
|
val updatedRights = initial.withCastlingRights(rights)
|
||||||
|
val updatedEp = initial.withEnPassantSquare(square)
|
||||||
|
val updatedClock = initial.withHalfMoveClock(17)
|
||||||
|
|
||||||
|
updatedTurn.turn shouldBe Color.Black
|
||||||
|
updatedTurn.board shouldBe initial.board
|
||||||
|
|
||||||
|
updatedRights.castlingRights shouldBe rights
|
||||||
|
updatedRights.turn shouldBe initial.turn
|
||||||
|
|
||||||
|
updatedEp.enPassantSquare shouldBe square
|
||||||
|
updatedEp.castlingRights shouldBe initial.castlingRights
|
||||||
|
|
||||||
|
updatedClock.halfMoveClock shouldBe 17
|
||||||
|
updatedClock.moves shouldBe initial.moves
|
||||||
|
|
||||||
|
test("withMove appends move to history"):
|
||||||
|
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||||
|
GameContext.initial.withMove(move).moves shouldBe List(move)
|
||||||
|
|
||||||
|
test("withResult sets Win result"):
|
||||||
|
val win = Some(GameResult.Win(Color.White))
|
||||||
|
GameContext.initial.withResult(win).result shouldBe win
|
||||||
|
|
||||||
|
test("withResult sets Draw result"):
|
||||||
|
val draw = Some(GameResult.Draw(DrawReason.Stalemate))
|
||||||
|
GameContext.initial.withResult(draw).result shouldBe draw
|
||||||
|
|
||||||
|
test("withResult clears result"):
|
||||||
|
val ctx = GameContext.initial.withResult(Some(GameResult.Win(Color.Black)))
|
||||||
|
ctx.withResult(None).result shouldBe None
|
||||||
|
|
||||||
|
test("kingSquare returns white king position"):
|
||||||
|
GameContext.initial.kingSquare(Color.White) shouldBe Some(Square(File.E, Rank.R1))
|
||||||
|
|
||||||
|
test("kingSquare returns black king position"):
|
||||||
|
GameContext.initial.kingSquare(Color.Black) shouldBe Some(Square(File.E, Rank.R8))
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package de.nowchess.api.move
|
||||||
|
|
||||||
|
import de.nowchess.api.board.{File, Rank, Square}
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class MoveTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
private val e2 = Square(File.E, Rank.R2)
|
||||||
|
private val e4 = Square(File.E, Rank.R4)
|
||||||
|
|
||||||
|
test("Move defaults to Normal and keeps from/to squares") {
|
||||||
|
val m = Move(e2, e4)
|
||||||
|
m.from shouldBe e2
|
||||||
|
m.to shouldBe e4
|
||||||
|
m.moveType shouldBe MoveType.Normal()
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Move accepts all supported move types") {
|
||||||
|
val moveTypes = List(
|
||||||
|
MoveType.Normal(isCapture = true),
|
||||||
|
MoveType.CastleKingside,
|
||||||
|
MoveType.CastleQueenside,
|
||||||
|
MoveType.EnPassant,
|
||||||
|
MoveType.Promotion(PromotionPiece.Queen),
|
||||||
|
MoveType.Promotion(PromotionPiece.Rook),
|
||||||
|
MoveType.Promotion(PromotionPiece.Bishop),
|
||||||
|
MoveType.Promotion(PromotionPiece.Knight),
|
||||||
|
)
|
||||||
|
|
||||||
|
moveTypes.foreach { moveType =>
|
||||||
|
Move(e2, e4, moveType).moveType shouldBe moveType
|
||||||
|
}
|
||||||
|
}
|
||||||