Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eeae4f01b4 | |||
| 45b5719d63 | |||
| b2683a7f5a | |||
| 3437dab49b | |||
| b72e8ec017 | |||
| 7136803c7e | |||
| faf7eb38ea | |||
| 344bed6935 | |||
| 4938560014 | |||
| 4a397eed7f | |||
| 9d656624d8 | |||
| e2b4342f60 |
@@ -1146,3 +1146,235 @@
|
||||
### 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))
|
||||
## (2026-06-29)
|
||||
|
||||
### 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
|
||||
|
||||
* **bot:** include quiet promotions in quiescence search ([4938560](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/49385600147021cd29f00a8eecc6be7ba8470717))
|
||||
* 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))
|
||||
## (2026-06-30)
|
||||
|
||||
### 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
|
||||
|
||||
* **bot:** include quiet promotions in quiescence search ([4938560](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/49385600147021cd29f00a8eecc6be7ba8470717))
|
||||
* **bot:** seed search with game history, add contempt and NNUE mop-up ([faf7eb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/faf7eb38ea7a0d3bc41ae4c2ef9a5195822f390c))
|
||||
* 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))
|
||||
## (2026-06-30)
|
||||
|
||||
### 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))
|
||||
* **bot:** add Lazy SMP parallel search for the NNUE bot ([3437dab](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3437dab49b2cc3f7b7e726febb07ad759e878079))
|
||||
* **bot:** clock-aware time management and stream-driven tournament play ([45b5719](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/45b5719d630c9480398f51510cc598d2d6ce39db))
|
||||
* **bot:** implement bot-vs-bot harness for NNUE evaluation ([b2683a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b2683a7f5ab6f3e41883736ac5eeaaee2e1ea5c1))
|
||||
* **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
|
||||
|
||||
* **bot:** include quiet promotions in quiescence search ([4938560](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/49385600147021cd29f00a8eecc6be7ba8470717))
|
||||
* **bot:** seed search with game history, add contempt and NNUE mop-up ([faf7eb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/faf7eb38ea7a0d3bc41ae4c2ef9a5195822f390c))
|
||||
* 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))
|
||||
|
||||
@@ -92,28 +92,7 @@
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from train import train_nnue, burst_train, DEFAULT_HIDDEN_SIZES\n",
|
||||
"\n",
|
||||
"WEIGHTS_DIR = Path(DRIVE_ROOT) / 'weights'\n",
|
||||
"WEIGHTS_DIR.mkdir(parents=True, exist_ok=True)\n",
|
||||
"OUTPUT_FILE = str(WEIGHTS_DIR / 'nnue_weights.pt')\n",
|
||||
"\n",
|
||||
"# ── Training hyperparameters ──────────────────────────────────────────────────\n",
|
||||
"HIDDEN_SIZES = DEFAULT_HIDDEN_SIZES # [1536, 1024, 512, 256]\n",
|
||||
"BATCH_SIZE = 16384\n",
|
||||
"EPOCHS = 100\n",
|
||||
"EARLY_STOPPING = 10 # None to disable\n",
|
||||
"SUBSAMPLE_RATIO = 1.0\n",
|
||||
"\n",
|
||||
"# Resume from latest checkpoint if one exists\n",
|
||||
"checkpoints = sorted(WEIGHTS_DIR.glob('nnue_weights_v*.pt'))\n",
|
||||
"CHECKPOINT = str(checkpoints[-1]) if checkpoints else None\n",
|
||||
"if CHECKPOINT:\n",
|
||||
" print(f'Resuming from checkpoint: {CHECKPOINT}')\n",
|
||||
"else:\n",
|
||||
" print('Starting training from scratch.')"
|
||||
],
|
||||
"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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,6 +13,11 @@ import chess
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
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):
|
||||
@@ -77,9 +82,12 @@ class NNUEDataset(Dataset):
|
||||
def __getitem__(self, idx):
|
||||
fen = self.positions[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_features; negate eval
|
||||
# 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
|
||||
@@ -90,7 +98,7 @@ class NNUEDataset(Dataset):
|
||||
else:
|
||||
target = torch.sigmoid(torch.tensor(eval_val / 400.0, dtype=torch.float32))
|
||||
|
||||
return features, target
|
||||
return indices, target
|
||||
|
||||
# King-relative (HalfKP) encoding: two perspectives, one per side's king.
|
||||
# Each piece is encoded as: kingSq * 768 + pieceIdx * 64 + sq
|
||||
@@ -105,14 +113,14 @@ _PIECE_TO_IDX = {
|
||||
}
|
||||
|
||||
|
||||
def fen_to_features(fen):
|
||||
"""Convert FEN to 98304-dim king-relative (HalfKP) feature vector.
|
||||
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.
|
||||
perspective. The caller is responsible for negating the eval label to match.
|
||||
"""
|
||||
features = torch.zeros(INPUT_SIZE, dtype=torch.float32)
|
||||
indices = []
|
||||
try:
|
||||
board = chess.Board(fen)
|
||||
# Perspective flip: present all positions as if White is to move
|
||||
@@ -121,20 +129,41 @@ def fen_to_features(fen):
|
||||
wk = board.king(chess.WHITE)
|
||||
bk = board.king(chess.BLACK)
|
||||
if wk is None or bk is None:
|
||||
return features
|
||||
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)
|
||||
features[wk * 768 + pidx * 64 + sq] = 1.0
|
||||
indices.append(wk * 768 + pidx * 64 + sq)
|
||||
# Black-king perspective (indices _HALF_SIZE .. INPUT_SIZE-1)
|
||||
features[_HALF_SIZE + bk * 768 + pidx * 64 + sq] = 1.0
|
||||
indices.append(_HALF_SIZE + bk * 768 + pidx * 64 + sq)
|
||||
except Exception:
|
||||
pass
|
||||
return torch.zeros(0, dtype=torch.long)
|
||||
return torch.tensor(indices, dtype=torch.long)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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.
|
||||
@@ -256,17 +285,19 @@ def _setup_training(data_file, batch_size, subsample_ratio):
|
||||
train_dataset,
|
||||
batch_size=batch_size,
|
||||
sampler=train_sampler,
|
||||
num_workers=8,
|
||||
num_workers=LOADER_WORKERS,
|
||||
pin_memory=True,
|
||||
persistent_workers=True
|
||||
persistent_workers=LOADER_WORKERS > 0,
|
||||
collate_fn=_collate_sparse,
|
||||
)
|
||||
val_loader = DataLoader(
|
||||
val_dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=False,
|
||||
num_workers=8,
|
||||
num_workers=LOADER_WORKERS,
|
||||
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
|
||||
@@ -304,8 +335,9 @@ def _run_training_season(
|
||||
model.train()
|
||||
train_loss = 0.0
|
||||
with tqdm(total=len(train_loader), desc=f"Epoch {epoch_display}/{total_epochs} - Train") as pbar:
|
||||
for batch_features, batch_targets in train_loader:
|
||||
batch_features = batch_features.to(device)
|
||||
for (rows, cols, bsz), batch_targets in train_loader:
|
||||
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)
|
||||
|
||||
optimizer.zero_grad()
|
||||
@@ -318,7 +350,7 @@ def _run_training_season(
|
||||
scaler.step(optimizer)
|
||||
scaler.update()
|
||||
|
||||
train_loss += loss.item() * batch_features.size(0)
|
||||
train_loss += loss.item() * bsz
|
||||
pbar.update(1)
|
||||
|
||||
train_loss /= len(train_dataset)
|
||||
@@ -328,14 +360,15 @@ def _run_training_season(
|
||||
val_loss = 0.0
|
||||
with torch.no_grad():
|
||||
with tqdm(total=len(val_loader), desc=f"Epoch {epoch_display}/{total_epochs} - Val") as pbar:
|
||||
for batch_features, batch_targets in val_loader:
|
||||
batch_features = batch_features.to(device)
|
||||
for (rows, cols, bsz), batch_targets in val_loader:
|
||||
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)
|
||||
|
||||
with torch.amp.autocast('cuda' if torch.cuda.is_available() else 'cpu'):
|
||||
outputs = model(batch_features)
|
||||
loss = criterion(outputs, batch_targets)
|
||||
val_loss += loss.item() * batch_features.size(0)
|
||||
val_loss += loss.item() * bsz
|
||||
pbar.update(1)
|
||||
|
||||
val_loss /= len(val_dataset)
|
||||
|
||||
@@ -3,4 +3,33 @@ package de.nowchess.bot
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
|
||||
type Bot = GameContext => Option[Move]
|
||||
/** Remaining wall-clock for the side to move and the Fischer increment, both in milliseconds. [[TimeControl.Unlimited]]
|
||||
* is the sentinel for callers without a real clock (local play, self-play, tests): bots then fall back to their own
|
||||
* fixed budgets.
|
||||
*/
|
||||
final case class TimeControl(remainingMs: Long, incrementMs: Long):
|
||||
def isClocked: Boolean = remainingMs >= 0L
|
||||
def budgetMs: Long = TimeControl.budget(remainingMs, incrementMs)
|
||||
|
||||
object TimeControl:
|
||||
val Unlimited: TimeControl = TimeControl(-1L, 0L)
|
||||
|
||||
private val OverheadMs = 1500L
|
||||
private val PanicMs = 20000L
|
||||
private val MaxBudget = 8000L
|
||||
private val PanicCap = 2500L
|
||||
private val FloorMs = 50L
|
||||
|
||||
def budget(remainingMs: Long, incrementMs: Long): Long =
|
||||
val usable = math.max(0L, remainingMs - OverheadMs)
|
||||
if usable <= 0L then FloorMs
|
||||
else if remainingMs < PanicMs then clamp(usable / 15 + incrementMs / 2, PanicCap)
|
||||
else clamp(usable / 30 + incrementMs * 4 / 5, MaxBudget)
|
||||
|
||||
private def clamp(value: Long, ceiling: Long): Long =
|
||||
math.max(FloorMs, math.min(value, ceiling))
|
||||
|
||||
trait Bot:
|
||||
def move(context: GameContext, time: TimeControl): Option[Move]
|
||||
def apply(context: GameContext): Option[Move] = move(context, TimeControl.Unlimited)
|
||||
def apply(context: GameContext, time: TimeControl): Option[Move] = move(context, time)
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
package de.nowchess.bot.bots
|
||||
|
||||
import de.nowchess.bot.Bot
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.api.rules.RuleSet
|
||||
import de.nowchess.bot.bots.classic.EvaluationClassic
|
||||
import de.nowchess.bot.logic.AlphaBetaSearch
|
||||
import de.nowchess.bot.util.PolyglotBook
|
||||
import de.nowchess.bot.{BotDifficulty, BotMoveRepetition}
|
||||
import de.nowchess.bot.{Bot, BotDifficulty, BotMoveRepetition, TimeControl}
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
|
||||
object ClassicalBot:
|
||||
private val defaultBudgetMs = 1000L
|
||||
|
||||
def apply(
|
||||
difficulty: BotDifficulty,
|
||||
rules: RuleSet = DefaultRules,
|
||||
book: Option[PolyglotBook] = None,
|
||||
): Bot =
|
||||
val search = AlphaBetaSearch(rules, weights = EvaluationClassic)
|
||||
val timeBudgetMs = 1000L
|
||||
context =>
|
||||
val blockedMoves = BotMoveRepetition.blockedMoves(context)
|
||||
book
|
||||
.flatMap(_.probe(context))
|
||||
.filterNot(blockedMoves.contains)
|
||||
.orElse(search.bestMoveWithTime(context, timeBudgetMs, blockedMoves))
|
||||
val search = AlphaBetaSearch(rules, weights = EvaluationClassic)
|
||||
new Bot:
|
||||
def move(context: GameContext, time: TimeControl): Option[Move] =
|
||||
val budget = if time.isClocked then time.budgetMs else defaultBudgetMs
|
||||
val blockedMoves = BotMoveRepetition.blockedMoves(context)
|
||||
book
|
||||
.flatMap(_.probe(context))
|
||||
.filterNot(blockedMoves.contains)
|
||||
.orElse(search.bestMoveWithTime(context, budget, blockedMoves))
|
||||
|
||||
@@ -7,12 +7,20 @@ import de.nowchess.api.rules.RuleSet
|
||||
import de.nowchess.bot.ai.Evaluation
|
||||
import de.nowchess.bot.bots.classic.EvaluationClassic
|
||||
import de.nowchess.bot.bots.nnue.EvaluationNNUE
|
||||
import de.nowchess.bot.logic.{AlphaBetaSearch, TranspositionTable}
|
||||
import de.nowchess.bot.logic.{ParallelSearch, TranspositionTable}
|
||||
import de.nowchess.bot.util.PolyglotBook
|
||||
import de.nowchess.bot.{BotDifficulty, BotMoveRepetition, Config}
|
||||
import de.nowchess.bot.{Bot, BotDifficulty, BotMoveRepetition, Config, TimeControl}
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
|
||||
object HybridBot:
|
||||
|
||||
private def defaultThreads: Int =
|
||||
sys.env.get("NNUE_SEARCH_THREADS").flatMap(_.toIntOption).filter(_ >= 1).getOrElse(1)
|
||||
|
||||
// The veto re-search must share the move's budget, not double it: give the main search the bulk and
|
||||
// reserve a slice for the at-most-one veto re-search so a vetoed move never costs two full budgets.
|
||||
private val MainSearchShare = 0.7
|
||||
|
||||
def apply(
|
||||
difficulty: BotDifficulty,
|
||||
rules: RuleSet = DefaultRules,
|
||||
@@ -20,28 +28,34 @@ object HybridBot:
|
||||
nnueEvaluation: Evaluation = EvaluationNNUE,
|
||||
classicalEvaluation: Evaluation = EvaluationClassic,
|
||||
vetoReporter: String => Unit = println(_),
|
||||
searchThreads: Int = defaultThreads,
|
||||
): Bot =
|
||||
val search = AlphaBetaSearch(rules, TranspositionTable(), classicalEvaluation)
|
||||
context =>
|
||||
val blockedMoves = BotMoveRepetition.blockedMoves(context)
|
||||
// Use ParallelSearch to enable multi-threaded (SMP) search similar to NNUEBot
|
||||
val search = ParallelSearch(rules, TranspositionTable(), () => classicalEvaluation, searchThreads)
|
||||
new Bot:
|
||||
def move(context: GameContext, time: TimeControl): Option[Move] =
|
||||
val totalBudget = if time.isClocked then time.budgetMs else Config.TIME_LIMIT_MS
|
||||
val mainBudget = math.max(1L, (totalBudget * MainSearchShare).toLong)
|
||||
val vetoBudget = math.max(1L, totalBudget - mainBudget)
|
||||
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 nnueScore(m: Move): Int = nnueEvaluation.evaluate(rules.applyMove(context)(m))
|
||||
def classicalScore(m: Move): Int = classicalEvaluation.evaluate(rules.applyMove(context)(m))
|
||||
|
||||
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)
|
||||
def refine(m: Move): Move =
|
||||
val moveNnue = nnueScore(m)
|
||||
if (classicalScore(m) - moveNnue).abs <= Config.VETO_THRESHOLD then m
|
||||
else
|
||||
search
|
||||
.bestMoveWithTime(context, vetoBudget, blockedMoves + m)
|
||||
.filterNot(blockedMoves.contains)
|
||||
.filter(alt => nnueScore(alt) < moveNnue)
|
||||
.map { alt =>
|
||||
vetoReporter(f"[Veto] ${m.from}->${m.to} replaced by ${alt.from}->${alt.to} — NNUE prefers it")
|
||||
alt
|
||||
}
|
||||
.getOrElse(m)
|
||||
|
||||
book.flatMap(_.probe(context)).filterNot(blockedMoves.contains).orElse {
|
||||
search.bestMoveWithTime(context, Config.TIME_LIMIT_MS, blockedMoves).map(refine)
|
||||
}
|
||||
book.flatMap(_.probe(context)).filterNot(blockedMoves.contains).orElse {
|
||||
search.bestMoveWithTime(context, mainBudget, blockedMoves).map(refine)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
package de.nowchess.bot.bots
|
||||
|
||||
import de.nowchess.bot.Bot
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.api.rules.RuleSet
|
||||
import de.nowchess.bot.bots.nnue.EvaluationNNUE
|
||||
import de.nowchess.bot.logic.AlphaBetaSearch
|
||||
import de.nowchess.bot.logic.{ParallelSearch, TranspositionTable}
|
||||
import de.nowchess.bot.util.{PolyglotBook, ZobristHash}
|
||||
import de.nowchess.bot.{BotDifficulty, BotMoveRepetition}
|
||||
import de.nowchess.bot.{Bot, BotDifficulty, BotMoveRepetition, TimeControl}
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
|
||||
object NNUEBot:
|
||||
private def defaultThreads: Int =
|
||||
sys.env.get("NNUE_SEARCH_THREADS").flatMap(_.toIntOption).filter(_ >= 1).getOrElse(1)
|
||||
|
||||
def apply(
|
||||
difficulty: BotDifficulty,
|
||||
rules: RuleSet = DefaultRules,
|
||||
book: Option[PolyglotBook] = None,
|
||||
fixedMoveTimeMs: Option[Long] = None,
|
||||
searchThreads: Int = defaultThreads,
|
||||
): Bot =
|
||||
val search = AlphaBetaSearch(rules, weights = EvaluationNNUE)
|
||||
context =>
|
||||
val blockedMoves = BotMoveRepetition.blockedMoves(context)
|
||||
book
|
||||
.flatMap(_.probe(context))
|
||||
.filterNot(blockedMoves.contains)
|
||||
.orElse {
|
||||
val moves = BotMoveRepetition.filterAllowed(context, rules.allLegalMoves(context))
|
||||
if moves.isEmpty then None
|
||||
else
|
||||
val scored = batchEvaluateRoot(rules, context, moves)
|
||||
val bestMove = scored.maxBy(_._2)._1
|
||||
val budget = fixedMoveTimeMs.getOrElse(allocateTime(scored))
|
||||
search.bestMoveWithTime(context, budget, blockedMoves, scored.toMap).orElse(Some(bestMove))
|
||||
}
|
||||
val search = ParallelSearch(rules, TranspositionTable(), () => EvaluationNNUE.freshEvaluator(), searchThreads)
|
||||
new Bot:
|
||||
def move(context: GameContext, time: TimeControl): Option[Move] =
|
||||
val blockedMoves = BotMoveRepetition.blockedMoves(context)
|
||||
book
|
||||
.flatMap(_.probe(context))
|
||||
.filterNot(blockedMoves.contains)
|
||||
.orElse {
|
||||
val moves = BotMoveRepetition.filterAllowed(context, rules.allLegalMoves(context))
|
||||
if moves.isEmpty then None
|
||||
else
|
||||
val scored = batchEvaluateRoot(rules, context, moves)
|
||||
val bestMove = scored.maxBy(_._2)._1
|
||||
val budget = fixedMoveTimeMs.getOrElse(if time.isClocked then time.budgetMs else 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)] =
|
||||
EvaluationNNUE.initAccumulator(context)
|
||||
|
||||
+32
-5
@@ -4,15 +4,17 @@ import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.bot.ai.Evaluation
|
||||
|
||||
object EvaluationNNUE extends Evaluation:
|
||||
|
||||
private val nnue = NNUE(NbaiLoader.loadDefault())
|
||||
/** One independent NNUE evaluator: wraps its own [[NNUE]] (own accumulator stack, scratch buffers and eval cache) plus
|
||||
* the endgame mop-up correction. Independent instances may run concurrently as long as they share only the read-only
|
||||
* [[NNUEWeights]].
|
||||
*/
|
||||
final class NNUEEvaluator(nnue: NNUE) extends Evaluation:
|
||||
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
|
||||
/** Full-board evaluate — used as fallback and by non-search callers. */
|
||||
def evaluate(context: GameContext): Int = nnue.evaluate(context)
|
||||
def evaluate(context: GameContext): Int = nnue.evaluate(context) + MopUp.score(context)
|
||||
|
||||
// ── Accumulator hooks (incremental L1) ───────────────────────────────────
|
||||
|
||||
@@ -28,4 +30,29 @@ object EvaluationNNUE extends Evaluation:
|
||||
else nnue.pushAccumulator(childPly, move, parent.board, child.board)
|
||||
|
||||
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) + MopUp.score(context)
|
||||
|
||||
/** Default singleton evaluator plus a factory for independent per-thread evaluators that share the loaded weights. */
|
||||
object EvaluationNNUE extends Evaluation:
|
||||
|
||||
private val weights = NNUEWeights(NbaiLoader.loadDefault())
|
||||
private val default = NNUEEvaluator(NNUE(weights))
|
||||
|
||||
/** Build a fresh evaluator backed by its own [[NNUE]] but sharing the immutable [[weights]] — one per search thread.
|
||||
*/
|
||||
def freshEvaluator(): Evaluation = NNUEEvaluator(NNUE(weights))
|
||||
|
||||
val CHECKMATE_SCORE: Int = default.CHECKMATE_SCORE
|
||||
val DRAW_SCORE: Int = default.DRAW_SCORE
|
||||
|
||||
def evaluate(context: GameContext): Int = default.evaluate(context)
|
||||
|
||||
override def initAccumulator(context: GameContext): Unit = default.initAccumulator(context)
|
||||
|
||||
override def copyAccumulator(parentPly: Int, childPly: Int): Unit = default.copyAccumulator(parentPly, childPly)
|
||||
|
||||
override def pushAccumulator(childPly: Int, move: Move, parent: GameContext, child: GameContext): Unit =
|
||||
default.pushAccumulator(childPly, move, parent, child)
|
||||
|
||||
override def evaluateAccumulator(ply: Int, context: GameContext, hash: Long): Int =
|
||||
default.evaluateAccumulator(ply, context, hash)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.nowchess.bot.bots.nnue
|
||||
|
||||
import de.nowchess.api.board.{Color, PieceType, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
|
||||
/** Endgame "mop-up" correction for the NNUE evaluation.
|
||||
*
|
||||
* Pure NNUE lacks explicit mating knowledge, so KX-vs-K conversions stall. When one side is reduced to a lone king and
|
||||
* the other holds sufficient mating material, this term rewards driving the bare king to the edge and walking the
|
||||
* winning king in. Returns a value from the side-to-move perspective (positive = good for side to move). Zero in any
|
||||
* position that is not a lone-king endgame, so middlegame NNUE output is untouched.
|
||||
*/
|
||||
object MopUp:
|
||||
|
||||
private val EDGE_WEIGHT = 10
|
||||
private val PROXIMITY_WEIGHT = 4
|
||||
private val MIN_WINNER_VALUE = 400
|
||||
|
||||
def score(context: GameContext): Int =
|
||||
loneKingColor(context) match
|
||||
case None => 0
|
||||
case Some(loser) =>
|
||||
val winner = loser.opposite
|
||||
if winnerValue(context, winner) < MIN_WINNER_VALUE then 0
|
||||
else mopUp(context, winner, loser) * (if context.turn == winner then 1 else -1)
|
||||
|
||||
private def mopUp(context: GameContext, winner: Color, loser: Color): Int =
|
||||
(for
|
||||
loserKing <- context.kingSquare(loser)
|
||||
winnerKing <- context.kingSquare(winner)
|
||||
yield EDGE_WEIGHT * centerDistance(loserKing) +
|
||||
PROXIMITY_WEIGHT * (14 - kingDistance(winnerKing, loserKing))).getOrElse(0)
|
||||
|
||||
private def loneKingColor(context: GameContext): Option[Color] =
|
||||
val nonKing = context.board.pieces.values.filter(_.pieceType != PieceType.King)
|
||||
val whiteHasOther = nonKing.exists(_.color == Color.White)
|
||||
val blackHasOther = nonKing.exists(_.color == Color.Black)
|
||||
if whiteHasOther == blackHasOther then None
|
||||
else if whiteHasOther then Some(Color.Black)
|
||||
else Some(Color.White)
|
||||
|
||||
private def winnerValue(context: GameContext, winner: Color): Int =
|
||||
context.board.pieces.values.foldLeft(0) { (sum, piece) =>
|
||||
if piece.color != winner then sum
|
||||
else
|
||||
sum + (piece.pieceType match
|
||||
case PieceType.Queen => 900
|
||||
case PieceType.Rook => 500
|
||||
case PieceType.Bishop => 330
|
||||
case PieceType.Knight => 320
|
||||
case _ => 0
|
||||
)
|
||||
}
|
||||
|
||||
private def centerDistance(sq: Square): Int =
|
||||
val fileDist = math.max(3 - sq.file.ordinal, sq.file.ordinal - 4)
|
||||
val rankDist = math.max(3 - sq.rank.ordinal, sq.rank.ordinal - 4)
|
||||
fileDist + rankDist
|
||||
|
||||
private def kingDistance(a: Square, b: Square): Int =
|
||||
(a.file.ordinal - b.file.ordinal).abs + (a.rank.ordinal - b.rank.ordinal).abs
|
||||
@@ -4,20 +4,20 @@ import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
|
||||
class NNUE(model: NbaiModel):
|
||||
object NNUE:
|
||||
def apply(model: NbaiModel): NNUE = new NNUE(NNUEWeights(model))
|
||||
def apply(weights: NNUEWeights): NNUE = new NNUE(weights)
|
||||
|
||||
/** Per-thread NNUE evaluator: owns the mutable accumulator stack, scratch buffers and eval cache, while sharing the
|
||||
* read-only [[NNUEWeights]]. Construct one instance per search thread (cheap — only buffer allocation); they may all
|
||||
* share a single weights instance.
|
||||
*/
|
||||
class NNUE(weights: NNUEWeights):
|
||||
|
||||
import weights.{accSize, l1WeightsT, model, HALF_SIZE}
|
||||
|
||||
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 validateAccum = sys.env.contains("NNUE_VALIDATE")
|
||||
|
||||
// Column-major L1 weights: l1WeightsT(featureIdx * accSize + outputIdx)
|
||||
private val l1WeightsT: Array[Float] =
|
||||
val w = model.weights(0).weights
|
||||
val t = new Array[Float](featureSize * accSize)
|
||||
for j <- 0 until featureSize; i <- 0 until accSize do t(j * accSize + i) = w(i * featureSize + j)
|
||||
t
|
||||
|
||||
// ── Accumulator stack ────────────────────────────────────────────────────
|
||||
|
||||
private val MAX_PLY = 128
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.nowchess.bot.bots.nnue
|
||||
|
||||
/** Immutable, shareable NNUE parameters.
|
||||
*
|
||||
* Heavy to build (transposes the L1 weight matrix once, ~98304 × accSize floats) but read-only thereafter, so a single
|
||||
* instance is safely shared across many per-thread [[NNUE]] evaluators. Holds no accumulator or scratch state — those
|
||||
* live on each [[NNUE]] instance — which is what makes parallel search (independent evaluators sharing these weights)
|
||||
* possible without duplicating the weight matrix.
|
||||
*/
|
||||
class NNUEWeights(val model: NbaiModel):
|
||||
|
||||
val HALF_SIZE: Int = 49152 // 64 king-squares × 12 piece-types × 64 piece-squares
|
||||
val featureSize: Int = model.layers(0).inputSize // 98304 (= HALF_SIZE * 2) for king-relative
|
||||
val accSize: Int = model.layers(0).outputSize
|
||||
|
||||
// Column-major L1 weights: l1WeightsT(featureIdx * accSize + outputIdx)
|
||||
val l1WeightsT: Array[Float] =
|
||||
val w = model.weights(0).weights
|
||||
val t = new Array[Float](featureSize * accSize)
|
||||
for j <- 0 until featureSize; i <- 0 until accSize do t(j * accSize + i) = w(i * featureSize + j)
|
||||
t
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.nowchess.bot.logic
|
||||
|
||||
import de.nowchess.api.board.PieceType
|
||||
import de.nowchess.api.board.{CastlingRights, PieceType}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType}
|
||||
import de.nowchess.bot.ai.Evaluation
|
||||
@@ -26,6 +26,7 @@ final class AlphaBetaSearch(
|
||||
private val TIME_CHECK_FREQUENCY = 1000
|
||||
private val FUTILITY_MARGIN = 100
|
||||
private val CHECK_EXTENSION = 1
|
||||
private val CONTEMPT = 25
|
||||
|
||||
private val timeStartMs = AtomicLong(0L)
|
||||
private val timeLimitMs = AtomicLong(0L)
|
||||
@@ -67,6 +68,7 @@ final class AlphaBetaSearch(
|
||||
timeLimitMs.set(Long.MaxValue / 4)
|
||||
nodeCount.set(0)
|
||||
val rootHash = ZobristHash.hash(context)
|
||||
val history = historyCounts(context)
|
||||
(1 to maxDepth)
|
||||
.foldLeft((None: Option[Move], 0)) { case ((bestSoFar, prevScore), depth) =>
|
||||
val (alpha, beta) =
|
||||
@@ -78,6 +80,7 @@ final class AlphaBetaSearch(
|
||||
beta,
|
||||
ASPIRATION_DELTA,
|
||||
rootHash,
|
||||
history,
|
||||
excludedRootMoves,
|
||||
hints,
|
||||
)
|
||||
@@ -92,7 +95,7 @@ final class AlphaBetaSearch(
|
||||
bestMoveWithTime(context, timeBudgetMs, Set.empty)
|
||||
|
||||
def bestMoveWithTime(context: GameContext, timeBudgetMs: Long, excludedRootMoves: Set[Move]): Option[Move] =
|
||||
doTimedSearch(context, timeBudgetMs, excludedRootMoves, Map.empty)
|
||||
doTimedSearch(context, timeBudgetMs, excludedRootMoves, Map.empty, clearTt = true)
|
||||
|
||||
def bestMoveWithTime(
|
||||
context: GameContext,
|
||||
@@ -100,21 +103,34 @@ final class AlphaBetaSearch(
|
||||
excludedRootMoves: Set[Move],
|
||||
hints: Map[Move, Int],
|
||||
): Option[Move] =
|
||||
doTimedSearch(context, timeBudgetMs, excludedRootMoves, hints)
|
||||
doTimedSearch(context, timeBudgetMs, excludedRootMoves, hints, clearTt = true)
|
||||
|
||||
/** Timed search over a transposition table that is shared with other workers (Lazy SMP): the caller is responsible
|
||||
* for clearing it once before launching all workers, so this worker must not clear it.
|
||||
*/
|
||||
def bestMoveWithTimeSharedTt(
|
||||
context: GameContext,
|
||||
timeBudgetMs: Long,
|
||||
excludedRootMoves: Set[Move],
|
||||
hints: Map[Move, Int],
|
||||
): Option[Move] =
|
||||
doTimedSearch(context, timeBudgetMs, excludedRootMoves, hints, clearTt = false)
|
||||
|
||||
private def doTimedSearch(
|
||||
context: GameContext,
|
||||
timeBudgetMs: Long,
|
||||
excludedRootMoves: Set[Move],
|
||||
hints: Map[Move, Int],
|
||||
clearTt: Boolean,
|
||||
): Option[Move] =
|
||||
tt.clear()
|
||||
if clearTt then tt.clear()
|
||||
ordering.clear()
|
||||
weights.initAccumulator(context)
|
||||
timeStartMs.set(System.currentTimeMillis)
|
||||
timeLimitMs.set(timeBudgetMs)
|
||||
nodeCount.set(0)
|
||||
val rootHash = ZobristHash.hash(context)
|
||||
val history = historyCounts(context)
|
||||
|
||||
@scala.annotation.tailrec
|
||||
def loop(bestSoFar: Option[Move], prevScore: Int, depth: Int, lastDepth: Int): (Option[Move], Int) =
|
||||
@@ -129,6 +145,7 @@ final class AlphaBetaSearch(
|
||||
beta,
|
||||
ASPIRATION_DELTA,
|
||||
rootHash,
|
||||
history,
|
||||
excludedRootMoves,
|
||||
hints,
|
||||
)
|
||||
@@ -154,10 +171,11 @@ final class AlphaBetaSearch(
|
||||
beta: Int,
|
||||
initialWindow: Int,
|
||||
rootHash: Long,
|
||||
history: Map[Long, Int],
|
||||
excludedRootMoves: Set[Move],
|
||||
hints: Map[Move, Int],
|
||||
): (Int, Option[Move]) =
|
||||
val state = SearchState(rootHash, Map(rootHash -> 1))
|
||||
val state = SearchState(rootHash, history)
|
||||
|
||||
@scala.annotation.tailrec
|
||||
def loop(currentAlpha: Int, currentBeta: Int, delta: Int, attempt: Int): (Int, Option[Move]) =
|
||||
@@ -173,6 +191,32 @@ final class AlphaBetaSearch(
|
||||
|
||||
loop(alpha, beta, initialWindow, 0)
|
||||
|
||||
private def drawScore(ply: Int): Int =
|
||||
if ply % 2 == 0 then weights.DRAW_SCORE - CONTEMPT else weights.DRAW_SCORE + CONTEMPT
|
||||
|
||||
private def historyCounts(context: GameContext): Map[Long, Int] =
|
||||
val initialTurn = if context.moves.size % 2 == 0 then context.turn else context.turn.opposite
|
||||
val root = GameContext(
|
||||
board = context.initialBoard,
|
||||
turn = initialTurn,
|
||||
castlingRights = CastlingRights.Initial,
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moves = List.empty,
|
||||
)
|
||||
val rootHash = ZobristHash.hash(root)
|
||||
context.moves
|
||||
.foldLeft((root, rootHash, List(rootHash))) { case ((cur, curHash, acc), move) =>
|
||||
val next = rules.applyMove(cur)(move)
|
||||
val nextHash = ZobristHash.nextHash(cur, curHash, move, next)
|
||||
(next, nextHash, nextHash :: acc)
|
||||
}
|
||||
._3
|
||||
.groupBy(identity)
|
||||
.view
|
||||
.mapValues(_.size)
|
||||
.toMap
|
||||
|
||||
private def hasNonPawnMaterial(context: GameContext): Boolean =
|
||||
context.board.pieces.values.exists { piece =>
|
||||
piece.color == context.turn &&
|
||||
@@ -226,7 +270,8 @@ final class AlphaBetaSearch(
|
||||
): Option[(Int, Option[Move])] =
|
||||
if count % TIME_CHECK_FREQUENCY == 0 && isOutOfTime then
|
||||
Some((weights.evaluateAccumulator(params.ply, params.context, params.state.hash), None))
|
||||
else if params.state.repetitions.getOrElse(params.state.hash, 0) >= 3 then Some((weights.DRAW_SCORE, None))
|
||||
else if params.ply > 0 && params.state.repetitions.getOrElse(params.state.hash, 0) >= 2 then
|
||||
Some((drawScore(params.ply), None))
|
||||
else ttCutoff(params)
|
||||
|
||||
private def ttCutoff(params: SearchParams): Option[(Int, Option[Move])] =
|
||||
@@ -248,12 +293,12 @@ final class AlphaBetaSearch(
|
||||
if legalMoves.isEmpty then
|
||||
Some(
|
||||
(
|
||||
if rules.isCheckmate(params.context) then -(weights.CHECKMATE_SCORE - params.ply) else weights.DRAW_SCORE,
|
||||
if rules.isCheckmate(params.context) then -(weights.CHECKMATE_SCORE - params.ply) else drawScore(params.ply),
|
||||
None,
|
||||
),
|
||||
)
|
||||
else if rules.isInsufficientMaterial(params.context) || rules.isFiftyMoveRule(params.context) then
|
||||
Some((weights.DRAW_SCORE, None))
|
||||
Some((drawScore(params.ply), None))
|
||||
else if params.depth == 0 then
|
||||
Some((quiescence(params.context, params.ply, params.window.alpha, params.window.beta, params.state.hash), None))
|
||||
else None
|
||||
@@ -468,5 +513,5 @@ final class AlphaBetaSearch(
|
||||
private def isCapture(context: GameContext, move: Move): Boolean = move.moveType match
|
||||
case MoveType.Normal(true) => true
|
||||
case MoveType.EnPassant => true
|
||||
case MoveType.Promotion(_) => context.board.pieceAt(move.to).exists(_.color != context.turn)
|
||||
case MoveType.Promotion(_) => true
|
||||
case _ => false
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.nowchess.bot.logic
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.api.rules.RuleSet
|
||||
import de.nowchess.bot.ai.Evaluation
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
|
||||
import java.util.concurrent.{Callable, Executors}
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
/** Lazy SMP search coordinator.
|
||||
*
|
||||
* Runs `numThreads` independent [[AlphaBetaSearch]] workers over one shared transposition table for the same time
|
||||
* budget. Every worker has its own evaluator (independent NNUE accumulator) and move-ordering state, but they share
|
||||
* the thread-safe TT, so faster-progressing threads deepen entries the others reuse. Only the main worker's move is
|
||||
* returned; helpers exist purely to enrich the shared TT.
|
||||
*
|
||||
* `numThreads <= 1` runs a single worker via the ordinary clearing entry point, byte-identical to sequential
|
||||
* [[AlphaBetaSearch]].
|
||||
*/
|
||||
final class ParallelSearch(
|
||||
rules: RuleSet = DefaultRules,
|
||||
tt: TranspositionTable = TranspositionTable(),
|
||||
evalFactory: () => Evaluation,
|
||||
numThreads: Int = 1,
|
||||
):
|
||||
|
||||
private val threadCount = math.max(1, numThreads)
|
||||
private val workers = Vector.fill(threadCount)(AlphaBetaSearch(rules, tt, evalFactory()))
|
||||
|
||||
def bestMoveWithTime(
|
||||
context: GameContext,
|
||||
timeBudgetMs: Long,
|
||||
excludedRootMoves: Set[Move] = Set.empty,
|
||||
hints: Map[Move, Int] = Map.empty,
|
||||
): Option[Move] =
|
||||
if threadCount == 1 then workers.head.bestMoveWithTime(context, timeBudgetMs, excludedRootMoves, hints)
|
||||
else runParallel(context, timeBudgetMs, excludedRootMoves, hints)
|
||||
|
||||
private def runParallel(
|
||||
context: GameContext,
|
||||
timeBudgetMs: Long,
|
||||
excludedRootMoves: Set[Move],
|
||||
hints: Map[Move, Int],
|
||||
): Option[Move] =
|
||||
tt.clear()
|
||||
val pool = Executors.newFixedThreadPool(threadCount)
|
||||
try
|
||||
val tasks = workers.map { worker =>
|
||||
new Callable[Option[Move]]:
|
||||
def call(): Option[Move] =
|
||||
worker.bestMoveWithTimeSharedTt(context, timeBudgetMs, excludedRootMoves, hints)
|
||||
}
|
||||
pool.invokeAll(tasks.asJava).get(0).get()
|
||||
finally pool.shutdownNow()
|
||||
@@ -0,0 +1,180 @@
|
||||
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.api.board.Color
|
||||
import de.nowchess.bot.{Bot, BotDifficulty}
|
||||
import de.nowchess.bot.bots.{HybridBot, 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
|
||||
|
||||
enum GameResult:
|
||||
case Bot1Wins
|
||||
case Bot2Wins
|
||||
case Draw
|
||||
|
||||
/** Standalone bot-vs-bot harness. Runs two NNUEBots against each other from randomised openings and writes the visited
|
||||
* positions as one FEN per line. Each bot can use different difficulty levels and weight files.
|
||||
*
|
||||
* Games run sequentially. Bots alternate: bot1 plays white, then bot1 plays black, alternating per game to ensure fair
|
||||
* evaluation.
|
||||
*/
|
||||
object BotVsBotMain:
|
||||
|
||||
private case class Config(
|
||||
games: Int = 1,
|
||||
out: String = "modules/official-bots/python/data/botvbot.txt",
|
||||
weights1: Option[String] = None,
|
||||
weights2: Option[String] = None,
|
||||
difficulty1: String = "Hard",
|
||||
difficulty2: String = "Hard",
|
||||
moveTimeMs: Long = 1000L,
|
||||
randomPlies: Int = 8,
|
||||
maxPlies: Int = 200,
|
||||
seed: Long = System.nanoTime(),
|
||||
)
|
||||
|
||||
def main(args: Array[String]): Unit =
|
||||
val config = parse(args.toList, Config())
|
||||
|
||||
val rules = DefaultRules
|
||||
val difficulty1 = parseDifficulty(config.difficulty1)
|
||||
val difficulty2 = parseDifficulty(config.difficulty2)
|
||||
|
||||
config.weights1.foreach(System.setProperty("nnue.weights", _))
|
||||
val bot1 = HybridBot(difficulty1, rules, searchThreads = 6)
|
||||
|
||||
config.weights2.foreach(System.setProperty("nnue.weights", _))
|
||||
val bot2 = HybridBot(difficulty2, rules)
|
||||
|
||||
val rng = new Random(config.seed)
|
||||
val seen = mutable.HashSet.empty[String]
|
||||
var bot1Wins = 0
|
||||
var bot2Wins = 0
|
||||
var draws = 0
|
||||
|
||||
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
|
||||
val bot1AsWhite = game % 2 == 0
|
||||
val result = playGame(rules, bot1, bot2, rng, config, seen, writer, bot1AsWhite)
|
||||
game += 1
|
||||
|
||||
result match
|
||||
case Some(GameResult.Bot1Wins) =>
|
||||
bot1Wins += 1
|
||||
println(s"Game $game: Bot1 wins${if bot1AsWhite then " (as white)" else " (as black)"}")
|
||||
case Some(GameResult.Bot2Wins) =>
|
||||
bot2Wins += 1
|
||||
println(s"Game $game: Bot2 wins${if !bot1AsWhite then " (as white)" else " (as black)"}")
|
||||
case Some(GameResult.Draw) =>
|
||||
draws += 1
|
||||
println(s"Game $game: Draw")
|
||||
case None => ()
|
||||
|
||||
if game % 25 == 0 then
|
||||
writer.flush()
|
||||
println(
|
||||
s" Progress: $game/${config.games} | Positions: ${seen.size} | Bot1: $bot1Wins, Bot2: $bot2Wins, Draws: $draws\n",
|
||||
)
|
||||
finally writer.close()
|
||||
println(s"\nFinal Statistics:")
|
||||
println(s" Bot1 wins: $bot1Wins")
|
||||
println(s" Bot2 wins: $bot2Wins")
|
||||
println(s" Draws: $draws")
|
||||
println(s" Positions: ${seen.size} -> ${config.out}")
|
||||
|
||||
private def playGame(
|
||||
rules: RuleSet,
|
||||
bot1: Bot,
|
||||
bot2: Bot,
|
||||
rng: Random,
|
||||
config: Config,
|
||||
seen: mutable.HashSet[String],
|
||||
writer: BufferedWriter,
|
||||
bot1AsWhite: Boolean,
|
||||
): Option[GameResult] =
|
||||
randomOpening(rules, rng, config.randomPlies, GameContext.initial) match
|
||||
case None => 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
|
||||
val currentBot =
|
||||
if (plies - config.randomPlies) % 2 == 0 then if bot1AsWhite then bot1 else bot2
|
||||
else if bot1AsWhite then bot2
|
||||
else bot1
|
||||
|
||||
currentBot(ctx) match
|
||||
case None => live = false
|
||||
case Some(move) =>
|
||||
ctx = rules.applyMove(ctx)(move)
|
||||
plies += 1
|
||||
record(rules, ctx, seen, writer)
|
||||
|
||||
determineWinner(rules, ctx, bot1AsWhite)
|
||||
|
||||
private def determineWinner(rules: RuleSet, ctx: GameContext, bot1AsWhite: Boolean): Option[GameResult] =
|
||||
val legalMoves = rules.allLegalMoves(ctx)
|
||||
|
||||
if legalMoves.nonEmpty then Some(GameResult.Draw)
|
||||
else if rules.isCheck(ctx) then
|
||||
if ctx.turn == (if bot1AsWhite then Color.Black else Color.White) then Some(GameResult.Bot1Wins)
|
||||
else Some(GameResult.Bot2Wins)
|
||||
else if rules.isFiftyMoveRule(ctx) || rules.isThreefoldRepetition(ctx) || rules.isInsufficientMaterial(ctx) then
|
||||
Some(GameResult.Draw)
|
||||
else Some(GameResult.Draw)
|
||||
|
||||
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 "--weights1" :: v :: rest => parse(rest, acc.copy(weights1 = Some(v)))
|
||||
case "--weights2" :: v :: rest => parse(rest, acc.copy(weights2 = Some(v)))
|
||||
case "--difficulty1" :: v :: rest => parse(rest, acc.copy(difficulty1 = v))
|
||||
case "--difficulty2" :: v :: rest => parse(rest, acc.copy(difficulty2 = 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)
|
||||
|
||||
private def parseDifficulty(name: String): BotDifficulty = name match
|
||||
case "Easy" => BotDifficulty.Easy
|
||||
case "Medium" => BotDifficulty.Medium
|
||||
case "Expert" => BotDifficulty.Expert
|
||||
case _ => BotDifficulty.Hard
|
||||
@@ -3,7 +3,7 @@ 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.{Bot, BotDifficulty}
|
||||
import de.nowchess.bot.bots.NNUEBot
|
||||
import de.nowchess.io.fen.FenExporter
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
@@ -55,7 +55,7 @@ object SelfPlayMain:
|
||||
|
||||
private def playGame(
|
||||
rules: RuleSet,
|
||||
bot: GameContext => Option[Move],
|
||||
bot: Bot,
|
||||
rng: Random,
|
||||
config: Config,
|
||||
seen: mutable.HashSet[String],
|
||||
|
||||
+68
-7
@@ -2,7 +2,7 @@ package de.nowchess.bot.service
|
||||
|
||||
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.bot.{Bot, BotController}
|
||||
import de.nowchess.bot.{Bot, BotController, TimeControl}
|
||||
import de.nowchess.bot.client.AccountServiceClient
|
||||
import de.nowchess.bot.config.RedisConfig
|
||||
import de.nowchess.io.fen.FenParser
|
||||
@@ -20,6 +20,7 @@ import scala.jdk.CollectionConverters.*
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import java.io.{BufferedReader, InputStream, InputStreamReader}
|
||||
import java.util.concurrent.{ConcurrentHashMap, ExecutorService, Executors}
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
@Startup
|
||||
@ApplicationScoped
|
||||
@@ -42,6 +43,8 @@ class TournamentBotGamePlayer:
|
||||
|
||||
private val hardestDifficulty = "expert"
|
||||
private val autoJoinIntervalMs = 15000L
|
||||
// Detect the opponent's move fast: every poll spent waiting runs our clock without us thinking.
|
||||
private val pollIntervalMs = 250L
|
||||
|
||||
private val gameTerminalStatuses =
|
||||
Set("checkmate", "stalemate", "draw", "resigned", "timeout", "aborted", "finished")
|
||||
@@ -393,12 +396,58 @@ class TournamentBotGamePlayer:
|
||||
private def playGame(cfg: TournamentBotConfig, gameId: String, color: String): Unit =
|
||||
Try {
|
||||
log.infof("Playing game %s as %s", gameId, color)
|
||||
pollGameLoop(cfg, gameId, color)
|
||||
if !streamGameLoop(cfg, gameId, color) then
|
||||
log.infof("Stream unavailable for game %s — falling back to polling", gameId)
|
||||
pollGameLoop(cfg, gameId, color)
|
||||
activeGames.remove(gameId)
|
||||
} match
|
||||
case Failure(ex) => log.errorf(ex, "Game %s crashed", gameId); activeGames.remove(gameId)
|
||||
case Success(_) => ()
|
||||
|
||||
// Push-based play: the game stream delivers the opponent's move the instant it lands, so our clock
|
||||
// is not burned waiting between polls. Heartbeats (every 10s) keep the NDJSON connection flushing.
|
||||
// Returns true if the game was driven to completion via the stream; false to fall back to polling.
|
||||
private def streamGameLoop(cfg: TournamentBotConfig, gameId: String, color: String): Boolean =
|
||||
val myColor = resolveColor(cfg, gameId).getOrElse(color)
|
||||
val lastFen = AtomicReference("")
|
||||
Try {
|
||||
val response = authed(cfg, target(cfg).path("game").path(gameId).path("stream"))
|
||||
.header("Accept", "application/x-ndjson")
|
||||
.get()
|
||||
try
|
||||
if response.getStatus != 200 then
|
||||
log.warnf("Game stream %s returned status %d", gameId, response.getStatus)
|
||||
false
|
||||
else
|
||||
log.infof("Streaming game %s as %s", gameId, myColor)
|
||||
forEachLine(response.readEntity(classOf[InputStream])): line =>
|
||||
parse(line).foreach(node => handleStreamEvent(cfg, gameId, myColor, node, lastFen))
|
||||
true
|
||||
finally response.close()
|
||||
} match
|
||||
case Success(completed) => completed
|
||||
case Failure(ex) => log.warnf(ex, "Game stream %s failed", gameId); false
|
||||
|
||||
private def handleStreamEvent(
|
||||
cfg: TournamentBotConfig,
|
||||
gameId: String,
|
||||
myColor: String,
|
||||
node: JsonNode,
|
||||
lastFen: AtomicReference[String],
|
||||
): Unit =
|
||||
val eventType = node.path("type").asText()
|
||||
if eventType == "move" || eventType == "gameState" then
|
||||
val status = node.path("status").asText("ongoing")
|
||||
val turn = node.path("turn").asText()
|
||||
val fen = node.path("fen").asText()
|
||||
if !gameTerminalStatuses.contains(status) && turn == myColor && fen.nonEmpty && fen != lastFen.get then
|
||||
lastFen.set(fen)
|
||||
val time = readTimeControl(node, myColor)
|
||||
log.infof("Our turn (stream) in game %s — computing move (fen=%s, budget=%dms)", gameId, fen, time.budgetMs)
|
||||
computeUci(cfg, fen, time) match
|
||||
case None => log.warnf("No move found for game %s (fen=%s)", gameId, fen)
|
||||
case Some(uci) => submitMove(cfg, gameId, uci)
|
||||
|
||||
// The native JAX-RS client buffers streaming responses, so reading the NDJSON game stream blocks
|
||||
// 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 =
|
||||
@@ -426,16 +475,28 @@ class TournamentBotGamePlayer:
|
||||
val fen = node.path("fen").asText()
|
||||
if turn == myColor && status == "ongoing" && fen.nonEmpty && fen != lastFen then
|
||||
lastFen = fen
|
||||
log.infof("Our turn in game %s — computing move (fen=%s)", gameId, fen)
|
||||
computeUci(cfg, fen) match
|
||||
val time = readTimeControl(node, myColor)
|
||||
log.infof("Our turn in game %s — computing move (fen=%s, budget=%dms)", gameId, fen, time.budgetMs)
|
||||
computeUci(cfg, fen, time) match
|
||||
case None => log.warnf("No move found for game %s (fen=%s)", gameId, fen)
|
||||
case Some(uci) => submitMove(cfg, gameId, uci)
|
||||
sleep(1000)
|
||||
sleep(pollIntervalMs)
|
||||
|
||||
private def computeUci(cfg: TournamentBotConfig, fen: String): Option[String] =
|
||||
// Server clock is reported in seconds; convert to a millisecond TimeControl so the engine can
|
||||
// size its move budget against the real clock instead of a fixed guess.
|
||||
private def readTimeControl(node: JsonNode, myColor: String): TimeControl =
|
||||
val clock = node.path("clock")
|
||||
if clock.isMissingNode || clock.isNull then TimeControl.Unlimited
|
||||
else
|
||||
val field = if myColor == "white" then "whiteTime" else "blackTime"
|
||||
val remainingMs = (clock.path(field).asDouble(0.0) * 1000.0).toLong
|
||||
val incrementMs = (clock.path("increment").asDouble(0.0) * 1000.0).toLong
|
||||
TimeControl(remainingMs, incrementMs)
|
||||
|
||||
private def computeUci(cfg: TournamentBotConfig, fen: String, time: TimeControl): Option[String] =
|
||||
FenParser.parseFen(fen) match
|
||||
case Left(err) => log.warnf("FEN parse failed: %s (%s)", fen, err.toString); None
|
||||
case Right(context) => engine(cfg).apply(context).map(toUci)
|
||||
case Right(context) => engine(cfg).move(context, time).map(toUci)
|
||||
|
||||
private def submitMove(cfg: TournamentBotConfig, gameId: String, uci: String): Unit =
|
||||
Try {
|
||||
|
||||
@@ -253,6 +253,30 @@ class AlphaBetaSearchTest extends AnyFunSuite with Matchers:
|
||||
val search = AlphaBetaSearch(promoCaptureRules, weights = EvaluationClassic)
|
||||
search.bestMove(ctx, maxDepth = 1) should be(Some(promoCapture))
|
||||
|
||||
test("quiet promotion is treated as tactical in quiescence"):
|
||||
// Pawn pushes to an empty back-rank square (no capture). Must still be searched in
|
||||
// quiescence so a bot does not skip queening at the search horizon.
|
||||
val quietPromo = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn,
|
||||
),
|
||||
)
|
||||
val ctx = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val quietPromoRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
def allLegalMoves(context: GameContext): List[Move] = List(quietPromo)
|
||||
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
|
||||
val search = AlphaBetaSearch(quietPromoRules, weights = EvaluationClassic)
|
||||
search.bestMove(ctx, maxDepth = 1) should be(Some(quietPromo))
|
||||
|
||||
test("draw when isInsufficientMaterial with legal moves present"):
|
||||
val legalMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val drawRules = new RuleSet:
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.nowchess.bot
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.bot.bots.nnue.MopUp
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class MopUpTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def ctx(turn: Color, pieces: (Square, Piece)*): GameContext =
|
||||
GameContext.initial.withBoard(Board(pieces.toMap)).withTurn(turn)
|
||||
|
||||
private val wk = Square(File.E, Rank.R1) -> Piece.WhiteKing
|
||||
private val wq = Square(File.D, Rank.R1) -> Piece.WhiteQueen
|
||||
private val bkCorner = Square(File.H, Rank.R8) -> Piece.BlackKing
|
||||
private val bkCenter = Square(File.D, Rank.R4) -> Piece.BlackKing
|
||||
|
||||
test("zero in a balanced middlegame-like position (both sides have material)"):
|
||||
MopUp.score(ctx(Color.White, wk, wq, bkCorner, Square(File.A, Rank.R8) -> Piece.BlackQueen)) should be(0)
|
||||
|
||||
test("zero when winner lacks mating material (lone king vs king)"):
|
||||
MopUp.score(ctx(Color.White, wk, bkCorner)) should be(0)
|
||||
|
||||
test("positive for the winning side to move in KQ vs K"):
|
||||
MopUp.score(ctx(Color.White, wk, wq, bkCorner)) should be > 0
|
||||
|
||||
test("negative for the bare-king side to move in KQ vs K"):
|
||||
MopUp.score(ctx(Color.Black, wk, wq, bkCorner)) should be < 0
|
||||
|
||||
test("cornered bare king scores higher than centralized bare king"):
|
||||
val cornered = MopUp.score(ctx(Color.White, wk, wq, bkCorner))
|
||||
val centralized = MopUp.score(ctx(Color.White, wk, wq, bkCenter))
|
||||
cornered should be > centralized
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.nowchess.bot
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.bot.bots.classic.EvaluationClassic
|
||||
import de.nowchess.bot.logic.{ParallelSearch, TranspositionTable}
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class ParallelSearchTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def search(threads: Int): ParallelSearch =
|
||||
ParallelSearch(DefaultRules, TranspositionTable(), () => EvaluationClassic, threads)
|
||||
|
||||
test("single-threaded coordinator returns a legal move on the initial position"):
|
||||
val move = search(1).bestMoveWithTime(GameContext.initial, 200L)
|
||||
move should not be None
|
||||
DefaultRules.allLegalMoves(GameContext.initial) should contain(move.get)
|
||||
|
||||
test("multi-threaded Lazy SMP returns a legal move and does not crash under concurrency"):
|
||||
val parallel = search(4)
|
||||
for _ <- 1 to 5 do
|
||||
val move = parallel.bestMoveWithTime(GameContext.initial, 200L)
|
||||
move should not be None
|
||||
DefaultRules.allLegalMoves(GameContext.initial) should contain(move.get)
|
||||
|
||||
test("numThreads below one is clamped to a single worker"):
|
||||
search(0).bestMoveWithTime(GameContext.initial, 100L) should not be None
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=39
|
||||
MINOR=43
|
||||
PATCH=0
|
||||
|
||||
Reference in New Issue
Block a user