Compare commits

..

12 Commits

Author SHA1 Message Date
TeamCity 52b171c7af ci: bump version with Build-46
Build & Test (NowChessSystems) TeamCity build finished
2026-04-22 06:51:59 +00:00
Janis a386f57c21 fix: IO microservice (#38)
Reviewed-on: #38
2026-04-22 08:36:54 +02:00
TeamCity 3ca2afbb4b ci: bump version with Build-45 2026-04-21 13:49:26 +00:00
Janis b5a2966ada feat: NCS-53 changed IO to MicroService for easier scaling (#37)
Reviewed-on: #37
Reviewed-by: Shahd Lala <shosho996@blackhole.local>
2026-04-21 15:38:58 +02:00
TeamCity 74a4fce0ca ci: bump version with Build-44 2026-04-21 11:13:30 +00:00
Janis 8fc97bde02 refactor: align JSON string formatting in JsonParserTest 2026-04-21 13:02:18 +02:00
shosho996 2d75b2e80e test: NCS-45 IO Test reduction (#32)
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 f088c4e9ff feat: NCS-37 Quarkus integration (#35)
Reviewed-on: #35
Reviewed-by: Leon Hermann <lq@blackhole.local>
2026-04-21 12:35:20 +02:00
TeamCity 8a1cf909d4 ci: bump version with Build-43 2026-04-19 20:53:56 +00:00
Janis 33e785d22a feat: NCS-40 Rework Draw System (#34)
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 d16cec176b ci: bump version with Build-42 2026-04-19 14:01:11 +00:00
Janis 8744bee2dd feat: NCS-41 Bot Platform (#33)
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
49 changed files with 228 additions and 2523658 deletions
+87
View File
@@ -0,0 +1,87 @@
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 }}
+14
View File
@@ -80,3 +80,17 @@
* 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-22)
### 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))
### Bug Fixes
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([fb5c61d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fb5c61de63292e5d70c06304cba2193686aa1607))
@@ -0,0 +1,3 @@
package de.nowchess.api.dto
case class ImportFenRequest(fen: String)
@@ -0,0 +1,3 @@
package de.nowchess.api.dto
case class ImportPgnRequest(pgn: String)
@@ -1,7 +1,6 @@
package de.nowchess.io
package de.nowchess.api.io
import de.nowchess.api.game.GameContext
trait GameContextExport:
def exportGameContext(context: GameContext): String
@@ -1,7 +1,6 @@
package de.nowchess.io
package de.nowchess.api.io
import de.nowchess.api.game.GameContext
trait GameContextImport:
def importGameContext(input: String): Either[String, GameContext]
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=10
MINOR=11
PATCH=0
File diff suppressed because it is too large Load Diff
@@ -1,66 +0,0 @@
{
"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
}
]
}
+31
View File
@@ -400,3 +400,34 @@
* 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-22)
### 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))
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([fb5c61d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fb5c61de63292e5d70c06304cba2193686aa1607))
* 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))
+2 -1
View File
@@ -48,7 +48,6 @@ dependencies {
}
implementation(project(":modules:api"))
implementation(project(":modules:io"))
implementation(project(":modules:rule"))
implementation(project(":modules:bot"))
@@ -69,6 +68,8 @@ 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"]!!}")
@@ -3,7 +3,7 @@
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
# ./gradlew :modules:core: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 build/*-runner /work/application
COPY --chown=1001:root --chmod=0755 modules/core/build/*-runner /work/application
EXPOSE 8080
USER 1001
@@ -1,7 +1,7 @@
package de.nowchess.chess.client
import de.nowchess.api.dto.{ImportFenRequest, ImportPgnRequest}
import de.nowchess.api.game.GameContext
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest}
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
@@ -5,7 +5,6 @@ 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
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,9 @@
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.io.{GameContextExport, GameContextImport}
import de.nowchess.api.io.{GameContextExport, GameContextImport}
import de.nowchess.rules.RuleSet
import de.nowchess.rules.sets.DefaultRules
@@ -12,9 +12,6 @@ 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
import de.nowchess.io.pgn.PgnExporter
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest}
import io.smallrye.mutiny.Multi
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
@@ -89,16 +86,8 @@ class GameResource:
private def toGameStateDto(entry: GameEntry): GameStateDto =
val ctx = entry.engine.context
GameStateDto(
fen = FenExporter.exportGameContext(ctx),
pgn = PgnExporter.exportGame(
Map(
"Event" -> "NowChess game",
"White" -> entry.white.displayName,
"Black" -> entry.black.displayName,
"Result" -> "*",
),
ctx.moves,
),
fen = ioClient.exportFen(ctx),
pgn = ioClient.exportPgn(ctx),
turn = ctx.turn.label.toLowerCase,
status = statusOf(entry),
winner = ctx.result.collect { case GameResult.Win(c) => c.label.toLowerCase },
@@ -149,7 +138,6 @@ class GameResource:
val black = playerInfoFrom(req.black, DefaultBlack)
val entry = newEntry(GameContext.initial, white, black)
registry.store(entry)
println(s"Created game ${entry.gameId}")
created(toGameFullDto(entry))
@GET
@@ -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.io.GameContextImport
import de.nowchess.api.io.GameContextImport
import de.nowchess.rules.RuleSet
import de.nowchess.rules.sets.DefaultRules
import org.scalatest.funsuite.AnyFunSuite
@@ -1,16 +1,13 @@
package de.nowchess.chess.engine
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.chess.observer.{GameEvent, Observer}
import de.nowchess.io.fen.FenParser
import de.nowchess.io.pgn.PgnExporter
import de.nowchess.io.fen.FenExporter
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
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 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=18
MINOR=19
PATCH=0
+16
View File
@@ -99,3 +99,19 @@
* 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))
## (2026-04-22)
### 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))
### Bug Fixes
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([fb5c61d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fb5c61de63292e5d70c06304cba2193686aa1607))
@@ -3,17 +3,17 @@
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
# ./gradlew :modules:io:build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t quarkus/backcore .
# docker build -f src/main/docker/Dockerfile.native -t quarkus/backio .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/backcore
# docker run -i --rm -p 8080:8080 quarkus/backio
#
# 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 build/*-runner /work/application
COPY --chown=1001:root --chmod=0755 modules/io/build/*-runner /work/application
EXPOSE 8080
USER 1001
@@ -1,6 +1,8 @@
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.io.GameContextExport
import de.nowchess.api.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.io.GameContextImport
import de.nowchess.api.io.GameContextImport
object FenParser extends GameContextImport:
@@ -2,9 +2,10 @@ 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,8 +6,9 @@ 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.io.GameContextExport
import de.nowchess.api.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,7 +5,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.io.GameContextImport
import de.nowchess.api.io.GameContextImport
import scala.util.Try
/** Imports a GameContext from JSON format using Jackson.
@@ -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.io.GameContextExport
import de.nowchess.api.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.io.GameContextImport
import de.nowchess.api.io.GameContextImport
import de.nowchess.rules.sets.DefaultRules
/** A parsed PGN game containing headers and the resolved move list. */
@@ -1,13 +1,14 @@
package de.nowchess.io
import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{File, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.io.GameContextExport
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 scala.util.Using
import java.nio.file.{Files, Paths}
class GameFileServiceSuite extends AnyFunSuite with Matchers:
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=12
MINOR=13
PATCH=0
+13
View File
@@ -105,3 +105,16 @@
### 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-22)
### 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))
+2 -47
View File
@@ -1,7 +1,6 @@
plugins {
id("scala")
id("org.scoverage") version "8.1"
id("io.quarkus")
}
group = "de.nowchess"
@@ -26,10 +25,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") {
@@ -45,56 +40,20 @@ dependencies {
implementation(project(":modules:api"))
implementation(platform("${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", "junit-jupiter")
includeEngines("scalatest")
testLogging {
events("passed", "skipped", "failed")
events("skipped", "failed")
}
}
finalizedBy(tasks.reportScoverage)
@@ -102,7 +61,3 @@ tasks.test {
tasks.reportScoverage {
dependsOn(tasks.test)
}
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
@@ -1,5 +0,0 @@
quarkus:
http:
port: 8081
application:
name: rule-service
@@ -1,11 +0,0 @@
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)
@@ -1,128 +0,0 @@
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)
@@ -1,37 +0,0 @@
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)
@@ -1,94 +0,0 @@
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))
@@ -1,14 +0,0 @@
package de.nowchess.rules.config
import com.fasterxml.jackson.databind.ObjectMapper
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JacksonConfigTest extends AnyFunSuite with Matchers:
test("customize registers DefaultScalaModule enabling Option serialization"):
val config = new JacksonConfig()
val mapper = new ObjectMapper()
config.customize(mapper)
mapper.writeValueAsString(None) shouldBe "null"
mapper.writeValueAsString(Some("hello")) shouldBe """"hello""""
@@ -1,174 +0,0 @@
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")
test("toGameContext round-trips a valid en passant square"):
val ctx = GameContext.initial.copy(enPassantSquare = Some(Square(File.E, Rank.R3)))
DtoMapper.toGameContext(DtoMapper.fromGameContext(ctx)).map(_.enPassantSquare) shouldBe Right(
Some(Square(File.E, Rank.R3)),
)
// ── 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")
@@ -1,300 +0,0 @@
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,167 +0,0 @@
package de.nowchess.rules.resource
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, MoveType}
import de.nowchess.rules.dto.{ContextMoveRequest, ContextRequest, ContextSquareRequest, DtoMapper}
import de.nowchess.rules.sets.DefaultRules
import jakarta.ws.rs.BadRequestException
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class RuleSetResourceUnitTest extends AnyFunSuite with Matchers:
private val resource = new RuleSetResource()
private val rules = DefaultRules
private def ctx(g: GameContext) = ContextRequest(DtoMapper.fromGameContext(g))
private def ctxSq(g: GameContext, sq: String) = ContextSquareRequest(DtoMapper.fromGameContext(g), sq)
private def ctxMv(g: GameContext, m: Move) = ContextMoveRequest(DtoMapper.fromGameContext(g), DtoMapper.fromMove(m))
// ── position builders ─────────────────────────────────────────────
private def checkContext(): 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 foolsMate(): GameContext =
val moves = List(("f2", "f3"), ("e7", "e5"), ("g2", "g4"), ("d8", "h4"))
moves.foldLeft(GameContext.initial) { (c, ft) =>
val from = Square.fromAlgebraic(ft._1).get
val to = Square.fromAlgebraic(ft._2).get
rules.legalMoves(c)(from).find(_.to == to).fold(c)(rules.applyMove(c))
}
private def stalemateContext(): 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 kingsOnlyContext(): 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 threefoldContext(): 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(c: GameContext, from: Square, to: Square): GameContext =
rules.legalMoves(c)(from).find(_.to == to).fold(c)(rules.applyMove(c))
val c1 = mv(GameContext.initial, g1, f3)
val c2 = mv(c1, g8, f6)
val c3 = mv(c2, f3, g1)
val c4 = mv(c3, f6, g8)
val c5 = mv(c4, g1, f3)
val c6 = mv(c5, g8, f6)
val c7 = mv(c6, f3, g1)
mv(c7, f6, g8)
// ── allLegalMoves ─────────────────────────────────────────────────
test("allLegalMoves returns 20 moves for initial position"):
resource.allLegalMoves(ctx(GameContext.initial)).moves should have size 20
// ── legalMoves ────────────────────────────────────────────────────
test("legalMoves returns 2 moves for e2 pawn"):
resource.legalMoves(ctxSq(GameContext.initial, "e2")).moves should have size 2
test("legalMoves throws BadRequestException for invalid square"):
an[BadRequestException] should be thrownBy
resource.legalMoves(ctxSq(GameContext.initial, "z9"))
// ── candidateMoves ────────────────────────────────────────────────
test("candidateMoves returns moves for e2 pawn"):
resource.candidateMoves(ctxSq(GameContext.initial, "e2")).moves should not be empty
test("candidateMoves throws BadRequestException for invalid square"):
an[BadRequestException] should be thrownBy
resource.candidateMoves(ctxSq(GameContext.initial, "z9"))
// ── isCheck ───────────────────────────────────────────────────────
test("isCheck returns false for initial position"):
resource.isCheck(ctx(GameContext.initial)).result shouldBe false
test("isCheck returns true when king is attacked"):
resource.isCheck(ctx(checkContext())).result shouldBe true
test("isCheck throws BadRequestException for invalid context"):
an[BadRequestException] should be thrownBy
resource.isCheck(
ctx(GameContext.initial).copy(context = DtoMapper.fromGameContext(GameContext.initial).copy(turn = "Red")),
)
// ── isCheckmate ───────────────────────────────────────────────────
test("isCheckmate returns false for initial position"):
resource.isCheckmate(ctx(GameContext.initial)).result shouldBe false
test("isCheckmate returns true for Fool's mate"):
resource.isCheckmate(ctx(foolsMate())).result shouldBe true
// ── isStalemate ───────────────────────────────────────────────────
test("isStalemate returns false for initial position"):
resource.isStalemate(ctx(GameContext.initial)).result shouldBe false
test("isStalemate returns true for stalemate position"):
resource.isStalemate(ctx(stalemateContext())).result shouldBe true
// ── isInsufficientMaterial ────────────────────────────────────────
test("isInsufficientMaterial returns false for initial position"):
resource.isInsufficientMaterial(ctx(GameContext.initial)).result shouldBe false
test("isInsufficientMaterial returns true for kings only"):
resource.isInsufficientMaterial(ctx(kingsOnlyContext())).result shouldBe true
// ── isFiftyMoveRule ───────────────────────────────────────────────
test("isFiftyMoveRule returns false for initial position"):
resource.isFiftyMoveRule(ctx(GameContext.initial)).result shouldBe false
test("isFiftyMoveRule returns true when halfMoveClock is 100"):
resource.isFiftyMoveRule(ctx(GameContext.initial.copy(halfMoveClock = 100))).result shouldBe true
// ── isThreefoldRepetition ─────────────────────────────────────────
test("isThreefoldRepetition returns false for initial position"):
resource.isThreefoldRepetition(ctx(GameContext.initial)).result shouldBe false
test("isThreefoldRepetition returns true after repeated moves"):
resource.isThreefoldRepetition(ctx(threefoldContext())).result shouldBe true
// ── applyMove ─────────────────────────────────────────────────────
test("applyMove returns updated context with switched turn"):
val move = rules
.legalMoves(GameContext.initial)(Square(File.E, Rank.R2))
.find(_.to == Square(File.E, Rank.R4))
.get
resource.applyMove(ctxMv(GameContext.initial, move)).turn shouldBe "Black"
test("applyMove throws BadRequestException for invalid move"):
val badMove = DtoMapper
.fromMove(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)))
.copy(moveType = "unknown")
an[BadRequestException] should be thrownBy
resource.applyMove(ContextMoveRequest(DtoMapper.fromGameContext(GameContext.initial), badMove))
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=7
MINOR=8
PATCH=0