Compare commits

..

11 Commits

Author SHA1 Message Date
LQ63 a176f9e7ca feat(rule): Rules as a microservice
Build & Test (NowChessSystems) TeamCity build failed
Added rules as a microservice
2026-04-21 12:49:39 +02:00
Janis b52623b8f6 feat: Refactor GameResource to use var injection for GameRegistry and ObjectMapper 2026-04-21 12:49:39 +02:00
Janis 5d3d3b4be3 feat: Rework draw handling in GameEngine and GameResource 2026-04-21 12:49:39 +02:00
Janis 3febed3cca feat: NCS-37 Add initial API structure and DTOs for NowChess application 2026-04-21 12:48:47 +02:00
Janis b706c65a0a feat: Add Dockerfiles and configuration for Quarkus application 2026-04-21 12:43:48 +02:00
shosho996 1401297e7f test: NCS-45 IO Test reduction (#32)
Build & Test (NowChessSystems) TeamCity build failed
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #32
Co-authored-by: Shahd Lala <shosho996@blackhole.local>
Co-committed-by: Shahd Lala <shosho996@blackhole.local>
2026-04-21 12:39:19 +02:00
Janis 5ad5efb41e feat: NCS-37 Quarkus integration (#35)
Build & Test (NowChessSystems) TeamCity build failed
Reviewed-on: #35
Reviewed-by: Leon Hermann <lq@blackhole.local>
2026-04-21 12:35:20 +02:00
TeamCity f215ec681a ci: bump version with Build-43 2026-04-19 20:53:56 +00:00
Janis 0091d50467 feat: NCS-40 Rework Draw System (#34)
Build & Test (NowChessSystems) TeamCity build finished
Reviewed-on: #34
Reviewed-by: Shahd Lala <shosho996@blackhole.local>
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2026-04-19 22:44:48 +02:00
TeamCity 2e4c7549b5 ci: bump version with Build-42 2026-04-19 14:01:11 +00:00
Janis dceab0875e feat: NCS-41 Bot Platform (#33)
Build & Test (NowChessSystems) TeamCity build finished
Co-authored-by: Janis <janis@nowchess.de>
Reviewed-on: #33
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2026-04-19 15:52:08 +02:00
100 changed files with 2523520 additions and 1074 deletions
-87
View File
@@ -1,87 +0,0 @@
name: Build & Push Native Image
on:
push:
branches:
- main
workflow_dispatch:
jobs:
check-actor:
runs-on: ubuntu-latest
outputs:
allowed: ${{ steps.check.outputs.allowed }}
steps:
- id: check
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" || "${{ github.actor }}" == "TeamCity" ]]; then
echo "allowed=true" >> "$GITHUB_OUTPUT"
else
echo "allowed=false" >> "$GITHUB_OUTPUT"
fi
build-and-push:
needs: check-actor
if: needs.check-actor.outputs.allowed == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
module:
- core
- io
steps:
- uses: actions/checkout@v4
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: '21'
distribution: 'graalvm-community'
native-image-job-reports: 'true'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: gradle-${{ runner.os }}-
- name: Build native binary
run: ./gradlew :modules:${{ matrix.module }}:build -Dquarkus.native.enabled=true --no-daemon
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/now-chess/now-chess-systems/${{ matrix.module }}
tags: |
type=sha,prefix=,format=short
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: modules/${{ matrix.module }}/src/main/docker/Dockerfile.native
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.module }}
cache-to: type=gha,mode=max,scope=${{ matrix.module }}
+1 -1
View File
@@ -5,7 +5,7 @@
<option name="deprecationWarnings" value="true" />
<option name="uncheckedWarnings" value="true" />
</profile>
<profile name="Gradle 2" modules="NowChessSystems.modules.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.core.integrationTest,NowChessSystems.modules.core.main,NowChessSystems.modules.core.native-test,NowChessSystems.modules.core.quarkus-generated-sources,NowChessSystems.modules.core.quarkus-test-generated-sources,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.integrationTest,NowChessSystems.modules.io.main,NowChessSystems.modules.io.native-test,NowChessSystems.modules.io.quarkus-generated-sources,NowChessSystems.modules.io.quarkus-test-generated-sources,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">
<profile name="Gradle 2" modules="NowChessSystems.modules.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.core.integrationTest,NowChessSystems.modules.core.main,NowChessSystems.modules.core.native-test,NowChessSystems.modules.core.quarkus-generated-sources,NowChessSystems.modules.core.quarkus-test-generated-sources,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="uncheckedWarnings" value="true" />
<parameters>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ScalaProjectSettings">
<option name="scala3DisclaimerShown" value="true" />
</component>
</project>
-1
View File
@@ -9,7 +9,6 @@ Scala 3.5.1 · Gradle 9
./compile # Compile all modules — always run
./test # Run all tests
./coverage # Check coverage
./lint # Run linters
```
Use consistently.
-1
View File
@@ -9,7 +9,6 @@ Scala 3.5.1 · Gradle 9
./compile # Compile all modules — always run
./test # Run all tests
./coverage # Check coverage
./lint # Run linters
```
Try to stick to these commands for consistency.
-1
View File
@@ -1,4 +1,3 @@
vars {
baseUrl: http://localhost:8080
ioBaseUrl: http://localhost:8081
}
-100
View File
@@ -1,100 +0,0 @@
meta {
name: Export FEN
type: http
seq: 1
}
http {
method: POST
url: {{ioBaseUrl}}/io/export/fen
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"board": {
"a1": {"color": "White", "pieceType": "Rook"},
"b1": {"color": "White", "pieceType": "Knight"},
"c1": {"color": "White", "pieceType": "Bishop"},
"d1": {"color": "White", "pieceType": "Queen"},
"e1": {"color": "White", "pieceType": "King"},
"f1": {"color": "White", "pieceType": "Bishop"},
"g1": {"color": "White", "pieceType": "Knight"},
"h1": {"color": "White", "pieceType": "Rook"},
"a2": {"color": "White", "pieceType": "Pawn"},
"b2": {"color": "White", "pieceType": "Pawn"},
"c2": {"color": "White", "pieceType": "Pawn"},
"d2": {"color": "White", "pieceType": "Pawn"},
"e2": {"color": "White", "pieceType": "Pawn"},
"f2": {"color": "White", "pieceType": "Pawn"},
"g2": {"color": "White", "pieceType": "Pawn"},
"h2": {"color": "White", "pieceType": "Pawn"},
"a7": {"color": "Black", "pieceType": "Pawn"},
"b7": {"color": "Black", "pieceType": "Pawn"},
"c7": {"color": "Black", "pieceType": "Pawn"},
"d7": {"color": "Black", "pieceType": "Pawn"},
"e7": {"color": "Black", "pieceType": "Pawn"},
"f7": {"color": "Black", "pieceType": "Pawn"},
"g7": {"color": "Black", "pieceType": "Pawn"},
"h7": {"color": "Black", "pieceType": "Pawn"},
"a8": {"color": "Black", "pieceType": "Rook"},
"b8": {"color": "Black", "pieceType": "Knight"},
"c8": {"color": "Black", "pieceType": "Bishop"},
"d8": {"color": "Black", "pieceType": "Queen"},
"e8": {"color": "Black", "pieceType": "King"},
"f8": {"color": "Black", "pieceType": "Bishop"},
"g8": {"color": "Black", "pieceType": "Knight"},
"h8": {"color": "Black", "pieceType": "Rook"}
},
"turn": "White",
"castlingRights": {
"whiteKingSide": true,
"whiteQueenSide": true,
"blackKingSide": true,
"blackQueenSide": true
},
"enPassantSquare": null,
"halfMoveClock": 0,
"moves": [],
"result": null,
"initialBoard": {
"a1": {"color": "White", "pieceType": "Rook"},
"b1": {"color": "White", "pieceType": "Knight"},
"c1": {"color": "White", "pieceType": "Bishop"},
"d1": {"color": "White", "pieceType": "Queen"},
"e1": {"color": "White", "pieceType": "King"},
"f1": {"color": "White", "pieceType": "Bishop"},
"g1": {"color": "White", "pieceType": "Knight"},
"h1": {"color": "White", "pieceType": "Rook"},
"a2": {"color": "White", "pieceType": "Pawn"},
"b2": {"color": "White", "pieceType": "Pawn"},
"c2": {"color": "White", "pieceType": "Pawn"},
"d2": {"color": "White", "pieceType": "Pawn"},
"e2": {"color": "White", "pieceType": "Pawn"},
"f2": {"color": "White", "pieceType": "Pawn"},
"g2": {"color": "White", "pieceType": "Pawn"},
"h2": {"color": "White", "pieceType": "Pawn"},
"a7": {"color": "Black", "pieceType": "Pawn"},
"b7": {"color": "Black", "pieceType": "Pawn"},
"c7": {"color": "Black", "pieceType": "Pawn"},
"d7": {"color": "Black", "pieceType": "Pawn"},
"e7": {"color": "Black", "pieceType": "Pawn"},
"f7": {"color": "Black", "pieceType": "Pawn"},
"g7": {"color": "Black", "pieceType": "Pawn"},
"h7": {"color": "Black", "pieceType": "Pawn"},
"a8": {"color": "Black", "pieceType": "Rook"},
"b8": {"color": "Black", "pieceType": "Knight"},
"c8": {"color": "Black", "pieceType": "Bishop"},
"d8": {"color": "Black", "pieceType": "Queen"},
"e8": {"color": "Black", "pieceType": "King"},
"f8": {"color": "Black", "pieceType": "Bishop"},
"g8": {"color": "Black", "pieceType": "Knight"},
"h8": {"color": "Black", "pieceType": "Rook"}
}
}
}
-100
View File
@@ -1,100 +0,0 @@
meta {
name: Export PGN
type: http
seq: 2
}
http {
method: POST
url: {{ioBaseUrl}}/io/export/pgn
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"board": {
"a1": {"color": "White", "pieceType": "Rook"},
"b1": {"color": "White", "pieceType": "Knight"},
"c1": {"color": "White", "pieceType": "Bishop"},
"d1": {"color": "White", "pieceType": "Queen"},
"e1": {"color": "White", "pieceType": "King"},
"f1": {"color": "White", "pieceType": "Bishop"},
"g1": {"color": "White", "pieceType": "Knight"},
"h1": {"color": "White", "pieceType": "Rook"},
"a2": {"color": "White", "pieceType": "Pawn"},
"b2": {"color": "White", "pieceType": "Pawn"},
"c2": {"color": "White", "pieceType": "Pawn"},
"d2": {"color": "White", "pieceType": "Pawn"},
"e2": {"color": "White", "pieceType": "Pawn"},
"f2": {"color": "White", "pieceType": "Pawn"},
"g2": {"color": "White", "pieceType": "Pawn"},
"h2": {"color": "White", "pieceType": "Pawn"},
"a7": {"color": "Black", "pieceType": "Pawn"},
"b7": {"color": "Black", "pieceType": "Pawn"},
"c7": {"color": "Black", "pieceType": "Pawn"},
"d7": {"color": "Black", "pieceType": "Pawn"},
"e7": {"color": "Black", "pieceType": "Pawn"},
"f7": {"color": "Black", "pieceType": "Pawn"},
"g7": {"color": "Black", "pieceType": "Pawn"},
"h7": {"color": "Black", "pieceType": "Pawn"},
"a8": {"color": "Black", "pieceType": "Rook"},
"b8": {"color": "Black", "pieceType": "Knight"},
"c8": {"color": "Black", "pieceType": "Bishop"},
"d8": {"color": "Black", "pieceType": "Queen"},
"e8": {"color": "Black", "pieceType": "King"},
"f8": {"color": "Black", "pieceType": "Bishop"},
"g8": {"color": "Black", "pieceType": "Knight"},
"h8": {"color": "Black", "pieceType": "Rook"}
},
"turn": "White",
"castlingRights": {
"whiteKingSide": true,
"whiteQueenSide": true,
"blackKingSide": true,
"blackQueenSide": true
},
"enPassantSquare": null,
"halfMoveClock": 0,
"moves": [],
"result": null,
"initialBoard": {
"a1": {"color": "White", "pieceType": "Rook"},
"b1": {"color": "White", "pieceType": "Knight"},
"c1": {"color": "White", "pieceType": "Bishop"},
"d1": {"color": "White", "pieceType": "Queen"},
"e1": {"color": "White", "pieceType": "King"},
"f1": {"color": "White", "pieceType": "Bishop"},
"g1": {"color": "White", "pieceType": "Knight"},
"h1": {"color": "White", "pieceType": "Rook"},
"a2": {"color": "White", "pieceType": "Pawn"},
"b2": {"color": "White", "pieceType": "Pawn"},
"c2": {"color": "White", "pieceType": "Pawn"},
"d2": {"color": "White", "pieceType": "Pawn"},
"e2": {"color": "White", "pieceType": "Pawn"},
"f2": {"color": "White", "pieceType": "Pawn"},
"g2": {"color": "White", "pieceType": "Pawn"},
"h2": {"color": "White", "pieceType": "Pawn"},
"a7": {"color": "Black", "pieceType": "Pawn"},
"b7": {"color": "Black", "pieceType": "Pawn"},
"c7": {"color": "Black", "pieceType": "Pawn"},
"d7": {"color": "Black", "pieceType": "Pawn"},
"e7": {"color": "Black", "pieceType": "Pawn"},
"f7": {"color": "Black", "pieceType": "Pawn"},
"g7": {"color": "Black", "pieceType": "Pawn"},
"h7": {"color": "Black", "pieceType": "Pawn"},
"a8": {"color": "Black", "pieceType": "Rook"},
"b8": {"color": "Black", "pieceType": "Knight"},
"c8": {"color": "Black", "pieceType": "Bishop"},
"d8": {"color": "Black", "pieceType": "Queen"},
"e8": {"color": "Black", "pieceType": "King"},
"f8": {"color": "Black", "pieceType": "Bishop"},
"g8": {"color": "Black", "pieceType": "Knight"},
"h8": {"color": "Black", "pieceType": "Rook"}
}
}
}
-4
View File
@@ -1,4 +0,0 @@
meta {
name: export
seq: 2
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: Import FEN
type: http
seq: 1
}
http {
method: POST
url: {{ioBaseUrl}}/io/import/fen
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
}
}
-22
View File
@@ -1,22 +0,0 @@
meta {
name: Import PGN
type: http
seq: 2
}
http {
method: POST
url: {{ioBaseUrl}}/io/import/pgn
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"pgn": "1. e4 e5 2. Nf3 Nc6 *"
}
}
-4
View File
@@ -1,4 +0,0 @@
meta {
name: import
seq: 1
}
+2 -6
View File
@@ -36,11 +36,7 @@ val coverageExclusions = listOf(
"**/core/src/main/scala/de/nowchess/chess/registry/GameEntry.scala",
"**/core/src/main/scala/de/nowchess/chess/registry/GameRegistryImpl.scala",
// GameResource — REST integration layer with @Inject var fields; mocking dependencies for unit tests is infeasible with Quarkus DI; integration tests would require @QuarkusTest which Scoverage doesn't instrument
"**/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala",
// IoResource — same rationale as GameResource; @QuarkusTest not instrumented by Scoverage
"**/io/src/main/scala/de/nowchess/io/service/resource/IoResource.scala",
// JacksonConfig — Quarkus lifecycle hook, no testable logic beyond ObjectMapper registration
"**/io/src/main/scala/de/nowchess/io/service/config/JacksonConfig.scala",
"**/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala"
)
// Converts a Sonar-style glob to a scoverage regex (matched against full source path).
@@ -76,7 +72,7 @@ sonar {
val versions = mapOf(
"QUARKUS_SCALA3" to "1.0.0",
"SCALA3" to "3.5.1",
"SCALA_LIBRARY" to "2.13.16",
"SCALA_LIBRARY" to "2.13.18",
"SCALATEST" to "3.2.19",
"SCALATEST_JUNIT" to "0.1.11",
"SCOVERAGE" to "2.1.1",
-3
View File
@@ -1,3 +0,0 @@
#! /usr/bin/env bash
./gradlew scalafix spotlessCheck
-20
View File
@@ -60,23 +60,3 @@
* 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))
## (2026-04-21)
### 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-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
## (2026-04-21)
### 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-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
@@ -1,3 +0,0 @@
package de.nowchess.api.dto
case class ImportFenRequest(fen: String)
@@ -1,3 +0,0 @@
package de.nowchess.api.dto
case class ImportPgnRequest(pgn: String)
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=10
MINOR=8
PATCH=0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,66 @@
{
"version": 1,
"created": "2026-04-13T19:58:38.629943",
"total_positions": 3022562,
"stockfish_depth": 12,
"sources": [
{
"type": "legacy_import",
"path": "data/training_data.jsonl",
"count": 2009355,
"note": "Migrated from data/training_data.jsonl"
},
{
"type": "test_extend",
"count": 4,
"actual_count": 3
},
{
"type": "test_new_positions",
"count": 3,
"actual_count": 3
},
{
"type": "test_mixed",
"count": 5,
"actual_count": 0
},
{
"type": "test_all_dups",
"count": 2,
"actual_count": 0
},
{
"type": "guaranteed_unique",
"count": 10,
"actual_count": 8
},
{
"type": "merged_sources",
"count": 600000,
"sources": [
{
"type": "tactical",
"count": 600000,
"max_puzzles": 600000
}
],
"actual_count": 599993
},
{
"type": "merged_sources",
"count": 500000,
"sources": [
{
"type": "lichess",
"count": 500000,
"params": {
"min_depth": 20,
"max_positions": 500000
}
}
],
"actual_count": 500000
}
]
}
-59
View File
@@ -341,62 +341,3 @@
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
## (2026-04-21)
### Features
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
* NCS-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-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-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-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-40 Rework Draw System ([#34](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/34)) ([0091d50](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0091d50467e9f955f23570128b96c977c01bc51b))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
### Bug Fixes
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
## (2026-04-21)
### Features
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
* NCS-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-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-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-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-40 Rework Draw System ([#34](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/34)) ([0091d50](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0091d50467e9f955f23570128b96c977c01bc51b))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
* NCS-53 changed IO to MicroService for easier scaling ([#37](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/37)) ([9b51852](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9b5185298e9e721e6103ea8372ca29073913775c))
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
### Bug Fixes
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
+1 -5
View File
@@ -48,6 +48,7 @@ dependencies {
}
implementation(project(":modules:api"))
implementation(project(":modules:io"))
implementation(project(":modules:rule"))
implementation(project(":modules:bot"))
@@ -67,15 +68,11 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
testImplementation(project(":modules:io"))
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.quarkus:quarkus-junit5-mockito")
testImplementation("io.rest-assured:rest-assured")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
@@ -118,4 +115,3 @@ tasks.reportScoverage {
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
@@ -3,7 +3,7 @@
#
# Before building the container image run:
#
# ./gradlew :modules:core:build -Dquarkus.native.enabled=true
# ./gradlew build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
@@ -13,7 +13,7 @@
#
# docker run -i --rm -p 8080:8080 quarkus/backcore
#
# The `registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
# The ` registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`.
###
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7
@@ -21,7 +21,7 @@ WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 modules/core/build/*-runner /work/application
COPY --chown=1001:root --chmod=0755 build/*-runner /work/application
EXPOSE 8080
USER 1001
@@ -1,8 +1,3 @@
quarkus:
http:
port: 8080
application:
name: nowchess-core
rest-client:
io-service:
url: http://localhost:8081
greeting:
message: "hello"
@@ -0,0 +1,2 @@
greeting:
message: "hello"
@@ -0,0 +1,6 @@
-- This file allow to write SQL commands that will be emitted in test and dev.
-- The commands are commented as their support depends of the database
-- insert into myentity (id, field) values(1, 'field-1');
-- insert into myentity (id, field) values(2, 'field-2');
-- insert into myentity (id, field) values(3, 'field-3');
-- alter sequence myentity_seq restart with 4;
@@ -1,35 +0,0 @@
package de.nowchess.chess.client
import de.nowchess.api.dto.{ImportFenRequest, ImportPgnRequest}
import de.nowchess.api.game.GameContext
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
@Path("/io")
@RegisterRestClient(configKey = "io-service")
trait IoServiceClient:
@POST
@Path("/import/fen")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def importFen(body: ImportFenRequest): GameContext
@POST
@Path("/import/pgn")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def importPgn(body: ImportPgnRequest): GameContext
@POST
@Path("/export/fen")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.TEXT_PLAIN))
def exportFen(ctx: GameContext): String
@POST
@Path("/export/pgn")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array("application/x-chess-pgn"))
def exportPgn(ctx: GameContext): String
@@ -1,23 +1,11 @@
package de.nowchess.chess.config
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.Square
import io.quarkus.jackson.ObjectMapperCustomizer
import jakarta.inject.Singleton
@Singleton
class JacksonConfig extends ObjectMapperCustomizer:
def customize(mapper: ObjectMapper): Unit =
mapper.registerModule(new DefaultScalaModule() {
override def version(): Version =
// scalafix:off DisableSyntax.null
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
// scalafix:on DisableSyntax.null
})
val squareModule = new SimpleModule()
squareModule.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
squareModule.addKeySerializer(classOf[Square], new SquareKeySerializer())
mapper.registerModule(squareModule)
mapper.registerModule(DefaultScalaModule)
@@ -1,9 +1,6 @@
package de.nowchess.chess.config
import de.nowchess.api.board.{CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.dto.*
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import io.quarkus.runtime.annotations.RegisterForReflection
@RegisterForReflection(
@@ -21,19 +18,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection
classOf[LegalMovesResponseDto],
classOf[OkResponseDto],
classOf[PlayerInfoDto],
classOf[GameContext],
classOf[Color],
classOf[Piece],
classOf[PieceType],
classOf[CastlingRights],
classOf[Square],
classOf[File],
classOf[Rank],
classOf[Move],
classOf[MoveType],
classOf[PromotionPiece],
classOf[GameResult],
classOf[DrawReason],
),
)
class NativeReflectionConfig
@@ -1,8 +0,0 @@
package de.nowchess.chess.config
import com.fasterxml.jackson.databind.{DeserializationContext, KeyDeserializer}
import de.nowchess.api.board.Square
class SquareKeyDeserializer extends KeyDeserializer:
override def deserializeKey(key: String, ctx: DeserializationContext): AnyRef =
Square.fromAlgebraic(key).orNull
@@ -1,9 +0,0 @@
package de.nowchess.chess.config
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
import de.nowchess.api.board.Square
class SquareKeySerializer extends JsonSerializer[Square]:
override def serialize(value: Square, gen: JsonGenerator, provider: SerializerProvider): Unit =
gen.writeFieldName(value.toString)
@@ -7,7 +7,7 @@ import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.chess.controller.Parser
import de.nowchess.chess.observer.*
import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult}
import de.nowchess.api.io.{GameContextExport, GameContextImport}
import de.nowchess.io.{GameContextExport, GameContextImport}
import de.nowchess.rules.RuleSet
import de.nowchess.rules.sets.DefaultRules
@@ -6,18 +6,18 @@ import de.nowchess.api.dto.*
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.chess.client.IoServiceClient
import de.nowchess.chess.controller.Parser
import de.nowchess.chess.engine.GameEngine
import de.nowchess.chess.exception.{BadRequestException, GameNotFoundException}
import de.nowchess.chess.observer.*
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
import de.nowchess.io.fen.{FenExporter, FenParser}
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
import io.smallrye.mutiny.Multi
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
import org.eclipse.microprofile.rest.client.inject.RestClient
import java.util.concurrent.atomic.AtomicReference
import scala.compiletime.uninitialized
@@ -32,10 +32,6 @@ class GameResource:
@Inject
var objectMapper: ObjectMapper = uninitialized
@Inject
@RestClient
var ioClient: IoServiceClient = uninitialized
// scalafix:on DisableSyntax.var
private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1")
@@ -86,8 +82,16 @@ class GameResource:
private def toGameStateDto(entry: GameEntry): GameStateDto =
val ctx = entry.engine.context
GameStateDto(
fen = ioClient.exportFen(ctx),
pgn = ioClient.exportPgn(ctx),
fen = FenExporter.exportGameContext(ctx),
pgn = PgnExporter.exportGame(
Map(
"Event" -> "NowChess game",
"White" -> entry.white.displayName,
"Black" -> entry.black.displayName,
"Result" -> "*",
),
ctx.moves,
),
turn = ctx.turn.label.toLowerCase,
status = statusOf(entry),
winner = ctx.result.collect { case GameResult.Win(c) => c.label.toLowerCase },
@@ -259,7 +263,9 @@ class GameResource:
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def importFen(body: ImportFenRequestDto): Response =
val ctx = ioClient.importFen(ImportFenRequest(body.fen))
val ctx = FenParser.parseFen(body.fen) match
case Left(err) => throw BadRequestException("INVALID_FEN", err, Some("fen"))
case Right(ctx) => ctx
val white = playerInfoFrom(body.white, DefaultWhite)
val black = playerInfoFrom(body.black, DefaultBlack)
val entry = newEntry(ctx, white, black)
@@ -271,8 +277,11 @@ class GameResource:
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def importPgn(body: ImportPgnRequestDto): Response =
val ctx = ioClient.importPgn(ImportPgnRequest(body.pgn))
val entry = newEntry(ctx, DefaultWhite, DefaultBlack)
val engine = GameEngine()
engine.loadGame(PgnParser, body.pgn) match
case Left(err) => throw BadRequestException("INVALID_PGN", err, Some("pgn"))
case Right(_) => ()
val entry = GameEntry(registry.generateId(), engine, DefaultWhite, DefaultBlack)
registry.store(entry)
created(toGameFullDto(entry))
@@ -281,12 +290,21 @@ class GameResource:
@Produces(Array(MediaType.TEXT_PLAIN))
def exportFen(@PathParam("gameId") gameId: String): Response =
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
ok(ioClient.exportFen(entry.engine.context))
ok(FenExporter.exportGameContext(entry.engine.context))
@GET
@Path("/{gameId}/export/pgn")
@Produces(Array("application/x-chess-pgn"))
def exportPgn(@PathParam("gameId") gameId: String): Response =
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
ok(ioClient.exportPgn(entry.engine.context))
val pgn = PgnExporter.exportGame(
Map(
"Event" -> "NowChess game",
"White" -> entry.white.displayName,
"Black" -> entry.black.displayName,
"Result" -> "*",
),
entry.engine.context.moves,
)
ok(pgn)
// scalafix:on DisableSyntax.throw
@@ -4,7 +4,7 @@ import de.nowchess.api.board.{Board, Color, File, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, InvalidMoveReason, MoveRedoneEvent, Observer}
import de.nowchess.api.io.GameContextImport
import de.nowchess.io.GameContextImport
import de.nowchess.rules.RuleSet
import de.nowchess.rules.sets.DefaultRules
import org.scalatest.funsuite.AnyFunSuite
@@ -1,13 +1,16 @@
package de.nowchess.chess.engine
import de.nowchess.chess.observer.{GameEvent, Observer}
import scala.collection.mutable
import de.nowchess.api.board.{Board, Color}
import de.nowchess.api.game.GameContext
import de.nowchess.chess.observer.{GameEvent, Observer, PgnLoadedEvent}
import de.nowchess.io.pgn.PgnParser
import de.nowchess.io.fen.FenParser
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
import de.nowchess.io.pgn.PgnExporter
import de.nowchess.io.fen.FenExporter
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import scala.collection.mutable
class GameEngineLoadGameTest extends AnyFunSuite with Matchers:
test("loadGame with PgnParser: loads valid PGN and enables undo/redo"):
@@ -1,19 +1,11 @@
package de.nowchess.chess.resource
import de.nowchess.api.dto.*
import de.nowchess.api.game.GameContext
import de.nowchess.chess.client.IoServiceClient
import de.nowchess.chess.exception.BadRequestException
import de.nowchess.io.fen.FenExporter
import de.nowchess.io.pgn.PgnParser
import io.quarkus.test.InjectMock
import io.quarkus.test.junit.QuarkusTest
import jakarta.inject.Inject
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.junit.jupiter.api.{BeforeEach, DisplayName, Test}
import org.junit.jupiter.api.{DisplayName, Test}
import org.junit.jupiter.api.Assertions.*
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.when
import scala.compiletime.uninitialized
@@ -25,19 +17,6 @@ class GameResourceIntegrationTest:
@Inject
var resource: GameResource = uninitialized
@InjectMock
@RestClient
var ioClient: IoServiceClient = uninitialized
@BeforeEach
def setupMocks(): Unit =
when(ioClient.importFen(any())).thenReturn(GameContext.initial)
when(ioClient.importPgn(any())).thenReturn(
PgnParser.importGameContext("1. e4 c5").toOption.get,
)
when(ioClient.exportFen(any())).thenReturn(FenExporter.exportGameContext(GameContext.initial))
when(ioClient.exportPgn(any())).thenReturn("1. e4 c5")
@Test
@DisplayName("createGame returns 201")
def testCreateGame(): Unit =
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=18
MINOR=16
PATCH=0
-23
View File
@@ -76,26 +76,3 @@
* NCS-30 FEN Parser using ParserCombinators ([#21](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/21)) ([b4bc72f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b4bc72f7e49f94d6e1bc805c68680e5fe8ef8e36))
* NCS-31 FastParse FEN ([#22](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/22)) ([7a045d3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7a045d31d757bbc5aa6f4bad2664ebe8b8519cac))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
## (2026-04-21)
### 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-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-29 JSON - Cherry Picked ([#28](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/28)) ([dbcafd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dbcafd286993e0604a6fa286c5543581a149439e))
* NCS-30 FEN Parser using ParserCombinators ([#21](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/21)) ([b4bc72f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b4bc72f7e49f94d6e1bc805c68680e5fe8ef8e36))
* NCS-31 FastParse FEN ([#22](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/22)) ([7a045d3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7a045d31d757bbc5aa6f4bad2664ebe8b8519cac))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
## (2026-04-21)
### 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-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-29 JSON - Cherry Picked ([#28](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/28)) ([dbcafd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dbcafd286993e0604a6fa286c5543581a149439e))
* NCS-30 FEN Parser using ParserCombinators ([#21](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/21)) ([b4bc72f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b4bc72f7e49f94d6e1bc805c68680e5fe8ef8e36))
* NCS-31 FastParse FEN ([#22](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/22)) ([7a045d3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7a045d31d757bbc5aa6f4bad2664ebe8b8519cac))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
* NCS-53 changed IO to MicroService for easier scaling ([#37](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/37)) ([9b51852](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9b5185298e9e721e6103ea8372ca29073913775c))
+2 -42
View File
@@ -1,7 +1,6 @@
plugins {
id("scala")
id("org.scoverage") version "8.1"
id("io.quarkus")
}
group = "de.nowchess"
@@ -29,10 +28,6 @@ tasks.withType<ScalaCompile> {
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
}
val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project
dependencies {
compileOnly("org.scala-lang:scala3-compiler_3") {
@@ -58,51 +53,19 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${versions["JACKSON"]!!}")
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-rest")
implementation("io.quarkus:quarkus-rest-jackson")
implementation("io.quarkus:quarkus-arc")
implementation("io.quarkus:quarkus-config-yaml")
implementation("io.quarkus:quarkus-smallrye-health")
implementation("io.quarkus:quarkus-smallrye-openapi")
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.rest-assured:rest-assured")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
}
configurations.scoverage {
resolutionStrategy.eachDependency {
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
}
}
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
}
tasks.withType<Jar>().configureEach {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.test {
useJUnitPlatform {
includeEngines("scalatest", "junit-jupiter")
includeEngines("scalatest")
testLogging {
events("passed", "skipped", "failed")
events("skipped", "failed")
}
}
finalizedBy(tasks.reportScoverage)
@@ -110,6 +73,3 @@ tasks.test {
tasks.reportScoverage {
dependsOn(tasks.test)
}
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
@@ -1,13 +0,0 @@
quarkus:
http:
port: 8081
application:
name: nowchess-io
smallrye-openapi:
info-title: NowChess IO Service
info-version: 1.0.0
info-description: Chess notation import and export — FEN and PGN
path: /openapi
swagger-ui:
always-include: true
path: /swagger-ui
@@ -1,6 +1,7 @@
package de.nowchess.api.io
package de.nowchess.io
import de.nowchess.api.game.GameContext
trait GameContextExport:
def exportGameContext(context: GameContext): String
@@ -1,6 +1,7 @@
package de.nowchess.api.io
package de.nowchess.io
import de.nowchess.api.game.GameContext
trait GameContextImport:
def importGameContext(input: String): Either[String, GameContext]
@@ -1,8 +1,6 @@
package de.nowchess.io
import de.nowchess.api.game.GameContext
import de.nowchess.api.io.{GameContextExport, GameContextImport}
import java.nio.file.{Files, Path}
import java.nio.charset.StandardCharsets
import scala.util.Try
@@ -2,7 +2,7 @@ package de.nowchess.io.fen
import de.nowchess.api.board.*
import de.nowchess.api.game.GameContext
import de.nowchess.api.io.GameContextExport
import de.nowchess.io.GameContextExport
object FenExporter extends GameContextExport:
@@ -2,7 +2,7 @@ package de.nowchess.io.fen
import de.nowchess.api.board.*
import de.nowchess.api.game.GameContext
import de.nowchess.api.io.GameContextImport
import de.nowchess.io.GameContextImport
object FenParser extends GameContextImport:
@@ -2,10 +2,9 @@ package de.nowchess.io.fen
import de.nowchess.api.board.*
import de.nowchess.api.game.GameContext
import de.nowchess.io.GameContextImport
import scala.util.parsing.combinator.RegexParsers
import FenParserSupport.*
import de.nowchess.api.io.GameContextImport
object FenParserCombinators extends RegexParsers with GameContextImport:
@@ -4,8 +4,8 @@ import fastparse.*
import fastparse.NoWhitespace.*
import de.nowchess.api.board.*
import de.nowchess.api.game.GameContext
import de.nowchess.io.GameContextImport
import FenParserSupport.*
import de.nowchess.api.io.GameContextImport
object FenParserFastParse extends GameContextImport:
@@ -6,9 +6,8 @@ import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.*
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.game.GameContext
import de.nowchess.api.io.GameContextExport
import de.nowchess.io.GameContextExport
import de.nowchess.io.pgn.PgnExporter
import java.time.{LocalDate, ZoneId, ZonedDateTime}
/** Exports a GameContext to a comprehensive JSON format using Jackson.
@@ -5,8 +5,7 @@ import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.*
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.game.GameContext
import de.nowchess.api.io.GameContextImport
import de.nowchess.io.GameContextImport
import scala.util.Try
/** Imports a GameContext from JSON format using Jackson.
@@ -1,8 +0,0 @@
package de.nowchess.io.json
import com.fasterxml.jackson.databind.{DeserializationContext, KeyDeserializer}
import de.nowchess.api.board.Square
class SquareKeyDeserializer extends KeyDeserializer:
override def deserializeKey(key: String, ctx: DeserializationContext): AnyRef =
Square.fromAlgebraic(key).orNull
@@ -1,9 +0,0 @@
package de.nowchess.io.json
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
import de.nowchess.api.board.Square
class SquareKeySerializer extends JsonSerializer[Square]:
override def serialize(value: Square, gen: JsonGenerator, provider: SerializerProvider): Unit =
gen.writeFieldName(value.toString)
@@ -3,7 +3,7 @@ package de.nowchess.io.pgn
import de.nowchess.api.board.*
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.game.GameContext
import de.nowchess.api.io.GameContextExport
import de.nowchess.io.GameContextExport
import de.nowchess.rules.sets.DefaultRules
object PgnExporter extends GameContextExport:
@@ -3,7 +3,7 @@ package de.nowchess.io.pgn
import de.nowchess.api.board.*
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.game.GameContext
import de.nowchess.api.io.GameContextImport
import de.nowchess.io.GameContextImport
import de.nowchess.rules.sets.DefaultRules
/** A parsed PGN game containing headers and the resolved move list. */
@@ -1,24 +0,0 @@
package de.nowchess.io.service.config
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.Square
import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer}
import io.quarkus.jackson.ObjectMapperCustomizer
import jakarta.inject.Singleton
@Singleton
class JacksonConfig extends ObjectMapperCustomizer:
def customize(mapper: ObjectMapper): Unit =
mapper.registerModule(new DefaultScalaModule() {
override def version(): Version =
// scalafix:off DisableSyntax.null
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
// scalafix:on DisableSyntax.null
})
val squareModule = new SimpleModule()
squareModule.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
squareModule.addKeySerializer(classOf[Square], new SquareKeySerializer())
mapper.registerModule(squareModule)
@@ -1,29 +0,0 @@
package de.nowchess.io.service.config
import de.nowchess.api.board.{CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest, IoErrorDto}
import io.quarkus.runtime.annotations.RegisterForReflection
@RegisterForReflection(
targets = Array(
classOf[ImportFenRequest],
classOf[ImportPgnRequest],
classOf[IoErrorDto],
classOf[GameContext],
classOf[GameResult],
classOf[DrawReason],
classOf[Color],
classOf[Piece],
classOf[PieceType],
classOf[CastlingRights],
classOf[Square],
classOf[File],
classOf[Rank],
classOf[Move],
classOf[MoveType],
classOf[PromotionPiece],
),
)
class NativeReflectionConfig
@@ -1,3 +0,0 @@
package de.nowchess.io.service.dto
case class ImportFenRequest(fen: String)
@@ -1,3 +0,0 @@
package de.nowchess.io.service.dto
case class ImportPgnRequest(pgn: String)
@@ -1,3 +0,0 @@
package de.nowchess.io.service.dto
case class IoErrorDto(code: String, message: String)
@@ -1,77 +0,0 @@
package de.nowchess.io.service.resource
import de.nowchess.api.game.GameContext
import de.nowchess.io.fen.{FenExporter, FenParser}
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest, IoErrorDto}
import io.smallrye.mutiny.Uni
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
import org.eclipse.microprofile.openapi.annotations.Operation
import org.eclipse.microprofile.openapi.annotations.media.{Content, Schema}
import org.eclipse.microprofile.openapi.annotations.responses.{APIResponse, APIResponses}
import org.eclipse.microprofile.openapi.annotations.tags.Tag
@Path("/io")
@ApplicationScoped
@Tag(name = "IO", description = "Chess notation import and export")
class IoResource:
@POST
@Path("/import/fen")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
@Operation(summary = "Import FEN", description = "Parse a FEN string into a GameContext")
@APIResponses(
Array(
new APIResponse(responseCode = "200", description = "Parsed GameContext"),
new APIResponse(responseCode = "400", description = "Invalid FEN"),
),
)
def importFen(body: ImportFenRequest): Uni[Response] =
Uni.createFrom().item {
FenParser.parseFen(body.fen) match
case Left(err) =>
Response.status(400).entity(IoErrorDto("INVALID_FEN", err)).build()
case Right(ctx) =>
Response.ok(ctx).build()
}
@POST
@Path("/import/pgn")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
@Operation(summary = "Import PGN", description = "Parse a PGN string into a GameContext")
@APIResponses(
Array(
new APIResponse(responseCode = "200", description = "Parsed GameContext"),
new APIResponse(responseCode = "400", description = "Invalid PGN"),
),
)
def importPgn(body: ImportPgnRequest): Uni[Response] =
Uni.createFrom().item {
PgnParser.importGameContext(body.pgn) match
case Left(err) =>
Response.status(400).entity(IoErrorDto("INVALID_PGN", err)).build()
case Right(ctx) =>
Response.ok(ctx).build()
}
@POST
@Path("/export/fen")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.TEXT_PLAIN))
@Operation(summary = "Export FEN", description = "Serialize a GameContext to FEN notation")
@APIResponse(responseCode = "200", description = "FEN string")
def exportFen(ctx: GameContext): Uni[Response] =
Uni.createFrom().item(Response.ok(FenExporter.exportGameContext(ctx)).build())
@POST
@Path("/export/pgn")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array("application/x-chess-pgn"))
@Operation(summary = "Export PGN", description = "Serialize a GameContext to PGN notation")
@APIResponse(responseCode = "200", description = "PGN text")
def exportPgn(ctx: GameContext): Uni[Response] =
Uni.createFrom().item(Response.ok(PgnExporter.exportGameContext(ctx)).build())
@@ -1,14 +1,13 @@
package de.nowchess.io
import de.nowchess.api.board.{File, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.io.GameContextExport
import de.nowchess.api.board.{File, Rank, Square}
import de.nowchess.api.move.Move
import de.nowchess.io.json.{JsonExporter, JsonParser}
import java.nio.file.{Files, Paths}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import java.nio.file.{Files, Paths}
import scala.util.Using
class GameFileServiceSuite extends AnyFunSuite with Matchers:
@@ -130,7 +130,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
// Edge cases with defaults
test("parse invalid turn color returns error") {
val json = """{
val json = """{
"metadata": {},
"gameState": {"turn": "Invalid", "board": []},
"moves": []
@@ -141,7 +141,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse invalid piece type filters it out") {
val json = """{
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
@@ -158,7 +158,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse invalid color in board filters piece") {
val json = """{
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
@@ -175,7 +175,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse with missing turn uses default") {
val json = """{
val json = """{
"metadata": {},
"gameState": {"board": []},
"moves": []
@@ -187,7 +187,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse with missing board uses empty") {
val json = """{
val json = """{
"metadata": {},
"gameState": {"turn": "White"},
"moves": []
@@ -199,7 +199,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse with missing moves uses empty list") {
val json = """{
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []}
}"""
@@ -210,7 +210,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse invalid square in board filters it") {
val json = """{
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
@@ -227,7 +227,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse all valid piece types") {
val json = """{
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
@@ -255,7 +255,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse with all castling rights false") {
val json = """{
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
@@ -278,7 +278,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
// Move type parsing tests
test("parse all move type variations") {
val json = """{
val json = """{
"metadata": {"event": "Game", "result": "*"},
"gameState": {"turn": "White", "board": []},
"moves": [
@@ -303,7 +303,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse invalid move type defaults to None") {
val json = """{
val json = """{
"metadata": {"event": "Game"},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "e2", "to": "e4", "type": {"type": "unknown"}}]
@@ -313,7 +313,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse promotion with invalid piece uses default") {
val json = """{
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "invalid"}}]
@@ -323,7 +323,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse move with invalid from/to skips it") {
val json = """{
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "e2", "to": "invalid", "type": {"type": "normal"}}]
@@ -335,7 +335,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse normal move with isCapture true") {
val json = """{
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "e4", "to": "d5", "type": {"type": "normal", "isCapture": true}}]
@@ -348,7 +348,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse board with invalid pieces filters them") {
val json = """{
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
@@ -366,7 +366,7 @@ class JsonParserTest extends AnyFunSuite with Matchers:
}
test("parse with empty board") {
val json = """{
val json = """{
"metadata": {"event": "Game", "players": {"white": "A", "black": "B"}, "date": "2026-04-06", "result": "*"},
"gameState": {
"board": [],
@@ -1,62 +0,0 @@
package de.nowchess.io.json
import com.fasterxml.jackson.core.`type`.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.{File, Rank, Square}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class SquareKeyDeserializerTest extends AnyFunSuite with Matchers:
private def mapper: ObjectMapper =
val m = new ObjectMapper()
val mod = new SimpleModule()
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
m.registerModule(DefaultScalaModule)
m.registerModule(mod)
m
private def readMap(json: String): Map[Square, Int] =
mapper.readValue(json, new TypeReference[Map[Square, Int]] {})
test("deserializes valid algebraic key") {
val result = readMap("""{"e4":1}""")
result(Square(File.E, Rank.R4)) shouldBe 1
}
test("deserializes a1 corner") {
val result = readMap("""{"a1":1}""")
result(Square(File.A, Rank.R1)) shouldBe 1
}
test("deserializes h8 corner") {
val result = readMap("""{"h8":1}""")
result(Square(File.H, Rank.R8)) shouldBe 1
}
test("deserializes multiple squares") {
val result = readMap("""{"a1":1,"h8":2,"e4":3}""")
result(Square(File.A, Rank.R1)) shouldBe 1
result(Square(File.H, Rank.R8)) shouldBe 2
result(Square(File.E, Rank.R4)) shouldBe 3
}
// scalafix:off DisableSyntax.null
test("deserializeKey returns null for invalid square") {
new SquareKeyDeserializer().deserializeKey("invalid", null) shouldBe null
}
test("deserializeKey returns null for wrong-length key") {
new SquareKeyDeserializer().deserializeKey("e44", null) shouldBe null
}
test("deserializeKey returns null for bad file") {
new SquareKeyDeserializer().deserializeKey("z4", null) shouldBe null
}
test("deserializeKey returns null for bad rank") {
new SquareKeyDeserializer().deserializeKey("e9", null) shouldBe null
}
// scalafix:on DisableSyntax.null
@@ -1,50 +0,0 @@
package de.nowchess.io.json
import com.fasterxml.jackson.core.`type`.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.{File, Rank, Square}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class SquareKeySerializerTest extends AnyFunSuite with Matchers:
private def mapper: ObjectMapper =
val m = new ObjectMapper()
val mod = new SimpleModule()
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
m.registerModule(DefaultScalaModule)
m.registerModule(mod)
m
test("serializes square as algebraic notation") {
val json = mapper.writeValueAsString(Map(Square(File.E, Rank.R4) -> 1))
json should include("\"e4\"")
}
test("serializes a1 corner") {
val json = mapper.writeValueAsString(Map(Square(File.A, Rank.R1) -> 1))
json should include("\"a1\"")
}
test("serializes h8 corner") {
val json = mapper.writeValueAsString(Map(Square(File.H, Rank.R8) -> 1))
json should include("\"h8\"")
}
test("round-trips with SquareKeyDeserializer") {
val rt = {
val m = new ObjectMapper()
val mod = new SimpleModule()
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
m.registerModule(DefaultScalaModule)
m.registerModule(mod)
m
}
val original = Map(Square(File.D, Rank.R5) -> 99)
val json = rt.writeValueAsString(original)
val result = rt.readValue(json, new TypeReference[Map[Square, Int]] {})
result shouldBe original
}
@@ -1,81 +0,0 @@
package de.nowchess.io.service.resource
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.Square
import de.nowchess.api.game.GameContext
import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer}
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import io.restassured.http.ContentType
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
@QuarkusTest
class IoResourceTest:
private lazy val testMapper: ObjectMapper =
val m = new ObjectMapper()
val mod = new SimpleModule()
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
m.registerModule(new DefaultScalaModule())
m.registerModule(mod)
m
private def contextJson(ctx: GameContext): String = testMapper.writeValueAsString(ctx)
@Test
def importFenReturns200(): Unit =
val resp = RestAssured
.`given`()
.contentType(ContentType.JSON)
.body("""{"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"}""")
.post("/io/import/fen")
assertEquals(200, resp.statusCode())
@Test
def importFenInvalidReturns400(): Unit =
val resp = RestAssured
.`given`()
.contentType(ContentType.JSON)
.body("""{"fen":"not-a-fen"}""")
.post("/io/import/fen")
assertEquals(400, resp.statusCode())
@Test
def importPgnReturns200(): Unit =
val resp = RestAssured
.`given`()
.contentType(ContentType.JSON)
.body("""{"pgn":"1. e4 e5"}""")
.post("/io/import/pgn")
assertEquals(200, resp.statusCode())
@Test
def importPgnInvalidReturns400(): Unit =
val resp = RestAssured
.`given`()
.contentType(ContentType.JSON)
.body("""{"pgn":"not valid pgn !!!###"}""")
.post("/io/import/pgn")
assertEquals(400, resp.statusCode())
@Test
def exportFenReturns200WithFen(): Unit =
val resp = RestAssured
.`given`()
.contentType(ContentType.JSON)
.body(contextJson(GameContext.initial))
.post("/io/export/fen")
assertEquals(200, resp.statusCode())
assertTrue(resp.getBody.asString().contains("rnbqkbnr"))
@Test
def exportPgnReturns200(): Unit =
val resp = RestAssured
.`given`()
.contentType(ContentType.JSON)
.body(contextJson(GameContext.initial))
.post("/io/export/pgn")
assertEquals(200, resp.statusCode())
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=12
MINOR=10
PATCH=0
-26
View File
@@ -79,29 +79,3 @@
### Bug Fixes
* NCS-32 Queenside Castle doesn't care about pieces in the way ([#23](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/23)) ([fe8e3c0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fe8e3c05397f433bfa34d1999e9738c82790adf7))
## (2026-04-21)
### 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-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-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
### Bug Fixes
* NCS-32 Queenside Castle doesn't care about pieces in the way ([#23](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/23)) ([fe8e3c0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fe8e3c05397f433bfa34d1999e9738c82790adf7))
## (2026-04-21)
### 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-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-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
### Bug Fixes
* NCS-32 Queenside Castle doesn't care about pieces in the way ([#23](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/23)) ([fe8e3c0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fe8e3c05397f433bfa34d1999e9738c82790adf7))
+47 -2
View File
@@ -1,6 +1,7 @@
plugins {
id("scala")
id("org.scoverage") version "8.1"
id("io.quarkus")
}
group = "de.nowchess"
@@ -25,6 +26,10 @@ tasks.withType<ScalaCompile> {
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
}
val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project
dependencies {
compileOnly("org.scala-lang:scala3-compiler_3") {
@@ -40,20 +45,56 @@ dependencies {
implementation(project(":modules:api"))
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-rest")
implementation("io.quarkus:quarkus-hibernate-orm")
implementation("io.quarkus:quarkus-rest-client-jackson")
implementation("io.quarkus:quarkus-rest-client")
implementation("io.quarkus:quarkus-rest-jackson")
implementation("io.quarkus:quarkus-config-yaml")
implementation("io.quarkus:quarkus-smallrye-fault-tolerance")
implementation("io.quarkus:quarkus-smallrye-jwt")
implementation("io.quarkus:quarkus-smallrye-health")
implementation("io.quarkus:quarkus-micrometer")
implementation("io.quarkus:quarkus-arc")
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
testImplementation(project(":modules:io"))
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.rest-assured:rest-assured")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
}
configurations.scoverage {
resolutionStrategy.eachDependency {
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
}
}
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
}
tasks.withType<Jar>().configureEach {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.test {
useJUnitPlatform {
includeEngines("scalatest")
includeEngines("scalatest", "junit-jupiter")
testLogging {
events("skipped", "failed")
events("passed", "skipped", "failed")
}
}
finalizedBy(tasks.reportScoverage)
@@ -61,3 +102,7 @@ tasks.test {
tasks.reportScoverage {
dependsOn(tasks.test)
}
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
@@ -3,17 +3,17 @@
#
# Before building the container image run:
#
# ./gradlew :modules:io:build -Dquarkus.native.enabled=true
# ./gradlew build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t quarkus/backio .
# docker build -f src/main/docker/Dockerfile.native -t quarkus/backcore .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/backio
# docker run -i --rm -p 8080:8080 quarkus/backcore
#
# The `registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
# The ` registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`.
###
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7
@@ -21,7 +21,7 @@ WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 modules/io/build/*-runner /work/application
COPY --chown=1001:root --chmod=0755 build/*-runner /work/application
EXPOSE 8080
USER 1001
@@ -0,0 +1,5 @@
quarkus:
http:
port: 8081
application:
name: rule-service
@@ -0,0 +1,11 @@
package de.nowchess.rules.config
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import io.quarkus.jackson.ObjectMapperCustomizer
import jakarta.inject.Singleton
@Singleton
class JacksonConfig extends ObjectMapperCustomizer:
def customize(mapper: ObjectMapper): Unit =
mapper.registerModule(DefaultScalaModule)
@@ -0,0 +1,128 @@
package de.nowchess.rules.dto
import de.nowchess.api.board.{Board, CastlingRights, Color, Piece, PieceType, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
object DtoMapper:
def toColor(s: String): Either[String, Color] = s match
case "White" => Right(Color.White)
case "Black" => Right(Color.Black)
case other => Left(s"Unknown color: $other")
def toPieceType(s: String): Either[String, PieceType] = s match
case "Pawn" => Right(PieceType.Pawn)
case "Knight" => Right(PieceType.Knight)
case "Bishop" => Right(PieceType.Bishop)
case "Rook" => Right(PieceType.Rook)
case "Queen" => Right(PieceType.Queen)
case "King" => Right(PieceType.King)
case other => Left(s"Unknown piece type: $other")
def toSquare(s: String): Either[String, Square] =
Square.fromAlgebraic(s).toRight(s"Invalid square: $s")
def toMoveType(dto: MoveDto): Either[String, MoveType] = dto.moveType match
case "normal" => Right(MoveType.Normal(isCapture = false))
case "capture" => Right(MoveType.Normal(isCapture = true))
case "castleKingside" => Right(MoveType.CastleKingside)
case "castleQueenside" => Right(MoveType.CastleQueenside)
case "enPassant" => Right(MoveType.EnPassant)
case "promotion" =>
dto.promotionPiece.toRight("Missing promotion piece").flatMap(toPromotionPiece).map(MoveType.Promotion(_))
case other => Left(s"Unknown move type: $other")
def toMove(dto: MoveDto): Either[String, Move] =
for
from <- toSquare(dto.from)
to <- toSquare(dto.to)
moveType <- toMoveType(dto)
yield Move(from, to, moveType)
def toBoard(pieces: List[PieceOnSquareDto]): Either[String, Board] =
sequenceList(pieces.map(toPieceOnSquare)).map(entries => Board(entries.toMap))
def toGameContext(dto: GameContextDto): Either[String, GameContext] =
for
board <- toBoard(dto.board)
turn <- toColor(dto.turn)
epSquare <- sequenceOpt(dto.enPassantSquare.map(toSquare))
moves <- sequenceList(dto.moves.map(toMove))
initialBoard <- toBoard(dto.initialBoard)
yield GameContext(
board = board,
turn = turn,
castlingRights = toCastlingRights(dto.castlingRights),
enPassantSquare = epSquare,
halfMoveClock = dto.halfMoveClock,
moves = moves,
initialBoard = initialBoard,
)
def fromMove(move: Move): MoveDto =
val (moveType, promotionPiece) = fromMoveType(move.moveType)
MoveDto(move.from.toString, move.to.toString, moveType, promotionPiece)
def fromBoard(board: Board): List[PieceOnSquareDto] =
board.pieces.toList.map { case (sq, p) =>
PieceOnSquareDto(sq.toString, p.color.label, p.pieceType.label)
}
def fromGameContext(ctx: GameContext): GameContextDto =
GameContextDto(
board = fromBoard(ctx.board),
turn = ctx.turn.label,
castlingRights = fromCastlingRights(ctx.castlingRights),
enPassantSquare = ctx.enPassantSquare.map(_.toString),
halfMoveClock = ctx.halfMoveClock,
moves = ctx.moves.map(fromMove),
initialBoard = fromBoard(ctx.initialBoard),
)
private def toPromotionPiece(s: String): Either[String, PromotionPiece] = s match
case "Queen" => Right(PromotionPiece.Queen)
case "Rook" => Right(PromotionPiece.Rook)
case "Bishop" => Right(PromotionPiece.Bishop)
case "Knight" => Right(PromotionPiece.Knight)
case other => Left(s"Unknown promotion piece: $other")
private def fromMoveType(mt: MoveType): (String, Option[String]) = mt match
case MoveType.Normal(false) => ("normal", None)
case MoveType.Normal(true) => ("capture", None)
case MoveType.CastleKingside => ("castleKingside", None)
case MoveType.CastleQueenside => ("castleQueenside", None)
case MoveType.EnPassant => ("enPassant", None)
case MoveType.Promotion(pp) => ("promotion", Some(fromPromotionPiece(pp)))
private def fromPromotionPiece(pp: PromotionPiece): String = pp match
case PromotionPiece.Queen => "Queen"
case PromotionPiece.Rook => "Rook"
case PromotionPiece.Bishop => "Bishop"
case PromotionPiece.Knight => "Knight"
private def toCastlingRights(dto: CastlingRightsDto): CastlingRights =
CastlingRights(dto.whiteKingSide, dto.whiteQueenSide, dto.blackKingSide, dto.blackQueenSide)
private def fromCastlingRights(cr: CastlingRights): CastlingRightsDto =
CastlingRightsDto(cr.whiteKingSide, cr.whiteQueenSide, cr.blackKingSide, cr.blackQueenSide)
private def toPieceOnSquare(dto: PieceOnSquareDto): Either[String, (Square, Piece)] =
for
sq <- toSquare(dto.square)
color <- toColor(dto.color)
pieceType <- toPieceType(dto.pieceType)
yield sq -> Piece(color, pieceType)
private def sequenceList[R](list: List[Either[String, R]]): Either[String, List[R]] =
list.foldLeft[Either[String, List[R]]](Right(List.empty)) {
case (Right(acc), Right(v)) => Right(acc :+ v)
case (Left(e), _) => Left(e)
case (_, Left(e)) => Left(e)
}
private def sequenceOpt[R](opt: Option[Either[String, R]]): Either[String, Option[R]] =
opt match
case None => Right(None)
case Some(Right(v)) => Right(Some(v))
case Some(Left(e)) => Left(e)
@@ -0,0 +1,37 @@
package de.nowchess.rules.dto
case class PieceOnSquareDto(square: String, color: String, pieceType: String)
case class CastlingRightsDto(
whiteKingSide: Boolean,
whiteQueenSide: Boolean,
blackKingSide: Boolean,
blackQueenSide: Boolean,
)
case class MoveDto(
from: String,
to: String,
moveType: String,
promotionPiece: Option[String],
)
case class GameContextDto(
board: List[PieceOnSquareDto],
turn: String,
castlingRights: CastlingRightsDto,
enPassantSquare: Option[String],
halfMoveClock: Int,
moves: List[MoveDto],
initialBoard: List[PieceOnSquareDto],
)
case class ContextRequest(context: GameContextDto)
case class ContextSquareRequest(context: GameContextDto, square: String)
case class ContextMoveRequest(context: GameContextDto, move: MoveDto)
case class MovesResponse(moves: List[MoveDto])
case class BooleanResponse(result: Boolean)
@@ -0,0 +1,94 @@
package de.nowchess.rules.resource
import de.nowchess.rules.dto.*
import de.nowchess.rules.sets.DefaultRules
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
@Path("/api/rules")
@ApplicationScoped
class RuleSetResource:
private val rules = DefaultRules
// scalafix:off DisableSyntax.throw
private def parse[T](e: Either[String, T]): T = e match
case Right(v) => v
case Left(msg) => throw BadRequestException(msg)
// scalafix:on DisableSyntax.throw
@POST
@Path("/candidate-moves")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def candidateMoves(req: ContextSquareRequest): MovesResponse =
val ctx = parse(DtoMapper.toGameContext(req.context))
val sq = parse(DtoMapper.toSquare(req.square))
MovesResponse(rules.candidateMoves(ctx)(sq).map(DtoMapper.fromMove))
@POST
@Path("/legal-moves")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def legalMoves(req: ContextSquareRequest): MovesResponse =
val ctx = parse(DtoMapper.toGameContext(req.context))
val sq = parse(DtoMapper.toSquare(req.square))
MovesResponse(rules.legalMoves(ctx)(sq).map(DtoMapper.fromMove))
@POST
@Path("/all-legal-moves")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def allLegalMoves(req: ContextRequest): MovesResponse =
MovesResponse(rules.allLegalMoves(parse(DtoMapper.toGameContext(req.context))).map(DtoMapper.fromMove))
@POST
@Path("/is-check")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def isCheck(req: ContextRequest): BooleanResponse =
BooleanResponse(rules.isCheck(parse(DtoMapper.toGameContext(req.context))))
@POST
@Path("/is-checkmate")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def isCheckmate(req: ContextRequest): BooleanResponse =
BooleanResponse(rules.isCheckmate(parse(DtoMapper.toGameContext(req.context))))
@POST
@Path("/is-stalemate")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def isStalemate(req: ContextRequest): BooleanResponse =
BooleanResponse(rules.isStalemate(parse(DtoMapper.toGameContext(req.context))))
@POST
@Path("/is-insufficient-material")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def isInsufficientMaterial(req: ContextRequest): BooleanResponse =
BooleanResponse(rules.isInsufficientMaterial(parse(DtoMapper.toGameContext(req.context))))
@POST
@Path("/is-fifty-move-rule")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def isFiftyMoveRule(req: ContextRequest): BooleanResponse =
BooleanResponse(rules.isFiftyMoveRule(parse(DtoMapper.toGameContext(req.context))))
@POST
@Path("/is-threefold-repetition")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def isThreefoldRepetition(req: ContextRequest): BooleanResponse =
BooleanResponse(rules.isThreefoldRepetition(parse(DtoMapper.toGameContext(req.context))))
@POST
@Path("/apply-move")
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def applyMove(req: ContextMoveRequest): GameContextDto =
val ctx = parse(DtoMapper.toGameContext(req.context))
val move = parse(DtoMapper.toMove(req.move))
DtoMapper.fromGameContext(rules.applyMove(ctx)(move))
@@ -0,0 +1,168 @@
package de.nowchess.rules.dto
import de.nowchess.api.board.{Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class DtoMapperTest extends AnyFunSuite with Matchers:
// ── toColor ────────────────────────────────────────────────────────
test("toColor converts White"):
DtoMapper.toColor("White") shouldBe Right(Color.White)
test("toColor converts Black"):
DtoMapper.toColor("Black") shouldBe Right(Color.Black)
test("toColor rejects unknown"):
DtoMapper.toColor("Red") shouldBe a[Left[?, ?]]
// ── toPieceType ────────────────────────────────────────────────────
test("toPieceType converts all piece types"):
DtoMapper.toPieceType("Pawn") shouldBe Right(PieceType.Pawn)
DtoMapper.toPieceType("Knight") shouldBe Right(PieceType.Knight)
DtoMapper.toPieceType("Bishop") shouldBe Right(PieceType.Bishop)
DtoMapper.toPieceType("Rook") shouldBe Right(PieceType.Rook)
DtoMapper.toPieceType("Queen") shouldBe Right(PieceType.Queen)
DtoMapper.toPieceType("King") shouldBe Right(PieceType.King)
test("toPieceType rejects unknown"):
DtoMapper.toPieceType("Dragon") shouldBe a[Left[?, ?]]
// ── toSquare ───────────────────────────────────────────────────────
test("toSquare converts valid algebraic"):
DtoMapper.toSquare("e4") shouldBe Right(Square(File.E, Rank.R4))
test("toSquare rejects invalid"):
DtoMapper.toSquare("z9") shouldBe a[Left[?, ?]]
// ── toMoveType ────────────────────────────────────────────────────
test("toMoveType converts normal non-capture"):
DtoMapper.toMoveType(MoveDto("e2", "e4", "normal", None)) shouldBe Right(MoveType.Normal(false))
test("toMoveType converts capture"):
DtoMapper.toMoveType(MoveDto("e2", "d3", "capture", None)) shouldBe Right(MoveType.Normal(true))
test("toMoveType converts castleKingside"):
DtoMapper.toMoveType(MoveDto("e1", "g1", "castleKingside", None)) shouldBe Right(MoveType.CastleKingside)
test("toMoveType converts castleQueenside"):
DtoMapper.toMoveType(MoveDto("e1", "c1", "castleQueenside", None)) shouldBe Right(MoveType.CastleQueenside)
test("toMoveType converts enPassant"):
DtoMapper.toMoveType(MoveDto("e5", "d6", "enPassant", None)) shouldBe Right(MoveType.EnPassant)
test("toMoveType converts all promotion pieces"):
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Queen"))) shouldBe Right(
MoveType.Promotion(PromotionPiece.Queen),
)
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Rook"))) shouldBe Right(
MoveType.Promotion(PromotionPiece.Rook),
)
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Bishop"))) shouldBe Right(
MoveType.Promotion(PromotionPiece.Bishop),
)
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Knight"))) shouldBe Right(
MoveType.Promotion(PromotionPiece.Knight),
)
test("toMoveType rejects promotion without piece"):
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", None)) shouldBe a[Left[?, ?]]
test("toMoveType rejects promotion with unknown piece"):
DtoMapper.toMoveType(MoveDto("e7", "e8", "promotion", Some("Pawn"))) shouldBe a[Left[?, ?]]
test("toMoveType rejects unknown type"):
DtoMapper.toMoveType(MoveDto("e2", "e4", "unknown", None)) shouldBe a[Left[?, ?]]
// ── toBoard ───────────────────────────────────────────────────────
test("toBoard builds valid board"):
val pieces = List(
PieceOnSquareDto("e1", "White", "King"),
PieceOnSquareDto("e8", "Black", "King"),
)
val result = DtoMapper.toBoard(pieces)
result.isRight shouldBe true
result.map(_.pieceAt(Square(File.E, Rank.R1))) shouldBe Right(Some(Piece(Color.White, PieceType.King)))
test("toBoard rejects invalid square"):
DtoMapper.toBoard(List(PieceOnSquareDto("z9", "White", "King"))) shouldBe a[Left[?, ?]]
test("toBoard rejects invalid color"):
DtoMapper.toBoard(List(PieceOnSquareDto("e1", "Red", "King"))) shouldBe a[Left[?, ?]]
test("toBoard rejects invalid piece type"):
DtoMapper.toBoard(List(PieceOnSquareDto("e1", "White", "Dragon"))) shouldBe a[Left[?, ?]]
test("toBoard with multiple invalid pieces covers all sequenceList branches"):
val pieces = List(
PieceOnSquareDto("z9", "White", "King"),
PieceOnSquareDto("z8", "White", "Queen"),
)
DtoMapper.toBoard(pieces) shouldBe a[Left[?, ?]]
// ── toGameContext ─────────────────────────────────────────────────
test("toGameContext round-trips initial position"):
val ctx = GameContext.initial
val dto = DtoMapper.fromGameContext(ctx)
DtoMapper.toGameContext(dto) shouldBe Right(ctx)
test("toGameContext rejects invalid turn"):
val dto = DtoMapper.fromGameContext(GameContext.initial).copy(turn = "Red")
DtoMapper.toGameContext(dto) shouldBe a[Left[?, ?]]
test("toGameContext rejects invalid en passant square"):
val dto = DtoMapper.fromGameContext(GameContext.initial).copy(enPassantSquare = Some("z9"))
DtoMapper.toGameContext(dto) shouldBe a[Left[?, ?]]
test("toGameContext rejects invalid initial board"):
val badBoard = List(PieceOnSquareDto("z9", "White", "King"))
val dto = DtoMapper.fromGameContext(GameContext.initial).copy(initialBoard = badBoard)
DtoMapper.toGameContext(dto) shouldBe a[Left[?, ?]]
test("toGameContext rejects invalid move"):
val badMove = MoveDto("z9", "e4", "normal", None)
val dto = DtoMapper.fromGameContext(GameContext.initial).copy(moves = List(badMove))
DtoMapper.toGameContext(dto) shouldBe a[Left[?, ?]]
// ── fromGameContext ───────────────────────────────────────────────
test("fromGameContext includes en passant square when present"):
val ctx = GameContext.initial.copy(enPassantSquare = Some(Square(File.E, Rank.R3)))
DtoMapper.fromGameContext(ctx).enPassantSquare shouldBe Some("e3")
// ── fromMove ──────────────────────────────────────────────────────
test("fromMove converts all move types"):
DtoMapper.fromMove(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))).moveType shouldBe "normal"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R2), Square(File.D, Rank.R3), MoveType.Normal(true)))
.moveType shouldBe "capture"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside))
.moveType shouldBe "castleKingside"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside))
.moveType shouldBe "castleQueenside"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant))
.moveType shouldBe "enPassant"
DtoMapper
.fromMove(Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen)))
.promotionPiece shouldBe Some("Queen")
DtoMapper
.fromMove(Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Rook)))
.promotionPiece shouldBe Some("Rook")
DtoMapper
.fromMove(Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Bishop)))
.promotionPiece shouldBe Some("Bishop")
DtoMapper
.fromMove(Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Knight)))
.promotionPiece shouldBe Some("Knight")
@@ -0,0 +1,294 @@
package de.nowchess.rules.resource
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.Move
import de.nowchess.rules.dto.{ContextMoveRequest, ContextRequest, ContextSquareRequest, DtoMapper}
import de.nowchess.rules.sets.DefaultRules
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import io.restassured.http.ContentType
import org.hamcrest.Matchers.*
import org.junit.jupiter.api.Test
@QuarkusTest
class RuleSetResourceTest:
private val mapper = new ObjectMapper().registerModule(DefaultScalaModule)
private val rules = DefaultRules
private def request() = RestAssured.`given`()
private def toJson(value: AnyRef): String = mapper.writeValueAsString(value)
private def contextBody(ctx: GameContext): String =
toJson(ContextRequest(DtoMapper.fromGameContext(ctx)))
private def contextSquareBody(ctx: GameContext, square: String): String =
toJson(ContextSquareRequest(DtoMapper.fromGameContext(ctx), square))
private def contextMoveBody(ctx: GameContext, move: Move): String =
toJson(ContextMoveRequest(DtoMapper.fromGameContext(ctx), DtoMapper.fromMove(move)))
// ── all-legal-moves ───────────────────────────────────────────────
@Test
def allLegalMoves_initialPositionHas20Moves(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(GameContext.initial))
.when()
.post("/api/rules/all-legal-moves")
.`then`()
.statusCode(200)
.body("moves.size()", is(20))
// ── legal-moves ───────────────────────────────────────────────────
@Test
def legalMoves_e2PawnHas2Moves(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextSquareBody(GameContext.initial, "e2"))
.when()
.post("/api/rules/legal-moves")
.`then`()
.statusCode(200)
.body("moves.size()", is(2))
// ── candidate-moves ───────────────────────────────────────────────
@Test
def candidateMoves_e2PawnHas2Candidates(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextSquareBody(GameContext.initial, "e2"))
.when()
.post("/api/rules/candidate-moves")
.`then`()
.statusCode(200)
.body("moves.size()", is(2))
// ── is-check ──────────────────────────────────────────────────────
@Test
def isCheck_falseForInitialPosition(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(GameContext.initial))
.when()
.post("/api/rules/is-check")
.`then`()
.statusCode(200)
.body("result", is(false))
@Test
def isCheck_trueWhenKingAttacked(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(buildCheckContext()))
.when()
.post("/api/rules/is-check")
.`then`()
.statusCode(200)
.body("result", is(true))
// ── is-checkmate ──────────────────────────────────────────────────
@Test
def isCheckmate_falseForInitialPosition(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(GameContext.initial))
.when()
.post("/api/rules/is-checkmate")
.`then`()
.statusCode(200)
.body("result", is(false))
@Test
def isCheckmate_trueForFoolsMate(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(buildFoolsMate()))
.when()
.post("/api/rules/is-checkmate")
.`then`()
.statusCode(200)
.body("result", is(true))
// ── is-stalemate ──────────────────────────────────────────────────
@Test
def isStalemate_falseForInitialPosition(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(GameContext.initial))
.when()
.post("/api/rules/is-stalemate")
.`then`()
.statusCode(200)
.body("result", is(false))
@Test
def isStalemate_trueForStalematePosition(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(buildStalemateContext()))
.when()
.post("/api/rules/is-stalemate")
.`then`()
.statusCode(200)
.body("result", is(true))
// ── is-insufficient-material ──────────────────────────────────────
@Test
def isInsufficientMaterial_falseForInitialPosition(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(GameContext.initial))
.when()
.post("/api/rules/is-insufficient-material")
.`then`()
.statusCode(200)
.body("result", is(false))
@Test
def isInsufficientMaterial_trueForKingsOnly(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(buildKingsOnlyContext()))
.when()
.post("/api/rules/is-insufficient-material")
.`then`()
.statusCode(200)
.body("result", is(true))
// ── is-fifty-move-rule ────────────────────────────────────────────
@Test
def isFiftyMoveRule_falseForInitialPosition(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(GameContext.initial))
.when()
.post("/api/rules/is-fifty-move-rule")
.`then`()
.statusCode(200)
.body("result", is(false))
@Test
def isFiftyMoveRule_trueWhenClockAt100(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(GameContext.initial.copy(halfMoveClock = 100)))
.when()
.post("/api/rules/is-fifty-move-rule")
.`then`()
.statusCode(200)
.body("result", is(true))
// ── is-threefold-repetition ───────────────────────────────────────
@Test
def isThreefoldRepetition_falseForInitialPosition(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(GameContext.initial))
.when()
.post("/api/rules/is-threefold-repetition")
.`then`()
.statusCode(200)
.body("result", is(false))
@Test
def isThreefoldRepetition_trueAfterRepeatedMoves(): Unit =
request()
.contentType(ContentType.JSON)
.body(contextBody(buildThreefoldContext()))
.when()
.post("/api/rules/is-threefold-repetition")
.`then`()
.statusCode(200)
.body("result", is(true))
// ── apply-move ────────────────────────────────────────────────────
@Test
def applyMove_updatesContext(): Unit =
val move = rules
.legalMoves(GameContext.initial)(Square(File.E, Rank.R2))
.find(_.to == Square(File.E, Rank.R4))
.get
request()
.contentType(ContentType.JSON)
.body(contextMoveBody(GameContext.initial, move))
.when()
.post("/api/rules/apply-move")
.`then`()
.statusCode(200)
.body("turn", is("Black"))
// ── error handling ────────────────────────────────────────────────
@Test
def invalidSquare_returns400(): Unit =
request()
.contentType(ContentType.JSON)
.body(toJson(ContextSquareRequest(DtoMapper.fromGameContext(GameContext.initial), "z9")))
.when()
.post("/api/rules/legal-moves")
.`then`()
.statusCode(400)
// ── position builders ─────────────────────────────────────────────
private def buildCheckContext(): GameContext =
val board = Board(Map(
Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King),
Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King),
Square(File.E, Rank.R3) -> Piece(Color.Black, PieceType.Rook),
))
GameContext(board, Color.White, CastlingRights.None, None, 0, List.empty, initialBoard = board)
private def buildFoolsMate(): GameContext =
val moves = List(("f2", "f3"), ("e7", "e5"), ("g2", "g4"), ("d8", "h4"))
moves.foldLeft(GameContext.initial) { (ctx, fromTo) =>
val from = Square.fromAlgebraic(fromTo._1).get
val to = Square.fromAlgebraic(fromTo._2).get
rules.legalMoves(ctx)(from).find(_.to == to).fold(ctx)(rules.applyMove(ctx))
}
private def buildStalemateContext(): GameContext =
val board = Board(Map(
Square(File.H, Rank.R8) -> Piece(Color.Black, PieceType.King),
Square(File.F, Rank.R7) -> Piece(Color.White, PieceType.Queen),
Square(File.G, Rank.R6) -> Piece(Color.White, PieceType.King),
))
GameContext(board, Color.Black, CastlingRights.None, None, 0, List.empty, initialBoard = board)
private def buildKingsOnlyContext(): GameContext =
val board = Board(Map(
Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King),
Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King),
))
GameContext(board, Color.White, CastlingRights.None, None, 0, List.empty, initialBoard = board)
private def buildThreefoldContext(): GameContext =
val g1 = Square(File.G, Rank.R1)
val f3 = Square(File.F, Rank.R3)
val g8 = Square(File.G, Rank.R8)
val f6 = Square(File.F, Rank.R6)
def mv(ctx: GameContext, from: Square, to: Square): GameContext =
rules.legalMoves(ctx)(from).find(_.to == to).fold(ctx)(rules.applyMove(ctx))
val ctx1 = mv(GameContext.initial, g1, f3)
val ctx2 = mv(ctx1, g8, f6)
val ctx3 = mv(ctx2, f3, g1)
val ctx4 = mv(ctx3, f6, g8)
val ctx5 = mv(ctx4, g1, f3)
val ctx6 = mv(ctx5, g8, f6)
val ctx7 = mv(ctx6, f3, g1)
mv(ctx7, f6, g8)
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=7
MINOR=5
PATCH=0