Compare commits
63 Commits
tournament-0.4.0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a397eed7f | |||
| 9d656624d8 | |||
| e2b4342f60 | |||
| 9d56446c65 | |||
| 1c80abdb8a | |||
| c8cbcdca3b | |||
| e4fee85134 | |||
| b4709b4a33 | |||
| 9f9140cb58 | |||
| fa10852bc9 | |||
| 44f376f032 | |||
| 7372867a82 | |||
| c3e7b82ae8 | |||
| e88b081947 | |||
| 1b30c3be39 | |||
| f8ca95af3c | |||
| 4a50db0721 | |||
| 260db25803 | |||
| 80e1cc258b | |||
| bfc46723e6 | |||
| bace029a8a | |||
| 7664042193 | |||
| a604b4ad42 | |||
| fdf4c94811 | |||
| d9f30f0bfe | |||
| 1f4e9c8498 | |||
| e2b13c0c8f | |||
| bfb15c7299 | |||
| 627f017cdc | |||
| 10113fd057 | |||
| b57e5827df | |||
| b98bdd2a64 | |||
| 285b73efbd | |||
| 06f2adfeb6 | |||
| 4651bb796f | |||
| 1df29cf3a6 | |||
| ff492e1dc8 | |||
| 9978b7ea78 | |||
| 9a9784673f | |||
| 83dd2d4335 | |||
| 4377e05d5c | |||
| 3188241737 | |||
| 64b5d5567f | |||
| 65c3fabd91 | |||
| b0ddb274d2 | |||
| fd23c6e514 | |||
| 386ddc5c19 | |||
| a63d195cb3 | |||
| 28cbc2e184 | |||
| 1be9949c0b | |||
| 6d06edda69 | |||
| 845dc9c293 | |||
| 5b000a6e5f | |||
| 97015cb95e | |||
| a268a9acb7 | |||
| 71cb2cc56c | |||
| f43d1930d8 | |||
| da0e6d1ee2 | |||
| a6c600d6ce | |||
| 8e17c14dff | |||
| a91ba5da9a | |||
| f079f42d28 | |||
| be941ff414 |
@@ -127,7 +127,7 @@ jobs:
|
|||||||
if [ -n "$MARCH" ]; then
|
if [ -n "$MARCH" ]; then
|
||||||
MARCH_ARG="-Dquarkus.native.additional-build-args=$MARCH"
|
MARCH_ARG="-Dquarkus.native.additional-build-args=$MARCH"
|
||||||
fi
|
fi
|
||||||
./gradlew :modules:${{ matrix.module }}:build -x test -Dquarkus.native.enabled=true -Dquarkus.package.jar.enabled=false -Dquarkus.profile=deployed $MARCH_ARG --no-daemon
|
./gradlew :modules:${{ matrix.module }}:build -x test -x spotlessScalaCheck -x checkScalafix -Dquarkus.native.enabled=true -Dquarkus.package.jar.enabled=false -Dquarkus.profile=deployed $MARCH_ARG --no-daemon
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
if: steps.image-check.outputs.exists == 'false'
|
if: steps.image-check.outputs.exists == 'false'
|
||||||
|
|||||||
@@ -610,3 +610,45 @@
|
|||||||
|
|
||||||
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||||
* 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
|
||||||
|
|
||||||
|
* **account:** implement token pair handling for login and refresh endpoints ([9296db8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9296db88b7131bbda9b9b0da65c327ef9063ee31))
|
||||||
|
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
|
||||||
|
* 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))
|
||||||
|
* **api:** define shared EventEnvelope and EventType for Redis EventBus ([#61](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/61)) ([595c172](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/595c172900da99de367c274488c3ccbeaef55882))
|
||||||
|
* **bot-platform:** migrate BotRegistry to Redis Streams consumer group ([#63](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/63)) ([0ad2e10](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0ad2e10999213df6dd00f0c31a088c28a4dc0083))
|
||||||
|
* **config:** add H2 database configuration for testing environment ([39c9e49](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39c9e492cef2515368c074da9406f95e9c0c9e64))
|
||||||
|
* **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))
|
||||||
|
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
|
||||||
|
* 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:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
|
||||||
|
* **reflection:** add native reflection configuration for tournament classes ([65bc6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/65bc6a759937543df2d29905688bfa9e68d0c9d4))
|
||||||
|
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||||
|
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
|
||||||
|
* **ws:** migrate challenge notifications to Redis Streams ([#66](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/66)) ([55f102c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/55f102cbaa684be94a158b16aaa42a50b36afaf3))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **account:** configure JDBC connection pool size to prevent exhaustion under load ([29072ef](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/29072efbfb1cfa1c3b1a85b4c1a587c971d245f9))
|
||||||
|
* **auth:** add InternalClientHeadersFactory for custom client headers management ([e279c39](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e279c39246470156bf11e745ee72204018d4229d))
|
||||||
|
* 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))
|
||||||
|
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
|
||||||
|
* **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))
|
||||||
|
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
|
||||||
|
* **tests:** update token path to accessToken in ChallengeResourceTest ([354db11](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/354db11972342c47a1034303c11bccfb92e60109))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
|||||||
@@ -193,6 +193,14 @@ class AccountResource:
|
|||||||
val bots = accountService.getOfficialBotAccounts()
|
val bots = accountService.getOfficialBotAccounts()
|
||||||
Response.ok(bots.map(toOfficialBotDto)).build()
|
Response.ok(bots.map(toOfficialBotDto)).build()
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/official-bots/{name}/token")
|
||||||
|
@InternalOnly
|
||||||
|
def getOfficialBotToken(@PathParam("name") name: String): Response =
|
||||||
|
accountService.getOfficialBotTokenByName(name) match
|
||||||
|
case None => Response.status(Response.Status.NOT_FOUND).build()
|
||||||
|
case Some(token) => Response.ok(RotatedTokenDto(token)).build()
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/official-bots")
|
@Path("/official-bots")
|
||||||
@RolesAllowed(Array("Admin"))
|
@RolesAllowed(Array("Admin"))
|
||||||
|
|||||||
@@ -225,6 +225,9 @@ class AccountService:
|
|||||||
def getOfficialBotAccounts(): List[OfficialBotAccount] =
|
def getOfficialBotAccounts(): List[OfficialBotAccount] =
|
||||||
officialBotAccountRepository.findAll()
|
officialBotAccountRepository.findAll()
|
||||||
|
|
||||||
|
def getOfficialBotTokenByName(name: String): Option[String] =
|
||||||
|
officialBotAccountRepository.findByName(name).map(_.token)
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
def deleteOfficialBotAccount(botId: UUID): Either[AccountError, Unit] =
|
def deleteOfficialBotAccount(botId: UUID): Either[AccountError, Unit] =
|
||||||
officialBotAccountRepository.findById(botId) match
|
officialBotAccountRepository.findById(botId) match
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=25
|
MINOR=26
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -23,3 +23,78 @@
|
|||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* **analytics:** upgrade Spark to 4.0.3 — 3.5.x has no official Docker image ([46af115](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/46af1154de34a8596cb6cb28c6fad7aba90f597c))
|
* **analytics:** upgrade Spark to 4.0.3 — 3.5.x has no official Docker image ([46af115](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/46af1154de34a8596cb6cb28c6fad7aba90f597c))
|
||||||
|
## (2026-06-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add Dockerfile, CI workflow, and stable jar name for K8s deployment ([95215b6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/95215b6a420fd526df1aa395f9b087556c8ad03b))
|
||||||
|
* **analytics:** add PostgreSQL JDBC write-back to all four batch jobs ([0e0ea4c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0e0ea4c9893c6efed52e633e55d05ab3ed004502))
|
||||||
|
* **analytics:** add Spark batch analytics module ([259b3bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/259b3bbb24c0f23326269b93f4b3c84012f727cd))
|
||||||
|
* **analytics:** add Structured Streaming, MLlib clustering, GraphX jobs ([e1d80b9](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e1d80b9331666feea191b1fd08aa762f3581c918))
|
||||||
|
* **official-bots:** park expert bot on tournament server at startup ([#76](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/76)) ([751a58b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/751a58b6061f7434115e229a7661894c76768bc2))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** upgrade Spark to 4.0.3 — 3.5.x has no official Docker image ([46af115](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/46af1154de34a8596cb6cb28c6fad7aba90f597c))
|
||||||
|
## (2026-06-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add 7 new Spark analytics jobs and extend GameSource ([8e17c14](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8e17c14dff740cd115011dfbf17de35083b8fe46))
|
||||||
|
* **analytics:** add Dockerfile, CI workflow, and stable jar name for K8s deployment ([95215b6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/95215b6a420fd526df1aa395f9b087556c8ad03b))
|
||||||
|
* **analytics:** add PostgreSQL JDBC write-back to all four batch jobs ([0e0ea4c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0e0ea4c9893c6efed52e633e55d05ab3ed004502))
|
||||||
|
* **analytics:** add Spark batch analytics module ([259b3bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/259b3bbb24c0f23326269b93f4b3c84012f727cd))
|
||||||
|
* **analytics:** add Structured Streaming, MLlib clustering, GraphX jobs ([e1d80b9](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e1d80b9331666feea191b1fd08aa762f3581c918))
|
||||||
|
* **official-bots:** park expert bot on tournament server at startup ([#76](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/76)) ([751a58b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/751a58b6061f7434115e229a7661894c76768bc2))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** upgrade Spark to 4.0.3 — 3.5.x has no official Docker image ([46af115](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/46af1154de34a8596cb6cb28c6fad7aba90f597c))
|
||||||
|
## (2026-06-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add 7 new Spark analytics jobs and extend GameSource ([8e17c14](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8e17c14dff740cd115011dfbf17de35083b8fe46))
|
||||||
|
* **analytics:** add Dockerfile, CI workflow, and stable jar name for K8s deployment ([95215b6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/95215b6a420fd526df1aa395f9b087556c8ad03b))
|
||||||
|
* **analytics:** add PostgreSQL JDBC write-back to all four batch jobs ([0e0ea4c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0e0ea4c9893c6efed52e633e55d05ab3ed004502))
|
||||||
|
* **analytics:** add Spark batch analytics module ([259b3bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/259b3bbb24c0f23326269b93f4b3c84012f727cd))
|
||||||
|
* **analytics:** add Structured Streaming, MLlib clustering, GraphX jobs ([e1d80b9](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e1d80b9331666feea191b1fd08aa762f3581c918))
|
||||||
|
* **analytics:** always write results to PostgreSQL regardless of input source ([da0e6d1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/da0e6d1ee2d391ecb6291396f82471eb51b1b25e))
|
||||||
|
* **official-bots:** park expert bot on tournament server at startup ([#76](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/76)) ([751a58b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/751a58b6061f7434115e229a7661894c76768bc2))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** upgrade Spark to 4.0.3 — 3.5.x has no official Docker image ([46af115](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/46af1154de34a8596cb6cb28c6fad7aba90f597c))
|
||||||
|
## (2026-06-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add 7 new Spark analytics jobs and extend GameSource ([8e17c14](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8e17c14dff740cd115011dfbf17de35083b8fe46))
|
||||||
|
* **analytics:** add Dockerfile, CI workflow, and stable jar name for K8s deployment ([95215b6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/95215b6a420fd526df1aa395f9b087556c8ad03b))
|
||||||
|
* **analytics:** add PostgreSQL JDBC write-back to all four batch jobs ([0e0ea4c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0e0ea4c9893c6efed52e633e55d05ab3ed004502))
|
||||||
|
* **analytics:** add Spark batch analytics module ([259b3bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/259b3bbb24c0f23326269b93f4b3c84012f727cd))
|
||||||
|
* **analytics:** add Structured Streaming, MLlib clustering, GraphX jobs ([e1d80b9](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e1d80b9331666feea191b1fd08aa762f3581c918))
|
||||||
|
* **analytics:** always write results to PostgreSQL regardless of input source ([da0e6d1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/da0e6d1ee2d391ecb6291396f82471eb51b1b25e))
|
||||||
|
* **official-bots:** park expert bot on tournament server at startup ([#76](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/76)) ([751a58b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/751a58b6061f7434115e229a7661894c76768bc2))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** upgrade Spark to 4.0.3 — 3.5.x has no official Docker image ([46af115](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/46af1154de34a8596cb6cb28c6fad7aba90f597c))
|
||||||
|
* **analytics:** write decompressed PGN to shared PVC path for executor access ([a268a9a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a268a9acb7ba190c76e996ccf3ea3bd00e5cec92))
|
||||||
|
## (2026-06-23)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add 7 new Spark analytics jobs and extend GameSource ([8e17c14](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8e17c14dff740cd115011dfbf17de35083b8fe46))
|
||||||
|
* **analytics:** add accuracy and blunder analysis job for Lichess data ([c3e7b82](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c3e7b82ae806adf5713ce4d267c1155e73a40ff5))
|
||||||
|
* **analytics:** add Dockerfile, CI workflow, and stable jar name for K8s deployment ([95215b6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/95215b6a420fd526df1aa395f9b087556c8ad03b))
|
||||||
|
* **analytics:** add PostgreSQL JDBC write-back to all four batch jobs ([0e0ea4c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0e0ea4c9893c6efed52e633e55d05ab3ed004502))
|
||||||
|
* **analytics:** add Spark batch analytics module ([259b3bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/259b3bbb24c0f23326269b93f4b3c84012f727cd))
|
||||||
|
* **analytics:** add Structured Streaming, MLlib clustering, GraphX jobs ([e1d80b9](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e1d80b9331666feea191b1fd08aa762f3581c918))
|
||||||
|
* **analytics:** always write results to PostgreSQL regardless of input source ([da0e6d1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/da0e6d1ee2d391ecb6291396f82471eb51b1b25e))
|
||||||
|
* **official-bots:** park expert bot on tournament server at startup ([#76](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/76)) ([751a58b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/751a58b6061f7434115e229a7661894c76768bc2))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** upgrade Spark to 4.0.3 — 3.5.x has no official Docker image ([46af115](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/46af1154de34a8596cb6cb28c6fad7aba90f597c))
|
||||||
|
* **analytics:** write decompressed PGN to shared PVC path for executor access ([a268a9a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a268a9acb7ba190c76e996ccf3ea3bd00e5cec92))
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.expressions.Window
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
|
||||||
|
/** Per-move accuracy & blunder analysis mined from Lichess `[%eval ...]` move annotations.
|
||||||
|
*
|
||||||
|
* Unlike the flat single-`groupBy` summaries (opening rates, colour advantage), this job reconstructs the *quality of
|
||||||
|
* every move* from the engine evaluations Lichess embeds in the movetext (`{ [%eval 0.24] }`, mate scores `[%eval
|
||||||
|
* #-3]`) and turns them into the same accuracy signals lichess.com surfaces: average centipawn loss (ACPL), and counts
|
||||||
|
* of inaccuracies / mistakes / blunders.
|
||||||
|
*
|
||||||
|
* Pipeline (all Spark SQL string/array functions + window funcs — no UDFs, Catalyst-friendly):
|
||||||
|
* 1. Keep only games carrying `[%eval` comments.
|
||||||
|
* 2. `regexp_extract_all` pulls every eval in ply order; mate scores collapse to ±10 pawns, normal evals are clamped
|
||||||
|
* to ±10 so a single huge swing cannot dominate the mean. All evals are White-POV pawns.
|
||||||
|
* 3. `posexplode` → one row per ply; a per-game window `lag` gives the eval *before* the move.
|
||||||
|
* 4. Centipawn loss for the side that moved = how much the eval moved against them (white wants it up, black down),
|
||||||
|
* floored at 0 and scaled to centipawns.
|
||||||
|
* 5. Roll up to (game, side): ACPL + inaccuracy(≥50cp) / mistake(≥100cp) / blunder(≥200cp) counts, tagged with that
|
||||||
|
* side's Elo and whether they won.
|
||||||
|
*
|
||||||
|
* Outputs (Parquet + CSV + JDBC):
|
||||||
|
* - `accuracy_by_rating` — ACPL, avg blunders/mistakes/inaccuracies per game and win-rate, per Elo band. Shows how
|
||||||
|
* move quality scales with rating.
|
||||||
|
* - `blunder_outcome` — win-rate bucketed by number of blunders in the game. Quantifies "one blunder costs you the
|
||||||
|
* game".
|
||||||
|
*
|
||||||
|
* Requires the eval-annotated Lichess dump (`NOWCHESS_PGN_PATH` → an evals dump); JDBC games carry no per-move evals.
|
||||||
|
*/
|
||||||
|
object AccuracyBlunderJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-accuracy"
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Accuracy & Blunders")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String, outputDir: String): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.loadExtended(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.select("pgn", "result", "white_elo", "black_elo")
|
||||||
|
.filter(F.col("result").isNotNull.and(F.col("pgn").contains("[%eval")))
|
||||||
|
.withColumn("game_id", F.monotonically_increasing_id())
|
||||||
|
|
||||||
|
// White-POV pawn evals in ply order; mate → ±10, normal evals clamped to ±10.
|
||||||
|
val evalStrs = F.expr("""regexp_extract_all(pgn, '\\[%eval ([^\\]]+)\\]', 1)""")
|
||||||
|
val evalCps = F.expr(
|
||||||
|
"transform(eval_strs, x -> CASE " +
|
||||||
|
"WHEN x LIKE '#-%' THEN -10.0 " +
|
||||||
|
"WHEN x LIKE '#%' THEN 10.0 " +
|
||||||
|
"ELSE greatest(-10.0, least(10.0, cast(x as double))) END)",
|
||||||
|
)
|
||||||
|
|
||||||
|
val withEvals = games
|
||||||
|
.withColumn("eval_strs", evalStrs)
|
||||||
|
.withColumn("eval_cp", evalCps)
|
||||||
|
.filter(F.size(F.col("eval_cp")) >= 2)
|
||||||
|
|
||||||
|
val plies = withEvals.select(
|
||||||
|
F.col("game_id"),
|
||||||
|
F.col("result"),
|
||||||
|
F.col("white_elo"),
|
||||||
|
F.col("black_elo"),
|
||||||
|
F.posexplode(F.col("eval_cp")).as(Seq("ply", "eval_after")),
|
||||||
|
)
|
||||||
|
|
||||||
|
val byGame = Window.partitionBy("game_id").orderBy("ply")
|
||||||
|
val mover = F.when(F.col("ply") % 2 === 0, "white").otherwise("black")
|
||||||
|
val evalBefore = F.coalesce(F.lag("eval_after", 1).over(byGame), F.lit(0.15))
|
||||||
|
val cpl = F.greatest(
|
||||||
|
F.lit(0.0),
|
||||||
|
F.when(F.col("mover") === "white", evalBefore - F.col("eval_after"))
|
||||||
|
.otherwise(F.col("eval_after") - evalBefore),
|
||||||
|
) * 100
|
||||||
|
|
||||||
|
val moves = plies
|
||||||
|
.withColumn("mover", mover)
|
||||||
|
.withColumn("cpl", cpl)
|
||||||
|
|
||||||
|
val perSide = moves
|
||||||
|
.groupBy("game_id", "mover", "result", "white_elo", "black_elo")
|
||||||
|
.agg(
|
||||||
|
F.round(F.avg("cpl"), 1).as("acpl"),
|
||||||
|
F.sum(F.when(F.col("cpl") >= 200, 1).otherwise(0)).as("blunders"),
|
||||||
|
F.sum(F.when(F.col("cpl") >= 100 && F.col("cpl") < 200, 1).otherwise(0)).as("mistakes"),
|
||||||
|
F.sum(F.when(F.col("cpl") >= 50 && F.col("cpl") < 100, 1).otherwise(0)).as("inaccuracies"),
|
||||||
|
)
|
||||||
|
.withColumn(
|
||||||
|
"self_elo",
|
||||||
|
F.when(F.col("mover") === "white", F.col("white_elo")).otherwise(F.col("black_elo")),
|
||||||
|
)
|
||||||
|
.withColumn("won", F.when(F.col("mover") === F.col("result"), 1).otherwise(0))
|
||||||
|
|
||||||
|
writeAccuracyByRating(perSide, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
writeBlunderOutcome(perSide, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
|
||||||
|
private def writeAccuracyByRating(
|
||||||
|
perSide: org.apache.spark.sql.DataFrame,
|
||||||
|
jdbcUrl: String,
|
||||||
|
dbUser: String,
|
||||||
|
dbPass: String,
|
||||||
|
outputDir: String,
|
||||||
|
): Unit =
|
||||||
|
val elo = F.col("self_elo")
|
||||||
|
val band = F
|
||||||
|
.when(elo < 1200, "<1200")
|
||||||
|
.when(elo < 1500, "1200–1499")
|
||||||
|
.when(elo < 1800, "1500–1799")
|
||||||
|
.when(elo < 2100, "1800–2099")
|
||||||
|
.otherwise("2100+")
|
||||||
|
val bandOrder = F
|
||||||
|
.when(elo < 1200, 1)
|
||||||
|
.when(elo < 1500, 2)
|
||||||
|
.when(elo < 1800, 3)
|
||||||
|
.when(elo < 2100, 4)
|
||||||
|
.otherwise(5)
|
||||||
|
|
||||||
|
val stats = perSide
|
||||||
|
.filter(elo.isNotNull)
|
||||||
|
.withColumn("band", band)
|
||||||
|
.withColumn("band_order", bandOrder)
|
||||||
|
.groupBy("band", "band_order")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("player_games"),
|
||||||
|
F.round(F.avg("acpl"), 1).as("avg_acpl"),
|
||||||
|
F.round(F.avg("blunders"), 2).as("avg_blunders"),
|
||||||
|
F.round(F.avg("mistakes"), 2).as("avg_mistakes"),
|
||||||
|
F.round(F.avg("inaccuracies"), 2).as("avg_inaccuracies"),
|
||||||
|
F.round(F.avg("won"), 3).as("win_rate"),
|
||||||
|
)
|
||||||
|
.orderBy(F.asc("band_order"))
|
||||||
|
.drop("band_order")
|
||||||
|
|
||||||
|
write(stats, outputDir, "accuracy_by_rating", jdbcUrl, dbUser, dbPass, "analytics_accuracy_by_rating")
|
||||||
|
|
||||||
|
private def writeBlunderOutcome(
|
||||||
|
perSide: org.apache.spark.sql.DataFrame,
|
||||||
|
jdbcUrl: String,
|
||||||
|
dbUser: String,
|
||||||
|
dbPass: String,
|
||||||
|
outputDir: String,
|
||||||
|
): Unit =
|
||||||
|
val b = F.col("blunders")
|
||||||
|
val bucket = F.when(b === 0, "0").when(b === 1, "1").when(b === 2, "2").otherwise("3+")
|
||||||
|
val order = F.when(b === 0, 0).when(b === 1, 1).when(b === 2, 2).otherwise(3)
|
||||||
|
|
||||||
|
val stats = perSide
|
||||||
|
.withColumn("blunder_bucket", bucket)
|
||||||
|
.withColumn("bucket_order", order)
|
||||||
|
.groupBy("blunder_bucket", "bucket_order")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("player_games"),
|
||||||
|
F.round(F.avg("won"), 3).as("win_rate"),
|
||||||
|
F.round(F.avg("acpl"), 1).as("avg_acpl"),
|
||||||
|
)
|
||||||
|
.orderBy(F.asc("bucket_order"))
|
||||||
|
.drop("bucket_order")
|
||||||
|
|
||||||
|
write(stats, outputDir, "blunder_outcome", jdbcUrl, dbUser, dbPass, "analytics_blunder_outcome")
|
||||||
|
|
||||||
|
private def write(
|
||||||
|
df: org.apache.spark.sql.DataFrame,
|
||||||
|
outputDir: String,
|
||||||
|
name: String,
|
||||||
|
jdbcUrl: String,
|
||||||
|
dbUser: String,
|
||||||
|
dbPass: String,
|
||||||
|
table: String,
|
||||||
|
): Unit =
|
||||||
|
df.write.mode("overwrite").parquet(s"$outputDir/$name")
|
||||||
|
df.write.mode("overwrite").option("header", "true").csv(s"$outputDir/${name}_csv")
|
||||||
|
if !GameSource.isPgnMode then
|
||||||
|
df.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", table)
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.expressions.Window
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
|
||||||
|
/** Time-management & clock-pressure analysis mined from Lichess `[%clk ...]` move annotations.
|
||||||
|
*
|
||||||
|
* Lichess records each player's remaining clock after every move (`{ [%clk 0:02:31] }`). This job reconstructs
|
||||||
|
* per-move thinking time and remaining-time from those stamps to answer questions the existing time-control summary
|
||||||
|
* cannot: how long do players actually think, how often do they fall into time scrambles (<10 s left), how often do
|
||||||
|
* they flag (lose on time), and does burning the clock correlate with winning?
|
||||||
|
*
|
||||||
|
* Pipeline (Spark SQL string/array funcs + window funcs — no UDFs):
|
||||||
|
* 1. `regexp_extract_all` pulls every `h:mm:ss` clock in ply order, converted to seconds.
|
||||||
|
* 2. `posexplode` → one row per ply; even plies are White's clock, odd plies Black's.
|
||||||
|
* 3. A per-(game,side) window `lag` gives the same side's previous clock; the difference is that move's thinking time.
|
||||||
|
* Remaining clock <10 s marks a time-scramble move.
|
||||||
|
* 4. Roll up to (game, side): avg move time, scramble fraction, min clock, Elo, win flag, and whether the side lost on
|
||||||
|
* time (`Termination "Time forfeit"`).
|
||||||
|
*
|
||||||
|
* Outputs (Parquet + CSV + JDBC):
|
||||||
|
* - `clock_by_rating` — avg move time, scramble fraction, flag-loss rate and win-rate per Elo band.
|
||||||
|
* - `scramble_outcome` — win-rate bucketed by how much of the game was played in time-scramble. Quantifies the cost of
|
||||||
|
* time trouble.
|
||||||
|
*
|
||||||
|
* Requires a clock-annotated Lichess dump (`NOWCHESS_PGN_PATH`).
|
||||||
|
*/
|
||||||
|
object ClockPressureJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-clock-pressure"
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Clock Pressure")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String, outputDir: String): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.loadExtended(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.select("pgn", "result", "white_elo", "black_elo", "termination")
|
||||||
|
.filter(F.col("result").isNotNull.and(F.col("pgn").contains("[%clk")))
|
||||||
|
.withColumn("game_id", F.monotonically_increasing_id())
|
||||||
|
|
||||||
|
val clkStrs = F.expr("""regexp_extract_all(pgn, '\\[%clk ([^\\]]+)\\]', 1)""")
|
||||||
|
// "h:mm:ss" → seconds.
|
||||||
|
val clkSecs = F.expr(
|
||||||
|
"transform(clk_strs, x -> " +
|
||||||
|
"cast(split(x, ':')[0] as double) * 3600 + " +
|
||||||
|
"cast(split(x, ':')[1] as double) * 60 + " +
|
||||||
|
"cast(split(x, ':')[2] as double))",
|
||||||
|
)
|
||||||
|
|
||||||
|
val withClk = games
|
||||||
|
.withColumn("clk_strs", clkStrs)
|
||||||
|
.withColumn("clk_sec", clkSecs)
|
||||||
|
.filter(F.size(F.col("clk_sec")) >= 4)
|
||||||
|
|
||||||
|
val plies = withClk.select(
|
||||||
|
F.col("game_id"),
|
||||||
|
F.col("result"),
|
||||||
|
F.col("white_elo"),
|
||||||
|
F.col("black_elo"),
|
||||||
|
F.col("termination"),
|
||||||
|
F.posexplode(F.col("clk_sec")).as(Seq("ply", "clk_after")),
|
||||||
|
)
|
||||||
|
|
||||||
|
val mover = F.when(F.col("ply") % 2 === 0, "white").otherwise("black")
|
||||||
|
val bySide = Window.partitionBy("game_id", "mover").orderBy("ply")
|
||||||
|
val moveTime = F.lag("clk_after", 1).over(bySide) - F.col("clk_after")
|
||||||
|
|
||||||
|
val moves = plies
|
||||||
|
.withColumn("mover", mover)
|
||||||
|
.withColumn("move_time", moveTime)
|
||||||
|
|
||||||
|
val perSide = moves
|
||||||
|
.groupBy("game_id", "mover", "result", "white_elo", "black_elo", "termination")
|
||||||
|
.agg(
|
||||||
|
F.round(F.avg("move_time"), 1).as("avg_move_time"),
|
||||||
|
F.count("*").as("moves"),
|
||||||
|
F.round(F.min("clk_after"), 1).as("min_clk"),
|
||||||
|
F.sum(F.when(F.col("clk_after") < 10, 1).otherwise(0)).as("scramble_moves"),
|
||||||
|
)
|
||||||
|
.withColumn("scramble_fraction", F.round(F.col("scramble_moves") / F.col("moves"), 3))
|
||||||
|
.withColumn(
|
||||||
|
"self_elo",
|
||||||
|
F.when(F.col("mover") === "white", F.col("white_elo")).otherwise(F.col("black_elo")),
|
||||||
|
)
|
||||||
|
.withColumn("won", F.when(F.col("mover") === F.col("result"), 1).otherwise(0))
|
||||||
|
.withColumn(
|
||||||
|
"flag_loss",
|
||||||
|
F.when(
|
||||||
|
F.coalesce(F.col("termination"), F.lit("")).contains("Time forfeit") && F.col("won") === 0,
|
||||||
|
1,
|
||||||
|
).otherwise(0),
|
||||||
|
)
|
||||||
|
|
||||||
|
writeClockByRating(perSide, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
writeScrambleOutcome(perSide, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
|
||||||
|
private def writeClockByRating(
|
||||||
|
perSide: org.apache.spark.sql.DataFrame,
|
||||||
|
jdbcUrl: String,
|
||||||
|
dbUser: String,
|
||||||
|
dbPass: String,
|
||||||
|
outputDir: String,
|
||||||
|
): Unit =
|
||||||
|
val elo = F.col("self_elo")
|
||||||
|
val band = F
|
||||||
|
.when(elo < 1200, "<1200")
|
||||||
|
.when(elo < 1500, "1200–1499")
|
||||||
|
.when(elo < 1800, "1500–1799")
|
||||||
|
.when(elo < 2100, "1800–2099")
|
||||||
|
.otherwise("2100+")
|
||||||
|
val bandOrder = F
|
||||||
|
.when(elo < 1200, 1)
|
||||||
|
.when(elo < 1500, 2)
|
||||||
|
.when(elo < 1800, 3)
|
||||||
|
.when(elo < 2100, 4)
|
||||||
|
.otherwise(5)
|
||||||
|
|
||||||
|
val stats = perSide
|
||||||
|
.filter(elo.isNotNull)
|
||||||
|
.withColumn("band", band)
|
||||||
|
.withColumn("band_order", bandOrder)
|
||||||
|
.groupBy("band", "band_order")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("player_games"),
|
||||||
|
F.round(F.avg("avg_move_time"), 1).as("avg_move_time_s"),
|
||||||
|
F.round(F.avg("scramble_fraction"), 3).as("avg_scramble_fraction"),
|
||||||
|
F.round(F.avg("flag_loss"), 3).as("flag_loss_rate"),
|
||||||
|
F.round(F.avg("won"), 3).as("win_rate"),
|
||||||
|
)
|
||||||
|
.orderBy(F.asc("band_order"))
|
||||||
|
.drop("band_order")
|
||||||
|
|
||||||
|
write(stats, outputDir, "clock_by_rating", jdbcUrl, dbUser, dbPass, "analytics_clock_by_rating")
|
||||||
|
|
||||||
|
private def writeScrambleOutcome(
|
||||||
|
perSide: org.apache.spark.sql.DataFrame,
|
||||||
|
jdbcUrl: String,
|
||||||
|
dbUser: String,
|
||||||
|
dbPass: String,
|
||||||
|
outputDir: String,
|
||||||
|
): Unit =
|
||||||
|
val sf = F.col("scramble_fraction")
|
||||||
|
val bucket = F
|
||||||
|
.when(sf === 0, "none")
|
||||||
|
.when(sf < 0.05, "<5%")
|
||||||
|
.when(sf < 0.20, "5–20%")
|
||||||
|
.otherwise(">20%")
|
||||||
|
val order = F
|
||||||
|
.when(sf === 0, 0)
|
||||||
|
.when(sf < 0.05, 1)
|
||||||
|
.when(sf < 0.20, 2)
|
||||||
|
.otherwise(3)
|
||||||
|
|
||||||
|
val stats = perSide
|
||||||
|
.withColumn("scramble_bucket", bucket)
|
||||||
|
.withColumn("bucket_order", order)
|
||||||
|
.groupBy("scramble_bucket", "bucket_order")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("player_games"),
|
||||||
|
F.round(F.avg("won"), 3).as("win_rate"),
|
||||||
|
F.round(F.avg("flag_loss"), 3).as("flag_loss_rate"),
|
||||||
|
)
|
||||||
|
.orderBy(F.asc("bucket_order"))
|
||||||
|
.drop("bucket_order")
|
||||||
|
|
||||||
|
write(stats, outputDir, "scramble_outcome", jdbcUrl, dbUser, dbPass, "analytics_scramble_outcome")
|
||||||
|
|
||||||
|
private def write(
|
||||||
|
df: org.apache.spark.sql.DataFrame,
|
||||||
|
outputDir: String,
|
||||||
|
name: String,
|
||||||
|
jdbcUrl: String,
|
||||||
|
dbUser: String,
|
||||||
|
dbPass: String,
|
||||||
|
table: String,
|
||||||
|
): Unit =
|
||||||
|
df.write.mode("overwrite").parquet(s"$outputDir/$name")
|
||||||
|
df.write.mode("overwrite").option("header", "true").csv(s"$outputDir/${name}_csv")
|
||||||
|
if !GameSource.isPgnMode then
|
||||||
|
df.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", table)
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.Row
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
import org.apache.spark.sql.types.DataTypes
|
||||||
|
import org.apache.spark.sql.types.StructField
|
||||||
|
import org.apache.spark.sql.types.StructType
|
||||||
|
|
||||||
|
import scala.jdk.CollectionConverters.*
|
||||||
|
|
||||||
|
object ColorAdvantageJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-color-advantage"
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Color Advantage")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String, outputDir: String): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.load(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.select("result")
|
||||||
|
.filter(F.col("result").isNotNull)
|
||||||
|
|
||||||
|
val totalGames = games.count()
|
||||||
|
val whiteWins = games.filter(F.col("result") === "white").count()
|
||||||
|
val blackWins = games.filter(F.col("result") === "black").count()
|
||||||
|
val draws = games.filter(F.col("result") === "draw").count()
|
||||||
|
|
||||||
|
val schema = StructType(
|
||||||
|
Seq(
|
||||||
|
StructField("color", DataTypes.StringType, false),
|
||||||
|
StructField("total_games", DataTypes.LongType, false),
|
||||||
|
StructField("wins", DataTypes.LongType, false),
|
||||||
|
StructField("losses", DataTypes.LongType, false),
|
||||||
|
StructField("draws", DataTypes.LongType, false),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val rows = List(
|
||||||
|
Row("white", totalGames, whiteWins, blackWins, draws),
|
||||||
|
Row("black", totalGames, blackWins, whiteWins, draws),
|
||||||
|
)
|
||||||
|
|
||||||
|
val stats = spark
|
||||||
|
.createDataFrame(rows.asJava, schema)
|
||||||
|
.withColumn("win_rate", F.round(F.col("wins") / F.col("total_games").cast("double"), 3))
|
||||||
|
.orderBy(F.asc("color"))
|
||||||
|
|
||||||
|
stats.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.option("header", "true")
|
||||||
|
.csv(s"$outputDir/color_advantage")
|
||||||
|
|
||||||
|
stats.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_color_advantage")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
|
||||||
|
object DailyActivityJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-daily-activity"
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Daily Activity")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String, outputDir: String): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.loadExtended(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.select("result", "utc_date", "utc_time")
|
||||||
|
.filter(F.col("utc_time").isNotNull.and(F.col("utc_date").isNotNull))
|
||||||
|
|
||||||
|
val hourOfDay = F.regexp_extract(F.col("utc_time"), "^(\\d{2})", 1).cast("int")
|
||||||
|
val dow = F.dayofweek(F.to_date(F.col("utc_date"), "yyyy.MM.dd"))
|
||||||
|
|
||||||
|
val tagged = games
|
||||||
|
.withColumn("hour_of_day", hourOfDay)
|
||||||
|
.withColumn("dow", dow)
|
||||||
|
|
||||||
|
val hourly = tagged
|
||||||
|
.groupBy("hour_of_day")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("total_games"),
|
||||||
|
F.sum(F.when(F.col("result") === "white", 1).otherwise(0)).as("white_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "black", 1).otherwise(0)).as("black_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "draw", 1).otherwise(0)).as("draws"),
|
||||||
|
)
|
||||||
|
.withColumn("white_win_rate", F.round(F.col("white_wins") / F.col("total_games").cast("double"), 3))
|
||||||
|
.orderBy(F.asc("hour_of_day"))
|
||||||
|
.select("hour_of_day", "total_games", "white_wins", "black_wins", "draws", "white_win_rate")
|
||||||
|
|
||||||
|
hourly.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.option("header", "true")
|
||||||
|
.csv(s"$outputDir/hourly_activity")
|
||||||
|
|
||||||
|
hourly.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_hourly_activity")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
|
|
||||||
|
val dayName = F
|
||||||
|
.when(F.col("dow") === 1, "Sunday")
|
||||||
|
.when(F.col("dow") === 2, "Monday")
|
||||||
|
.when(F.col("dow") === 3, "Tuesday")
|
||||||
|
.when(F.col("dow") === 4, "Wednesday")
|
||||||
|
.when(F.col("dow") === 5, "Thursday")
|
||||||
|
.when(F.col("dow") === 6, "Friday")
|
||||||
|
.otherwise("Saturday")
|
||||||
|
|
||||||
|
val weekly = tagged
|
||||||
|
.withColumn("day_of_week", dayName)
|
||||||
|
.withColumn("day_order", F.col("dow"))
|
||||||
|
.groupBy("day_of_week", "day_order")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("total_games"),
|
||||||
|
F.sum(F.when(F.col("result") === "white", 1).otherwise(0)).as("white_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "black", 1).otherwise(0)).as("black_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "draw", 1).otherwise(0)).as("draws"),
|
||||||
|
)
|
||||||
|
.withColumn("white_win_rate", F.round(F.col("white_wins") / F.col("total_games").cast("double"), 3))
|
||||||
|
.orderBy(F.asc("day_order"))
|
||||||
|
.drop("day_order")
|
||||||
|
.select("day_of_week", "total_games", "white_wins", "black_wins", "draws", "white_win_rate")
|
||||||
|
|
||||||
|
weekly.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.option("header", "true")
|
||||||
|
.csv(s"$outputDir/weekly_activity")
|
||||||
|
|
||||||
|
weekly.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_weekly_activity")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
|
||||||
|
object EloDistributionJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-elo-distribution"
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Elo Distribution")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String, outputDir: String): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.loadExtended(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.filter(F.col("white_elo").isNotNull)
|
||||||
|
|
||||||
|
val whiteElo = games.select(F.col("white_elo").as("elo"))
|
||||||
|
val blackElo = games.select(F.col("black_elo").as("elo"))
|
||||||
|
val allElo = whiteElo.union(blackElo).filter(F.col("elo").isNotNull)
|
||||||
|
|
||||||
|
val bucketMin = (F.floor(F.col("elo") / 200) * 200).cast("int")
|
||||||
|
val bucketLabel = F.when(
|
||||||
|
F.col("elo") >= 2800,
|
||||||
|
F.lit("2800+"),
|
||||||
|
).otherwise(F.concat(bucketMin.cast("string"), F.lit("-"), (bucketMin + 199).cast("string")))
|
||||||
|
|
||||||
|
val distribution = allElo
|
||||||
|
.withColumn("elo_bucket", bucketLabel)
|
||||||
|
.withColumn("bucket_order", F.when(F.col("elo") >= 2800, 2800).otherwise(bucketMin))
|
||||||
|
.groupBy("elo_bucket", "bucket_order")
|
||||||
|
.agg(F.count("*").as("player_count"))
|
||||||
|
.orderBy(F.asc("bucket_order"))
|
||||||
|
.select("elo_bucket", "player_count")
|
||||||
|
|
||||||
|
distribution.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.option("header", "true")
|
||||||
|
.csv(s"$outputDir/elo_distribution")
|
||||||
|
|
||||||
|
distribution.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_elo_distribution")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
|
||||||
|
object GameLengthJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-game-length"
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Game Length")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String, outputDir: String): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.load(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.select("result", "move_count")
|
||||||
|
.filter(F.col("result").isNotNull.and(F.col("move_count").isNotNull))
|
||||||
|
|
||||||
|
val moves = F.col("move_count")
|
||||||
|
val bucket = F
|
||||||
|
.when(moves <= 10, "1-10")
|
||||||
|
.when(moves <= 20, "11-20")
|
||||||
|
.when(moves <= 30, "21-30")
|
||||||
|
.when(moves <= 40, "31-40")
|
||||||
|
.when(moves <= 60, "41-60")
|
||||||
|
.when(moves <= 100, "61-100")
|
||||||
|
.otherwise("101+")
|
||||||
|
val bucketOrder = F
|
||||||
|
.when(moves <= 10, 1)
|
||||||
|
.when(moves <= 20, 2)
|
||||||
|
.when(moves <= 30, 3)
|
||||||
|
.when(moves <= 40, 4)
|
||||||
|
.when(moves <= 60, 5)
|
||||||
|
.when(moves <= 100, 6)
|
||||||
|
.otherwise(7)
|
||||||
|
|
||||||
|
val tagged = games
|
||||||
|
.withColumn("move_bucket", bucket)
|
||||||
|
.withColumn("bucket_order", bucketOrder)
|
||||||
|
|
||||||
|
val distribution = tagged
|
||||||
|
.groupBy("move_bucket", "bucket_order")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("total_games"),
|
||||||
|
F.sum(F.when(F.col("result") === "white", 1).otherwise(0)).as("white_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "black", 1).otherwise(0)).as("black_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "draw", 1).otherwise(0)).as("draws"),
|
||||||
|
)
|
||||||
|
.withColumn("white_win_rate", F.round(F.col("white_wins") / F.col("total_games").cast("double"), 3))
|
||||||
|
.withColumn("black_win_rate", F.round(F.col("black_wins") / F.col("total_games").cast("double"), 3))
|
||||||
|
.withColumn("draw_rate", F.round(F.col("draws") / F.col("total_games").cast("double"), 3))
|
||||||
|
.orderBy(F.asc("bucket_order"))
|
||||||
|
.drop("bucket_order")
|
||||||
|
.select(
|
||||||
|
"move_bucket",
|
||||||
|
"total_games",
|
||||||
|
"white_wins",
|
||||||
|
"black_wins",
|
||||||
|
"draws",
|
||||||
|
"white_win_rate",
|
||||||
|
"black_win_rate",
|
||||||
|
"draw_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
distribution.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.option("header", "true")
|
||||||
|
.csv(s"$outputDir/game_length_distribution")
|
||||||
|
|
||||||
|
distribution.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_game_length_distribution")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
|
|
||||||
|
val byResult = games
|
||||||
|
.groupBy("result")
|
||||||
|
.agg(
|
||||||
|
F.round(F.avg("move_count"), 1).as("avg_move_count"),
|
||||||
|
F.min("move_count").as("min_moves"),
|
||||||
|
F.max("move_count").as("max_moves"),
|
||||||
|
)
|
||||||
|
.orderBy(F.asc("result"))
|
||||||
|
|
||||||
|
byResult.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.option("header", "true")
|
||||||
|
.csv(s"$outputDir/game_length_by_result")
|
||||||
|
|
||||||
|
byResult.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_game_length_by_result")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -9,17 +9,17 @@ import org.apache.spark.sql.functions as F
|
|||||||
*
|
*
|
||||||
* Every batch job consumes the same five-column shape:
|
* Every batch job consumes the same five-column shape:
|
||||||
* - white_id, black_id : player identifiers
|
* - white_id, black_id : player identifiers
|
||||||
* - result : one of "white", "black", "draw"
|
* - result : one of "white", "black", "draw"
|
||||||
* - move_count : number of plies
|
* - move_count : number of plies
|
||||||
* - pgn : full PGN ("[Event …]…\n\n1. e4 …"), header and movetext separated by a blank line
|
* - pgn : full PGN ("[Event …]…\n\n1. e4 …"), header and movetext separated by a blank line
|
||||||
*
|
*
|
||||||
* Two backends, selected by the `NOWCHESS_PGN_PATH` environment variable:
|
* Two backends, selected by the `NOWCHESS_PGN_PATH` environment variable:
|
||||||
* - unset → PostgreSQL `game_records` table (production)
|
* - unset → PostgreSQL `game_records` table (production)
|
||||||
* - set → a Lichess PGN dump file/URL (demo). Point it at a `lichess_db_standard_rated_*.pgn[.zst]`
|
* - set → a Lichess PGN dump file/URL (demo). Point it at a `lichess_db_standard_rated_*.pgn[.zst]` to drive every
|
||||||
* to drive every batch job from real Lichess games.
|
* batch job from real Lichess games.
|
||||||
*
|
*
|
||||||
* Lichess parsing uses only Spark SQL string functions — no UDFs — so Catalyst can push predicates,
|
* Lichess parsing uses only Spark SQL string functions — no UDFs — so Catalyst can push predicates, matching the
|
||||||
* matching the no-UDF approach already used in OpeningBookJob.
|
* no-UDF approach already used in OpeningBookJob.
|
||||||
*/
|
*/
|
||||||
object GameSource:
|
object GameSource:
|
||||||
|
|
||||||
@@ -33,6 +33,19 @@ object GameSource:
|
|||||||
case Some(path) => fromLichessPgn(spark, path)
|
case Some(path) => fromLichessPgn(spark, path)
|
||||||
case None => fromJdbc(spark, jdbcUrl, dbUser, dbPass)
|
case None => fromJdbc(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
|
||||||
|
def loadExtended(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String): DataFrame =
|
||||||
|
sys.env.get(PgnPathEnv) match
|
||||||
|
case Some(path) => fromLichessPgnExtended(spark, path)
|
||||||
|
case None =>
|
||||||
|
fromJdbc(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.withColumn("white_elo", F.lit(null).cast("int"))
|
||||||
|
.withColumn("black_elo", F.lit(null).cast("int"))
|
||||||
|
.withColumn("time_control", F.lit(null).cast("string"))
|
||||||
|
.withColumn("utc_date", F.lit(null).cast("string"))
|
||||||
|
.withColumn("utc_time", F.lit(null).cast("string"))
|
||||||
|
.withColumn("termination", F.lit(null).cast("string"))
|
||||||
|
.withColumn("eco", F.lit(null).cast("string"))
|
||||||
|
|
||||||
def fromJdbc(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String): DataFrame =
|
def fromJdbc(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String): DataFrame =
|
||||||
spark.read
|
spark.read
|
||||||
.format("jdbc")
|
.format("jdbc")
|
||||||
@@ -48,16 +61,16 @@ object GameSource:
|
|||||||
/** Parses a Lichess PGN dump into the normalised game shape.
|
/** Parses a Lichess PGN dump into the normalised game shape.
|
||||||
*
|
*
|
||||||
* `path` may be:
|
* `path` may be:
|
||||||
* - an http(s)/ftp URL — fetched once via SparkContext.addFile and distributed to executors, then read
|
* - an http(s)/ftp URL — fetched once via SparkContext.addFile and distributed to executors, then read from the
|
||||||
* from the local replica (no S3/PVC needed; handy for a staging demo)
|
* local replica (no S3/PVC needed; handy for a staging demo)
|
||||||
* - any Hadoop-readable path (file://, hdfs://, s3a://, …)
|
* - any Hadoop-readable path (file://, hdfs://, s3a://, …)
|
||||||
*
|
*
|
||||||
* `.zst` dumps (Lichess' native format) are decompressed in-process via zstd-jni; `.gz`/`.bz2` are
|
* `.zst` dumps (Lichess' native format) are decompressed in-process via zstd-jni; `.gz`/`.bz2` are handled by
|
||||||
* handled by Spark's text reader codecs.
|
* Spark's text reader codecs.
|
||||||
*
|
*
|
||||||
* Records are split on the "[Event " tag that opens every game, so each row holds one complete game
|
* Records are split on the "[Event " tag that opens every game, so each row holds one complete game (the empty
|
||||||
* (the empty fragment before the first game is filtered out). Header tags are read with regexp_extract;
|
* fragment before the first game is filtered out). Header tags are read with regexp_extract; the movetext (after the
|
||||||
* the movetext (after the blank line) is cleaned of clock/eval comments and move numbers to count plies.
|
* blank line) is cleaned of clock/eval comments and move numbers to count plies.
|
||||||
*/
|
*/
|
||||||
def fromLichessPgn(spark: SparkSession, path: String): DataFrame =
|
def fromLichessPgn(spark: SparkSession, path: String): DataFrame =
|
||||||
val resolved = resolvePath(spark, path)
|
val resolved = resolvePath(spark, path)
|
||||||
@@ -89,31 +102,85 @@ object GameSource:
|
|||||||
)
|
)
|
||||||
.filter((F.col("white_id") =!= "").and(F.col("black_id") =!= ""))
|
.filter((F.col("white_id") =!= "").and(F.col("black_id") =!= ""))
|
||||||
|
|
||||||
/** Turns an http(s)/ftp URL into a cluster-local path by fetching it once with SparkContext.addFile,
|
private def fromLichessPgnExtended(spark: SparkSession, path: String): DataFrame =
|
||||||
* which distributes the file to every executor. `.zst` is decompressed in-process and the plain `.pgn`
|
val resolved = resolvePath(spark, path)
|
||||||
* is redistributed. Non-URL paths are returned unchanged.
|
val record = F.col("value")
|
||||||
|
|
||||||
|
val resultTag = F.regexp_extract(record, "Result \"([^\"]*)\"", 1)
|
||||||
|
val result = F
|
||||||
|
.when(resultTag === "1-0", "white")
|
||||||
|
.when(resultTag === "0-1", "black")
|
||||||
|
.when(resultTag === "1/2-1/2", "draw")
|
||||||
|
.otherwise(F.lit(null).cast("string"))
|
||||||
|
|
||||||
|
val moveText = F.coalesce(F.split(record, "\n\n").getItem(1), F.lit(""))
|
||||||
|
val noComment = F.regexp_replace(moveText, "\\{[^}]*\\}", "")
|
||||||
|
val noResult = F.regexp_replace(noComment, "(1-0|0-1|1/2-1/2|\\*)", "")
|
||||||
|
val noNumbers = F.regexp_replace(noResult, "\\d+\\.+", " ")
|
||||||
|
val plies = F.size(F.filter(F.split(F.trim(noNumbers), "\\s+"), tok => F.length(tok) > 0))
|
||||||
|
|
||||||
|
def nullable(extracted: org.apache.spark.sql.Column): org.apache.spark.sql.Column =
|
||||||
|
F.when(F.length(extracted) > 0, extracted).otherwise(F.lit(null).cast("string"))
|
||||||
|
|
||||||
|
val whiteElo = nullable(F.regexp_extract(record, "WhiteElo \"([^\"]*)\"", 1)).cast("int")
|
||||||
|
val blackElo = nullable(F.regexp_extract(record, "BlackElo \"([^\"]*)\"", 1)).cast("int")
|
||||||
|
|
||||||
|
spark.read
|
||||||
|
.option("lineSep", "[Event ")
|
||||||
|
.text(resolved)
|
||||||
|
.filter(F.length(F.trim(record)) > 0)
|
||||||
|
.select(
|
||||||
|
F.regexp_extract(record, "White \"([^\"]*)\"", 1).as("white_id"),
|
||||||
|
F.regexp_extract(record, "Black \"([^\"]*)\"", 1).as("black_id"),
|
||||||
|
result.as("result"),
|
||||||
|
plies.as("move_count"),
|
||||||
|
F.concat(F.lit("[Event "), record).as("pgn"),
|
||||||
|
whiteElo.as("white_elo"),
|
||||||
|
blackElo.as("black_elo"),
|
||||||
|
nullable(F.regexp_extract(record, "TimeControl \"([^\"]*)\"", 1)).as("time_control"),
|
||||||
|
nullable(F.regexp_extract(record, "UTCDate \"([^\"]*)\"", 1)).as("utc_date"),
|
||||||
|
nullable(F.regexp_extract(record, "UTCTime \"([^\"]*)\"", 1)).as("utc_time"),
|
||||||
|
nullable(F.regexp_extract(record, "Termination \"([^\"]*)\"", 1)).as("termination"),
|
||||||
|
nullable(F.regexp_extract(record, "ECO \"([^\"]*)\"", 1)).as("eco"),
|
||||||
|
)
|
||||||
|
.filter((F.col("white_id") =!= "").and(F.col("black_id") =!= ""))
|
||||||
|
|
||||||
|
/** Turns an http(s)/ftp URL into a path readable by all executors.
|
||||||
|
*
|
||||||
|
* Downloads the file once on the driver, decompresses `.zst` if needed, then writes the result to
|
||||||
|
* `NOWCHESS_PGN_CACHE_DIR` (default `/tmp`). That directory must be on a filesystem shared between the driver pod
|
||||||
|
* and all executor pods — in the k8s deployment this is the `spark-analytics-output` PVC mounted at
|
||||||
|
* `/spark-output`, so set `NOWCHESS_PGN_CACHE_DIR=/spark-output/.pgn-cache`.
|
||||||
|
*
|
||||||
|
* Skips download if the destination file already exists (cache-friendly for repeated runs).
|
||||||
|
* Non-URL paths are returned unchanged.
|
||||||
*/
|
*/
|
||||||
private def resolvePath(spark: SparkSession, path: String): String =
|
private def resolvePath(spark: SparkSession, path: String): String =
|
||||||
if !path.matches("^(https?|ftp)://.*") then path
|
if !path.matches("^(https?|ftp)://.*") then path
|
||||||
else
|
else
|
||||||
spark.sparkContext.addFile(path)
|
val cacheDir = sys.env.getOrElse("NOWCHESS_PGN_CACHE_DIR", "/tmp")
|
||||||
val local = SparkFiles.get(baseName(path))
|
val destName = baseName(path).stripSuffix(".zst")
|
||||||
if !local.endsWith(".zst") then "file://" + local
|
val destPath = s"$cacheDir/$destName"
|
||||||
else distribute(spark, decompressZstd(local))
|
if !java.io.File(destPath).exists() then
|
||||||
|
spark.sparkContext.addFile(path)
|
||||||
|
val downloaded = SparkFiles.get(baseName(path))
|
||||||
|
if downloaded.endsWith(".zst") then decompressZstd(downloaded, destPath)
|
||||||
|
else
|
||||||
|
java.io.File(destPath).getParentFile.mkdirs()
|
||||||
|
java.nio.file.Files.copy(
|
||||||
|
java.nio.file.Paths.get(downloaded),
|
||||||
|
java.io.File(destPath).toPath,
|
||||||
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING,
|
||||||
|
)
|
||||||
|
"file://" + destPath
|
||||||
|
|
||||||
private def baseName(path: String): String = path.substring(path.lastIndexOf('/') + 1)
|
private def baseName(path: String): String = path.substring(path.lastIndexOf('/') + 1)
|
||||||
|
|
||||||
private def distribute(spark: SparkSession, localPath: String): String =
|
/** Decompresses a `.zst` file to `destPath` using zstd-jni (bundled with Spark at runtime). */
|
||||||
spark.sparkContext.addFile("file://" + localPath)
|
private def decompressZstd(srcPath: String, destPath: String): Unit =
|
||||||
"file://" + SparkFiles.get(baseName(localPath))
|
java.io.File(destPath).getParentFile.mkdirs()
|
||||||
|
|
||||||
/** Decompresses a `.zst` file to a temp `.pgn` using zstd-jni (bundled with Spark at runtime). */
|
|
||||||
private def decompressZstd(srcPath: String): String =
|
|
||||||
val out = java.io.File.createTempFile("lichess-", ".pgn")
|
|
||||||
out.deleteOnExit()
|
|
||||||
val in = com.github.luben.zstd.ZstdInputStream(
|
val in = com.github.luben.zstd.ZstdInputStream(
|
||||||
java.io.BufferedInputStream(java.io.FileInputStream(srcPath)),
|
java.io.BufferedInputStream(java.io.FileInputStream(srcPath)),
|
||||||
)
|
)
|
||||||
try java.nio.file.Files.copy(in, out.toPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING)
|
try java.nio.file.Files.copy(in, java.io.File(destPath).toPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING)
|
||||||
finally in.close()
|
finally in.close()
|
||||||
out.getAbsolutePath
|
|
||||||
|
|||||||
@@ -72,16 +72,15 @@ object OpeningBookJob:
|
|||||||
.option("header", "true")
|
.option("header", "true")
|
||||||
.csv(s"$outputDir/opening_book_top1000")
|
.csv(s"$outputDir/opening_book_top1000")
|
||||||
|
|
||||||
if !GameSource.isPgnMode then
|
top1000.write
|
||||||
top1000.write
|
.mode("overwrite")
|
||||||
.mode("overwrite")
|
.format("jdbc")
|
||||||
.format("jdbc")
|
.option("url", jdbcUrl)
|
||||||
.option("url", jdbcUrl)
|
.option("dbtable", "analytics_opening_stats")
|
||||||
.option("dbtable", "analytics_opening_stats")
|
.option("user", dbUser)
|
||||||
.option("user", dbUser)
|
.option("password", dbPass)
|
||||||
.option("password", dbPass)
|
.option("driver", "org.postgresql.Driver")
|
||||||
.option("driver", "org.postgresql.Driver")
|
.save()
|
||||||
.save()
|
|
||||||
|
|
||||||
/** Extracts the first `maxPlies` moves from a PGN column as a space-separated string.
|
/** Extracts the first `maxPlies` moves from a PGN column as a space-separated string.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -119,26 +119,25 @@ object PlayerClusteringJob:
|
|||||||
.option("header", "true")
|
.option("header", "true")
|
||||||
.csv(s"$outputDir/cluster_archetypes")
|
.csv(s"$outputDir/cluster_archetypes")
|
||||||
|
|
||||||
if !GameSource.isPgnMode then
|
clustersDf.write
|
||||||
clustersDf.write
|
.mode("overwrite")
|
||||||
.mode("overwrite")
|
.format("jdbc")
|
||||||
.format("jdbc")
|
.option("url", jdbcUrl)
|
||||||
.option("url", jdbcUrl)
|
.option("dbtable", "analytics_player_clusters")
|
||||||
.option("dbtable", "analytics_player_clusters")
|
.option("user", dbUser)
|
||||||
.option("user", dbUser)
|
.option("password", dbPass)
|
||||||
.option("password", dbPass)
|
.option("driver", "org.postgresql.Driver")
|
||||||
.option("driver", "org.postgresql.Driver")
|
.save()
|
||||||
.save()
|
|
||||||
|
|
||||||
archetypes.write
|
archetypes.write
|
||||||
.mode("overwrite")
|
.mode("overwrite")
|
||||||
.format("jdbc")
|
.format("jdbc")
|
||||||
.option("url", jdbcUrl)
|
.option("url", jdbcUrl)
|
||||||
.option("dbtable", "analytics_cluster_archetypes")
|
.option("dbtable", "analytics_cluster_archetypes")
|
||||||
.option("user", dbUser)
|
.option("user", dbUser)
|
||||||
.option("password", dbPass)
|
.option("password", dbPass)
|
||||||
.option("driver", "org.postgresql.Driver")
|
.option("driver", "org.postgresql.Driver")
|
||||||
.save()
|
.save()
|
||||||
|
|
||||||
private def buildPlayerStats(games: org.apache.spark.sql.DataFrame): org.apache.spark.sql.DataFrame =
|
private def buildPlayerStats(games: org.apache.spark.sql.DataFrame): org.apache.spark.sql.DataFrame =
|
||||||
val asWhite = games.select(
|
val asWhite = games.select(
|
||||||
|
|||||||
@@ -109,16 +109,15 @@ object PlayerGraphJob:
|
|||||||
.mode("overwrite")
|
.mode("overwrite")
|
||||||
.parquet(s"$outputDir/player_graph")
|
.parquet(s"$outputDir/player_graph")
|
||||||
|
|
||||||
if !GameSource.isPgnMode then
|
result.write
|
||||||
result.write
|
.mode("overwrite")
|
||||||
.mode("overwrite")
|
.format("jdbc")
|
||||||
.format("jdbc")
|
.option("url", jdbcUrl)
|
||||||
.option("url", jdbcUrl)
|
.option("dbtable", "analytics_player_graph")
|
||||||
.option("dbtable", "analytics_player_graph")
|
.option("user", dbUser)
|
||||||
.option("user", dbUser)
|
.option("password", dbPass)
|
||||||
.option("password", dbPass)
|
.option("driver", "org.postgresql.Driver")
|
||||||
.option("driver", "org.postgresql.Driver")
|
.save()
|
||||||
.save()
|
|
||||||
|
|
||||||
// How many players belong to each connected component?
|
// How many players belong to each connected component?
|
||||||
// A large dominant component + many singletons is the expected shape.
|
// A large dominant component + many singletons is the expected shape.
|
||||||
@@ -135,6 +134,16 @@ object PlayerGraphJob:
|
|||||||
.option("header", "true")
|
.option("header", "true")
|
||||||
.csv(s"$outputDir/component_sizes")
|
.csv(s"$outputDir/component_sizes")
|
||||||
|
|
||||||
|
componentSizes.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_component_sizes")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
|
|
||||||
// Build a two-column DataFrame (vertex_id: Long, valueCol: valueType) from an RDD.
|
// Build a two-column DataFrame (vertex_id: Long, valueCol: valueType) from an RDD.
|
||||||
// Used to bridge GraphX RDD results into the DataFrame API without implicits.
|
// Used to bridge GraphX RDD results into the DataFrame API without implicits.
|
||||||
private def rddToFrame[T](
|
private def rddToFrame[T](
|
||||||
|
|||||||
@@ -77,13 +77,17 @@ object PlayerStatsJob:
|
|||||||
.mode("overwrite")
|
.mode("overwrite")
|
||||||
.parquet(s"$outputDir/player_stats")
|
.parquet(s"$outputDir/player_stats")
|
||||||
|
|
||||||
if !GameSource.isPgnMode then
|
stats.write
|
||||||
stats.write
|
.mode("overwrite")
|
||||||
.mode("overwrite")
|
.option("header", "true")
|
||||||
.format("jdbc")
|
.csv(s"$outputDir/player_stats_csv")
|
||||||
.option("url", jdbcUrl)
|
|
||||||
.option("dbtable", "analytics_player_stats")
|
stats.write
|
||||||
.option("user", dbUser)
|
.mode("overwrite")
|
||||||
.option("password", dbPass)
|
.format("jdbc")
|
||||||
.option("driver", "org.postgresql.Driver")
|
.option("url", jdbcUrl)
|
||||||
.save()
|
.option("dbtable", "analytics_player_stats")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
|
||||||
|
object RatingMismatchJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-rating-mismatch"
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Rating Mismatch")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String, outputDir: String): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.loadExtended(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.select("result", "white_elo", "black_elo")
|
||||||
|
.filter(F.col("white_elo").isNotNull.and(F.col("black_elo").isNotNull))
|
||||||
|
|
||||||
|
val eloDiff = F.col("white_elo") - F.col("black_elo")
|
||||||
|
val bracket = F
|
||||||
|
.when(eloDiff < -200, "Black +200")
|
||||||
|
.when(eloDiff < -100, "Black +100–200")
|
||||||
|
.when(eloDiff < -50, "Black +50–100")
|
||||||
|
.when(eloDiff <= 50, "Even (±50)")
|
||||||
|
.when(eloDiff <= 100, "White +50–100")
|
||||||
|
.when(eloDiff <= 200, "White +100–200")
|
||||||
|
.otherwise("White +200")
|
||||||
|
val bracketOrder = F
|
||||||
|
.when(eloDiff < -200, 1)
|
||||||
|
.when(eloDiff < -100, 2)
|
||||||
|
.when(eloDiff < -50, 3)
|
||||||
|
.when(eloDiff <= 50, 4)
|
||||||
|
.when(eloDiff <= 100, 5)
|
||||||
|
.when(eloDiff <= 200, 6)
|
||||||
|
.otherwise(7)
|
||||||
|
|
||||||
|
val stats = games
|
||||||
|
.withColumn("elo_diff", eloDiff)
|
||||||
|
.withColumn("bracket", bracket)
|
||||||
|
.withColumn("bracket_order", bracketOrder)
|
||||||
|
.groupBy("bracket", "bracket_order")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("total_games"),
|
||||||
|
F.sum(F.when(F.col("result") === "white", 1).otherwise(0)).as("white_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "black", 1).otherwise(0)).as("black_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "draw", 1).otherwise(0)).as("draws"),
|
||||||
|
)
|
||||||
|
.withColumn("white_win_rate", F.round(F.col("white_wins") / F.col("total_games").cast("double"), 3))
|
||||||
|
.orderBy(F.asc("bracket_order"))
|
||||||
|
.drop("bracket_order")
|
||||||
|
.select("bracket", "total_games", "white_wins", "black_wins", "draws", "white_win_rate")
|
||||||
|
|
||||||
|
stats.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.option("header", "true")
|
||||||
|
.csv(s"$outputDir/rating_mismatch")
|
||||||
|
|
||||||
|
stats.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_rating_mismatch")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.expressions.Window
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
|
||||||
|
/** Smurf / sandbagging anomaly detection via population z-scores.
|
||||||
|
*
|
||||||
|
* Smurfs (strong players on fresh accounts) and sandbaggers leave a statistical signature: a win-rate, an upset-rate
|
||||||
|
* (beating higher-rated opponents) and a self-Elo climb that sit far above the population norm. This job builds those
|
||||||
|
* three features per player, standardises each against the whole player base, and flags the players whose combined
|
||||||
|
* deviation is extreme.
|
||||||
|
*
|
||||||
|
* Features per player (from each game's own/opponent Elo):
|
||||||
|
* - win_rate — fraction of decisive results won
|
||||||
|
* - upset_rate — wins vs higher-rated opponents / games vs higher-rated opponents
|
||||||
|
* - elo_climb — max self-Elo − min self-Elo across their games (rapid rating gain)
|
||||||
|
*
|
||||||
|
* Standardisation uses a single unbounded window (`Window.partitionBy()`), i.e. mean/stddev over every qualifying
|
||||||
|
* player, so z = (x − μ) / σ. The composite anomaly score sums the three z-scores. No UDFs — pure SQL aggregates +
|
||||||
|
* window functions, so Catalyst plans the whole job.
|
||||||
|
*
|
||||||
|
* Outputs (Parquet + CSV + JDBC):
|
||||||
|
* - `anomaly_scores` — every qualifying player with features, z-scores and composite, ranked most-anomalous first.
|
||||||
|
* - `flagged_smurfs` — the suspicious subset (high composite, or the classic high-winrate / few-games / steep-climb
|
||||||
|
* profile).
|
||||||
|
*
|
||||||
|
* Meaningful only when Elo is present (Lichess dump); requires `minGames` (arg 1, default 15) to avoid small-sample
|
||||||
|
* noise.
|
||||||
|
*/
|
||||||
|
object SmurfAnomalyJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-smurf-anomaly"
|
||||||
|
val minGames = if args.length > 1 then args(1).toInt else 15
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Smurf Anomaly Detection")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir, minGames)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(
|
||||||
|
spark: SparkSession,
|
||||||
|
jdbcUrl: String,
|
||||||
|
dbUser: String,
|
||||||
|
dbPass: String,
|
||||||
|
outputDir: String,
|
||||||
|
minGames: Int,
|
||||||
|
): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.loadExtended(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.select("white_id", "black_id", "result", "white_elo", "black_elo")
|
||||||
|
.filter(F.col("result").isNotNull)
|
||||||
|
|
||||||
|
val asWhite = games.select(
|
||||||
|
F.col("white_id").as("player_id"),
|
||||||
|
F.col("white_elo").as("self_elo"),
|
||||||
|
F.col("black_elo").as("opp_elo"),
|
||||||
|
F.when(F.col("result") === "white", 1).otherwise(0).as("won"),
|
||||||
|
)
|
||||||
|
val asBlack = games.select(
|
||||||
|
F.col("black_id").as("player_id"),
|
||||||
|
F.col("black_elo").as("self_elo"),
|
||||||
|
F.col("white_elo").as("opp_elo"),
|
||||||
|
F.when(F.col("result") === "black", 1).otherwise(0).as("won"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val playerGames = asWhite
|
||||||
|
.union(asBlack)
|
||||||
|
.filter(F.col("self_elo").isNotNull.and(F.col("opp_elo").isNotNull))
|
||||||
|
|
||||||
|
val higher = F.col("opp_elo") > F.col("self_elo")
|
||||||
|
|
||||||
|
val features = playerGames
|
||||||
|
.groupBy("player_id")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("total_games"),
|
||||||
|
F.round(F.avg("won"), 3).as("win_rate"),
|
||||||
|
F.round(F.avg("self_elo"), 0).as("avg_self_elo"),
|
||||||
|
(F.max("self_elo") - F.min("self_elo")).as("elo_climb"),
|
||||||
|
F.sum(F.when(higher, 1).otherwise(0)).as("vs_higher"),
|
||||||
|
F.sum(F.when(higher && F.col("won") === 1, 1).otherwise(0)).as("upsets"),
|
||||||
|
)
|
||||||
|
.filter(F.col("total_games") >= minGames)
|
||||||
|
.withColumn("upset_rate", F.round(F.col("upsets") / F.greatest(F.col("vs_higher"), F.lit(1)), 3))
|
||||||
|
|
||||||
|
val all = Window.partitionBy()
|
||||||
|
def z(col: String): org.apache.spark.sql.Column =
|
||||||
|
val mean = F.avg(col).over(all)
|
||||||
|
val std = F.stddev(col).over(all)
|
||||||
|
F.round((F.col(col) - mean) / F.when(std === 0 || std.isNull, F.lit(1.0)).otherwise(std), 2)
|
||||||
|
|
||||||
|
val scored = features
|
||||||
|
.withColumn("z_win_rate", z("win_rate"))
|
||||||
|
.withColumn("z_upset_rate", z("upset_rate"))
|
||||||
|
.withColumn("z_elo_climb", z("elo_climb"))
|
||||||
|
.withColumn(
|
||||||
|
"anomaly_score",
|
||||||
|
F.round(F.col("z_win_rate") + F.col("z_upset_rate") + F.col("z_elo_climb"), 2),
|
||||||
|
)
|
||||||
|
.withColumn(
|
||||||
|
"flagged",
|
||||||
|
(F.col("anomaly_score") >= 4.0)
|
||||||
|
.or(F.col("win_rate") >= 0.8 && F.col("total_games") < 50 && F.col("elo_climb") >= 300),
|
||||||
|
)
|
||||||
|
|
||||||
|
val ordered = scored
|
||||||
|
.select(
|
||||||
|
"player_id",
|
||||||
|
"total_games",
|
||||||
|
"win_rate",
|
||||||
|
"avg_self_elo",
|
||||||
|
"elo_climb",
|
||||||
|
"upset_rate",
|
||||||
|
"z_win_rate",
|
||||||
|
"z_upset_rate",
|
||||||
|
"z_elo_climb",
|
||||||
|
"anomaly_score",
|
||||||
|
"flagged",
|
||||||
|
)
|
||||||
|
.orderBy(F.desc("anomaly_score"))
|
||||||
|
|
||||||
|
write(ordered, outputDir, "anomaly_scores", jdbcUrl, dbUser, dbPass, "analytics_smurf_anomaly")
|
||||||
|
|
||||||
|
val flagged = ordered.filter(F.col("flagged") === true)
|
||||||
|
write(flagged, outputDir, "flagged_smurfs", jdbcUrl, dbUser, dbPass, "analytics_flagged_smurfs")
|
||||||
|
|
||||||
|
private def write(
|
||||||
|
df: org.apache.spark.sql.DataFrame,
|
||||||
|
outputDir: String,
|
||||||
|
name: String,
|
||||||
|
jdbcUrl: String,
|
||||||
|
dbUser: String,
|
||||||
|
dbPass: String,
|
||||||
|
table: String,
|
||||||
|
): Unit =
|
||||||
|
df.write.mode("overwrite").parquet(s"$outputDir/$name")
|
||||||
|
df.write.mode("overwrite").option("header", "true").csv(s"$outputDir/${name}_csv")
|
||||||
|
if !GameSource.isPgnMode then
|
||||||
|
df.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", table)
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
|
||||||
|
object TerminationStatsJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-termination-stats"
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Termination Stats")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String, outputDir: String): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.loadExtended(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.select("result", "termination")
|
||||||
|
.filter(F.col("termination").isNotNull.and(F.col("termination") =!= ""))
|
||||||
|
|
||||||
|
val stats = games
|
||||||
|
.groupBy("termination")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("total_games"),
|
||||||
|
F.sum(F.when(F.col("result") === "white", 1).otherwise(0)).as("white_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "black", 1).otherwise(0)).as("black_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "draw", 1).otherwise(0)).as("draws"),
|
||||||
|
)
|
||||||
|
.withColumn("draw_rate", F.round(F.col("draws") / F.col("total_games").cast("double"), 3))
|
||||||
|
.withColumnRenamed("termination", "termination_type")
|
||||||
|
.orderBy(F.desc("total_games"))
|
||||||
|
.select("termination_type", "total_games", "white_wins", "black_wins", "draws", "draw_rate")
|
||||||
|
|
||||||
|
stats.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.option("header", "true")
|
||||||
|
.csv(s"$outputDir/termination_stats")
|
||||||
|
|
||||||
|
stats.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_termination_stats")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package de.nowchess.analytics
|
||||||
|
|
||||||
|
import org.apache.spark.sql.SparkSession
|
||||||
|
import org.apache.spark.sql.functions as F
|
||||||
|
|
||||||
|
object TimeControlJob:
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val jdbcUrl = sys.env.getOrElse("NOWCHESS_JDBC_URL", "jdbc:postgresql://localhost:5432/nowchess")
|
||||||
|
val dbUser = sys.env.getOrElse("NOWCHESS_DB_USER", "nowchess")
|
||||||
|
val dbPass = sys.env.getOrElse("NOWCHESS_DB_PASS", "nowchess")
|
||||||
|
val outputDir = if args.length > 0 then args(0) else "/tmp/nowchess-time-control"
|
||||||
|
|
||||||
|
val spark = SparkSession
|
||||||
|
.builder()
|
||||||
|
.appName("NowChess Time Control")
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
run(spark, jdbcUrl, dbUser, dbPass, outputDir)
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
def run(spark: SparkSession, jdbcUrl: String, dbUser: String, dbPass: String, outputDir: String): Unit =
|
||||||
|
val games = GameSource
|
||||||
|
.loadExtended(spark, jdbcUrl, dbUser, dbPass)
|
||||||
|
.select("result", "time_control")
|
||||||
|
.filter(
|
||||||
|
F.col("time_control").isNotNull
|
||||||
|
.and(F.col("time_control") =!= "")
|
||||||
|
.and(F.col("time_control") =!= "-"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val baseSeconds = F.regexp_extract(F.col("time_control"), "^(?:\\d+/)?(\\d+)", 1).cast("int")
|
||||||
|
val category = F
|
||||||
|
.when(baseSeconds < 30, "UltraBullet")
|
||||||
|
.when(baseSeconds < 180, "Bullet")
|
||||||
|
.when(baseSeconds < 480, "Blitz")
|
||||||
|
.when(baseSeconds < 1500, "Rapid")
|
||||||
|
.when(baseSeconds < 86400, "Classical")
|
||||||
|
.otherwise("Correspondence")
|
||||||
|
|
||||||
|
val stats = games
|
||||||
|
.withColumn("category", category)
|
||||||
|
.groupBy("category")
|
||||||
|
.agg(
|
||||||
|
F.count("*").as("total_games"),
|
||||||
|
F.sum(F.when(F.col("result") === "white", 1).otherwise(0)).as("white_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "black", 1).otherwise(0)).as("black_wins"),
|
||||||
|
F.sum(F.when(F.col("result") === "draw", 1).otherwise(0)).as("draws"),
|
||||||
|
)
|
||||||
|
.withColumn("white_win_rate", F.round(F.col("white_wins") / F.col("total_games").cast("double"), 3))
|
||||||
|
.withColumn("draw_rate", F.round(F.col("draws") / F.col("total_games").cast("double"), 3))
|
||||||
|
.orderBy(F.desc("total_games"))
|
||||||
|
.select("category", "total_games", "white_wins", "black_wins", "draws", "white_win_rate", "draw_rate")
|
||||||
|
|
||||||
|
stats.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.option("header", "true")
|
||||||
|
.csv(s"$outputDir/time_control_stats")
|
||||||
|
|
||||||
|
stats.write
|
||||||
|
.mode("overwrite")
|
||||||
|
.format("jdbc")
|
||||||
|
.option("url", jdbcUrl)
|
||||||
|
.option("dbtable", "analytics_time_control_stats")
|
||||||
|
.option("user", dbUser)
|
||||||
|
.option("password", dbPass)
|
||||||
|
.option("driver", "org.postgresql.Driver")
|
||||||
|
.save()
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=3
|
MINOR=8
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -2182,3 +2182,76 @@
|
|||||||
|
|
||||||
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||||
* 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-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add authentication permissions for metrics endpoints in application.yml ([04edd4d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/04edd4d6fd8a63196c36f6d67992832febc9bebb))
|
||||||
|
* add CORS configuration and reorder JWT settings in application.yml ([a49f9be](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a49f9be146f04c14561c305d980846a92f8c12b2))
|
||||||
|
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
|
||||||
|
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||||
|
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
|
||||||
|
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||||
|
* **config:** add GameWritebackEventDto to reflection targets ([87f29a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87f29a720422f538ef70699533500e060337b8ea))
|
||||||
|
* **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))
|
||||||
|
* **core:** publish GameOver event to Redis Streams ([#64](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/64)) ([676e411](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/676e4110c0893917d8bc7f836db6a19c69c5e9a5))
|
||||||
|
* **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))
|
||||||
|
* **game:** add GET /{gameId}/fen-history endpoint ([fba324a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fba324a5b01c23f97e28621ad8b72265701e0f11))
|
||||||
|
* implement clock expiry scanning and handling for game records ([#53](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/53)) ([8f9eb12](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8f9eb12f663efabe4dc72b94394438652ad0ef02))
|
||||||
|
* 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))
|
||||||
|
* implement periodic scaling checks and enhance instance management in AutoScaler ([3f12f69](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f12f695f132b92f634d98df2c037292498b6e86))
|
||||||
|
* **logging:** add DEBUG/INFO/WARN logging across services (NCS-72) ([#41](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/41)) ([804a4bf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/804a4bf179e3dfb19e2be4390e7e543caf5237c6))
|
||||||
|
* 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)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
|
||||||
|
* NCS-40 Rework Draw System ([#34](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/34)) ([33e785d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e785d22af87724839b62ae91dfe74a05b398c3))
|
||||||
|
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
|
||||||
|
* NCS-53 changed IO to MicroService for easier scaling ([#37](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/37)) ([b5a2966](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b5a2966adafa9650f0f7d601bdeb8fdd13710327))
|
||||||
|
* 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-78 Add Traceability to the Applications ([#48](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/48)) ([c96a09b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c96a09bb5cee59fc23205bb63baa8b217a7e1b00))
|
||||||
|
* 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))
|
||||||
|
* 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))
|
||||||
|
* **redis:** implement game writeback stream processing with error handling and retries ([ae3ef76](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/ae3ef766e8b7596a09e466cd4fb386119f17ca5c))
|
||||||
|
* **reflection:** add native reflection configuration for tournament classes ([e318250](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e31825021c0fca7cbe7d9f85755646114c83cf0c))
|
||||||
|
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
|
||||||
|
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||||
|
* update application.yml with new API root paths and add Micrometer and OpenTelemetry dependencies ([72ce262](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/72ce262bc491f94297700e6002fb5d0812e2cc2a))
|
||||||
|
* 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))
|
||||||
|
* **auth:** change InternalAuthFilter to use @Singleton and add HTTP tests for secret validation ([c08d530](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c08d5303eb9e70d36c8eebf6a061ccb71e118fe5))
|
||||||
|
* **auth:** update InternalAuthFilter to use @ApplicationScoped and add index-dependency configuration ([6e0fd95](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6e0fd9523e001756ce7109e639ebb54be4fcdabf))
|
||||||
|
* **core:** add logs to trace subscribeGame call in createGame ([f5614c3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f5614c358255598ba1230e42a56b22934d79183c))
|
||||||
|
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
|
||||||
|
* **heartbeat:** inject ObjectMapper into InstanceHeartbeatService ([#42](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/42)) ([0c98151](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0c981517da1f94cd10ae396e47bde2b35d0b3ba0))
|
||||||
|
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
|
||||||
|
* Lints ([dc224ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dc224abe26acf5361c56956006e1cc51b75b0b7e))
|
||||||
|
* NCS-84 More Verbose Logging ([#51](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/51)) ([4ad92ab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4ad92ab23698267f8faa59c4e18388d4a0042cca))
|
||||||
|
* NCS-85 Database Writeback fails without Logs ([#52](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/52)) ([7323908](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/73239088d985f01aa6b1067ed9097a845e471d4f))
|
||||||
|
* **pgn:** add SAN disambiguation and check/checkmate suffixes [NCS-42] ([#56](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/56)) ([2579539](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2579539084152178f4482ddb7b84b7f1162f10da))
|
||||||
|
* **redis:** add max pool wait time and switch to ReactiveRedisDataSource for heartbeat updates ([33e5017](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/33e5017f51a998327b180f778f73964cc10c05d3))
|
||||||
|
* **redis:** enhance GameRedisSubscriberManager to use ReactiveRedisDataSource and improve subscription handling ([0eb752d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0eb752d4935377f75aab710b7f4eda4b29098e6a))
|
||||||
|
* **redis:** prevent concurrent Redis heartbeat refreshes using AtomicBoolean ([847b132](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/847b13202cb909d18ca3304c27ebe17ce2312b8e))
|
||||||
|
* **redis:** simplify refreshRedisHeartbeat logic and ensure proper error handling ([1813ea1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1813ea1d2d5d093f7925f87371b5e29820bf1136))
|
||||||
|
* **redis:** update Redis configuration with max pool size and waiting parameters ([5baf6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5baf6a7cdbea484fc49c02e2b5a1c3919b7fa2c4))
|
||||||
|
* remove unused HTTP root-path configurations from application.yml ([3ed3e59](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ed3e59ee456d54cd3d65ece4f36623e256b9736))
|
||||||
|
* resolve 6 coordinator bugs (cache eviction, rebalance race, pod matching, lookup inefficiency) ([5619c82](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5619c8223ad7091706909eda8c907a29d215fd30))
|
||||||
|
* update documentation to reflect new functions in CoordinatorGrpcServer and InstanceRegistry ([f7ce4df](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f7ce4df595cbdc2ef84122781f4851ff140c0f44))
|
||||||
|
* 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))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "feat: add authentication permissions for metrics endpoints in application.yml" ([a298417](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a298417b9e4d68dc73bbf40be63d9484536e9f83))
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
|||||||
@@ -343,10 +343,10 @@ class GameResource:
|
|||||||
@Path("/{gameId}/fen-history")
|
@Path("/{gameId}/fen-history")
|
||||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
def getFenHistory(@PathParam("gameId") gameId: String): Response =
|
def getFenHistory(@PathParam("gameId") gameId: String): Response =
|
||||||
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
||||||
val engine = entry.engine
|
val engine = entry.engine
|
||||||
val initial = engine.initialContext
|
val initial = engine.initialContext
|
||||||
val moves = engine.context.moves
|
val moves = engine.context.moves
|
||||||
val contexts = moves.scanLeft(initial)((ctx, move) => engine.ruleSet.applyMove(ctx)(move))
|
val contexts = moves.scanLeft(initial)((ctx, move) => engine.ruleSet.applyMove(ctx)(move))
|
||||||
val fens = contexts.map(ctx => ioClient.exportFen(ctx))
|
val fens = contexts.map(ctx => ioClient.exportFen(ctx))
|
||||||
ok(FenHistoryDto(fens))
|
ok(FenHistoryDto(fens))
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=52
|
MINOR=53
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -340,3 +340,865 @@
|
|||||||
### 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-21)
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
* 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:** 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))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
## (2026-06-21)
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
* 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:** 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))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
## (2026-06-21)
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
* **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))
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
|
||||||
|
### 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:** 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:** 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))
|
||||||
|
## (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:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **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:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **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:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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 games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||||
|
* **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:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **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:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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 games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||||
|
* **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:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||||
|
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **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:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||||
|
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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 games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||||
|
* **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:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||||
|
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **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:** activate opening book in expert bot (native-safe) ([260db25](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/260db25803ec55ce99e55782791eabdc190dfed4))
|
||||||
|
* **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:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||||
|
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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 games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||||
|
* **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:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||||
|
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **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:** activate opening book in expert bot (native-safe) ([260db25](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/260db25803ec55ce99e55782791eabdc190dfed4))
|
||||||
|
* **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:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||||
|
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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 games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||||
|
* **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:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||||
|
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||||
|
* **official-bots:** use ThreadLocalRandom in PolyglotBook for native image ([1b30c3b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1b30c3be393d25712c8743d3d9057207f8bbb67c))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
## (2026-06-24)
|
||||||
|
|
||||||
|
### 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:** activate opening book in expert bot (native-safe) ([260db25](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/260db25803ec55ce99e55782791eabdc190dfed4))
|
||||||
|
* **official-bots:** add Google Colab notebook for NNUE training (NCS-111) ([#81](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/81)) ([fa10852](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fa10852bc98451d4068ec6fb9e7a486b5e53ef5c))
|
||||||
|
* **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:** implement king-relative (HalfKP) encoding in NNUE (NCS-109) ([#80](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/80)) ([44f376f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/44f376f03221f086b898741436e13c93fd314dd1))
|
||||||
|
* **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))
|
||||||
|
* modified training pipeline ([9f9140c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9f9140cb585345cd244a1dfee1a06e51a5f7f7a8))
|
||||||
|
* **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:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||||
|
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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 games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||||
|
* **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:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||||
|
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||||
|
* **official-bots:** use ThreadLocalRandom in PolyglotBook for native image ([1b30c3b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1b30c3be393d25712c8743d3d9057207f8bbb67c))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
## (2026-06-24)
|
||||||
|
|
||||||
|
### 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-110:** feed NNUE root-move scores into search move ordering ([#83](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/83)) ([e4fee85](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e4fee8513430093d46957970618935e99591519f))
|
||||||
|
* 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:** activate opening book in expert bot (native-safe) ([260db25](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/260db25803ec55ce99e55782791eabdc190dfed4))
|
||||||
|
* **official-bots:** add Google Colab notebook for NNUE training (NCS-111) ([#81](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/81)) ([fa10852](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fa10852bc98451d4068ec6fb9e7a486b5e53ef5c))
|
||||||
|
* **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:** implement king-relative (HalfKP) encoding in NNUE (NCS-109) ([#80](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/80)) ([44f376f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/44f376f03221f086b898741436e13c93fd314dd1))
|
||||||
|
* **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))
|
||||||
|
* modified training pipeline ([9f9140c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9f9140cb585345cd244a1dfee1a06e51a5f7f7a8))
|
||||||
|
* **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:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||||
|
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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 games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||||
|
* **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:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||||
|
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||||
|
* **official-bots:** use ThreadLocalRandom in PolyglotBook for native image ([1b30c3b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1b30c3be393d25712c8743d3d9057207f8bbb67c))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
## (2026-06-24)
|
||||||
|
|
||||||
|
### 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-110:** feed NNUE root-move scores into search move ordering ([#83](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/83)) ([e4fee85](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e4fee8513430093d46957970618935e99591519f))
|
||||||
|
* 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:** activate opening book in expert bot (native-safe) ([260db25](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/260db25803ec55ce99e55782791eabdc190dfed4))
|
||||||
|
* **official-bots:** add Google Colab notebook for NNUE training (NCS-111) ([#81](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/81)) ([fa10852](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fa10852bc98451d4068ec6fb9e7a486b5e53ef5c))
|
||||||
|
* **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:** implement king-relative (HalfKP) encoding in NNUE (NCS-109) ([#80](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/80)) ([44f376f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/44f376f03221f086b898741436e13c93fd314dd1))
|
||||||
|
* **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))
|
||||||
|
* **official-bots:** standalone self-play + one-shot dataset builder for NNUE training ([1c80abd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1c80abdb8a45814d642d43c633cde81ce7374c4f))
|
||||||
|
* **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))
|
||||||
|
* modified training pipeline ([9f9140c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9f9140cb585345cd244a1dfee1a06e51a5f7f7a8))
|
||||||
|
* **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:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||||
|
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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 games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||||
|
* **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:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||||
|
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||||
|
* **official-bots:** use ThreadLocalRandom in PolyglotBook for native image ([1b30c3b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1b30c3be393d25712c8743d3d9057207f8bbb67c))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
## (2026-06-24)
|
||||||
|
|
||||||
|
### 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-110:** feed NNUE root-move scores into search move ordering ([#83](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/83)) ([e4fee85](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e4fee8513430093d46957970618935e99591519f))
|
||||||
|
* 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:** activate opening book in expert bot (native-safe) ([260db25](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/260db25803ec55ce99e55782791eabdc190dfed4))
|
||||||
|
* **official-bots:** add Google Colab notebook for NNUE training (NCS-111) ([#81](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/81)) ([fa10852](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fa10852bc98451d4068ec6fb9e7a486b5e53ef5c))
|
||||||
|
* **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:** implement king-relative (HalfKP) encoding in NNUE (NCS-109) ([#80](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/80)) ([44f376f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/44f376f03221f086b898741436e13c93fd314dd1))
|
||||||
|
* **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))
|
||||||
|
* **official-bots:** standalone self-play + one-shot dataset builder for NNUE training ([1c80abd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1c80abdb8a45814d642d43c633cde81ce7374c4f))
|
||||||
|
* **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))
|
||||||
|
* modified training pipeline ([9f9140c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9f9140cb585345cd244a1dfee1a06e51a5f7f7a8))
|
||||||
|
* **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:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||||
|
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||||
|
* **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 games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||||
|
* **official-bots:** play only own tournament games with correct color ([4651bb7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4651bb796f07a21bd013d9521b2dfe2e1078cebb))
|
||||||
|
* **official-bots:** prevent Colab OOM in NNUE training ([e2b4342](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e2b4342f602215b5e8de6fccafc4105525a1ddd1))
|
||||||
|
* **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:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||||
|
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||||
|
* **official-bots:** stream NNUE features as sparse indices to stop host OOM ([9d65662](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9d656624d85889f55746faa5704578e248f9b088))
|
||||||
|
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||||
|
* **official-bots:** use ThreadLocalRandom in PolyglotBook for native image ([1b30c3b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1b30c3be393d25712c8743d3d9057207f8bbb67c))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ tasks.withType<JavaCompile> {
|
|||||||
options.compilerArgs.add("-parameters")
|
options.compilerArgs.add("-parameters")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register<JavaExec>("selfPlay") {
|
||||||
|
group = "nnue"
|
||||||
|
description = "Run standalone NNUEBot self-play and write FENs for labeling."
|
||||||
|
mainClass.set("de.nowchess.bot.selfplay.SelfPlayMain")
|
||||||
|
classpath = sourceSets["main"].runtimeClasspath
|
||||||
|
args((project.findProperty("spArgs")?.toString() ?: "").split(" ").filter { it.isNotBlank() })
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
compileOnly("org.scala-lang:scala3-compiler_3") {
|
compileOnly("org.scala-lang:scala3-compiler_3") {
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Concept: NNUE Training Data — Quality, Scale, and Transfer to Colab
|
||||||
|
|
||||||
|
Local generation + labeling is **not** a constraint (Ryzen 9800X3D / RTX 5070 / 32 GB).
|
||||||
|
So the design splits cleanly:
|
||||||
|
|
||||||
|
- **Data plane = local box.** Generate, label, shard, publish. Cheap, fast, no limits.
|
||||||
|
- **Train plane = Colab.** Pull a dataset version, GPU-train, export `.nbai`.
|
||||||
|
|
||||||
|
Colab never runs Stockfish and never sees a browser upload. Three problems below:
|
||||||
|
**(1) good data, (2) growing it over time, (3) getting it there easily** — (3) is the priority.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Generating *good* training sets
|
||||||
|
|
||||||
|
### The current weak spot
|
||||||
|
|
||||||
|
`generate.py` plays **fully random games** (`random.choice(legal_moves)`). Random play
|
||||||
|
produces positions that never occur in real games — material chaos, nonsense pawn
|
||||||
|
structures. An NNUE trained on that learns to evaluate a distribution it will never
|
||||||
|
face. Fine as filler, wrong as the backbone.
|
||||||
|
|
||||||
|
### What a good NNUE dataset needs
|
||||||
|
|
||||||
|
1. **Realistic position distribution.** Positions should resemble what the bot actually
|
||||||
|
reaches in search — from real games and engine play, not coin-flip moves.
|
||||||
|
2. **Phase coverage.** Openings, middlegames, endgames all represented. Endgames are
|
||||||
|
under-sampled by random play and matter most for precise eval.
|
||||||
|
3. **Eval balance.** Real game data is dominated by near-equal positions. If 80% of
|
||||||
|
labels sit in `[-0.5, +0.5]`, the net learns "everything is roughly equal." Resample
|
||||||
|
to flatten the eval histogram (cap per-bucket counts).
|
||||||
|
4. **Accurate labels.** Deeper Stockfish = better target. Locally you can afford
|
||||||
|
depth 16–20. Or skip labeling entirely with the Lichess eval DB (below).
|
||||||
|
5. **Clean positions.** Dedup by FEN; drop terminal/checkmate/stalemate; the side to
|
||||||
|
move should not already be in check unless intended; tag the game phase.
|
||||||
|
|
||||||
|
### Recommended source mix (per dataset version)
|
||||||
|
|
||||||
|
| Source | Role | How | Weight |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Lichess eval DB** | Backbone | `lichess_importer.py` — millions of FENs **pre-labeled** by deep Stockfish, real human positions, correct sign convention | 50–70% |
|
||||||
|
| **Engine self-play** | Bot's own distribution | NNUEBot (or vs Stockfish) plays games; sample positions; label with local Stockfish | 20–40% |
|
||||||
|
| **Tactical puzzles** | Sharp/critical positions | `tactical_positions_extractor.py` (Lichess puzzle DB) | 5–15% |
|
||||||
|
| **Random play** | Cheap diversity filler | existing `generate.py`, capped low | ≤10% |
|
||||||
|
|
||||||
|
The backbone is real, pre-labeled data — so labeling cost is near zero and quality is
|
||||||
|
high. Self-play is the part that adapts data to *your* bot. Random play stays only as
|
||||||
|
a thin diversity sprinkle.
|
||||||
|
|
||||||
|
### Self-play flywheel (the quality engine over time)
|
||||||
|
|
||||||
|
The strongest lever: **net N generates the games that train net N+1.**
|
||||||
|
|
||||||
|
```
|
||||||
|
net_vN ──play self-play games──► sample positions ──label (Stockfish)──►
|
||||||
|
▲ │
|
||||||
|
└──────────────── train on (backbone + new self-play) ◄─────────────────┘
|
||||||
|
net_v(N+1)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each generation, the bot reaches positions closer to its real playing distribution,
|
||||||
|
labels them with a stronger-than-bot oracle (Stockfish), and learns the gap. Standard
|
||||||
|
modern NNUE practice. Keep the Lichess backbone mixed in every round so the net does
|
||||||
|
not overfit to its own blind spots.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Scaling datasets over time — append-only shards
|
||||||
|
|
||||||
|
Do **not** maintain one growing `labeled.jsonl` and re-copy it. Make a dataset an
|
||||||
|
**immutable set of shards plus a manifest**:
|
||||||
|
|
||||||
|
```
|
||||||
|
datasets/
|
||||||
|
shards/
|
||||||
|
lichess_000001.jsonl.zst # ~50–100k positions each, ~5–10 MB compressed
|
||||||
|
lichess_000002.jsonl.zst
|
||||||
|
selfplay_v7_000001.jsonl.zst
|
||||||
|
tactical_000001.jsonl.zst
|
||||||
|
...
|
||||||
|
manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`manifest.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dataset_version": 7,
|
||||||
|
"created": "2026-06-24T...",
|
||||||
|
"total_positions": 4200000,
|
||||||
|
"scale": 300.0,
|
||||||
|
"shards": [
|
||||||
|
{"file": "lichess_000001.jsonl.zst", "positions": 100000,
|
||||||
|
"sha256": "...", "source": "lichess_eval", "stockfish_depth": 0},
|
||||||
|
{"file": "selfplay_v7_000001.jsonl.zst", "positions": 80000,
|
||||||
|
"sha256": "...", "source": "selfplay", "net": "v7", "stockfish_depth": 18}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Properties this buys:
|
||||||
|
|
||||||
|
- **Growth = add shards.** Generate a new batch, label it, write one new shard, append
|
||||||
|
one manifest entry. Never touch existing shards. O(new data), not O(total).
|
||||||
|
- **Provenance.** Each shard records source + net + depth. You can later down-weight or
|
||||||
|
drop a bad batch by editing the manifest, no relabeling.
|
||||||
|
- **Dedup across shards** by FEN hash at build time; record dropped counts in metadata.
|
||||||
|
- **Reproducible mixes.** A "dataset version" is just a manifest selecting shards +
|
||||||
|
per-source sampling weights. Cheap to define many mixes over the same shard pool.
|
||||||
|
- **Resumable, cache-friendly transfer** (next section) — the whole reason for shards.
|
||||||
|
|
||||||
|
`dataset.py`'s existing `ds_vN` + `metadata.json` scheme generalizes to this directly:
|
||||||
|
the dataset dir holds `shards/` + `manifest.json` instead of one `labeled.jsonl`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Getting data to Colab easily ← top priority
|
||||||
|
|
||||||
|
Shards make this trivial: **incremental sync, never a full re-upload.**
|
||||||
|
|
||||||
|
### Recommended: rclone → Google Drive, read from mounted Drive
|
||||||
|
|
||||||
|
Colab mounts Drive natively, so the cheapest path is to make Drive the shard store and
|
||||||
|
sync into it with `rclone` (only uploads new/changed shards):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local, after building shards:
|
||||||
|
rclone copy datasets/ gdrive:NowChess/datasets --progress
|
||||||
|
# ^ uploads only shards Drive doesn't have yet. Adding 80k positions = one small file.
|
||||||
|
```
|
||||||
|
|
||||||
|
Colab side, one cell:
|
||||||
|
|
||||||
|
```python
|
||||||
|
SRC = '/content/drive/MyDrive/NowChess/datasets' # mounted, no download
|
||||||
|
import json, shutil, pathlib
|
||||||
|
manifest = json.load(open(f'{SRC}/manifest.json'))
|
||||||
|
local = pathlib.Path('/content/datasets'); local.mkdir(exist_ok=True)
|
||||||
|
for sh in manifest['shards']: # copy Drive→local SSD (fast seq read)
|
||||||
|
dst = local / sh['file']
|
||||||
|
if not dst.exists(): # cache: only copy missing shards
|
||||||
|
shutil.copy(f"{SRC}/shards/{sh['file']}", dst)
|
||||||
|
```
|
||||||
|
|
||||||
|
Why this wins on "easy":
|
||||||
|
- **No browser upload, ever.** One `rclone copy` from your PC.
|
||||||
|
- **Incremental both directions.** Add a shard locally → next `rclone copy` ships only
|
||||||
|
that shard. Colab copies only shards it doesn't already have on `/content`.
|
||||||
|
- **Zero new infra.** Drive is already mounted in the notebook.
|
||||||
|
|
||||||
|
### Alternative: Gitea release per dataset version (if Drive quota hurts)
|
||||||
|
|
||||||
|
You self-host `git.janis-eccarius.de`. Tag `ds_v7`, attach shards + `manifest.json` as
|
||||||
|
release assets. Colab reads the manifest, then parallel-`wget` only the shards it lacks
|
||||||
|
(checksum-verified). Versioned, immutable, no Drive quota, token-gated. Slightly more
|
||||||
|
wiring than rclone→Drive.
|
||||||
|
|
||||||
|
Pick rclone→Drive for minimum friction; Gitea releases if you want hard versioning and
|
||||||
|
to keep Drive small.
|
||||||
|
|
||||||
|
### Notebook changes either way
|
||||||
|
|
||||||
|
- Clone repo to **ephemeral `/content`** (fast), not Drive. Persist only datasets +
|
||||||
|
checkpoints.
|
||||||
|
- Drop Option A (no Colab generation) and Option B (no browser upload). One "sync
|
||||||
|
dataset version" cell instead.
|
||||||
|
- Train reads shards via a streaming `.jsonl.zst` loader (apply per-source sampling
|
||||||
|
weights + eval-bucket balancing here). Keep burst-train + Drive checkpoints + `.nbai`
|
||||||
|
export.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resulting workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
LOCAL (9800X3D / RTX5070) COLAB (GPU)
|
||||||
|
───────────────────────── ───────────
|
||||||
|
import Lichess eval DB ─┐
|
||||||
|
self-play with net_vN ─┼─► label ─► dedup ─► write new shard(s) ─► manifest++
|
||||||
|
tactical / random ─┘ │
|
||||||
|
rclone copy ────────┘
|
||||||
|
datasets/ → Drive
|
||||||
|
│ (only new shards move)
|
||||||
|
▼
|
||||||
|
sync version → copy missing shards → train (GPU)
|
||||||
|
│
|
||||||
|
export .nbai
|
||||||
|
▼
|
||||||
|
place in src/main/resources/, rebuild native image
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build order
|
||||||
|
|
||||||
|
1. **Shard format + manifest** in `dataset.py`: write/read `shards/*.jsonl.zst` +
|
||||||
|
`manifest.json`; dedup-across-shards on build; provenance per shard.
|
||||||
|
2. **Streaming `.zst` dataloader** in `train.py`: read shards, apply per-source weights
|
||||||
|
and eval-bucket balancing.
|
||||||
|
3. **Self-play generator** in `src/`: NNUEBot/Stockfish self-play → positions → local
|
||||||
|
Stockfish label → new shard. This is the scaling engine.
|
||||||
|
4. **`dataset_sync.py`**: `push` (rclone→Drive or Gitea upload) / `pull` (cache-aware).
|
||||||
|
5. **Notebook rewrite**: ephemeral clone, single sync cell, weighted streaming loader.
|
||||||
|
6. Wire `lichess_importer.py` as the backbone shard source.
|
||||||
|
|
||||||
|
## Open decisions
|
||||||
|
|
||||||
|
- **Transfer backend** — rclone→Drive (easiest, recommended) vs Gitea releases (hard
|
||||||
|
versioning).
|
||||||
|
- **Self-play opponent** — NNUEBot vs itself (own distribution) vs vs-Stockfish
|
||||||
|
(stronger, more decisive games). Likely a mix.
|
||||||
|
- **Backbone/self-play ratio** — start ~60/30/10 (lichess/selfplay/tactical), tune by
|
||||||
|
measured strength.
|
||||||
|
- **Shard size** — 50k vs 100k positions/shard (transfer granularity vs file count).
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# Implementation Plan: Two One-Liner Tools (self-play + dataset)
|
||||||
|
|
||||||
|
Goal: **two tools, two start scripts, minimal params.**
|
||||||
|
|
||||||
|
```
|
||||||
|
./selfplay.sh # bot plays games against itself, writes selfplay FENs (Scala, standalone)
|
||||||
|
./dataset.sh # builds the ENTIRE training dataset + rclone push to Drive (Python, one script)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both default-everything. Optional first positional arg only when you want to override
|
||||||
|
the one number that matters.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool 1 — `selfplay.sh` (standalone bot, no microservices)
|
||||||
|
|
||||||
|
### Why it can be standalone
|
||||||
|
|
||||||
|
`Bot` is just `GameContext => Option[Move]` (`Bot.scala`). `NNUEBot.apply` needs only
|
||||||
|
`DefaultRules` (rule module) + `EvaluationNNUE` (loads the bundled `.nbai`). No Quarkus,
|
||||||
|
no coordinator/account/ws. The bot module already depends on `api, rule, io`, and `io`
|
||||||
|
has `FenExporter` + `GameContext.initial` exists. So a plain JVM `main` can run games
|
||||||
|
with zero service wiring.
|
||||||
|
|
||||||
|
### New file: `SelfPlayMain.scala`
|
||||||
|
|
||||||
|
`modules/official-bots/src/main/scala/de/nowchess/bot/selfplay/SelfPlayMain.scala`
|
||||||
|
|
||||||
|
Loop per game:
|
||||||
|
|
||||||
|
1. Start from `GameContext.initial`.
|
||||||
|
2. **Opening diversity** — play `R` random legal plies (default 8). Without this,
|
||||||
|
NNUEBot vs itself is deterministic → the *same game every time*. Random openings are
|
||||||
|
what make the games diverse. (Optional later: seed from polyglot book instead.)
|
||||||
|
3. Then both sides = `NNUEBot(difficulty)`. Apply moves via `DefaultRules.applyMove`.
|
||||||
|
4. Stop on `isCheckmate / isStalemate / isInsufficientMaterial / isFiftyMoveRule /
|
||||||
|
isThreefoldRepetition`, or ply cap (default 200).
|
||||||
|
5. Emit one **FEN per ply** (via `FenExporter`), skipping positions where side-to-move
|
||||||
|
is in check and terminal positions — same filter philosophy the labeler wants.
|
||||||
|
6. Append FENs to the output file (one per line) — exactly the format `label.py` reads.
|
||||||
|
|
||||||
|
Config = a small `case class` with defaults; read from env/args. Defaults:
|
||||||
|
`games=2000`, `randomOpeningPlies=8`, `maxPlies=200`, `out=python/data/selfplay.txt`,
|
||||||
|
`threads = availableProcessors`. Parallelize games across threads (each game is
|
||||||
|
independent; bot is pure).
|
||||||
|
|
||||||
|
Output is **FENs only** — labeling happens in Tool 2 with Stockfish. Keeps the bot tool
|
||||||
|
single-responsibility and fast.
|
||||||
|
|
||||||
|
### Gradle: a plain run task (not Quarkus)
|
||||||
|
|
||||||
|
Add to `modules/official-bots/build.gradle.kts`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
tasks.register<JavaExec>("selfPlay") {
|
||||||
|
group = "nnue"
|
||||||
|
mainClass.set("de.nowchess.bot.selfplay.SelfPlayMain")
|
||||||
|
classpath = sourceSets["main"].runtimeClasspath
|
||||||
|
args(project.findProperty("spArgs")?.toString()?.split(" ") ?: emptyList())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `selfplay.sh` (repo `python/` dir)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
GAMES="${1:-2000}"
|
||||||
|
cd "$(dirname "$0")/../../.." # repo root
|
||||||
|
./gradlew -q :official-bots:selfPlay -PspArgs="--games $GAMES --out modules/official-bots/python/data/selfplay.txt"
|
||||||
|
echo "Self-play FENs -> modules/official-bots/python/data/selfplay.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./selfplay.sh # 2000 games, bundled net
|
||||||
|
./selfplay.sh 8000 # more games
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool 2 — `dataset.sh` → `build_dataset.py` (builds EVERYTHING)
|
||||||
|
|
||||||
|
One Python script that produces a complete, sharded, pushed dataset. No TUI, no
|
||||||
|
multi-step menus. It runs the whole data plane end to end:
|
||||||
|
|
||||||
|
```
|
||||||
|
lichess eval DB ─┐
|
||||||
|
selfplay.txt ─┼─► label (local Stockfish, skip already-labeled) ─► dedup ─►
|
||||||
|
tactical ─┤ eval-bucket
|
||||||
|
random filler ─┘ balance ─►
|
||||||
|
write shards/*.jsonl.zst + manifest.json ─► rclone push
|
||||||
|
```
|
||||||
|
|
||||||
|
### New file: `build_dataset.py` (top-level `python/`)
|
||||||
|
|
||||||
|
Reuses existing modules — orchestrates, doesn't reinvent:
|
||||||
|
|
||||||
|
- **Backbone:** `lichess_importer.py` — download + sample N pre-labeled positions from
|
||||||
|
the Lichess eval DB (no Stockfish cost).
|
||||||
|
- **Self-play:** read `data/selfplay.txt` FENs → `label.py` with local Stockfish
|
||||||
|
(depth 18, all cores — your box eats this).
|
||||||
|
- **Tactical:** `tactical_positions_extractor.py` → `label.py`.
|
||||||
|
- **Random filler:** `generate.py` (small cap) → `label.py`.
|
||||||
|
- **Merge:** dedup by FEN across all sources; **eval-bucket balancing** (cap positions
|
||||||
|
per eval bin so near-equal positions don't dominate).
|
||||||
|
- **Shard + manifest:** split into `shards/*.jsonl.zst` (~100k positions each) + write
|
||||||
|
`manifest.json` (positions, sha256, source, net, depth per shard). Append-only:
|
||||||
|
existing shards untouched, new run adds shards + entries (the scaling story from the
|
||||||
|
concept).
|
||||||
|
- **Push:** `rclone copy datasets/ gdrive:NowChess/datasets` — ships only new shards.
|
||||||
|
|
||||||
|
### One config block, sane defaults
|
||||||
|
|
||||||
|
Top of the script — the *only* thing you ever touch:
|
||||||
|
|
||||||
|
```python
|
||||||
|
LICHESS_POSITIONS = 2_000_000 # backbone
|
||||||
|
USE_SELFPLAY = True # reads data/selfplay.txt if present
|
||||||
|
TACTICAL_PUZZLES = 200_000
|
||||||
|
RANDOM_FILLER = 100_000
|
||||||
|
STOCKFISH_DEPTH = 18
|
||||||
|
RCLONE_REMOTE = "gdrive:NowChess/datasets"
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything else (paths, workers=all cores, shard size, balancing bins) is internal.
|
||||||
|
|
||||||
|
### `dataset.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
python build_dataset.py "$@"
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dataset.sh # full dataset (lichess + selfplay + tactical + filler) -> Drive
|
||||||
|
```
|
||||||
|
|
||||||
|
That single command: downloads backbone, labels self-play/tactical/filler, dedups,
|
||||||
|
balances, shards, and rclone-pushes to Drive. Colab then syncs (concept doc §3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## End-to-end loop (the flywheel)
|
||||||
|
|
||||||
|
```
|
||||||
|
./selfplay.sh # bot generates games with the current net
|
||||||
|
./dataset.sh # fold them into a new dataset version, push to Drive
|
||||||
|
# (Colab) sync + train -> export nnue_weights.nbai
|
||||||
|
# drop .nbai into modules/official-bots/src/main/resources/, rebuild
|
||||||
|
./selfplay.sh # next net plays stronger, better games... repeat
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build order
|
||||||
|
|
||||||
|
1. `SelfPlayMain.scala` — standalone game loop, random openings, parallel games, FEN out.
|
||||||
|
2. `selfPlay` Gradle `JavaExec` task + `selfplay.sh`.
|
||||||
|
3. `build_dataset.py` — orchestrate existing importer/label/tactical/generate into
|
||||||
|
shards + manifest; rclone push.
|
||||||
|
4. `dataset.sh`.
|
||||||
|
5. Shard/manifest read support in `dataset.py` + zstd streaming loader in `train.py`
|
||||||
|
(consumed on Colab).
|
||||||
|
6. Notebook: single "sync dataset version" cell, ephemeral `/content` clone.
|
||||||
|
|
||||||
|
## Decisions to confirm
|
||||||
|
|
||||||
|
- **Self-play opponent:** NNUEBot vs itself + random openings (planned). Add vs-Stockfish
|
||||||
|
later if more decisive games wanted.
|
||||||
|
- **Self-play net source:** use the `.nbai` bundled in `resources` (simplest), or accept
|
||||||
|
a `--weights path`? Plan = bundled by default.
|
||||||
|
- **rclone remote name:** confirm `gdrive` is your configured rclone remote, and the
|
||||||
|
target folder `NowChess/datasets`.
|
||||||
|
- **Stockfish path on your box:** `$STOCKFISH_PATH` or `/usr/games/stockfish`?
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
{
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5,
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.10.0"
|
||||||
|
},
|
||||||
|
"colab": {
|
||||||
|
"provenance": [],
|
||||||
|
"gpuType": "T4"
|
||||||
|
},
|
||||||
|
"accelerator": "GPU"
|
||||||
|
},
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": "# NNUE Training Pipeline\n\nGPU training on Colab. Data is built **locally** (`./dataset.sh` → sharded, pushed to\nDrive via rclone); this notebook only **syncs shards → trains → exports `.nbai`**.\nNo generation, no Stockfish labeling, no browser uploads here.\n\n**Runtime:** GPU (T4 or better). Runtime → Change runtime type → T4 GPU.\n\n**Persistence:** Datasets and checkpoints live on Google Drive, so training resumes\nafter a session timeout. The repo is cloned to ephemeral `/content` for speed.",
|
||||||
|
"id": "intro-md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"---\n",
|
||||||
|
"## ⚙️ 1 — Setup"
|
||||||
|
],
|
||||||
|
"id": "setup-md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Mount Google Drive for checkpoint persistence\n",
|
||||||
|
"from google.colab import drive\n",
|
||||||
|
"drive.mount('/content/drive')"
|
||||||
|
],
|
||||||
|
"id": "mount-drive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": "import os\n\n# ── Configure these paths once ───────────────────────────────────────────────\nREPO_URL = 'https://git.janis-eccarius.de/NowChess/NowChessSystems.git'\nDRIVE_ROOT = '/content/drive/MyDrive/NowChess' # datasets + weights persist here\nREPO_DIR = '/content/NowChessSystems' # ephemeral, fast local clone\nPYTHON_DIR = f'{REPO_DIR}/modules/official-bots/python'\n# ─────────────────────────────────────────────────────────────────────────────\n\nos.makedirs(DRIVE_ROOT, exist_ok=True)\n\n# Clone to ephemeral /content (NOT Drive) — fast checkout, no Drive bloat.\nif not os.path.isdir(REPO_DIR):\n !git clone --depth=1 \"{REPO_URL}\" \"{REPO_DIR}\"\n print('Repo cloned to /content.')\nelse:\n !git -C \"{REPO_DIR}\" pull --ff-only\n print('Repo updated.')",
|
||||||
|
"id": "clone-repo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": "# Install Python dependencies. No Stockfish — labeling happens on the local box,\n# this notebook only trains on already-labeled shards.\n!pip install -q chess tqdm rich zstandard\n\nimport sys\nsys.path.insert(0, f'{PYTHON_DIR}/src')\nsys.path.insert(0, PYTHON_DIR)\nprint('Python path configured.')",
|
||||||
|
"id": "install-deps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": "---\n## 🗄️ 2 — Data\n\nDatasets are built **locally** (`./dataset.sh`) and pushed to Drive with rclone as\ncompressed shards under `MyDrive/NowChess/datasets/`. Here we just sync those shards\nto the fast local disk — no generation, no labeling, no browser uploads.\n\nThe cell reads `manifest.json` and copies only shards not already cached on `/content`.",
|
||||||
|
"id": "data-md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": "import json, shutil\nfrom pathlib import Path\n\n# Source: shards synced from the local box via `rclone copy datasets/ gdrive:NowChess/datasets`\nDRIVE_DATASETS = Path(DRIVE_ROOT) / 'datasets'\nLOCAL_DATASETS = Path('/content/datasets')\n(LOCAL_DATASETS / 'shards').mkdir(parents=True, exist_ok=True)\n\nmanifest = json.load(open(DRIVE_DATASETS / 'manifest.json'))\nprint(f\"Dataset v{manifest['dataset_version']}: \"\n f\"{manifest['total_positions']:,} positions across {len(manifest['shards'])} shards\")\n\ncopied = 0\nfor sh in manifest['shards']:\n dst = LOCAL_DATASETS / 'shards' / sh['file']\n if not dst.exists(): # cache: only copy shards we don't already have\n shutil.copy(DRIVE_DATASETS / 'shards' / sh['file'], dst)\n copied += 1\nshutil.copy(DRIVE_DATASETS / 'manifest.json', LOCAL_DATASETS / 'manifest.json')\n\nDATA_PATH = str(LOCAL_DATASETS) # train_nnue / burst_train read this dir of shards directly\nprint(f\"Synced {copied} new shard(s). Dataset ready at {DATA_PATH}\")",
|
||||||
|
"id": "data-paths"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"---\n",
|
||||||
|
"## 🏋️ 3 — Train\n",
|
||||||
|
"\n",
|
||||||
|
"Standard training runs a fixed number of epochs. \n",
|
||||||
|
"**Burst mode** is better for Colab: it repeatedly restarts from the best checkpoint within a time budget, surviving session disconnects gracefully."
|
||||||
|
],
|
||||||
|
"id": "train-md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": "from train import train_nnue, burst_train, DEFAULT_HIDDEN_SIZES\n\nWEIGHTS_DIR = Path(DRIVE_ROOT) / 'weights'\nWEIGHTS_DIR.mkdir(parents=True, exist_ok=True)\nOUTPUT_FILE = str(WEIGHTS_DIR / 'nnue_weights.pt')\n\n# ── Training hyperparameters ──────────────────────────────────────────────────\nHIDDEN_SIZES = DEFAULT_HIDDEN_SIZES\n# Features are streamed as sparse indices and densified on the GPU per batch, so\n# host RAM is no longer the limit — GPU memory is. A dense batch is\n# batch_size * 98304 * 4 bytes on the GPU (~3.2 GB at 8192 on a 16 GB T4).\nBATCH_SIZE = 8192\nEPOCHS = 100\nEARLY_STOPPING = 10 # None to disable\nSUBSAMPLE_RATIO = 1.0\n\n# Resume from latest checkpoint if one exists\ncheckpoints = sorted(WEIGHTS_DIR.glob('nnue_weights_v*.pt'))\nCHECKPOINT = str(checkpoints[-1]) if checkpoints else None\nif CHECKPOINT:\n print(f'Resuming from checkpoint: {CHECKPOINT}')\nelse:\n print('Starting training from scratch.')",
|
||||||
|
"id": "train-config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": "# ── Standard training ─────────────────────────────────────────────────────────\n# Use this when you have a reliable long-running session.\n\ntrain_nnue(\n data_file=DATA_PATH,\n output_file=OUTPUT_FILE,\n epochs=EPOCHS,\n batch_size=BATCH_SIZE,\n checkpoint=CHECKPOINT,\n use_versioning=True,\n early_stopping_patience=EARLY_STOPPING,\n subsample_ratio=SUBSAMPLE_RATIO,\n hidden_sizes=HIDDEN_SIZES,\n)",
|
||||||
|
"id": "standard-train"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": "# ── Burst training (recommended for Colab free tier) ─────────────────────────\n# Restarts from the global best each time early stopping fires.\n# Set BURST_MINUTES to slightly less than the Colab session limit (~70 min).\n\nBURST_MINUTES = 70\nEPOCHS_PER_SEASON = 30\nBURST_PATIENCE = 8\n\nburst_train(\n data_file=DATA_PATH,\n output_file=OUTPUT_FILE,\n duration_minutes=BURST_MINUTES,\n epochs_per_season=EPOCHS_PER_SEASON,\n early_stopping_patience=BURST_PATIENCE,\n batch_size=BATCH_SIZE,\n initial_checkpoint=CHECKPOINT,\n use_versioning=True,\n subsample_ratio=SUBSAMPLE_RATIO,\n hidden_sizes=HIDDEN_SIZES,\n)",
|
||||||
|
"id": "burst-train"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"---\n",
|
||||||
|
"## 📦 4 — Export\n",
|
||||||
|
"\n",
|
||||||
|
"Convert the best `.pt` checkpoint to the `.nbai` binary format read by `NbaiLoader` in Scala."
|
||||||
|
],
|
||||||
|
"id": "export-md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from export import export_to_nbai\n",
|
||||||
|
"\n",
|
||||||
|
"NBAI_FILE = Path(DRIVE_ROOT) / 'nnue_weights.nbai'\n",
|
||||||
|
"\n",
|
||||||
|
"# Pick the latest versioned checkpoint\n",
|
||||||
|
"checkpoints = sorted(WEIGHTS_DIR.glob('nnue_weights_v*.pt'))\n",
|
||||||
|
"if not checkpoints:\n",
|
||||||
|
" raise FileNotFoundError('No checkpoints found in ' + str(WEIGHTS_DIR))\n",
|
||||||
|
"\n",
|
||||||
|
"latest = checkpoints[-1]\n",
|
||||||
|
"print(f'Exporting {latest.name} → {NBAI_FILE.name}')\n",
|
||||||
|
"\n",
|
||||||
|
"export_to_nbai(\n",
|
||||||
|
" weights_file=str(latest),\n",
|
||||||
|
" output_file=str(NBAI_FILE),\n",
|
||||||
|
" trained_by='colab',\n",
|
||||||
|
")\n",
|
||||||
|
"print('Export complete.')"
|
||||||
|
],
|
||||||
|
"id": "export-cell"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"---\n",
|
||||||
|
"## ⬇️ 5 — Download\n",
|
||||||
|
"\n",
|
||||||
|
"Download the `.nbai` weights file and the latest `.pt` checkpoint to your local machine.\n",
|
||||||
|
"\n",
|
||||||
|
"Place `nnue_weights.nbai` in `modules/official-bots/src/main/resources/` and rebuild the native image."
|
||||||
|
],
|
||||||
|
"id": "download-md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from google.colab import files\n",
|
||||||
|
"\n",
|
||||||
|
"if NBAI_FILE.exists():\n",
|
||||||
|
" files.download(str(NBAI_FILE))\n",
|
||||||
|
" print(f'Downloading {NBAI_FILE.name}')\n",
|
||||||
|
"else:\n",
|
||||||
|
" print('No .nbai file found — run the Export cell first.')\n",
|
||||||
|
"\n",
|
||||||
|
"checkpoints = sorted(WEIGHTS_DIR.glob('nnue_weights_v*.pt'))\n",
|
||||||
|
"if checkpoints:\n",
|
||||||
|
" latest = checkpoints[-1]\n",
|
||||||
|
" files.download(str(latest))\n",
|
||||||
|
" print(f'Downloading checkpoint {latest.name}')\n",
|
||||||
|
"else:\n",
|
||||||
|
" print('No .pt checkpoint found.')"
|
||||||
|
],
|
||||||
|
"id": "download-cell"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build the ENTIRE NNUE training dataset with one command.
|
||||||
|
|
||||||
|
Orchestrates the existing source modules (Lichess eval DB, self-play, tactical puzzles,
|
||||||
|
random filler), labels what needs labeling with local Stockfish, deduplicates, balances
|
||||||
|
the eval distribution, writes append-only compressed shards + a manifest, and pushes to
|
||||||
|
Google Drive with rclone.
|
||||||
|
|
||||||
|
./dataset.sh # build everything + push
|
||||||
|
./dataset.sh --no-push # build only
|
||||||
|
./dataset.sh --no-lichess # skip the (large) Lichess backbone
|
||||||
|
|
||||||
|
Tune the CONFIG block below — that is the only thing you normally touch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import zstandard as zstd
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(HERE / "src"))
|
||||||
|
|
||||||
|
from generate import play_random_game_and_collect_positions
|
||||||
|
from label import label_positions_with_stockfish
|
||||||
|
from lichess_importer import import_lichess_evals
|
||||||
|
from tactical_positions_extractor import download_and_extract_puzzle_db, extract_tactical_only
|
||||||
|
|
||||||
|
# ── CONFIG — the only knobs you normally touch ───────────────────────────────
|
||||||
|
LICHESS_POSITIONS = 2_000_000 # backbone positions from the Lichess eval DB
|
||||||
|
USE_SELFPLAY = True # label data/selfplay.txt if present
|
||||||
|
TACTICAL_PUZZLES = 200_000 # tactical positions from the Lichess puzzle DB
|
||||||
|
RANDOM_FILLER = 100_000 # cheap random-play positions
|
||||||
|
STOCKFISH_DEPTH = 14 # local labeling depth (selfplay/tactical/random)
|
||||||
|
RCLONE_REMOTE = "gdrive:NowChess/datasets"
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
LABEL_BATCH = 64 # positions per Stockfish task (small = smooth progress + load balance)
|
||||||
|
SHARD_SIZE = 100_000 # positions per shard
|
||||||
|
BALANCE_BINS = 64 # eval histogram bins over [-1, 1]
|
||||||
|
BALANCE_FACTOR = 2.0 # cap each bin at FACTOR x the uniform bin size
|
||||||
|
LICHESS_EVAL_URL = "https://database.lichess.org/lichess_db_eval.jsonl.zst"
|
||||||
|
|
||||||
|
STOCKFISH_PATH = os.environ.get("STOCKFISH_PATH", "/usr/games/stockfish")
|
||||||
|
WORKERS = os.cpu_count() or 4
|
||||||
|
|
||||||
|
DATA_DIR = HERE / "data"
|
||||||
|
WORK_DIR = HERE / "data" / "_build"
|
||||||
|
DATASETS_DIR = HERE / "datasets"
|
||||||
|
SHARDS_DIR = DATASETS_DIR / "shards"
|
||||||
|
MANIFEST = DATASETS_DIR / "manifest.json"
|
||||||
|
LICHESS_DB = HERE / "trainingdata" / "lichess_db_eval.jsonl.zst"
|
||||||
|
|
||||||
|
|
||||||
|
def label(fens_file: Path, out: Path) -> int:
|
||||||
|
"""Label a FEN file with local Stockfish. Returns positions written."""
|
||||||
|
if not fens_file.exists():
|
||||||
|
return 0
|
||||||
|
label_positions_with_stockfish(
|
||||||
|
str(fens_file), str(out), STOCKFISH_PATH,
|
||||||
|
batch_size=LABEL_BATCH, depth=STOCKFISH_DEPTH, num_workers=WORKERS,
|
||||||
|
)
|
||||||
|
return count_lines(out)
|
||||||
|
|
||||||
|
|
||||||
|
def count_lines(path: Path) -> int:
|
||||||
|
if not path.exists():
|
||||||
|
return 0
|
||||||
|
with open(path) as f:
|
||||||
|
return sum(1 for _ in f)
|
||||||
|
|
||||||
|
|
||||||
|
def source_lichess(out: Path) -> int:
|
||||||
|
if not LICHESS_DB.exists():
|
||||||
|
print(f"Downloading Lichess eval DB → {LICHESS_DB} (large, one-time)...")
|
||||||
|
LICHESS_DB.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
urllib.request.urlretrieve(LICHESS_EVAL_URL, LICHESS_DB)
|
||||||
|
return import_lichess_evals(str(LICHESS_DB), str(out), max_positions=LICHESS_POSITIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def source_selfplay(out: Path) -> int:
|
||||||
|
return label(DATA_DIR / "selfplay.txt", out)
|
||||||
|
|
||||||
|
|
||||||
|
def source_tactical(out: Path) -> int:
|
||||||
|
puzzle_csv = download_and_extract_puzzle_db(output_dir=str(HERE / "tactical_data"))
|
||||||
|
if puzzle_csv is None:
|
||||||
|
return 0
|
||||||
|
fens = WORK_DIR / "tactical_fens.txt"
|
||||||
|
extract_tactical_only(str(puzzle_csv), str(fens), max_puzzles=TACTICAL_PUZZLES)
|
||||||
|
return label(fens, out)
|
||||||
|
|
||||||
|
|
||||||
|
def source_random(out: Path) -> int:
|
||||||
|
fens = WORK_DIR / "random_fens.txt"
|
||||||
|
play_random_game_and_collect_positions(
|
||||||
|
str(fens), total_positions=RANDOM_FILLER, num_workers=WORKERS,
|
||||||
|
)
|
||||||
|
return label(fens, out)
|
||||||
|
|
||||||
|
|
||||||
|
def build_sources(args) -> dict[str, Path]:
|
||||||
|
"""Run each enabled source into its own labeled jsonl. Returns {name: path}."""
|
||||||
|
WORK_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
plan = [
|
||||||
|
("lichess", args.lichess, source_lichess),
|
||||||
|
("selfplay", args.selfplay, source_selfplay),
|
||||||
|
("tactical", args.tactical, source_tactical),
|
||||||
|
("random", args.random, source_random),
|
||||||
|
]
|
||||||
|
outputs: dict[str, Path] = {}
|
||||||
|
for name, enabled, fn in plan:
|
||||||
|
if not enabled:
|
||||||
|
continue
|
||||||
|
out = WORK_DIR / f"{name}_labeled.jsonl"
|
||||||
|
out.unlink(missing_ok=True)
|
||||||
|
print(f"\n=== Source: {name} ===")
|
||||||
|
written = fn(out)
|
||||||
|
print(f"{name}: {written:,} labeled positions")
|
||||||
|
if written:
|
||||||
|
outputs[name] = out
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
|
||||||
|
def existing_fens() -> set[str]:
|
||||||
|
"""FENs already present in the dataset, so growth stays deduplicated."""
|
||||||
|
seen: set[str] = set()
|
||||||
|
if not MANIFEST.exists():
|
||||||
|
return seen
|
||||||
|
manifest = json.loads(MANIFEST.read_text())
|
||||||
|
for shard in manifest.get("shards", []):
|
||||||
|
for rec in read_shard(SHARDS_DIR / shard["file"]):
|
||||||
|
seen.add(rec["fen"])
|
||||||
|
return seen
|
||||||
|
|
||||||
|
|
||||||
|
def read_shard(path: Path):
|
||||||
|
dctx = zstd.ZstdDecompressor()
|
||||||
|
with open(path, "rb") as fh, dctx.stream_reader(fh) as reader:
|
||||||
|
for line in iter_text(reader):
|
||||||
|
yield json.loads(line)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_text(reader):
|
||||||
|
import io
|
||||||
|
yield from io.TextIOWrapper(reader, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def merge_dedup(outputs: dict[str, Path], skip: set[str]):
|
||||||
|
"""Merge all source jsonl, drop dupes (within batch + vs existing dataset)."""
|
||||||
|
seen = set(skip)
|
||||||
|
records, per_source = [], {}
|
||||||
|
for name, path in outputs.items():
|
||||||
|
kept = 0
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
rec = json.loads(line)
|
||||||
|
fen = rec["fen"]
|
||||||
|
if fen in seen:
|
||||||
|
continue
|
||||||
|
seen.add(fen)
|
||||||
|
rec["source"] = name
|
||||||
|
records.append(rec)
|
||||||
|
kept += 1
|
||||||
|
per_source[name] = kept
|
||||||
|
return records, per_source
|
||||||
|
|
||||||
|
|
||||||
|
def balance(records: list) -> list:
|
||||||
|
"""Flatten the eval histogram: cap each bin at FACTOR x the uniform bin size."""
|
||||||
|
if not records:
|
||||||
|
return records
|
||||||
|
cap = max(1, int(BALANCE_FACTOR * len(records) / BALANCE_BINS))
|
||||||
|
bins: dict[int, int] = {}
|
||||||
|
kept = []
|
||||||
|
random.shuffle(records)
|
||||||
|
for rec in records:
|
||||||
|
b = min(BALANCE_BINS - 1, int((rec["eval"] + 1.0) / 2.0 * BALANCE_BINS))
|
||||||
|
if bins.get(b, 0) < cap:
|
||||||
|
bins[b] = bins.get(b, 0) + 1
|
||||||
|
kept.append(rec)
|
||||||
|
return kept
|
||||||
|
|
||||||
|
|
||||||
|
def sha256(path: Path) -> str:
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(1 << 20), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def write_shards(records: list, build_id: str) -> list[dict]:
|
||||||
|
SHARDS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
cctx = zstd.ZstdCompressor(level=10)
|
||||||
|
entries = []
|
||||||
|
for i in range(0, len(records), SHARD_SIZE):
|
||||||
|
chunk = records[i : i + SHARD_SIZE]
|
||||||
|
name = f"{build_id}_{i // SHARD_SIZE:05d}.jsonl.zst"
|
||||||
|
path = SHARDS_DIR / name
|
||||||
|
with open(path, "wb") as fh, cctx.stream_writer(fh) as w:
|
||||||
|
for rec in chunk:
|
||||||
|
w.write((json.dumps(rec) + "\n").encode("utf-8"))
|
||||||
|
entries.append({"file": name, "positions": len(chunk),
|
||||||
|
"sha256": sha256(path), "build_id": build_id})
|
||||||
|
print(f" wrote {name} ({len(chunk):,} positions)")
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def update_manifest(new_shards: list[dict], build: dict) -> None:
|
||||||
|
manifest = json.loads(MANIFEST.read_text()) if MANIFEST.exists() else {
|
||||||
|
"dataset_version": 0, "scale": 300.0, "builds": [], "shards": [],
|
||||||
|
}
|
||||||
|
manifest["dataset_version"] += 1
|
||||||
|
manifest["created"] = build["created"]
|
||||||
|
manifest["builds"].append(build)
|
||||||
|
manifest["shards"].extend(new_shards)
|
||||||
|
manifest["total_positions"] = sum(s["positions"] for s in manifest["shards"])
|
||||||
|
MANIFEST.write_text(json.dumps(manifest, indent=2))
|
||||||
|
print(f"\nDataset version {manifest['dataset_version']}: "
|
||||||
|
f"{manifest['total_positions']:,} total positions across {len(manifest['shards'])} shards")
|
||||||
|
|
||||||
|
|
||||||
|
def push() -> None:
|
||||||
|
if not subprocess.run(["which", "rclone"], capture_output=True).stdout:
|
||||||
|
print("rclone not found — skipping push.")
|
||||||
|
return
|
||||||
|
print(f"\nPushing {DATASETS_DIR} → {RCLONE_REMOTE} ...")
|
||||||
|
subprocess.run(["rclone", "copy", str(DATASETS_DIR), RCLONE_REMOTE, "--progress"], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
p = argparse.ArgumentParser(description="Build the entire NNUE dataset.")
|
||||||
|
for name in ("lichess", "selfplay", "tactical", "random", "push"):
|
||||||
|
p.add_argument(f"--no-{name}", dest=name, action="store_false")
|
||||||
|
p.add_argument("--push-only", action="store_true", help="Push the existing dataset, build nothing.")
|
||||||
|
return p.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
if args.push_only:
|
||||||
|
push()
|
||||||
|
return
|
||||||
|
build_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||||
|
|
||||||
|
outputs = build_sources(args)
|
||||||
|
if not outputs:
|
||||||
|
print("No sources produced data — nothing to build.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n=== Merge / dedup / balance ===")
|
||||||
|
records, per_source = merge_dedup(outputs, existing_fens())
|
||||||
|
print(f"merged unique (new): {len(records):,}")
|
||||||
|
records = balance(records)
|
||||||
|
print(f"after balancing: {len(records):,}")
|
||||||
|
|
||||||
|
new_shards = write_shards(records, build_id)
|
||||||
|
update_manifest(new_shards, {
|
||||||
|
"build_id": build_id,
|
||||||
|
"created": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"stockfish_depth": STOCKFISH_DEPTH,
|
||||||
|
"sources": per_source,
|
||||||
|
"kept_after_balance": len(records),
|
||||||
|
})
|
||||||
|
|
||||||
|
if args.push:
|
||||||
|
push()
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the ENTIRE NNUE training dataset + push to Drive. One command.
|
||||||
|
#
|
||||||
|
# ./dataset.sh # build everything + rclone push
|
||||||
|
# ./dataset.sh --no-push # build only
|
||||||
|
# ./dataset.sh --no-lichess # skip the large Lichess backbone
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
PY="python3"
|
||||||
|
if [[ -x "$SCRIPT_DIR/.venv/bin/python" ]]; then
|
||||||
|
PY="$SCRIPT_DIR/.venv/bin/python"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$PY" build_dataset.py "$@"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Standalone bot self-play -> FENs for labeling. No microservices.
|
||||||
|
#
|
||||||
|
# ./selfplay.sh # 500 games with the bundled net
|
||||||
|
# ./selfplay.sh 2000 # more games
|
||||||
|
# ./selfplay.sh 2000 path.nbai # play with a specific net
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GAMES="${1:-500}"
|
||||||
|
WEIGHTS="${2:-}"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
OUT="$SCRIPT_DIR/data/selfplay.txt"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
SP_ARGS="--games $GAMES --out $OUT"
|
||||||
|
if [[ -n "$WEIGHTS" ]]; then
|
||||||
|
SP_ARGS="$SP_ARGS --weights $WEIGHTS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
./gradlew -q :modules:official-bots:selfPlay -PspArgs="$SP_ARGS"
|
||||||
|
echo "Self-play FENs -> $OUT"
|
||||||
@@ -13,6 +13,38 @@ import chess
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import re
|
import re
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import os
|
||||||
|
|
||||||
|
# DataLoader workers: cap to the machine's CPUs (Colab free tier = 2). Too many
|
||||||
|
# workers each fork the dataset and OOM-kill the runtime.
|
||||||
|
LOADER_WORKERS = int(os.environ.get("NNUE_LOADER_WORKERS", min(4, os.cpu_count() or 2)))
|
||||||
|
|
||||||
|
|
||||||
|
def _shard_files(data_file):
|
||||||
|
"""Resolve a data path to a list of shard files. Accepts a single .jsonl/.jsonl.zst
|
||||||
|
file, or a directory (searched recursively for shards, e.g. a synced datasets/ dir)."""
|
||||||
|
p = Path(data_file)
|
||||||
|
if p.is_dir():
|
||||||
|
shards = sorted(p.rglob("*.jsonl.zst")) or sorted(p.rglob("*.jsonl"))
|
||||||
|
if not shards:
|
||||||
|
raise FileNotFoundError(f"No .jsonl/.jsonl.zst shards found under {p}")
|
||||||
|
print(f"Loading {len(shards)} shard(s) from {p}")
|
||||||
|
return shards
|
||||||
|
return [p]
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_dataset_lines(data_file):
|
||||||
|
"""Yield text lines from every shard, transparently decompressing .zst shards."""
|
||||||
|
import io
|
||||||
|
for shard in _shard_files(data_file):
|
||||||
|
if str(shard).endswith(".zst"):
|
||||||
|
import zstandard as zstd
|
||||||
|
with open(shard, "rb") as fh, zstd.ZstdDecompressor().stream_reader(fh) as reader:
|
||||||
|
yield from io.TextIOWrapper(reader, encoding="utf-8")
|
||||||
|
else:
|
||||||
|
with open(shard, "r") as fh:
|
||||||
|
yield from fh
|
||||||
|
|
||||||
|
|
||||||
class NNUEDataset(Dataset):
|
class NNUEDataset(Dataset):
|
||||||
"""Dataset of chess positions with evaluations."""
|
"""Dataset of chess positions with evaluations."""
|
||||||
@@ -23,27 +55,26 @@ class NNUEDataset(Dataset):
|
|||||||
self.evals_raw = []
|
self.evals_raw = []
|
||||||
self.is_normalized = None
|
self.is_normalized = None
|
||||||
|
|
||||||
with open(data_file, 'r') as f:
|
for line in _iter_dataset_lines(data_file):
|
||||||
for line in f:
|
try:
|
||||||
try:
|
data = json.loads(line)
|
||||||
data = json.loads(line)
|
fen = data['fen']
|
||||||
fen = data['fen']
|
eval_val = data['eval']
|
||||||
eval_val = data['eval']
|
self.positions.append(fen)
|
||||||
self.positions.append(fen)
|
self.evals.append(eval_val)
|
||||||
self.evals.append(eval_val)
|
|
||||||
|
|
||||||
# Check if normalized or raw
|
# Check if normalized or raw
|
||||||
if self.is_normalized is None:
|
if self.is_normalized is None:
|
||||||
# If eval is in range [-1, 1], assume normalized
|
# If eval is in range [-1, 1], assume normalized
|
||||||
self.is_normalized = abs(eval_val) <= 1.0
|
self.is_normalized = abs(eval_val) <= 1.0
|
||||||
|
|
||||||
# Store raw if available
|
# Store raw if available
|
||||||
if 'eval_raw' in data:
|
if 'eval_raw' in data:
|
||||||
self.evals_raw.append(data['eval_raw'])
|
self.evals_raw.append(data['eval_raw'])
|
||||||
else:
|
else:
|
||||||
self.evals_raw.append(eval_val)
|
self.evals_raw.append(eval_val)
|
||||||
except (json.JSONDecodeError, KeyError):
|
except (json.JSONDecodeError, KeyError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.positions)
|
return len(self.positions)
|
||||||
@@ -51,7 +82,15 @@ class NNUEDataset(Dataset):
|
|||||||
def __getitem__(self, idx):
|
def __getitem__(self, idx):
|
||||||
fen = self.positions[idx]
|
fen = self.positions[idx]
|
||||||
eval_val = self.evals[idx]
|
eval_val = self.evals[idx]
|
||||||
features = fen_to_features(fen)
|
# Return only the active feature indices (~64), not a dense 98304-dim vector.
|
||||||
|
# The training loop scatters these into a dense batch on the GPU, keeping host
|
||||||
|
# RAM tiny. Densifying per-item here OOM-kills the runtime.
|
||||||
|
indices = fen_to_indices(fen)
|
||||||
|
|
||||||
|
# Board is flipped for Black-to-move in fen_to_indices; negate eval
|
||||||
|
# so the label still means "good for the side shown as White after flip"
|
||||||
|
if ' b ' in fen:
|
||||||
|
eval_val = -eval_val
|
||||||
|
|
||||||
# Use evaluation as-is if normalized, otherwise apply sigmoid scaling
|
# Use evaluation as-is if normalized, otherwise apply sigmoid scaling
|
||||||
if self.is_normalized:
|
if self.is_normalized:
|
||||||
@@ -59,40 +98,82 @@ class NNUEDataset(Dataset):
|
|||||||
else:
|
else:
|
||||||
target = torch.sigmoid(torch.tensor(eval_val / 400.0, dtype=torch.float32))
|
target = torch.sigmoid(torch.tensor(eval_val / 400.0, dtype=torch.float32))
|
||||||
|
|
||||||
return features, target
|
return indices, target
|
||||||
|
|
||||||
def fen_to_features(fen):
|
# King-relative (HalfKP) encoding: two perspectives, one per side's king.
|
||||||
"""Convert FEN to 768-dimensional binary feature vector."""
|
# Each piece is encoded as: kingSq * 768 + pieceIdx * 64 + sq
|
||||||
# Piece type to index: pawn=0, knight=1, bishop=2, rook=3, queen=4, king=5
|
# White perspective uses white king square; black perspective uses black king square.
|
||||||
piece_to_idx = {'p': 0, 'n': 1, 'b': 2, 'r': 3, 'q': 4, 'k': 5,
|
# Total input dimension = 2 × 64 × 12 × 64 = 98304.
|
||||||
'P': 6, 'N': 7, 'B': 8, 'R': 9, 'Q': 10, 'K': 11}
|
_HALF_SIZE = 64 * 12 * 64 # 49152 features per perspective
|
||||||
|
INPUT_SIZE = _HALF_SIZE * 2 # 98304
|
||||||
|
|
||||||
features = torch.zeros(768, dtype=torch.float32)
|
_PIECE_TO_IDX = {
|
||||||
|
'p': 0, 'n': 1, 'b': 2, 'r': 3, 'q': 4, 'k': 5,
|
||||||
|
'P': 6, 'N': 7, 'B': 8, 'R': 9, 'Q': 10, 'K': 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fen_to_indices(fen):
|
||||||
|
"""Active king-relative (HalfKP) feature indices for a FEN (~64 entries).
|
||||||
|
|
||||||
|
For Black-to-move positions the board is mirrored (ranks flipped, colours
|
||||||
|
swapped) so the network always sees the position from the side-to-move's
|
||||||
|
perspective. The caller is responsible for negating the eval label to match.
|
||||||
|
"""
|
||||||
|
indices = []
|
||||||
try:
|
try:
|
||||||
board = chess.Board(fen)
|
board = chess.Board(fen)
|
||||||
|
# Perspective flip: present all positions as if White is to move
|
||||||
|
if board.turn == chess.BLACK:
|
||||||
|
board = board.mirror()
|
||||||
|
wk = board.king(chess.WHITE)
|
||||||
|
bk = board.king(chess.BLACK)
|
||||||
|
if wk is None or bk is None:
|
||||||
|
return torch.zeros(0, dtype=torch.long)
|
||||||
|
for sq in chess.SQUARES:
|
||||||
|
piece = board.piece_at(sq)
|
||||||
|
if piece is None:
|
||||||
|
continue
|
||||||
|
pidx = _PIECE_TO_IDX[piece.symbol()]
|
||||||
|
# White-king perspective (indices 0 .. _HALF_SIZE-1)
|
||||||
|
indices.append(wk * 768 + pidx * 64 + sq)
|
||||||
|
# Black-king perspective (indices _HALF_SIZE .. INPUT_SIZE-1)
|
||||||
|
indices.append(_HALF_SIZE + bk * 768 + pidx * 64 + sq)
|
||||||
|
except Exception:
|
||||||
|
return torch.zeros(0, dtype=torch.long)
|
||||||
|
return torch.tensor(indices, dtype=torch.long)
|
||||||
|
|
||||||
# 12 piece types × 64 squares = 768
|
|
||||||
for square in chess.SQUARES:
|
|
||||||
piece = board.piece_at(square)
|
|
||||||
if piece is not None:
|
|
||||||
piece_char = piece.symbol()
|
|
||||||
if piece_char in piece_to_idx:
|
|
||||||
piece_idx = piece_to_idx[piece_char]
|
|
||||||
feature_idx = piece_idx * 64 + square
|
|
||||||
features[feature_idx] = 1.0
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
def fen_to_features(fen):
|
||||||
|
"""Dense 98304-dim HalfKP vector. Kept for external callers; training uses the
|
||||||
|
sparse indices + GPU scatter path instead (see _collate_sparse)."""
|
||||||
|
features = torch.zeros(INPUT_SIZE, dtype=torch.float32)
|
||||||
|
features[fen_to_indices(fen)] = 1.0
|
||||||
return features
|
return features
|
||||||
|
|
||||||
DEFAULT_HIDDEN_SIZES = [1536, 1024, 512, 256]
|
|
||||||
|
def _collate_sparse(batch):
|
||||||
|
"""Collate (indices, target) items into (row_idx, col_idx, batch_size), targets.
|
||||||
|
|
||||||
|
Row/col index pairs address the active features of a dense [B, INPUT_SIZE] tensor
|
||||||
|
that the training loop allocates on the GPU — so the host only ever holds the
|
||||||
|
sparse indices, never a dense batch."""
|
||||||
|
idx_list, targets = zip(*batch)
|
||||||
|
rows = torch.cat([torch.full((idx.numel(),), i, dtype=torch.long)
|
||||||
|
for i, idx in enumerate(idx_list)])
|
||||||
|
cols = torch.cat(idx_list)
|
||||||
|
return (rows, cols, len(idx_list)), torch.stack(targets)
|
||||||
|
|
||||||
|
# Smaller hidden layers are appropriate: the L1 input is very sparse (~64 active
|
||||||
|
# features out of 98304) so the L1 itself is cheap to update incrementally; the
|
||||||
|
# larger capacity comes from the wider perspective encoding, not deeper layers.
|
||||||
|
DEFAULT_HIDDEN_SIZES = [512, 256, 128]
|
||||||
|
|
||||||
|
|
||||||
class NNUE(nn.Module):
|
class NNUE(nn.Module):
|
||||||
"""NNUE neural network with configurable hidden layers.
|
"""NNUE neural network with configurable hidden layers.
|
||||||
|
|
||||||
Architecture: 768 → hidden_sizes[0] → ... → hidden_sizes[-1] → 1
|
Architecture: INPUT_SIZE → hidden_sizes[0] → ... → hidden_sizes[-1] → 1
|
||||||
Layer attributes follow the naming l1, l2, ..., lN so export.py can
|
Layer attributes follow the naming l1, l2, ..., lN so export.py can
|
||||||
infer the architecture directly from the state_dict.
|
infer the architecture directly from the state_dict.
|
||||||
"""
|
"""
|
||||||
@@ -102,7 +183,7 @@ class NNUE(nn.Module):
|
|||||||
if hidden_sizes is None:
|
if hidden_sizes is None:
|
||||||
hidden_sizes = DEFAULT_HIDDEN_SIZES
|
hidden_sizes = DEFAULT_HIDDEN_SIZES
|
||||||
self.hidden_sizes = list(hidden_sizes)
|
self.hidden_sizes = list(hidden_sizes)
|
||||||
sizes = [768] + self.hidden_sizes + [1]
|
sizes = [INPUT_SIZE] + self.hidden_sizes + [1]
|
||||||
num_hidden = len(self.hidden_sizes)
|
num_hidden = len(self.hidden_sizes)
|
||||||
|
|
||||||
for i in range(num_hidden):
|
for i in range(num_hidden):
|
||||||
@@ -204,17 +285,19 @@ def _setup_training(data_file, batch_size, subsample_ratio):
|
|||||||
train_dataset,
|
train_dataset,
|
||||||
batch_size=batch_size,
|
batch_size=batch_size,
|
||||||
sampler=train_sampler,
|
sampler=train_sampler,
|
||||||
num_workers=8,
|
num_workers=LOADER_WORKERS,
|
||||||
pin_memory=True,
|
pin_memory=True,
|
||||||
persistent_workers=True
|
persistent_workers=LOADER_WORKERS > 0,
|
||||||
|
collate_fn=_collate_sparse,
|
||||||
)
|
)
|
||||||
val_loader = DataLoader(
|
val_loader = DataLoader(
|
||||||
val_dataset,
|
val_dataset,
|
||||||
batch_size=batch_size,
|
batch_size=batch_size,
|
||||||
shuffle=False,
|
shuffle=False,
|
||||||
num_workers=8,
|
num_workers=LOADER_WORKERS,
|
||||||
pin_memory=True,
|
pin_memory=True,
|
||||||
persistent_workers=True
|
persistent_workers=LOADER_WORKERS > 0,
|
||||||
|
collate_fn=_collate_sparse,
|
||||||
)
|
)
|
||||||
|
|
||||||
return device, dataset, train_dataset, val_dataset, train_loader, val_loader, num_positions
|
return device, dataset, train_dataset, val_dataset, train_loader, val_loader, num_positions
|
||||||
@@ -252,8 +335,9 @@ def _run_training_season(
|
|||||||
model.train()
|
model.train()
|
||||||
train_loss = 0.0
|
train_loss = 0.0
|
||||||
with tqdm(total=len(train_loader), desc=f"Epoch {epoch_display}/{total_epochs} - Train") as pbar:
|
with tqdm(total=len(train_loader), desc=f"Epoch {epoch_display}/{total_epochs} - Train") as pbar:
|
||||||
for batch_features, batch_targets in train_loader:
|
for (rows, cols, bsz), batch_targets in train_loader:
|
||||||
batch_features = batch_features.to(device)
|
batch_features = torch.zeros(bsz, INPUT_SIZE, device=device)
|
||||||
|
batch_features[rows.to(device), cols.to(device)] = 1.0
|
||||||
batch_targets = batch_targets.to(device).unsqueeze(1)
|
batch_targets = batch_targets.to(device).unsqueeze(1)
|
||||||
|
|
||||||
optimizer.zero_grad()
|
optimizer.zero_grad()
|
||||||
@@ -266,7 +350,7 @@ def _run_training_season(
|
|||||||
scaler.step(optimizer)
|
scaler.step(optimizer)
|
||||||
scaler.update()
|
scaler.update()
|
||||||
|
|
||||||
train_loss += loss.item() * batch_features.size(0)
|
train_loss += loss.item() * bsz
|
||||||
pbar.update(1)
|
pbar.update(1)
|
||||||
|
|
||||||
train_loss /= len(train_dataset)
|
train_loss /= len(train_dataset)
|
||||||
@@ -276,14 +360,15 @@ def _run_training_season(
|
|||||||
val_loss = 0.0
|
val_loss = 0.0
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
with tqdm(total=len(val_loader), desc=f"Epoch {epoch_display}/{total_epochs} - Val") as pbar:
|
with tqdm(total=len(val_loader), desc=f"Epoch {epoch_display}/{total_epochs} - Val") as pbar:
|
||||||
for batch_features, batch_targets in val_loader:
|
for (rows, cols, bsz), batch_targets in val_loader:
|
||||||
batch_features = batch_features.to(device)
|
batch_features = torch.zeros(bsz, INPUT_SIZE, device=device)
|
||||||
|
batch_features[rows.to(device), cols.to(device)] = 1.0
|
||||||
batch_targets = batch_targets.to(device).unsqueeze(1)
|
batch_targets = batch_targets.to(device).unsqueeze(1)
|
||||||
|
|
||||||
with torch.amp.autocast('cuda' if torch.cuda.is_available() else 'cpu'):
|
with torch.amp.autocast('cuda' if torch.cuda.is_available() else 'cpu'):
|
||||||
outputs = model(batch_features)
|
outputs = model(batch_features)
|
||||||
loss = criterion(outputs, batch_targets)
|
loss = criterion(outputs, batch_targets)
|
||||||
val_loss += loss.item() * batch_features.size(0)
|
val_loss += loss.item() * bsz
|
||||||
pbar.update(1)
|
pbar.update(1)
|
||||||
|
|
||||||
val_loss /= len(val_dataset)
|
val_loss /= len(val_dataset)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ nowchess:
|
|||||||
prefix: nowchess
|
prefix: nowchess
|
||||||
internal:
|
internal:
|
||||||
secret: 123abc
|
secret: 123abc
|
||||||
|
tournament:
|
||||||
|
service-url: http://localhost:8088
|
||||||
|
|
||||||
"%deployed":
|
"%deployed":
|
||||||
quarkus:
|
quarkus:
|
||||||
@@ -49,3 +51,5 @@ nowchess:
|
|||||||
prefix: ${REDIS_PREFIX:nowchess}
|
prefix: ${REDIS_PREFIX:nowchess}
|
||||||
internal:
|
internal:
|
||||||
secret: ${INTERNAL_SECRET}
|
secret: ${INTERNAL_SECRET}
|
||||||
|
tournament:
|
||||||
|
service-url: ${TOURNAMENT_SERVICE_URL:http://localhost:8088}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,14 +1,20 @@
|
|||||||
package de.nowchess.bot
|
package de.nowchess.bot
|
||||||
|
|
||||||
import de.nowchess.bot.bots.ClassicalBot
|
import de.nowchess.bot.bots.{ClassicalBot, HybridBot}
|
||||||
|
import de.nowchess.bot.util.PolyglotBook
|
||||||
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 openingBook = PolyglotBook.fromResource("/opening_book.bin")
|
||||||
|
|
||||||
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(_), book = Some(openingBook)),
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ object NNUEBot:
|
|||||||
difficulty: BotDifficulty,
|
difficulty: BotDifficulty,
|
||||||
rules: RuleSet = DefaultRules,
|
rules: RuleSet = DefaultRules,
|
||||||
book: Option[PolyglotBook] = None,
|
book: Option[PolyglotBook] = None,
|
||||||
|
fixedMoveTimeMs: Option[Long] = None,
|
||||||
): Bot =
|
): Bot =
|
||||||
val search = AlphaBetaSearch(rules, weights = EvaluationNNUE)
|
val search = AlphaBetaSearch(rules, weights = EvaluationNNUE)
|
||||||
context =>
|
context =>
|
||||||
@@ -28,7 +29,8 @@ object NNUEBot:
|
|||||||
else
|
else
|
||||||
val scored = batchEvaluateRoot(rules, context, moves)
|
val scored = batchEvaluateRoot(rules, context, moves)
|
||||||
val bestMove = scored.maxBy(_._2)._1
|
val bestMove = scored.maxBy(_._2)._1
|
||||||
search.bestMoveWithTime(context, allocateTime(scored), blockedMoves).orElse(Some(bestMove))
|
val budget = fixedMoveTimeMs.getOrElse(allocateTime(scored))
|
||||||
|
search.bestMoveWithTime(context, budget, blockedMoves, scored.toMap).orElse(Some(bestMove))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def batchEvaluateRoot(rules: RuleSet, context: GameContext, moves: List[Move]): List[(Move, Int)] =
|
private def batchEvaluateRoot(rules: RuleSet, context: GameContext, moves: List[Move]): List[(Move, Int)] =
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ object EvaluationNNUE extends Evaluation:
|
|||||||
nnue.copyAccumulator(parentPly, childPly)
|
nnue.copyAccumulator(parentPly, childPly)
|
||||||
|
|
||||||
override def pushAccumulator(childPly: Int, move: Move, parent: GameContext, child: GameContext): Unit =
|
override def pushAccumulator(childPly: Int, move: Move, parent: GameContext, child: GameContext): Unit =
|
||||||
// Use incremental updates, but recompute from scratch every 10 plies to prevent accumulation errors
|
// Recompute every 10 plies to prevent floating-point drift; king moves always recompute internally
|
||||||
if childPly % 10 == 0 then nnue.recomputeAccumulator(childPly, child.board)
|
if childPly % 10 == 0 then nnue.recomputeAccumulator(childPly, child.board)
|
||||||
else nnue.pushAccumulator(childPly, move, parent.board)
|
else nnue.pushAccumulator(childPly, move, parent.board, child.board)
|
||||||
|
|
||||||
override def evaluateAccumulator(ply: Int, context: GameContext, hash: Long): Int =
|
override def evaluateAccumulator(ply: Int, context: GameContext, hash: Long): Int =
|
||||||
nnue.evaluateAtPlyWithValidation(ply, context.turn, hash, context.board)
|
nnue.evaluateAtPlyWithValidation(ply, context.turn, hash, context.board)
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package de.nowchess.bot.bots.nnue
|
package de.nowchess.bot.bots.nnue
|
||||||
|
|
||||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Square}
|
import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square}
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
|
|
||||||
class NNUE(model: NbaiModel):
|
class NNUE(model: NbaiModel):
|
||||||
|
|
||||||
private val featureSize = model.layers(0).inputSize
|
private val HALF_SIZE = 49152 // 64 king-squares × 12 piece-types × 64 piece-squares
|
||||||
|
private val featureSize = model.layers(0).inputSize // 98304 (= HALF_SIZE * 2) for king-relative
|
||||||
private val accSize = model.layers(0).outputSize
|
private val accSize = model.layers(0).outputSize
|
||||||
private val validateAccum = sys.env.contains("NNUE_VALIDATE") // Enable with NNUE_VALIDATE=1
|
private val validateAccum = sys.env.contains("NNUE_VALIDATE")
|
||||||
|
|
||||||
// Column-major L1 weights for cache-friendly sparse & incremental updates.
|
// Column-major L1 weights: l1WeightsT(featureIdx * accSize + outputIdx)
|
||||||
// l1WeightsT(featureIdx * accSize + outputIdx) = l1Weights(outputIdx * featureSize + featureIdx)
|
|
||||||
private val l1WeightsT: Array[Float] =
|
private val l1WeightsT: Array[Float] =
|
||||||
val w = model.weights(0).weights
|
val w = model.weights(0).weights
|
||||||
val t = new Array[Float](featureSize * accSize)
|
val t = new Array[Float](featureSize * accSize)
|
||||||
@@ -23,7 +23,6 @@ class NNUE(model: NbaiModel):
|
|||||||
private val MAX_PLY = 128
|
private val MAX_PLY = 128
|
||||||
private val l1Stack: Array[Array[Float]] = Array.fill(MAX_PLY + 1)(new Array[Float](accSize))
|
private val l1Stack: Array[Array[Float]] = Array.fill(MAX_PLY + 1)(new Array[Float](accSize))
|
||||||
|
|
||||||
// Shared evaluation buffers: index i holds the output of layers(i) (all except the scalar output layer).
|
|
||||||
private val evalBuffers: Array[Array[Float]] = model.layers.init.map(l => new Array[Float](l.outputSize))
|
private val evalBuffers: Array[Array[Float]] = model.layers.init.map(l => new Array[Float](l.outputSize))
|
||||||
|
|
||||||
// ── Eval cache ───────────────────────────────────────────────────────────
|
// ── Eval cache ───────────────────────────────────────────────────────────
|
||||||
@@ -36,9 +35,29 @@ class NNUE(model: NbaiModel):
|
|||||||
|
|
||||||
private def squareNum(sq: Square): Int = sq.rank.ordinal * 8 + sq.file.ordinal
|
private def squareNum(sq: Square): Int = sq.rank.ordinal * 8 + sq.file.ordinal
|
||||||
|
|
||||||
private def featureIndex(piece: Piece, sqNum: Int): Int =
|
// Mirror square vertically (rank 0 ↔ rank 7) for the perspective flip
|
||||||
val colorOffset = if piece.color == Color.White then 6 else 0
|
private def flipSqNum(sqNum: Int): Int = (7 - sqNum / 8) * 8 + sqNum % 8
|
||||||
(colorOffset + piece.pieceType.ordinal) * 64 + sqNum
|
|
||||||
|
private def pieceIdx(piece: Piece): Int =
|
||||||
|
if piece.color == Color.White then 6 + piece.pieceType.ordinal else piece.pieceType.ordinal
|
||||||
|
|
||||||
|
// White-king perspective: index in [0, HALF_SIZE)
|
||||||
|
private def featureIdxWhite(piece: Piece, sqNum: Int, wkSq: Int): Int =
|
||||||
|
wkSq * 768 + pieceIdx(piece) * 64 + sqNum
|
||||||
|
|
||||||
|
// Black-king perspective: index in [HALF_SIZE, featureSize)
|
||||||
|
private def featureIdxBlack(piece: Piece, sqNum: Int, bkSq: Int): Int =
|
||||||
|
HALF_SIZE + bkSq * 768 + pieceIdx(piece) * 64 + sqNum
|
||||||
|
|
||||||
|
private def wkSqOf(board: Board): Int =
|
||||||
|
board.pieces
|
||||||
|
.collectFirst { case (sq, p) if p.pieceType == PieceType.King && p.color == Color.White => squareNum(sq) }
|
||||||
|
.getOrElse(0)
|
||||||
|
|
||||||
|
private def bkSqOf(board: Board): Int =
|
||||||
|
board.pieces
|
||||||
|
.collectFirst { case (sq, p) if p.pieceType == PieceType.King && p.color == Color.Black => squareNum(sq) }
|
||||||
|
.getOrElse(0)
|
||||||
|
|
||||||
private def addColumn(l1Pre: Array[Float], featureIdx: Int): Unit =
|
private def addColumn(l1Pre: Array[Float], featureIdx: Int): Unit =
|
||||||
val offset = featureIdx * accSize
|
val offset = featureIdx * accSize
|
||||||
@@ -48,92 +67,96 @@ class NNUE(model: NbaiModel):
|
|||||||
val offset = featureIdx * accSize
|
val offset = featureIdx * accSize
|
||||||
for i <- 0 until accSize do l1Pre(i) -= l1WeightsT(offset + i)
|
for i <- 0 until accSize do l1Pre(i) -= l1WeightsT(offset + i)
|
||||||
|
|
||||||
|
private def addPiece(l1: Array[Float], piece: Piece, sqNum: Int, wkSq: Int, bkSq: Int): Unit =
|
||||||
|
addColumn(l1, featureIdxWhite(piece, sqNum, wkSq))
|
||||||
|
addColumn(l1, featureIdxBlack(piece, sqNum, bkSq))
|
||||||
|
|
||||||
|
private def removePiece(l1: Array[Float], piece: Piece, sqNum: Int, wkSq: Int, bkSq: Int): Unit =
|
||||||
|
subtractColumn(l1, featureIdxWhite(piece, sqNum, wkSq))
|
||||||
|
subtractColumn(l1, featureIdxBlack(piece, sqNum, bkSq))
|
||||||
|
|
||||||
// ── Accumulator init ─────────────────────────────────────────────────────
|
// ── Accumulator init ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
def initAccumulator(board: Board): Unit =
|
def initAccumulator(board: Board): Unit =
|
||||||
|
val wkSq = wkSqOf(board)
|
||||||
|
val bkSq = bkSqOf(board)
|
||||||
System.arraycopy(model.weights(0).bias, 0, l1Stack(0), 0, accSize)
|
System.arraycopy(model.weights(0).bias, 0, l1Stack(0), 0, accSize)
|
||||||
for (sq, piece) <- board.pieces do addColumn(l1Stack(0), featureIndex(piece, squareNum(sq)))
|
for (sq, piece) <- board.pieces do addPiece(l1Stack(0), piece, squareNum(sq), wkSq, bkSq)
|
||||||
|
|
||||||
// ── Accumulator push (incremental updates) ───────────────────────────────
|
// ── Accumulator push (incremental updates) ───────────────────────────────
|
||||||
|
|
||||||
def pushAccumulator(childPly: Int, move: Move, board: Board): Unit =
|
def pushAccumulator(childPly: Int, move: Move, parentBoard: Board, childBoard: Board): Unit =
|
||||||
System.arraycopy(l1Stack(childPly - 1), 0, l1Stack(childPly), 0, accSize)
|
System.arraycopy(l1Stack(childPly - 1), 0, l1Stack(childPly), 0, accSize)
|
||||||
val l1 = l1Stack(childPly)
|
if isKingMove(move, parentBoard) then recomputeAccumulatorInto(l1Stack(childPly), childBoard)
|
||||||
move.moveType match
|
else applyNonKingDelta(l1Stack(childPly), move, parentBoard)
|
||||||
case MoveType.Normal(_) => applyNormalDelta(l1, move, board)
|
|
||||||
case MoveType.EnPassant => applyEnPassantDelta(l1, move, board)
|
private def isKingMove(move: Move, board: Board): Boolean =
|
||||||
case MoveType.CastleKingside | MoveType.CastleQueenside => applyCastleDelta(l1, move, board)
|
move.moveType == MoveType.CastleKingside ||
|
||||||
case MoveType.Promotion(p) => applyPromotionDelta(l1, move, p, board)
|
move.moveType == MoveType.CastleQueenside ||
|
||||||
|
board.pieceAt(move.from).exists(_.pieceType == PieceType.King)
|
||||||
|
|
||||||
def copyAccumulator(parentPly: Int, childPly: Int): Unit =
|
def copyAccumulator(parentPly: Int, childPly: Int): Unit =
|
||||||
System.arraycopy(l1Stack(parentPly), 0, l1Stack(childPly), 0, accSize)
|
System.arraycopy(l1Stack(parentPly), 0, l1Stack(childPly), 0, accSize)
|
||||||
|
|
||||||
def recomputeAccumulator(ply: Int, board: Board): Unit =
|
def recomputeAccumulator(ply: Int, board: Board): Unit =
|
||||||
System.arraycopy(model.weights(0).bias, 0, l1Stack(ply), 0, accSize)
|
recomputeAccumulatorInto(l1Stack(ply), board)
|
||||||
for (sq, piece) <- board.pieces do addColumn(l1Stack(ply), featureIndex(piece, squareNum(sq)))
|
|
||||||
|
private def recomputeAccumulatorInto(l1: Array[Float], board: Board): Unit =
|
||||||
|
val wkSq = wkSqOf(board)
|
||||||
|
val bkSq = bkSqOf(board)
|
||||||
|
System.arraycopy(model.weights(0).bias, 0, l1, 0, accSize)
|
||||||
|
for (sq, piece) <- board.pieces do addPiece(l1, piece, squareNum(sq), wkSq, bkSq)
|
||||||
|
|
||||||
def validateAccumulator(ply: Int, board: Board): Boolean =
|
def validateAccumulator(ply: Int, board: Board): Boolean =
|
||||||
// Compute what L1 should be from scratch
|
val expected = new Array[Float](accSize)
|
||||||
val expectedL1 = new Array[Float](accSize)
|
val wkSq = wkSqOf(board)
|
||||||
System.arraycopy(model.weights(0).bias, 0, expectedL1, 0, accSize)
|
val bkSq = bkSqOf(board)
|
||||||
for (sq, piece) <- board.pieces do addColumn(expectedL1, featureIndex(piece, squareNum(sq)))
|
System.arraycopy(model.weights(0).bias, 0, expected, 0, accSize)
|
||||||
|
for (sq, piece) <- board.pieces do addPiece(expected, piece, squareNum(sq), wkSq, bkSq)
|
||||||
// Compare with actual L1
|
|
||||||
val actual = l1Stack(ply)
|
val actual = l1Stack(ply)
|
||||||
val maxError =
|
(0 until accSize).forall(i => math.abs(actual(i) - expected(i)) < 0.001f)
|
||||||
(0 until accSize).foldLeft(0f) { (currentMax, i) =>
|
|
||||||
val error = math.abs(actual(i) - expectedL1(i))
|
|
||||||
math.max(currentMax, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxError < 0.001f // Allow small floating-point errors
|
// ── Non-king incremental deltas ──────────────────────────────────────────
|
||||||
|
|
||||||
private def applyNormalDelta(l1: Array[Float], move: Move, board: Board): Unit =
|
private def applyNonKingDelta(l1: Array[Float], move: Move, board: Board): Unit =
|
||||||
// Extract source and destination square indices early
|
val wkSq = wkSqOf(board)
|
||||||
val fromNum = squareNum(move.from)
|
val bkSq = bkSqOf(board)
|
||||||
val toNum = squareNum(move.to)
|
move.moveType match
|
||||||
|
case MoveType.Normal(_) => applyNormalDelta(l1, move, board, wkSq, bkSq)
|
||||||
|
case MoveType.EnPassant => applyEnPassantDelta(l1, move, board, wkSq, bkSq)
|
||||||
|
case MoveType.Promotion(p) => applyPromotionDelta(l1, move, p, board, wkSq, bkSq)
|
||||||
|
case _ => () // king moves handled before this point
|
||||||
|
|
||||||
// Get the moving piece
|
private def applyNormalDelta(l1: Array[Float], move: Move, board: Board, wkSq: Int, bkSq: Int): Unit =
|
||||||
board.pieceAt(move.from).foreach { mover =>
|
board.pieceAt(move.from).foreach { mover =>
|
||||||
subtractColumn(l1, featureIndex(mover, fromNum))
|
val fromNum = squareNum(move.from)
|
||||||
|
val toNum = squareNum(move.to)
|
||||||
// If there's a capture, subtract the captured piece
|
removePiece(l1, mover, fromNum, wkSq, bkSq)
|
||||||
board.pieceAt(move.to).foreach { cap =>
|
board.pieceAt(move.to).foreach(cap => removePiece(l1, cap, toNum, wkSq, bkSq))
|
||||||
subtractColumn(l1, featureIndex(cap, toNum))
|
addPiece(l1, mover, toNum, wkSq, bkSq)
|
||||||
}
|
|
||||||
|
|
||||||
// Add the piece to its new location
|
|
||||||
addColumn(l1, featureIndex(mover, toNum))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def applyEnPassantDelta(l1: Array[Float], move: Move, board: Board): Unit =
|
private def applyEnPassantDelta(l1: Array[Float], move: Move, board: Board, wkSq: Int, bkSq: Int): Unit =
|
||||||
board.pieceAt(move.from).foreach { pawn =>
|
board.pieceAt(move.from).foreach { pawn =>
|
||||||
val capturedSq = Square(move.to.file, move.from.rank)
|
val capturedSq = Square(move.to.file, move.from.rank)
|
||||||
subtractColumn(l1, featureIndex(pawn, squareNum(move.from)))
|
removePiece(l1, pawn, squareNum(move.from), wkSq, bkSq)
|
||||||
board.pieceAt(capturedSq).foreach(cap => subtractColumn(l1, featureIndex(cap, squareNum(capturedSq))))
|
board.pieceAt(capturedSq).foreach(cap => removePiece(l1, cap, squareNum(capturedSq), wkSq, bkSq))
|
||||||
addColumn(l1, featureIndex(pawn, squareNum(move.to)))
|
addPiece(l1, pawn, squareNum(move.to), wkSq, bkSq)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def applyCastleDelta(l1: Array[Float], move: Move, board: Board): Unit =
|
private def applyPromotionDelta(
|
||||||
board.pieceAt(move.from).foreach { king =>
|
l1: Array[Float],
|
||||||
val rank = move.from.rank
|
move: Move,
|
||||||
val kingside = move.moveType == MoveType.CastleKingside
|
promo: PromotionPiece,
|
||||||
val (rookFrom, rookTo) =
|
board: Board,
|
||||||
if kingside then (Square(File.H, rank), Square(File.F, rank))
|
wkSq: Int,
|
||||||
else (Square(File.A, rank), Square(File.D, rank))
|
bkSq: Int,
|
||||||
val rook = Piece(king.color, PieceType.Rook)
|
): Unit =
|
||||||
subtractColumn(l1, featureIndex(king, squareNum(move.from)))
|
|
||||||
addColumn(l1, featureIndex(king, squareNum(move.to)))
|
|
||||||
subtractColumn(l1, featureIndex(rook, squareNum(rookFrom)))
|
|
||||||
addColumn(l1, featureIndex(rook, squareNum(rookTo)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private def applyPromotionDelta(l1: Array[Float], move: Move, promo: PromotionPiece, board: Board): Unit =
|
|
||||||
board.pieceAt(move.from).foreach { pawn =>
|
board.pieceAt(move.from).foreach { pawn =>
|
||||||
val toNum = squareNum(move.to)
|
val toNum = squareNum(move.to)
|
||||||
subtractColumn(l1, featureIndex(pawn, squareNum(move.from)))
|
removePiece(l1, pawn, squareNum(move.from), wkSq, bkSq)
|
||||||
board.pieceAt(move.to).foreach(cap => subtractColumn(l1, featureIndex(cap, toNum)))
|
board.pieceAt(move.to).foreach(cap => removePiece(l1, cap, toNum, wkSq, bkSq))
|
||||||
addColumn(l1, featureIndex(Piece(pawn.color, promotedType(promo)), toNum))
|
addPiece(l1, Piece(pawn.color, promotedType(promo)), toNum, wkSq, bkSq)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def promotedType(promo: PromotionPiece): PieceType = promo match
|
private def promotedType(promo: PromotionPiece): PieceType = promo match
|
||||||
@@ -154,7 +177,6 @@ class NNUE(model: NbaiModel):
|
|||||||
score
|
score
|
||||||
|
|
||||||
def evaluateAtPlyWithValidation(ply: Int, turn: Color, hash: Long, board: Board): Int =
|
def evaluateAtPlyWithValidation(ply: Int, turn: Color, hash: Long, board: Board): Int =
|
||||||
// For debugging: validate that incremental accumulator matches recomputation
|
|
||||||
if validateAccum && ply > 0 && ply % 10 != 0 then
|
if validateAccum && ply > 0 && ply % 10 != 0 then
|
||||||
val isValid = validateAccumulator(ply, board)
|
val isValid = validateAccumulator(ply, board)
|
||||||
if !isValid then System.err.println(s"WARNING: NNUE accumulator diverged at ply $ply")
|
if !isValid then System.err.println(s"WARNING: NNUE accumulator diverged at ply $ply")
|
||||||
@@ -206,9 +228,23 @@ class NNUE(model: NbaiModel):
|
|||||||
private val legacyL1 = new Array[Float](accSize)
|
private val legacyL1 = new Array[Float](accSize)
|
||||||
|
|
||||||
def evaluate(context: GameContext): Int =
|
def evaluate(context: GameContext): Int =
|
||||||
|
// Match training: for Black-to-move positions, mirror the board (ranks flipped,
|
||||||
|
// colours swapped) so the model always sees from the side-to-move's perspective.
|
||||||
|
// The scoreFromOutput negation then converts back to White's absolute perspective.
|
||||||
|
val (wkSq, bkSq, pieces, turn) =
|
||||||
|
if context.turn == Color.Black then
|
||||||
|
val wk = flipSqNum(bkSqOf(context.board)) // flipped Black king → new "White" king
|
||||||
|
val bk = flipSqNum(wkSqOf(context.board)) // flipped White king → new "Black" king
|
||||||
|
val flipped = context.board.pieces.map { case (sq, p) =>
|
||||||
|
(sq, Piece(p.color.opposite, p.pieceType))
|
||||||
|
}
|
||||||
|
(wk, bk, flipped, Color.Black) // pass Black so scoreFromOutput negates the result
|
||||||
|
else (wkSqOf(context.board), bkSqOf(context.board), context.board.pieces, context.turn)
|
||||||
System.arraycopy(model.weights(0).bias, 0, legacyL1, 0, accSize)
|
System.arraycopy(model.weights(0).bias, 0, legacyL1, 0, accSize)
|
||||||
for (sq, piece) <- context.board.pieces do addColumn(legacyL1, featureIndex(piece, squareNum(sq)))
|
for (sq, piece) <- pieces do
|
||||||
runL2toOutput(legacyL1, context.turn)
|
val sqNum = if turn == Color.Black then flipSqNum(squareNum(sq)) else squareNum(sq)
|
||||||
|
addPiece(legacyL1, piece, sqNum, wkSq, bkSq)
|
||||||
|
runL2toOutput(legacyL1, turn)
|
||||||
|
|
||||||
def benchmark(): Unit =
|
def benchmark(): Unit =
|
||||||
val context = GameContext.initial
|
val context = GameContext.initial
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.nowchess.bot.bots.nnue
|
package de.nowchess.bot.bots.nnue
|
||||||
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.nio.file.{Files, Path}
|
||||||
import java.nio.{ByteBuffer, ByteOrder}
|
import java.nio.{ByteBuffer, ByteOrder}
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
@@ -17,13 +18,28 @@ object NbaiLoader:
|
|||||||
val weights = descs.map(_ => readLayerWeights(buf))
|
val weights = descs.map(_ => readLayerWeights(buf))
|
||||||
NbaiModel(metadata, descs, weights)
|
NbaiModel(metadata, descs, weights)
|
||||||
|
|
||||||
/** Tries /nnue_weights.nbai on the classpath; falls back to migrating /nnue_weights.bin. */
|
/** Loads weights from the `nnue.weights` system property if it points at a readable file; otherwise tries
|
||||||
|
* /nnue_weights.nbai on the classpath, falling back to migrating /nnue_weights.bin.
|
||||||
|
*/
|
||||||
def loadDefault(): NbaiModel =
|
def loadDefault(): NbaiModel =
|
||||||
Option(getClass.getResourceAsStream("/nnue_weights.nbai")) match
|
overrideModel().getOrElse {
|
||||||
case Some(s) =>
|
Option(getClass.getResourceAsStream("/nnue_weights.nbai")) match
|
||||||
|
case Some(s) =>
|
||||||
|
try load(s)
|
||||||
|
finally s.close()
|
||||||
|
case None => NbaiMigrator.migrateFromBin()
|
||||||
|
}
|
||||||
|
|
||||||
|
private def overrideModel(): Option[NbaiModel] =
|
||||||
|
sys.props
|
||||||
|
.get("nnue.weights")
|
||||||
|
.map(Path.of(_))
|
||||||
|
.filter(Files.isRegularFile(_))
|
||||||
|
.map { path =>
|
||||||
|
val s = Files.newInputStream(path)
|
||||||
try load(s)
|
try load(s)
|
||||||
finally s.close()
|
finally s.close()
|
||||||
case None => NbaiMigrator.migrateFromBin()
|
}
|
||||||
|
|
||||||
private def checkHeader(buf: ByteBuffer): Unit =
|
private def checkHeader(buf: ByteBuffer): Unit =
|
||||||
val magic = buf.getInt()
|
val magic = buf.getInt()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.eclipse.microprofile.rest.client.annotation.{RegisterClientHeaders, R
|
|||||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||||
|
|
||||||
case class SyncOfficialBotsRequest(bots: List[String])
|
case class SyncOfficialBotsRequest(bots: List[String])
|
||||||
|
case class BotTokenResponse(token: String)
|
||||||
|
|
||||||
@Path("/api/account/official-bots")
|
@Path("/api/account/official-bots")
|
||||||
@RegisterRestClient(configKey = "account-service")
|
@RegisterRestClient(configKey = "account-service")
|
||||||
@@ -18,3 +19,8 @@ trait AccountServiceClient:
|
|||||||
@Path("/sync")
|
@Path("/sync")
|
||||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
def syncBots(req: SyncOfficialBotsRequest): Unit
|
def syncBots(req: SyncOfficialBotsRequest): Unit
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{name}/token")
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def getBotToken(@PathParam("name") name: String): BotTokenResponse
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package de.nowchess.bot.config
|
||||||
|
|
||||||
|
import de.nowchess.bot.resource.{JoinTournamentRequest, JoinTournamentResponse}
|
||||||
|
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||||
|
|
||||||
|
@RegisterForReflection(
|
||||||
|
targets = Array(
|
||||||
|
classOf[JoinTournamentRequest],
|
||||||
|
classOf[JoinTournamentResponse],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class NativeReflectionConfig
|
||||||
@@ -32,6 +32,8 @@ final class AlphaBetaSearch(
|
|||||||
private val nodeCount = AtomicInteger(0)
|
private val nodeCount = AtomicInteger(0)
|
||||||
private val ordering = MoveOrdering.OrderingContext()
|
private val ordering = MoveOrdering.OrderingContext()
|
||||||
|
|
||||||
|
def lastNodeCount: Int = nodeCount.get()
|
||||||
|
|
||||||
private final case class QuiescenceNode(
|
private final case class QuiescenceNode(
|
||||||
context: GameContext,
|
context: GameContext,
|
||||||
ply: Int,
|
ply: Int,
|
||||||
@@ -47,6 +49,17 @@ final class AlphaBetaSearch(
|
|||||||
bestMove(context, maxDepth, Set.empty)
|
bestMove(context, maxDepth, Set.empty)
|
||||||
|
|
||||||
def bestMove(context: GameContext, maxDepth: Int, excludedRootMoves: Set[Move]): Option[Move] =
|
def bestMove(context: GameContext, maxDepth: Int, excludedRootMoves: Set[Move]): Option[Move] =
|
||||||
|
doDepthSearch(context, maxDepth, excludedRootMoves, Map.empty)
|
||||||
|
|
||||||
|
def bestMove(context: GameContext, maxDepth: Int, excludedRootMoves: Set[Move], hints: Map[Move, Int]): Option[Move] =
|
||||||
|
doDepthSearch(context, maxDepth, excludedRootMoves, hints)
|
||||||
|
|
||||||
|
private def doDepthSearch(
|
||||||
|
context: GameContext,
|
||||||
|
maxDepth: Int,
|
||||||
|
excludedRootMoves: Set[Move],
|
||||||
|
hints: Map[Move, Int],
|
||||||
|
): Option[Move] =
|
||||||
tt.clear()
|
tt.clear()
|
||||||
ordering.clear()
|
ordering.clear()
|
||||||
weights.initAccumulator(context)
|
weights.initAccumulator(context)
|
||||||
@@ -66,6 +79,7 @@ final class AlphaBetaSearch(
|
|||||||
ASPIRATION_DELTA,
|
ASPIRATION_DELTA,
|
||||||
rootHash,
|
rootHash,
|
||||||
excludedRootMoves,
|
excludedRootMoves,
|
||||||
|
hints,
|
||||||
)
|
)
|
||||||
(move.orElse(bestSoFar), score)
|
(move.orElse(bestSoFar), score)
|
||||||
}
|
}
|
||||||
@@ -78,6 +92,22 @@ final class AlphaBetaSearch(
|
|||||||
bestMoveWithTime(context, timeBudgetMs, Set.empty)
|
bestMoveWithTime(context, timeBudgetMs, Set.empty)
|
||||||
|
|
||||||
def bestMoveWithTime(context: GameContext, timeBudgetMs: Long, excludedRootMoves: Set[Move]): Option[Move] =
|
def bestMoveWithTime(context: GameContext, timeBudgetMs: Long, excludedRootMoves: Set[Move]): Option[Move] =
|
||||||
|
doTimedSearch(context, timeBudgetMs, excludedRootMoves, Map.empty)
|
||||||
|
|
||||||
|
def bestMoveWithTime(
|
||||||
|
context: GameContext,
|
||||||
|
timeBudgetMs: Long,
|
||||||
|
excludedRootMoves: Set[Move],
|
||||||
|
hints: Map[Move, Int],
|
||||||
|
): Option[Move] =
|
||||||
|
doTimedSearch(context, timeBudgetMs, excludedRootMoves, hints)
|
||||||
|
|
||||||
|
private def doTimedSearch(
|
||||||
|
context: GameContext,
|
||||||
|
timeBudgetMs: Long,
|
||||||
|
excludedRootMoves: Set[Move],
|
||||||
|
hints: Map[Move, Int],
|
||||||
|
): Option[Move] =
|
||||||
tt.clear()
|
tt.clear()
|
||||||
ordering.clear()
|
ordering.clear()
|
||||||
weights.initAccumulator(context)
|
weights.initAccumulator(context)
|
||||||
@@ -100,6 +130,7 @@ final class AlphaBetaSearch(
|
|||||||
ASPIRATION_DELTA,
|
ASPIRATION_DELTA,
|
||||||
rootHash,
|
rootHash,
|
||||||
excludedRootMoves,
|
excludedRootMoves,
|
||||||
|
hints,
|
||||||
)
|
)
|
||||||
loop(move.orElse(bestSoFar), score, depth + 1, depth)
|
loop(move.orElse(bestSoFar), score, depth + 1, depth)
|
||||||
|
|
||||||
@@ -124,14 +155,17 @@ final class AlphaBetaSearch(
|
|||||||
initialWindow: Int,
|
initialWindow: Int,
|
||||||
rootHash: Long,
|
rootHash: Long,
|
||||||
excludedRootMoves: Set[Move],
|
excludedRootMoves: Set[Move],
|
||||||
|
hints: Map[Move, Int],
|
||||||
): (Int, Option[Move]) =
|
): (Int, Option[Move]) =
|
||||||
val state = SearchState(rootHash, Map(rootHash -> 1))
|
val state = SearchState(rootHash, Map(rootHash -> 1))
|
||||||
|
|
||||||
@scala.annotation.tailrec
|
@scala.annotation.tailrec
|
||||||
def loop(currentAlpha: Int, currentBeta: Int, delta: Int, attempt: Int): (Int, Option[Move]) =
|
def loop(currentAlpha: Int, currentBeta: Int, delta: Int, attempt: Int): (Int, Option[Move]) =
|
||||||
if attempt >= 3 || attempt >= depth then search(context, depth, 0, Window(-INF, INF), state, excludedRootMoves)
|
if attempt >= 3 || attempt >= depth then
|
||||||
|
search(context, depth, 0, Window(-INF, INF), state, excludedRootMoves, hints)
|
||||||
else
|
else
|
||||||
val (score, move) = search(context, depth, 0, Window(currentAlpha, currentBeta), state, excludedRootMoves)
|
val (score, move) =
|
||||||
|
search(context, depth, 0, Window(currentAlpha, currentBeta), state, excludedRootMoves, hints)
|
||||||
if score > currentAlpha && score < currentBeta then (score, move)
|
if score > currentAlpha && score < currentBeta then (score, move)
|
||||||
else if score <= currentAlpha then
|
else if score <= currentAlpha then
|
||||||
loop(score - delta, currentBeta, math.min(delta * 2, ASPIRATION_DELTA_MAX), attempt + 1)
|
loop(score - delta, currentBeta, math.min(delta * 2, ASPIRATION_DELTA_MAX), attempt + 1)
|
||||||
@@ -156,12 +190,14 @@ final class AlphaBetaSearch(
|
|||||||
beta: Int,
|
beta: Int,
|
||||||
state: SearchState,
|
state: SearchState,
|
||||||
excludedRootMoves: Set[Move],
|
excludedRootMoves: Set[Move],
|
||||||
|
hints: Map[Move, Int],
|
||||||
): Option[Int] =
|
): Option[Int] =
|
||||||
val nullCtx = nullMoveContext(context)
|
val nullCtx = nullMoveContext(context)
|
||||||
val nullState = state.advance(ZobristHash.hash(nullCtx))
|
val nullState = state.advance(ZobristHash.hash(nullCtx))
|
||||||
val reductionDepth = math.max(0, depth - 1 - NULL_MOVE_R)
|
val reductionDepth = math.max(0, depth - 1 - NULL_MOVE_R)
|
||||||
weights.copyAccumulator(ply, ply + 1)
|
weights.copyAccumulator(ply, ply + 1)
|
||||||
val (score, _) = search(nullCtx, reductionDepth, ply + 1, Window(-beta, -beta + 1), nullState, excludedRootMoves)
|
val (score, _) =
|
||||||
|
search(nullCtx, reductionDepth, ply + 1, Window(-beta, -beta + 1), nullState, excludedRootMoves, hints)
|
||||||
if -score >= beta then Some(beta) else None
|
if -score >= beta then Some(beta) else None
|
||||||
|
|
||||||
/** Negamax alpha-beta search returning (score, best move). */
|
/** Negamax alpha-beta search returning (score, best move). */
|
||||||
@@ -172,8 +208,9 @@ final class AlphaBetaSearch(
|
|||||||
window: Window,
|
window: Window,
|
||||||
state: SearchState,
|
state: SearchState,
|
||||||
excludedRootMoves: Set[Move],
|
excludedRootMoves: Set[Move],
|
||||||
|
hints: Map[Move, Int],
|
||||||
): (Int, Option[Move]) =
|
): (Int, Option[Move]) =
|
||||||
val params = SearchParams(context, depth, ply, window, state, excludedRootMoves)
|
val params = SearchParams(context, depth, ply, window, state, excludedRootMoves, hints)
|
||||||
searchNode(params)
|
searchNode(params)
|
||||||
|
|
||||||
private def searchNode(params: SearchParams): (Int, Option[Move]) =
|
private def searchNode(params: SearchParams): (Int, Option[Move]) =
|
||||||
@@ -235,13 +272,14 @@ final class AlphaBetaSearch(
|
|||||||
params.window.beta,
|
params.window.beta,
|
||||||
params.state,
|
params.state,
|
||||||
params.excludedRootMoves,
|
params.excludedRootMoves,
|
||||||
|
params.rootHints,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.flatten
|
.flatten
|
||||||
|
|
||||||
nullResult.map((_, None)).getOrElse {
|
nullResult.map((_, None)).getOrElse {
|
||||||
val ttBest = tt.probe(params.state.hash).flatMap(_.bestMove)
|
val ttBest = tt.probe(params.state.hash).flatMap(_.bestMove)
|
||||||
val ordered = MoveOrdering.sort(params.context, legalMoves, ttBest, params.ply, ordering)
|
val ordered = MoveOrdering.sort(params.context, legalMoves, ttBest, params.ply, ordering, params.rootHints)
|
||||||
searchSequential(
|
searchSequential(
|
||||||
params.context,
|
params.context,
|
||||||
params.depth,
|
params.depth,
|
||||||
@@ -250,6 +288,7 @@ final class AlphaBetaSearch(
|
|||||||
ordered,
|
ordered,
|
||||||
params.state,
|
params.state,
|
||||||
params.excludedRootMoves,
|
params.excludedRootMoves,
|
||||||
|
params.rootHints,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +319,7 @@ final class AlphaBetaSearch(
|
|||||||
Window(-a - 1, -a),
|
Window(-a - 1, -a),
|
||||||
childState,
|
childState,
|
||||||
params.excludedRootMoves,
|
params.excludedRootMoves,
|
||||||
|
params.rootHints,
|
||||||
)
|
)
|
||||||
val s = -rs
|
val s = -rs
|
||||||
if s > a then
|
if s > a then
|
||||||
@@ -290,6 +330,7 @@ final class AlphaBetaSearch(
|
|||||||
Window(betaNeg, -a),
|
Window(betaNeg, -a),
|
||||||
childState,
|
childState,
|
||||||
params.excludedRootMoves,
|
params.excludedRootMoves,
|
||||||
|
params.rootHints,
|
||||||
)
|
)
|
||||||
-fs
|
-fs
|
||||||
else s
|
else s
|
||||||
@@ -301,6 +342,7 @@ final class AlphaBetaSearch(
|
|||||||
Window(betaNeg, -a),
|
Window(betaNeg, -a),
|
||||||
childState,
|
childState,
|
||||||
params.excludedRootMoves,
|
params.excludedRootMoves,
|
||||||
|
params.rootHints,
|
||||||
)
|
)
|
||||||
-rs
|
-rs
|
||||||
|
|
||||||
@@ -364,8 +406,9 @@ final class AlphaBetaSearch(
|
|||||||
ordered: List[Move],
|
ordered: List[Move],
|
||||||
state: SearchState,
|
state: SearchState,
|
||||||
excludedRootMoves: Set[Move],
|
excludedRootMoves: Set[Move],
|
||||||
|
rootHints: Map[Move, Int] = Map.empty,
|
||||||
): (Int, Option[Move]) =
|
): (Int, Option[Move]) =
|
||||||
val params = SearchParams(context, depth, ply, window, state, excludedRootMoves)
|
val params = SearchParams(context, depth, ply, window, state, excludedRootMoves, rootHints)
|
||||||
val (bestMove, bestScore, cutoff) = searchLoop(0, 0, LoopAcc(None, -INF, window.alpha), params, ordered)
|
val (bestMove, bestScore, cutoff) = searchLoop(0, 0, LoopAcc(None, -INF, window.alpha), params, ordered)
|
||||||
val flag =
|
val flag =
|
||||||
if cutoff then TTFlag.Lower
|
if cutoff then TTFlag.Lower
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ object MoveOrdering:
|
|||||||
ttBestMove: Option[Move],
|
ttBestMove: Option[Move],
|
||||||
ply: Int = 0,
|
ply: Int = 0,
|
||||||
ordering: OrderingContext = new OrderingContext(),
|
ordering: OrderingContext = new OrderingContext(),
|
||||||
|
rootHints: Map[Move, Int] = Map.empty,
|
||||||
): Int =
|
): Int =
|
||||||
if ttBestMove.exists(m => m.from == move.from && m.to == move.to) then Int.MaxValue
|
if ttBestMove.exists(m => m.from == move.from && m.to == move.to) then Int.MaxValue
|
||||||
|
else if ply == 0 && rootHints.nonEmpty then rootHints.getOrElse(move, Int.MinValue / 2)
|
||||||
else
|
else
|
||||||
move.moveType match
|
move.moveType match
|
||||||
case MoveType.Promotion(PromotionPiece.Queen) =>
|
case MoveType.Promotion(PromotionPiece.Queen) =>
|
||||||
@@ -56,8 +58,9 @@ object MoveOrdering:
|
|||||||
ttBestMove: Option[Move],
|
ttBestMove: Option[Move],
|
||||||
ply: Int = 0,
|
ply: Int = 0,
|
||||||
ordering: OrderingContext = new OrderingContext(),
|
ordering: OrderingContext = new OrderingContext(),
|
||||||
|
rootHints: Map[Move, Int] = Map.empty,
|
||||||
): List[Move] =
|
): List[Move] =
|
||||||
moves.sortBy(m => -score(context, m, ttBestMove, ply, ordering))
|
moves.sortBy(m => -score(context, m, ttBestMove, ply, ordering, rootHints))
|
||||||
|
|
||||||
private def scoreQuietMove(move: Move, ply: Int, ordering: OrderingContext): Int =
|
private def scoreQuietMove(move: Move, ply: Int, ordering: OrderingContext): Int =
|
||||||
val isKiller = ordering.getKillerMoves(ply).exists(k => k.from == move.from && k.to == move.to)
|
val isKiller = ordering.getKillerMoves(ply).exists(k => k.from == move.from && k.to == move.to)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ final case class SearchParams(
|
|||||||
window: Window,
|
window: Window,
|
||||||
state: SearchState,
|
state: SearchState,
|
||||||
excludedRootMoves: Set[Move],
|
excludedRootMoves: Set[Move],
|
||||||
|
rootHints: Map[Move, Int] = Map.empty,
|
||||||
)
|
)
|
||||||
|
|
||||||
final case class SearchState(hash: Long, repetitions: Map[Long, Int]):
|
final case class SearchState(hash: Long, repetitions: Map[Long, Int]):
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ package de.nowchess.bot.resource
|
|||||||
|
|
||||||
case class JoinTournamentRequest(
|
case class JoinTournamentRequest(
|
||||||
tournamentId: String,
|
tournamentId: String,
|
||||||
botToken: String,
|
botToken: Option[String],
|
||||||
difficulty: String,
|
difficulty: String,
|
||||||
serverUrl: Option[String],
|
serverUrl: Option[String],
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-5
@@ -25,20 +25,18 @@ class TournamentJoinResource:
|
|||||||
@POST
|
@POST
|
||||||
@Path("/join-tournament")
|
@Path("/join-tournament")
|
||||||
def joinTournament(req: JoinTournamentRequest): Response =
|
def joinTournament(req: JoinTournamentRequest): Response =
|
||||||
val serverUrl = req.serverUrl.filter(_.nonEmpty).getOrElse(player.defaultServerUrl)
|
|
||||||
val difficulty = if req.difficulty.nonEmpty then req.difficulty else "medium"
|
val difficulty = if req.difficulty.nonEmpty then req.difficulty else "medium"
|
||||||
log.infof(
|
log.infof(
|
||||||
"Official bot join requested — tournament=%s difficulty=%s server=%s",
|
"Official bot join requested — tournament=%s difficulty=%s",
|
||||||
req.tournamentId,
|
req.tournamentId,
|
||||||
difficulty,
|
difficulty,
|
||||||
serverUrl,
|
|
||||||
)
|
)
|
||||||
player.joinTournament(req.tournamentId, req.botToken, difficulty, serverUrl) match
|
player.joinTournament(req.tournamentId, req.botToken, difficulty) match
|
||||||
case Right(botId) =>
|
case Right(botId) =>
|
||||||
val resp = JoinTournamentResponse(botId, difficulty, "joining")
|
val resp = JoinTournamentResponse(botId, difficulty, "joining")
|
||||||
Response.ok(resp).build()
|
Response.ok(resp).build()
|
||||||
case Left(err) =>
|
case Left(err) =>
|
||||||
Response
|
Response
|
||||||
.status(Response.Status.BAD_GATEWAY)
|
.status(Response.Status.BAD_REQUEST)
|
||||||
.entity(s"""{"error":"$err"}""")
|
.entity(s"""{"error":"$err"}""")
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package de.nowchess.bot.selfplay
|
||||||
|
|
||||||
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.api.move.Move
|
||||||
|
import de.nowchess.api.rules.RuleSet
|
||||||
|
import de.nowchess.bot.BotDifficulty
|
||||||
|
import de.nowchess.bot.bots.NNUEBot
|
||||||
|
import de.nowchess.io.fen.FenExporter
|
||||||
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
|
|
||||||
|
import java.io.{BufferedWriter, FileWriter}
|
||||||
|
import java.nio.file.{Files, Path}
|
||||||
|
import scala.collection.mutable
|
||||||
|
import scala.util.Random
|
||||||
|
|
||||||
|
/** Standalone self-play harness. Runs NNUEBot against itself from randomised openings and writes the visited positions
|
||||||
|
* as one FEN per line — the input format expected by the Python labeler. No microservices.
|
||||||
|
*
|
||||||
|
* Games run sequentially because EvaluationNNUE holds a shared accumulator; the small per-move time budget keeps
|
||||||
|
* throughput high. Stockfish relabels every position later, so shallow self-play search is sufficient.
|
||||||
|
*/
|
||||||
|
object SelfPlayMain:
|
||||||
|
|
||||||
|
private case class Config(
|
||||||
|
games: Int = 500,
|
||||||
|
out: String = "modules/official-bots/python/data/selfplay.txt",
|
||||||
|
weights: Option[String] = None,
|
||||||
|
moveTimeMs: Long = 50L,
|
||||||
|
randomPlies: Int = 8,
|
||||||
|
maxPlies: Int = 200,
|
||||||
|
seed: Long = System.nanoTime(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def main(args: Array[String]): Unit =
|
||||||
|
val config = parse(args.toList, Config())
|
||||||
|
config.weights.foreach(System.setProperty("nnue.weights", _))
|
||||||
|
|
||||||
|
val rules = DefaultRules
|
||||||
|
val bot = NNUEBot(BotDifficulty.Hard, rules, fixedMoveTimeMs = Some(config.moveTimeMs))
|
||||||
|
val rng = new Random(config.seed)
|
||||||
|
val seen = mutable.HashSet.empty[String]
|
||||||
|
|
||||||
|
Files.createDirectories(Path.of(config.out).toAbsolutePath.getParent)
|
||||||
|
val writer = new BufferedWriter(new FileWriter(config.out))
|
||||||
|
try
|
||||||
|
var game = 0
|
||||||
|
while game < config.games do
|
||||||
|
playGame(rules, bot, rng, config, seen, writer)
|
||||||
|
game += 1
|
||||||
|
if game % 25 == 0 then
|
||||||
|
writer.flush()
|
||||||
|
println(s"games=$game/${config.games} positions=${seen.size}")
|
||||||
|
finally writer.close()
|
||||||
|
println(s"Done. ${seen.size} unique positions -> ${config.out}")
|
||||||
|
|
||||||
|
private def playGame(
|
||||||
|
rules: RuleSet,
|
||||||
|
bot: GameContext => Option[Move],
|
||||||
|
rng: Random,
|
||||||
|
config: Config,
|
||||||
|
seen: mutable.HashSet[String],
|
||||||
|
writer: BufferedWriter,
|
||||||
|
): Unit =
|
||||||
|
randomOpening(rules, rng, config.randomPlies, GameContext.initial) match
|
||||||
|
case None => ()
|
||||||
|
case Some(start) =>
|
||||||
|
var ctx = start
|
||||||
|
var plies = config.randomPlies
|
||||||
|
var live = true
|
||||||
|
while live && plies < config.maxPlies do
|
||||||
|
if isTerminal(rules, ctx) then live = false
|
||||||
|
else
|
||||||
|
bot(ctx) match
|
||||||
|
case None => live = false
|
||||||
|
case Some(move) =>
|
||||||
|
ctx = rules.applyMove(ctx)(move)
|
||||||
|
plies += 1
|
||||||
|
record(rules, ctx, seen, writer)
|
||||||
|
|
||||||
|
private def randomOpening(rules: RuleSet, rng: Random, plies: Int, start: GameContext): Option[GameContext] =
|
||||||
|
var ctx = start
|
||||||
|
var i = 0
|
||||||
|
while i < plies do
|
||||||
|
val legal = rules.allLegalMoves(ctx)
|
||||||
|
if legal.isEmpty then return None
|
||||||
|
ctx = rules.applyMove(ctx)(legal(rng.nextInt(legal.size)))
|
||||||
|
i += 1
|
||||||
|
Some(ctx)
|
||||||
|
|
||||||
|
private def record(rules: RuleSet, ctx: GameContext, seen: mutable.HashSet[String], writer: BufferedWriter): Unit =
|
||||||
|
if !rules.isCheck(ctx) && !isTerminal(rules, ctx) then
|
||||||
|
val fen = FenExporter.gameContextToFen(ctx)
|
||||||
|
if seen.add(fen) then
|
||||||
|
writer.write(fen)
|
||||||
|
writer.newLine()
|
||||||
|
|
||||||
|
private def isTerminal(rules: RuleSet, ctx: GameContext): Boolean =
|
||||||
|
rules.allLegalMoves(ctx).isEmpty ||
|
||||||
|
rules.isInsufficientMaterial(ctx) ||
|
||||||
|
rules.isFiftyMoveRule(ctx) ||
|
||||||
|
rules.isThreefoldRepetition(ctx)
|
||||||
|
|
||||||
|
private def parse(args: List[String], acc: Config): Config = args match
|
||||||
|
case "--games" :: v :: rest => parse(rest, acc.copy(games = v.toInt))
|
||||||
|
case "--out" :: v :: rest => parse(rest, acc.copy(out = v))
|
||||||
|
case "--weights" :: v :: rest => parse(rest, acc.copy(weights = Some(v)))
|
||||||
|
case "--move-ms" :: v :: rest => parse(rest, acc.copy(moveTimeMs = v.toLong))
|
||||||
|
case "--random-plies" :: v :: rest => parse(rest, acc.copy(randomPlies = v.toInt))
|
||||||
|
case "--max-plies" :: v :: rest => parse(rest, acc.copy(maxPlies = v.toInt))
|
||||||
|
case "--seed" :: v :: rest => parse(rest, acc.copy(seed = v.toLong))
|
||||||
|
case Nil => acc
|
||||||
|
case unknown :: rest => println(s"Ignoring unknown arg: $unknown"); parse(rest, acc)
|
||||||
+8
-4
@@ -16,13 +16,17 @@ object TournamentBotConfig:
|
|||||||
private val mapper = new ObjectMapper()
|
private val mapper = new ObjectMapper()
|
||||||
|
|
||||||
def fromEnv(env: Map[String, String]): Option[TournamentBotConfig] =
|
def fromEnv(env: Map[String, String]): Option[TournamentBotConfig] =
|
||||||
|
fromEnvWithToken(env, None)
|
||||||
|
|
||||||
|
def fromEnvWithToken(env: Map[String, String], resolvedToken: Option[String]): Option[TournamentBotConfig] =
|
||||||
|
val token = env.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty).orElse(resolvedToken)
|
||||||
for
|
for
|
||||||
tournamentId <- env.get("TOURNAMENT_ID").filter(_.nonEmpty)
|
tournamentId <- env.get("TOURNAMENT_ID").filter(_.nonEmpty)
|
||||||
token <- env.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty)
|
tok <- token
|
||||||
botId <- jwtSubject(token)
|
botId <- jwtSubject(tok)
|
||||||
serverUrl = env.getOrElse("TOURNAMENT_SERVER_URL", "http://141.37.123.132:8086")
|
serverUrl = env.getOrElse("TOURNAMENT_SERVICE_URL", "http://localhost:8086")
|
||||||
difficulty = env.getOrElse("TOURNAMENT_BOT_DIFFICULTY", "medium")
|
difficulty = env.getOrElse("TOURNAMENT_BOT_DIFFICULTY", "medium")
|
||||||
yield TournamentBotConfig(serverUrl, tournamentId, token, botId, difficulty)
|
yield TournamentBotConfig(serverUrl, tournamentId, tok, botId, difficulty)
|
||||||
|
|
||||||
def jwtSubject(token: String): Option[String] =
|
def jwtSubject(token: String): Option[String] =
|
||||||
Try {
|
Try {
|
||||||
|
|||||||
+331
-105
@@ -3,13 +3,17 @@ package de.nowchess.bot.service
|
|||||||
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
|
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.bot.{Bot, BotController}
|
import de.nowchess.bot.{Bot, BotController}
|
||||||
|
import de.nowchess.bot.client.AccountServiceClient
|
||||||
|
import de.nowchess.bot.config.RedisConfig
|
||||||
import de.nowchess.io.fen.FenParser
|
import de.nowchess.io.fen.FenParser
|
||||||
|
import io.quarkus.redis.datasource.RedisDataSource
|
||||||
import io.quarkus.runtime.Startup
|
import io.quarkus.runtime.Startup
|
||||||
import jakarta.annotation.{PostConstruct, PreDestroy}
|
import jakarta.annotation.{PostConstruct, PreDestroy}
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import jakarta.ws.rs.client.{Client, ClientBuilder, Entity}
|
import jakarta.ws.rs.client.{Client, ClientBuilder, Entity}
|
||||||
import jakarta.ws.rs.core.MediaType
|
import jakarta.ws.rs.core.MediaType
|
||||||
|
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||||
import org.jboss.logging.Logger
|
import org.jboss.logging.Logger
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
import scala.jdk.CollectionConverters.*
|
import scala.jdk.CollectionConverters.*
|
||||||
@@ -24,73 +28,291 @@ class TournamentBotGamePlayer:
|
|||||||
private val log = Logger.getLogger(classOf[TournamentBotGamePlayer])
|
private val log = Logger.getLogger(classOf[TournamentBotGamePlayer])
|
||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||||
@Inject var botController: BotController = uninitialized
|
@Inject var botController: BotController = uninitialized
|
||||||
|
@Inject var redis: RedisDataSource = uninitialized
|
||||||
|
@Inject var redisConfig: RedisConfig = uninitialized
|
||||||
|
@Inject @RestClient var accountServiceClient: AccountServiceClient = uninitialized
|
||||||
// scalafix:on DisableSyntax.var
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
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 config = TournamentBotConfig.fromEnv(System.getenv().asScala.toMap)
|
private val hardestDifficulty = "expert"
|
||||||
|
private val autoJoinIntervalMs = 15000L
|
||||||
|
|
||||||
|
private val gameTerminalStatuses =
|
||||||
|
Set("checkmate", "stalemate", "draw", "resigned", "timeout", "aborted", "finished")
|
||||||
|
|
||||||
// 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 defaultServerUrl: String =
|
val tournamentServiceUrl: String =
|
||||||
System.getenv().asScala.getOrElse("TOURNAMENT_SERVER_URL", "http://141.37.123.132: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 =
|
||||||
parkOnStartup()
|
val env = System.getenv().asScala.toMap
|
||||||
config match
|
val difficulty = env.getOrElse("TOURNAMENT_BOT_DIFFICULTY", "medium")
|
||||||
|
val token = resolveToken(difficulty)
|
||||||
|
parkOnStartup(token)
|
||||||
|
TournamentBotConfig.fromEnvWithToken(env, token) match
|
||||||
case None =>
|
case None =>
|
||||||
log.info("Tournament bot disabled — set TOURNAMENT_ID and TOURNAMENT_BOT_TOKEN to enable")
|
log.info("Tournament bot disabled — set TOURNAMENT_ID to enable")
|
||||||
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 parkOnStartup(): Unit =
|
private def startAutoJoin(): Unit =
|
||||||
park(defaultServerUrl, "expert") match
|
val thread = new Thread(() => autoJoinLoop(), "TournamentBot-auto-join")
|
||||||
case Some(id) => log.infof("Parked expert bot on %s as id %s", defaultServerUrl, id)
|
thread.setDaemon(true)
|
||||||
case None => log.warnf("Failed to park expert bot on %s", defaultServerUrl)
|
thread.start()
|
||||||
|
log.infof("Auto-join enabled — server=%s difficulty=%s", autoJoinServerUrl, hardestDifficulty)
|
||||||
|
|
||||||
private def park(serverUrl: String, difficulty: String): Option[String] =
|
private def autoJoinLoop(): Unit =
|
||||||
System.getenv().asScala.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty).flatMap { token =>
|
while running do
|
||||||
Try {
|
Try(autoJoinScan()).failed.foreach(ex => log.warnf(ex, "Auto-join scan failed"))
|
||||||
val body = s"""{"name":"${botName(difficulty)}"}"""
|
sleep(autoJoinIntervalMs)
|
||||||
val response = client
|
|
||||||
.target(serverUrl)
|
private def autoJoinScan(): Unit =
|
||||||
.path("api")
|
resolveAutoJoinToken().foreach { token =>
|
||||||
.path("bots")
|
TournamentBotConfig.jwtSubject(token).foreach { botId =>
|
||||||
.request(MediaType.APPLICATION_JSON)
|
val open = openTournaments()
|
||||||
.header("Authorization", s"Bearer $token")
|
log.infof("Auto-join scan — server=%s open tournaments=%d bot=%s", autoJoinServerUrl, open.size, botId)
|
||||||
.post(Entity.entity(body, MediaType.APPLICATION_JSON))
|
open.foreach { tournamentId =>
|
||||||
if response.getStatus == 201 || response.getStatus == 200 then
|
if joinedTournaments.add(tournamentId) then
|
||||||
val id = objectMapper.readTree(response.readEntity(classOf[String])).path("id").asText()
|
val cfg = TournamentBotConfig(autoJoinServerUrl, tournamentId, token, botId, hardestDifficulty)
|
||||||
response.close()
|
if !joinedOrParticipating(cfg) then joinedTournaments.remove(tournamentId)
|
||||||
Option(id).filter(_.nonEmpty)
|
}
|
||||||
else { log.warnf("Parking bot %s returned status %d", botName(difficulty), response.getStatus); response.close(); None }
|
playPendingGames(token, botId)
|
||||||
}.getOrElse(None)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The tournament-server does not reliably replay gameStart to late subscribers, so we cannot
|
||||||
|
// depend on the event stream to discover games. Poll each joined tournament for our active game.
|
||||||
|
private def playPendingGames(token: String, botId: String): Unit =
|
||||||
|
joinedTournaments.forEach { tournamentId =>
|
||||||
|
val cfg = TournamentBotConfig(autoJoinServerUrl, tournamentId, token, botId, hardestDifficulty)
|
||||||
|
pendingGame(cfg).foreach { (gameId, color) =>
|
||||||
|
if activeGames.add(gameId) then
|
||||||
|
log.infof("Polled active game %s as %s in tournament %s", gameId, color, tournamentId)
|
||||||
|
workers.submit(new Runnable { def run(): Unit = playGame(cfg, gameId, color) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def pendingGame(cfg: TournamentBotConfig): Option[(String, String)] =
|
||||||
|
for
|
||||||
|
detail <- fetchJson(cfg, target(cfg))
|
||||||
|
if detail.path("status").asText() == "started"
|
||||||
|
round = detail.path("round").asInt(0)
|
||||||
|
if round > 0
|
||||||
|
pairings <- fetchJson(cfg, target(cfg).path("round").path(round.toString)).map(_.path("pairings"))
|
||||||
|
result <- findBotGame(pairings, cfg.botId)
|
||||||
|
yield result
|
||||||
|
|
||||||
|
private def findBotGame(pairings: JsonNode, botId: String): Option[(String, String)] =
|
||||||
|
pairings
|
||||||
|
.elements()
|
||||||
|
.asScala
|
||||||
|
.flatMap { p =>
|
||||||
|
val whiteId = p.path("white").path("id").asText()
|
||||||
|
val blackId = p.path("black").path("id").asText()
|
||||||
|
val color = if whiteId == botId then Some("white") else if blackId == botId then Some("black") else None
|
||||||
|
color.flatMap(c => activeMatch(p.path("matches")).map(gameId => (gameId, c)))
|
||||||
|
}
|
||||||
|
.nextOption()
|
||||||
|
|
||||||
|
private def activeMatch(matches: JsonNode): Option[String] =
|
||||||
|
matches
|
||||||
|
.elements()
|
||||||
|
.asScala
|
||||||
|
.find(m => m.path("gameId").asText().nonEmpty && !(m.has("outcome") && !m.path("outcome").isNull))
|
||||||
|
.map(_.path("gameId").asText())
|
||||||
|
|
||||||
|
private def fetchJson(cfg: TournamentBotConfig, t: jakarta.ws.rs.client.WebTarget): Option[JsonNode] =
|
||||||
|
Try {
|
||||||
|
val response = authed(cfg, t).get()
|
||||||
|
try
|
||||||
|
if response.getStatus == 200 then Some(objectMapper.readTree(response.readEntity(classOf[String])))
|
||||||
|
else None
|
||||||
|
finally response.close()
|
||||||
|
}.getOrElse(None)
|
||||||
|
|
||||||
|
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] =
|
||||||
|
val name = botName(difficulty)
|
||||||
|
val redisKey = s"${redisConfig.prefix}:tournament-bot:token:$name"
|
||||||
|
fetchTokenFromAccountService(name)
|
||||||
|
.orElse(registerWithServer(tournamentServiceUrl, name))
|
||||||
|
.map { token =>
|
||||||
|
redis.value(classOf[String]).set(redisKey, token)
|
||||||
|
log.infof("Refreshed bot token for %s — stored in Redis", name)
|
||||||
|
token
|
||||||
|
}
|
||||||
|
.orElse {
|
||||||
|
Option(redis.value(classOf[String]).get(redisKey)).filter(_.nonEmpty).map { token =>
|
||||||
|
log.infof("Using cached bot token for %s from Redis", name)
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.orElse {
|
||||||
|
System.getenv().asScala.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty).map { token =>
|
||||||
|
log.infof("Using TOURNAMENT_BOT_TOKEN env var for %s", name)
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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] =
|
||||||
|
Try(accountServiceClient.getBotToken(name).token).toOption
|
||||||
|
.filter(_.nonEmpty)
|
||||||
|
.orElse {
|
||||||
|
Try {
|
||||||
|
val allNames = BotController.listBots.map(botName)
|
||||||
|
accountServiceClient.syncBots(de.nowchess.bot.client.SyncOfficialBotsRequest(allNames))
|
||||||
|
accountServiceClient.getBotToken(name).token
|
||||||
|
}.toOption.filter(_.nonEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def parkOnStartup(token: Option[String]): Unit =
|
||||||
|
val localAccountUrl = System.getenv().asScala.getOrElse("ACCOUNT_SERVICE_URL", "http://localhost:8083")
|
||||||
|
token match
|
||||||
|
case None => log.warn("No bot token resolved — skipping local park")
|
||||||
|
case Some(tok) =>
|
||||||
|
BotController.listBots.foreach(diff => parkOnAccountService(localAccountUrl, diff, tok))
|
||||||
|
fetchRemoteServers().foreach { serverUrl =>
|
||||||
|
BotController.listBots.foreach { diff =>
|
||||||
|
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] =
|
||||||
|
Try {
|
||||||
|
val response = client
|
||||||
|
.target(tournamentServiceUrl)
|
||||||
|
.path("api")
|
||||||
|
.path("tournament")
|
||||||
|
.path("servers")
|
||||||
|
.request(MediaType.APPLICATION_JSON)
|
||||||
|
.get()
|
||||||
|
if response.getStatus == 200 then
|
||||||
|
val node = objectMapper.readTree(response.readEntity(classOf[String]))
|
||||||
|
response.close()
|
||||||
|
node.path("servers").elements().asScala.toList.map(_.path("url").asText()).filter(_.nonEmpty)
|
||||||
|
else { response.close(); Nil }
|
||||||
|
}.getOrElse(Nil)
|
||||||
|
|
||||||
|
private def parkOnAccountService(serverUrl: String, difficulty: String, token: String): Unit =
|
||||||
|
Try {
|
||||||
|
val body = s"""{"name":"${botName(difficulty)}"}"""
|
||||||
|
val response = client
|
||||||
|
.target(serverUrl)
|
||||||
|
.path("api")
|
||||||
|
.path("account")
|
||||||
|
.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 %s as id %s", botName(difficulty), serverUrl, id)
|
||||||
|
else log.warnf("Park %s on %s returned status %d", botName(difficulty), serverUrl, response.getStatus)
|
||||||
|
response.close()
|
||||||
|
}.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(
|
||||||
tournamentId: String,
|
tournamentId: String,
|
||||||
botToken: String,
|
botToken: Option[String],
|
||||||
difficulty: String,
|
difficulty: String,
|
||||||
serverUrl: String,
|
|
||||||
): Either[String, String] =
|
): Either[String, String] =
|
||||||
TournamentBotConfig.jwtSubject(botToken) match
|
val redisKey = s"${redisConfig.prefix}:tournament-bot:token:${botName(difficulty)}"
|
||||||
case None => Left("Invalid bot token — could not extract subject")
|
val resolvedToken = botToken
|
||||||
case Some(botId) =>
|
.filter(_.nonEmpty)
|
||||||
val cfg = TournamentBotConfig(serverUrl, tournamentId, botToken, botId, difficulty)
|
.orElse(Option(redis.value(classOf[String]).get(redisKey)).filter(_.nonEmpty))
|
||||||
if join(cfg) then
|
.orElse(resolveToken(difficulty))
|
||||||
startAsync(cfg)
|
resolvedToken match
|
||||||
Right(botId)
|
case None => Left("No bot token provided and TOURNAMENT_BOT_TOKEN not configured")
|
||||||
else Left("Failed to join tournament")
|
case Some(token) =>
|
||||||
|
TournamentBotConfig.jwtSubject(token) match
|
||||||
|
case None => Left("Invalid bot token — could not extract subject")
|
||||||
|
case Some(botId) =>
|
||||||
|
val cfg = TournamentBotConfig(tournamentServiceUrl, tournamentId, token, botId, difficulty)
|
||||||
|
if joinedOrParticipating(cfg) then
|
||||||
|
startAsync(cfg)
|
||||||
|
Right(botId)
|
||||||
|
else Left("Failed to join tournament")
|
||||||
|
|
||||||
private def startAsync(cfg: TournamentBotConfig): Unit =
|
private def startAsync(cfg: TournamentBotConfig): Unit =
|
||||||
val thread = new Thread(() => streamLoop(cfg), s"TournamentBot-${cfg.tournamentId}")
|
val thread = new Thread(() => streamLoop(cfg), s"TournamentBot-${cfg.tournamentId}")
|
||||||
@@ -110,15 +332,18 @@ class TournamentBotGamePlayer:
|
|||||||
case Failure(ex) => log.warnf(ex, "Tournament event stream dropped — reconnecting"); sleep(5000)
|
case Failure(ex) => log.warnf(ex, "Tournament event stream dropped — reconnecting"); sleep(5000)
|
||||||
case Success(_) => sleep(2000)
|
case Success(_) => sleep(2000)
|
||||||
|
|
||||||
private def join(cfg: TournamentBotConfig): Boolean =
|
// 200 = joined, 409 = already a participant (e.g. after a restart) — both mean "play this tournament".
|
||||||
|
private def joinedOrParticipating(cfg: TournamentBotConfig): Boolean =
|
||||||
Try {
|
Try {
|
||||||
val response = authed(cfg, target(cfg).path("join"))
|
val response = authed(cfg, target(cfg).path("join"))
|
||||||
.post(Entity.entity("", MediaType.APPLICATION_JSON))
|
.post(Entity.entity("", MediaType.APPLICATION_JSON))
|
||||||
val ok = response.getStatus == 200
|
val status = response.getStatus
|
||||||
if ok then log.infof("Joined tournament %s", cfg.tournamentId)
|
|
||||||
else log.errorf("Failed to join tournament %s — status %d", cfg.tournamentId, response.getStatus)
|
|
||||||
response.close()
|
response.close()
|
||||||
ok
|
status match
|
||||||
|
case 200 => log.infof("Joined tournament %s", cfg.tournamentId); true
|
||||||
|
case 409 => log.infof("Already in tournament %s — resuming", cfg.tournamentId); true
|
||||||
|
case other =>
|
||||||
|
log.errorf("Failed to join tournament %s — status %d", cfg.tournamentId, other); false
|
||||||
}.getOrElse { log.error("Join request failed"); false }
|
}.getOrElse { log.error("Join request failed"); false }
|
||||||
|
|
||||||
private def streamEvents(cfg: TournamentBotConfig): Unit =
|
private def streamEvents(cfg: TournamentBotConfig): Unit =
|
||||||
@@ -133,69 +358,79 @@ class TournamentBotGamePlayer:
|
|||||||
log.infof("Listening to tournament %s event stream", cfg.tournamentId)
|
log.infof("Listening to tournament %s event stream", cfg.tournamentId)
|
||||||
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())
|
||||||
onGameStart(cfg, node.path("gameId").asText(), node.path("color").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
|
||||||
()
|
log.infof("gameStart received — tournament=%s game=%s bot=%s", cfg.tournamentId, gameId, cfg.botId)
|
||||||
|
resolveColor(cfg, gameId) match
|
||||||
|
case None => log.infof("Skipping game %s — bot %s is not a participant", gameId, cfg.botId)
|
||||||
|
case Some(color) =>
|
||||||
|
if activeGames.add(gameId) then
|
||||||
|
log.infof("Joining game %s as %s", gameId, color)
|
||||||
|
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 {
|
||||||
log.infof("Playing game %s as %s", gameId, color)
|
log.infof("Playing game %s as %s", gameId, color)
|
||||||
openGameStream(cfg, gameId).foreach(consumeGameStream(cfg, gameId, color, _))
|
pollGameLoop(cfg, gameId, color)
|
||||||
activeGames.remove(gameId)
|
activeGames.remove(gameId)
|
||||||
} match
|
} match
|
||||||
case Failure(ex) => log.errorf(ex, "Game %s crashed", gameId); activeGames.remove(gameId)
|
case Failure(ex) => log.errorf(ex, "Game %s crashed", gameId); activeGames.remove(gameId)
|
||||||
case Success(_) => ()
|
case Success(_) => ()
|
||||||
|
|
||||||
private def consumeGameStream(cfg: TournamentBotConfig, gameId: String, color: String, stream: InputStream): Unit =
|
// The native JAX-RS client buffers streaming responses, so reading the NDJSON game stream blocks
|
||||||
val reader = new BufferedReader(new InputStreamReader(stream))
|
// forever. Poll the game state with plain GETs (which work) and move when it is our turn.
|
||||||
|
private def pollGameLoop(cfg: TournamentBotConfig, gameId: String, color: String): Unit =
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
var done = false
|
var done = false
|
||||||
|
var lastFen = ""
|
||||||
// scalafix:on DisableSyntax.var
|
// scalafix:on DisableSyntax.var
|
||||||
Iterator
|
while running && !done do
|
||||||
.continually(reader.readLine())
|
fetchJson(cfg, target(cfg).path("game").path(gameId)) match
|
||||||
.map(Option(_))
|
case None => sleep(2000)
|
||||||
.takeWhile(opt => opt.isDefined && running && !done)
|
case Some(node) =>
|
||||||
.flatten
|
val status = node.path("status").asText()
|
||||||
.foreach { line =>
|
if gameTerminalStatuses.contains(status) then
|
||||||
parse(line).foreach: node =>
|
log.infof("Game %s ended — status=%s", gameId, status); done = true
|
||||||
node.path("type").asText() match
|
else
|
||||||
case "gameState" =>
|
// TEMP: tournament-server reports wrong color in pairings (everyone white).
|
||||||
maybeMove(
|
// The game endpoint white/black ids are correct, so derive our color from it.
|
||||||
cfg,
|
val whiteId = node.path("white").path("id").asText()
|
||||||
gameId,
|
val blackId = node.path("black").path("id").asText()
|
||||||
color,
|
val myColor =
|
||||||
node.path("turn").asText(),
|
if whiteId == cfg.botId then "white"
|
||||||
node.path("status").asText(),
|
else if blackId == cfg.botId then "black"
|
||||||
node.path("fen").asText(),
|
else color
|
||||||
)
|
val turn = node.path("turn").asText()
|
||||||
case "move" =>
|
val fen = node.path("fen").asText()
|
||||||
maybeMove(cfg, gameId, color, node.path("turn").asText(), "ongoing", node.path("fen").asText())
|
if turn == myColor && status == "ongoing" && fen.nonEmpty && fen != lastFen then
|
||||||
case "gameEnd" =>
|
lastFen = fen
|
||||||
log.infof(
|
log.infof("Our turn in game %s — computing move (fen=%s)", gameId, fen)
|
||||||
"Game %s ended — status=%s winner=%s",
|
computeUci(cfg, fen) match
|
||||||
gameId,
|
case None => log.warnf("No move found for game %s (fen=%s)", gameId, fen)
|
||||||
node.path("status").asText(),
|
case Some(uci) => submitMove(cfg, gameId, uci)
|
||||||
node.path("winner").asText(),
|
sleep(1000)
|
||||||
); done = true
|
|
||||||
case _ => ()
|
|
||||||
}
|
|
||||||
|
|
||||||
private def maybeMove(
|
|
||||||
cfg: TournamentBotConfig,
|
|
||||||
gameId: String,
|
|
||||||
color: String,
|
|
||||||
turn: String,
|
|
||||||
status: String,
|
|
||||||
fen: String,
|
|
||||||
): Unit =
|
|
||||||
if turn == color && status == "ongoing" && fen.nonEmpty then
|
|
||||||
computeUci(cfg, fen) match
|
|
||||||
case None => log.warnf("No move found for game %s (fen=%s)", gameId, fen)
|
|
||||||
case Some(uci) => submitMove(cfg, gameId, uci)
|
|
||||||
|
|
||||||
private def computeUci(cfg: TournamentBotConfig, fen: String): Option[String] =
|
private def computeUci(cfg: TournamentBotConfig, fen: String): Option[String] =
|
||||||
FenParser.parseFen(fen) match
|
FenParser.parseFen(fen) match
|
||||||
@@ -213,15 +448,6 @@ class TournamentBotGamePlayer:
|
|||||||
case Failure(ex) => log.errorf(ex, "Error submitting move %s in game %s", uci, gameId)
|
case Failure(ex) => log.errorf(ex, "Error submitting move %s in game %s", uci, gameId)
|
||||||
case Success(_) => ()
|
case Success(_) => ()
|
||||||
|
|
||||||
private def openGameStream(cfg: TournamentBotConfig, gameId: String): Option[InputStream] =
|
|
||||||
Try {
|
|
||||||
val response = authed(cfg, target(cfg).path("game").path(gameId).path("stream"))
|
|
||||||
.header("Accept", "application/x-ndjson")
|
|
||||||
.get()
|
|
||||||
if response.getStatus == 200 then Some(response.readEntity(classOf[InputStream]))
|
|
||||||
else { log.warnf("Game stream %s returned status %d", gameId, response.getStatus); response.close(); None }
|
|
||||||
}.getOrElse(None)
|
|
||||||
|
|
||||||
private def engine(cfg: TournamentBotConfig): Bot =
|
private def engine(cfg: TournamentBotConfig): Bot =
|
||||||
botController.getBot(cfg.difficulty).orElse(botController.getBot("medium")).get
|
botController.getBot(cfg.difficulty).orElse(botController.getBot("medium")).get
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import de.nowchess.api.board.*
|
|||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
|
|
||||||
import java.io.{DataInputStream, FileInputStream}
|
import java.io.{DataInputStream, FileInputStream, InputStream}
|
||||||
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.util.Random
|
|
||||||
|
|
||||||
/** Reads a Polyglot opening book (.bin file) and probes it for moves.
|
/** Reads a Polyglot opening book (.bin file) and probes it for moves.
|
||||||
*
|
*
|
||||||
@@ -16,24 +16,11 @@ import scala.util.Random
|
|||||||
* - weight: 2 bytes (Short) — move weight (higher = preferred)
|
* - weight: 2 bytes (Short) — move weight (higher = preferred)
|
||||||
* - learn: 4 bytes (Int) — learning data (unused)
|
* - learn: 4 bytes (Int) — learning data (unused)
|
||||||
*/
|
*/
|
||||||
final class PolyglotBook(path: String):
|
final class PolyglotBook private (entries: Map[Long, Vector[BookEntry]]):
|
||||||
|
|
||||||
private val entries: Map[Long, Vector[BookEntry]] =
|
|
||||||
try {
|
|
||||||
val r = loadBookFile(path)
|
|
||||||
println(s"Book loaded successfully. ${r.size} entries found.")
|
|
||||||
r
|
|
||||||
} catch
|
|
||||||
case e: Exception =>
|
|
||||||
println(s"Error loading book: $e")
|
|
||||||
// Gracefully fail: return empty map if book cannot be loaded
|
|
||||||
// This allows the bot to work even if the book file is missing
|
|
||||||
scala.collection.immutable.Map.empty
|
|
||||||
|
|
||||||
/** Probe the book for a move in the given position. Returns a weighted random move, or None if not in book. */
|
/** Probe the book for a move in the given position. Returns a weighted random move, or None if not in book. */
|
||||||
def probe(context: GameContext): Option[Move] =
|
def probe(context: GameContext): Option[Move] =
|
||||||
val hash = PolyglotHash.hash(context)
|
val hash = PolyglotHash.hash(context)
|
||||||
println(f"0x$hash%016X")
|
|
||||||
entries.get(hash).flatMap { bookEntries =>
|
entries.get(hash).flatMap { bookEntries =>
|
||||||
if bookEntries.isEmpty then None
|
if bookEntries.isEmpty then None
|
||||||
else
|
else
|
||||||
@@ -41,24 +28,6 @@ final class PolyglotBook(path: String):
|
|||||||
decodeMove(entry.move, context)
|
decodeMove(entry.move, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def loadBookFile(path: String): Map[Long, Vector[BookEntry]] =
|
|
||||||
val input = DataInputStream(FileInputStream(path))
|
|
||||||
try
|
|
||||||
val result = mutable.Map[Long, Vector[BookEntry]]()
|
|
||||||
while input.available() > 0 do
|
|
||||||
val key = input.readLong()
|
|
||||||
val move = input.readShort()
|
|
||||||
val weight = input.readShort()
|
|
||||||
input.readInt() // learning data (unused)
|
|
||||||
|
|
||||||
val entry = BookEntry(key, move, weight)
|
|
||||||
result.updateWith(key) {
|
|
||||||
case Some(entries) => Some(entries :+ entry)
|
|
||||||
case None => Some(Vector(entry))
|
|
||||||
}
|
|
||||||
result.toMap
|
|
||||||
finally input.close()
|
|
||||||
|
|
||||||
/** Decode a packed Polyglot move short into an Option[Move].
|
/** Decode a packed Polyglot move short into an Option[Move].
|
||||||
*
|
*
|
||||||
* Bit layout of the move Short:
|
* Bit layout of the move Short:
|
||||||
@@ -124,7 +93,7 @@ final class PolyglotBook(path: String):
|
|||||||
if entries.length == 1 then entries.head
|
if entries.length == 1 then entries.head
|
||||||
else
|
else
|
||||||
val totalWeight = entries.map(_.weight).sum
|
val totalWeight = entries.map(_.weight).sum
|
||||||
val pick = Random.nextInt(totalWeight.max(1)) // NOSONAR
|
val pick = ThreadLocalRandom.current().nextInt(totalWeight.max(1)) // NOSONAR
|
||||||
|
|
||||||
@scala.annotation.tailrec
|
@scala.annotation.tailrec
|
||||||
def select(remaining: Int, idx: Int): BookEntry =
|
def select(remaining: Int, idx: Int): BookEntry =
|
||||||
@@ -134,4 +103,48 @@ final class PolyglotBook(path: String):
|
|||||||
|
|
||||||
select(pick, 0)
|
select(pick, 0)
|
||||||
|
|
||||||
|
object PolyglotBook:
|
||||||
|
|
||||||
|
/** Load a book from a filesystem path. Fails gracefully to an empty book. */
|
||||||
|
def apply(path: String): PolyglotBook =
|
||||||
|
safeLoad(s"file $path")(FileInputStream(path))
|
||||||
|
|
||||||
|
/** Load a book from a classpath resource (native-image safe: the resource is embedded in the binary, so no file must
|
||||||
|
* be mounted into the pod).
|
||||||
|
*/
|
||||||
|
def fromResource(name: String): PolyglotBook =
|
||||||
|
Option(getClass.getResourceAsStream(name)) match
|
||||||
|
case Some(stream) => safeLoad(s"resource $name")(stream)
|
||||||
|
case None =>
|
||||||
|
println(s"Error loading book: resource $name not found on classpath")
|
||||||
|
new PolyglotBook(Map.empty)
|
||||||
|
|
||||||
|
private def safeLoad(source: String)(stream: => InputStream): PolyglotBook =
|
||||||
|
try
|
||||||
|
val entries = parse(stream)
|
||||||
|
println(s"Book loaded successfully from $source. ${entries.size} entries found.")
|
||||||
|
new PolyglotBook(entries)
|
||||||
|
catch
|
||||||
|
case e: Exception =>
|
||||||
|
println(s"Error loading book from $source: $e")
|
||||||
|
new PolyglotBook(Map.empty)
|
||||||
|
|
||||||
|
private def parse(stream: InputStream): Map[Long, Vector[BookEntry]] =
|
||||||
|
val input = DataInputStream(stream)
|
||||||
|
try
|
||||||
|
val result = mutable.Map[Long, Vector[BookEntry]]()
|
||||||
|
while input.available() > 0 do
|
||||||
|
val key = input.readLong()
|
||||||
|
val move = input.readShort()
|
||||||
|
val weight = input.readShort()
|
||||||
|
input.readInt() // learning data (unused)
|
||||||
|
|
||||||
|
val entry = BookEntry(key, move, weight)
|
||||||
|
result.updateWith(key) {
|
||||||
|
case Some(entries) => Some(entries :+ entry)
|
||||||
|
case None => Some(Vector(entry))
|
||||||
|
}
|
||||||
|
result.toMap
|
||||||
|
finally input.close()
|
||||||
|
|
||||||
private case class BookEntry(key: Long, move: Short, weight: Int)
|
private case class BookEntry(key: Long, move: Short, weight: Int)
|
||||||
|
|||||||
@@ -312,6 +312,24 @@ class AlphaBetaSearchTest extends AnyFunSuite with Matchers:
|
|||||||
val search = AlphaBetaSearch(qRules, weights = ZeroEval)
|
val search = AlphaBetaSearch(qRules, weights = ZeroEval)
|
||||||
search.bestMove(GameContext.initial, maxDepth = 1) should be(Some(rootMove))
|
search.bestMove(GameContext.initial, maxDepth = 1) should be(Some(rootMove))
|
||||||
|
|
||||||
|
test("bestMove with root hints returns a valid move without regression"):
|
||||||
|
val context = GameContext.initial
|
||||||
|
val legalMoves = DefaultRules.allLegalMoves(context)
|
||||||
|
val hints = legalMoves.zipWithIndex.map { case (m, i) => m -> (legalMoves.length - i) }.toMap
|
||||||
|
val withHints = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||||
|
.bestMove(context, maxDepth = 2, Set.empty, hints)
|
||||||
|
withHints should not be None
|
||||||
|
legalMoves should contain(withHints.get)
|
||||||
|
|
||||||
|
test("bestMoveWithTime with root hints returns a valid move without regression"):
|
||||||
|
val context = GameContext.initial
|
||||||
|
val legalMoves = DefaultRules.allLegalMoves(context)
|
||||||
|
val hints = legalMoves.zipWithIndex.map { case (m, i) => m -> (legalMoves.length - i) }.toMap
|
||||||
|
val withHints = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||||
|
.bestMoveWithTime(context, 500L, Set.empty, hints)
|
||||||
|
withHints should not be None
|
||||||
|
legalMoves should contain(withHints.get)
|
||||||
|
|
||||||
test("quiescence depth-limit in-check branch is exercised"):
|
test("quiescence depth-limit in-check branch is exercised"):
|
||||||
val rootMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal())
|
val rootMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal())
|
||||||
val capMove = Move(Square(File.D, Rank.R2), Square(File.D, Rank.R3), MoveType.Normal(true))
|
val capMove = Move(Square(File.D, Rank.R2), Square(File.D, Rank.R3), MoveType.Normal(true))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -217,3 +217,60 @@ class MoveOrderingTest extends AnyFunSuite with Matchers:
|
|||||||
val castle = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
|
val castle = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
|
||||||
|
|
||||||
MoveOrdering.score(context, castle, None) should be(0)
|
MoveOrdering.score(context, castle, None) should be(0)
|
||||||
|
|
||||||
|
test("root hints override capture heuristics at ply 0"):
|
||||||
|
val board = Board(
|
||||||
|
Map(
|
||||||
|
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||||
|
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||||
|
Square(File.D, Rank.R5) -> Piece.BlackRook,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||||
|
val quietMove = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R6))
|
||||||
|
val rookCapture = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true))
|
||||||
|
val hints = Map(quietMove -> 500, rookCapture -> 100)
|
||||||
|
|
||||||
|
MoveOrdering.score(context, quietMove, None, ply = 0, rootHints = hints) should equal(500)
|
||||||
|
MoveOrdering.score(context, rookCapture, None, ply = 0, rootHints = hints) should equal(100)
|
||||||
|
MoveOrdering.score(context, rookCapture, None, ply = 0, rootHints = hints) should be <
|
||||||
|
MoveOrdering.score(context, quietMove, None, ply = 0, rootHints = hints)
|
||||||
|
|
||||||
|
test("root hints ignored at ply > 0"):
|
||||||
|
val board = Board(Map(Square(File.E, Rank.R4) -> Piece.WhiteQueen, Square(File.E, Rank.R5) -> Piece.BlackPawn))
|
||||||
|
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||||
|
val capture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
|
||||||
|
val quiet = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R4))
|
||||||
|
val hints = Map(quiet -> 99999, capture -> -99999)
|
||||||
|
|
||||||
|
val captureScore = MoveOrdering.score(context, capture, None, ply = 1, rootHints = hints)
|
||||||
|
val quietScore = MoveOrdering.score(context, quiet, None, ply = 1, rootHints = hints)
|
||||||
|
captureScore should be > quietScore
|
||||||
|
|
||||||
|
test("move absent from root hints gets Int.MinValue / 2 fallback"):
|
||||||
|
val board = Board(Map(Square(File.E, Rank.R4) -> Piece.WhiteQueen))
|
||||||
|
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||||
|
val move1 = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R6))
|
||||||
|
val move2 = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5))
|
||||||
|
val hints = Map(move1 -> 0)
|
||||||
|
|
||||||
|
MoveOrdering.score(context, move2, None, ply = 0, rootHints = hints) should equal(Int.MinValue / 2)
|
||||||
|
|
||||||
|
test("sort uses root hints at ply 0 to reorder moves"):
|
||||||
|
val board = Board(
|
||||||
|
Map(
|
||||||
|
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||||
|
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||||
|
Square(File.D, Rank.R5) -> Piece.BlackRook,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||||
|
val rookCapture = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true))
|
||||||
|
val pawnCapture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
|
||||||
|
val quiet = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R6))
|
||||||
|
val hints = Map(quiet -> 9999, pawnCapture -> 500, rookCapture -> 100)
|
||||||
|
|
||||||
|
val sorted = MoveOrdering.sort(context, List(rookCapture, pawnCapture, quiet), None, ply = 0, rootHints = hints)
|
||||||
|
sorted.head should equal(quiet)
|
||||||
|
sorted(1) should equal(pawnCapture)
|
||||||
|
sorted(2) should equal(rookCapture)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=20
|
MINOR=40
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -37,3 +37,103 @@
|
|||||||
|
|
||||||
* **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))
|
||||||
* 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-21)
|
||||||
|
|
||||||
|
### 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:** 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))
|
||||||
|
## (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))
|
||||||
|
## (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 HS256 director token for native tournament-server calls ([b98bdd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b98bdd2a64eb6c8279bd3cfe15d70628025ef0e5))
|
||||||
|
* **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))
|
||||||
|
## (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:** sync native-server participants and route start ([#78](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/78)) ([1f4e9c8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1f4e9c8498f55d95ab48758df60c7618445bf6ca))
|
||||||
|
* **tournament:** use HS256 director token for native tournament-server calls ([b98bdd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b98bdd2a64eb6c8279bd3cfe15d70628025ef0e5))
|
||||||
|
* **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))
|
||||||
|
## (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:** mirror bot join onto native twin ([7664042](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76640421930c26a9260da002c90e2966b97a57a4))
|
||||||
|
* **tournament:** replace scala.util.Random singleton with UUID for native image ([a50884a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a50884a11b1de500e74c18fd08d2d102d53cc3e9))
|
||||||
|
* **tournament:** sync native-server participants and route start ([#78](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/78)) ([1f4e9c8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1f4e9c8498f55d95ab48758df60c7618445bf6ca))
|
||||||
|
* **tournament:** use HS256 director token for native tournament-server calls ([b98bdd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b98bdd2a64eb6c8279bd3cfe15d70628025ef0e5))
|
||||||
|
* **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))
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ nowchess:
|
|||||||
prefix: ${REDIS_PREFIX:nowchess}
|
prefix: ${REDIS_PREFIX:nowchess}
|
||||||
internal:
|
internal:
|
||||||
secret: ${INTERNAL_SECRET:123abc}
|
secret: ${INTERNAL_SECRET:123abc}
|
||||||
|
tournament:
|
||||||
|
self-url: ""
|
||||||
|
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:
|
||||||
@@ -46,6 +51,12 @@ mp:
|
|||||||
hibernate-orm:
|
hibernate-orm:
|
||||||
schema-management:
|
schema-management:
|
||||||
strategy: update
|
strategy: update
|
||||||
|
nowchess:
|
||||||
|
tournament:
|
||||||
|
self-url: ${TOURNAMENT_SELF_URL:}
|
||||||
|
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:
|
||||||
|
|||||||
+1
-1
@@ -26,13 +26,13 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
|||||||
classOf[RoundPairingsDto],
|
classOf[RoundPairingsDto],
|
||||||
classOf[ErrorDto],
|
classOf[ErrorDto],
|
||||||
classOf[OkDto],
|
classOf[OkDto],
|
||||||
|
classOf[ReplicateTournamentRequest],
|
||||||
classOf[CorePlayerInfo],
|
classOf[CorePlayerInfo],
|
||||||
classOf[CoreTimeControl],
|
classOf[CoreTimeControl],
|
||||||
classOf[CoreCreateGameRequest],
|
classOf[CoreCreateGameRequest],
|
||||||
classOf[CoreGameResponse],
|
classOf[CoreGameResponse],
|
||||||
classOf[GameWritebackEventDto],
|
classOf[GameWritebackEventDto],
|
||||||
classOf[ExternalTournamentServer],
|
classOf[ExternalTournamentServer],
|
||||||
classOf[RegisterServerRequest],
|
|
||||||
classOf[ExternalTournamentServerList],
|
classOf[ExternalTournamentServerList],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.time.Instant
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "tournaments")
|
@Table(name = "tournaments")
|
||||||
class Tournament:
|
class Tournament:
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off
|
||||||
@Id
|
@Id
|
||||||
var id: String = uninitialized
|
var id: String = uninitialized
|
||||||
|
|
||||||
@@ -30,4 +30,10 @@ class Tournament:
|
|||||||
var startsAt: Instant = uninitialized
|
var startsAt: Instant = uninitialized
|
||||||
var winnerId: String = uninitialized
|
var winnerId: String = uninitialized
|
||||||
var winnerName: String = uninitialized
|
var winnerName: String = uninitialized
|
||||||
|
|
||||||
|
@Column(nullable = true)
|
||||||
|
var originServerUrl: String = null
|
||||||
|
|
||||||
|
@Column(nullable = true)
|
||||||
|
var nativeTournamentId: String = null
|
||||||
// scalafix:on
|
// scalafix:on
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package de.nowchess.tournament.dto
|
package de.nowchess.tournament.dto
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
case class BotRef(id: String, name: String)
|
case class BotRef(id: String, name: String)
|
||||||
|
|
||||||
case class Clock(limit: Int, increment: Int)
|
case class Clock(limit: Int, increment: Int)
|
||||||
@@ -72,3 +74,15 @@ case class RoundPairingsDto(round: Int, pairings: List[PairingDto])
|
|||||||
case class ErrorDto(error: String)
|
case class ErrorDto(error: String)
|
||||||
|
|
||||||
case class OkDto(ok: Boolean = true)
|
case class OkDto(ok: Boolean = true)
|
||||||
|
|
||||||
|
case class ReplicateTournamentRequest(
|
||||||
|
id: String,
|
||||||
|
fullName: String,
|
||||||
|
nbRounds: Int,
|
||||||
|
clockLimit: Int,
|
||||||
|
clockIncrement: Int,
|
||||||
|
rated: Boolean,
|
||||||
|
createdBy: String,
|
||||||
|
startsAt: Instant,
|
||||||
|
status: String,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
package de.nowchess.tournament.dto
|
package de.nowchess.tournament.dto
|
||||||
|
|
||||||
case class ExternalTournamentServer(id: String, label: String, url: String)
|
case class ExternalTournamentServer(id: String, label: String, url: String)
|
||||||
case class RegisterServerRequest(label: String, url: String)
|
|
||||||
case class ExternalTournamentServerList(servers: List[ExternalTournamentServer])
|
case class ExternalTournamentServerList(servers: List[ExternalTournamentServer])
|
||||||
|
|||||||
+285
-65
@@ -9,13 +9,14 @@ import de.nowchess.tournament.service.{
|
|||||||
TournamentService,
|
TournamentService,
|
||||||
TournamentStreamManager,
|
TournamentStreamManager,
|
||||||
}
|
}
|
||||||
import io.smallrye.mutiny.Multi
|
|
||||||
import jakarta.annotation.security.{PermitAll, RolesAllowed}
|
import jakarta.annotation.security.{PermitAll, RolesAllowed}
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import jakarta.ws.rs.*
|
import jakarta.ws.rs.*
|
||||||
import jakarta.ws.rs.core.{Context, HttpHeaders, MediaType, Response, StreamingOutput}
|
import jakarta.ws.rs.core.{Context, HttpHeaders, MediaType, Response, StreamingOutput}
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken
|
import org.eclipse.microprofile.jwt.JsonWebToken
|
||||||
|
import java.util.Optional
|
||||||
import org.jboss.logging.Logger
|
import org.jboss.logging.Logger
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
import scala.jdk.CollectionConverters.*
|
import scala.jdk.CollectionConverters.*
|
||||||
@@ -36,15 +37,24 @@ class TournamentResource:
|
|||||||
@Inject var externalClient: ExternalTournamentClient = uninitialized
|
@Inject var externalClient: ExternalTournamentClient = uninitialized
|
||||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||||
@Context var headers: HttpHeaders = uninitialized
|
@Context var headers: HttpHeaders = uninitialized
|
||||||
|
|
||||||
|
@ConfigProperty(name = "nowchess.tournament.self-url")
|
||||||
|
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
|
||||||
@PermitAll
|
@PermitAll
|
||||||
def list(): Response =
|
def list(): Response =
|
||||||
val (created, started, finished) = tournamentService.list()
|
val (created, started, finished) = tournamentService.list()
|
||||||
val internalCreated = created.map(t => objectMapper.valueToTree[JsonNode](tournamentService.toDto(t)))
|
val internalCreated = created.map(nativeOverlay)
|
||||||
val internalStarted = started.map(t => objectMapper.valueToTree[JsonNode](tournamentService.toDto(t)))
|
val internalStarted = started.map(nativeOverlay)
|
||||||
val internalFinished = finished.map(t => objectMapper.valueToTree[JsonNode](tournamentService.toDto(t)))
|
val internalFinished = finished.map(nativeOverlay)
|
||||||
|
|
||||||
val (extCreated, extStarted, extFinished) = registry
|
val (extCreated, extStarted, extFinished) = registry
|
||||||
.serverUrls()
|
.serverUrls()
|
||||||
@@ -85,48 +95,153 @@ class TournamentResource:
|
|||||||
val userId = Option(jwt.getSubject).getOrElse("")
|
val userId = Option(jwt.getSubject).getOrElse("")
|
||||||
val form = CreateTournamentForm(name, nbRounds, clockLimit, clockIncrement, rated)
|
val form = CreateTournamentForm(name, nbRounds, clockLimit, clockIncrement, rated)
|
||||||
val t = tournamentService.create(userId, form)
|
val t = tournamentService.create(userId, form)
|
||||||
|
selfUrl.ifPresent { url =>
|
||||||
|
registry.serverUrls().filterNot(externalClient.isNativeServer).foreach { remoteUrl =>
|
||||||
|
if !externalClient.replicateTournament(remoteUrl, toReplicateRequest(t), url) then
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
externalClient.publishNative(nativeServerUrl, directorName, form) match
|
||||||
|
case Some(nativeId) => tournamentService.setNativeTournamentId(t.id, nativeId)
|
||||||
|
case None => log.warnf("Failed to publish tournament %s to native server %s", t.id, nativeServerUrl)
|
||||||
|
|
||||||
|
// Mirror a bot join onto the native twin so it surfaces in the UI, which reads participant and
|
||||||
|
// standings fields from the native server (see nativeOverlay).
|
||||||
|
private def joinNativeTwin(id: String, botName: String): Unit =
|
||||||
|
if nativeServerUrl.nonEmpty then
|
||||||
|
tournamentService.get(id).flatMap(nativeIdFor).foreach { nativeId =>
|
||||||
|
if externalClient.joinNativeAsBot(nativeServerUrl, nativeId, botName) then
|
||||||
|
log.infof("Joined bot %s on native twin %s of tournament %s", botName, nativeId, id)
|
||||||
|
else log.warnf("Failed to join bot %s on native twin %s of tournament %s", botName, nativeId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the native-server twin of a local tournament. Backfills the stored id by matching
|
||||||
|
// fullName against the native list for tournaments created before the id was captured.
|
||||||
|
private def nativeIdFor(t: de.nowchess.tournament.domain.Tournament): Option[String] =
|
||||||
|
if nativeServerUrl.isEmpty then None
|
||||||
|
else
|
||||||
|
Option(t.nativeTournamentId).filter(_.nonEmpty).orElse {
|
||||||
|
val found = externalClient
|
||||||
|
.fetchList(nativeServerUrl)
|
||||||
|
.flatMap { node =>
|
||||||
|
Seq("created", "started", "finished").iterator
|
||||||
|
.flatMap(k => node.path(k).elements().asScala)
|
||||||
|
.find(_.path("fullName").asText() == t.fullName)
|
||||||
|
.map(_.path("id").asText())
|
||||||
|
.filter(_.nonEmpty)
|
||||||
|
}
|
||||||
|
found.foreach(id => tournamentService.setNativeTournamentId(t.id, id))
|
||||||
|
found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay live participant/standings/status fields from the native twin onto a local DTO so
|
||||||
|
// bots that joined directly on the native server are reflected in NowChess.
|
||||||
|
private def nativeOverlay(t: de.nowchess.tournament.domain.Tournament): JsonNode =
|
||||||
|
val standings = tournamentService.getStandings(t.id)
|
||||||
|
val dto = objectMapper.valueToTree[JsonNode](tournamentService.toDto(t, standings))
|
||||||
|
nativeIdFor(t).flatMap(nid => externalClient.fetch(nativeServerUrl, nid)) match
|
||||||
|
case Some(native) =>
|
||||||
|
val merged = dto.deepCopy[com.fasterxml.jackson.databind.node.ObjectNode]()
|
||||||
|
Seq("nbPlayers", "standing", "status", "round", "winner").foreach { field =>
|
||||||
|
if native.has(field) then merged.set(field, native.get(field))
|
||||||
|
}
|
||||||
|
merged
|
||||||
|
case None => dto
|
||||||
|
|
||||||
|
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
|
||||||
def get(@PathParam("id") id: String): Response =
|
def get(@PathParam("id") id: String): Response =
|
||||||
tournamentService.get(id) match
|
tournamentService.get(id) match
|
||||||
case Some(t) =>
|
case Some(t) =>
|
||||||
val standings = tournamentService.getStandings(id)
|
Response.ok(nativeOverlay(t)).build()
|
||||||
Response.ok(tournamentService.toDto(t, standings)).build()
|
|
||||||
case None =>
|
case None =>
|
||||||
resolveServer(id)
|
resolveServer(id)
|
||||||
.flatMap(url => externalClient.fetch(url, id).map(node => Response.ok(node).build()))
|
.flatMap(url => externalClient.fetch(url, id).map(node => Response.ok(node).build()))
|
||||||
.getOrElse(Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Tournament $id not found")).build())
|
.getOrElse(Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Tournament $id not found")).build())
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/replicate")
|
||||||
|
@PermitAll
|
||||||
|
def replicate(req: ReplicateTournamentRequest): Response =
|
||||||
|
val originUrl = Option(headers.getHeaderString("X-Origin-Url")).getOrElse("")
|
||||||
|
if originUrl.isEmpty then
|
||||||
|
Response.status(Response.Status.BAD_REQUEST).entity(ErrorDto("Missing X-Origin-Url header")).build()
|
||||||
|
else
|
||||||
|
tournamentService.get(req.id) match
|
||||||
|
case Some(_) => Response.status(Response.Status.CONFLICT).entity(ErrorDto("Tournament already exists")).build()
|
||||||
|
case None =>
|
||||||
|
tournamentService.replicate(req, originUrl)
|
||||||
|
Response.status(Response.Status.CREATED).build()
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
@RolesAllowed(Array("**"))
|
@RolesAllowed(Array("**"))
|
||||||
def terminate(@PathParam("id") id: String): Response =
|
def terminate(@PathParam("id") id: String): Response =
|
||||||
val userId = Option(jwt.getSubject).getOrElse("")
|
val userId = Option(jwt.getSubject).getOrElse("")
|
||||||
tournamentService.terminate(id, userId) match
|
tournamentService.get(id).flatMap(t => Option(t.originServerUrl)) match
|
||||||
case Right(_) => Response.noContent().build()
|
case Some(originUrl) =>
|
||||||
case Left(error) => errorResponse(error)
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
|
val (status, body) = externalClient.proxyPost(originUrl, s"api/tournament/$id", auth)
|
||||||
|
Response.status(status).entity(body).build()
|
||||||
|
case None =>
|
||||||
|
tournamentService.terminate(id, userId) match
|
||||||
|
case Right(_) => Response.noContent().build()
|
||||||
|
case Left(error) => errorResponse(error)
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/{id}/start")
|
@Path("/{id}/start")
|
||||||
@RolesAllowed(Array("**"))
|
@RolesAllowed(Array("**"))
|
||||||
def start(@PathParam("id") id: String): Response =
|
def start(@PathParam("id") id: String): Response =
|
||||||
val userId = Option(jwt.getSubject).getOrElse("")
|
val userId = Option(jwt.getSubject).getOrElse("")
|
||||||
tournamentService.start(id, userId) match
|
tournamentService.get(id).flatMap(t => Option(t.originServerUrl)) match
|
||||||
case Right(t) => Response.ok(tournamentService.toDto(t)).build()
|
case Some(originUrl) =>
|
||||||
case Left(error) =>
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
error match
|
val (status, body) = externalClient.proxyPost(originUrl, s"api/tournament/$id/start", auth)
|
||||||
case TournamentError.NotFound(_) =>
|
Response.status(status).entity(body).build()
|
||||||
val auth = Option(headers.getHeaderString("Authorization"))
|
case None =>
|
||||||
resolveServer(id)
|
tournamentService.get(id).flatMap(nativeIdFor) match
|
||||||
.map { url =>
|
case Some(nativeId) =>
|
||||||
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/start", auth)
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
Response.status(status).entity(body).build()
|
val (status, body) = externalClient.proxyPost(nativeServerUrl, s"api/tournament/$nativeId/start", auth)
|
||||||
}
|
if status / 100 == 2 then tournamentService.markStatus(id, "started")
|
||||||
.getOrElse(errorResponse(error))
|
Response.status(status).entity(body).build()
|
||||||
case _ => errorResponse(error)
|
case None =>
|
||||||
|
tournamentService.start(id, userId) match
|
||||||
|
case Right(t) => Response.ok(tournamentService.toDto(t)).build()
|
||||||
|
case Left(error) =>
|
||||||
|
error match
|
||||||
|
case TournamentError.NotFound(_) =>
|
||||||
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
|
resolveServer(id)
|
||||||
|
.map { url =>
|
||||||
|
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/start", auth)
|
||||||
|
Response.status(status).entity(body).build()
|
||||||
|
}
|
||||||
|
.getOrElse(errorResponse(error))
|
||||||
|
case _ => errorResponse(error)
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/{id}/join")
|
@Path("/{id}/join")
|
||||||
@@ -136,21 +251,34 @@ class TournamentResource:
|
|||||||
if tokenType != "bot" then
|
if tokenType != "bot" then
|
||||||
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto("Only bots can join tournaments")).build()
|
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto("Only bots can join tournaments")).build()
|
||||||
else
|
else
|
||||||
val botId = Option(jwt.getSubject).getOrElse("")
|
tournamentService.get(id).flatMap(t => Option(t.originServerUrl)) match
|
||||||
val botName = Option(jwt.getClaim[AnyRef]("name")).map(_.toString).getOrElse(botId)
|
case Some(originUrl) =>
|
||||||
tournamentService.join(id, botId, botName) match
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
case Right(_) => Response.ok(OkDto()).build()
|
val (status, body) = externalClient.proxyPost(originUrl, s"api/tournament/$id/join", auth)
|
||||||
case Left(error) =>
|
Response.status(status).entity(body).build()
|
||||||
error match
|
case None =>
|
||||||
case TournamentError.NotFound(_) =>
|
val botId = Option(jwt.getSubject).getOrElse("")
|
||||||
val auth = Option(headers.getHeaderString("Authorization"))
|
val botName = Option(jwt.getClaim[AnyRef]("name")).map(_.toString).getOrElse(botId)
|
||||||
resolveServer(id)
|
tournamentService.join(id, botId, botName) match
|
||||||
.map { url =>
|
case Right(_) =>
|
||||||
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/join", auth)
|
joinNativeTwin(id, botName)
|
||||||
Response.status(status).entity(body).build()
|
Response.ok(OkDto()).build()
|
||||||
}
|
// Already in the NowChess participant list but possibly never mirrored onto the native
|
||||||
.getOrElse(errorResponse(error))
|
// twin (where the UI reads participants from) — make the native join idempotent.
|
||||||
case _ => errorResponse(error)
|
case Left(TournamentError.AlreadyJoined) =>
|
||||||
|
joinNativeTwin(id, botName)
|
||||||
|
Response.ok(OkDto()).build()
|
||||||
|
case Left(error) =>
|
||||||
|
error match
|
||||||
|
case TournamentError.NotFound(_) =>
|
||||||
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
|
resolveServer(id)
|
||||||
|
.map { url =>
|
||||||
|
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/join", auth)
|
||||||
|
Response.status(status).entity(body).build()
|
||||||
|
}
|
||||||
|
.getOrElse(errorResponse(error))
|
||||||
|
case _ => errorResponse(error)
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/{id}/withdraw")
|
@Path("/{id}/withdraw")
|
||||||
@@ -160,20 +288,26 @@ class TournamentResource:
|
|||||||
if tokenType != "bot" then
|
if tokenType != "bot" then
|
||||||
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto("Only bots can withdraw")).build()
|
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto("Only bots can withdraw")).build()
|
||||||
else
|
else
|
||||||
val botId = Option(jwt.getSubject).getOrElse("")
|
tournamentService.get(id).flatMap(t => Option(t.originServerUrl)) match
|
||||||
tournamentService.withdraw(id, botId) match
|
case Some(originUrl) =>
|
||||||
case Right(_) => Response.ok(OkDto()).build()
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
case Left(error) =>
|
val (status, body) = externalClient.proxyPost(originUrl, s"api/tournament/$id/withdraw", auth)
|
||||||
error match
|
Response.status(status).entity(body).build()
|
||||||
case TournamentError.NotFound(_) =>
|
case None =>
|
||||||
val auth = Option(headers.getHeaderString("Authorization"))
|
val botId = Option(jwt.getSubject).getOrElse("")
|
||||||
resolveServer(id)
|
tournamentService.withdraw(id, botId) match
|
||||||
.map { url =>
|
case Right(_) => Response.ok(OkDto()).build()
|
||||||
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/withdraw", auth)
|
case Left(error) =>
|
||||||
Response.status(status).entity(body).build()
|
error match
|
||||||
}
|
case TournamentError.NotFound(_) =>
|
||||||
.getOrElse(errorResponse(error))
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
case _ => errorResponse(error)
|
resolveServer(id)
|
||||||
|
.map { url =>
|
||||||
|
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/withdraw", auth)
|
||||||
|
Response.status(status).entity(body).build()
|
||||||
|
}
|
||||||
|
.getOrElse(errorResponse(error))
|
||||||
|
case _ => errorResponse(error)
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{id}/results")
|
@Path("/{id}/results")
|
||||||
@@ -236,15 +370,84 @@ class TournamentResource:
|
|||||||
@Path("/{id}/stream")
|
@Path("/{id}/stream")
|
||||||
@RolesAllowed(Array("**"))
|
@RolesAllowed(Array("**"))
|
||||||
@Produces(Array("application/x-ndjson"))
|
@Produces(Array("application/x-ndjson"))
|
||||||
def stream(@PathParam("id") id: String): Multi[String] =
|
def stream(@PathParam("id") id: String): Response =
|
||||||
tournamentService.get(id) match
|
tournamentService.get(id) match
|
||||||
case None => Multi.createFrom().failure(new NotFoundException(s"Tournament $id not found"))
|
case Some(t) if Option(t.originServerUrl).isDefined =>
|
||||||
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
|
externalClient
|
||||||
|
.proxyGetStream(t.originServerUrl, s"api/tournament/$id/stream", auth)
|
||||||
|
.map { inputStream =>
|
||||||
|
Response
|
||||||
|
.ok(new StreamingOutput {
|
||||||
|
def write(output: java.io.OutputStream): Unit =
|
||||||
|
val buf = new Array[Byte](4096)
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
var n = inputStream.read(buf)
|
||||||
|
while n >= 0 do
|
||||||
|
output.write(buf, 0, n)
|
||||||
|
output.flush()
|
||||||
|
n = inputStream.read(buf)
|
||||||
|
// scalafix:on
|
||||||
|
})
|
||||||
|
.`type`("application/x-ndjson")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.getOrElse(
|
||||||
|
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Tournament $id stream unavailable")).build(),
|
||||||
|
)
|
||||||
case Some(_) =>
|
case Some(_) =>
|
||||||
val botId = Option(jwt.getSubject).getOrElse("")
|
val botId = Option(jwt.getSubject).getOrElse("")
|
||||||
Multi.createFrom().emitter[String] { emitter =>
|
val queue = new java.util.concurrent.LinkedBlockingQueue[Option[String]]()
|
||||||
streamManager.register(id, botId, emitter)
|
val emitter = new io.smallrye.mutiny.subscription.MultiEmitter[String] {
|
||||||
emitter.onTermination(() => streamManager.unregister(id, botId, emitter))
|
def emit(item: String): io.smallrye.mutiny.subscription.MultiEmitter[String] =
|
||||||
|
queue.put(Some(item)); this
|
||||||
|
def fail(failure: Throwable): Unit = queue.put(None)
|
||||||
|
def complete(): Unit = queue.put(None)
|
||||||
|
def requested(): Long = Long.MaxValue
|
||||||
|
def isCancelled: Boolean = false
|
||||||
|
def onTermination(
|
||||||
|
onTermination: java.lang.Runnable,
|
||||||
|
): io.smallrye.mutiny.subscription.MultiEmitter[String] = this
|
||||||
}
|
}
|
||||||
|
streamManager.register(id, botId, emitter)
|
||||||
|
Response
|
||||||
|
.ok(new StreamingOutput {
|
||||||
|
def write(output: java.io.OutputStream): Unit =
|
||||||
|
try
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
var cont = true
|
||||||
|
while cont do
|
||||||
|
queue.take() match
|
||||||
|
case None => cont = false
|
||||||
|
case Some(line) =>
|
||||||
|
output.write((line + "\n").getBytes("UTF-8"))
|
||||||
|
output.flush()
|
||||||
|
// scalafix:on
|
||||||
|
finally streamManager.unregister(id, botId, emitter)
|
||||||
|
})
|
||||||
|
.`type`("application/x-ndjson")
|
||||||
|
.build()
|
||||||
|
case None =>
|
||||||
|
val auth = Option(headers.getHeaderString("Authorization"))
|
||||||
|
resolveServer(id)
|
||||||
|
.flatMap(url => externalClient.proxyGetStream(url, s"api/tournament/$id/stream", auth))
|
||||||
|
.map { inputStream =>
|
||||||
|
Response
|
||||||
|
.ok(new StreamingOutput {
|
||||||
|
def write(output: java.io.OutputStream): Unit =
|
||||||
|
val buf = new Array[Byte](4096)
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
var n = inputStream.read(buf)
|
||||||
|
while n >= 0 do
|
||||||
|
output.write(buf, 0, n)
|
||||||
|
output.flush()
|
||||||
|
n = inputStream.read(buf)
|
||||||
|
// scalafix:on
|
||||||
|
})
|
||||||
|
.`type`("application/x-ndjson")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.getOrElse(Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Tournament $id not found")).build())
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{id}/game/{gameId}")
|
@Path("/{id}/game/{gameId}")
|
||||||
@@ -297,15 +500,32 @@ class TournamentResource:
|
|||||||
.getOrElse(Response.status(Response.Status.NOT_FOUND).build())
|
.getOrElse(Response.status(Response.Status.NOT_FOUND).build())
|
||||||
|
|
||||||
private def resolveServer(tournamentId: String): Option[String] =
|
private def resolveServer(tournamentId: String): Option[String] =
|
||||||
registry.findServerUrl(tournamentId).orElse {
|
tournamentService
|
||||||
registry
|
.get(tournamentId)
|
||||||
.serverUrls()
|
.flatMap(t => Option(t.originServerUrl))
|
||||||
.find(url => externalClient.fetch(url, tournamentId).isDefined)
|
.orElse(registry.findServerUrl(tournamentId))
|
||||||
.map { url =>
|
.orElse {
|
||||||
registry.bindTournament(tournamentId, url)
|
registry
|
||||||
url
|
.serverUrls()
|
||||||
}
|
.find(url => externalClient.fetch(url, tournamentId).isDefined)
|
||||||
}
|
.map { url =>
|
||||||
|
registry.bindTournament(tournamentId, url)
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def toReplicateRequest(t: de.nowchess.tournament.domain.Tournament): ReplicateTournamentRequest =
|
||||||
|
ReplicateTournamentRequest(
|
||||||
|
id = t.id,
|
||||||
|
fullName = t.fullName,
|
||||||
|
nbRounds = t.nbRounds,
|
||||||
|
clockLimit = t.clockLimit,
|
||||||
|
clockIncrement = t.clockIncrement,
|
||||||
|
rated = t.rated,
|
||||||
|
createdBy = t.createdBy,
|
||||||
|
startsAt = Option(t.startsAt).getOrElse(java.time.Instant.now()),
|
||||||
|
status = t.status,
|
||||||
|
)
|
||||||
|
|
||||||
private def errorResponse(error: TournamentError): Response =
|
private def errorResponse(error: TournamentError): Response =
|
||||||
val status = error match
|
val status = error match
|
||||||
|
|||||||
+1
-11
@@ -1,6 +1,6 @@
|
|||||||
package de.nowchess.tournament.resource
|
package de.nowchess.tournament.resource
|
||||||
|
|
||||||
import de.nowchess.tournament.dto.{ErrorDto, ExternalTournamentServerList, RegisterServerRequest}
|
import de.nowchess.tournament.dto.ExternalTournamentServerList
|
||||||
import de.nowchess.tournament.service.TournamentServerRegistry
|
import de.nowchess.tournament.service.TournamentServerRegistry
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
@@ -23,13 +23,3 @@ class TournamentServerResource:
|
|||||||
@GET
|
@GET
|
||||||
def list(): Response =
|
def list(): Response =
|
||||||
Response.ok(ExternalTournamentServerList(registry.list())).build()
|
Response.ok(ExternalTournamentServerList(registry.list())).build()
|
||||||
|
|
||||||
@POST
|
|
||||||
def register(req: RegisterServerRequest): Response =
|
|
||||||
Response.status(201).entity(registry.register(req.label, req.url)).build()
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
@Path("/{id}")
|
|
||||||
def remove(@PathParam("id") id: String): Response =
|
|
||||||
if registry.remove(id) then Response.noContent().build()
|
|
||||||
else Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Server $id not found")).build()
|
|
||||||
|
|||||||
+115
-3
@@ -1,10 +1,12 @@
|
|||||||
package de.nowchess.tournament.service
|
package de.nowchess.tournament.service
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
|
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
|
||||||
|
import de.nowchess.tournament.dto.ReplicateTournamentRequest
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import jakarta.ws.rs.client.{Client, ClientBuilder, Entity}
|
import jakarta.ws.rs.client.{Client, ClientBuilder, Entity}
|
||||||
import jakarta.ws.rs.core.MediaType
|
import jakarta.ws.rs.core.MediaType
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty
|
||||||
import scala.compiletime.uninitialized
|
import scala.compiletime.uninitialized
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
@@ -12,11 +14,106 @@ import scala.util.Try
|
|||||||
class ExternalTournamentClient:
|
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
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
private def buildClient(): Client = ClientBuilder.newClient()
|
private def buildClient(): Client = ClientBuilder.newClient()
|
||||||
|
|
||||||
|
// The tournament-server only accepts HS256 tokens it issued. Never forward a NowChessSystems
|
||||||
|
// RS256 user token to it — swap in the director token registered on that server.
|
||||||
|
private def normalize(url: String): String = url.stripSuffix("/")
|
||||||
|
|
||||||
|
def isNativeServer(serverUrl: String): Boolean =
|
||||||
|
nativeServerUrl.nonEmpty && normalize(serverUrl) == normalize(nativeServerUrl)
|
||||||
|
|
||||||
|
private def directorBearer(): Option[String] =
|
||||||
|
directorToken
|
||||||
|
.orElse {
|
||||||
|
val fresh = registerDirector(nativeServerUrl, directorName)
|
||||||
|
directorToken = fresh
|
||||||
|
fresh
|
||||||
|
}
|
||||||
|
.map(t => s"Bearer $t")
|
||||||
|
|
||||||
|
private def authFor(serverUrl: String, userAuth: Option[String]): Option[String] =
|
||||||
|
if isNativeServer(serverUrl) then directorBearer() else userAuth
|
||||||
|
|
||||||
|
def publishNative(serverUrl: String, directorName: String, form: String): Option[String] =
|
||||||
|
val token = directorToken.orElse {
|
||||||
|
val fresh = registerDirector(serverUrl, directorName)
|
||||||
|
directorToken = fresh
|
||||||
|
fresh
|
||||||
|
}
|
||||||
|
token.flatMap { tok =>
|
||||||
|
createNative(serverUrl, tok, form).orElse {
|
||||||
|
val refreshed = registerDirector(serverUrl, directorName)
|
||||||
|
directorToken = refreshed
|
||||||
|
refreshed.flatMap(createNative(serverUrl, _, form))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def registerDirector(serverUrl: String, name: String): Option[String] =
|
||||||
|
registerAccount(serverUrl, name, isBot = false)
|
||||||
|
|
||||||
|
private def registerAccount(serverUrl: String, name: String, isBot: Boolean): Option[String] =
|
||||||
|
Try {
|
||||||
|
val client = buildClient()
|
||||||
|
val body = s"""{"name":"${name.replace("\"", "\\\"")}","isBot":$isBot}"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
// The tournament server holds only the director token, which cannot join as a bot. Register the
|
||||||
|
// bot on the native server by name to mint a bot token, then join the native twin as that bot.
|
||||||
|
def joinNativeAsBot(serverUrl: String, tournamentId: String, botName: String): Boolean =
|
||||||
|
registerAccount(serverUrl, botName, isBot = true).exists { token =>
|
||||||
|
Try {
|
||||||
|
val client = buildClient()
|
||||||
|
val response = client
|
||||||
|
.target(s"$serverUrl/api/tournament/$tournamentId/join")
|
||||||
|
.request(MediaType.APPLICATION_JSON)
|
||||||
|
.header("Authorization", s"Bearer $token")
|
||||||
|
.post(Entity.json(""))
|
||||||
|
try response.getStatus / 100 == 2 || response.getStatus == 409
|
||||||
|
finally
|
||||||
|
response.close()
|
||||||
|
client.close()
|
||||||
|
}.getOrElse(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def createNative(serverUrl: String, token: String, form: String): Option[String] =
|
||||||
|
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
|
||||||
|
if response.getStatus / 100 == 2 then
|
||||||
|
Option(objectMapper.readTree(response.readEntity(classOf[String])).path("id").asText()).filter(_.nonEmpty)
|
||||||
|
else None
|
||||||
|
finally
|
||||||
|
response.close()
|
||||||
|
client.close()
|
||||||
|
}.getOrElse(None)
|
||||||
|
|
||||||
def fetchList(serverUrl: String): Option[JsonNode] =
|
def fetchList(serverUrl: String): Option[JsonNode] =
|
||||||
Try {
|
Try {
|
||||||
val client = buildClient()
|
val client = buildClient()
|
||||||
@@ -58,7 +155,7 @@ class ExternalTournamentClient:
|
|||||||
Try {
|
Try {
|
||||||
val client = buildClient()
|
val client = buildClient()
|
||||||
val builder = client.target(s"$serverUrl/$path").request(MediaType.APPLICATION_JSON)
|
val builder = client.target(s"$serverUrl/$path").request(MediaType.APPLICATION_JSON)
|
||||||
val withAuth = authHeader.fold(builder)(h => builder.header("Authorization", h))
|
val withAuth = authFor(serverUrl, authHeader).fold(builder)(h => builder.header("Authorization", h))
|
||||||
val response = withAuth.post(Entity.json(""))
|
val response = withAuth.post(Entity.json(""))
|
||||||
try (response.getStatus, response.readEntity(classOf[String]))
|
try (response.getStatus, response.readEntity(classOf[String]))
|
||||||
finally
|
finally
|
||||||
@@ -66,11 +163,26 @@ class ExternalTournamentClient:
|
|||||||
client.close()
|
client.close()
|
||||||
}.getOrElse((502, """{"error":"External server unreachable"}"""))
|
}.getOrElse((502, """{"error":"External server unreachable"}"""))
|
||||||
|
|
||||||
|
def replicateTournament(serverUrl: String, req: ReplicateTournamentRequest, selfUrl: String): Boolean =
|
||||||
|
Try {
|
||||||
|
val client = buildClient()
|
||||||
|
val body = objectMapper.writeValueAsString(req)
|
||||||
|
val response = client
|
||||||
|
.target(s"$serverUrl/api/tournament/replicate")
|
||||||
|
.request(MediaType.APPLICATION_JSON)
|
||||||
|
.header("X-Origin-Url", selfUrl)
|
||||||
|
.post(Entity.entity(body, MediaType.APPLICATION_JSON))
|
||||||
|
try response.getStatus / 100 == 2
|
||||||
|
finally
|
||||||
|
response.close()
|
||||||
|
client.close()
|
||||||
|
}.getOrElse(false)
|
||||||
|
|
||||||
def proxyGetStream(serverUrl: String, path: String, authHeader: Option[String]): Option[java.io.InputStream] =
|
def proxyGetStream(serverUrl: String, path: String, authHeader: Option[String]): Option[java.io.InputStream] =
|
||||||
Try {
|
Try {
|
||||||
val client = buildClient()
|
val client = buildClient()
|
||||||
val builder = client.target(s"$serverUrl/$path").request("application/x-ndjson")
|
val builder = client.target(s"$serverUrl/$path").request("application/x-ndjson")
|
||||||
val withAuth = authHeader.fold(builder)(h => builder.header("Authorization", h))
|
val withAuth = authFor(serverUrl, authHeader).fold(builder)(h => builder.header("Authorization", h))
|
||||||
val response = withAuth.get()
|
val response = withAuth.get()
|
||||||
if response.getStatus == 200 then Some(response.readEntity(classOf[java.io.InputStream]))
|
if response.getStatus == 200 then Some(response.readEntity(classOf[java.io.InputStream]))
|
||||||
else
|
else
|
||||||
|
|||||||
+13
-1
@@ -1,17 +1,29 @@
|
|||||||
package de.nowchess.tournament.service
|
package de.nowchess.tournament.service
|
||||||
|
|
||||||
import de.nowchess.tournament.dto.ExternalTournamentServer
|
import de.nowchess.tournament.dto.ExternalTournamentServer
|
||||||
|
import jakarta.annotation.PostConstruct
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import java.util.UUID
|
import org.eclipse.microprofile.config.inject.ConfigProperty
|
||||||
|
import java.util.{Optional, UUID}
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import scala.jdk.CollectionConverters.*
|
import scala.jdk.CollectionConverters.*
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
class TournamentServerRegistry:
|
class TournamentServerRegistry:
|
||||||
|
|
||||||
|
@ConfigProperty(name = "nowchess.tournament.external-servers")
|
||||||
|
var externalServers: Optional[String] = scala.compiletime.uninitialized
|
||||||
|
|
||||||
private val servers = new ConcurrentHashMap[String, ExternalTournamentServer]()
|
private val servers = new ConcurrentHashMap[String, ExternalTournamentServer]()
|
||||||
private val tournaments = new ConcurrentHashMap[String, String]()
|
private val tournaments = new ConcurrentHashMap[String, String]()
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
def init(): Unit =
|
||||||
|
if externalServers.isPresent then
|
||||||
|
externalServers.get().split(",").map(_.trim).filter(_.nonEmpty).foreach { url =>
|
||||||
|
register(url, url)
|
||||||
|
}
|
||||||
|
|
||||||
def register(label: String, url: String): ExternalTournamentServer =
|
def register(label: String, url: String): ExternalTournamentServer =
|
||||||
val id = UUID.randomUUID().toString
|
val id = UUID.randomUUID().toString
|
||||||
val server = ExternalTournamentServer(id, label, url.stripSuffix("/"))
|
val server = ExternalTournamentServer(id, label, url.stripSuffix("/"))
|
||||||
|
|||||||
+26
@@ -9,6 +9,7 @@ import de.nowchess.tournament.dto.{
|
|||||||
Clock,
|
Clock,
|
||||||
CreateTournamentForm,
|
CreateTournamentForm,
|
||||||
PairingDto,
|
PairingDto,
|
||||||
|
ReplicateTournamentRequest,
|
||||||
ResultDto,
|
ResultDto,
|
||||||
Standing,
|
Standing,
|
||||||
TournamentDto,
|
TournamentDto,
|
||||||
@@ -61,9 +62,34 @@ class TournamentService:
|
|||||||
tournamentRepository.persist(t)
|
tournamentRepository.persist(t)
|
||||||
t
|
t
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def replicate(req: ReplicateTournamentRequest, originServerUrl: String): Tournament =
|
||||||
|
val t = new Tournament()
|
||||||
|
t.id = req.id
|
||||||
|
t.fullName = req.fullName
|
||||||
|
t.nbRounds = req.nbRounds
|
||||||
|
t.clockLimit = req.clockLimit
|
||||||
|
t.clockIncrement = req.clockIncrement
|
||||||
|
t.rated = req.rated
|
||||||
|
t.status = req.status
|
||||||
|
t.currentRound = 0
|
||||||
|
t.createdBy = req.createdBy
|
||||||
|
t.startsAt = req.startsAt
|
||||||
|
t.originServerUrl = originServerUrl
|
||||||
|
tournamentRepository.persist(t)
|
||||||
|
t
|
||||||
|
|
||||||
def get(id: String): Option[Tournament] =
|
def get(id: String): Option[Tournament] =
|
||||||
tournamentRepository.findOptById(id)
|
tournamentRepository.findOptById(id)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def setNativeTournamentId(id: String, nativeId: String): Unit =
|
||||||
|
tournamentRepository.findOptById(id).foreach(_.nativeTournamentId = nativeId)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def markStatus(id: String, status: String): Unit =
|
||||||
|
tournamentRepository.findOptById(id).foreach(_.status = status)
|
||||||
|
|
||||||
def list(): (List[Tournament], List[Tournament], List[Tournament]) =
|
def list(): (List[Tournament], List[Tournament], List[Tournament]) =
|
||||||
(
|
(
|
||||||
tournamentRepository.findByStatus("created"),
|
tournamentRepository.findByStatus("created"),
|
||||||
|
|||||||
@@ -30,3 +30,7 @@ nowchess:
|
|||||||
secret: test-secret
|
secret: test-secret
|
||||||
auth:
|
auth:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
tournament:
|
||||||
|
self-url: ""
|
||||||
|
external-servers: ""
|
||||||
|
native-server-url: "http://localhost:1"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=4
|
MINOR=9
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
Reference in New Issue
Block a user