Compare commits

...

10 Commits

Author SHA1 Message Date
TeamCity 06f2adfeb6 ci: bump version with Build-142 2026-06-23 08:52:12 +00:00
Janis 4651bb796f fix(official-bots): play only own tournament games with correct color
Build & Test (NowChessSystems) TeamCity build finished
The tournament stream broadcasts a gameStart per color for every pairing in
the round, without a player id. The bot latched the first color it saw and
played games it was not part of, submitting moves for the wrong color that
the server rejected. Now it fetches game detail and matches its botId against
white/black to resolve its real color, skipping games it is not in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:37:28 +02:00
Janis 1df29cf3a6 feat(official-bots): make HybridBot veto actionable and use it for expert
Build & Test (NowChessSystems) TeamCity build finished
When classical and NNUE evals diverge above the veto threshold, HybridBot
now re-searches excluding the suspect move and switches to NNUE's preferred
alternative instead of merely logging. BotController maps the expert bot to
HybridBot so tournament auto-join uses it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:30:55 +02:00
TeamCity ff492e1dc8 ci: bump version with Build-140 2026-06-23 08:10:24 +00:00
Janis 9978b7ea78 feat(tournament): auto-join external tournaments and publish created ones (#77)
Build & Test (NowChessSystems) TeamCity build finished
Official bots now poll the external tournament server and auto-join every
created tournament with the hardest bot (expert). Tournaments created in
NowChessSystems are forwarded to the native tournament server so the bots
can see and join them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Reviewed-on: #77
2026-06-23 10:01:35 +02:00
TeamCity 9a9784673f ci: bump version with Build-139 2026-06-22 20:44:36 +00:00
Janis Eccarius 83dd2d4335 fix(official-bots): prioritize Redis token over stale env var in joinTournament
Build & Test (NowChessSystems) TeamCity build finished
The env var TOURNAMENT_BOT_TOKEN was checked before Redis, so a stale
token set in the k8s secret always won over the freshly-registered token
stored in Redis at startup. Swap order: request param → Redis → env var.

Also add WARN-level logging when registerWithServer fails (non-2xx or
exception), making the failure visible in the log stream since INFO is
filtered in production.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 22:22:28 +02:00
TeamCity 4377e05d5c ci: bump version with Build-138 2026-06-22 19:47:25 +00:00
Janis Eccarius 3188241737 fix(official-bots): park on external tournament servers using correct endpoint and token
Build & Test (NowChessSystems) TeamCity build finished
External tournament servers expose POST /api/bots (registry) not
POST /api/account/bots. They also require their own HMAC-HS256 token,
not the NowChessSystems RS256 account-service token.

parkOnStartup now:
- Parks on the local NowChessSystems account service via /api/account/bots
  using the resolved NowChessSystems token (unchanged)
- For each remote server from fetchRemoteServers(), calls
  registerWithServer(serverUrl, name) to obtain a server-specific token
  via POST /api/auth/register (public endpoint), then parks via
  POST /api/bots using that token

registerWithTournamentServer extracted into registerWithServer(url, name)
so it can be reused for both the primary tournament server (resolveToken)
and all remote servers (parkOnStartup).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 21:13:00 +02:00
Janis Eccarius 64b5d5567f fix(official-bots): register with tournament server directly to get correct token
Build & Test (NowChessSystems) TeamCity build finished
The TOURNAMENT_SERVICE_URL points to the NowChessTools tournament server
which uses its own HMAC-HS256 JWTs issued by POST /api/auth/register.
Tokens from the NowChessSystems account service (RS256) are rejected
with 401 by that server.

resolveToken now first calls POST {tournamentServiceUrl}/api/auth/register
(public endpoint, idempotent — finds existing identity by name or creates).
This returns the correct HMAC-HS256 token for the target server and is
stored in Redis. Falls back to the account service path for deployments
where TOURNAMENT_SERVICE_URL points to the NowChessSystems tournament module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 21:02:14 +02:00
11 changed files with 447 additions and 83 deletions
+159
View File
@@ -506,3 +506,162 @@
### Reverts ### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656)) * Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-22)
### Features
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **analytics:** add Spark batch analytics module ([#70](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/70)) ([39f1657](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39f1657e1db6e84889af338c43be8cb5c03c3ec3))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
* **tournament:** federate tournaments across clusters with DB replication ([5b000a6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5b000a6e5f04ea6770d1c7ab6bfdaded77a99172))
* **tournament:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-22)
### Features
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **analytics:** add Spark batch analytics module ([#70](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/70)) ([39f1657](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39f1657e1db6e84889af338c43be8cb5c03c3ec3))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
* **tournament:** federate tournaments across clusters with DB replication ([5b000a6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5b000a6e5f04ea6770d1c7ab6bfdaded77a99172))
* **tournament:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
* **official-bots:** prioritize Redis token over stale env var in joinTournament ([83dd2d4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/83dd2d4335ca48eb3e5aa234a75367574276ba63))
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-23)
### Features
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **analytics:** add Spark batch analytics module ([#70](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/70)) ([39f1657](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39f1657e1db6e84889af338c43be8cb5c03c3ec3))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
* **tournament:** auto-join external tournaments and publish created ones ([#77](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/77)) ([9978b7e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9978b7ea78eb658a225a461b9cd339386c0c14f3))
* **tournament:** federate tournaments across clusters with DB replication ([5b000a6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5b000a6e5f04ea6770d1c7ab6bfdaded77a99172))
* **tournament:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
* **official-bots:** prioritize Redis token over stale env var in joinTournament ([83dd2d4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/83dd2d4335ca48eb3e5aa234a75367574276ba63))
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
## (2026-06-23)
### Features
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
* **analytics:** add Spark batch analytics module ([#70](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/70)) ([39f1657](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39f1657e1db6e84889af338c43be8cb5c03c3ec3))
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
* **official-bots:** make HybridBot veto actionable and use it for expert ([1df29cf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1df29cf3a6e21af3f396b2b7a6da67d978f941ae))
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
* **tournament:** auto-join external tournaments and publish created ones ([#77](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/77)) ([9978b7e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9978b7ea78eb658a225a461b9cd339386c0c14f3))
* **tournament:** federate tournaments across clusters with DB replication ([5b000a6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5b000a6e5f04ea6770d1c7ab6bfdaded77a99172))
* **tournament:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
### Bug Fixes
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
* **official-bots:** play only own tournament games with correct color ([4651bb7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4651bb796f07a21bd013d9521b2dfe2e1078cebb))
* **official-bots:** prioritize Redis token over stale env var in joinTournament ([83dd2d4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/83dd2d4335ca48eb3e5aa234a75367574276ba63))
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
### Reverts
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
@@ -1,14 +1,17 @@
package de.nowchess.bot package de.nowchess.bot
import de.nowchess.bot.bots.ClassicalBot import de.nowchess.bot.bots.{ClassicalBot, HybridBot}
import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.context.ApplicationScoped
import org.jboss.logging.Logger
object BotController: object BotController:
private val log = Logger.getLogger(classOf[BotController])
private val bots: Map[String, Bot] = Map( private val bots: Map[String, Bot] = Map(
"easy" -> ClassicalBot(BotDifficulty.Easy), "easy" -> ClassicalBot(BotDifficulty.Easy),
"medium" -> ClassicalBot(BotDifficulty.Medium), "medium" -> ClassicalBot(BotDifficulty.Medium),
"hard" -> ClassicalBot(BotDifficulty.Hard), "hard" -> ClassicalBot(BotDifficulty.Hard),
"expert" -> ClassicalBot(BotDifficulty.Expert), "expert" -> HybridBot(BotDifficulty.Expert, vetoReporter = log.debug(_)),
) )
def getBot(name: String): Option[Bot] = bots.get(name.toLowerCase) def getBot(name: String): Option[Bot] = bots.get(name.toLowerCase)
@@ -24,16 +24,24 @@ object HybridBot:
val search = AlphaBetaSearch(rules, TranspositionTable(), classicalEvaluation) val search = AlphaBetaSearch(rules, TranspositionTable(), classicalEvaluation)
context => context =>
val blockedMoves = BotMoveRepetition.blockedMoves(context) val blockedMoves = BotMoveRepetition.blockedMoves(context)
def nnueScore(move: Move): Int = nnueEvaluation.evaluate(rules.applyMove(context)(move))
def classicalScore(move: Move): Int = classicalEvaluation.evaluate(rules.applyMove(context)(move))
def refine(move: Move): Move =
val moveNnue = nnueScore(move)
if (classicalScore(move) - moveNnue).abs <= Config.VETO_THRESHOLD then move
else
search
.bestMoveWithTime(context, Config.TIME_LIMIT_MS, blockedMoves + move)
.filterNot(blockedMoves.contains)
.filter(alt => nnueScore(alt) < moveNnue)
.map { alt =>
vetoReporter(f"[Veto] ${move.from}->${move.to} replaced by ${alt.from}->${alt.to} — NNUE prefers it")
alt
}
.getOrElse(move)
book.flatMap(_.probe(context)).filterNot(blockedMoves.contains).orElse { book.flatMap(_.probe(context)).filterNot(blockedMoves.contains).orElse {
search.bestMoveWithTime(context, Config.TIME_LIMIT_MS, blockedMoves).map { move => search.bestMoveWithTime(context, Config.TIME_LIMIT_MS, blockedMoves).map(refine)
val next = rules.applyMove(context)(move)
val staticNnue = nnueEvaluation.evaluate(next)
val classical = classicalEvaluation.evaluate(next)
val diff = (classical - staticNnue).abs
if diff > Config.VETO_THRESHOLD then
vetoReporter(
f"[Veto] ${move.from}->${move.to}: nnue=$staticNnue classical=$classical diff=$diff — flagged but trusted (deep search)",
)
move
}
} }
@@ -38,14 +38,22 @@ class TournamentBotGamePlayer:
private val client: Client = ClientBuilder.newClient() private val client: Client = ClientBuilder.newClient()
private val workers: ExecutorService = Executors.newCachedThreadPool() private val workers: ExecutorService = Executors.newCachedThreadPool()
private val activeGames = ConcurrentHashMap.newKeySet[String]() private val activeGames = ConcurrentHashMap.newKeySet[String]()
private val joinedTournaments = ConcurrentHashMap.newKeySet[String]()
private val hardestDifficulty = "expert"
private val autoJoinIntervalMs = 15000L
// scalafix:off DisableSyntax.var // scalafix:off DisableSyntax.var
@volatile private var running = true @volatile private var running = true
@volatile private var autoJoinToken: Option[String] = None
// scalafix:on DisableSyntax.var // scalafix:on DisableSyntax.var
val tournamentServiceUrl: String = val tournamentServiceUrl: String =
System.getenv().asScala.getOrElse("TOURNAMENT_SERVICE_URL", "http://localhost:8086") System.getenv().asScala.getOrElse("TOURNAMENT_SERVICE_URL", "http://localhost:8086")
val autoJoinServerUrl: String =
System.getenv().asScala.getOrElse("TOURNAMENT_AUTO_JOIN_URL", "http://141.37.123.132:8086")
@PostConstruct @PostConstruct
def initialize(): Unit = def initialize(): Unit =
val env = System.getenv().asScala.toMap val env = System.getenv().asScala.toMap
@@ -58,14 +66,58 @@ class TournamentBotGamePlayer:
case Some(cfg) => case Some(cfg) =>
log.infof("Tournament bot enabled — server=%s tournament=%s bot=%s", cfg.serverUrl, cfg.tournamentId, cfg.botId) log.infof("Tournament bot enabled — server=%s tournament=%s bot=%s", cfg.serverUrl, cfg.tournamentId, cfg.botId)
startAsync(cfg) startAsync(cfg)
startAutoJoin()
private def startAutoJoin(): Unit =
val thread = new Thread(() => autoJoinLoop(), "TournamentBot-auto-join")
thread.setDaemon(true)
thread.start()
log.infof("Auto-join enabled — server=%s difficulty=%s", autoJoinServerUrl, hardestDifficulty)
private def autoJoinLoop(): Unit =
while running do
Try(autoJoinScan()).failed.foreach(ex => log.warnf(ex, "Auto-join scan failed"))
sleep(autoJoinIntervalMs)
private def autoJoinScan(): Unit =
resolveAutoJoinToken().foreach { token =>
TournamentBotConfig.jwtSubject(token).foreach { botId =>
openTournaments().foreach { tournamentId =>
if joinedTournaments.add(tournamentId) then
val cfg = TournamentBotConfig(autoJoinServerUrl, tournamentId, token, botId, hardestDifficulty)
if join(cfg) then startAsync(cfg)
else joinedTournaments.remove(tournamentId)
}
}
}
private def resolveAutoJoinToken(): Option[String] =
autoJoinToken match
case some @ Some(_) => some
case None =>
autoJoinToken = registerWithServer(autoJoinServerUrl, botName(hardestDifficulty))
autoJoinToken
private def openTournaments(): List[String] =
Try {
val response = client.target(autoJoinServerUrl)
.path("api").path("tournament")
.request(MediaType.APPLICATION_JSON).get()
if response.getStatus == 200 then
val node = objectMapper.readTree(response.readEntity(classOf[String]))
response.close()
node.path("created").elements().asScala.toList.map(_.path("id").asText()).filter(_.nonEmpty)
else { response.close(); Nil }
}.getOrElse(Nil)
private def resolveToken(difficulty: String): Option[String] = private def resolveToken(difficulty: String): Option[String] =
val name = botName(difficulty) val name = botName(difficulty)
val redisKey = s"${redisConfig.prefix}:tournament-bot:token:$name" val redisKey = s"${redisConfig.prefix}:tournament-bot:token:$name"
fetchTokenFromAccountService(name) registerWithServer(tournamentServiceUrl, name)
.orElse(fetchTokenFromAccountService(name))
.map { token => .map { token =>
redis.value(classOf[String]).set(redisKey, token) redis.value(classOf[String]).set(redisKey, token)
log.infof("Fetched fresh bot token for %s from account service", name) log.infof("Refreshed bot token for %s — stored in Redis", name)
token token
} }
.orElse { .orElse {
@@ -81,6 +133,26 @@ class TournamentBotGamePlayer:
} }
} }
private def registerWithServer(serverUrl: String, name: String): Option[String] =
Try {
val body = s"""{"name":"${name.replace("\"", "\\\"")}","isBot":true}"""
val response = client.target(serverUrl)
.path("api").path("auth").path("register")
.request(MediaType.APPLICATION_JSON)
.post(Entity.entity(body, MediaType.APPLICATION_JSON))
val status = response.getStatus
if status == 200 || status == 201 then
val token = objectMapper.readTree(response.readEntity(classOf[String])).path("token").asText()
response.close()
Option(token).filter(_.nonEmpty)
else
val errBody = response.readEntity(classOf[String])
log.warnf("Register %s on %s returned status %d: %s", name, serverUrl, status, errBody)
response.close()
None
}.recover { case ex => log.warnf(ex, "Register %s on %s failed", name, serverUrl); None }
.toOption.flatten
private def fetchTokenFromAccountService(name: String): Option[String] = private def fetchTokenFromAccountService(name: String): Option[String] =
Try(accountServiceClient.getBotToken(name).token).toOption.filter(_.nonEmpty) Try(accountServiceClient.getBotToken(name).token).toOption.filter(_.nonEmpty)
.orElse { .orElse {
@@ -92,14 +164,19 @@ class TournamentBotGamePlayer:
} }
private def parkOnStartup(token: Option[String]): Unit = private def parkOnStartup(token: Option[String]): Unit =
val localAccountUrl = System.getenv().asScala.getOrElse("ACCOUNT_SERVICE_URL", "http://localhost:8083")
token match token match
case None => log.warn("No bot token resolved — skipping park") case None => log.warn("No bot token resolved — skipping local park")
case Some(tok) => case Some(tok) =>
val localAccountUrl = System.getenv().asScala.getOrElse("ACCOUNT_SERVICE_URL", "http://localhost:8083") BotController.listBots.foreach(diff => parkOnAccountService(localAccountUrl, diff, tok))
BotController.listBots.foreach(diff => parkOn(localAccountUrl, diff, tok)) fetchRemoteServers().foreach { serverUrl =>
fetchRemoteServers().foreach { serverUrl => BotController.listBots.foreach { diff =>
BotController.listBots.foreach(diff => parkOn(serverUrl, diff, tok)) val name = botName(diff)
} registerWithServer(serverUrl, name) match
case None => log.warnf("Could not register %s on %s — skipping park", name, serverUrl)
case Some(tok) => parkOnTournamentServer(serverUrl, name, tok)
}
}
private def fetchRemoteServers(): List[String] = private def fetchRemoteServers(): List[String] =
Try { Try {
@@ -113,7 +190,7 @@ class TournamentBotGamePlayer:
else { response.close(); Nil } else { response.close(); Nil }
}.getOrElse(Nil) }.getOrElse(Nil)
private def parkOn(serverUrl: String, difficulty: String, token: String): Unit = private def parkOnAccountService(serverUrl: String, difficulty: String, token: String): Unit =
Try { Try {
val body = s"""{"name":"${botName(difficulty)}"}""" val body = s"""{"name":"${botName(difficulty)}"}"""
val response = client.target(serverUrl).path("api").path("account").path("bots") val response = client.target(serverUrl).path("api").path("account").path("bots")
@@ -127,6 +204,20 @@ class TournamentBotGamePlayer:
response.close() response.close()
}.failed.foreach(ex => log.warnf(ex, "Failed to park %s on %s", botName(difficulty), serverUrl)) }.failed.foreach(ex => log.warnf(ex, "Failed to park %s on %s", botName(difficulty), serverUrl))
private def parkOnTournamentServer(serverUrl: String, name: String, token: String): Unit =
Try {
val body = s"""{"name":"${name.replace("\"", "\\\"")}"}"""
val response = client.target(serverUrl).path("api").path("bots")
.request(MediaType.APPLICATION_JSON)
.header("Authorization", s"Bearer $token")
.post(Entity.entity(body, MediaType.APPLICATION_JSON))
if response.getStatus == 201 || response.getStatus == 200 then
val id = objectMapper.readTree(response.readEntity(classOf[String])).path("id").asText()
log.infof("Parked bot %s on tournament server %s as id %s", name, serverUrl, id)
else log.warnf("Park %s on tournament server %s returned status %d", name, serverUrl, response.getStatus)
response.close()
}.failed.foreach(ex => log.warnf(ex, "Failed to park %s on tournament server %s", name, serverUrl))
private def botName(difficulty: String): String = s"NowChess ${difficulty.capitalize}" private def botName(difficulty: String): String = s"NowChess ${difficulty.capitalize}"
def joinTournament( def joinTournament(
@@ -136,8 +227,8 @@ class TournamentBotGamePlayer:
): Either[String, String] = ): Either[String, String] =
val redisKey = s"${redisConfig.prefix}:tournament-bot:token:${botName(difficulty)}" val redisKey = s"${redisConfig.prefix}:tournament-bot:token:${botName(difficulty)}"
val resolvedToken = botToken.filter(_.nonEmpty) val resolvedToken = botToken.filter(_.nonEmpty)
.orElse(System.getenv().asScala.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty))
.orElse(Option(redis.value(classOf[String]).get(redisKey)).filter(_.nonEmpty)) .orElse(Option(redis.value(classOf[String]).get(redisKey)).filter(_.nonEmpty))
.orElse(System.getenv().asScala.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty))
resolvedToken match resolvedToken match
case None => Left("No bot token provided and TOURNAMENT_BOT_TOKEN not configured") case None => Left("No bot token provided and TOURNAMENT_BOT_TOKEN not configured")
case Some(token) => case Some(token) =>
@@ -192,12 +283,35 @@ class TournamentBotGamePlayer:
forEachLine(response.readEntity(classOf[InputStream])): line => forEachLine(response.readEntity(classOf[InputStream])): line =>
parse(line).foreach: node => parse(line).foreach: node =>
if node.path("type").asText() == "gameStart" then if node.path("type").asText() == "gameStart" then
onGameStart(cfg, node.path("gameId").asText(), node.path("color").asText()) onGameStart(cfg, node.path("gameId").asText())
private def onGameStart(cfg: TournamentBotConfig, gameId: String, color: String): Unit = private def onGameStart(cfg: TournamentBotConfig, gameId: String): Unit =
if gameId.nonEmpty && color.nonEmpty && activeGames.add(gameId) then if gameId.isEmpty then ()
workers.submit(new Runnable { def run(): Unit = playGame(cfg, gameId, color) }) else
() resolveColor(cfg, gameId) match
case None => log.debugf("Ignoring game %s — bot %s is not a participant", gameId, cfg.botId)
case Some(color) =>
if activeGames.add(gameId) then
workers.submit(new Runnable { def run(): Unit = playGame(cfg, gameId, color) })
()
private def resolveColor(cfg: TournamentBotConfig, gameId: String): Option[String] =
fetchGame(cfg, gameId).flatMap { node =>
val whiteId = node.path("white").path("id").asText()
val blackId = node.path("black").path("id").asText()
if whiteId == cfg.botId then Some("white")
else if blackId == cfg.botId then Some("black")
else None
}
private def fetchGame(cfg: TournamentBotConfig, gameId: String): Option[JsonNode] =
Try {
val response = authed(cfg, target(cfg).path("game").path(gameId)).get()
try
if response.getStatus == 200 then Some(objectMapper.readTree(response.readEntity(classOf[String])))
else { log.warnf("Game detail %s returned status %d", gameId, response.getStatus); None }
finally response.close()
}.getOrElse(None)
private def playGame(cfg: TournamentBotConfig, gameId: String, color: String): Unit = private def playGame(cfg: TournamentBotConfig, gameId: String, color: String): Unit =
Try { Try {
@@ -80,76 +80,58 @@ class HybridBotTest extends AnyFunSuite with Matchers:
bot.apply(ctx) should be(Some(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal()))) bot.apply(ctx) should be(Some(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())))
finally Files.deleteIfExists(tempFile) finally Files.deleteIfExists(tempFile)
test("HybridBot reports veto when classical and NNUE differ above threshold"): // Classical search picks mateMove (delivers mate); NNUE distrusts it and prefers altMove.
val forcedMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal()) private val mateMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
val oneMoveRules = new RuleSet: private val altMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal())
def candidateMoves(context: GameContext)(square: Square): List[Move] = List(forcedMove)
def legalMoves(context: GameContext)(square: Square): List[Move] = List(forcedMove)
def allLegalMoves(context: GameContext): List[Move] = List(forcedMove)
def isCheck(context: GameContext): Boolean = false
def isCheckmate(context: GameContext): Boolean = false
def isStalemate(context: GameContext): Boolean = false
def isInsufficientMaterial(context: GameContext): Boolean = false
def isFiftyMoveRule(context: GameContext): Boolean = false
def isThreefoldRepetition(context: GameContext): Boolean = false
def applyMove(context: GameContext)(move: Move): GameContext =
context.copy(turn = context.turn.opposite, moves = context.moves :+ move)
object LowNnue extends Evaluation: private def vetoRules: RuleSet = new RuleSet:
val CHECKMATE_SCORE: Int = 10_000_000 private def fresh(ctx: GameContext): Boolean = ctx.moves.isEmpty
val DRAW_SCORE: Int = 0 def candidateMoves(context: GameContext)(square: Square): List[Move] = Nil
def evaluate(context: GameContext): Int = 0 def legalMoves(context: GameContext)(square: Square): List[Move] = Nil
def allLegalMoves(context: GameContext): List[Move] =
if fresh(context) then List(mateMove, altMove) else Nil
def isCheck(context: GameContext): Boolean = false
def isCheckmate(context: GameContext): Boolean = context.moves.lastOption.contains(mateMove)
def isStalemate(context: GameContext): Boolean = context.moves.lastOption.contains(altMove)
def isInsufficientMaterial(context: GameContext): Boolean = false
def isFiftyMoveRule(context: GameContext): Boolean = false
def isThreefoldRepetition(context: GameContext): Boolean = false
def applyMove(context: GameContext)(move: Move): GameContext =
context.copy(turn = context.turn.opposite, moves = context.moves :+ move)
object HighClassic extends Evaluation: // NNUE rates the mate move worse for us (higher = better for opponent) than the alternative.
val CHECKMATE_SCORE: Int = 10_000_000 private object DistrustfulNnue extends Evaluation:
val DRAW_SCORE: Int = 0 val CHECKMATE_SCORE: Int = 10_000_000
def evaluate(context: GameContext): Int = 10_000 val DRAW_SCORE: Int = 0
def evaluate(context: GameContext): Int = if context.moves.lastOption.contains(mateMove) then 5_000 else 0
private object HighClassic extends Evaluation:
val CHECKMATE_SCORE: Int = 10_000_000
val DRAW_SCORE: Int = 0
def evaluate(context: GameContext): Int = if context.moves.lastOption.contains(mateMove) then 10_000 else 0
test("HybridBot switches to NNUE's preferred move and reports veto when evals diverge"):
val reported = AtomicBoolean(false) val reported = AtomicBoolean(false)
val bot = HybridBot( val bot = HybridBot(
BotDifficulty.Easy, BotDifficulty.Easy,
rules = oneMoveRules, rules = vetoRules,
nnueEvaluation = LowNnue, nnueEvaluation = DistrustfulNnue,
classicalEvaluation = HighClassic, classicalEvaluation = HighClassic,
vetoReporter = _ => reported.set(true), vetoReporter = _ => reported.set(true),
) )
bot.apply(GameContext.initial) should be(Some(forcedMove)) bot.apply(GameContext.initial) should be(Some(altMove))
reported.get should be(true) reported.get should be(true)
test("HybridBot default veto reporter prints when threshold is exceeded"): test("HybridBot default veto reporter prints when threshold is exceeded"):
val forcedMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal())
val oneMoveRules = new RuleSet:
def candidateMoves(context: GameContext)(square: Square): List[Move] = List(forcedMove)
def legalMoves(context: GameContext)(square: Square): List[Move] = List(forcedMove)
def allLegalMoves(context: GameContext): List[Move] = List(forcedMove)
def isCheck(context: GameContext): Boolean = false
def isCheckmate(context: GameContext): Boolean = false
def isStalemate(context: GameContext): Boolean = false
def isInsufficientMaterial(context: GameContext): Boolean = false
def isFiftyMoveRule(context: GameContext): Boolean = false
def isThreefoldRepetition(context: GameContext): Boolean = false
def applyMove(context: GameContext)(move: Move): GameContext =
context.copy(turn = context.turn.opposite, moves = context.moves :+ move)
object LowNnue extends Evaluation:
val CHECKMATE_SCORE: Int = 10_000_000
val DRAW_SCORE: Int = 0
def evaluate(context: GameContext): Int = 0
object HighClassic extends Evaluation:
val CHECKMATE_SCORE: Int = 10_000_000
val DRAW_SCORE: Int = 0
def evaluate(context: GameContext): Int = 10_000
val bot = HybridBot( val bot = HybridBot(
BotDifficulty.Easy, BotDifficulty.Easy,
rules = oneMoveRules, rules = vetoRules,
nnueEvaluation = LowNnue, nnueEvaluation = DistrustfulNnue,
classicalEvaluation = HighClassic, classicalEvaluation = HighClassic,
) )
val printed = Console.withOut(new java.io.ByteArrayOutputStream()) { val printed = Console.withOut(new java.io.ByteArrayOutputStream()) {
bot.apply(GameContext.initial) bot.apply(GameContext.initial)
} }
printed should be(Some(forcedMove)) printed should be(Some(altMove))
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0 MAJOR=0
MINOR=25 MINOR=29
PATCH=0 PATCH=0
+19
View File
@@ -55,3 +55,22 @@
* **tournament:** replace scala.util.Random singleton with UUID for native image ([a50884a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a50884a11b1de500e74c18fd08d2d102d53cc3e9)) * **tournament:** replace scala.util.Random singleton with UUID for native image ([a50884a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a50884a11b1de500e74c18fd08d2d102d53cc3e9))
* **tournament:** use Optional[String] for selfUrl ConfigProperty to avoid startup failure ([28cbc2e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/28cbc2e18447aa8a04a5868889a49b555075d0c6)) * **tournament:** use Optional[String] for selfUrl ConfigProperty to avoid startup failure ([28cbc2e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/28cbc2e18447aa8a04a5868889a49b555075d0c6))
* wrap server list response in ExternalTournamentServerList ([f2d79e4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f2d79e4952aea6bde762c294eb202474b7827054)) * wrap server list response in ExternalTournamentServerList ([f2d79e4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f2d79e4952aea6bde762c294eb202474b7827054))
## (2026-06-23)
### Features
* **analytics:** add Spark batch analytics module ([#70](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/70)) ([39f1657](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39f1657e1db6e84889af338c43be8cb5c03c3ec3))
* NCS-121 pipeline for tournament ([#68](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/68)) ([145f467](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/145f4676483f92bfe6f2d9ca40e2cb4200982e87))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* **reflection:** add GameWritebackEventDto to native reflection configuration ([1aee39c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1aee39c1ad286984501ac4b47da2b72d60b58a6f))
* **reflection:** add native reflection configuration for tournament classes ([65bc6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/65bc6a759937543df2d29905688bfa9e68d0c9d4))
* **tournament:** auto-join external tournaments and publish created ones ([#77](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/77)) ([9978b7e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9978b7ea78eb658a225a461b9cd339386c0c14f3))
* **tournament:** federate tournaments across clusters with DB replication ([5b000a6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5b000a6e5f04ea6770d1c7ab6bfdaded77a99172))
* **tournament:** remove dynamic server add/remove endpoints ([6d06edd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6d06edda69a50de65cd9efa27f26a4cc6b437f9d))
* **tournament:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
### Bug Fixes
* **tournament:** replace scala.util.Random singleton with UUID for native image ([a50884a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a50884a11b1de500e74c18fd08d2d102d53cc3e9))
* **tournament:** use Optional[String] for selfUrl ConfigProperty to avoid startup failure ([28cbc2e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/28cbc2e18447aa8a04a5868889a49b555075d0c6))
* wrap server list response in ExternalTournamentServerList ([f2d79e4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f2d79e4952aea6bde762c294eb202474b7827054))
@@ -30,6 +30,8 @@ nowchess:
tournament: tournament:
self-url: "" self-url: ""
external-servers: "" external-servers: ""
native-server-url: ${TOURNAMENT_NATIVE_SERVER_URL:http://141.37.123.132:8086}
director-name: ${TOURNAMENT_DIRECTOR_NAME:NowChess System}
mp: mp:
jwt: jwt:
@@ -53,6 +55,8 @@ mp:
tournament: tournament:
self-url: ${TOURNAMENT_SELF_URL:} self-url: ${TOURNAMENT_SELF_URL:}
external-servers: ${TOURNAMENT_EXTERNAL_SERVERS:} external-servers: ${TOURNAMENT_EXTERNAL_SERVERS:}
native-server-url: ${TOURNAMENT_NATIVE_SERVER_URL:http://141.37.123.132:8086}
director-name: ${TOURNAMENT_DIRECTOR_NAME:NowChess System}
"%test": "%test":
quarkus: quarkus:
@@ -40,6 +40,12 @@ class TournamentResource:
@ConfigProperty(name = "nowchess.tournament.self-url") @ConfigProperty(name = "nowchess.tournament.self-url")
var selfUrl: Optional[String] = uninitialized var selfUrl: Optional[String] = uninitialized
@ConfigProperty(name = "nowchess.tournament.native-server-url", defaultValue = "http://141.37.123.132:8086")
var nativeServerUrl: String = uninitialized
@ConfigProperty(name = "nowchess.tournament.director-name", defaultValue = "NowChess System")
var directorName: String = uninitialized
// scalafix:on // scalafix:on
@GET @GET
@@ -95,8 +101,31 @@ class TournamentResource:
log.warnf("Failed to replicate tournament %s to %s", t.id, remoteUrl) log.warnf("Failed to replicate tournament %s to %s", t.id, remoteUrl)
} }
} }
publishToNativeServer(t)
Response.status(Response.Status.CREATED).entity(tournamentService.toDto(t)).build() Response.status(Response.Status.CREATED).entity(tournamentService.toDto(t)).build()
private def publishToNativeServer(t: de.nowchess.tournament.domain.Tournament): Unit =
if nativeServerUrl.nonEmpty then
val form = encodeForm(
Map(
"name" -> t.fullName,
"nbRounds" -> t.nbRounds.toString,
"clockLimit" -> t.clockLimit.toString,
"clockIncrement" -> t.clockIncrement.toString,
"rated" -> t.rated.toString,
),
)
if !externalClient.publishNative(nativeServerUrl, directorName, form) then
log.warnf("Failed to publish tournament %s to native server %s", t.id, nativeServerUrl)
private def encodeForm(params: Map[String, String]): String =
params
.map((k, v) => s"${enc(k)}=${enc(v)}")
.mkString("&")
private def enc(s: String): String =
java.net.URLEncoder.encode(s, "UTF-8")
@GET @GET
@Path("/{id}") @Path("/{id}")
@PermitAll @PermitAll
@@ -14,10 +14,56 @@ class ExternalTournamentClient:
// scalafix:off DisableSyntax.var // scalafix:off DisableSyntax.var
@Inject var objectMapper: ObjectMapper = uninitialized @Inject var objectMapper: ObjectMapper = uninitialized
@volatile private var directorToken: Option[String] = None
// scalafix:on // scalafix:on
private def buildClient(): Client = ClientBuilder.newClient() private def buildClient(): Client = ClientBuilder.newClient()
def publishNative(serverUrl: String, directorName: String, form: String): Boolean =
val token = directorToken.orElse {
val fresh = registerDirector(serverUrl, directorName)
directorToken = fresh
fresh
}
token.exists { tok =>
if createNative(serverUrl, tok, form) then true
else
val refreshed = registerDirector(serverUrl, directorName)
directorToken = refreshed
refreshed.exists(createNative(serverUrl, _, form))
}
private def registerDirector(serverUrl: String, name: String): Option[String] =
Try {
val client = buildClient()
val body = s"""{"name":"${name.replace("\"", "\\\"")}","isBot":false}"""
val response = client
.target(s"$serverUrl/api/auth/register")
.request(MediaType.APPLICATION_JSON)
.post(Entity.entity(body, MediaType.APPLICATION_JSON))
try
if response.getStatus / 100 == 2 then
Option(objectMapper.readTree(response.readEntity(classOf[String])).path("token").asText()).filter(_.nonEmpty)
else None
finally
response.close()
client.close()
}.getOrElse(None)
private def createNative(serverUrl: String, token: String, form: String): Boolean =
Try {
val client = buildClient()
val response = client
.target(s"$serverUrl/api/tournament")
.request(MediaType.APPLICATION_JSON)
.header("Authorization", s"Bearer $token")
.post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED))
try response.getStatus / 100 == 2
finally
response.close()
client.close()
}.getOrElse(false)
def fetchList(serverUrl: String): Option[JsonNode] = def fetchList(serverUrl: String): Option[JsonNode] =
Try { Try {
val client = buildClient() val client = buildClient()
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0 MAJOR=0
MINOR=5 MINOR=6
PATCH=0 PATCH=0