From 01ffced3e305f2ebb180e66ad491313fc98761ae Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Mon, 2 Feb 2026 23:26:07 -0500 Subject: [PATCH 1/4] [build] add rules_lint --- .bazelrc | 3 + BUILD.bazel | 10 + MODULE.bazel.lock | 393 +++++++++++++++++- bazel/format/BUILD.bazel | 21 + bazel/tools.MODULE.bazel | 8 + .../muchq/one_d4/engine/GameReplayerTest.java | 17 +- 6 files changed, 444 insertions(+), 8 deletions(-) create mode 100644 bazel/format/BUILD.bazel diff --git a/.bazelrc b/.bazelrc index 2daefd67..6c111c85 100644 --- a/.bazelrc +++ b/.bazelrc @@ -43,6 +43,9 @@ common --java_runtime_version=remotejdk_25 common --tool_java_language_version=25 common --tool_java_runtime_version=remotejdk_25 +build --experimental_strict_java_deps=error +build --experimental_java_classpath=bazel + # build --strategy=Scalac=worker # layering check fails because golf_service:handlers depends on :protobuf for json_util.h diff --git a/BUILD.bazel b/BUILD.bazel index 1640a5d0..dfc4bb9a 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,6 +1,16 @@ load("@bazel_gazelle//:def.bzl", "gazelle") load("@buildifier_prebuilt//:rules.bzl", "buildifier", "buildifier_test") +alias( + name = "format", + actual = "//bazel/format", +) + +alias( + name = "format.check", + actual = "//bazel/format:format.check", +) + gazelle(name = "gazelle") buildifier( diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index e1b0cb68..4364483f 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -2,19 +2,58 @@ "lockFileVersion": 26, "registryFileHashes": { "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://bcr.bazel.build/modules/ape/1.0.1/MODULE.bazel": "37411cfd13bfc28cd264674d660a3ecb3b5b35b9dbe4c0b2be098683641b3fee", + "https://bcr.bazel.build/modules/ape/1.0.1/source.json": "96bc5909d1e3ccc4203272815ef874dbfd99651e240c05049f12193d16c1110b", "https://bcr.bazel.build/modules/apple_support/2.2.0/MODULE.bazel": "5e32bc42e413a4544df7afee7fa4c7aff73e670a44e56e8e45ce1954332034ad", "https://bcr.bazel.build/modules/apple_support/2.2.0/source.json": "368bf1de5dace2a411e792e6df2ea5384eb8db3899680ed51b37eb48b88fd8ac", + "https://bcr.bazel.build/modules/aspect_bazel_lib/1.42.2/MODULE.bazel": "2e0d8ab25c57a14f56ace1c8e881b69050417ff91b2fb7718dc00d201f3c3478", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.0.0/MODULE.bazel": "e118477db5c49419a88d78ebc7a2c2cea9d49600fe0f490c1903324a2c16ecd9", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.0/MODULE.bazel": "7fe0191f047d4fe4a4a46c1107e2350cbb58a8fc2e10913aa4322d3190dec0bf", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.0/source.json": "369df5b7f2eae82f200fff95cf1425f90dee90a0d0948122060b48150ff0e224", + "https://bcr.bazel.build/modules/aspect_rules_js/1.40.0/MODULE.bazel": "01a1014e95e6816b68ecee2584ae929c7d6a1b72e4333ab1ff2d2c6c30babdf1", + "https://bcr.bazel.build/modules/aspect_rules_js/1.40.0/source.json": "b6fd491369e9ef888fdef64b839023a2360caaea8eb370d2cfbfdd2a96721311", + "https://bcr.bazel.build/modules/aspect_rules_lint/2.0.0/MODULE.bazel": "1f4c81edd9af0c54e65dedbe36653fc63dc9a91605313d54d5ab371f853d693c", + "https://bcr.bazel.build/modules/aspect_rules_lint/2.0.0/source.json": "3c3a55b5b424100feca2fd656dcdcd8a0c9fd3304ce609ce71a4d6d46d00a03c", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "aa975a83e72bcaac62ee61ab12b788ea324a1d05c4aab28aadb202f647881679", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/source.json": "786cbc49377fb6bf4859aec5b1c61f8fc26b08e9fdb929e2dde2e1e2a406bd24", + "https://bcr.bazel.build/modules/bazel_features/1.0.0/MODULE.bazel": "d7f022dc887efb96e1ee51cec7b2e48d41e36ff59a6e4f216c40e4029e1585bf", + "https://bcr.bazel.build/modules/bazel_features/1.13.0/MODULE.bazel": "c14c33c7c3c730612bdbe14ebbb5e61936b6f11322ea95a6e91cd1ba962f94df", "https://bcr.bazel.build/modules/bazel_features/1.41.0/MODULE.bazel": "6e0f87fafed801273c371d41e22a15a6f8abf83fdd7f87d5e44ad317b94433d0", "https://bcr.bazel.build/modules/bazel_features/1.41.0/source.json": "8fd525b31b0883c47e0593443cdd10219b94a7556b3195fc02d75c86c66cfe30", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0-beta.1/MODULE.bazel": "407729e232f611c3270005b016b437005daa7b1505826798ea584169a476e878", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "d6e00979a98ac14ada5e31c8794708b41434d461e7e7ca39b59b765e6d233b18", + "https://bcr.bazel.build/modules/buildifier_prebuilt/6.4.0/MODULE.bazel": "37389c6b5a40c59410b4226d3bb54b08637f393d66e2fa57925c6fcf68e64bf4", "https://bcr.bazel.build/modules/buildifier_prebuilt/8.2.1.2/MODULE.bazel": "6d078b1d6ddccb91b3090853945fbfdd41169bb2b2c1dd00fbd388f0be1b09f5", "https://bcr.bazel.build/modules/buildifier_prebuilt/8.2.1.2/source.json": "93439c253ffe47da41f6b4b85807b4e14fefa5424b7af533a740a374e40c00f2", "https://bcr.bazel.build/modules/contrib_rules_jvm/0.32.0/MODULE.bazel": "0a51c56b2fc0d8ce6184f19513c78ce2a8a56119bc2adbc1859ff84c30724c29", "https://bcr.bazel.build/modules/contrib_rules_jvm/0.32.0/source.json": "55b30b443561e3cce13001197961c732661ceb8e0e573b6e734d2bb6e52b3c1b", + "https://bcr.bazel.build/modules/download_utils/1.0.1/MODULE.bazel": "f1d0afade59e37de978506d6bbf08d7fe5f94964e86944aaf58efcead827b41b", + "https://bcr.bazel.build/modules/download_utils/1.0.1/source.json": "05ddc5a3b1f7d8f3e5e0fd1617479e1cf72d63d59ab2b1f0463557a14fc6be0a", + "https://bcr.bazel.build/modules/jq.bzl/0.4.0/MODULE.bazel": "a7b39b37589f2b0dad53fd6c1ccaabbdb290330caa920d7ef3e6aad068cd4ab2", + "https://bcr.bazel.build/modules/jq.bzl/0.4.0/source.json": "52ec7530c4618e03f634b30ff719814a68d7d39c235938b7aa2abbfe1eb1c52c", "https://bcr.bazel.build/modules/protobuf/33.5/MODULE.bazel": "df58cd1c41c9d1257afa7f3110b23d970c107bf806b2e4d8c59a344d05504b0c", "https://bcr.bazel.build/modules/protobuf/33.5/source.json": "fe53cb512afd722159c4c763f3fbbcc6ab850d45d1f389d8374f91c11e83bcd7", + "https://bcr.bazel.build/modules/rules_buf/0.5.2/MODULE.bazel": "5f2492d284ab9bedf2668178303abf5f3cd7d8cdf85d768951008e88456e9c6a", + "https://bcr.bazel.build/modules/rules_buf/0.5.2/source.json": "41876d4834c0832de4b393de6e55dfd1cb3b25d3109e4ba90eb7fb57c560e0d9", "https://bcr.bazel.build/modules/rules_cc/0.2.15/MODULE.bazel": "6a0a4a75a57aa6dc888300d848053a58c6b12a29f89d4304e1c41448514ec6e8", + "https://bcr.bazel.build/modules/rules_diff/1.0.0/MODULE.bazel": "1739509d8db9a6cd7d3584822340d3dfe1f9f27e62462fbca60aa061d88741b2", + "https://bcr.bazel.build/modules/rules_diff/1.0.0/source.json": "fc3824aed007b4db160ffb994036c6e558550857b6634a8e9ccee3e74c659312", + "https://bcr.bazel.build/modules/rules_java/7.0.6/MODULE.bazel": "6ddb07d9857a1a3accc9f6d005f20c969c4659c7710e6269a51db3527e0ea969", + "https://bcr.bazel.build/modules/rules_multirun/0.9.0/MODULE.bazel": "32d628ef586b5b23f67e55886b7bc38913ea4160420d66ae90521dda2ff37df0", + "https://bcr.bazel.build/modules/rules_multirun/0.9.0/source.json": "e882ba77962fa6c5fe68619e5c7d0374ec9a219fb8d03c42eadaf6d0243771bd", + "https://bcr.bazel.build/modules/rules_multitool/0.11.0/MODULE.bazel": "8d9dda78d2398e136300d3ef4fbcc89ede7c32c158d8c016fa7d032df41c4aaf", + "https://bcr.bazel.build/modules/rules_multitool/0.11.0/source.json": "0b86574a1eaff37c33aafaff095ea16d6ac846beb94ffc74c4fcf626f8f80681", + "https://bcr.bazel.build/modules/rules_nodejs/6.5.2/MODULE.bazel": "7f9ea68a0ce6d82905ce9f74e76ab8a8b4531ed4c747018c9d76424ad0b3370d", + "https://bcr.bazel.build/modules/rules_nodejs/6.5.2/source.json": "6a6ca0940914d55c550d1417cad13a56c9900e23f651a762d8ccc5a64adcf661", + "https://bcr.bazel.build/modules/rules_python/0.26.0/MODULE.bazel": "42cb98cd15954e83b96b540dcc6d5a618eb061f056147ac4ea46e687a066a7c7", + "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel": "65dc875cc1a06c30d5bbdba7ab021fd9e551a6579e408a3943a61303e2228a53", "https://bcr.bazel.build/modules/rules_python/1.8.3/MODULE.bazel": "f343e159b59701334be3914416b9f1b72845801ba47920fcb288af4ce8c5cce3", "https://bcr.bazel.build/modules/rules_python/1.8.3/source.json": "e5439f308e3c6f79f318a0f87108db46fc575be89370c3dfb3f7e0eaa571a3f8", + "https://bcr.bazel.build/modules/rules_rust/0.67.0/MODULE.bazel": "87c3816c4321352dcfd9e9e26b58e84efc5b21351ae3ef8fb5d0d57bde7237f5", + "https://bcr.bazel.build/modules/rules_shell/0.5.0/MODULE.bazel": "8c8447370594d45539f66858b602b0bb2cb2d3401a4ebb9ad25830c59c0f366d", + "https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351", + "https://bcr.bazel.build/modules/toolchain_utils/1.0.2/MODULE.bazel": "9b8be503a4fcfd3b8b952525bff0869177a5234d5c35dc3e566b9f5ca2f755a1", + "https://bcr.bazel.build/modules/toolchain_utils/1.0.2/source.json": "88769ec576dddacafd8cca4631812cf8eead89f10a29d9405d9f7a553de6bf87", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", @@ -34,6 +73,7 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/abseil-cpp/20250814.0/MODULE.bazel": "c43c16ca2c432566cdb78913964497259903ebe8fb7d9b57b38e9f1425b427b8", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/ape/1.0.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/apple_rules_lint/0.4.0/MODULE.bazel": "c59831c3a5389430516203777816527f257329a5da363994e1d62b9ae6729f71", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/apple_rules_lint/0.4.0/source.json": "105883202602181f43f109372e1b9ea19e89bbe3bce4bc1fe9bb0baa51eb61ae", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", @@ -49,21 +89,28 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/1.31.2/MODULE.bazel": "7bee702b4862612f29333590f4b658a5832d433d6f8e4395f090e8f4e85d442f", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/1.38.0/MODULE.bazel": "6307fec451ba9962c1c969eb516ebfe1e46528f7fa92e1c9ac8646bef4cdaa3f", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/1.42.2/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/2.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca", - "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/2.19.3/source.json": "ffab9254c65ba945f8369297ad97ca0dec213d3adc6e07877e23a48624a8b456", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/2.22.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "780d1a6522b28f5edb7ea09630748720721dfe27690d65a2d33aa7509de77e07", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_rules_js/1.33.1/MODULE.bazel": "db3e7f16e471cf6827059d03af7c21859e7a0d2bc65429a3a11f005d46fc501b", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_rules_js/1.39.0/MODULE.bazel": "aece421d479e3c31dc3e5f6d49a12acc2700457c03c556650ec7a0ff23fc0d95", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_rules_js/1.40.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_rules_lint/0.12.0/MODULE.bazel": "e767c5dbfeb254ec03275a7701b5cfde2c4d2873676804bc7cb27ddff3728fed", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_rules_lint/2.0.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel-diff/12.1.1/MODULE.bazel": "fa847d03dab6199c0a99a9af121d4d239d425fb1e9dca9bb4ad15aad454c308f", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel-diff/12.1.1/source.json": "d9c6301f8dc8b6c166723d634a5a6bd3cb0fa745605fa80902570f556a9ff318", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/0.1.0/MODULE.bazel": "47011d645b0f949f42ee67f2e8775188a9cf4a0a1528aa2fa4952f2fd00906fd", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.13.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", @@ -82,6 +129,8 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.41.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_lib/3.0.0-beta.1/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_lib/3.0.0/source.json": "895f21909c6fba01d7c17914bb6c8e135982275a1b18cdaa4e62272217ef1751", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", @@ -105,6 +154,7 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/boringssl/0.20240913.0/MODULE.bazel": "fcaa7503a5213290831a91ed1eb538551cf11ac0bc3a6ad92d0fef92c5bd25fb", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/boringssl/0.20241024.0/MODULE.bazel": "b540cff73d948cb79cb0bc108d7cef391d2098a25adabfda5043e4ef548dbc87", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/boringssl/0.20241024.0/source.json": "d843092e682b84188c043ac742965d7f96e04c846c7e338187e03238674909a9", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/buildifier_prebuilt/6.4.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/buildifier_prebuilt/8.2.1.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/buildozer/8.2.1/MODULE.bazel": "61e9433c574c2bd9519cad7fa66b9c1d2b8e8d5f3ae5d6528a2c2d26e68d874d", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/buildozer/8.2.1/source.json": "7c33f6a26ee0216f85544b4bca5e9044579e0219b6898dd653f5fb449cf2e484", @@ -129,6 +179,7 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/curl/8.8.0/MODULE.bazel": "7da3b3e79b0b4ee8f8c95d640bc6ad7b430ce66ef6e9c9d2bc29b3b5ef85f6fe", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/cython/3.0.11-1/MODULE.bazel": "868b3f5c956c3657420d2302004c6bb92606bfa47e314bab7f2ba0630c7c966c", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/cython/3.0.11-1/source.json": "da318be900b8ca9c3d1018839d3bebc5a8e1645620d0848fa2c696d4ecf7c296", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/download_utils/1.0.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/envoy_api/0.0.0-20250128-4de3c74/MODULE.bazel": "1fe72489212c530086e3ffb0e018b2bfef4663200ca03571570f9f006bef1d75", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/envoy_api/0.0.0-20251105-4a2b9a3/MODULE.bazel": "b66e87a0e0c2207f07e35c321388eb1feb036344565977444b52912c53a84466", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/envoy_api/0.0.0-20251105-4a2b9a3/source.json": "c4780edf780977f2ab7d00a189432c5b0b2fa08c6e4e2e09d2950499364a687d", @@ -189,7 +240,7 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/helly25_bzl/0.3.1/MODULE.bazel": "3a4be20f6fc13be32ad44643b8252ef5af09eee936f1d943cd4fd7867fa92826", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/helly25_bzl/0.3.1/source.json": "b129ab1828492de2c163785bbeb4065c166de52d932524b4317beb5b7f917994", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f", - "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/jq.bzl/0.1.0/source.json": "746bf13cac0860f091df5e4911d0c593971cd8796b5ad4e809b2f8e133eee3d5", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/jq.bzl/0.4.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000", @@ -296,6 +347,7 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_buf/0.5.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", @@ -321,6 +373,7 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_cc/0.2.9/MODULE.bazel": "34263f1dca62ea664265438cef714d7db124c03e1ed55ebb4f1dc860164308d1", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_diff/1.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_foreign_cc/0.10.1/MODULE.bazel": "b9527010e5fef060af92b6724edb3691970a5b1f76f74b21d39f7d433641be60", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_foreign_cc/0.13.0/MODULE.bazel": "5a9419cd02f6c2328eafd5234be8bef4d53357af80873392f5907f73f348c61b", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_foreign_cc/0.15.1/MODULE.bazel": "c2c60d26c79fda484acb95cdbec46e89d6b28b4845cb277160ce1e0c8622bb88", @@ -349,6 +402,7 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_java/7.0.6/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", @@ -384,7 +438,10 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_license/0.0.8/MODULE.bazel": "5669c6fe49b5134dbf534db681ad3d67a2d49cfc197e4a95f1ca2fd7f3aebe96", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_multirun/0.9.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_multitool/0.11.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_nodejs/5.8.2/MODULE.bazel": "6bc03c8f37f69401b888023bf511cb6ee4781433b0cb56236b2e55a21e3a026a", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_nodejs/6.5.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_oci/2.2.7/MODULE.bazel": "f6150e4b224d459f7f6523ef65967464ca4efdd266c7fbf2f5a2a51011957e0c", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_oci/2.2.7/source.json": "b099f02af330f47f19dc67fc9300ef6e1937a8c86882690db0e7a2fcea8c7f6b", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_perl/0.2.4/MODULE.bazel": "5f5af7be4bf5fb88d91af7469518f0fd2161718aefc606188f7cd51f436ca938", @@ -407,6 +464,8 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_python/0.26.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_python/0.27.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_python/0.29.0/MODULE.bazel": "2ac8cd70524b4b9ec49a0b8284c79e4cd86199296f82f6e0d5da3f783d660c82", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", @@ -427,6 +486,7 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_python/1.8.3/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_rust/0.45.1/MODULE.bazel": "a69d0db3a958fab2c6520961e1b2287afcc8b36690fd31bbc4f6f7391397150d", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_rust/0.51.0/MODULE.bazel": "2b6d1617ac8503bfdcc0e4520c20539d4bba3a691100bee01afe193ceb0310f9", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_rust/0.67.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_rust/0.68.1/MODULE.bazel": "8d3332ef4079673385eb81f8bd68b012decc04ac00c9d5a01a40eff90301732c", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_rust/0.68.1/source.json": "3378e746f81b62457fdfd37391244fa8ff075ba85c05931ee4f3a20ac1efe963", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_rust_prost/0.68.1/MODULE.bazel": "b74007ee6f527a1853d1bffb6af31d6b5782d431fb575c3e704b87d41dd823dd", @@ -434,6 +494,7 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_shell/0.5.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", @@ -456,8 +517,10 @@ "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/tar.bzl/0.5.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/tar.bzl/0.6.0/source.json": "4a620381df075a16cb3a7ed57bd1d05f7480222394c64a20fa51bdb636fda658", + "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/toolchain_utils/1.0.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/toolchains_llvm/1.6.0/MODULE.bazel": "39603859cafb1c6830160fcd6370552e836790e6abb2bfb8d13bff53c0c10a64", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/toolchains_llvm/1.6.0/source.json": "6bd3ef95a288dd2bb1582eca332af850c9a5428a23bb92cb1c57c2dfe6cb7369", "https://raw.githubusercontent.com/aaylward/bazel-central-registry/otel_cpp_bazel_8_9/modules/upb/0.0.0-20211020-160625a/MODULE.bazel": "6cced416be2dc5b9c05efd5b997049ba795e5e4e6fafbe1624f4587767638928", @@ -582,6 +645,91 @@ } } }, + "@@aspect_rules_js+//npm:extensions.bzl%pnpm": { + "general": { + "bzlTransitiveDigest": "+WJlD7mb9wTbH+PIvXhU72LCQDqarvTtOiGojDyaI4M=", + "usagesDigest": "ZYGEy1FrDUNPBzAzD+ujlHkMEsVPMYOvpHm9RhUexUE=", + "recordedInputs": [ + "REPO_MAPPING:aspect_bazel_lib+,bazel_skylib bazel_skylib+", + "REPO_MAPPING:aspect_bazel_lib+,bazel_tools bazel_tools", + "REPO_MAPPING:aspect_rules_js+,aspect_bazel_lib aspect_bazel_lib+", + "REPO_MAPPING:aspect_rules_js+,bazel_features bazel_features+", + "REPO_MAPPING:aspect_rules_js+,bazel_skylib bazel_skylib+", + "REPO_MAPPING:aspect_rules_js+,bazel_tools bazel_tools", + "REPO_MAPPING:bazel_features+,bazel_tools bazel_tools" + ], + "generatedRepoSpecs": { + "pnpm": { + "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_rule", + "attributes": { + "package": "pnpm", + "version": "8.6.7", + "root_package": "", + "link_workspace": "", + "link_packages": {}, + "integrity": "sha512-vRIWpD/L4phf9Bk2o/O2TDR8fFoJnpYrp2TKqTIZF/qZ2/rgL3qKXzHofHgbXsinwMoSEigz28sqk3pQ+yMEQQ==", + "url": "", + "commit": "", + "patch_args": [ + "-p0" + ], + "patches": [], + "custom_postinstall": "", + "npm_auth": "", + "npm_auth_basic": "", + "npm_auth_username": "", + "npm_auth_password": "", + "lifecycle_hooks": [], + "extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])", + "extract_full_archive": true + } + }, + "pnpm__links": { + "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_links", + "attributes": { + "package": "pnpm", + "version": "8.6.7", + "dev": false, + "root_package": "", + "link_packages": {}, + "deps": {}, + "transitive_closure": {}, + "lifecycle_build_target": false, + "lifecycle_hooks_env": [], + "lifecycle_hooks_execution_requirements": [ + "no-sandbox" + ], + "lifecycle_hooks_use_default_shell_env": false, + "bins": {}, + "package_visibility": [ + "//visibility:public" + ] + } + } + } + } + }, + "@@aspect_tools_telemetry+//:extension.bzl%telemetry": { + "general": { + "bzlTransitiveDigest": "fci87DpYWA6H5Y1ZlMP6Z8kfY8ihz2RnF0ATh4NVNwM=", + "usagesDigest": "i1FqFhjXyyH6bfERz4zwy4c9HppAM5D5gaFRtmhxi/E=", + "recordedInputs": [ + "REPO_MAPPING:aspect_tools_telemetry+,aspect_bazel_lib aspect_bazel_lib+", + "REPO_MAPPING:aspect_tools_telemetry+,bazel_skylib bazel_skylib+" + ], + "generatedRepoSpecs": { + "aspect_tools_telemetry_report": { + "repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository", + "attributes": { + "deps": { + "aspect_rules_lint": "2.0.0", + "aspect_tools_telemetry": "0.2.8" + } + } + } + } + } + }, "@@envoy_api+//bazel:repositories.bzl%non_module_deps": { "general": { "bzlTransitiveDigest": "DITA13hLUg1ghuzFJXkoidwLUcSUZ8eP36QseHw3oQ4=", @@ -633,6 +781,24 @@ } } }, + "@@rules_buf+//buf:extensions.bzl%buf": { + "general": { + "bzlTransitiveDigest": "WHVEN1cdKxWF4f5E2WIaAHsAL5oQWnLSKNwyembaVUg=", + "usagesDigest": "vxN6C2h72rUERbAmd1476FWpxdxo1NhYoY5JSFXJT3g=", + "recordedInputs": [ + "REPO_MAPPING:rules_buf+,bazel_tools bazel_tools" + ], + "generatedRepoSpecs": { + "rules_buf_toolchains": { + "repoRuleId": "@@rules_buf+//buf/internal:toolchain.bzl%buf_download_releases", + "attributes": { + "version": "v1.47.2", + "sha256": "1b37b75dc0a777a0cba17fa2604bc9906e55bb4c578823d8b7a8fe3fc9fe4439" + } + } + } + } + }, "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { "general": { "bzlTransitiveDigest": "m5FNZZeNCyu5NFVLxp9nxedZaJmJL7/GN/fgl03/YTM=", @@ -707,9 +873,228 @@ } } }, + "@@rules_multitool+//multitool:extension.bzl%multitool": { + "general": { + "bzlTransitiveDigest": "wUJ1HvKiH/mPbMSlPmvFm3JOgZvHsh4jse3lhz6L72E=", + "usagesDigest": "WjHBiibpTymLa2F2x3D2cM3gVlX16LYA1epg0mcbqXs=", + "recordedInputs": [ + "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals", + "REPO_MAPPING:bazel_features+,bazel_features_version bazel_features++version_extension+bazel_features_version", + "REPO_MAPPING:rules_multitool+,bazel_features bazel_features+" + ], + "generatedRepoSpecs": { + "multitool.linux_arm64": { + "repoRuleId": "@@rules_multitool+//multitool/private:multitool.bzl%_env_specific_tools", + "attributes": { + "lockfiles": [ + "@@aspect_rules_lint+//format:multitool.lock.json", + "@@aspect_rules_lint+//lint:multitool.lock.json" + ], + "os": "linux", + "cpu": "arm64" + } + }, + "multitool.linux_x86_64": { + "repoRuleId": "@@rules_multitool+//multitool/private:multitool.bzl%_env_specific_tools", + "attributes": { + "lockfiles": [ + "@@aspect_rules_lint+//format:multitool.lock.json", + "@@aspect_rules_lint+//lint:multitool.lock.json" + ], + "os": "linux", + "cpu": "x86_64" + } + }, + "multitool.macos_arm64": { + "repoRuleId": "@@rules_multitool+//multitool/private:multitool.bzl%_env_specific_tools", + "attributes": { + "lockfiles": [ + "@@aspect_rules_lint+//format:multitool.lock.json", + "@@aspect_rules_lint+//lint:multitool.lock.json" + ], + "os": "macos", + "cpu": "arm64" + } + }, + "multitool.macos_x86_64": { + "repoRuleId": "@@rules_multitool+//multitool/private:multitool.bzl%_env_specific_tools", + "attributes": { + "lockfiles": [ + "@@aspect_rules_lint+//format:multitool.lock.json", + "@@aspect_rules_lint+//lint:multitool.lock.json" + ], + "os": "macos", + "cpu": "x86_64" + } + }, + "multitool.windows_arm64": { + "repoRuleId": "@@rules_multitool+//multitool/private:multitool.bzl%_env_specific_tools", + "attributes": { + "lockfiles": [ + "@@aspect_rules_lint+//format:multitool.lock.json", + "@@aspect_rules_lint+//lint:multitool.lock.json" + ], + "os": "windows", + "cpu": "arm64" + } + }, + "multitool.windows_x86_64": { + "repoRuleId": "@@rules_multitool+//multitool/private:multitool.bzl%_env_specific_tools", + "attributes": { + "lockfiles": [ + "@@aspect_rules_lint+//format:multitool.lock.json", + "@@aspect_rules_lint+//lint:multitool.lock.json" + ], + "os": "windows", + "cpu": "x86_64" + } + }, + "multitool": { + "repoRuleId": "@@rules_multitool+//multitool/private:multitool.bzl%_multitool_hub", + "attributes": { + "lockfiles": [ + "@@aspect_rules_lint+//format:multitool.lock.json", + "@@aspect_rules_lint+//lint:multitool.lock.json" + ] + } + } + } + } + }, + "@@rules_nodejs+//nodejs:extensions.bzl%node": { + "general": { + "bzlTransitiveDigest": "/lTHCaRNgSReFf+4uUCXCXtWuHmJgHnNHci3qdSFfck=", + "usagesDigest": "+/eD+bbRTXQvCxo1j0JSXtArQ5RZAZSjW14VyIMoWxY=", + "recordedInputs": [], + "generatedRepoSpecs": { + "nodejs_linux_amd64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.5", + "include_headers": false, + "platform": "linux_amd64" + } + }, + "nodejs_linux_arm64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.5", + "include_headers": false, + "platform": "linux_arm64" + } + }, + "nodejs_linux_s390x": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.5", + "include_headers": false, + "platform": "linux_s390x" + } + }, + "nodejs_linux_ppc64le": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.5", + "include_headers": false, + "platform": "linux_ppc64le" + } + }, + "nodejs_darwin_amd64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.5", + "include_headers": false, + "platform": "darwin_amd64" + } + }, + "nodejs_darwin_arm64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.5", + "include_headers": false, + "platform": "darwin_arm64" + } + }, + "nodejs_windows_amd64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.5", + "include_headers": false, + "platform": "windows_amd64" + } + }, + "nodejs_windows_arm64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "20.19.5", + "include_headers": false, + "platform": "windows_arm64" + } + }, + "nodejs": { + "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "nodejs" + } + }, + "nodejs_host": { + "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "nodejs" + } + }, + "nodejs_toolchains": { + "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_toolchains_repo.bzl%nodejs_toolchains_repo", + "attributes": { + "user_node_repository_name": "nodejs" + } + } + } + } + }, "@@rules_oci+//oci:extensions.bzl%oci": { "general": { - "bzlTransitiveDigest": "lG1n2HkWw8ikh8oafpWlAVlVMERJ/3dVt5E9W5AqtnA=", + "bzlTransitiveDigest": "gcY3ib0Ck4BU15jEgtoRu37hlzCcUoNrjvc7G1Y7aMc=", "usagesDigest": "Kgby1nDW9rgAbA8BJL/5yvJ8seegm7g/iZbjvDizaEE=", "recordedInputs": [ "REPO_MAPPING:aspect_bazel_lib+,bazel_tools bazel_tools", @@ -5728,7 +6113,7 @@ }, "@@rules_rust+//crate_universe/private:internal_extensions.bzl%cu_nr": { "general": { - "bzlTransitiveDigest": "DGMBY1dpvP/xuzm7me7Ny9YWY2PbbS18SWkGV04oHQA=", + "bzlTransitiveDigest": "uKpZLq6SGAUC9rZgwrWhj7wHRtNNeqPJj1jFrhg+SCI=", "usagesDigest": "v4We18mWSPeKV4GPp9Gne78W+jZOgP2pC1i4UN9br1g=", "recordedInputs": [ "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals", diff --git a/bazel/format/BUILD.bazel b/bazel/format/BUILD.bazel new file mode 100644 index 00000000..7cabd619 --- /dev/null +++ b/bazel/format/BUILD.bazel @@ -0,0 +1,21 @@ +load("@aspect_rules_lint//format:defs.bzl", "format_multirun") +load("@rules_java//java:defs.bzl", "java_binary") + +java_binary( + name = "java-format", + jvm_flags = [ + "--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + ], + main_class = "com.google.googlejavaformat.java.Main", + runtime_deps = ["@google-java-format//jar"], +) + +format_multirun( + name = "format", + java = ":java-format", + visibility = ["//visibility:public"], +) diff --git a/bazel/tools.MODULE.bazel b/bazel/tools.MODULE.bazel index ed34873a..aeeba45e 100644 --- a/bazel/tools.MODULE.bazel +++ b/bazel/tools.MODULE.bazel @@ -18,3 +18,11 @@ bazel_dep(name = "buildifier_prebuilt", version = "8.2.1.2", dev_dependency = Tr bazel_dep(name = "bazel-diff", version = "12.1.1") bazel_dep(name = "rules_shell", version = "0.6.1") +bazel_dep(name = "aspect_rules_lint", version = "2.0.0") + +http_jar = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar") +http_jar( + name = "google-java-format", + sha256 = "697707af07c7753f29cba415c6a76b7882702ff464f807da98b28069b8751910", + url = "https://repo1.maven.org/maven2/com/google/googlejavaformat/google-java-format/1.33.0/google-java-format-1.33.0-all-deps.jar", +) diff --git a/jvm/src/test/java/com/muchq/one_d4/engine/GameReplayerTest.java b/jvm/src/test/java/com/muchq/one_d4/engine/GameReplayerTest.java index cff44475..2eb8bc68 100644 --- a/jvm/src/test/java/com/muchq/one_d4/engine/GameReplayerTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/engine/GameReplayerTest.java @@ -176,13 +176,22 @@ public void testAmbiguousMoveWithFile() { } @Test - public void testAmbiguousMoveWithRank() { - // Simpler test for rank disambiguation - two rooks can move to same square - // After Rae1 and Rfe1 type moves + public void testLongerGame() { + // Test that a typical opening sequence replays correctly List positions = replayer.replay( "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O"); - // Should handle the game without errors assertThat(positions).hasSize(17); // initial + 16 half-moves } + + @Test + public void testAmbiguousMoveWithRank() { + // After move 10, white has Ra3 and Rf1. Both can reach a1 (back rank is clear + // since Nc3, Be3, Qd2 have moved those pieces). R3a1 uses rank disambiguation. + List positions = replayer.replay( + "1. e4 e5 2. d3 d6 3. Nf3 Nf6 4. Nc3 Nc6 5. Be2 Be7 6. O-O O-O " + + "7. Be3 Be6 8. Qd2 Qd7 9. a4 a5 10. Ra3 Ra6 11. R3a1"); + + assertThat(positions).hasSize(22); // initial + 21 half-moves + } } From 104823e637af29cf2185cb938c808b6f08fa222d Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Mon, 2 Feb 2026 23:27:19 -0500 Subject: [PATCH 2/4] buildifier --- bazel/tools.MODULE.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/bazel/tools.MODULE.bazel b/bazel/tools.MODULE.bazel index aeeba45e..25a96e95 100644 --- a/bazel/tools.MODULE.bazel +++ b/bazel/tools.MODULE.bazel @@ -21,6 +21,7 @@ bazel_dep(name = "rules_shell", version = "0.6.1") bazel_dep(name = "aspect_rules_lint", version = "2.0.0") http_jar = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar") + http_jar( name = "google-java-format", sha256 = "697707af07c7753f29cba415c6a76b7882702ff464f807da98b28069b8751910", From 00a6d8e0a101cb1d0f563e16b8426f54dbafe7da Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Mon, 2 Feb 2026 23:34:24 -0500 Subject: [PATCH 3/4] add format check to CI --- .github/workflows/branch.yml | 3 + .github/workflows/publish.yml | 5 +- jvm/src/main/java/com/muchq/cards/Card.java | 31 +- jvm/src/main/java/com/muchq/cards/Deck.java | 60 +-- jvm/src/main/java/com/muchq/cards/Rank.java | 28 +- jvm/src/main/java/com/muchq/cards/Suit.java | 10 +- .../com/muchq/cards/castle/GameState.java | 4 +- .../java/com/muchq/cards/castle/Player.java | 4 +- .../com/muchq/cards/castle/ThreeDown.java | 7 +- .../java/com/muchq/cards/castle/ThreeUp.java | 7 +- .../java/com/muchq/cards/castle/Turn.java | 3 +- .../java/com/muchq/chess_com_api/Best.java | 14 +- .../com/muchq/chess_com_api/ChessClient.java | 82 ++-- .../java/com/muchq/chess_com_api/Demo.java | 33 +- .../muchq/chess_com_api/InstantRating.java | 11 +- .../java/com/muchq/chess_com_api/Last.java | 14 +- .../com/muchq/chess_com_api/PlayedGame.java | 96 ++-- .../java/com/muchq/chess_com_api/Player.java | 95 ++-- .../com/muchq/chess_com_api/PlayerResult.java | 12 +- .../java/com/muchq/chess_com_api/Stats.java | 3 +- .../muchq/chess_com_api/StatsResponse.java | 13 +- .../chess_com_api/StreamingPlatform.java | 15 +- .../java/com/muchq/chess_com_api/Tactics.java | 3 +- .../com/muchq/http_client/core/Header.java | 24 +- .../muchq/http_client/core/HttpClient.java | 5 +- .../muchq/http_client/core/HttpRequest.java | 260 ++++++----- .../muchq/http_client/core/HttpResponse.java | 26 +- .../http_client/jdk11/Jdk11HttpClient.java | 85 ++-- .../http_client/jdk11/Jdk11HttpResponse.java | 108 +++-- .../java/com/muchq/imagine/ImageUtils.java | 424 +++++++++--------- jvm/src/main/java/com/muchq/imagine/Main.java | 18 +- .../main/java/com/muchq/imagine/Radius.java | 25 +- .../main/java/com/muchq/json/JsonUtils.java | 13 +- .../main/java/com/muchq/lunarcat/Service.java | 8 +- .../muchq/lunarcat/config/Configuration.java | 3 +- .../config/LunarCatServiceModule.java | 12 +- .../ErrorResponseMessageBodyWriter.java | 30 +- .../providers/OptionalMessageBodyWriter.java | 32 +- .../providers/UnhandledExceptionMapper.java | 7 +- .../mcpserver/McpAuthenticationFilter.java | 13 +- .../java/com/muchq/mcpserver/McpModule.java | 47 +- .../muchq/mcpserver/McpRequestHandler.java | 9 +- .../com/muchq/mcpserver/dtos/ClientInfo.java | 3 +- .../com/muchq/mcpserver/dtos/ContentItem.java | 3 +- .../mcpserver/tools/ChessComGamesTool.java | 98 ++-- .../mcpserver/tools/ChessComPlayerTool.java | 81 ++-- .../mcpserver/tools/ChessComStatsTool.java | 80 ++-- .../com/muchq/mcpserver/tools/McpTool.java | 11 +- .../muchq/mcpserver/tools/ServerTimeTool.java | 40 +- .../muchq/mcpserver/tools/ToolRegistry.java | 15 +- jvm/src/main/java/com/muchq/one_d4/App.java | 10 +- .../java/com/muchq/one_d4/IndexerModule.java | 198 ++++---- .../com/muchq/one_d4/api/IndexController.java | 79 ++-- .../com/muchq/one_d4/api/QueryController.java | 50 ++- .../muchq/one_d4/api/dto/GameFeatureRow.java | 73 ++- .../muchq/one_d4/api/dto/IndexRequest.java | 3 +- .../muchq/one_d4/api/dto/IndexResponse.java | 3 +- .../muchq/one_d4/api/dto/QueryRequest.java | 10 +- .../muchq/one_d4/api/dto/QueryResponse.java | 3 +- .../com/muchq/one_d4/chessql/ast/AndExpr.java | 3 +- .../one_d4/chessql/ast/ComparisonExpr.java | 3 +- .../com/muchq/one_d4/chessql/ast/Expr.java | 3 +- .../com/muchq/one_d4/chessql/ast/InExpr.java | 3 +- .../muchq/one_d4/chessql/ast/MotifExpr.java | 3 +- .../com/muchq/one_d4/chessql/ast/NotExpr.java | 3 +- .../com/muchq/one_d4/chessql/ast/OrExpr.java | 3 +- .../chessql/compiler/CompiledQuery.java | 3 +- .../chessql/compiler/QueryCompiler.java | 2 +- .../one_d4/chessql/compiler/SqlCompiler.java | 158 +++---- .../com/muchq/one_d4/chessql/lexer/Lexer.java | 200 +++++---- .../com/muchq/one_d4/chessql/lexer/Token.java | 8 +- .../muchq/one_d4/chessql/lexer/TokenType.java | 52 +-- .../one_d4/chessql/parser/ParseException.java | 16 +- .../muchq/one_d4/chessql/parser/Parser.java | 343 +++++++------- .../muchq/one_d4/db/DataSourceFactory.java | 21 +- .../com/muchq/one_d4/db/GameFeatureDao.java | 231 +++++----- .../com/muchq/one_d4/db/GameFeatureStore.java | 48 +- .../muchq/one_d4/db/IndexingRequestDao.java | 146 +++--- .../muchq/one_d4/db/IndexingRequestStore.java | 31 +- .../java/com/muchq/one_d4/db/Migration.java | 205 ++++----- .../muchq/one_d4/engine/FeatureExtractor.java | 76 ++-- .../com/muchq/one_d4/engine/GameReplayer.java | 83 ++-- .../com/muchq/one_d4/engine/PgnParser.java | 66 +-- .../one_d4/engine/model/GameFeatures.java | 13 +- .../com/muchq/one_d4/engine/model/Motif.java | 10 +- .../muchq/one_d4/engine/model/ParsedGame.java | 3 +- .../one_d4/engine/model/PositionContext.java | 3 +- .../muchq/one_d4/motifs/CrossPinDetector.java | 164 +++---- .../motifs/DiscoveredAttackDetector.java | 164 +++---- .../com/muchq/one_d4/motifs/ForkDetector.java | 205 +++++---- .../muchq/one_d4/motifs/MotifDetector.java | 6 +- .../com/muchq/one_d4/motifs/PinDetector.java | 236 +++++----- .../muchq/one_d4/motifs/SkewerDetector.java | 145 +++--- .../one_d4/queue/InMemoryIndexQueue.java | 36 +- .../com/muchq/one_d4/queue/IndexMessage.java | 4 +- .../com/muchq/one_d4/queue/IndexQueue.java | 8 +- .../com/muchq/one_d4/worker/IndexWorker.java | 212 ++++----- .../one_d4/worker/IndexWorkerLifecycle.java | 67 ++- .../com/muchq/one_d4/worker/ResultMapper.java | 117 ++--- .../com/muchq/stecky/GuiceConfigurator.java | 3 +- .../com/muchq/stecky/WebSocketServer.java | 13 +- jvm/src/main/java/com/muchq/yochat/App.java | 9 +- .../java/com/muchq/yochat/ChatHandler.java | 9 +- .../java/com/muchq/yochat/lib/YoServer.java | 9 +- .../com/muchq/imagine/ImageUtilsTest.java | 31 +- .../java/com/muchq/json/JsonUtilsTest.java | 3 +- .../java/com/muchq/lunarcat/ServiceTest.java | 29 +- .../lunarcat/config/ConfigurationTest.java | 21 +- .../util/PublicPreconditionsTest.java | 3 +- .../tools/ChessComGamesToolTest.java | 168 ++++--- .../tools/ChessComPlayerToolTest.java | 136 +++--- .../tools/ChessComStatsToolTest.java | 112 +++-- .../mcpserver/tools/ServerTimeToolTest.java | 97 ++-- .../chessql/compiler/SqlCompilerTest.java | 189 ++++---- .../muchq/one_d4/chessql/lexer/LexerTest.java | 151 ++++--- .../one_d4/chessql/parser/ParserTest.java | 218 +++++---- .../muchq/one_d4/engine/GameReplayerTest.java | 379 ++++++++-------- .../muchq/one_d4/engine/PgnParserTest.java | 101 +++-- .../muchq/one_d4/motifs/ForkDetectorTest.java | 352 +++++++-------- .../muchq/one_d4/motifs/PinDetectorTest.java | 329 +++++++------- .../one_d4/motifs/SkewerDetectorTest.java | 317 ++++++------- .../one_d4/queue/InMemoryIndexQueueTest.java | 66 +-- .../muchq/one_d4/worker/ResultMapperTest.java | 355 +++++++-------- 123 files changed, 4328 insertions(+), 4337 deletions(-) diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index faf2e94b..92e7e66d 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -39,6 +39,9 @@ jobs: bazelisk-cache: true disk-cache: ${{ github.workflow }} repository-cache: true + - name: bazel-format-check + run: | + bazel run //:format.check # Dependencies are pre-installed in the Docker image # No need to run setup-linux anymore! - name: Create bazel-diff output directory diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a0987a8b..ab0ef79f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,6 +32,10 @@ jobs: build --remote_cache=https://storage.googleapis.com/moon_base_build_cache build --remote_upload_local_results=true + - name: bazel-format-check + run: | + bazel run //:format.check + - name: bazel-test run: | bazel build //... && bazel test //... @@ -46,4 +50,3 @@ jobs: - name: bazel-run-oci-push run: | bazel query 'kind(oci_push, //...)' | xargs -n1 bazel run - diff --git a/jvm/src/main/java/com/muchq/cards/Card.java b/jvm/src/main/java/com/muchq/cards/Card.java index a06412bf..e57a9ab9 100644 --- a/jvm/src/main/java/com/muchq/cards/Card.java +++ b/jvm/src/main/java/com/muchq/cards/Card.java @@ -3,14 +3,27 @@ import java.util.List; public record Card(Suit suit, Rank rank) { - private static final List SUITS = List.of(Suit.CLUBS, Suit.DIAMONDS, Suit.HEARTS, Suit.SPADES); - private static final List RANKS = List.of( - Rank.TWO, Rank.THREE, Rank.FOUR, Rank.FIVE, Rank.SIX, Rank.SEVEN, Rank.EIGHT, - Rank.NINE, Rank.TEN, Rank.JACK, Rank.QUEEN, Rank.KING, Rank.ACE); + private static final List SUITS = + List.of(Suit.CLUBS, Suit.DIAMONDS, Suit.HEARTS, Suit.SPADES); + private static final List RANKS = + List.of( + Rank.TWO, + Rank.THREE, + Rank.FOUR, + Rank.FIVE, + Rank.SIX, + Rank.SEVEN, + Rank.EIGHT, + Rank.NINE, + Rank.TEN, + Rank.JACK, + Rank.QUEEN, + Rank.KING, + Rank.ACE); - public static Card forIndex(int index) { - Suit s = SUITS.get(index % 4); - Rank r = RANKS.get(index % 13); - return new Card(s, r); - } + public static Card forIndex(int index) { + Suit s = SUITS.get(index % 4); + Rank r = RANKS.get(index % 13); + return new Card(s, r); + } } diff --git a/jvm/src/main/java/com/muchq/cards/Deck.java b/jvm/src/main/java/com/muchq/cards/Deck.java index edbb838d..b0ca6b1d 100644 --- a/jvm/src/main/java/com/muchq/cards/Deck.java +++ b/jvm/src/main/java/com/muchq/cards/Deck.java @@ -7,41 +7,41 @@ import java.util.List; public record Deck(Deque cards) { - - public boolean hasNext() { - return !cards.isEmpty(); - } - public Card nextCard() { - return cards.removeLast(); - } + public boolean hasNext() { + return !cards.isEmpty(); + } - public Deck shuffled() { - List newCards = new ArrayList<>(cards); - Collections.shuffle(newCards); - return new Deck(new ArrayDeque<>(newCards)); - } + public Card nextCard() { + return cards.removeLast(); + } - public static Deck withJokers() { - var cards = cardsListWithNoJokers(); - cards.add(new Card(Suit.NONE, Rank.JOKER)); - cards.add(new Card(Suit.NONE, Rank.JOKER)); - return new Deck(cards); - } + public Deck shuffled() { + List newCards = new ArrayList<>(cards); + Collections.shuffle(newCards); + return new Deck(new ArrayDeque<>(newCards)); + } - public static Deck noJokers() { - return new Deck(cardsListWithNoJokers()); - } + public static Deck withJokers() { + var cards = cardsListWithNoJokers(); + cards.add(new Card(Suit.NONE, Rank.JOKER)); + cards.add(new Card(Suit.NONE, Rank.JOKER)); + return new Deck(cards); + } + + public static Deck noJokers() { + return new Deck(cardsListWithNoJokers()); + } - private static Deque cardsListWithNoJokers() { - List cards = new ArrayList<>(); - for (var suit : Suit.values()) { - for (var value : Rank.values()) { - if (suit != Suit.NONE && value != Rank.JOKER) { - cards.add(new Card(suit, value)); - } - } + private static Deque cardsListWithNoJokers() { + List cards = new ArrayList<>(); + for (var suit : Suit.values()) { + for (var value : Rank.values()) { + if (suit != Suit.NONE && value != Rank.JOKER) { + cards.add(new Card(suit, value)); } - return new ArrayDeque<>(cards); + } } + return new ArrayDeque<>(cards); + } } diff --git a/jvm/src/main/java/com/muchq/cards/Rank.java b/jvm/src/main/java/com/muchq/cards/Rank.java index 324b541a..50aa1878 100644 --- a/jvm/src/main/java/com/muchq/cards/Rank.java +++ b/jvm/src/main/java/com/muchq/cards/Rank.java @@ -1,18 +1,18 @@ package com.muchq.cards; public enum Rank { - TWO, - THREE, - FOUR, - FIVE, - SIX, - SEVEN, - EIGHT, - NINE, - TEN, - JACK, - QUEEN, - KING, - ACE, - JOKER + TWO, + THREE, + FOUR, + FIVE, + SIX, + SEVEN, + EIGHT, + NINE, + TEN, + JACK, + QUEEN, + KING, + ACE, + JOKER } diff --git a/jvm/src/main/java/com/muchq/cards/Suit.java b/jvm/src/main/java/com/muchq/cards/Suit.java index b5241cbd..ec15dd69 100644 --- a/jvm/src/main/java/com/muchq/cards/Suit.java +++ b/jvm/src/main/java/com/muchq/cards/Suit.java @@ -1,9 +1,9 @@ package com.muchq.cards; public enum Suit { - CLUBS, - DIAMONDS, - HEARTS, - SPADES, - NONE; + CLUBS, + DIAMONDS, + HEARTS, + SPADES, + NONE; } diff --git a/jvm/src/main/java/com/muchq/cards/castle/GameState.java b/jvm/src/main/java/com/muchq/cards/castle/GameState.java index 4ef7d159..ec83a0d2 100644 --- a/jvm/src/main/java/com/muchq/cards/castle/GameState.java +++ b/jvm/src/main/java/com/muchq/cards/castle/GameState.java @@ -1,9 +1,7 @@ package com.muchq.cards.castle; import com.muchq.cards.Card; - import java.util.Deque; import java.util.List; -public record GameState(Deque drawPile, List players, Turn lastPlayed) { -} +public record GameState(Deque drawPile, List players, Turn lastPlayed) {} diff --git a/jvm/src/main/java/com/muchq/cards/castle/Player.java b/jvm/src/main/java/com/muchq/cards/castle/Player.java index ba82b4cf..39c7ff84 100644 --- a/jvm/src/main/java/com/muchq/cards/castle/Player.java +++ b/jvm/src/main/java/com/muchq/cards/castle/Player.java @@ -1,8 +1,6 @@ package com.muchq.cards.castle; import com.muchq.cards.Card; - import java.util.List; -public record Player(String name, List hand, ThreeUp faceUp, ThreeDown faceDown) { -} +public record Player(String name, List hand, ThreeUp faceUp, ThreeDown faceDown) {} diff --git a/jvm/src/main/java/com/muchq/cards/castle/ThreeDown.java b/jvm/src/main/java/com/muchq/cards/castle/ThreeDown.java index f1955039..aab618aa 100644 --- a/jvm/src/main/java/com/muchq/cards/castle/ThreeDown.java +++ b/jvm/src/main/java/com/muchq/cards/castle/ThreeDown.java @@ -1,11 +1,10 @@ package com.muchq.cards.castle; import com.muchq.cards.Card; - import java.util.Optional; public record ThreeDown(Optional left, Optional center, Optional right) { - public boolean isEmpty() { - return left().isEmpty() && center().isEmpty() && right().isEmpty(); - } + public boolean isEmpty() { + return left().isEmpty() && center().isEmpty() && right().isEmpty(); + } } diff --git a/jvm/src/main/java/com/muchq/cards/castle/ThreeUp.java b/jvm/src/main/java/com/muchq/cards/castle/ThreeUp.java index b3c9c07a..1ac9205a 100644 --- a/jvm/src/main/java/com/muchq/cards/castle/ThreeUp.java +++ b/jvm/src/main/java/com/muchq/cards/castle/ThreeUp.java @@ -1,11 +1,10 @@ package com.muchq.cards.castle; import com.muchq.cards.Card; - import java.util.Optional; public record ThreeUp(Optional left, Optional center, Optional right) { - public boolean isEmpty() { - return left().isEmpty() && center().isEmpty() && right().isEmpty(); - } + public boolean isEmpty() { + return left().isEmpty() && center().isEmpty() && right().isEmpty(); + } } diff --git a/jvm/src/main/java/com/muchq/cards/castle/Turn.java b/jvm/src/main/java/com/muchq/cards/castle/Turn.java index 43294211..79ab33fb 100644 --- a/jvm/src/main/java/com/muchq/cards/castle/Turn.java +++ b/jvm/src/main/java/com/muchq/cards/castle/Turn.java @@ -2,5 +2,4 @@ import com.muchq.cards.Rank; -public record Turn(Rank rank, int howMany) { -} +public record Turn(Rank rank, int howMany) {} diff --git a/jvm/src/main/java/com/muchq/chess_com_api/Best.java b/jvm/src/main/java/com/muchq/chess_com_api/Best.java index 856e406a..79556a45 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/Best.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/Best.java @@ -2,14 +2,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.Instant; public record Best(int rating, Instant instant, String gameUrl) { - @JsonCreator - public static Best create(@JsonProperty("rating") int rating, - @JsonProperty("date") int epochSeconds, - @JsonProperty("game") String gameUrl) { - return new Best(rating, Instant.ofEpochSecond(epochSeconds), gameUrl); - } + @JsonCreator + public static Best create( + @JsonProperty("rating") int rating, + @JsonProperty("date") int epochSeconds, + @JsonProperty("game") String gameUrl) { + return new Best(rating, Instant.ofEpochSecond(epochSeconds), gameUrl); + } } diff --git a/jvm/src/main/java/com/muchq/chess_com_api/ChessClient.java b/jvm/src/main/java/com/muchq/chess_com_api/ChessClient.java index af59b102..dfc7bd3a 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/ChessClient.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/ChessClient.java @@ -4,63 +4,61 @@ import com.muchq.http_client.core.HttpClient; import com.muchq.http_client.core.HttpRequest; import com.muchq.http_client.core.HttpResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ChessClient { - private static final Logger LOG = LoggerFactory.getLogger(ChessClient.class); - private static final String BASE_URL = "https://api.chess.com/pub/player"; - private static final DateTimeFormatter YEAR_MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM"); + private static final Logger LOG = LoggerFactory.getLogger(ChessClient.class); + private static final String BASE_URL = "https://api.chess.com/pub/player"; + private static final DateTimeFormatter YEAR_MONTH_FORMATTER = + DateTimeFormatter.ofPattern("yyyy/MM"); - private final HttpClient httpClient; - private final ObjectMapper mapper; + private final HttpClient httpClient; + private final ObjectMapper mapper; - public ChessClient(HttpClient httpClient, ObjectMapper objectMapper) { - this.httpClient = httpClient; - this.mapper = objectMapper; - } + public ChessClient(HttpClient httpClient, ObjectMapper objectMapper) { + this.httpClient = httpClient; + this.mapper = objectMapper; + } - public Optional fetchPlayer(String player) { - String url = BASE_URL + "/" + player; - return getAs(url, Player.class); - } + public Optional fetchPlayer(String player) { + String url = BASE_URL + "/" + player; + return getAs(url, Player.class); + } - public Optional fetchStats(String player) { - String url = BASE_URL + "/" + player + "/stats"; - return getAs(url, StatsResponse.class); - } + public Optional fetchStats(String player) { + String url = BASE_URL + "/" + player + "/stats"; + return getAs(url, StatsResponse.class); + } - public Optional fetchGames(String player, YearMonth yearMonth) { - String url = BASE_URL + "/" + player + "/games/" + yearMonth.format(YEAR_MONTH_FORMATTER); - return getAs(url, GamesResponse.class); - } + public Optional fetchGames(String player, YearMonth yearMonth) { + String url = BASE_URL + "/" + player + "/games/" + yearMonth.format(YEAR_MONTH_FORMATTER); + return getAs(url, GamesResponse.class); + } - private Optional getAs(String url, Class clazz) { - HttpRequest request = HttpRequest.newBuilder() - .setUrl(url) - .build(); + private Optional getAs(String url, Class clazz) { + HttpRequest request = HttpRequest.newBuilder().setUrl(url).build(); - HttpResponse response = httpClient.execute(request); + HttpResponse response = httpClient.execute(request); - if (response.getStatusCode() == 404) { - return Optional.empty(); - } + if (response.getStatusCode() == 404) { + return Optional.empty(); + } - // TODO: Failsafe-ify, 429, etc - if (response.getStatusCode() != 200) { - LOG.debug(response.toString()); - throw new RuntimeException("api error"); - } + // TODO: Failsafe-ify, 429, etc + if (response.getStatusCode() != 200) { + LOG.debug(response.toString()); + throw new RuntimeException("api error"); + } - try { - return Optional.of(mapper.readValue(response.getAsInputStream(), clazz)); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + return Optional.of(mapper.readValue(response.getAsInputStream(), clazz)); + } catch (IOException e) { + throw new RuntimeException(e); } + } } diff --git a/jvm/src/main/java/com/muchq/chess_com_api/Demo.java b/jvm/src/main/java/com/muchq/chess_com_api/Demo.java index 2b86a1ed..c9efbaae 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/Demo.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/Demo.java @@ -2,32 +2,31 @@ import com.muchq.http_client.jdk.Jdk11HttpClient; import com.muchq.json.JsonUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.net.http.HttpClient; import java.time.YearMonth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Demo { - private static final Logger LOG = LoggerFactory.getLogger(Demo.class); + private static final Logger LOG = LoggerFactory.getLogger(Demo.class); - public static void main(String[] args) { - var mapper = JsonUtils.mapper(); + public static void main(String[] args) { + var mapper = JsonUtils.mapper(); try (var delegate = HttpClient.newHttpClient(); var httpClient = new Jdk11HttpClient(delegate)) { - var chessClient = new ChessClient(httpClient, mapper); + var chessClient = new ChessClient(httpClient, mapper); - // read player info - var player = chessClient.fetchPlayer("hikaru"); - LOG.info("player: {}", player); + // read player info + var player = chessClient.fetchPlayer("hikaru"); + LOG.info("player: {}", player); - // read stats - var stats = chessClient.fetchStats("hikaru"); - LOG.info("stats: {}", stats); + // read stats + var stats = chessClient.fetchStats("hikaru"); + LOG.info("stats: {}", stats); - // read games - var games = chessClient.fetchGames("hikaru", YearMonth.now()); - LOG.info("games: {}", games); - } + // read games + var games = chessClient.fetchGames("hikaru", YearMonth.now()); + LOG.info("games: {}", games); } + } } diff --git a/jvm/src/main/java/com/muchq/chess_com_api/InstantRating.java b/jvm/src/main/java/com/muchq/chess_com_api/InstantRating.java index e264dd80..65cd1c2c 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/InstantRating.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/InstantRating.java @@ -2,13 +2,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.Instant; public record InstantRating(int rating, Instant instant) { - @JsonCreator - public static InstantRating create(@JsonProperty("rating") int rating, - @JsonProperty("date") int epochSeconds) { - return new InstantRating(rating, Instant.ofEpochSecond(epochSeconds)); - } + @JsonCreator + public static InstantRating create( + @JsonProperty("rating") int rating, @JsonProperty("date") int epochSeconds) { + return new InstantRating(rating, Instant.ofEpochSecond(epochSeconds)); + } } diff --git a/jvm/src/main/java/com/muchq/chess_com_api/Last.java b/jvm/src/main/java/com/muchq/chess_com_api/Last.java index af0224c9..e0c61dfa 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/Last.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/Last.java @@ -2,14 +2,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.Instant; public record Last(int rating, Instant instant, int rd) { - @JsonCreator - public static Last create(@JsonProperty("rating") int rating, - @JsonProperty("date") int epochSeconds, - @JsonProperty("rd") int rd) { - return new Last(rating, Instant.ofEpochSecond(epochSeconds), rd); - } + @JsonCreator + public static Last create( + @JsonProperty("rating") int rating, + @JsonProperty("date") int epochSeconds, + @JsonProperty("rd") int rd) { + return new Last(rating, Instant.ofEpochSecond(epochSeconds), rd); + } } diff --git a/jvm/src/main/java/com/muchq/chess_com_api/PlayedGame.java b/jvm/src/main/java/com/muchq/chess_com_api/PlayedGame.java index 5315453e..254b4c79 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/PlayedGame.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/PlayedGame.java @@ -4,54 +4,52 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Instant; -public record PlayedGame(String url, - String pgn, - Instant endTime, - boolean rated, - Accuracies accuracies, - String tcn, - String uuid, - String initialSetup, - String fen, - String timeClass, - String rules, - PlayerResult whiteResult, - PlayerResult blackResult, - String eco - ) { +public record PlayedGame( + String url, + String pgn, + Instant endTime, + boolean rated, + Accuracies accuracies, + String tcn, + String uuid, + String initialSetup, + String fen, + String timeClass, + String rules, + PlayerResult whiteResult, + PlayerResult blackResult, + String eco) { - @JsonCreator - public static PlayedGame create( - @JsonProperty("url") String url, - @JsonProperty("pgn") String pgn, - @JsonProperty("end_time") int endTimeEpochSeconds, - @JsonProperty("rated") boolean rated, - @JsonProperty("accuracies") Accuracies accuracies, - @JsonProperty("tcn") String tcn, - @JsonProperty("uuid") String uuid, - @JsonProperty("initial_setup") String initialSetup, - @JsonProperty("fen") String fen, - @JsonProperty("time_class") String timeClass, - @JsonProperty("rules") String rules, - @JsonProperty("white") PlayerResult whiteResult, - @JsonProperty("black") PlayerResult blackResult, - @JsonProperty("eco") String eco - ){ - return new PlayedGame( - url, - pgn, - Instant.ofEpochSecond(endTimeEpochSeconds), - rated, - accuracies, - tcn, - uuid, - initialSetup, - fen, - timeClass, - rules, - whiteResult, - blackResult, - eco - ); - } + @JsonCreator + public static PlayedGame create( + @JsonProperty("url") String url, + @JsonProperty("pgn") String pgn, + @JsonProperty("end_time") int endTimeEpochSeconds, + @JsonProperty("rated") boolean rated, + @JsonProperty("accuracies") Accuracies accuracies, + @JsonProperty("tcn") String tcn, + @JsonProperty("uuid") String uuid, + @JsonProperty("initial_setup") String initialSetup, + @JsonProperty("fen") String fen, + @JsonProperty("time_class") String timeClass, + @JsonProperty("rules") String rules, + @JsonProperty("white") PlayerResult whiteResult, + @JsonProperty("black") PlayerResult blackResult, + @JsonProperty("eco") String eco) { + return new PlayedGame( + url, + pgn, + Instant.ofEpochSecond(endTimeEpochSeconds), + rated, + accuracies, + tcn, + uuid, + initialSetup, + fen, + timeClass, + rules, + whiteResult, + blackResult, + eco); + } } diff --git a/jvm/src/main/java/com/muchq/chess_com_api/Player.java b/jvm/src/main/java/com/muchq/chess_com_api/Player.java index f0de09c7..8f48d86c 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/Player.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/Player.java @@ -2,55 +2,54 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.Instant; import java.util.List; -public record Player(int playerId, - String playerApiUrl, - String playerPageUrl, - String name, - String username, - int followers, - String countryUrl, - Instant lastOnlineAt, - Instant joinedAt, - String status, - boolean streamer, - boolean verified, - String league, - List streamingPlatforms) { - @JsonCreator - public static Player create(@JsonProperty("player_id") int playerId, - @JsonProperty("@id") String playerApiUrl, - @JsonProperty("url") String playerPageUrl, - @JsonProperty("name") String name, - @JsonProperty("username") String username, - @JsonProperty("followers") int followers, - @JsonProperty("country") String countryUrl, - @JsonProperty("last_online") int lastOnlineEpochSeconds, - @JsonProperty("joined") int joinedEpochSeconds, - @JsonProperty("status") String status, - @JsonProperty("is_streamer") boolean streamer, - @JsonProperty("verified") boolean verified, - @JsonProperty("league") String league, - @JsonProperty("streaming_platforms") List streamingPlatforms - ) { - return new Player( - playerId, - playerApiUrl, - playerPageUrl, - name, - username, - followers, - countryUrl, - Instant.ofEpochSecond(lastOnlineEpochSeconds), - Instant.ofEpochSecond(joinedEpochSeconds), - status, - streamer, - verified, - league, - streamingPlatforms - ); - } +public record Player( + int playerId, + String playerApiUrl, + String playerPageUrl, + String name, + String username, + int followers, + String countryUrl, + Instant lastOnlineAt, + Instant joinedAt, + String status, + boolean streamer, + boolean verified, + String league, + List streamingPlatforms) { + @JsonCreator + public static Player create( + @JsonProperty("player_id") int playerId, + @JsonProperty("@id") String playerApiUrl, + @JsonProperty("url") String playerPageUrl, + @JsonProperty("name") String name, + @JsonProperty("username") String username, + @JsonProperty("followers") int followers, + @JsonProperty("country") String countryUrl, + @JsonProperty("last_online") int lastOnlineEpochSeconds, + @JsonProperty("joined") int joinedEpochSeconds, + @JsonProperty("status") String status, + @JsonProperty("is_streamer") boolean streamer, + @JsonProperty("verified") boolean verified, + @JsonProperty("league") String league, + @JsonProperty("streaming_platforms") List streamingPlatforms) { + return new Player( + playerId, + playerApiUrl, + playerPageUrl, + name, + username, + followers, + countryUrl, + Instant.ofEpochSecond(lastOnlineEpochSeconds), + Instant.ofEpochSecond(joinedEpochSeconds), + status, + streamer, + verified, + league, + streamingPlatforms); + } } diff --git a/jvm/src/main/java/com/muchq/chess_com_api/PlayerResult.java b/jvm/src/main/java/com/muchq/chess_com_api/PlayerResult.java index 73cf4b72..b3c83e28 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/PlayerResult.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/PlayerResult.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; -public record PlayerResult(@JsonProperty("rating") int rating, - @JsonProperty("result") String result, - @JsonProperty("@id") String playerUrl, - @JsonProperty("username") String username, - @JsonProperty("uuid") String uuid - ) {} +public record PlayerResult( + @JsonProperty("rating") int rating, + @JsonProperty("result") String result, + @JsonProperty("@id") String playerUrl, + @JsonProperty("username") String username, + @JsonProperty("uuid") String uuid) {} diff --git a/jvm/src/main/java/com/muchq/chess_com_api/Stats.java b/jvm/src/main/java/com/muchq/chess_com_api/Stats.java index 65358dc5..4fe83e8c 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/Stats.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/Stats.java @@ -1,4 +1,3 @@ package com.muchq.chess_com_api; -public record Stats(Last last, Best best, Record record) { -} +public record Stats(Last last, Best best, Record record) {} diff --git a/jvm/src/main/java/com/muchq/chess_com_api/StatsResponse.java b/jvm/src/main/java/com/muchq/chess_com_api/StatsResponse.java index 556f7f5d..4c735821 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/StatsResponse.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/StatsResponse.java @@ -3,10 +3,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; public record StatsResponse( - @JsonProperty("chess_daily") Stats chessDaily, - @JsonProperty("chess_rapid") Stats chessRapid, - @JsonProperty("chess_bullet") Stats chessBullet, - @JsonProperty("chess_blitz") Stats chessBlitz, - @JsonProperty("fide") int fide, - @JsonProperty("tactics") Tactics tactics) { -} + @JsonProperty("chess_daily") Stats chessDaily, + @JsonProperty("chess_rapid") Stats chessRapid, + @JsonProperty("chess_bullet") Stats chessBullet, + @JsonProperty("chess_blitz") Stats chessBlitz, + @JsonProperty("fide") int fide, + @JsonProperty("tactics") Tactics tactics) {} diff --git a/jvm/src/main/java/com/muchq/chess_com_api/StreamingPlatform.java b/jvm/src/main/java/com/muchq/chess_com_api/StreamingPlatform.java index e164148e..721e1e45 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/StreamingPlatform.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/StreamingPlatform.java @@ -1,13 +1,12 @@ package com.muchq.chess_com_api; -import com.fasterxml.jackson.annotation.JsonCreator;import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; public record StreamingPlatform(String type, String channelUrl) { - @JsonCreator - public static StreamingPlatform create( - @JsonProperty("type") String type, - @JsonProperty("channel_url") String channelUrl - ) { - return new StreamingPlatform(type, channelUrl); - } + @JsonCreator + public static StreamingPlatform create( + @JsonProperty("type") String type, @JsonProperty("channel_url") String channelUrl) { + return new StreamingPlatform(type, channelUrl); + } } diff --git a/jvm/src/main/java/com/muchq/chess_com_api/Tactics.java b/jvm/src/main/java/com/muchq/chess_com_api/Tactics.java index f2eaf638..ca4718e0 100644 --- a/jvm/src/main/java/com/muchq/chess_com_api/Tactics.java +++ b/jvm/src/main/java/com/muchq/chess_com_api/Tactics.java @@ -1,4 +1,3 @@ package com.muchq.chess_com_api; -public record Tactics(InstantRating highest, InstantRating lowest) { -} +public record Tactics(InstantRating highest, InstantRating lowest) {} diff --git a/jvm/src/main/java/com/muchq/http_client/core/Header.java b/jvm/src/main/java/com/muchq/http_client/core/Header.java index 6823dd11..fcbfc054 100644 --- a/jvm/src/main/java/com/muchq/http_client/core/Header.java +++ b/jvm/src/main/java/com/muchq/http_client/core/Header.java @@ -3,19 +3,19 @@ import java.util.Objects; public class Header { - private final String name; - private final String value; + private final String name; + private final String value; - public Header(String name, String value) { - this.name = Objects.requireNonNull(name); - this.value = Objects.requireNonNull(value); - } + public Header(String name, String value) { + this.name = Objects.requireNonNull(name); + this.value = Objects.requireNonNull(value); + } - public String getName() { - return name; - } + public String getName() { + return name; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } } diff --git a/jvm/src/main/java/com/muchq/http_client/core/HttpClient.java b/jvm/src/main/java/com/muchq/http_client/core/HttpClient.java index 0139b3d5..a71dbe15 100644 --- a/jvm/src/main/java/com/muchq/http_client/core/HttpClient.java +++ b/jvm/src/main/java/com/muchq/http_client/core/HttpClient.java @@ -3,6 +3,7 @@ import java.io.Closeable; public interface HttpClient extends Closeable { - HttpResponse execute(HttpRequest request); - HttpResponse executeAsync(HttpRequest request); + HttpResponse execute(HttpRequest request); + + HttpResponse executeAsync(HttpRequest request); } diff --git a/jvm/src/main/java/com/muchq/http_client/core/HttpRequest.java b/jvm/src/main/java/com/muchq/http_client/core/HttpRequest.java index 5754528b..9101dc8e 100644 --- a/jvm/src/main/java/com/muchq/http_client/core/HttpRequest.java +++ b/jvm/src/main/java/com/muchq/http_client/core/HttpRequest.java @@ -1,174 +1,168 @@ package com.muchq.http_client.core; -import org.jspecify.annotations.Nullable; +import static java.nio.charset.StandardCharsets.UTF_8; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Objects; - -import static java.nio.charset.StandardCharsets.UTF_8; +import org.jspecify.annotations.Nullable; public class HttpRequest { - private static final String ACCEPT = "Accept"; - private static final String CONTENT_TYPE = "Content-Type"; - - public enum Method { - GET(false), - POST(true), - PUT(true), - DELETE(true), - PATCH(true), - HEAD(false); + private static final String ACCEPT = "Accept"; + private static final String CONTENT_TYPE = "Content-Type"; - private final boolean allowsBody; + public enum Method { + GET(false), + POST(true), + PUT(true), + DELETE(true), + PATCH(true), + HEAD(false); - private Method(boolean allowsBody) { - this.allowsBody = allowsBody; - } + private final boolean allowsBody; - public boolean allowsBody() { - return allowsBody; - } + private Method(boolean allowsBody) { + this.allowsBody = allowsBody; } - public enum ContentType { - TEXT("text/plain; charset=UTF-8"), - JSON("application/json"), - XML("text/xml"), - PROTOBUF("application/x-protobuf"), - FORM("application/x-www-form-urlencoded"), - CSV("text/csv; charset=UTF-8"), - OCTET_STREAM("application/octet-stream"); + public boolean allowsBody() { + return allowsBody; + } + } - private final String headerValue; + public enum ContentType { + TEXT("text/plain; charset=UTF-8"), + JSON("application/json"), + XML("text/xml"), + PROTOBUF("application/x-protobuf"), + FORM("application/x-www-form-urlencoded"), + CSV("text/csv; charset=UTF-8"), + OCTET_STREAM("application/octet-stream"); - ContentType(String headerValue) { - this.headerValue = headerValue; - } + private final String headerValue; - public String getHeaderValue() { - return headerValue; - } + ContentType(String headerValue) { + this.headerValue = headerValue; } - private final Method method; - private final URI url; - private final List
headers; - private final byte[] body; - - private HttpRequest( - Method method, - URI url, - List
headers, - byte @Nullable [] body - ) { - this.method = Objects.requireNonNull(method); - this.url = Objects.requireNonNull(url); - this.headers = Objects.requireNonNull(headers); - this.body = body; + public String getHeaderValue() { + return headerValue; } - - public static Builder newBuilder() { - return new Builder(); + } + + private final Method method; + private final URI url; + private final List
headers; + private final byte[] body; + + private HttpRequest(Method method, URI url, List
headers, byte @Nullable [] body) { + this.method = Objects.requireNonNull(method); + this.url = Objects.requireNonNull(url); + this.headers = Objects.requireNonNull(headers); + this.body = body; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public Method getMethod() { + return method; + } + + public URI getUrl() { + return url; + } + + public List
getHeaders() { + return headers; + } + + public byte[] getBody() { + return body; + } + + public static class Builder { + private String url = null; + private Method method = Method.GET; + private final List
headers = new ArrayList<>(); + private byte[] body = null; + private ContentType contentType = ContentType.JSON; + private ContentType accept = ContentType.JSON; + + private Builder() {} + + public Builder setUrl(String url) { + this.url = Objects.requireNonNull(url); + return this; } - public Method getMethod() { - return method; + public Builder setMethod(Method method) { + this.method = Objects.requireNonNull(method); + return this; } - public URI getUrl() { - return url; + public Builder addHeader(String name, String value) { + headers.add(new Header(name, value)); + return this; } - public List
getHeaders() { - return headers; + public Builder setBody(String body) { + return setBody(Objects.requireNonNull(body).getBytes(UTF_8)); } - public byte[] getBody() { - return body; + public Builder setBody(byte[] body) { + this.body = Objects.requireNonNull(body); + return this; } - public static class Builder { - private String url = null; - private Method method = Method.GET; - private final List
headers = new ArrayList<>(); - private byte[] body = null; - private ContentType contentType = ContentType.JSON; - private ContentType accept = ContentType.JSON; - - private Builder() {} - - public Builder setUrl(String url) { - this.url = Objects.requireNonNull(url); - return this; - } - - public Builder setMethod(Method method) { - this.method = Objects.requireNonNull(method); - return this; - } - - public Builder addHeader(String name, String value) { - headers.add(new Header(name, value)); - return this; - } - - public Builder setBody(String body) { - return setBody(Objects.requireNonNull(body).getBytes(UTF_8)); - } - - public Builder setBody(byte[] body) { - this.body = Objects.requireNonNull(body); - return this; - } - - public Builder setContentType(ContentType contentType) { - this.contentType = Objects.requireNonNull(contentType); - return this; - } + public Builder setContentType(ContentType contentType) { + this.contentType = Objects.requireNonNull(contentType); + return this; + } - public Builder setAccept(ContentType accept) { - this.accept = Objects.requireNonNull(accept); - return this; - } + public Builder setAccept(ContentType accept) { + this.accept = Objects.requireNonNull(accept); + return this; + } - public HttpRequest build() { - URI url = buildUrl(); - List
headers = buildHeaders(); - validateBodyState(); + public HttpRequest build() { + URI url = buildUrl(); + List
headers = buildHeaders(); + validateBodyState(); - return new HttpRequest(method, url, headers, body); - } + return new HttpRequest(method, url, headers, body); + } - private URI buildUrl() { - Objects.requireNonNull(url, "URL is not set"); - return URI.create(url); - } + private URI buildUrl() { + Objects.requireNonNull(url, "URL is not set"); + return URI.create(url); + } - private void validateBodyState() { - if (body == null) { - return; - } + private void validateBodyState() { + if (body == null) { + return; + } - if (!method.allowsBody) { - throw new IllegalStateException("Cannot set body with method " + method); - } - } + if (!method.allowsBody) { + throw new IllegalStateException("Cannot set body with method " + method); + } + } - private List
buildHeaders() { - if (contentType != null && !headerPresent(CONTENT_TYPE)) { - headers.add(new Header(CONTENT_TYPE, contentType.getHeaderValue())); - } - if (accept != null && !headerPresent(ACCEPT)) { - headers.add(new Header(ACCEPT, accept.getHeaderValue())); - } + private List
buildHeaders() { + if (contentType != null && !headerPresent(CONTENT_TYPE)) { + headers.add(new Header(CONTENT_TYPE, contentType.getHeaderValue())); + } + if (accept != null && !headerPresent(ACCEPT)) { + headers.add(new Header(ACCEPT, accept.getHeaderValue())); + } - return List.copyOf(headers); - } + return List.copyOf(headers); + } - private boolean headerPresent(String headerName) { - return headers.stream().anyMatch(header -> header.getName().equalsIgnoreCase(headerName)); - } + private boolean headerPresent(String headerName) { + return headers.stream().anyMatch(header -> header.getName().equalsIgnoreCase(headerName)); } + } } diff --git a/jvm/src/main/java/com/muchq/http_client/core/HttpResponse.java b/jvm/src/main/java/com/muchq/http_client/core/HttpResponse.java index 3a18bf68..693c6f50 100644 --- a/jvm/src/main/java/com/muchq/http_client/core/HttpResponse.java +++ b/jvm/src/main/java/com/muchq/http_client/core/HttpResponse.java @@ -4,17 +4,23 @@ import java.util.List; public interface HttpResponse { - HttpRequest getRequest(); + HttpRequest getRequest(); - int getStatusCode(); - boolean isSuccess(); - boolean isError(); - boolean isClientError(); - boolean isServerError(); + int getStatusCode(); - List
getHeaders(); + boolean isSuccess(); - String getAsString(); - byte[] getAsBytes(); - InputStream getAsInputStream(); + boolean isError(); + + boolean isClientError(); + + boolean isServerError(); + + List
getHeaders(); + + String getAsString(); + + byte[] getAsBytes(); + + InputStream getAsInputStream(); } diff --git a/jvm/src/main/java/com/muchq/http_client/jdk11/Jdk11HttpClient.java b/jvm/src/main/java/com/muchq/http_client/jdk11/Jdk11HttpClient.java index c655f5ee..5e078f76 100644 --- a/jvm/src/main/java/com/muchq/http_client/jdk11/Jdk11HttpClient.java +++ b/jvm/src/main/java/com/muchq/http_client/jdk11/Jdk11HttpClient.java @@ -3,7 +3,6 @@ import com.muchq.http_client.core.HttpClient; import com.muchq.http_client.core.HttpRequest; import com.muchq.http_client.core.HttpResponse; - import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -11,48 +10,50 @@ public class Jdk11HttpClient implements HttpClient { - private final java.net.http.HttpClient delegate; - - public Jdk11HttpClient(java.net.http.HttpClient delegate) { - this.delegate = Objects.requireNonNull(delegate); - } - - @Override - public HttpResponse execute(HttpRequest request) { - java.net.http.HttpRequest httpRequest = toJdk11HttpRequest(request); - java.net.http.HttpResponse response = null; - try { - response = delegate.send(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofInputStream()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - return new Jdk11HttpResponse(request, response); + private final java.net.http.HttpClient delegate; + + public Jdk11HttpClient(java.net.http.HttpClient delegate) { + this.delegate = Objects.requireNonNull(delegate); + } + + @Override + public HttpResponse execute(HttpRequest request) { + java.net.http.HttpRequest httpRequest = toJdk11HttpRequest(request); + java.net.http.HttpResponse response = null; + try { + response = + delegate.send(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofInputStream()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); } - - @Override - public HttpResponse executeAsync(HttpRequest request) { - throw new UnsupportedOperationException(); + return new Jdk11HttpResponse(request, response); + } + + @Override + public HttpResponse executeAsync(HttpRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + delegate.close(); + } + + private java.net.http.HttpRequest toJdk11HttpRequest(HttpRequest request) { + var builder = java.net.http.HttpRequest.newBuilder().uri(request.getUrl()); + request.getHeaders().forEach(h -> builder.setHeader(h.getName(), h.getValue())); + + if (!request.getMethod().allowsBody() || request.getBody() == null) { + builder.method(request.getMethod().name(), java.net.http.HttpRequest.BodyPublishers.noBody()); + } else { + builder.method( + request.getMethod().name(), + java.net.http.HttpRequest.BodyPublishers.ofByteArray(request.getBody())); } - @Override - public void close() { - delegate.close(); - } - - private java.net.http.HttpRequest toJdk11HttpRequest(HttpRequest request) { - var builder = java.net.http.HttpRequest.newBuilder().uri(request.getUrl()); - request.getHeaders().forEach(h -> builder.setHeader(h.getName(), h.getValue())); - - if (!request.getMethod().allowsBody() || request.getBody() == null) { - builder.method(request.getMethod().name(), java.net.http.HttpRequest.BodyPublishers.noBody()); - } else { - builder.method(request.getMethod().name(), java.net.http.HttpRequest.BodyPublishers.ofByteArray(request.getBody())); - } - - return builder.build(); - } + return builder.build(); + } } - diff --git a/jvm/src/main/java/com/muchq/http_client/jdk11/Jdk11HttpResponse.java b/jvm/src/main/java/com/muchq/http_client/jdk11/Jdk11HttpResponse.java index 91f93747..cb1e9b03 100644 --- a/jvm/src/main/java/com/muchq/http_client/jdk11/Jdk11HttpResponse.java +++ b/jvm/src/main/java/com/muchq/http_client/jdk11/Jdk11HttpResponse.java @@ -3,7 +3,6 @@ import com.muchq.http_client.core.Header; import com.muchq.http_client.core.HttpRequest; import com.muchq.http_client.core.HttpResponse; - import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -13,72 +12,67 @@ public class Jdk11HttpResponse implements HttpResponse { - private final HttpRequest request; - private final java.net.http.HttpResponse delegate; + private final HttpRequest request; + private final java.net.http.HttpResponse delegate; - public Jdk11HttpResponse(HttpRequest request, java.net.http.HttpResponse delegate) { - this.request = Objects.requireNonNull(request); - this.delegate = Objects.requireNonNull(delegate); - } + public Jdk11HttpResponse(HttpRequest request, java.net.http.HttpResponse delegate) { + this.request = Objects.requireNonNull(request); + this.delegate = Objects.requireNonNull(delegate); + } - @Override - public HttpRequest getRequest() { - return request; - } + @Override + public HttpRequest getRequest() { + return request; + } - @Override - public int getStatusCode() { - return delegate.statusCode(); - } + @Override + public int getStatusCode() { + return delegate.statusCode(); + } - @Override - public boolean isSuccess() { - return delegate.statusCode() > 199 && delegate.statusCode() < 400; - } + @Override + public boolean isSuccess() { + return delegate.statusCode() > 199 && delegate.statusCode() < 400; + } - @Override - public boolean isError() { - return !isSuccess(); - } + @Override + public boolean isError() { + return !isSuccess(); + } - @Override - public boolean isClientError() { - return delegate.statusCode() > 399 && delegate.statusCode() < 500; - } + @Override + public boolean isClientError() { + return delegate.statusCode() > 399 && delegate.statusCode() < 500; + } - @Override - public boolean isServerError() { - return delegate.statusCode() > 499; - } + @Override + public boolean isServerError() { + return delegate.statusCode() > 499; + } - @Override - public List
getHeaders() { - return delegate.headers() - .map() - .entrySet() - .stream() - .flatMap(entry -> - entry.getValue().stream().map(value -> new Header(entry.getKey(), value)) - ) - .collect(Collectors.toList()); - } + @Override + public List
getHeaders() { + return delegate.headers().map().entrySet().stream() + .flatMap(entry -> entry.getValue().stream().map(value -> new Header(entry.getKey(), value))) + .collect(Collectors.toList()); + } - @Override - public String getAsString() { - return new String(getAsBytes(), StandardCharsets.UTF_8); - } + @Override + public String getAsString() { + return new String(getAsBytes(), StandardCharsets.UTF_8); + } - @Override - public byte[] getAsBytes() { - try (InputStream inputStream = getAsInputStream()) { - return inputStream.readAllBytes(); - } catch (IOException e) { - throw new RuntimeException(e); - } + @Override + public byte[] getAsBytes() { + try (InputStream inputStream = getAsInputStream()) { + return inputStream.readAllBytes(); + } catch (IOException e) { + throw new RuntimeException(e); } + } - @Override - public InputStream getAsInputStream() { - return delegate.body(); - } + @Override + public InputStream getAsInputStream() { + return delegate.body(); + } } diff --git a/jvm/src/main/java/com/muchq/imagine/ImageUtils.java b/jvm/src/main/java/com/muchq/imagine/ImageUtils.java index 754f5390..c49c04a6 100644 --- a/jvm/src/main/java/com/muchq/imagine/ImageUtils.java +++ b/jvm/src/main/java/com/muchq/imagine/ImageUtils.java @@ -8,231 +8,235 @@ import java.util.function.Function; public final class ImageUtils { - private static final int MAX_DEPTH = 50; - - private static final int[] SOBEL_X_KERNEL = {1, 0, -1, 2, 0, -2, 1, 0, -1}; - private static final int[] SOBEL_Y_KERNEL = {1, 2, 1, 0, 0, 0, -1, -2, -1}; - - private ImageUtils() { - throw new AssertionError(); - } - - public static BufferedImage meanBlur(BufferedImage input) { - int[] kernel = {1, 1, 1, 1, 1, 1, 1, 1, 1}; - return convolveAndScaleByKernelSum(input, kernel); - } - - public static BufferedImage gaussianBlur(BufferedImage input, Radius radius) { - return convolveAndScaleByKernelSum(input, radius.getGaussianKernel()); - } - - public static BufferedImage grayGaussianBlur(BufferedImage input, Radius radius, int depth) { - validateDepth(depth); - int[] kernel = radius.getGaussianKernel(); - BufferedImage grayCopy = grayScale(input); - BufferedImage blurred = grayCopy; - for (int i = 0; i < depth; i++) { - blurred = convolveAndScaleByKernelSum(blurred, kernel); + private static final int MAX_DEPTH = 50; + + private static final int[] SOBEL_X_KERNEL = {1, 0, -1, 2, 0, -2, 1, 0, -1}; + private static final int[] SOBEL_Y_KERNEL = {1, 2, 1, 0, 0, 0, -1, -2, -1}; + + private ImageUtils() { + throw new AssertionError(); + } + + public static BufferedImage meanBlur(BufferedImage input) { + int[] kernel = {1, 1, 1, 1, 1, 1, 1, 1, 1}; + return convolveAndScaleByKernelSum(input, kernel); + } + + public static BufferedImage gaussianBlur(BufferedImage input, Radius radius) { + return convolveAndScaleByKernelSum(input, radius.getGaussianKernel()); + } + + public static BufferedImage grayGaussianBlur(BufferedImage input, Radius radius, int depth) { + validateDepth(depth); + int[] kernel = radius.getGaussianKernel(); + BufferedImage grayCopy = grayScale(input); + BufferedImage blurred = grayCopy; + for (int i = 0; i < depth; i++) { + blurred = convolveAndScaleByKernelSum(blurred, kernel); + } + return blurred; + } + + public static BufferedImage grayScale(BufferedImage input) { + BufferedImage copied = + new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + Graphics graphics = copied.getGraphics(); + graphics.drawImage(input, 0, 0, null); + graphics.dispose(); + return copied; + } + + // assumes a gray image + public static BufferedImage sobel(BufferedImage input) { + // wasteful atm, but maybe worth caching these partial results? + int[] inputPixels = bytesToInts(getPixels(input)); + int[] sobelX = convolve(inputPixels, input.getWidth(), input.getHeight(), SOBEL_X_KERNEL); + int[] sobelY = convolve(inputPixels, input.getWidth(), input.getHeight(), SOBEL_Y_KERNEL); + + Function scaleToByte = (d) -> (byte) ((255 * d) / 360); + BufferedImage sobel = + new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + byte[] sobelPixels = getPixels(sobel); + for (int i = 0; i < sobelPixels.length; i++) { + int xi = sobelX[i]; + int yi = sobelY[i]; + sobelPixels[i] = scaleToByte.apply(Math.sqrt(xi * xi + yi * yi)); + } + + return sobel; + } + + public static BufferedImage gate(BufferedImage input, int threshold) { + BufferedImage copied = copy(input); + byte[] pixels = getPixels(copied); + + for (int i = 0; i < pixels.length; i++) { + if ((pixels[i] & 0xff) > threshold) { + pixels[i] = 0; + } else { + pixels[i] = (byte) 255; + } + } + + return copied; + } + + public static BufferedImage grayScaleSlow(BufferedImage input) { + BufferedImage copied = + new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + + byte[] pixels = getPixels(input); + byte[] outputPixels = getPixels(copied); + final boolean hasAlpha = copied.getAlphaRaster() != null; + final int bytesPerPixel = hasAlpha ? 4 : 3; + + for (int p = 0, outputIndex = 0; p < pixels.length; p += bytesPerPixel, outputIndex++) { + int i = hasAlpha ? p + 1 : p; + int r = pixels[i] & 0xff; + int g = pixels[i + 1] & 0xff; + int b = pixels[i + 2] & 0xff; + byte avg = (byte) ((r + g + b) / 3); + outputPixels[outputIndex] = avg; + } + return copied; + } + + public static BufferedImage copy(BufferedImage input) { + ColorModel colorModel = input.getColorModel(); + boolean isAlphaPremultiplied = colorModel.isAlphaPremultiplied(); + WritableRaster raster = input.copyData(null); + return new BufferedImage(colorModel, raster, isAlphaPremultiplied, null); + } + + public static BufferedImage convolveAndScaleByKernelSum(BufferedImage input, int[] kernel) { + final int kernelSum = sum(kernel); + byte[] inputPixels = getPixels(input); + int[] convOutput = + convolve(inputPixels, input.getWidth(), input.getHeight(), kernel, (i) -> i / kernelSum); + + BufferedImage output = + new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + byte[] outputPixels = getPixels(output); + for (int i = 0; i < outputPixels.length; i++) { + outputPixels[i] = (byte) convOutput[i]; + } + + return output; + } + + /** + * @param input a 1d array representing a 2d matrix laid out with pixels horizontally adjacent + * pixels in the 2d matrix adjacent in the 1d array. To find the pixel immediately below a + * pixel at index i, look at index i + width + * @param width matrix width + * @param height matrix height + * @param kernel + * @param scaler scale the final value + */ + public static int[] convolve( + int[] input, int width, int height, int[] kernel, Function scaler) { + // TODO: assert that kernel.length is an odd square (3x3, 5x5, 7x7) + // TODO: what is a GPU? + final int edgeOffset = (int) Math.sqrt(kernel.length) / 2; + final int[] outputPixels = new int[input.length]; + + for (int row = edgeOffset; row < height - edgeOffset; row++) { + for (int col = edgeOffset; col < width - edgeOffset; col++) { + int i = 0; + int dotProduct = 0; + for (int r = -edgeOffset; r <= edgeOffset; r++) { + for (int c = -edgeOffset; c <= edgeOffset; c++) { + int neighborPixel = input[computeIndex(row + r, col + c, width)]; + dotProduct = dotProduct + kernel[i] * neighborPixel; + i++; + } } - return blurred; - } - - public static BufferedImage grayScale(BufferedImage input) { - BufferedImage copied = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - Graphics graphics = copied.getGraphics(); - graphics.drawImage(input, 0, 0, null); - graphics.dispose(); - return copied; - } - // assumes a gray image - public static BufferedImage sobel(BufferedImage input) { - // wasteful atm, but maybe worth caching these partial results? - int[] inputPixels = bytesToInts(getPixels(input)); - int[] sobelX = convolve(inputPixels, input.getWidth(), input.getHeight(), SOBEL_X_KERNEL); - int[] sobelY = convolve(inputPixels, input.getWidth(), input.getHeight(), SOBEL_Y_KERNEL); - - Function scaleToByte = (d) -> (byte)((255 * d) / 360); - BufferedImage sobel = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - byte[] sobelPixels = getPixels(sobel); - for (int i=0; i scaler) { + // TODO: assert that kernel.length is an odd square (3x3, 5x5, 7x7) + // TODO: what is a GPU? + final int edgeOffset = (int) Math.sqrt(kernel.length) / 2; + final int[] outputPixels = new int[input.length]; + + for (int row = edgeOffset; row < height - edgeOffset; row++) { + for (int col = edgeOffset; col < width - edgeOffset; col++) { + int i = 0; + int dotProduct = 0; + for (int r = -edgeOffset; r <= edgeOffset; r++) { + for (int c = -edgeOffset; c <= edgeOffset; c++) { + int neighborPixel = input[computeIndex(row + r, col + c, width)] & 0xff; + dotProduct = dotProduct + kernel[i] * neighborPixel; + i++; + } } - return sobel; + outputPixels[computeIndex(row, col, width)] = scaler.apply(dotProduct); + } } - public static BufferedImage gate(BufferedImage input, int threshold) { - BufferedImage copied = copy(input); - byte[] pixels = getPixels(copied); + return outputPixels; + } - for (int i=0; i threshold) { - pixels[i] = 0; - } else { - pixels[i] = (byte)255; - } - } + /** + * @param input a 1d array representing a 2d matrix laid out with pixels horizontally adjacent + * pixels in the 2d matrix adjacent in the 1d array. To find the pixel immediately below a + * pixel at index i, look at index i + width + * @param width matrix width + * @param height matrix height + * @param kernel + */ + public static int[] convolve(int[] input, int width, int height, int[] kernel) { + return convolve(input, width, height, kernel, Function.identity()); + } - return copied; - } - - public static BufferedImage grayScaleSlow(BufferedImage input) { - BufferedImage copied = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - - byte[] pixels = getPixels(input); - byte[] outputPixels = getPixels(copied); - final boolean hasAlpha = copied.getAlphaRaster() != null; - final int bytesPerPixel = hasAlpha ? 4 : 3; - - for (int p=0, outputIndex=0; p i/kernelSum); - - BufferedImage output = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - byte[] outputPixels = getPixels(output); - for (int i=0; i MAX_DEPTH) { + throw new RuntimeException("blur depth must be > 0 and < " + MAX_DEPTH); } + } - /** - * - * @param input a 1d array representing a 2d matrix laid out with pixels horizontally adjacent - * pixels in the 2d matrix adjacent in the 1d array. To find the pixel immediately - * below a pixel at index i, look at index i + width - * @param width matrix width - * @param height matrix height - * @param kernel - * @param scaler scale the final value - */ - public static int[] convolve(int[] input, int width, int height, int[] kernel, Function scaler) { - // TODO: assert that kernel.length is an odd square (3x3, 5x5, 7x7) - // TODO: what is a GPU? - final int edgeOffset = (int)Math.sqrt(kernel.length)/2; - final int[] outputPixels = new int[input.length]; - - for (int row = edgeOffset; row < height - edgeOffset; row++) { - for (int col = edgeOffset; col < width - edgeOffset; col++) { - int i = 0; - int dotProduct = 0; - for (int r = -edgeOffset; r <= edgeOffset; r++) { - for (int c = -edgeOffset; c <= edgeOffset; c++) { - int neighborPixel = input[computeIndex(row+r, col+c, width)]; - dotProduct = dotProduct + kernel[i] * neighborPixel; - i++; - } - } - - outputPixels[computeIndex(row, col, width)] = scaler.apply(dotProduct); - } - } + private static int[] bytesToInts(byte[] bytes) { + int[] ints = new int[bytes.length]; - return outputPixels; + for (int i = 0; i < bytes.length; i++) { + ints[i] = bytes[i] & 0xff; } - /** - * - * @param input a 1d array representing a 2d matrix laid out with pixels horizontally adjacent - * pixels in the 2d matrix adjacent in the 1d array. To find the pixel immediately - * below a pixel at index i, look at index i + width - * @param width matrix width - * @param height matrix height - * @param kernel - * @param scaler scale the final value - */ - public static int[] convolve(byte[] input, int width, int height, int[] kernel, Function scaler) { - // TODO: assert that kernel.length is an odd square (3x3, 5x5, 7x7) - // TODO: what is a GPU? - final int edgeOffset = (int)Math.sqrt(kernel.length)/2; - final int[] outputPixels = new int[input.length]; - - for (int row = edgeOffset; row < height - edgeOffset; row++) { - for (int col = edgeOffset; col < width - edgeOffset; col++) { - int i = 0; - int dotProduct = 0; - for (int r = -edgeOffset; r <= edgeOffset; r++) { - for (int c = -edgeOffset; c <= edgeOffset; c++) { - int neighborPixel = input[computeIndex(row+r, col+c, width)] & 0xff; - dotProduct = dotProduct + kernel[i] * neighborPixel; - i++; - } - } - - outputPixels[computeIndex(row, col, width)] = scaler.apply(dotProduct); - } - } - - return outputPixels; - } - - /** - * - * @param input a 1d array representing a 2d matrix laid out with pixels horizontally adjacent - * pixels in the 2d matrix adjacent in the 1d array. To find the pixel immediately - * below a pixel at index i, look at index i + width - * @param width matrix width - * @param height matrix height - * @param kernel - */ - public static int[] convolve(int[] input, int width, int height, int[] kernel) { - return convolve(input, width, height, kernel, Function.identity()); - } - - private static int computeIndex(int row, int col, int width) { - return row*width + col; - } - - // no need to check for overflow here because these ints are all small kernel values - // and kernel sizes are small (3x3, 5x5, 7x7) - private static int sum(int[] ints) { - int sum = 0; - for (int i : ints) { - sum += i; - } - return sum; - } - - private static byte[] getPixels(BufferedImage image) { - return ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); - } - - private static void validateDepth(int depth) { - if (depth < 1 || depth > MAX_DEPTH) { - throw new RuntimeException("blur depth must be > 0 and < " + MAX_DEPTH); - } - } - - private static int[] bytesToInts(byte[] bytes) { - int[] ints = new int[bytes.length]; - - for (int i=0; i contextPathMaybe; private final Set modules = new HashSet<>(); - private Configuration(Integer port, Package basePackage, String contextPathMaybe, Set modules) { + private Configuration( + Integer port, Package basePackage, String contextPathMaybe, Set modules) { this.port = port; this.basePackage = basePackage; this.contextPathMaybe = Optional.ofNullable(contextPathMaybe); diff --git a/jvm/src/main/java/com/muchq/lunarcat/config/LunarCatServiceModule.java b/jvm/src/main/java/com/muchq/lunarcat/config/LunarCatServiceModule.java index 5d59a58b..159ad634 100644 --- a/jvm/src/main/java/com/muchq/lunarcat/config/LunarCatServiceModule.java +++ b/jvm/src/main/java/com/muchq/lunarcat/config/LunarCatServiceModule.java @@ -36,12 +36,12 @@ protected void configure() { private void bindStartupTasks() { Multibinder multibinder = Multibinder.newSetBinder(binder(), StartupTask.class); - Set> tasks = new Reflections( - new ConfigurationBuilder() - .forPackages(packagesToScan.toArray(new String[0])) - .setScanners(new SubTypesScanner(true)) - ) - .getSubTypesOf(StartupTask.class); + Set> tasks = + new Reflections( + new ConfigurationBuilder() + .forPackages(packagesToScan.toArray(new String[0])) + .setScanners(new SubTypesScanner(true))) + .getSubTypesOf(StartupTask.class); if (tasks != null) { for (Class task : tasks) { diff --git a/jvm/src/main/java/com/muchq/lunarcat/providers/ErrorResponseMessageBodyWriter.java b/jvm/src/main/java/com/muchq/lunarcat/providers/ErrorResponseMessageBodyWriter.java index ab72d7a9..dbf1e975 100644 --- a/jvm/src/main/java/com/muchq/lunarcat/providers/ErrorResponseMessageBodyWriter.java +++ b/jvm/src/main/java/com/muchq/lunarcat/providers/ErrorResponseMessageBodyWriter.java @@ -23,31 +23,31 @@ public ErrorResponseMessageBodyWriter(ObjectMapper mapper) { } @Override - public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + public boolean isWriteable( + Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { return ErrorResponse.class.isAssignableFrom(type); } @Override public long getSize( - ErrorResponse errorResponse, - Class type, - Type genericType, - Annotation[] annotations, - MediaType mediaType - ) { + ErrorResponse errorResponse, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { return 0; } @Override public void writeTo( - ErrorResponse errorResponse, - Class type, - Type genericType, - Annotation[] annotations, - MediaType mediaType, - MultivaluedMap httpHeaders, - OutputStream entityStream - ) throws IOException, WebApplicationException { + ErrorResponse errorResponse, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) + throws IOException, WebApplicationException { entityStream.write(mapper.writeValueAsBytes(errorResponse)); } } diff --git a/jvm/src/main/java/com/muchq/lunarcat/providers/OptionalMessageBodyWriter.java b/jvm/src/main/java/com/muchq/lunarcat/providers/OptionalMessageBodyWriter.java index 9e1b8391..a8b2bfe4 100644 --- a/jvm/src/main/java/com/muchq/lunarcat/providers/OptionalMessageBodyWriter.java +++ b/jvm/src/main/java/com/muchq/lunarcat/providers/OptionalMessageBodyWriter.java @@ -15,7 +15,7 @@ import javax.ws.rs.ext.Provider; @Provider -@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM }) +@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM}) public class OptionalMessageBodyWriter implements MessageBodyWriter> { private final ObjectMapper mapper; @@ -27,30 +27,30 @@ public OptionalMessageBodyWriter(ObjectMapper mapper) { @Override public long getSize( - Optional entity, - Class type, - Type genericType, - Annotation[] annotations, - MediaType mediaType - ) { + Optional entity, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { return 0; } @Override - public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + public boolean isWriteable( + Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { return (Optional.class.isAssignableFrom(type)); } @Override public void writeTo( - Optional entity, - Class type, - Type genericType, - Annotation[] annotations, - MediaType mediaType, - MultivaluedMap httpHeaders, - OutputStream entityStream - ) throws IOException { + Optional entity, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) + throws IOException { if (entity.isEmpty()) { throw new NotFoundException(); } diff --git a/jvm/src/main/java/com/muchq/lunarcat/providers/UnhandledExceptionMapper.java b/jvm/src/main/java/com/muchq/lunarcat/providers/UnhandledExceptionMapper.java index 5118d42f..108a85c1 100644 --- a/jvm/src/main/java/com/muchq/lunarcat/providers/UnhandledExceptionMapper.java +++ b/jvm/src/main/java/com/muchq/lunarcat/providers/UnhandledExceptionMapper.java @@ -10,7 +10,7 @@ import org.slf4j.LoggerFactory; @Provider -@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM }) +@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM}) public class UnhandledExceptionMapper implements ExceptionMapper { private static final Logger LOGGER = LoggerFactory.getLogger(UnhandledExceptionMapper.class); @@ -26,6 +26,9 @@ public Response toResponse(Exception e) { } private Response error(int status, String message) { - return Response.status(status).type(MediaType.APPLICATION_JSON_TYPE).entity(new ErrorResponse(message)).build(); + return Response.status(status) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(new ErrorResponse(message)) + .build(); } } diff --git a/jvm/src/main/java/com/muchq/mcpserver/McpAuthenticationFilter.java b/jvm/src/main/java/com/muchq/mcpserver/McpAuthenticationFilter.java index 6dcc282e..801c1bc9 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/McpAuthenticationFilter.java +++ b/jvm/src/main/java/com/muchq/mcpserver/McpAuthenticationFilter.java @@ -12,14 +12,13 @@ import io.micronaut.http.filter.HttpServerFilter; import io.micronaut.http.filter.ServerFilterChain; import jakarta.inject.Inject; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; - @Filter("/mcp/**") @Requires(property = "mcp.auth.token") public class McpAuthenticationFilter implements HttpServerFilter { @@ -65,14 +64,16 @@ public Publisher> doFilter( String token = authHeader.substring(7); // Remove "Bearer " prefix if (!MessageDigest.isEqual( - requiredToken.getBytes(StandardCharsets.UTF_8), - token.getBytes(StandardCharsets.UTF_8))) { + requiredToken.getBytes(StandardCharsets.UTF_8), token.getBytes(StandardCharsets.UTF_8))) { LOG.warn("Invalid authentication token"); return Mono.just( HttpResponse.status(HttpStatus.UNAUTHORIZED) .body( new JsonRpcResponse( - "2.0", null, null, new JsonRpcError(-32000, "Invalid authentication token")))); + "2.0", + null, + null, + new JsonRpcError(-32000, "Invalid authentication token")))); } LOG.debug("Authentication successful"); diff --git a/jvm/src/main/java/com/muchq/mcpserver/McpModule.java b/jvm/src/main/java/com/muchq/mcpserver/McpModule.java index 53c4f243..b113af8f 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/McpModule.java +++ b/jvm/src/main/java/com/muchq/mcpserver/McpModule.java @@ -13,44 +13,43 @@ import com.muchq.mcpserver.tools.ToolRegistry; import io.micronaut.context.annotation.Context; import io.micronaut.context.annotation.Factory; - import java.time.Clock; import java.util.List; @Factory public class McpModule { - @Context - public Clock clock() { - return Clock.systemUTC(); - } + @Context + public Clock clock() { + return Clock.systemUTC(); + } - @Context - public HttpClient httpClient() { - return new Jdk11HttpClient(java.net.http.HttpClient.newHttpClient()); - } + @Context + public HttpClient httpClient() { + return new Jdk11HttpClient(java.net.http.HttpClient.newHttpClient()); + } - @Context - public ChessClient chessClient(HttpClient httpClient, ObjectMapper objectMapper) { - return new ChessClient(httpClient, objectMapper); - } + @Context + public ChessClient chessClient(HttpClient httpClient, ObjectMapper objectMapper) { + return new ChessClient(httpClient, objectMapper); + } - @Context - public ObjectMapper objectMapper() { - return JsonUtils.mapper(); - } + @Context + public ObjectMapper objectMapper() { + return JsonUtils.mapper(); + } - @Context - public List mcpTools(Clock clock, ChessClient chessClient, ObjectMapper objectMapper) { + @Context + public List mcpTools(Clock clock, ChessClient chessClient, ObjectMapper objectMapper) { return List.of( new ChessComGamesTool(chessClient, objectMapper), new ChessComPlayerTool(chessClient, objectMapper), new ChessComStatsTool(chessClient, objectMapper), new ServerTimeTool(clock)); - } + } - @Context - public ToolRegistry toolRegistry(List tools) { - return new ToolRegistry(tools); - } + @Context + public ToolRegistry toolRegistry(List tools) { + return new ToolRegistry(tools); + } } diff --git a/jvm/src/main/java/com/muchq/mcpserver/McpRequestHandler.java b/jvm/src/main/java/com/muchq/mcpserver/McpRequestHandler.java index 962e9ac3..61d6bedf 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/McpRequestHandler.java +++ b/jvm/src/main/java/com/muchq/mcpserver/McpRequestHandler.java @@ -15,11 +15,10 @@ import com.muchq.mcpserver.tools.ToolRegistry; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; - @Singleton public class McpRequestHandler { private static final Logger LOG = LoggerFactory.getLogger(McpRequestHandler.class); @@ -64,13 +63,11 @@ private JsonRpcResponse handleToolsList(JsonRpcRequest request) { private JsonRpcResponse handleToolsCall(JsonRpcRequest request) { try { - ToolCallParams params = - objectMapper.convertValue(request.params(), ToolCallParams.class); + ToolCallParams params = objectMapper.convertValue(request.params(), ToolCallParams.class); String toolResult = toolRegistry.executeTool(params.name(), params.arguments()); - ToolCallResult result = - new ToolCallResult(List.of(new ContentItem("text", toolResult))); + ToolCallResult result = new ToolCallResult(List.of(new ContentItem("text", toolResult))); return new JsonRpcResponse("2.0", request.id(), result, null); } catch (Exception e) { diff --git a/jvm/src/main/java/com/muchq/mcpserver/dtos/ClientInfo.java b/jvm/src/main/java/com/muchq/mcpserver/dtos/ClientInfo.java index d7c10905..bb6e552c 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/dtos/ClientInfo.java +++ b/jvm/src/main/java/com/muchq/mcpserver/dtos/ClientInfo.java @@ -2,4 +2,5 @@ import com.fasterxml.jackson.annotation.JsonProperty; -public record ClientInfo(@JsonProperty("name") String name, @JsonProperty("version") String version) {} +public record ClientInfo( + @JsonProperty("name") String name, @JsonProperty("version") String version) {} diff --git a/jvm/src/main/java/com/muchq/mcpserver/dtos/ContentItem.java b/jvm/src/main/java/com/muchq/mcpserver/dtos/ContentItem.java index fb00c5dd..a3acf429 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/dtos/ContentItem.java +++ b/jvm/src/main/java/com/muchq/mcpserver/dtos/ContentItem.java @@ -2,5 +2,4 @@ import com.fasterxml.jackson.annotation.JsonProperty; -public record ContentItem( - @JsonProperty("type") String type, @JsonProperty("text") String text) {} +public record ContentItem(@JsonProperty("type") String type, @JsonProperty("text") String text) {} diff --git a/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComGamesTool.java b/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComGamesTool.java index 478a78a5..28c1947c 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComGamesTool.java +++ b/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComGamesTool.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.muchq.chess_com_api.ChessClient; - import java.io.IOException; import java.time.YearMonth; import java.util.List; @@ -10,54 +9,65 @@ public class ChessComGamesTool implements McpTool { - private final ChessClient chessClient; - private final ObjectMapper mapper; + private final ChessClient chessClient; + private final ObjectMapper mapper; - public ChessComGamesTool(ChessClient chessClient, ObjectMapper mapper) { - this.chessClient = chessClient; - this.mapper = mapper; - } + public ChessComGamesTool(ChessClient chessClient, ObjectMapper mapper) { + this.chessClient = chessClient; + this.mapper = mapper; + } - @Override - public String getName() { - return "chess_com_games"; - } + @Override + public String getName() { + return "chess_com_games"; + } - @Override - public String getDescription() { - return "Returns the requested user's chess.com games for the specified month and year. For example, username: hikaru, year: 2025, month: 01"; - } + @Override + public String getDescription() { + return "Returns the requested user's chess.com games for the specified month and year. For" + + " example, username: hikaru, year: 2025, month: 01"; + } + + @Override + public Map getInputSchema() { + return Map.of( + "type", "object", + "properties", + Map.of( + "username", + Map.of("type", "string", "description", "The player's chess.com username"), + "year", + Map.of( + "type", + "string", + "description", + "The year the games were played (yyyy format)"), + "month", + Map.of( + "type", + "string", + "description", + "The month the games were played (MM format)")), + "required", List.of("username", "year", "month")); + } + + @Override + public String execute(Map arguments) { + String player = (String) arguments.get("username"); + String yearStr = (String) arguments.get("year"); + String monthStr = (String) arguments.get("month"); + int year = Integer.parseInt(yearStr); + int month = Integer.parseInt(monthStr); + var gamesMaybe = chessClient.fetchGames(player, YearMonth.of(year, month)); - @Override - public Map getInputSchema() { - return Map.of( - "type", "object", - "properties", Map.of( - "username", Map.of("type", "string", "description", "The player's chess.com username"), - "year", Map.of("type", "string", "description", "The year the games were played (yyyy format)"), - "month", Map.of("type", "string", "description", "The month the games were played (MM format)") - ), - "required", List.of("username", "year", "month") - ); + if (gamesMaybe.isEmpty()) { + return "player not found"; } - @Override - public String execute(Map arguments) { - String player = (String) arguments.get("username"); - String yearStr = (String) arguments.get("year"); - String monthStr = (String) arguments.get("month"); - int year = Integer.parseInt(yearStr); - int month = Integer.parseInt(monthStr); - var gamesMaybe = chessClient.fetchGames(player, YearMonth.of(year, month)); - - if (gamesMaybe.isEmpty()) { - return "player not found"; - } - - try { - return mapper.writeValueAsString(gamesMaybe.get()); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + return mapper.writeValueAsString(gamesMaybe.get()); + } catch (IOException e) { + throw new RuntimeException(e); } + } } diff --git a/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComPlayerTool.java b/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComPlayerTool.java index 7ff82b8b..78ec1f05 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComPlayerTool.java +++ b/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComPlayerTool.java @@ -2,54 +2,53 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.muchq.chess_com_api.ChessClient; - import java.io.IOException; import java.util.List; import java.util.Map; public class ChessComPlayerTool implements McpTool { - private final ChessClient chessClient; - private final ObjectMapper mapper; - - public ChessComPlayerTool(ChessClient chessClient, ObjectMapper mapper) { - this.chessClient = chessClient; - this.mapper = mapper; - } - - @Override - public String getName() { - return "chess_com_player"; - } - - @Override - public String getDescription() { - return "Returns the requested user's chess.com player information"; - } - - @Override - public Map getInputSchema() { - return Map.of( - "type", "object", - "properties", Map.of( - "username", Map.of("type", "string", "description", "The player's chess.com username") - ), - "required", List.of("username") - ); + private final ChessClient chessClient; + private final ObjectMapper mapper; + + public ChessComPlayerTool(ChessClient chessClient, ObjectMapper mapper) { + this.chessClient = chessClient; + this.mapper = mapper; + } + + @Override + public String getName() { + return "chess_com_player"; + } + + @Override + public String getDescription() { + return "Returns the requested user's chess.com player information"; + } + + @Override + public Map getInputSchema() { + return Map.of( + "type", "object", + "properties", + Map.of( + "username", + Map.of("type", "string", "description", "The player's chess.com username")), + "required", List.of("username")); + } + + @Override + public String execute(Map arguments) { + String player = (String) arguments.get("username"); + var playerMaybe = chessClient.fetchPlayer(player); + if (playerMaybe.isEmpty()) { + return "player not found"; } - @Override - public String execute(Map arguments) { - String player = (String) arguments.get("username"); - var playerMaybe = chessClient.fetchPlayer(player); - if (playerMaybe.isEmpty()) { - return "player not found"; - } - - try { - return mapper.writeValueAsString(playerMaybe.get()); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + return mapper.writeValueAsString(playerMaybe.get()); + } catch (IOException e) { + throw new RuntimeException(e); } + } } diff --git a/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComStatsTool.java b/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComStatsTool.java index cad19104..87d7d4d5 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComStatsTool.java +++ b/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComStatsTool.java @@ -8,47 +8,47 @@ public class ChessComStatsTool implements McpTool { - private final ChessClient chessClient; - private final ObjectMapper mapper; - - public ChessComStatsTool(ChessClient chessClient, ObjectMapper mapper) { - this.chessClient = chessClient; - this.mapper = mapper; - } - - @Override - public String getName() { - return "chess_com_stats"; - } - - @Override - public String getDescription() { - return "Returns the requested user's chess.com player stats"; - } - - @Override - public Map getInputSchema() { - return Map.of( - "type", "object", - "properties", Map.of( - "username", Map.of("type", "string", "description", "The player's chess.com username") - ), - "required", List.of("username") - ); + private final ChessClient chessClient; + private final ObjectMapper mapper; + + public ChessComStatsTool(ChessClient chessClient, ObjectMapper mapper) { + this.chessClient = chessClient; + this.mapper = mapper; + } + + @Override + public String getName() { + return "chess_com_stats"; + } + + @Override + public String getDescription() { + return "Returns the requested user's chess.com player stats"; + } + + @Override + public Map getInputSchema() { + return Map.of( + "type", "object", + "properties", + Map.of( + "username", + Map.of("type", "string", "description", "The player's chess.com username")), + "required", List.of("username")); + } + + @Override + public String execute(Map arguments) { + String player = (String) arguments.get("username"); + var statsMaybe = chessClient.fetchStats(player); + if (statsMaybe.isEmpty()) { + return "player not found"; } - @Override - public String execute(Map arguments) { - String player = (String) arguments.get("username"); - var statsMaybe = chessClient.fetchStats(player); - if (statsMaybe.isEmpty()) { - return "player not found"; - } - - try { - return mapper.writeValueAsString(statsMaybe.get()); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + return mapper.writeValueAsString(statsMaybe.get()); + } catch (IOException e) { + throw new RuntimeException(e); } + } } diff --git a/jvm/src/main/java/com/muchq/mcpserver/tools/McpTool.java b/jvm/src/main/java/com/muchq/mcpserver/tools/McpTool.java index 30564903..152515c9 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/tools/McpTool.java +++ b/jvm/src/main/java/com/muchq/mcpserver/tools/McpTool.java @@ -3,8 +3,11 @@ import java.util.Map; public interface McpTool { - String getName(); - String getDescription(); - Map getInputSchema(); - String execute(Map arguments); + String getName(); + + String getDescription(); + + Map getInputSchema(); + + String execute(Map arguments); } diff --git a/jvm/src/main/java/com/muchq/mcpserver/tools/ServerTimeTool.java b/jvm/src/main/java/com/muchq/mcpserver/tools/ServerTimeTool.java index f2307b3d..a53ac3dd 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/tools/ServerTimeTool.java +++ b/jvm/src/main/java/com/muchq/mcpserver/tools/ServerTimeTool.java @@ -5,29 +5,29 @@ import java.util.Map; public class ServerTimeTool implements McpTool { - private final Clock clock; + private final Clock clock; - public ServerTimeTool(Clock clock) { - this.clock = clock; - } + public ServerTimeTool(Clock clock) { + this.clock = clock; + } - @Override - public String getName() { - return "server_time"; - } + @Override + public String getName() { + return "server_time"; + } - @Override - public String getDescription() { - return "Returns the current timestamp according to the server's system clock"; - } + @Override + public String getDescription() { + return "Returns the current timestamp according to the server's system clock"; + } - @Override - public Map getInputSchema() { - return Map.of("type", "object", "properties", Map.of()); - } + @Override + public Map getInputSchema() { + return Map.of("type", "object", "properties", Map.of()); + } - @Override - public String execute(Map arguments) { - return String.valueOf(Instant.now(clock).toEpochMilli()); - } + @Override + public String execute(Map arguments) { + return String.valueOf(Instant.now(clock).toEpochMilli()); + } } diff --git a/jvm/src/main/java/com/muchq/mcpserver/tools/ToolRegistry.java b/jvm/src/main/java/com/muchq/mcpserver/tools/ToolRegistry.java index 9f76f47d..f36d1ff1 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/tools/ToolRegistry.java +++ b/jvm/src/main/java/com/muchq/mcpserver/tools/ToolRegistry.java @@ -1,30 +1,29 @@ package com.muchq.mcpserver.tools; import com.muchq.mcpserver.dtos.Tool; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ToolRegistry { private static final Logger LOG = LoggerFactory.getLogger(ToolRegistry.class); private final Map toolsByName; public ToolRegistry(List tools) { - this.toolsByName = tools.stream().collect(Collectors.toMap(McpTool::getName, Function.identity())); + this.toolsByName = + tools.stream().collect(Collectors.toMap(McpTool::getName, Function.identity())); for (var tool : tools) { LOG.info("registered {} tool", tool.getName()); } } public List getTools() { - return toolsByName.values() - .stream() - .map(t -> new Tool(t.getName(), t.getDescription(), t.getInputSchema())) - .toList(); + return toolsByName.values().stream() + .map(t -> new Tool(t.getName(), t.getDescription(), t.getInputSchema())) + .toList(); } public String executeTool(String name, Map arguments) { diff --git a/jvm/src/main/java/com/muchq/one_d4/App.java b/jvm/src/main/java/com/muchq/one_d4/App.java index 10bf545c..a3e80d70 100644 --- a/jvm/src/main/java/com/muchq/one_d4/App.java +++ b/jvm/src/main/java/com/muchq/one_d4/App.java @@ -5,10 +5,10 @@ import org.slf4j.LoggerFactory; public class App { - private static final Logger LOG = LoggerFactory.getLogger(App.class); + private static final Logger LOG = LoggerFactory.getLogger(App.class); - public static void main(String[] args) { - LOG.info("Starting Chess Game Indexer"); - Micronaut.run(App.class, args); - } + public static void main(String[] args) { + LOG.info("Starting Chess Game Indexer"); + Micronaut.run(App.class, args); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/IndexerModule.java b/jvm/src/main/java/com/muchq/one_d4/IndexerModule.java index 67697517..62e6906c 100644 --- a/jvm/src/main/java/com/muchq/one_d4/IndexerModule.java +++ b/jvm/src/main/java/com/muchq/one_d4/IndexerModule.java @@ -4,6 +4,7 @@ import com.muchq.chess_com_api.ChessClient; import com.muchq.http_client.core.HttpClient; import com.muchq.http_client.jdk.Jdk11HttpClient; +import com.muchq.json.JsonUtils; import com.muchq.one_d4.chessql.compiler.CompiledQuery; import com.muchq.one_d4.chessql.compiler.QueryCompiler; import com.muchq.one_d4.chessql.compiler.SqlCompiler; @@ -26,111 +27,110 @@ import com.muchq.one_d4.queue.IndexQueue; import com.muchq.one_d4.worker.IndexWorker; import com.muchq.one_d4.worker.IndexWorkerLifecycle; -import com.muchq.json.JsonUtils; import io.micronaut.context.annotation.Context; import io.micronaut.context.annotation.Factory; import io.micronaut.context.annotation.Value; - -import javax.sql.DataSource; import java.util.List; +import javax.sql.DataSource; @Factory public class IndexerModule { - @Context - public ObjectMapper objectMapper() { - return JsonUtils.mapper(); - } - - @Context - public HttpClient httpClient() { - return new Jdk11HttpClient(java.net.http.HttpClient.newHttpClient()); - } - - @Context - public ChessClient chessClient(HttpClient httpClient, ObjectMapper objectMapper) { - return new ChessClient(httpClient, objectMapper); - } - - @Context - public DataSource dataSource( - @Value("${indexer.db.url:jdbc:h2:mem:indexer;DB_CLOSE_DELAY=-1}") String jdbcUrl, - @Value("${indexer.db.username:sa}") String username, - @Value("${indexer.db.password:}") String password) { - return DataSourceFactory.create(jdbcUrl, username, password); - } - - @Context - public Migration migration( - DataSource dataSource, - @Value("${indexer.db.url:jdbc:h2:mem:indexer;DB_CLOSE_DELAY=-1}") String jdbcUrl) { - boolean useH2 = jdbcUrl.contains(":h2:"); - Migration migration = new Migration(dataSource, useH2); - migration.run(); - return migration; - } - - @Context - public IndexingRequestStore indexingRequestStore(DataSource dataSource) { - return new IndexingRequestDao(dataSource); - } - - @Context - public GameFeatureStore gameFeatureStore( - DataSource dataSource, - @Value("${indexer.db.url:jdbc:h2:mem:indexer;DB_CLOSE_DELAY=-1}") String jdbcUrl) { - boolean useH2 = jdbcUrl.contains(":h2:"); - return new GameFeatureDao(dataSource, useH2); - } - - @Context - public IndexQueue indexQueue() { - return new InMemoryIndexQueue(); - } - - @Context - public QueryCompiler queryCompiler() { - return new SqlCompiler(); - } - - @Context - public List motifDetectors() { - return List.of( - new PinDetector(), - new CrossPinDetector(), - new ForkDetector(), - new SkewerDetector(), - new DiscoveredAttackDetector() - ); - } - - @Context - public PgnParser pgnParser() { - return new PgnParser(); - } - - @Context - public GameReplayer gameReplayer() { - return new GameReplayer(); - } - - @Context - public FeatureExtractor featureExtractor(PgnParser pgnParser, GameReplayer replayer, List detectors) { - return new FeatureExtractor(pgnParser, replayer, detectors); - } - - @Context - public IndexWorker indexWorker( - ChessClient chessClient, - FeatureExtractor featureExtractor, - IndexingRequestStore requestStore, - GameFeatureStore gameFeatureStore, - ObjectMapper objectMapper) { - return new IndexWorker(chessClient, featureExtractor, requestStore, gameFeatureStore, objectMapper); - } - - @Context - public IndexWorkerLifecycle indexWorkerLifecycle(IndexQueue queue, IndexWorker worker) { - return new IndexWorkerLifecycle(queue, worker); - } + @Context + public ObjectMapper objectMapper() { + return JsonUtils.mapper(); + } + + @Context + public HttpClient httpClient() { + return new Jdk11HttpClient(java.net.http.HttpClient.newHttpClient()); + } + + @Context + public ChessClient chessClient(HttpClient httpClient, ObjectMapper objectMapper) { + return new ChessClient(httpClient, objectMapper); + } + + @Context + public DataSource dataSource( + @Value("${indexer.db.url:jdbc:h2:mem:indexer;DB_CLOSE_DELAY=-1}") String jdbcUrl, + @Value("${indexer.db.username:sa}") String username, + @Value("${indexer.db.password:}") String password) { + return DataSourceFactory.create(jdbcUrl, username, password); + } + + @Context + public Migration migration( + DataSource dataSource, + @Value("${indexer.db.url:jdbc:h2:mem:indexer;DB_CLOSE_DELAY=-1}") String jdbcUrl) { + boolean useH2 = jdbcUrl.contains(":h2:"); + Migration migration = new Migration(dataSource, useH2); + migration.run(); + return migration; + } + + @Context + public IndexingRequestStore indexingRequestStore(DataSource dataSource) { + return new IndexingRequestDao(dataSource); + } + + @Context + public GameFeatureStore gameFeatureStore( + DataSource dataSource, + @Value("${indexer.db.url:jdbc:h2:mem:indexer;DB_CLOSE_DELAY=-1}") String jdbcUrl) { + boolean useH2 = jdbcUrl.contains(":h2:"); + return new GameFeatureDao(dataSource, useH2); + } + + @Context + public IndexQueue indexQueue() { + return new InMemoryIndexQueue(); + } + + @Context + public QueryCompiler queryCompiler() { + return new SqlCompiler(); + } + + @Context + public List motifDetectors() { + return List.of( + new PinDetector(), + new CrossPinDetector(), + new ForkDetector(), + new SkewerDetector(), + new DiscoveredAttackDetector()); + } + + @Context + public PgnParser pgnParser() { + return new PgnParser(); + } + + @Context + public GameReplayer gameReplayer() { + return new GameReplayer(); + } + + @Context + public FeatureExtractor featureExtractor( + PgnParser pgnParser, GameReplayer replayer, List detectors) { + return new FeatureExtractor(pgnParser, replayer, detectors); + } + + @Context + public IndexWorker indexWorker( + ChessClient chessClient, + FeatureExtractor featureExtractor, + IndexingRequestStore requestStore, + GameFeatureStore gameFeatureStore, + ObjectMapper objectMapper) { + return new IndexWorker( + chessClient, featureExtractor, requestStore, gameFeatureStore, objectMapper); + } + + @Context + public IndexWorkerLifecycle indexWorkerLifecycle(IndexQueue queue, IndexWorker worker) { + return new IndexWorkerLifecycle(queue, worker); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/api/IndexController.java b/jvm/src/main/java/com/muchq/one_d4/api/IndexController.java index 9eb3aef9..cfd96aab 100644 --- a/jvm/src/main/java/com/muchq/one_d4/api/IndexController.java +++ b/jvm/src/main/java/com/muchq/one_d4/api/IndexController.java @@ -5,6 +5,7 @@ import com.muchq.one_d4.db.IndexingRequestStore; import com.muchq.one_d4.queue.IndexMessage; import com.muchq.one_d4.queue.IndexQueue; +import jakarta.inject.Singleton; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -12,57 +13,55 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.inject.Singleton; -import java.util.UUID; - @Singleton @Path("/index") public class IndexController { - private static final Logger LOG = LoggerFactory.getLogger(IndexController.class); + private static final Logger LOG = LoggerFactory.getLogger(IndexController.class); - private final IndexingRequestStore requestDao; - private final IndexQueue queue; + private final IndexingRequestStore requestDao; + private final IndexQueue queue; - public IndexController(IndexingRequestStore requestDao, IndexQueue queue) { - this.requestDao = requestDao; - this.queue = queue; - } + public IndexController(IndexingRequestStore requestDao, IndexQueue queue) { + this.requestDao = requestDao; + this.queue = queue; + } - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public IndexResponse createIndex(IndexRequest request) { - LOG.info("POST /index player={} platform={} months={}-{}", - request.player(), request.platform(), request.startMonth(), request.endMonth()); + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public IndexResponse createIndex(IndexRequest request) { + LOG.info( + "POST /index player={} platform={} months={}-{}", + request.player(), + request.platform(), + request.startMonth(), + request.endMonth()); - UUID id = requestDao.create( - request.player(), - request.platform(), - request.startMonth(), - request.endMonth() - ); + UUID id = + requestDao.create( + request.player(), request.platform(), request.startMonth(), request.endMonth()); - queue.enqueue(new IndexMessage( - id, - request.player(), - request.platform(), - request.startMonth(), - request.endMonth() - )); + queue.enqueue( + new IndexMessage( + id, request.player(), request.platform(), request.startMonth(), request.endMonth())); - return new IndexResponse(id, "PENDING", 0, null); - } + return new IndexResponse(id, "PENDING", 0, null); + } - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - public IndexResponse getIndex(@PathParam("id") UUID id) { - LOG.info("GET /index/{}", id); - return requestDao.findById(id) - .map(row -> new IndexResponse(row.id(), row.status(), row.gamesIndexed(), row.errorMessage())) - .orElseThrow(() -> new RuntimeException("Indexing request not found: " + id)); - } + @GET + @Path("/{id}") + @Produces(MediaType.APPLICATION_JSON) + public IndexResponse getIndex(@PathParam("id") UUID id) { + LOG.info("GET /index/{}", id); + return requestDao + .findById(id) + .map( + row -> + new IndexResponse(row.id(), row.status(), row.gamesIndexed(), row.errorMessage())) + .orElseThrow(() -> new RuntimeException("Indexing request not found: " + id)); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/api/QueryController.java b/jvm/src/main/java/com/muchq/one_d4/api/QueryController.java index 8a2595ca..e4473a0a 100644 --- a/jvm/src/main/java/com/muchq/one_d4/api/QueryController.java +++ b/jvm/src/main/java/com/muchq/one_d4/api/QueryController.java @@ -8,46 +8,48 @@ import com.muchq.one_d4.chessql.compiler.QueryCompiler; import com.muchq.one_d4.chessql.parser.Parser; import com.muchq.one_d4.db.GameFeatureStore; +import jakarta.inject.Singleton; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.inject.Singleton; -import java.util.List; - @Singleton @Path("/query") public class QueryController { - private static final Logger LOG = LoggerFactory.getLogger(QueryController.class); + private static final Logger LOG = LoggerFactory.getLogger(QueryController.class); - private final GameFeatureStore gameFeatureStore; - private final QueryCompiler queryCompiler; + private final GameFeatureStore gameFeatureStore; + private final QueryCompiler queryCompiler; - public QueryController(GameFeatureStore gameFeatureStore, QueryCompiler queryCompiler) { - this.gameFeatureStore = gameFeatureStore; - this.queryCompiler = queryCompiler; - } + public QueryController( + GameFeatureStore gameFeatureStore, QueryCompiler queryCompiler) { + this.gameFeatureStore = gameFeatureStore; + this.queryCompiler = queryCompiler; + } - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public QueryResponse query(QueryRequest request) { - LOG.info("POST /query query={} limit={} offset={}", request.query(), request.limit(), request.offset()); + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public QueryResponse query(QueryRequest request) { + LOG.info( + "POST /query query={} limit={} offset={}", + request.query(), + request.limit(), + request.offset()); - Expr expr = Parser.parse(request.query()); - CompiledQuery compiled = queryCompiler.compile(expr); + Expr expr = Parser.parse(request.query()); + CompiledQuery compiled = queryCompiler.compile(expr); - List rows = - gameFeatureStore.query(compiled, request.limit(), request.offset()); + List rows = + gameFeatureStore.query(compiled, request.limit(), request.offset()); - List dtos = rows.stream() - .map(GameFeatureRow::fromStore) - .toList(); + List dtos = rows.stream().map(GameFeatureRow::fromStore).toList(); - return new QueryResponse(dtos, dtos.size()); - } + return new QueryResponse(dtos, dtos.size()); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/api/dto/GameFeatureRow.java b/jvm/src/main/java/com/muchq/one_d4/api/dto/GameFeatureRow.java index b4dc15fa..eb7a6870 100644 --- a/jvm/src/main/java/com/muchq/one_d4/api/dto/GameFeatureRow.java +++ b/jvm/src/main/java/com/muchq/one_d4/api/dto/GameFeatureRow.java @@ -1,45 +1,42 @@ package com.muchq.one_d4.api.dto; import com.muchq.one_d4.db.GameFeatureStore; - import java.time.Instant; public record GameFeatureRow( - String gameUrl, - String platform, - String whiteUsername, - String blackUsername, - Integer whiteElo, - Integer blackElo, - String timeClass, - String eco, - String result, - Instant playedAt, - Integer numMoves, - boolean hasPin, - boolean hasCrossPin, - boolean hasFork, - boolean hasSkewer, - boolean hasDiscoveredAttack -) { - public static GameFeatureRow fromStore(GameFeatureStore.GameFeature row) { - return new GameFeatureRow( - row.gameUrl(), - row.platform(), - row.whiteUsername(), - row.blackUsername(), - row.whiteElo(), - row.blackElo(), - row.timeClass(), - row.eco(), - row.result(), - row.playedAt(), - row.numMoves(), - row.hasPin(), - row.hasCrossPin(), - row.hasFork(), - row.hasSkewer(), - row.hasDiscoveredAttack() - ); - } + String gameUrl, + String platform, + String whiteUsername, + String blackUsername, + Integer whiteElo, + Integer blackElo, + String timeClass, + String eco, + String result, + Instant playedAt, + Integer numMoves, + boolean hasPin, + boolean hasCrossPin, + boolean hasFork, + boolean hasSkewer, + boolean hasDiscoveredAttack) { + public static GameFeatureRow fromStore(GameFeatureStore.GameFeature row) { + return new GameFeatureRow( + row.gameUrl(), + row.platform(), + row.whiteUsername(), + row.blackUsername(), + row.whiteElo(), + row.blackElo(), + row.timeClass(), + row.eco(), + row.result(), + row.playedAt(), + row.numMoves(), + row.hasPin(), + row.hasCrossPin(), + row.hasFork(), + row.hasSkewer(), + row.hasDiscoveredAttack()); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/api/dto/IndexRequest.java b/jvm/src/main/java/com/muchq/one_d4/api/dto/IndexRequest.java index 13389fdb..7ac4f17b 100644 --- a/jvm/src/main/java/com/muchq/one_d4/api/dto/IndexRequest.java +++ b/jvm/src/main/java/com/muchq/one_d4/api/dto/IndexRequest.java @@ -1,4 +1,3 @@ package com.muchq.one_d4.api.dto; -public record IndexRequest(String player, String platform, String startMonth, String endMonth) { -} +public record IndexRequest(String player, String platform, String startMonth, String endMonth) {} diff --git a/jvm/src/main/java/com/muchq/one_d4/api/dto/IndexResponse.java b/jvm/src/main/java/com/muchq/one_d4/api/dto/IndexResponse.java index f8c7052c..6a9b74c4 100644 --- a/jvm/src/main/java/com/muchq/one_d4/api/dto/IndexResponse.java +++ b/jvm/src/main/java/com/muchq/one_d4/api/dto/IndexResponse.java @@ -2,5 +2,4 @@ import java.util.UUID; -public record IndexResponse(UUID id, String status, int gamesIndexed, String errorMessage) { -} +public record IndexResponse(UUID id, String status, int gamesIndexed, String errorMessage) {} diff --git a/jvm/src/main/java/com/muchq/one_d4/api/dto/QueryRequest.java b/jvm/src/main/java/com/muchq/one_d4/api/dto/QueryRequest.java index 6ed26905..c284db74 100644 --- a/jvm/src/main/java/com/muchq/one_d4/api/dto/QueryRequest.java +++ b/jvm/src/main/java/com/muchq/one_d4/api/dto/QueryRequest.java @@ -1,9 +1,9 @@ package com.muchq.one_d4.api.dto; public record QueryRequest(String query, int limit, int offset) { - public QueryRequest { - if (limit <= 0) limit = 50; - if (limit > 1000) limit = 1000; - if (offset < 0) offset = 0; - } + public QueryRequest { + if (limit <= 0) limit = 50; + if (limit > 1000) limit = 1000; + if (offset < 0) offset = 0; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/api/dto/QueryResponse.java b/jvm/src/main/java/com/muchq/one_d4/api/dto/QueryResponse.java index af0d22d0..5085e9ed 100644 --- a/jvm/src/main/java/com/muchq/one_d4/api/dto/QueryResponse.java +++ b/jvm/src/main/java/com/muchq/one_d4/api/dto/QueryResponse.java @@ -2,5 +2,4 @@ import java.util.List; -public record QueryResponse(List games, int count) { -} +public record QueryResponse(List games, int count) {} diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/AndExpr.java b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/AndExpr.java index fbc63813..03bff23a 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/AndExpr.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/AndExpr.java @@ -2,5 +2,4 @@ import java.util.List; -public record AndExpr(List operands) implements Expr { -} +public record AndExpr(List operands) implements Expr {} diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/ComparisonExpr.java b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/ComparisonExpr.java index ec8dff59..0649e1db 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/ComparisonExpr.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/ComparisonExpr.java @@ -1,4 +1,3 @@ package com.muchq.one_d4.chessql.ast; -public record ComparisonExpr(String field, String operator, Object value) implements Expr { -} +public record ComparisonExpr(String field, String operator, Object value) implements Expr {} diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/Expr.java b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/Expr.java index 436d9eac..536f9a14 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/Expr.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/Expr.java @@ -1,4 +1,3 @@ package com.muchq.one_d4.chessql.ast; -public sealed interface Expr permits OrExpr, AndExpr, NotExpr, ComparisonExpr, InExpr, MotifExpr { -} +public sealed interface Expr permits OrExpr, AndExpr, NotExpr, ComparisonExpr, InExpr, MotifExpr {} diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/InExpr.java b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/InExpr.java index 7e0dff53..f73177d9 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/InExpr.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/InExpr.java @@ -2,5 +2,4 @@ import java.util.List; -public record InExpr(String field, List values) implements Expr { -} +public record InExpr(String field, List values) implements Expr {} diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/MotifExpr.java b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/MotifExpr.java index f4cf8d37..3c632748 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/MotifExpr.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/MotifExpr.java @@ -1,4 +1,3 @@ package com.muchq.one_d4.chessql.ast; -public record MotifExpr(String motifName) implements Expr { -} +public record MotifExpr(String motifName) implements Expr {} diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/NotExpr.java b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/NotExpr.java index 05bcd928..5d88ba9d 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/NotExpr.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/NotExpr.java @@ -1,4 +1,3 @@ package com.muchq.one_d4.chessql.ast; -public record NotExpr(Expr operand) implements Expr { -} +public record NotExpr(Expr operand) implements Expr {} diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/OrExpr.java b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/OrExpr.java index 164727ae..62873294 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/ast/OrExpr.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/ast/OrExpr.java @@ -2,5 +2,4 @@ import java.util.List; -public record OrExpr(List operands) implements Expr { -} +public record OrExpr(List operands) implements Expr {} diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/CompiledQuery.java b/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/CompiledQuery.java index 058332fc..5c4c4b3a 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/CompiledQuery.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/CompiledQuery.java @@ -2,5 +2,4 @@ import java.util.List; -public record CompiledQuery(String sql, List parameters) { -} +public record CompiledQuery(String sql, List parameters) {} diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/QueryCompiler.java b/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/QueryCompiler.java index c3dd7cfd..b7db03dc 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/QueryCompiler.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/QueryCompiler.java @@ -3,5 +3,5 @@ import com.muchq.one_d4.chessql.ast.Expr; public interface QueryCompiler { - T compile(Expr expr); + T compile(Expr expr); } diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/SqlCompiler.java b/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/SqlCompiler.java index b204ee27..1f1e9c8d 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/SqlCompiler.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/compiler/SqlCompiler.java @@ -7,7 +7,6 @@ import com.muchq.one_d4.chessql.ast.MotifExpr; import com.muchq.one_d4.chessql.ast.NotExpr; import com.muchq.one_d4.chessql.ast.OrExpr; - import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -15,91 +14,98 @@ import java.util.stream.Collectors; public class SqlCompiler implements QueryCompiler { - private static final Set VALID_COLUMNS = Set.of( - "white_username", "black_username", "white_elo", "black_elo", - "time_class", "eco", "result", "num_moves", "platform", - "game_url", "played_at" - ); + private static final Set VALID_COLUMNS = + Set.of( + "white_username", + "black_username", + "white_elo", + "black_elo", + "time_class", + "eco", + "result", + "num_moves", + "platform", + "game_url", + "played_at"); - private static final Set VALID_MOTIFS = Set.of( - "pin", "cross_pin", "fork", "skewer", "discovered_attack" - ); + private static final Set VALID_MOTIFS = + Set.of("pin", "cross_pin", "fork", "skewer", "discovered_attack"); - private static final Map FIELD_MAP = Map.of( - "white.elo", "white_elo", - "black.elo", "black_elo", - "white.username", "white_username", - "black.username", "black_username", - "time.class", "time_class", - "num.moves", "num_moves", - "game.url", "game_url", - "played.at", "played_at" - ); + private static final Map FIELD_MAP = + Map.of( + "white.elo", "white_elo", + "black.elo", "black_elo", + "white.username", "white_username", + "black.username", "black_username", + "time.class", "time_class", + "num.moves", "num_moves", + "game.url", "game_url", + "played.at", "played_at"); - private static final Set VALID_OPS = Set.of("=", "!=", "<", "<=", ">", ">="); + private static final Set VALID_OPS = Set.of("=", "!=", "<", "<=", ">", ">="); - public CompiledQuery compile(Expr expr) { - List params = new ArrayList<>(); - String sql = compileExpr(expr, params); - return new CompiledQuery(sql, params); - } + public CompiledQuery compile(Expr expr) { + List params = new ArrayList<>(); + String sql = compileExpr(expr, params); + return new CompiledQuery(sql, params); + } - private String compileExpr(Expr expr, List params) { - return switch (expr) { - case OrExpr or -> or.operands().stream() - .map(e -> compileExpr(e, params)) - .collect(Collectors.joining(" OR ", "(", ")")); - case AndExpr and -> and.operands().stream() - .map(e -> compileExpr(e, params)) - .collect(Collectors.joining(" AND ", "(", ")")); - case NotExpr not -> "(NOT " + compileExpr(not.operand(), params) + ")"; - case ComparisonExpr cmp -> compileComparison(cmp, params); - case InExpr in -> compileIn(in, params); - case MotifExpr motif -> compileMotif(motif); - }; - } + private String compileExpr(Expr expr, List params) { + return switch (expr) { + case OrExpr or -> + or.operands().stream() + .map(e -> compileExpr(e, params)) + .collect(Collectors.joining(" OR ", "(", ")")); + case AndExpr and -> + and.operands().stream() + .map(e -> compileExpr(e, params)) + .collect(Collectors.joining(" AND ", "(", ")")); + case NotExpr not -> "(NOT " + compileExpr(not.operand(), params) + ")"; + case ComparisonExpr cmp -> compileComparison(cmp, params); + case InExpr in -> compileIn(in, params); + case MotifExpr motif -> compileMotif(motif); + }; + } - private String compileComparison(ComparisonExpr cmp, List params) { - String column = resolveColumn(cmp.field()); - String op = cmp.operator(); - if (!VALID_OPS.contains(op)) { - throw new IllegalArgumentException("Invalid operator: " + op); - } - params.add(cmp.value()); - return column + " " + op + " ?"; + private String compileComparison(ComparisonExpr cmp, List params) { + String column = resolveColumn(cmp.field()); + String op = cmp.operator(); + if (!VALID_OPS.contains(op)) { + throw new IllegalArgumentException("Invalid operator: " + op); } + params.add(cmp.value()); + return column + " " + op + " ?"; + } - private String compileIn(InExpr in, List params) { - String column = resolveColumn(in.field()); - params.addAll(in.values()); - String placeholders = in.values().stream() - .map(v -> "?") - .collect(Collectors.joining(", ")); - return column + " IN (" + placeholders + ")"; - } + private String compileIn(InExpr in, List params) { + String column = resolveColumn(in.field()); + params.addAll(in.values()); + String placeholders = in.values().stream().map(v -> "?").collect(Collectors.joining(", ")); + return column + " IN (" + placeholders + ")"; + } - private String compileMotif(MotifExpr motif) { - String name = motif.motifName(); - if (!VALID_MOTIFS.contains(name)) { - throw new IllegalArgumentException("Unknown motif: " + name); - } - return "has_" + name + " = TRUE"; + private String compileMotif(MotifExpr motif) { + String name = motif.motifName(); + if (!VALID_MOTIFS.contains(name)) { + throw new IllegalArgumentException("Unknown motif: " + name); } + return "has_" + name + " = TRUE"; + } - private String resolveColumn(String field) { - String mapped = FIELD_MAP.get(field); - if (mapped != null) { - return mapped; - } - // Try direct column name (already underscore-separated) - if (VALID_COLUMNS.contains(field)) { - return field; - } - // Try converting dots to underscores - String underscored = field.replace('.', '_'); - if (VALID_COLUMNS.contains(underscored)) { - return underscored; - } - throw new IllegalArgumentException("Unknown field: " + field); + private String resolveColumn(String field) { + String mapped = FIELD_MAP.get(field); + if (mapped != null) { + return mapped; + } + // Try direct column name (already underscore-separated) + if (VALID_COLUMNS.contains(field)) { + return field; + } + // Try converting dots to underscores + String underscored = field.replace('.', '_'); + if (VALID_COLUMNS.contains(underscored)) { + return underscored; } + throw new IllegalArgumentException("Unknown field: " + field); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/Lexer.java b/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/Lexer.java index 94320bc0..18b37ae8 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/Lexer.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/Lexer.java @@ -5,116 +5,118 @@ import java.util.Map; public class Lexer { - private static final Map KEYWORDS = Map.of( - "AND", TokenType.AND, - "OR", TokenType.OR, - "NOT", TokenType.NOT, - "IN", TokenType.IN, - "motif", TokenType.MOTIF - ); + private static final Map KEYWORDS = + Map.of( + "AND", TokenType.AND, + "OR", TokenType.OR, + "NOT", TokenType.NOT, + "IN", TokenType.IN, + "motif", TokenType.MOTIF); - private final String input; - private int pos; + private final String input; + private int pos; - public Lexer(String input) { - this.input = input; - this.pos = 0; - } - - public List tokenize() { - List tokens = new ArrayList<>(); - while (pos < input.length()) { - char c = input.charAt(pos); + public Lexer(String input) { + this.input = input; + this.pos = 0; + } - if (Character.isWhitespace(c)) { - pos++; - continue; - } + public List tokenize() { + List tokens = new ArrayList<>(); + while (pos < input.length()) { + char c = input.charAt(pos); - if (c == '(') { - tokens.add(new Token(TokenType.LPAREN, "(", pos++)); - } else if (c == ')') { - tokens.add(new Token(TokenType.RPAREN, ")", pos++)); - } else if (c == '[') { - tokens.add(new Token(TokenType.LBRACKET, "[", pos++)); - } else if (c == ']') { - tokens.add(new Token(TokenType.RBRACKET, "]", pos++)); - } else if (c == ',') { - tokens.add(new Token(TokenType.COMMA, ",", pos++)); - } else if (c == '.') { - tokens.add(new Token(TokenType.DOT, ".", pos++)); - } else if (c == '=') { - tokens.add(new Token(TokenType.EQ, "=", pos++)); - } else if (c == '!' && peek() == '=') { - tokens.add(new Token(TokenType.NEQ, "!=", pos)); - pos += 2; - } else if (c == '<' && peek() == '=') { - tokens.add(new Token(TokenType.LTE, "<=", pos)); - pos += 2; - } else if (c == '<') { - tokens.add(new Token(TokenType.LT, "<", pos++)); - } else if (c == '>' && peek() == '=') { - tokens.add(new Token(TokenType.GTE, ">=", pos)); - pos += 2; - } else if (c == '>') { - tokens.add(new Token(TokenType.GT, ">", pos++)); - } else if (c == '"') { - tokens.add(readString()); - } else if (Character.isDigit(c) || (c == '-' && pos + 1 < input.length() && Character.isDigit(input.charAt(pos + 1)))) { - tokens.add(readNumber()); - } else if (Character.isLetter(c) || c == '_') { - tokens.add(readIdentifierOrKeyword()); - } else { - throw new IllegalArgumentException("Unexpected character '" + c + "' at position " + pos); - } - } + if (Character.isWhitespace(c)) { + pos++; + continue; + } - tokens.add(new Token(TokenType.EOF, "", pos)); - return tokens; + if (c == '(') { + tokens.add(new Token(TokenType.LPAREN, "(", pos++)); + } else if (c == ')') { + tokens.add(new Token(TokenType.RPAREN, ")", pos++)); + } else if (c == '[') { + tokens.add(new Token(TokenType.LBRACKET, "[", pos++)); + } else if (c == ']') { + tokens.add(new Token(TokenType.RBRACKET, "]", pos++)); + } else if (c == ',') { + tokens.add(new Token(TokenType.COMMA, ",", pos++)); + } else if (c == '.') { + tokens.add(new Token(TokenType.DOT, ".", pos++)); + } else if (c == '=') { + tokens.add(new Token(TokenType.EQ, "=", pos++)); + } else if (c == '!' && peek() == '=') { + tokens.add(new Token(TokenType.NEQ, "!=", pos)); + pos += 2; + } else if (c == '<' && peek() == '=') { + tokens.add(new Token(TokenType.LTE, "<=", pos)); + pos += 2; + } else if (c == '<') { + tokens.add(new Token(TokenType.LT, "<", pos++)); + } else if (c == '>' && peek() == '=') { + tokens.add(new Token(TokenType.GTE, ">=", pos)); + pos += 2; + } else if (c == '>') { + tokens.add(new Token(TokenType.GT, ">", pos++)); + } else if (c == '"') { + tokens.add(readString()); + } else if (Character.isDigit(c) + || (c == '-' && pos + 1 < input.length() && Character.isDigit(input.charAt(pos + 1)))) { + tokens.add(readNumber()); + } else if (Character.isLetter(c) || c == '_') { + tokens.add(readIdentifierOrKeyword()); + } else { + throw new IllegalArgumentException("Unexpected character '" + c + "' at position " + pos); + } } - private char peek() { - return pos + 1 < input.length() ? input.charAt(pos + 1) : '\0'; - } + tokens.add(new Token(TokenType.EOF, "", pos)); + return tokens; + } - private Token readString() { - int start = pos; - pos++; // skip opening quote - StringBuilder sb = new StringBuilder(); - while (pos < input.length() && input.charAt(pos) != '"') { - if (input.charAt(pos) == '\\' && pos + 1 < input.length()) { - pos++; - sb.append(input.charAt(pos)); - } else { - sb.append(input.charAt(pos)); - } - pos++; - } - if (pos >= input.length()) { - throw new IllegalArgumentException("Unterminated string at position " + start); - } - pos++; // skip closing quote - return new Token(TokenType.STRING, sb.toString(), start); + private char peek() { + return pos + 1 < input.length() ? input.charAt(pos + 1) : '\0'; + } + + private Token readString() { + int start = pos; + pos++; // skip opening quote + StringBuilder sb = new StringBuilder(); + while (pos < input.length() && input.charAt(pos) != '"') { + if (input.charAt(pos) == '\\' && pos + 1 < input.length()) { + pos++; + sb.append(input.charAt(pos)); + } else { + sb.append(input.charAt(pos)); + } + pos++; + } + if (pos >= input.length()) { + throw new IllegalArgumentException("Unterminated string at position " + start); } + pos++; // skip closing quote + return new Token(TokenType.STRING, sb.toString(), start); + } - private Token readNumber() { - int start = pos; - if (input.charAt(pos) == '-') { - pos++; - } - while (pos < input.length() && Character.isDigit(input.charAt(pos))) { - pos++; - } - return new Token(TokenType.NUMBER, input.substring(start, pos), start); + private Token readNumber() { + int start = pos; + if (input.charAt(pos) == '-') { + pos++; + } + while (pos < input.length() && Character.isDigit(input.charAt(pos))) { + pos++; } + return new Token(TokenType.NUMBER, input.substring(start, pos), start); + } - private Token readIdentifierOrKeyword() { - int start = pos; - while (pos < input.length() && (Character.isLetterOrDigit(input.charAt(pos)) || input.charAt(pos) == '_')) { - pos++; - } - String word = input.substring(start, pos); - TokenType type = KEYWORDS.getOrDefault(word, TokenType.IDENTIFIER); - return new Token(type, word, start); + private Token readIdentifierOrKeyword() { + int start = pos; + while (pos < input.length() + && (Character.isLetterOrDigit(input.charAt(pos)) || input.charAt(pos) == '_')) { + pos++; } + String word = input.substring(start, pos); + TokenType type = KEYWORDS.getOrDefault(word, TokenType.IDENTIFIER); + return new Token(type, word, start); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/Token.java b/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/Token.java index 782e6136..f67f6ffc 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/Token.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/Token.java @@ -1,8 +1,8 @@ package com.muchq.one_d4.chessql.lexer; public record Token(TokenType type, String value, int position) { - @Override - public String toString() { - return "Token(" + type + ", " + value + ", pos=" + position + ")"; - } + @Override + public String toString() { + return "Token(" + type + ", " + value + ", pos=" + position + ")"; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/TokenType.java b/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/TokenType.java index 551d7d62..464323c2 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/TokenType.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/lexer/TokenType.java @@ -1,34 +1,34 @@ package com.muchq.one_d4.chessql.lexer; public enum TokenType { - // Literals - NUMBER, - STRING, - IDENTIFIER, + // Literals + NUMBER, + STRING, + IDENTIFIER, - // Operators - EQ, // = - NEQ, // != - LT, // < - LTE, // <= - GT, // > - GTE, // >= + // Operators + EQ, // = + NEQ, // != + LT, // < + LTE, // <= + GT, // > + GTE, // >= - // Keywords - AND, - OR, - NOT, - IN, - MOTIF, + // Keywords + AND, + OR, + NOT, + IN, + MOTIF, - // Delimiters - LPAREN, - RPAREN, - LBRACKET, - RBRACKET, - COMMA, - DOT, + // Delimiters + LPAREN, + RPAREN, + LBRACKET, + RBRACKET, + COMMA, + DOT, - // End - EOF + // End + EOF } diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/parser/ParseException.java b/jvm/src/main/java/com/muchq/one_d4/chessql/parser/ParseException.java index 67dcbad6..e7b51cd4 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/parser/ParseException.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/parser/ParseException.java @@ -1,14 +1,14 @@ package com.muchq.one_d4.chessql.parser; public class ParseException extends RuntimeException { - private final int position; + private final int position; - public ParseException(String message, int position) { - super(message + " at position " + position); - this.position = position; - } + public ParseException(String message, int position) { + super(message + " at position " + position); + this.position = position; + } - public int getPosition() { - return position; - } + public int getPosition() { + return position; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/chessql/parser/Parser.java b/jvm/src/main/java/com/muchq/one_d4/chessql/parser/Parser.java index 45713783..7fe56fe0 100644 --- a/jvm/src/main/java/com/muchq/one_d4/chessql/parser/Parser.java +++ b/jvm/src/main/java/com/muchq/one_d4/chessql/parser/Parser.java @@ -10,175 +10,192 @@ import com.muchq.one_d4.chessql.lexer.Lexer; import com.muchq.one_d4.chessql.lexer.Token; import com.muchq.one_d4.chessql.lexer.TokenType; - import java.util.ArrayList; import java.util.List; public class Parser { - private final List tokens; - private int pos; - - public Parser(List tokens) { - this.tokens = tokens; - this.pos = 0; - } - - public static Expr parse(String input) { - List tokens = new Lexer(input).tokenize(); - Parser parser = new Parser(tokens); - Expr expr = parser.parseExpr(); - parser.expect(TokenType.EOF); - return expr; - } - - public Expr parseExpr() { - return parseOr(); - } - - private Expr parseOr() { - Expr left = parseAnd(); - List operands = new ArrayList<>(); - operands.add(left); - - while (check(TokenType.OR)) { - advance(); - operands.add(parseAnd()); - } - - return operands.size() == 1 ? operands.get(0) : new OrExpr(operands); - } - - private Expr parseAnd() { - Expr left = parseNot(); - List operands = new ArrayList<>(); - operands.add(left); - - while (check(TokenType.AND)) { - advance(); - operands.add(parseNot()); - } - - return operands.size() == 1 ? operands.get(0) : new AndExpr(operands); - } - - private Expr parseNot() { - if (check(TokenType.NOT)) { - advance(); - return new NotExpr(parseNot()); - } - return parsePrimary(); - } - - private Expr parsePrimary() { - if (check(TokenType.LPAREN)) { - advance(); - Expr expr = parseExpr(); - expect(TokenType.RPAREN); - return expr; - } - - if (check(TokenType.MOTIF)) { - return parseMotif(); - } - - if (check(TokenType.IDENTIFIER)) { - return parseFieldExpr(); - } - - throw new ParseException("Unexpected token: " + current(), current().position()); - } - - private Expr parseMotif() { - advance(); // consume 'motif' - expect(TokenType.LPAREN); - Token name = expect(TokenType.IDENTIFIER); - expect(TokenType.RPAREN); - return new MotifExpr(name.value()); - } + private final List tokens; + private int pos; - private Expr parseFieldExpr() { - String field = parseFieldName(); - - if (check(TokenType.IN)) { - advance(); - return parseInValues(field); - } - - String op = parseCompOp(); - Object value = parseValue(); - return new ComparisonExpr(field, op, value); - } + public Parser(List tokens) { + this.tokens = tokens; + this.pos = 0; + } - private String parseFieldName() { - Token first = expect(TokenType.IDENTIFIER); - StringBuilder sb = new StringBuilder(first.value()); + public static Expr parse(String input) { + List tokens = new Lexer(input).tokenize(); + Parser parser = new Parser(tokens); + Expr expr = parser.parseExpr(); + parser.expect(TokenType.EOF); + return expr; + } - while (check(TokenType.DOT)) { - advance(); - Token next = expect(TokenType.IDENTIFIER); - sb.append('.').append(next.value()); - } + public Expr parseExpr() { + return parseOr(); + } - return sb.toString(); - } - - private String parseCompOp() { - Token t = current(); - return switch (t.type()) { - case EQ -> { advance(); yield "="; } - case NEQ -> { advance(); yield "!="; } - case LT -> { advance(); yield "<"; } - case LTE -> { advance(); yield "<="; } - case GT -> { advance(); yield ">"; } - case GTE -> { advance(); yield ">="; } - default -> throw new ParseException("Expected comparison operator, got: " + t, t.position()); - }; - } - - private Object parseValue() { - Token t = current(); - if (t.type() == TokenType.NUMBER) { - advance(); - return Integer.parseInt(t.value()); - } - if (t.type() == TokenType.STRING) { - advance(); - return t.value(); - } - throw new ParseException("Expected value, got: " + t, t.position()); - } - - private InExpr parseInValues(String field) { - expect(TokenType.LBRACKET); - List values = new ArrayList<>(); - values.add(parseValue()); - while (check(TokenType.COMMA)) { - advance(); - values.add(parseValue()); - } - expect(TokenType.RBRACKET); - return new InExpr(field, values); - } - - private Token current() { - return tokens.get(pos); - } - - private boolean check(TokenType type) { - return current().type() == type; - } - - private Token advance() { - Token t = tokens.get(pos); - pos++; - return t; - } - - private Token expect(TokenType type) { - Token t = current(); - if (t.type() != type) { - throw new ParseException("Expected " + type + ", got " + t.type(), t.position()); - } - return advance(); - } + private Expr parseOr() { + Expr left = parseAnd(); + List operands = new ArrayList<>(); + operands.add(left); + + while (check(TokenType.OR)) { + advance(); + operands.add(parseAnd()); + } + + return operands.size() == 1 ? operands.get(0) : new OrExpr(operands); + } + + private Expr parseAnd() { + Expr left = parseNot(); + List operands = new ArrayList<>(); + operands.add(left); + + while (check(TokenType.AND)) { + advance(); + operands.add(parseNot()); + } + + return operands.size() == 1 ? operands.get(0) : new AndExpr(operands); + } + + private Expr parseNot() { + if (check(TokenType.NOT)) { + advance(); + return new NotExpr(parseNot()); + } + return parsePrimary(); + } + + private Expr parsePrimary() { + if (check(TokenType.LPAREN)) { + advance(); + Expr expr = parseExpr(); + expect(TokenType.RPAREN); + return expr; + } + + if (check(TokenType.MOTIF)) { + return parseMotif(); + } + + if (check(TokenType.IDENTIFIER)) { + return parseFieldExpr(); + } + + throw new ParseException("Unexpected token: " + current(), current().position()); + } + + private Expr parseMotif() { + advance(); // consume 'motif' + expect(TokenType.LPAREN); + Token name = expect(TokenType.IDENTIFIER); + expect(TokenType.RPAREN); + return new MotifExpr(name.value()); + } + + private Expr parseFieldExpr() { + String field = parseFieldName(); + + if (check(TokenType.IN)) { + advance(); + return parseInValues(field); + } + + String op = parseCompOp(); + Object value = parseValue(); + return new ComparisonExpr(field, op, value); + } + + private String parseFieldName() { + Token first = expect(TokenType.IDENTIFIER); + StringBuilder sb = new StringBuilder(first.value()); + + while (check(TokenType.DOT)) { + advance(); + Token next = expect(TokenType.IDENTIFIER); + sb.append('.').append(next.value()); + } + + return sb.toString(); + } + + private String parseCompOp() { + Token t = current(); + return switch (t.type()) { + case EQ -> { + advance(); + yield "="; + } + case NEQ -> { + advance(); + yield "!="; + } + case LT -> { + advance(); + yield "<"; + } + case LTE -> { + advance(); + yield "<="; + } + case GT -> { + advance(); + yield ">"; + } + case GTE -> { + advance(); + yield ">="; + } + default -> throw new ParseException("Expected comparison operator, got: " + t, t.position()); + }; + } + + private Object parseValue() { + Token t = current(); + if (t.type() == TokenType.NUMBER) { + advance(); + return Integer.parseInt(t.value()); + } + if (t.type() == TokenType.STRING) { + advance(); + return t.value(); + } + throw new ParseException("Expected value, got: " + t, t.position()); + } + + private InExpr parseInValues(String field) { + expect(TokenType.LBRACKET); + List values = new ArrayList<>(); + values.add(parseValue()); + while (check(TokenType.COMMA)) { + advance(); + values.add(parseValue()); + } + expect(TokenType.RBRACKET); + return new InExpr(field, values); + } + + private Token current() { + return tokens.get(pos); + } + + private boolean check(TokenType type) { + return current().type() == type; + } + + private Token advance() { + Token t = tokens.get(pos); + pos++; + return t; + } + + private Token expect(TokenType type) { + Token t = current(); + if (t.type() != type) { + throw new ParseException("Expected " + type + ", got " + t.type(), t.position()); + } + return advance(); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/db/DataSourceFactory.java b/jvm/src/main/java/com/muchq/one_d4/db/DataSourceFactory.java index b70212d3..8a5ac64d 100644 --- a/jvm/src/main/java/com/muchq/one_d4/db/DataSourceFactory.java +++ b/jvm/src/main/java/com/muchq/one_d4/db/DataSourceFactory.java @@ -2,19 +2,18 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; - import javax.sql.DataSource; public class DataSourceFactory { - private DataSourceFactory() {} + private DataSourceFactory() {} - public static DataSource create(String jdbcUrl, String username, String password) { - HikariConfig config = new HikariConfig(); - config.setJdbcUrl(jdbcUrl); - config.setUsername(username); - config.setPassword(password); - config.setMaximumPoolSize(10); - config.setMinimumIdle(2); - return new HikariDataSource(config); - } + public static DataSource create(String jdbcUrl, String username, String password) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(jdbcUrl); + config.setUsername(username); + config.setPassword(password); + config.setMaximumPoolSize(10); + config.setMinimumIdle(2); + return new HikariDataSource(config); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/db/GameFeatureDao.java b/jvm/src/main/java/com/muchq/one_d4/db/GameFeatureDao.java index 7eadd4ea..a970c4d1 100644 --- a/jvm/src/main/java/com/muchq/one_d4/db/GameFeatureDao.java +++ b/jvm/src/main/java/com/muchq/one_d4/db/GameFeatureDao.java @@ -1,10 +1,6 @@ package com.muchq.one_d4.db; import com.muchq.one_d4.chessql.compiler.CompiledQuery; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -14,131 +10,136 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class GameFeatureDao implements GameFeatureStore { - private static final Logger LOG = LoggerFactory.getLogger(GameFeatureDao.class); + private static final Logger LOG = LoggerFactory.getLogger(GameFeatureDao.class); - private static final String H2_INSERT = """ - MERGE INTO game_features ( - request_id, game_url, platform, white_username, black_username, - white_elo, black_elo, time_class, eco, result, played_at, num_moves, - has_pin, has_cross_pin, has_fork, has_skewer, has_discovered_attack, - motifs_json, pgn - ) KEY (game_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """; + private static final String H2_INSERT = + """ + MERGE INTO game_features ( + request_id, game_url, platform, white_username, black_username, + white_elo, black_elo, time_class, eco, result, played_at, num_moves, + has_pin, has_cross_pin, has_fork, has_skewer, has_discovered_attack, + motifs_json, pgn + ) KEY (game_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; - private static final String PG_INSERT = """ - INSERT INTO game_features ( - request_id, game_url, platform, white_username, black_username, - white_elo, black_elo, time_class, eco, result, played_at, num_moves, - has_pin, has_cross_pin, has_fork, has_skewer, has_discovered_attack, - motifs_json, pgn - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?) - ON CONFLICT (game_url) DO NOTHING - """; + private static final String PG_INSERT = + """ + INSERT INTO game_features ( + request_id, game_url, platform, white_username, black_username, + white_elo, black_elo, time_class, eco, result, played_at, num_moves, + has_pin, has_cross_pin, has_fork, has_skewer, has_discovered_attack, + motifs_json, pgn + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?) + ON CONFLICT (game_url) DO NOTHING + """; - private final DataSource dataSource; - private final boolean useH2; + private final DataSource dataSource; + private final boolean useH2; - public GameFeatureDao(DataSource dataSource, boolean useH2) { - this.dataSource = dataSource; - this.useH2 = useH2; - } + public GameFeatureDao(DataSource dataSource, boolean useH2) { + this.dataSource = dataSource; + this.useH2 = useH2; + } - @Override - public void insert(GameFeature row) { - String sql = useH2 ? H2_INSERT : PG_INSERT; - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { - ps.setObject(1, row.requestId()); - ps.setString(2, row.gameUrl()); - ps.setString(3, row.platform()); - ps.setString(4, row.whiteUsername()); - ps.setString(5, row.blackUsername()); - setIntOrNull(ps, 6, row.whiteElo()); - setIntOrNull(ps, 7, row.blackElo()); - ps.setString(8, row.timeClass()); - ps.setString(9, row.eco()); - ps.setString(10, row.result()); - ps.setTimestamp(11, row.playedAt() != null ? Timestamp.from(row.playedAt()) : null); - setIntOrNull(ps, 12, row.numMoves()); - ps.setBoolean(13, row.hasPin()); - ps.setBoolean(14, row.hasCrossPin()); - ps.setBoolean(15, row.hasFork()); - ps.setBoolean(16, row.hasSkewer()); - ps.setBoolean(17, row.hasDiscoveredAttack()); - ps.setString(18, row.motifsJson()); - ps.setString(19, row.pgn()); - ps.executeUpdate(); - } catch (SQLException e) { - LOG.error("Failed to insert game feature for game_url={}", row.gameUrl(), e); - throw new RuntimeException("Failed to insert game feature", e); - } + @Override + public void insert(GameFeature row) { + String sql = useH2 ? H2_INSERT : PG_INSERT; + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, row.requestId()); + ps.setString(2, row.gameUrl()); + ps.setString(3, row.platform()); + ps.setString(4, row.whiteUsername()); + ps.setString(5, row.blackUsername()); + setIntOrNull(ps, 6, row.whiteElo()); + setIntOrNull(ps, 7, row.blackElo()); + ps.setString(8, row.timeClass()); + ps.setString(9, row.eco()); + ps.setString(10, row.result()); + ps.setTimestamp(11, row.playedAt() != null ? Timestamp.from(row.playedAt()) : null); + setIntOrNull(ps, 12, row.numMoves()); + ps.setBoolean(13, row.hasPin()); + ps.setBoolean(14, row.hasCrossPin()); + ps.setBoolean(15, row.hasFork()); + ps.setBoolean(16, row.hasSkewer()); + ps.setBoolean(17, row.hasDiscoveredAttack()); + ps.setString(18, row.motifsJson()); + ps.setString(19, row.pgn()); + ps.executeUpdate(); + } catch (SQLException e) { + LOG.error("Failed to insert game feature for game_url={}", row.gameUrl(), e); + throw new RuntimeException("Failed to insert game feature", e); } + } - @Override - public List query(Object compiledQuery, int limit, int offset) { - if (!(compiledQuery instanceof CompiledQuery cq)) { - throw new IllegalArgumentException("Expected CompiledQuery, got: " + compiledQuery.getClass()); - } - String sql = "SELECT * FROM game_features WHERE " + cq.sql() - + " LIMIT ? OFFSET ?"; - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { - int idx = 1; - for (Object param : cq.parameters()) { - ps.setObject(idx++, param); - } - ps.setInt(idx++, limit); - ps.setInt(idx, offset); + @Override + public List query(Object compiledQuery, int limit, int offset) { + if (!(compiledQuery instanceof CompiledQuery cq)) { + throw new IllegalArgumentException( + "Expected CompiledQuery, got: " + compiledQuery.getClass()); + } + String sql = "SELECT * FROM game_features WHERE " + cq.sql() + " LIMIT ? OFFSET ?"; + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + int idx = 1; + for (Object param : cq.parameters()) { + ps.setObject(idx++, param); + } + ps.setInt(idx++, limit); + ps.setInt(idx, offset); - try (ResultSet rs = ps.executeQuery()) { - List results = new ArrayList<>(); - while (rs.next()) { - results.add(mapRow(rs)); - } - return results; - } - } catch (SQLException e) { - throw new RuntimeException("Failed to query game features", e); + try (ResultSet rs = ps.executeQuery()) { + List results = new ArrayList<>(); + while (rs.next()) { + results.add(mapRow(rs)); } + return results; + } + } catch (SQLException e) { + throw new RuntimeException("Failed to query game features", e); } + } - private GameFeature mapRow(ResultSet rs) throws SQLException { - return new GameFeature( - UUID.fromString(rs.getString("id")), - UUID.fromString(rs.getString("request_id")), - rs.getString("game_url"), - rs.getString("platform"), - rs.getString("white_username"), - rs.getString("black_username"), - getIntOrNull(rs, "white_elo"), - getIntOrNull(rs, "black_elo"), - rs.getString("time_class"), - rs.getString("eco"), - rs.getString("result"), - rs.getTimestamp("played_at") != null ? rs.getTimestamp("played_at").toInstant() : null, - getIntOrNull(rs, "num_moves"), - rs.getBoolean("has_pin"), - rs.getBoolean("has_cross_pin"), - rs.getBoolean("has_fork"), - rs.getBoolean("has_skewer"), - rs.getBoolean("has_discovered_attack"), - rs.getString("motifs_json"), - rs.getString("pgn") - ); - } + private GameFeature mapRow(ResultSet rs) throws SQLException { + return new GameFeature( + UUID.fromString(rs.getString("id")), + UUID.fromString(rs.getString("request_id")), + rs.getString("game_url"), + rs.getString("platform"), + rs.getString("white_username"), + rs.getString("black_username"), + getIntOrNull(rs, "white_elo"), + getIntOrNull(rs, "black_elo"), + rs.getString("time_class"), + rs.getString("eco"), + rs.getString("result"), + rs.getTimestamp("played_at") != null ? rs.getTimestamp("played_at").toInstant() : null, + getIntOrNull(rs, "num_moves"), + rs.getBoolean("has_pin"), + rs.getBoolean("has_cross_pin"), + rs.getBoolean("has_fork"), + rs.getBoolean("has_skewer"), + rs.getBoolean("has_discovered_attack"), + rs.getString("motifs_json"), + rs.getString("pgn")); + } - private static void setIntOrNull(PreparedStatement ps, int idx, Integer value) throws SQLException { - if (value != null) { - ps.setInt(idx, value); - } else { - ps.setNull(idx, Types.INTEGER); - } + private static void setIntOrNull(PreparedStatement ps, int idx, Integer value) + throws SQLException { + if (value != null) { + ps.setInt(idx, value); + } else { + ps.setNull(idx, Types.INTEGER); } + } - private static Integer getIntOrNull(ResultSet rs, String column) throws SQLException { - int val = rs.getInt(column); - return rs.wasNull() ? null : val; - } + private static Integer getIntOrNull(ResultSet rs, String column) throws SQLException { + int val = rs.getInt(column); + return rs.wasNull() ? null : val; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/db/GameFeatureStore.java b/jvm/src/main/java/com/muchq/one_d4/db/GameFeatureStore.java index 207a0d22..1073472d 100644 --- a/jvm/src/main/java/com/muchq/one_d4/db/GameFeatureStore.java +++ b/jvm/src/main/java/com/muchq/one_d4/db/GameFeatureStore.java @@ -5,29 +5,29 @@ import java.util.UUID; public interface GameFeatureStore { - void insert(GameFeature feature); - List query(Object compiledQuery, int limit, int offset); + void insert(GameFeature feature); - record GameFeature( - UUID id, - UUID requestId, - String gameUrl, - String platform, - String whiteUsername, - String blackUsername, - Integer whiteElo, - Integer blackElo, - String timeClass, - String eco, - String result, - Instant playedAt, - Integer numMoves, - boolean hasPin, - boolean hasCrossPin, - boolean hasFork, - boolean hasSkewer, - boolean hasDiscoveredAttack, - String motifsJson, - String pgn - ) {} + List query(Object compiledQuery, int limit, int offset); + + record GameFeature( + UUID id, + UUID requestId, + String gameUrl, + String platform, + String whiteUsername, + String blackUsername, + Integer whiteElo, + Integer blackElo, + String timeClass, + String eco, + String result, + Instant playedAt, + Integer numMoves, + boolean hasPin, + boolean hasCrossPin, + boolean hasFork, + boolean hasSkewer, + boolean hasDiscoveredAttack, + String motifsJson, + String pgn) {} } diff --git a/jvm/src/main/java/com/muchq/one_d4/db/IndexingRequestDao.java b/jvm/src/main/java/com/muchq/one_d4/db/IndexingRequestDao.java index c474136c..79f3381e 100644 --- a/jvm/src/main/java/com/muchq/one_d4/db/IndexingRequestDao.java +++ b/jvm/src/main/java/com/muchq/one_d4/db/IndexingRequestDao.java @@ -1,94 +1,94 @@ package com.muchq.one_d4.db; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Optional; import java.util.UUID; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IndexingRequestDao implements IndexingRequestStore { - private static final Logger LOG = LoggerFactory.getLogger(IndexingRequestDao.class); + private static final Logger LOG = LoggerFactory.getLogger(IndexingRequestDao.class); - private final DataSource dataSource; + private final DataSource dataSource; - public IndexingRequestDao(DataSource dataSource) { - this.dataSource = dataSource; - } + public IndexingRequestDao(DataSource dataSource) { + this.dataSource = dataSource; + } - @Override - public UUID create(String player, String platform, String startMonth, String endMonth) { - UUID id = UUID.randomUUID(); - String sql = """ - INSERT INTO indexing_requests (id, player, platform, start_month, end_month) - VALUES (?, ?, ?, ?, ?) - """; - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { - ps.setObject(1, id); - ps.setString(2, player); - ps.setString(3, platform); - ps.setString(4, startMonth); - ps.setString(5, endMonth); - ps.executeUpdate(); - return id; - } catch (SQLException e) { - throw new RuntimeException("Failed to create indexing request", e); - } + @Override + public UUID create(String player, String platform, String startMonth, String endMonth) { + UUID id = UUID.randomUUID(); + String sql = + """ + INSERT INTO indexing_requests (id, player, platform, start_month, end_month) + VALUES (?, ?, ?, ?, ?) + """; + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, id); + ps.setString(2, player); + ps.setString(3, platform); + ps.setString(4, startMonth); + ps.setString(5, endMonth); + ps.executeUpdate(); + return id; + } catch (SQLException e) { + throw new RuntimeException("Failed to create indexing request", e); } + } - @Override - public Optional findById(UUID id) { - String sql = "SELECT * FROM indexing_requests WHERE id = ?"; - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { - ps.setObject(1, id); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - return Optional.of(mapRow(rs)); - } - return Optional.empty(); - } - } catch (SQLException e) { - throw new RuntimeException("Failed to find indexing request", e); + @Override + public Optional findById(UUID id) { + String sql = "SELECT * FROM indexing_requests WHERE id = ?"; + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, id); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return Optional.of(mapRow(rs)); } + return Optional.empty(); + } + } catch (SQLException e) { + throw new RuntimeException("Failed to find indexing request", e); } + } - @Override - public void updateStatus(UUID id, String status, String errorMessage, int gamesIndexed) { - String sql = """ - UPDATE indexing_requests - SET status = ?, error_message = ?, games_indexed = ?, updated_at = now() - WHERE id = ? - """; - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { - ps.setString(1, status); - ps.setString(2, errorMessage); - ps.setInt(3, gamesIndexed); - ps.setObject(4, id); - ps.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeException("Failed to update indexing request status", e); - } + @Override + public void updateStatus(UUID id, String status, String errorMessage, int gamesIndexed) { + String sql = + """ + UPDATE indexing_requests + SET status = ?, error_message = ?, games_indexed = ?, updated_at = now() + WHERE id = ? + """; + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, status); + ps.setString(2, errorMessage); + ps.setInt(3, gamesIndexed); + ps.setObject(4, id); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Failed to update indexing request status", e); } + } - private IndexingRequest mapRow(ResultSet rs) throws SQLException { - return new IndexingRequest( - UUID.fromString(rs.getString("id")), - rs.getString("player"), - rs.getString("platform"), - rs.getString("start_month"), - rs.getString("end_month"), - rs.getString("status"), - rs.getTimestamp("created_at").toInstant(), - rs.getTimestamp("updated_at").toInstant(), - rs.getString("error_message"), - rs.getInt("games_indexed") - ); - } + private IndexingRequest mapRow(ResultSet rs) throws SQLException { + return new IndexingRequest( + UUID.fromString(rs.getString("id")), + rs.getString("player"), + rs.getString("platform"), + rs.getString("start_month"), + rs.getString("end_month"), + rs.getString("status"), + rs.getTimestamp("created_at").toInstant(), + rs.getTimestamp("updated_at").toInstant(), + rs.getString("error_message"), + rs.getInt("games_indexed")); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/db/IndexingRequestStore.java b/jvm/src/main/java/com/muchq/one_d4/db/IndexingRequestStore.java index 40433ba7..0554f09c 100644 --- a/jvm/src/main/java/com/muchq/one_d4/db/IndexingRequestStore.java +++ b/jvm/src/main/java/com/muchq/one_d4/db/IndexingRequestStore.java @@ -5,20 +5,21 @@ import java.util.UUID; public interface IndexingRequestStore { - UUID create(String player, String platform, String startMonth, String endMonth); - Optional findById(UUID id); - void updateStatus(UUID id, String status, String errorMessage, int gamesIndexed); + UUID create(String player, String platform, String startMonth, String endMonth); - record IndexingRequest( - UUID id, - String player, - String platform, - String startMonth, - String endMonth, - String status, - Instant createdAt, - Instant updatedAt, - String errorMessage, - int gamesIndexed - ) {} + Optional findById(UUID id); + + void updateStatus(UUID id, String status, String errorMessage, int gamesIndexed); + + record IndexingRequest( + UUID id, + String player, + String platform, + String startMonth, + String endMonth, + String status, + Instant createdAt, + Instant updatedAt, + String errorMessage, + int gamesIndexed) {} } diff --git a/jvm/src/main/java/com/muchq/one_d4/db/Migration.java b/jvm/src/main/java/com/muchq/one_d4/db/Migration.java index 82897760..12b6ba9a 100644 --- a/jvm/src/main/java/com/muchq/one_d4/db/Migration.java +++ b/jvm/src/main/java/com/muchq/one_d4/db/Migration.java @@ -1,119 +1,122 @@ package com.muchq.one_d4.db; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Migration { - private static final Logger LOG = LoggerFactory.getLogger(Migration.class); + private static final Logger LOG = LoggerFactory.getLogger(Migration.class); - private static final String H2_INDEXING_REQUESTS = """ - CREATE TABLE IF NOT EXISTS indexing_requests ( - id UUID DEFAULT random_uuid() PRIMARY KEY, - player VARCHAR(255) NOT NULL, - platform VARCHAR(50) NOT NULL, - start_month VARCHAR(7) NOT NULL, - end_month VARCHAR(7) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - created_at TIMESTAMP NOT NULL DEFAULT current_timestamp(), - updated_at TIMESTAMP NOT NULL DEFAULT current_timestamp(), - error_message TEXT, - games_indexed INT DEFAULT 0 - ) - """; + private static final String H2_INDEXING_REQUESTS = + """ + CREATE TABLE IF NOT EXISTS indexing_requests ( + id UUID DEFAULT random_uuid() PRIMARY KEY, + player VARCHAR(255) NOT NULL, + platform VARCHAR(50) NOT NULL, + start_month VARCHAR(7) NOT NULL, + end_month VARCHAR(7) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp(), + updated_at TIMESTAMP NOT NULL DEFAULT current_timestamp(), + error_message TEXT, + games_indexed INT DEFAULT 0 + ) + """; - private static final String H2_GAME_FEATURES = """ - CREATE TABLE IF NOT EXISTS game_features ( - id UUID DEFAULT random_uuid() PRIMARY KEY, - request_id UUID NOT NULL REFERENCES indexing_requests(id), - game_url VARCHAR(1024) NOT NULL UNIQUE, - platform VARCHAR(50) NOT NULL, - white_username VARCHAR(255), - black_username VARCHAR(255), - white_elo INT, - black_elo INT, - time_class VARCHAR(50), - eco VARCHAR(10), - result VARCHAR(20), - played_at TIMESTAMP, - num_moves INT, - has_pin BOOLEAN DEFAULT FALSE, - has_cross_pin BOOLEAN DEFAULT FALSE, - has_fork BOOLEAN DEFAULT FALSE, - has_skewer BOOLEAN DEFAULT FALSE, - has_discovered_attack BOOLEAN DEFAULT FALSE, - motifs_json TEXT, - pgn TEXT - ) - """; + private static final String H2_GAME_FEATURES = + """ + CREATE TABLE IF NOT EXISTS game_features ( + id UUID DEFAULT random_uuid() PRIMARY KEY, + request_id UUID NOT NULL REFERENCES indexing_requests(id), + game_url VARCHAR(1024) NOT NULL UNIQUE, + platform VARCHAR(50) NOT NULL, + white_username VARCHAR(255), + black_username VARCHAR(255), + white_elo INT, + black_elo INT, + time_class VARCHAR(50), + eco VARCHAR(10), + result VARCHAR(20), + played_at TIMESTAMP, + num_moves INT, + has_pin BOOLEAN DEFAULT FALSE, + has_cross_pin BOOLEAN DEFAULT FALSE, + has_fork BOOLEAN DEFAULT FALSE, + has_skewer BOOLEAN DEFAULT FALSE, + has_discovered_attack BOOLEAN DEFAULT FALSE, + motifs_json TEXT, + pgn TEXT + ) + """; - private static final String PG_INDEXING_REQUESTS = """ - CREATE TABLE IF NOT EXISTS indexing_requests ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - player VARCHAR(255) NOT NULL, - platform VARCHAR(50) NOT NULL, - start_month VARCHAR(7) NOT NULL, - end_month VARCHAR(7) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_at TIMESTAMP NOT NULL DEFAULT now(), - error_message TEXT, - games_indexed INT DEFAULT 0 - ) - """; + private static final String PG_INDEXING_REQUESTS = + """ + CREATE TABLE IF NOT EXISTS indexing_requests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + player VARCHAR(255) NOT NULL, + platform VARCHAR(50) NOT NULL, + start_month VARCHAR(7) NOT NULL, + end_month VARCHAR(7) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + created_at TIMESTAMP NOT NULL DEFAULT now(), + updated_at TIMESTAMP NOT NULL DEFAULT now(), + error_message TEXT, + games_indexed INT DEFAULT 0 + ) + """; - private static final String PG_GAME_FEATURES = """ - CREATE TABLE IF NOT EXISTS game_features ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - request_id UUID NOT NULL REFERENCES indexing_requests(id), - game_url VARCHAR(1024) NOT NULL UNIQUE, - platform VARCHAR(50) NOT NULL, - white_username VARCHAR(255), - black_username VARCHAR(255), - white_elo INT, - black_elo INT, - time_class VARCHAR(50), - eco VARCHAR(10), - result VARCHAR(20), - played_at TIMESTAMP, - num_moves INT, - has_pin BOOLEAN DEFAULT FALSE, - has_cross_pin BOOLEAN DEFAULT FALSE, - has_fork BOOLEAN DEFAULT FALSE, - has_skewer BOOLEAN DEFAULT FALSE, - has_discovered_attack BOOLEAN DEFAULT FALSE, - motifs_json JSONB, - pgn TEXT - ) - """; + private static final String PG_GAME_FEATURES = + """ + CREATE TABLE IF NOT EXISTS game_features ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + request_id UUID NOT NULL REFERENCES indexing_requests(id), + game_url VARCHAR(1024) NOT NULL UNIQUE, + platform VARCHAR(50) NOT NULL, + white_username VARCHAR(255), + black_username VARCHAR(255), + white_elo INT, + black_elo INT, + time_class VARCHAR(50), + eco VARCHAR(10), + result VARCHAR(20), + played_at TIMESTAMP, + num_moves INT, + has_pin BOOLEAN DEFAULT FALSE, + has_cross_pin BOOLEAN DEFAULT FALSE, + has_fork BOOLEAN DEFAULT FALSE, + has_skewer BOOLEAN DEFAULT FALSE, + has_discovered_attack BOOLEAN DEFAULT FALSE, + motifs_json JSONB, + pgn TEXT + ) + """; - private final DataSource dataSource; - private final boolean useH2; + private final DataSource dataSource; + private final boolean useH2; - public Migration(DataSource dataSource, boolean useH2) { - this.dataSource = dataSource; - this.useH2 = useH2; - } + public Migration(DataSource dataSource, boolean useH2) { + this.dataSource = dataSource; + this.useH2 = useH2; + } - public void run() { - try (Connection conn = dataSource.getConnection(); - Statement stmt = conn.createStatement()) { + public void run() { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { - if (useH2) { - stmt.execute(H2_INDEXING_REQUESTS); - stmt.execute(H2_GAME_FEATURES); - } else { - stmt.execute(PG_INDEXING_REQUESTS); - stmt.execute(PG_GAME_FEATURES); - } + if (useH2) { + stmt.execute(H2_INDEXING_REQUESTS); + stmt.execute(H2_GAME_FEATURES); + } else { + stmt.execute(PG_INDEXING_REQUESTS); + stmt.execute(PG_GAME_FEATURES); + } - LOG.info("Database migration completed successfully (H2={})", useH2); - } catch (SQLException e) { - throw new RuntimeException("Failed to run database migration", e); - } + LOG.info("Database migration completed successfully (H2={})", useH2); + } catch (SQLException e) { + throw new RuntimeException("Failed to run database migration", e); } + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/engine/FeatureExtractor.java b/jvm/src/main/java/com/muchq/one_d4/engine/FeatureExtractor.java index 455855ac..f0ba8774 100644 --- a/jvm/src/main/java/com/muchq/one_d4/engine/FeatureExtractor.java +++ b/jvm/src/main/java/com/muchq/one_d4/engine/FeatureExtractor.java @@ -5,54 +5,54 @@ import com.muchq.one_d4.engine.model.ParsedGame; import com.muchq.one_d4.engine.model.PositionContext; import com.muchq.one_d4.motifs.MotifDetector; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.EnumMap; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class FeatureExtractor { - private static final Logger LOG = LoggerFactory.getLogger(FeatureExtractor.class); - - private final PgnParser pgnParser; - private final GameReplayer replayer; - private final List detectors; - - public FeatureExtractor(PgnParser pgnParser, GameReplayer replayer, List detectors) { - this.pgnParser = pgnParser; - this.replayer = replayer; - this.detectors = detectors; + private static final Logger LOG = LoggerFactory.getLogger(FeatureExtractor.class); + + private final PgnParser pgnParser; + private final GameReplayer replayer; + private final List detectors; + + public FeatureExtractor( + PgnParser pgnParser, GameReplayer replayer, List detectors) { + this.pgnParser = pgnParser; + this.replayer = replayer; + this.detectors = detectors; + } + + public GameFeatures extract(String pgn) { + ParsedGame parsed = pgnParser.parse(pgn); + List positions; + try { + positions = replayer.replay(parsed.moveText()); + } catch (Exception e) { + LOG.warn("Failed to replay game, skipping motif detection", e); + return new GameFeatures(EnumSet.noneOf(Motif.class), 0, Map.of()); } - public GameFeatures extract(String pgn) { - ParsedGame parsed = pgnParser.parse(pgn); - List positions; - try { - positions = replayer.replay(parsed.moveText()); - } catch (Exception e) { - LOG.warn("Failed to replay game, skipping motif detection", e); - return new GameFeatures(EnumSet.noneOf(Motif.class), 0, Map.of()); - } + int numMoves = positions.isEmpty() ? 0 : positions.get(positions.size() - 1).moveNumber(); + Set foundMotifs = EnumSet.noneOf(Motif.class); + Map> allOccurrences = new EnumMap<>(Motif.class); - int numMoves = positions.isEmpty() ? 0 : positions.get(positions.size() - 1).moveNumber(); - Set foundMotifs = EnumSet.noneOf(Motif.class); - Map> allOccurrences = new EnumMap<>(Motif.class); - - for (MotifDetector detector : detectors) { - try { - List occurrences = detector.detect(positions); - if (!occurrences.isEmpty()) { - foundMotifs.add(detector.motif()); - allOccurrences.put(detector.motif(), occurrences); - } - } catch (Exception e) { - LOG.warn("Motif detector {} failed", detector.motif(), e); - } + for (MotifDetector detector : detectors) { + try { + List occurrences = detector.detect(positions); + if (!occurrences.isEmpty()) { + foundMotifs.add(detector.motif()); + allOccurrences.put(detector.motif(), occurrences); } - - return new GameFeatures(foundMotifs, numMoves, allOccurrences); + } catch (Exception e) { + LOG.warn("Motif detector {} failed", detector.motif(), e); + } } + + return new GameFeatures(foundMotifs, numMoves, allOccurrences); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/engine/GameReplayer.java b/jvm/src/main/java/com/muchq/one_d4/engine/GameReplayer.java index 12b78bd5..0a9e4aba 100644 --- a/jvm/src/main/java/com/muchq/one_d4/engine/GameReplayer.java +++ b/jvm/src/main/java/com/muchq/one_d4/engine/GameReplayer.java @@ -2,54 +2,55 @@ import chariot.chess.Board; import com.muchq.one_d4.engine.model.PositionContext; - import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class GameReplayer { - private static final Pattern MOVE_PATTERN = Pattern.compile( - "(?:\\d+\\.\\s*)?([KQRBNP]?[a-h]?[1-8]?x?[a-h][1-8](?:=[QRBN])?[+#]?|O-O-O|O-O)" - ); - - public List replay(String moveText) { - List positions = new ArrayList<>(); - Board board = Board.ofStandard(); - - positions.add(new PositionContext(0, board.toFEN(), true)); - - List moves = extractMoves(moveText); - int moveNumber = 1; - boolean whiteToMove = true; - - for (String move : moves) { - board = board.play(move); - if (!whiteToMove) { - moveNumber++; - } - whiteToMove = !whiteToMove; - positions.add(new PositionContext(moveNumber, board.toFEN(), whiteToMove)); - } - - return positions; + private static final Pattern MOVE_PATTERN = + Pattern.compile( + "(?:\\d+\\.\\s*)?([KQRBNP]?[a-h]?[1-8]?x?[a-h][1-8](?:=[QRBN])?[+#]?|O-O-O|O-O)"); + + public List replay(String moveText) { + List positions = new ArrayList<>(); + Board board = Board.ofStandard(); + + positions.add(new PositionContext(0, board.toFEN(), true)); + + List moves = extractMoves(moveText); + int moveNumber = 1; + boolean whiteToMove = true; + + for (String move : moves) { + board = board.play(move); + if (!whiteToMove) { + moveNumber++; + } + whiteToMove = !whiteToMove; + positions.add(new PositionContext(moveNumber, board.toFEN(), whiteToMove)); } - private List extractMoves(String moveText) { - List moves = new ArrayList<>(); - // Remove comments and variations - String cleaned = moveText.replaceAll("\\{[^}]*}", "") - .replaceAll("\\([^)]*\\)", "") - .replaceAll("\\$\\d+", ""); - - Matcher m = MOVE_PATTERN.matcher(cleaned); - while (m.find()) { - String move = m.group(1); - // Skip result indicators - if (!move.equals("1-0") && !move.equals("0-1") && !move.equals("1/2-1/2")) { - moves.add(move); - } - } - return moves; + return positions; + } + + private List extractMoves(String moveText) { + List moves = new ArrayList<>(); + // Remove comments and variations + String cleaned = + moveText + .replaceAll("\\{[^}]*}", "") + .replaceAll("\\([^)]*\\)", "") + .replaceAll("\\$\\d+", ""); + + Matcher m = MOVE_PATTERN.matcher(cleaned); + while (m.find()) { + String move = m.group(1); + // Skip result indicators + if (!move.equals("1-0") && !move.equals("0-1") && !move.equals("1/2-1/2")) { + moves.add(move); + } } + return moves; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/engine/PgnParser.java b/jvm/src/main/java/com/muchq/one_d4/engine/PgnParser.java index 2955cf06..23a4427d 100644 --- a/jvm/src/main/java/com/muchq/one_d4/engine/PgnParser.java +++ b/jvm/src/main/java/com/muchq/one_d4/engine/PgnParser.java @@ -1,45 +1,45 @@ package com.muchq.one_d4.engine; import com.muchq.one_d4.engine.model.ParsedGame; - import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class PgnParser { - private static final Pattern HEADER_PATTERN = Pattern.compile("\\[\\s*(\\w+)\\s+\"([^\"]*)\"\\s*]"); - - public ParsedGame parse(String pgn) { - Map headers = new LinkedHashMap<>(); - StringBuilder moveText = new StringBuilder(); - boolean inMoves = false; - - for (String line : pgn.split("\\r?\\n")) { - String trimmed = line.trim(); - if (trimmed.isEmpty()) { - if (!headers.isEmpty()) { - inMoves = true; - } - continue; - } - - if (!inMoves) { - Matcher m = HEADER_PATTERN.matcher(trimmed); - if (m.matches()) { - headers.put(m.group(1), m.group(2)); - continue; - } - } - - // If we've seen headers and this line doesn't match a header, it's movetext - inMoves = true; - if (!moveText.isEmpty()) { - moveText.append(' '); - } - moveText.append(trimmed); + private static final Pattern HEADER_PATTERN = + Pattern.compile("\\[\\s*(\\w+)\\s+\"([^\"]*)\"\\s*]"); + + public ParsedGame parse(String pgn) { + Map headers = new LinkedHashMap<>(); + StringBuilder moveText = new StringBuilder(); + boolean inMoves = false; + + for (String line : pgn.split("\\r?\\n")) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + if (!headers.isEmpty()) { + inMoves = true; } - - return new ParsedGame(headers, moveText.toString()); + continue; + } + + if (!inMoves) { + Matcher m = HEADER_PATTERN.matcher(trimmed); + if (m.matches()) { + headers.put(m.group(1), m.group(2)); + continue; + } + } + + // If we've seen headers and this line doesn't match a header, it's movetext + inMoves = true; + if (!moveText.isEmpty()) { + moveText.append(' '); + } + moveText.append(trimmed); } + + return new ParsedGame(headers, moveText.toString()); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/engine/model/GameFeatures.java b/jvm/src/main/java/com/muchq/one_d4/engine/model/GameFeatures.java index ae446c25..77c0be26 100644 --- a/jvm/src/main/java/com/muchq/one_d4/engine/model/GameFeatures.java +++ b/jvm/src/main/java/com/muchq/one_d4/engine/model/GameFeatures.java @@ -5,13 +5,10 @@ import java.util.Set; public record GameFeatures( - Set motifs, - int numMoves, - Map> occurrences -) { - public boolean hasMotif(Motif motif) { - return motifs.contains(motif); - } + Set motifs, int numMoves, Map> occurrences) { + public boolean hasMotif(Motif motif) { + return motifs.contains(motif); + } - public record MotifOccurrence(int moveNumber, String description) {} + public record MotifOccurrence(int moveNumber, String description) {} } diff --git a/jvm/src/main/java/com/muchq/one_d4/engine/model/Motif.java b/jvm/src/main/java/com/muchq/one_d4/engine/model/Motif.java index aafece56..bdfb60e6 100644 --- a/jvm/src/main/java/com/muchq/one_d4/engine/model/Motif.java +++ b/jvm/src/main/java/com/muchq/one_d4/engine/model/Motif.java @@ -1,9 +1,9 @@ package com.muchq.one_d4.engine.model; public enum Motif { - PIN, - CROSS_PIN, - FORK, - SKEWER, - DISCOVERED_ATTACK + PIN, + CROSS_PIN, + FORK, + SKEWER, + DISCOVERED_ATTACK } diff --git a/jvm/src/main/java/com/muchq/one_d4/engine/model/ParsedGame.java b/jvm/src/main/java/com/muchq/one_d4/engine/model/ParsedGame.java index 93030b44..db770a15 100644 --- a/jvm/src/main/java/com/muchq/one_d4/engine/model/ParsedGame.java +++ b/jvm/src/main/java/com/muchq/one_d4/engine/model/ParsedGame.java @@ -2,5 +2,4 @@ import java.util.Map; -public record ParsedGame(Map headers, String moveText) { -} +public record ParsedGame(Map headers, String moveText) {} diff --git a/jvm/src/main/java/com/muchq/one_d4/engine/model/PositionContext.java b/jvm/src/main/java/com/muchq/one_d4/engine/model/PositionContext.java index d3d02637..49b12ca9 100644 --- a/jvm/src/main/java/com/muchq/one_d4/engine/model/PositionContext.java +++ b/jvm/src/main/java/com/muchq/one_d4/engine/model/PositionContext.java @@ -1,4 +1,3 @@ package com.muchq.one_d4.engine.model; -public record PositionContext(int moveNumber, String fen, boolean whiteToMove) { -} +public record PositionContext(int moveNumber, String fen, boolean whiteToMove) {} diff --git a/jvm/src/main/java/com/muchq/one_d4/motifs/CrossPinDetector.java b/jvm/src/main/java/com/muchq/one_d4/motifs/CrossPinDetector.java index d01f183d..8429e940 100644 --- a/jvm/src/main/java/com/muchq/one_d4/motifs/CrossPinDetector.java +++ b/jvm/src/main/java/com/muchq/one_d4/motifs/CrossPinDetector.java @@ -3,104 +3,104 @@ import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.engine.model.PositionContext; - import java.util.ArrayList; import java.util.List; public class CrossPinDetector implements MotifDetector { - @Override - public Motif motif() { - return Motif.CROSS_PIN; + @Override + public Motif motif() { + return Motif.CROSS_PIN; + } + + @Override + public List detect(List positions) { + List occurrences = new ArrayList<>(); + + for (PositionContext ctx : positions) { + String placement = ctx.fen().split(" ")[0]; + int[][] board = PinDetector.parsePlacement(placement); + + // A cross-pin occurs when a piece is pinned along two different directions + // simultaneously (e.g., pinned by a rook on a file AND a bishop on a diagonal). + if (hasCrossPin(board, ctx.whiteToMove())) { + occurrences.add( + new GameFeatures.MotifOccurrence( + ctx.moveNumber(), "Cross-pin detected at move " + ctx.moveNumber())); + } } - @Override - public List detect(List positions) { - List occurrences = new ArrayList<>(); + return occurrences; + } - for (PositionContext ctx : positions) { - String placement = ctx.fen().split(" ")[0]; - int[][] board = PinDetector.parsePlacement(placement); + private boolean hasCrossPin(int[][] board, boolean whiteToMove) { + int kingPiece = whiteToMove ? 6 : -6; + int kingRow = -1, kingCol = -1; - // A cross-pin occurs when a piece is pinned along two different directions - // simultaneously (e.g., pinned by a rook on a file AND a bishop on a diagonal). - if (hasCrossPin(board, ctx.whiteToMove())) { - occurrences.add(new GameFeatures.MotifOccurrence( - ctx.moveNumber(), "Cross-pin detected at move " + ctx.moveNumber())); - } + for (int r = 0; r < 8; r++) { + for (int c = 0; c < 8; c++) { + if (board[r][c] == kingPiece) { + kingRow = r; + kingCol = c; } - - return occurrences; + } } + if (kingRow == -1) return false; - private boolean hasCrossPin(int[][] board, boolean whiteToMove) { - int kingPiece = whiteToMove ? 6 : -6; - int kingRow = -1, kingCol = -1; - - for (int r = 0; r < 8; r++) { - for (int c = 0; c < 8; c++) { - if (board[r][c] == kingPiece) { - kingRow = r; - kingCol = c; - } - } - } - if (kingRow == -1) return false; - - // Find all pinned pieces and check if any piece is pinned along two axes - int[][] directions = {{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}}; - List pinnedSquares = new ArrayList<>(); + // Find all pinned pieces and check if any piece is pinned along two axes + int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}; + List pinnedSquares = new ArrayList<>(); - for (int[] dir : directions) { - int[] pinned = findPinnedPiece(board, kingRow, kingCol, dir[0], dir[1], whiteToMove); - if (pinned != null) { - pinnedSquares.add(pinned); - } - } - - // Check for duplicate pinned squares (same piece pinned from two directions) - for (int i = 0; i < pinnedSquares.size(); i++) { - for (int j = i + 1; j < pinnedSquares.size(); j++) { - if (pinnedSquares.get(i)[0] == pinnedSquares.get(j)[0] - && pinnedSquares.get(i)[1] == pinnedSquares.get(j)[1]) { - return true; - } - } - } - return false; + for (int[] dir : directions) { + int[] pinned = findPinnedPiece(board, kingRow, kingCol, dir[0], dir[1], whiteToMove); + if (pinned != null) { + pinnedSquares.add(pinned); + } } - private int[] findPinnedPiece(int[][] board, int kr, int kc, int dr, int dc, boolean whiteKing) { - int r = kr + dr, c = kc + dc; - int[] friendlyPos = null; - - while (r >= 0 && r < 8 && c >= 0 && c < 8) { - int piece = board[r][c]; - if (piece != 0) { - boolean isWhitePiece = piece > 0; - if (isWhitePiece == whiteKing) { - if (friendlyPos != null) return null; - friendlyPos = new int[]{r, c}; - } else { - if (friendlyPos != null && isSlidingAttacker(piece, dr, dc)) { - return friendlyPos; - } - return null; - } - } - r += dr; - c += dc; + // Check for duplicate pinned squares (same piece pinned from two directions) + for (int i = 0; i < pinnedSquares.size(); i++) { + for (int j = i + 1; j < pinnedSquares.size(); j++) { + if (pinnedSquares.get(i)[0] == pinnedSquares.get(j)[0] + && pinnedSquares.get(i)[1] == pinnedSquares.get(j)[1]) { + return true; } - return null; + } } - - private boolean isSlidingAttacker(int piece, int dr, int dc) { - int absPiece = Math.abs(piece); - boolean isDiagonal = dr != 0 && dc != 0; - boolean isStraight = dr == 0 || dc == 0; - if (absPiece == 5) return true; - if (absPiece == 3 && isDiagonal) return true; - if (absPiece == 4 && isStraight) return true; - return false; + return false; + } + + private int[] findPinnedPiece(int[][] board, int kr, int kc, int dr, int dc, boolean whiteKing) { + int r = kr + dr, c = kc + dc; + int[] friendlyPos = null; + + while (r >= 0 && r < 8 && c >= 0 && c < 8) { + int piece = board[r][c]; + if (piece != 0) { + boolean isWhitePiece = piece > 0; + if (isWhitePiece == whiteKing) { + if (friendlyPos != null) return null; + friendlyPos = new int[] {r, c}; + } else { + if (friendlyPos != null && isSlidingAttacker(piece, dr, dc)) { + return friendlyPos; + } + return null; + } + } + r += dr; + c += dc; } + return null; + } + + private boolean isSlidingAttacker(int piece, int dr, int dc) { + int absPiece = Math.abs(piece); + boolean isDiagonal = dr != 0 && dc != 0; + boolean isStraight = dr == 0 || dc == 0; + if (absPiece == 5) return true; + if (absPiece == 3 && isDiagonal) return true; + if (absPiece == 4 && isStraight) return true; + return false; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/motifs/DiscoveredAttackDetector.java b/jvm/src/main/java/com/muchq/one_d4/motifs/DiscoveredAttackDetector.java index 4d0c0273..134c8e2d 100644 --- a/jvm/src/main/java/com/muchq/one_d4/motifs/DiscoveredAttackDetector.java +++ b/jvm/src/main/java/com/muchq/one_d4/motifs/DiscoveredAttackDetector.java @@ -3,109 +3,109 @@ import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.engine.model.PositionContext; - import java.util.ArrayList; import java.util.List; public class DiscoveredAttackDetector implements MotifDetector { - @Override - public Motif motif() { - return Motif.DISCOVERED_ATTACK; - } - - @Override - public List detect(List positions) { - List occurrences = new ArrayList<>(); + @Override + public Motif motif() { + return Motif.DISCOVERED_ATTACK; + } - // Compare consecutive positions to detect discovered attacks. - // A discovered attack occurs when a piece moves and reveals an attack - // from a sliding piece behind it. - for (int i = 1; i < positions.size(); i++) { - PositionContext before = positions.get(i - 1); - PositionContext after = positions.get(i); + @Override + public List detect(List positions) { + List occurrences = new ArrayList<>(); - String beforePlacement = before.fen().split(" ")[0]; - String afterPlacement = after.fen().split(" ")[0]; - int[][] boardBefore = PinDetector.parsePlacement(beforePlacement); - int[][] boardAfter = PinDetector.parsePlacement(afterPlacement); + // Compare consecutive positions to detect discovered attacks. + // A discovered attack occurs when a piece moves and reveals an attack + // from a sliding piece behind it. + for (int i = 1; i < positions.size(); i++) { + PositionContext before = positions.get(i - 1); + PositionContext after = positions.get(i); - // The side that just moved is the opposite of whose turn it now is - boolean moverIsWhite = after.whiteToMove() ? false : true; + String beforePlacement = before.fen().split(" ")[0]; + String afterPlacement = after.fen().split(" ")[0]; + int[][] boardBefore = PinDetector.parsePlacement(beforePlacement); + int[][] boardAfter = PinDetector.parsePlacement(afterPlacement); - if (hasDiscoveredAttack(boardBefore, boardAfter, moverIsWhite)) { - occurrences.add(new GameFeatures.MotifOccurrence( - after.moveNumber(), "Discovered attack at move " + after.moveNumber())); - } - } + // The side that just moved is the opposite of whose turn it now is + boolean moverIsWhite = after.whiteToMove() ? false : true; - return occurrences; + if (hasDiscoveredAttack(boardBefore, boardAfter, moverIsWhite)) { + occurrences.add( + new GameFeatures.MotifOccurrence( + after.moveNumber(), "Discovered attack at move " + after.moveNumber())); + } } - private boolean hasDiscoveredAttack(int[][] before, int[][] after, boolean moverIsWhite) { - // Find the piece that moved (square that became empty) - for (int r = 0; r < 8; r++) { - for (int c = 0; c < 8; c++) { - int pieceBefore = before[r][c]; - int pieceAfter = after[r][c]; + return occurrences; + } - if (pieceBefore != 0 && pieceAfter == 0) { - boolean isWhite = pieceBefore > 0; - if (isWhite == moverIsWhite) { - // This square was vacated by the moving piece. - // Check if any sliding piece behind it now has a new attack line. - if (revealsAttack(after, r, c, moverIsWhite)) { - return true; - } - } - } + private boolean hasDiscoveredAttack(int[][] before, int[][] after, boolean moverIsWhite) { + // Find the piece that moved (square that became empty) + for (int r = 0; r < 8; r++) { + for (int c = 0; c < 8; c++) { + int pieceBefore = before[r][c]; + int pieceAfter = after[r][c]; + + if (pieceBefore != 0 && pieceAfter == 0) { + boolean isWhite = pieceBefore > 0; + if (isWhite == moverIsWhite) { + // This square was vacated by the moving piece. + // Check if any sliding piece behind it now has a new attack line. + if (revealsAttack(after, r, c, moverIsWhite)) { + return true; } + } } - return false; + } } + return false; + } - private boolean revealsAttack(int[][] board, int vacatedR, int vacatedC, boolean moverIsWhite) { - int[][] directions = {{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}}; + private boolean revealsAttack(int[][] board, int vacatedR, int vacatedC, boolean moverIsWhite) { + int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}; - for (int[] dir : directions) { - // Look behind the vacated square for a friendly sliding piece - int br = vacatedR - dir[0], bc = vacatedC - dir[1]; - while (br >= 0 && br < 8 && bc >= 0 && bc < 8) { - int piece = board[br][bc]; - if (piece != 0) { - boolean isWhite = piece > 0; - if (isWhite == moverIsWhite && isSlidingAttacker(piece, dir)) { - // Check if there's an enemy piece along the forward direction - int fr = vacatedR + dir[0], fc = vacatedC + dir[1]; - while (fr >= 0 && fr < 8 && fc >= 0 && fc < 8) { - int target = board[fr][fc]; - if (target != 0) { - boolean targetIsWhite = target > 0; - if (targetIsWhite != moverIsWhite && Math.abs(target) >= 2) { - return true; - } - break; - } - fr += dir[0]; - fc += dir[1]; - } - } - break; + for (int[] dir : directions) { + // Look behind the vacated square for a friendly sliding piece + int br = vacatedR - dir[0], bc = vacatedC - dir[1]; + while (br >= 0 && br < 8 && bc >= 0 && bc < 8) { + int piece = board[br][bc]; + if (piece != 0) { + boolean isWhite = piece > 0; + if (isWhite == moverIsWhite && isSlidingAttacker(piece, dir)) { + // Check if there's an enemy piece along the forward direction + int fr = vacatedR + dir[0], fc = vacatedC + dir[1]; + while (fr >= 0 && fr < 8 && fc >= 0 && fc < 8) { + int target = board[fr][fc]; + if (target != 0) { + boolean targetIsWhite = target > 0; + if (targetIsWhite != moverIsWhite && Math.abs(target) >= 2) { + return true; } - br -= dir[0]; - bc -= dir[1]; + break; + } + fr += dir[0]; + fc += dir[1]; } + } + break; } - return false; + br -= dir[0]; + bc -= dir[1]; + } } + return false; + } - private boolean isSlidingAttacker(int piece, int[] dir) { - int absPiece = Math.abs(piece); - boolean isDiagonal = dir[0] != 0 && dir[1] != 0; - boolean isStraight = dir[0] == 0 || dir[1] == 0; - if (absPiece == 5) return true; - if (absPiece == 3 && isDiagonal) return true; - if (absPiece == 4 && isStraight) return true; - return false; - } + private boolean isSlidingAttacker(int piece, int[] dir) { + int absPiece = Math.abs(piece); + boolean isDiagonal = dir[0] != 0 && dir[1] != 0; + boolean isStraight = dir[0] == 0 || dir[1] == 0; + if (absPiece == 5) return true; + if (absPiece == 3 && isDiagonal) return true; + if (absPiece == 4 && isStraight) return true; + return false; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/motifs/ForkDetector.java b/jvm/src/main/java/com/muchq/one_d4/motifs/ForkDetector.java index 777a8426..35dc58aa 100644 --- a/jvm/src/main/java/com/muchq/one_d4/motifs/ForkDetector.java +++ b/jvm/src/main/java/com/muchq/one_d4/motifs/ForkDetector.java @@ -3,116 +3,127 @@ import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.engine.model.PositionContext; - import java.util.ArrayList; import java.util.List; public class ForkDetector implements MotifDetector { - @Override - public Motif motif() { - return Motif.FORK; + @Override + public Motif motif() { + return Motif.FORK; + } + + @Override + public List detect(List positions) { + List occurrences = new ArrayList<>(); + + for (PositionContext ctx : positions) { + String placement = ctx.fen().split(" ")[0]; + int[][] board = PinDetector.parsePlacement(placement); + + // Check if any piece attacks two or more enemy pieces of significant value + if (hasFork(board, !ctx.whiteToMove())) { + occurrences.add( + new GameFeatures.MotifOccurrence( + ctx.moveNumber(), "Fork detected at move " + ctx.moveNumber())); + } } - @Override - public List detect(List positions) { - List occurrences = new ArrayList<>(); - - for (PositionContext ctx : positions) { - String placement = ctx.fen().split(" ")[0]; - int[][] board = PinDetector.parsePlacement(placement); - - // Check if any piece attacks two or more enemy pieces of significant value - if (hasFork(board, !ctx.whiteToMove())) { - occurrences.add(new GameFeatures.MotifOccurrence( - ctx.moveNumber(), "Fork detected at move " + ctx.moveNumber())); + return occurrences; + } + + private boolean hasFork(int[][] board, boolean attackerIsWhite) { + for (int r = 0; r < 8; r++) { + for (int c = 0; c < 8; c++) { + int piece = board[r][c]; + if (piece == 0) continue; + boolean isWhite = piece > 0; + if (isWhite != attackerIsWhite) continue; + + int absPiece = Math.abs(piece); + List attacked = getAttackedSquares(board, r, c, absPiece, attackerIsWhite); + + // Count how many valuable enemy pieces are attacked + int valuableTargets = 0; + for (int[] sq : attacked) { + int target = board[sq[0]][sq[1]]; + if (target != 0 && (target > 0) != attackerIsWhite) { + int targetValue = Math.abs(target); + // Target must be at least a knight/bishop (value >= 2) + if (targetValue >= 2) { + valuableTargets++; } + } } - - return occurrences; + if (valuableTargets >= 2) return true; + } } - - private boolean hasFork(int[][] board, boolean attackerIsWhite) { - for (int r = 0; r < 8; r++) { - for (int c = 0; c < 8; c++) { - int piece = board[r][c]; - if (piece == 0) continue; - boolean isWhite = piece > 0; - if (isWhite != attackerIsWhite) continue; - - int absPiece = Math.abs(piece); - List attacked = getAttackedSquares(board, r, c, absPiece, attackerIsWhite); - - // Count how many valuable enemy pieces are attacked - int valuableTargets = 0; - for (int[] sq : attacked) { - int target = board[sq[0]][sq[1]]; - if (target != 0 && (target > 0) != attackerIsWhite) { - int targetValue = Math.abs(target); - // Target must be at least a knight/bishop (value >= 2) - if (targetValue >= 2) { - valuableTargets++; - } - } - } - if (valuableTargets >= 2) return true; - } - } - return false; + return false; + } + + private List getAttackedSquares( + int[][] board, int r, int c, int pieceType, boolean isWhite) { + List squares = new ArrayList<>(); + switch (pieceType) { + case 2 -> addKnightAttacks(r, c, squares); // Knight + case 1 -> addPawnAttacks(r, c, isWhite, squares); // Pawn + case 3 -> + addSlidingAttacks( + board, r, c, new int[][] {{1, 1}, {1, -1}, {-1, 1}, {-1, -1}}, squares); // Bishop + case 4 -> + addSlidingAttacks( + board, r, c, new int[][] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}, squares); // Rook + case 5 -> { // Queen + addSlidingAttacks( + board, + r, + c, + new int[][] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}, + squares); + } + case 6 -> addKingAttacks(r, c, squares); // King } - - private List getAttackedSquares(int[][] board, int r, int c, int pieceType, boolean isWhite) { - List squares = new ArrayList<>(); - switch (pieceType) { - case 2 -> addKnightAttacks(r, c, squares); // Knight - case 1 -> addPawnAttacks(r, c, isWhite, squares); // Pawn - case 3 -> addSlidingAttacks(board, r, c, new int[][]{{1,1},{1,-1},{-1,1},{-1,-1}}, squares); // Bishop - case 4 -> addSlidingAttacks(board, r, c, new int[][]{{0,1},{0,-1},{1,0},{-1,0}}, squares); // Rook - case 5 -> { // Queen - addSlidingAttacks(board, r, c, new int[][]{{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}}, squares); - } - case 6 -> addKingAttacks(r, c, squares); // King - } - return squares; + return squares; + } + + private void addKnightAttacks(int r, int c, List squares) { + int[][] offsets = {{-2, -1}, {-2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}, {2, -1}, {2, 1}}; + for (int[] off : offsets) { + int nr = r + off[0], nc = c + off[1]; + if (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) { + squares.add(new int[] {nr, nc}); + } } - - private void addKnightAttacks(int r, int c, List squares) { - int[][] offsets = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}}; - for (int[] off : offsets) { - int nr = r + off[0], nc = c + off[1]; - if (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) { - squares.add(new int[]{nr, nc}); - } + } + + private void addPawnAttacks(int r, int c, boolean isWhite, List squares) { + int dir = isWhite ? -1 : 1; + if (c > 0 && r + dir >= 0 && r + dir < 8) squares.add(new int[] {r + dir, c - 1}); + if (c < 7 && r + dir >= 0 && r + dir < 8) squares.add(new int[] {r + dir, c + 1}); + } + + private void addKingAttacks(int r, int c, List squares) { + for (int dr = -1; dr <= 1; dr++) { + for (int dc = -1; dc <= 1; dc++) { + if (dr == 0 && dc == 0) continue; + int nr = r + dr, nc = c + dc; + if (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) { + squares.add(new int[] {nr, nc}); } + } } - - private void addPawnAttacks(int r, int c, boolean isWhite, List squares) { - int dir = isWhite ? -1 : 1; - if (c > 0 && r + dir >= 0 && r + dir < 8) squares.add(new int[]{r + dir, c - 1}); - if (c < 7 && r + dir >= 0 && r + dir < 8) squares.add(new int[]{r + dir, c + 1}); - } - - private void addKingAttacks(int r, int c, List squares) { - for (int dr = -1; dr <= 1; dr++) { - for (int dc = -1; dc <= 1; dc++) { - if (dr == 0 && dc == 0) continue; - int nr = r + dr, nc = c + dc; - if (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) { - squares.add(new int[]{nr, nc}); - } - } - } - } - - private void addSlidingAttacks(int[][] board, int r, int c, int[][] directions, List squares) { - for (int[] dir : directions) { - int nr = r + dir[0], nc = c + dir[1]; - while (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) { - squares.add(new int[]{nr, nc}); - if (board[nr][nc] != 0) break; // blocked - nr += dir[0]; - nc += dir[1]; - } - } + } + + private void addSlidingAttacks( + int[][] board, int r, int c, int[][] directions, List squares) { + for (int[] dir : directions) { + int nr = r + dir[0], nc = c + dir[1]; + while (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) { + squares.add(new int[] {nr, nc}); + if (board[nr][nc] != 0) break; // blocked + nr += dir[0]; + nc += dir[1]; + } } + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/motifs/MotifDetector.java b/jvm/src/main/java/com/muchq/one_d4/motifs/MotifDetector.java index 907f22c7..0988b298 100644 --- a/jvm/src/main/java/com/muchq/one_d4/motifs/MotifDetector.java +++ b/jvm/src/main/java/com/muchq/one_d4/motifs/MotifDetector.java @@ -3,10 +3,10 @@ import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.engine.model.PositionContext; - import java.util.List; public interface MotifDetector { - Motif motif(); - List detect(List positions); + Motif motif(); + + List detect(List positions); } diff --git a/jvm/src/main/java/com/muchq/one_d4/motifs/PinDetector.java b/jvm/src/main/java/com/muchq/one_d4/motifs/PinDetector.java index 1ef55df1..43b29d76 100644 --- a/jvm/src/main/java/com/muchq/one_d4/motifs/PinDetector.java +++ b/jvm/src/main/java/com/muchq/one_d4/motifs/PinDetector.java @@ -3,138 +3,138 @@ import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.engine.model.PositionContext; - import java.util.ArrayList; import java.util.List; public class PinDetector implements MotifDetector { - @Override - public Motif motif() { - return Motif.PIN; + @Override + public Motif motif() { + return Motif.PIN; + } + + @Override + public List detect(List positions) { + List occurrences = new ArrayList<>(); + + for (PositionContext ctx : positions) { + String fen = ctx.fen(); + String placement = fen.split(" ")[0]; + if (detectPinFromFen(placement, ctx.whiteToMove())) { + occurrences.add( + new GameFeatures.MotifOccurrence( + ctx.moveNumber(), "Pin detected at move " + ctx.moveNumber())); + } } - @Override - public List detect(List positions) { - List occurrences = new ArrayList<>(); - - for (PositionContext ctx : positions) { - String fen = ctx.fen(); - String placement = fen.split(" ")[0]; - if (detectPinFromFen(placement, ctx.whiteToMove())) { - occurrences.add(new GameFeatures.MotifOccurrence( - ctx.moveNumber(), "Pin detected at move " + ctx.moveNumber())); - } + return occurrences; + } + + private boolean detectPinFromFen(String placement, boolean whiteToMove) { + // Find king position and check for pieces on diagonals/files/ranks + // between sliding attackers and the king. + // This is a heuristic approach - full implementation would use + // ray-casting from the king position. + int[][] boardArray = parsePlacement(placement); + int kingRow = -1, kingCol = -1; + int kingPiece = whiteToMove ? 6 : -6; // K=6, k=-6 + + for (int r = 0; r < 8; r++) { + for (int c = 0; c < 8; c++) { + if (boardArray[r][c] == kingPiece) { + kingRow = r; + kingCol = c; } - - return occurrences; + } } - private boolean detectPinFromFen(String placement, boolean whiteToMove) { - // Find king position and check for pieces on diagonals/files/ranks - // between sliding attackers and the king. - // This is a heuristic approach - full implementation would use - // ray-casting from the king position. - int[][] boardArray = parsePlacement(placement); - int kingRow = -1, kingCol = -1; - int kingPiece = whiteToMove ? 6 : -6; // K=6, k=-6 - - for (int r = 0; r < 8; r++) { - for (int c = 0; c < 8; c++) { - if (boardArray[r][c] == kingPiece) { - kingRow = r; - kingCol = c; - } - } - } - - if (kingRow == -1) return false; + if (kingRow == -1) return false; - // Check all 8 directions from the king for pins - int[][] directions = {{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}}; - for (int[] dir : directions) { - if (isPinAlongRay(boardArray, kingRow, kingCol, dir[0], dir[1], whiteToMove)) { - return true; - } - } - return false; + // Check all 8 directions from the king for pins + int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}; + for (int[] dir : directions) { + if (isPinAlongRay(boardArray, kingRow, kingCol, dir[0], dir[1], whiteToMove)) { + return true; + } } - - private boolean isPinAlongRay(int[][] board, int kr, int kc, int dr, int dc, boolean whiteKing) { - int friendlyPieceCount = 0; - int r = kr + dr, c = kc + dc; - boolean foundFriendly = false; - - while (r >= 0 && r < 8 && c >= 0 && c < 8) { - int piece = board[r][c]; - if (piece != 0) { - boolean isWhitePiece = piece > 0; - if (isWhitePiece == whiteKing) { - // Friendly piece - friendlyPieceCount++; - if (friendlyPieceCount > 1) return false; - foundFriendly = true; - } else { - // Enemy piece - check if it's a sliding attacker on this line - if (foundFriendly && isSlidingAttacker(piece, dr, dc)) { - return true; - } - return false; - } - } - r += dr; - c += dc; + return false; + } + + private boolean isPinAlongRay(int[][] board, int kr, int kc, int dr, int dc, boolean whiteKing) { + int friendlyPieceCount = 0; + int r = kr + dr, c = kc + dc; + boolean foundFriendly = false; + + while (r >= 0 && r < 8 && c >= 0 && c < 8) { + int piece = board[r][c]; + if (piece != 0) { + boolean isWhitePiece = piece > 0; + if (isWhitePiece == whiteKing) { + // Friendly piece + friendlyPieceCount++; + if (friendlyPieceCount > 1) return false; + foundFriendly = true; + } else { + // Enemy piece - check if it's a sliding attacker on this line + if (foundFriendly && isSlidingAttacker(piece, dr, dc)) { + return true; + } + return false; } - return false; + } + r += dr; + c += dc; } - - private boolean isSlidingAttacker(int piece, int dr, int dc) { - int absPiece = Math.abs(piece); - boolean isDiagonal = dr != 0 && dc != 0; - boolean isStraight = dr == 0 || dc == 0; - - // Queen (5) attacks on both diagonals and straights - if (absPiece == 5) return true; - // Bishop (3) attacks on diagonals - if (absPiece == 3 && isDiagonal) return true; - // Rook (4) attacks on straight lines - if (absPiece == 4 && isStraight) return true; - - return false; - } - - static int[][] parsePlacement(String placement) { - int[][] board = new int[8][8]; - String[] ranks = placement.split("/"); - for (int r = 0; r < 8; r++) { - int c = 0; - for (char ch : ranks[r].toCharArray()) { - if (Character.isDigit(ch)) { - c += ch - '0'; - } else { - board[r][c] = pieceValue(ch); - c++; - } - } + return false; + } + + private boolean isSlidingAttacker(int piece, int dr, int dc) { + int absPiece = Math.abs(piece); + boolean isDiagonal = dr != 0 && dc != 0; + boolean isStraight = dr == 0 || dc == 0; + + // Queen (5) attacks on both diagonals and straights + if (absPiece == 5) return true; + // Bishop (3) attacks on diagonals + if (absPiece == 3 && isDiagonal) return true; + // Rook (4) attacks on straight lines + if (absPiece == 4 && isStraight) return true; + + return false; + } + + static int[][] parsePlacement(String placement) { + int[][] board = new int[8][8]; + String[] ranks = placement.split("/"); + for (int r = 0; r < 8; r++) { + int c = 0; + for (char ch : ranks[r].toCharArray()) { + if (Character.isDigit(ch)) { + c += ch - '0'; + } else { + board[r][c] = pieceValue(ch); + c++; } - return board; - } - - static int pieceValue(char ch) { - return switch (ch) { - case 'K' -> 6; - case 'Q' -> 5; - case 'R' -> 4; - case 'B' -> 3; - case 'N' -> 2; - case 'P' -> 1; - case 'k' -> -6; - case 'q' -> -5; - case 'r' -> -4; - case 'b' -> -3; - case 'n' -> -2; - case 'p' -> -1; - default -> 0; - }; + } } + return board; + } + + static int pieceValue(char ch) { + return switch (ch) { + case 'K' -> 6; + case 'Q' -> 5; + case 'R' -> 4; + case 'B' -> 3; + case 'N' -> 2; + case 'P' -> 1; + case 'k' -> -6; + case 'q' -> -5; + case 'r' -> -4; + case 'b' -> -3; + case 'n' -> -2; + case 'p' -> -1; + default -> 0; + }; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/motifs/SkewerDetector.java b/jvm/src/main/java/com/muchq/one_d4/motifs/SkewerDetector.java index 99de7de2..bb4de3f2 100644 --- a/jvm/src/main/java/com/muchq/one_d4/motifs/SkewerDetector.java +++ b/jvm/src/main/java/com/muchq/one_d4/motifs/SkewerDetector.java @@ -3,91 +3,92 @@ import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.engine.model.PositionContext; - import java.util.ArrayList; import java.util.List; public class SkewerDetector implements MotifDetector { - @Override - public Motif motif() { - return Motif.SKEWER; + @Override + public Motif motif() { + return Motif.SKEWER; + } + + @Override + public List detect(List positions) { + List occurrences = new ArrayList<>(); + + for (PositionContext ctx : positions) { + String placement = ctx.fen().split(" ")[0]; + int[][] board = PinDetector.parsePlacement(placement); + + // A skewer is the opposite of a pin: a more valuable piece is in front, + // and when it moves, a less valuable piece behind is captured. + if (hasSkewer(board, !ctx.whiteToMove())) { + occurrences.add( + new GameFeatures.MotifOccurrence( + ctx.moveNumber(), "Skewer detected at move " + ctx.moveNumber())); + } } - @Override - public List detect(List positions) { - List occurrences = new ArrayList<>(); - - for (PositionContext ctx : positions) { - String placement = ctx.fen().split(" ")[0]; - int[][] board = PinDetector.parsePlacement(placement); - - // A skewer is the opposite of a pin: a more valuable piece is in front, - // and when it moves, a less valuable piece behind is captured. - if (hasSkewer(board, !ctx.whiteToMove())) { - occurrences.add(new GameFeatures.MotifOccurrence( - ctx.moveNumber(), "Skewer detected at move " + ctx.moveNumber())); - } - } - - return occurrences; - } + return occurrences; + } - private boolean hasSkewer(int[][] board, boolean attackerIsWhite) { - int[][] directions = {{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}}; + private boolean hasSkewer(int[][] board, boolean attackerIsWhite) { + int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}; - for (int r = 0; r < 8; r++) { - for (int c = 0; c < 8; c++) { - int piece = board[r][c]; - if (piece == 0) continue; - boolean isWhite = piece > 0; - if (isWhite != attackerIsWhite) continue; + for (int r = 0; r < 8; r++) { + for (int c = 0; c < 8; c++) { + int piece = board[r][c]; + if (piece == 0) continue; + boolean isWhite = piece > 0; + if (isWhite != attackerIsWhite) continue; - int absPiece = Math.abs(piece); - // Only sliding pieces can skewer - if (absPiece != 3 && absPiece != 4 && absPiece != 5) continue; + int absPiece = Math.abs(piece); + // Only sliding pieces can skewer + if (absPiece != 3 && absPiece != 4 && absPiece != 5) continue; - for (int[] dir : directions) { - if (!canAttackDirection(absPiece, dir)) continue; - if (isSkewerAlongRay(board, r, c, dir[0], dir[1], attackerIsWhite)) { - return true; - } - } - } + for (int[] dir : directions) { + if (!canAttackDirection(absPiece, dir)) continue; + if (isSkewerAlongRay(board, r, c, dir[0], dir[1], attackerIsWhite)) { + return true; + } } - return false; + } } - - private boolean canAttackDirection(int absPiece, int[] dir) { - boolean isDiagonal = dir[0] != 0 && dir[1] != 0; - boolean isStraight = dir[0] == 0 || dir[1] == 0; - if (absPiece == 5) return true; // Queen - if (absPiece == 3) return isDiagonal; // Bishop - if (absPiece == 4) return isStraight; // Rook - return false; - } - - private boolean isSkewerAlongRay(int[][] board, int ar, int ac, int dr, int dc, boolean attackerIsWhite) { - int r = ar + dr, c = ac + dc; - int firstValue = -1; - - while (r >= 0 && r < 8 && c >= 0 && c < 8) { - int piece = board[r][c]; - if (piece != 0) { - boolean isWhite = piece > 0; - if (isWhite == attackerIsWhite) return false; // friendly piece blocks - - int value = Math.abs(piece); - if (firstValue == -1) { - firstValue = value; - } else { - // Skewer: first piece (in front) is more valuable than second - return firstValue > value && value >= 2; - } - } - r += dr; - c += dc; + return false; + } + + private boolean canAttackDirection(int absPiece, int[] dir) { + boolean isDiagonal = dir[0] != 0 && dir[1] != 0; + boolean isStraight = dir[0] == 0 || dir[1] == 0; + if (absPiece == 5) return true; // Queen + if (absPiece == 3) return isDiagonal; // Bishop + if (absPiece == 4) return isStraight; // Rook + return false; + } + + private boolean isSkewerAlongRay( + int[][] board, int ar, int ac, int dr, int dc, boolean attackerIsWhite) { + int r = ar + dr, c = ac + dc; + int firstValue = -1; + + while (r >= 0 && r < 8 && c >= 0 && c < 8) { + int piece = board[r][c]; + if (piece != 0) { + boolean isWhite = piece > 0; + if (isWhite == attackerIsWhite) return false; // friendly piece blocks + + int value = Math.abs(piece); + if (firstValue == -1) { + firstValue = value; + } else { + // Skewer: first piece (in front) is more valuable than second + return firstValue > value && value >= 2; } - return false; + } + r += dr; + c += dc; } + return false; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/queue/InMemoryIndexQueue.java b/jvm/src/main/java/com/muchq/one_d4/queue/InMemoryIndexQueue.java index 2084f53d..d6141e10 100644 --- a/jvm/src/main/java/com/muchq/one_d4/queue/InMemoryIndexQueue.java +++ b/jvm/src/main/java/com/muchq/one_d4/queue/InMemoryIndexQueue.java @@ -6,26 +6,26 @@ import java.util.concurrent.TimeUnit; public class InMemoryIndexQueue implements IndexQueue { - private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); - @Override - public void enqueue(IndexMessage message) { - queue.add(message); - } + @Override + public void enqueue(IndexMessage message) { + queue.add(message); + } - @Override - public Optional poll(Duration timeout) { - try { - IndexMessage msg = queue.poll(timeout.toMillis(), TimeUnit.MILLISECONDS); - return Optional.ofNullable(msg); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return Optional.empty(); - } + @Override + public Optional poll(Duration timeout) { + try { + IndexMessage msg = queue.poll(timeout.toMillis(), TimeUnit.MILLISECONDS); + return Optional.ofNullable(msg); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return Optional.empty(); } + } - @Override - public int size() { - return queue.size(); - } + @Override + public int size() { + return queue.size(); + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/queue/IndexMessage.java b/jvm/src/main/java/com/muchq/one_d4/queue/IndexMessage.java index 30df8c66..9ea15950 100644 --- a/jvm/src/main/java/com/muchq/one_d4/queue/IndexMessage.java +++ b/jvm/src/main/java/com/muchq/one_d4/queue/IndexMessage.java @@ -2,5 +2,5 @@ import java.util.UUID; -public record IndexMessage(UUID requestId, String player, String platform, String startMonth, String endMonth) { -} +public record IndexMessage( + UUID requestId, String player, String platform, String startMonth, String endMonth) {} diff --git a/jvm/src/main/java/com/muchq/one_d4/queue/IndexQueue.java b/jvm/src/main/java/com/muchq/one_d4/queue/IndexQueue.java index 2a5b4618..73ab1773 100644 --- a/jvm/src/main/java/com/muchq/one_d4/queue/IndexQueue.java +++ b/jvm/src/main/java/com/muchq/one_d4/queue/IndexQueue.java @@ -4,7 +4,9 @@ import java.util.Optional; public interface IndexQueue { - void enqueue(IndexMessage message); - Optional poll(Duration timeout); - int size(); + void enqueue(IndexMessage message); + + Optional poll(Duration timeout); + + int size(); } diff --git a/jvm/src/main/java/com/muchq/one_d4/worker/IndexWorker.java b/jvm/src/main/java/com/muchq/one_d4/worker/IndexWorker.java index 91d0ca4a..107843ed 100644 --- a/jvm/src/main/java/com/muchq/one_d4/worker/IndexWorker.java +++ b/jvm/src/main/java/com/muchq/one_d4/worker/IndexWorker.java @@ -11,125 +11,125 @@ import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.queue.IndexMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.Instant; import java.time.YearMonth; import java.time.format.DateTimeFormatter; -import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IndexWorker { - private static final Logger LOG = LoggerFactory.getLogger(IndexWorker.class); - private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM"); - private static final Pattern ECO_PATTERN = Pattern.compile("\\[ECO\\s+\"([^\"]+)\"\\]"); - - private final ChessClient chessClient; - private final FeatureExtractor featureExtractor; - private final IndexingRequestStore requestStore; - private final GameFeatureStore gameFeatureStore; - private final ObjectMapper objectMapper; - - public IndexWorker( - ChessClient chessClient, - FeatureExtractor featureExtractor, - IndexingRequestStore requestStore, - GameFeatureStore gameFeatureStore, - ObjectMapper objectMapper) { - this.chessClient = chessClient; - this.featureExtractor = featureExtractor; - this.requestStore = requestStore; - this.gameFeatureStore = gameFeatureStore; - this.objectMapper = objectMapper; - } - - public void process(IndexMessage message) { - LOG.info("Processing index request {} for player={} platform={}", - message.requestId(), message.player(), message.platform()); - - try { - requestStore.updateStatus(message.requestId(), "PROCESSING", null, 0); - - YearMonth start = YearMonth.parse(message.startMonth(), MONTH_FORMAT); - YearMonth end = YearMonth.parse(message.endMonth(), MONTH_FORMAT); - int totalIndexed = 0; - - for (YearMonth month = start; !month.isAfter(end); month = month.plusMonths(1)) { - Optional response = chessClient.fetchGames(message.player(), month); - if (response.isEmpty()) { - LOG.warn("No games found for player={} month={}", message.player(), month); - continue; - } - - for (PlayedGame game : response.get().games()) { - try { - indexGame(message, game); - totalIndexed++; - } catch (Exception e) { - LOG.warn("Failed to index game {}", game.url(), e); - } - } - - requestStore.updateStatus(message.requestId(), "PROCESSING", null, totalIndexed); - } - - requestStore.updateStatus(message.requestId(), "COMPLETED", null, totalIndexed); - LOG.info("Completed indexing request {} with {} games", message.requestId(), totalIndexed); - } catch (Exception e) { - LOG.error("Failed to process index request {}", message.requestId(), e); - requestStore.updateStatus(message.requestId(), "FAILED", e.getMessage(), 0); + private static final Logger LOG = LoggerFactory.getLogger(IndexWorker.class); + private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM"); + private static final Pattern ECO_PATTERN = Pattern.compile("\\[ECO\\s+\"([^\"]+)\"\\]"); + + private final ChessClient chessClient; + private final FeatureExtractor featureExtractor; + private final IndexingRequestStore requestStore; + private final GameFeatureStore gameFeatureStore; + private final ObjectMapper objectMapper; + + public IndexWorker( + ChessClient chessClient, + FeatureExtractor featureExtractor, + IndexingRequestStore requestStore, + GameFeatureStore gameFeatureStore, + ObjectMapper objectMapper) { + this.chessClient = chessClient; + this.featureExtractor = featureExtractor; + this.requestStore = requestStore; + this.gameFeatureStore = gameFeatureStore; + this.objectMapper = objectMapper; + } + + public void process(IndexMessage message) { + LOG.info( + "Processing index request {} for player={} platform={}", + message.requestId(), + message.player(), + message.platform()); + + try { + requestStore.updateStatus(message.requestId(), "PROCESSING", null, 0); + + YearMonth start = YearMonth.parse(message.startMonth(), MONTH_FORMAT); + YearMonth end = YearMonth.parse(message.endMonth(), MONTH_FORMAT); + int totalIndexed = 0; + + for (YearMonth month = start; !month.isAfter(end); month = month.plusMonths(1)) { + Optional response = chessClient.fetchGames(message.player(), month); + if (response.isEmpty()) { + LOG.warn("No games found for player={} month={}", message.player(), month); + continue; } - } - - private void indexGame(IndexMessage message, PlayedGame game) { - GameFeatures features = featureExtractor.extract(game.pgn()); - String motifsJson; - try { - motifsJson = objectMapper.writeValueAsString(features.occurrences()); - } catch (JsonProcessingException e) { - motifsJson = "{}"; + for (PlayedGame game : response.get().games()) { + try { + indexGame(message, game); + totalIndexed++; + } catch (Exception e) { + LOG.warn("Failed to index game {}", game.url(), e); + } } - String result = determineResult(game); - - GameFeatureStore.GameFeature row = new GameFeatureStore.GameFeature( - null, // id generated by DB - message.requestId(), - game.url(), - message.platform(), - game.whiteResult() != null ? game.whiteResult().username() : null, - game.blackResult() != null ? game.blackResult().username() : null, - game.whiteResult() != null ? Integer.valueOf(game.whiteResult().rating()) : null, - game.blackResult() != null ? Integer.valueOf(game.blackResult().rating()) : null, - game.timeClass(), - extractEcoFromPgn(game.pgn()), - result, - game.endTime(), - features.numMoves(), - features.hasMotif(Motif.PIN), - features.hasMotif(Motif.CROSS_PIN), - features.hasMotif(Motif.FORK), - features.hasMotif(Motif.SKEWER), - features.hasMotif(Motif.DISCOVERED_ATTACK), - motifsJson, - game.pgn() - ); - - gameFeatureStore.insert(row); - } + requestStore.updateStatus(message.requestId(), "PROCESSING", null, totalIndexed); + } - private String determineResult(PlayedGame game) { - String whiteResult = game.whiteResult() != null ? game.whiteResult().result() : null; - String blackResult = game.blackResult() != null ? game.blackResult().result() : null; - return ResultMapper.mapResult(whiteResult, blackResult); + requestStore.updateStatus(message.requestId(), "COMPLETED", null, totalIndexed); + LOG.info("Completed indexing request {} with {} games", message.requestId(), totalIndexed); + } catch (Exception e) { + LOG.error("Failed to process index request {}", message.requestId(), e); + requestStore.updateStatus(message.requestId(), "FAILED", e.getMessage(), 0); } + } + + private void indexGame(IndexMessage message, PlayedGame game) { + GameFeatures features = featureExtractor.extract(game.pgn()); - private String extractEcoFromPgn(String pgn) { - Matcher m = ECO_PATTERN.matcher(pgn); - return m.find() ? m.group(1) : null; + String motifsJson; + try { + motifsJson = objectMapper.writeValueAsString(features.occurrences()); + } catch (JsonProcessingException e) { + motifsJson = "{}"; } + + String result = determineResult(game); + + GameFeatureStore.GameFeature row = + new GameFeatureStore.GameFeature( + null, // id generated by DB + message.requestId(), + game.url(), + message.platform(), + game.whiteResult() != null ? game.whiteResult().username() : null, + game.blackResult() != null ? game.blackResult().username() : null, + game.whiteResult() != null ? Integer.valueOf(game.whiteResult().rating()) : null, + game.blackResult() != null ? Integer.valueOf(game.blackResult().rating()) : null, + game.timeClass(), + extractEcoFromPgn(game.pgn()), + result, + game.endTime(), + features.numMoves(), + features.hasMotif(Motif.PIN), + features.hasMotif(Motif.CROSS_PIN), + features.hasMotif(Motif.FORK), + features.hasMotif(Motif.SKEWER), + features.hasMotif(Motif.DISCOVERED_ATTACK), + motifsJson, + game.pgn()); + + gameFeatureStore.insert(row); + } + + private String determineResult(PlayedGame game) { + String whiteResult = game.whiteResult() != null ? game.whiteResult().result() : null; + String blackResult = game.blackResult() != null ? game.blackResult().result() : null; + return ResultMapper.mapResult(whiteResult, blackResult); + } + + private String extractEcoFromPgn(String pgn) { + Matcher m = ECO_PATTERN.matcher(pgn); + return m.find() ? m.group(1) : null; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/worker/IndexWorkerLifecycle.java b/jvm/src/main/java/com/muchq/one_d4/worker/IndexWorkerLifecycle.java index c1fbf4c4..2d3a8cc0 100644 --- a/jvm/src/main/java/com/muchq/one_d4/worker/IndexWorkerLifecycle.java +++ b/jvm/src/main/java/com/muchq/one_d4/worker/IndexWorkerLifecycle.java @@ -4,44 +4,43 @@ import com.muchq.one_d4.queue.IndexQueue; import io.micronaut.context.event.ApplicationEventListener; import io.micronaut.runtime.server.event.ServerStartupEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.time.Duration; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IndexWorkerLifecycle implements ApplicationEventListener { - private static final Logger LOG = LoggerFactory.getLogger(IndexWorkerLifecycle.class); - - private final IndexQueue queue; - private final IndexWorker worker; - private volatile boolean running = true; - - public IndexWorkerLifecycle(IndexQueue queue, IndexWorker worker) { - this.queue = queue; - this.worker = worker; - } - - @Override - public void onApplicationEvent(ServerStartupEvent event) { - Thread workerThread = new Thread(this::pollLoop, "index-worker"); - workerThread.setDaemon(true); - workerThread.start(); - LOG.info("Index worker started"); + private static final Logger LOG = LoggerFactory.getLogger(IndexWorkerLifecycle.class); + + private final IndexQueue queue; + private final IndexWorker worker; + private volatile boolean running = true; + + public IndexWorkerLifecycle(IndexQueue queue, IndexWorker worker) { + this.queue = queue; + this.worker = worker; + } + + @Override + public void onApplicationEvent(ServerStartupEvent event) { + Thread workerThread = new Thread(this::pollLoop, "index-worker"); + workerThread.setDaemon(true); + workerThread.start(); + LOG.info("Index worker started"); + } + + private void pollLoop() { + while (running) { + try { + Optional message = queue.poll(Duration.ofSeconds(5)); + message.ifPresent(worker::process); + } catch (Exception e) { + LOG.error("Error in index worker poll loop", e); + } } + } - private void pollLoop() { - while (running) { - try { - Optional message = queue.poll(Duration.ofSeconds(5)); - message.ifPresent(worker::process); - } catch (Exception e) { - LOG.error("Error in index worker poll loop", e); - } - } - } - - public void stop() { - running = false; - } + public void stop() { + running = false; + } } diff --git a/jvm/src/main/java/com/muchq/one_d4/worker/ResultMapper.java b/jvm/src/main/java/com/muchq/one_d4/worker/ResultMapper.java index 1ca276f0..4376a897 100644 --- a/jvm/src/main/java/com/muchq/one_d4/worker/ResultMapper.java +++ b/jvm/src/main/java/com/muchq/one_d4/worker/ResultMapper.java @@ -3,72 +3,73 @@ /** * Maps chess.com API result values to standard chess notation. * - * Chess.com returns separate result strings for white and black: - * - "win" for the winning side - * - "resigned", "checkmated", "timeout", "abandoned" for the losing side - * - "agreed", "repetition", "stalemate", "insufficient", "50move", "timevsinsufficient" for draws + *

Chess.com returns separate result strings for white and black: - "win" for the winning side - + * "resigned", "checkmated", "timeout", "abandoned" for the losing side - "agreed", "repetition", + * "stalemate", "insufficient", "50move", "timevsinsufficient" for draws * - * This class normalizes these to standard notation: "1-0", "0-1", "1/2-1/2", or "unknown". + *

This class normalizes these to standard notation: "1-0", "0-1", "1/2-1/2", or "unknown". */ public class ResultMapper { - /** - * Determines the game result in standard notation. - * - * @param whiteResult chess.com result string for white (may be null) - * @param blackResult chess.com result string for black (may be null) - * @return "1-0" (white wins), "0-1" (black wins), "1/2-1/2" (draw), or "unknown" - */ - public static String mapResult(String whiteResult, String blackResult) { - if (whiteResult == null && blackResult == null) { - return "unknown"; - } - - // Explicit win - if ("win".equals(whiteResult)) { - return "1-0"; - } - if ("win".equals(blackResult)) { - return "0-1"; - } - - // Draw results - if (isDrawResult(whiteResult) || isDrawResult(blackResult)) { - return "1/2-1/2"; - } - - // White lost → black won - if (isLossResult(whiteResult)) { - return "0-1"; - } - // Black lost → white won - if (isLossResult(blackResult)) { - return "1-0"; - } + /** + * Determines the game result in standard notation. + * + * @param whiteResult chess.com result string for white (may be null) + * @param blackResult chess.com result string for black (may be null) + * @return "1-0" (white wins), "0-1" (black wins), "1/2-1/2" (draw), or "unknown" + */ + public static String mapResult(String whiteResult, String blackResult) { + if (whiteResult == null && blackResult == null) { + return "unknown"; + } - return "unknown"; + // Explicit win + if ("win".equals(whiteResult)) { + return "1-0"; + } + if ("win".equals(blackResult)) { + return "0-1"; } - /** - * Checks if the result string indicates a draw. - */ - public static boolean isDrawResult(String result) { - if (result == null) return false; - return switch (result) { - case "agreed", "repetition", "stalemate", "insufficient", - "50move", "timevsinsufficient", "drawn" -> true; - default -> false; - }; + // Draw results + if (isDrawResult(whiteResult) || isDrawResult(blackResult)) { + return "1/2-1/2"; } - /** - * Checks if the result string indicates a loss for that player. - */ - public static boolean isLossResult(String result) { - if (result == null) return false; - return switch (result) { - case "resigned", "checkmated", "timeout", "abandoned", "lose" -> true; - default -> false; - }; + // White lost → black won + if (isLossResult(whiteResult)) { + return "0-1"; } + // Black lost → white won + if (isLossResult(blackResult)) { + return "1-0"; + } + + return "unknown"; + } + + /** Checks if the result string indicates a draw. */ + public static boolean isDrawResult(String result) { + if (result == null) return false; + return switch (result) { + case "agreed", + "repetition", + "stalemate", + "insufficient", + "50move", + "timevsinsufficient", + "drawn" -> + true; + default -> false; + }; + } + + /** Checks if the result string indicates a loss for that player. */ + public static boolean isLossResult(String result) { + if (result == null) return false; + return switch (result) { + case "resigned", "checkmated", "timeout", "abandoned", "lose" -> true; + default -> false; + }; + } } diff --git a/jvm/src/main/java/com/muchq/stecky/GuiceConfigurator.java b/jvm/src/main/java/com/muchq/stecky/GuiceConfigurator.java index ee9e8c15..c0f8d4f6 100644 --- a/jvm/src/main/java/com/muchq/stecky/GuiceConfigurator.java +++ b/jvm/src/main/java/com/muchq/stecky/GuiceConfigurator.java @@ -6,8 +6,7 @@ public class GuiceConfigurator extends ServerEndpointConfig.Configurator { - @Inject - private static Injector injector; + @Inject private static Injector injector; @Override public T getEndpointInstance(Class endpointClass) { diff --git a/jvm/src/main/java/com/muchq/stecky/WebSocketServer.java b/jvm/src/main/java/com/muchq/stecky/WebSocketServer.java index 11f5914d..1c46347f 100644 --- a/jvm/src/main/java/com/muchq/stecky/WebSocketServer.java +++ b/jvm/src/main/java/com/muchq/stecky/WebSocketServer.java @@ -73,7 +73,10 @@ public Builder addMappings(Mapping... mappings) { public Builder addModules(Module... modules) { this.modules = - ImmutableSet.builder().addAll(ImmutableSet.copyOf(modules)).add(new WebSocketModule()).build(); + ImmutableSet.builder() + .addAll(ImmutableSet.copyOf(modules)) + .add(new WebSocketModule()) + .build(); return this; } @@ -108,11 +111,9 @@ public WebSocketServer build() { for (Mapping mapping : mappings) { try { wscontainer.addEndpoint( - ServerEndpointConfig.Builder - .create(mapping.getHandler(), mapping.getPath()) - .configurator(injector.getInstance(GuiceConfigurator.class)) - .build() - ); + ServerEndpointConfig.Builder.create(mapping.getHandler(), mapping.getPath()) + .configurator(injector.getInstance(GuiceConfigurator.class)) + .build()); } catch (DeploymentException e) { throw new RuntimeException(e); } diff --git a/jvm/src/main/java/com/muchq/yochat/App.java b/jvm/src/main/java/com/muchq/yochat/App.java index 3e8168c3..3304c42b 100644 --- a/jvm/src/main/java/com/muchq/yochat/App.java +++ b/jvm/src/main/java/com/muchq/yochat/App.java @@ -5,10 +5,9 @@ public class App { public static void main(String[] args) throws Exception { - YoServer - .builder() - .setChannelHandler(new ChatHandler()) - .setPort(Integer.parseInt(System.getenv("PORT"))) - .buildAndRun(); + YoServer.builder() + .setChannelHandler(new ChatHandler()) + .setPort(Integer.parseInt(System.getenv("PORT"))) + .buildAndRun(); } } diff --git a/jvm/src/main/java/com/muchq/yochat/ChatHandler.java b/jvm/src/main/java/com/muchq/yochat/ChatHandler.java index f27047c1..36d278ba 100644 --- a/jvm/src/main/java/com/muchq/yochat/ChatHandler.java +++ b/jvm/src/main/java/com/muchq/yochat/ChatHandler.java @@ -22,7 +22,8 @@ public class ChatHandler extends SimpleChannelInboundHandler { private final Map users = new ConcurrentHashMap<>(); private final Set usernames = new ConcurrentSkipListSet<>(); - private static final String HELLO = "Connected. Enter a username by typing `/name `.\n"; + private static final String HELLO = + "Connected. Enter a username by typing `/name `.\n"; private static final String GOODBYE = "Disconnected.\n"; private static final String SET_NAME_COMMAND = "/name "; private static final String HELP_COMMAND = "/help"; @@ -69,7 +70,8 @@ protected void channelRead0(ChannelHandlerContext context, String rawMessage) { if (LURKERS_COMMAND.equalsIgnoreCase(msg)) { LOGGER.info("{} ({}) asked for lurkers", context, users.get(context.channel())); - context.writeAndFlush("there are " + (channels.size() - users.size()) + " nameless lurkers.\n"); + context.writeAndFlush( + "there are " + (channels.size() - users.size()) + " nameless lurkers.\n"); return; } @@ -93,7 +95,8 @@ protected void channelRead0(ChannelHandlerContext context, String rawMessage) { if (HELP_COMMAND.equalsIgnoreCase(msg)) { LOGGER.info("{} ({}) asked for help", context, users.get(context.channel())); - context.writeAndFlush("/name to set your username\n/quit to disconnect\n/help prints this message\n"); + context.writeAndFlush( + "/name to set your username\n/quit to disconnect\n/help prints this message\n"); return; } diff --git a/jvm/src/main/java/com/muchq/yochat/lib/YoServer.java b/jvm/src/main/java/com/muchq/yochat/lib/YoServer.java index 8af5097e..afd89395 100644 --- a/jvm/src/main/java/com/muchq/yochat/lib/YoServer.java +++ b/jvm/src/main/java/com/muchq/yochat/lib/YoServer.java @@ -24,11 +24,10 @@ public void run() throws Exception { EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); - b - .group(bossGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .handler(new LoggingHandler(LogLevel.INFO)) - .childHandler(channelHandler); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(channelHandler); b.bind(port).sync().channel().closeFuture().sync(); } finally { diff --git a/jvm/src/test/java/com/muchq/imagine/ImageUtilsTest.java b/jvm/src/test/java/com/muchq/imagine/ImageUtilsTest.java index 65a8f8f9..5d203734 100644 --- a/jvm/src/test/java/com/muchq/imagine/ImageUtilsTest.java +++ b/jvm/src/test/java/com/muchq/imagine/ImageUtilsTest.java @@ -1,26 +1,25 @@ package com.muchq.imagine; -import org.junit.Test; - -import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; +import javax.imageio.ImageIO; +import org.junit.Test; public class ImageUtilsTest { - @Test - public void itCanBlurImages() throws IOException { - BufferedImage marbles = ImageIO.read(ImageUtils.class.getResourceAsStream("/MARBLES.BMP")); - BufferedImage graussian = ImageUtils.grayGaussianBlur(marbles, Radius.FIVE, 1); + @Test + public void itCanBlurImages() throws IOException { + BufferedImage marbles = ImageIO.read(ImageUtils.class.getResourceAsStream("/MARBLES.BMP")); + BufferedImage graussian = ImageUtils.grayGaussianBlur(marbles, Radius.FIVE, 1); - // assert stuff about pixels - } + // assert stuff about pixels + } - @Test - public void itCanSobelImages() throws IOException { - BufferedImage marbles = ImageIO.read(ImageUtils.class.getResourceAsStream("/MARBLES.BMP")); - BufferedImage graussian = ImageUtils.grayGaussianBlur(marbles, Radius.FIVE, 1); - BufferedImage sobel = ImageUtils.sobel(graussian); + @Test + public void itCanSobelImages() throws IOException { + BufferedImage marbles = ImageIO.read(ImageUtils.class.getResourceAsStream("/MARBLES.BMP")); + BufferedImage graussian = ImageUtils.grayGaussianBlur(marbles, Radius.FIVE, 1); + BufferedImage sobel = ImageUtils.sobel(graussian); - // assert stuff about pixels - } + // assert stuff about pixels + } } diff --git a/jvm/src/test/java/com/muchq/json/JsonUtilsTest.java b/jvm/src/test/java/com/muchq/json/JsonUtilsTest.java index f8f3265c..21f66df4 100644 --- a/jvm/src/test/java/com/muchq/json/JsonUtilsTest.java +++ b/jvm/src/test/java/com/muchq/json/JsonUtilsTest.java @@ -20,7 +20,8 @@ public void itCanReadAndWriteJsonAsTypeReference() { widgets.add(new Java8Widget(2)); String widgetStrings = JsonUtils.writeAsString(widgets); - List read = JsonUtils.readAs(widgetStrings, new TypeReference>() {}); + List read = + JsonUtils.readAs(widgetStrings, new TypeReference>() {}); assertThat(read).isEqualTo(widgets); } diff --git a/jvm/src/test/java/com/muchq/lunarcat/ServiceTest.java b/jvm/src/test/java/com/muchq/lunarcat/ServiceTest.java index 02b4af1a..3c3d5843 100644 --- a/jvm/src/test/java/com/muchq/lunarcat/ServiceTest.java +++ b/jvm/src/test/java/com/muchq/lunarcat/ServiceTest.java @@ -24,11 +24,11 @@ public class ServiceTest { public static void setup() { int port = getPort(); baseUrl = "http://localhost:" + port; - Configuration configuration = Configuration - .newBuilder() - .withPort(port) - .withBasePackage(Package.getPackage("com.muchq.lunarcat")) - .build(); + Configuration configuration = + Configuration.newBuilder() + .withPort(port) + .withBasePackage(Package.getPackage("com.muchq.lunarcat")) + .build(); service = new Service(configuration); service.run(ServerMode.NO_WAIT); } @@ -41,14 +41,16 @@ public static void tearDown() { @Test public void itServesRequests() { String message = "hey"; - HttpRequest request = HttpRequest.newBuilder().setUrl(baseUrl + "/test?message=" + message).build(); + HttpRequest request = + HttpRequest.newBuilder().setUrl(baseUrl + "/test?message=" + message).build(); Widget widget = client.execute(request).getAs(Widget.class); assertThat(widget.getMessage()).isEqualTo(message); } @Test public void itWritesOptionalResponses() { - HttpRequest request = HttpRequest.newBuilder().setUrl(baseUrl + "/test/optional-present").build(); + HttpRequest request = + HttpRequest.newBuilder().setUrl(baseUrl + "/test/optional-present").build(); HttpResponse response = client.execute(request); assertThat(response.getStatusCode()).isEqualTo(200); } @@ -62,7 +64,8 @@ public void itReturns404OnEmptyOptionals() { @Test public void itReturns404OnUnboundPath() { - HttpRequest request = HttpRequest.newBuilder().setUrl(baseUrl + "/this-is-not-a-real-path").build(); + HttpRequest request = + HttpRequest.newBuilder().setUrl(baseUrl + "/this-is-not-a-real-path").build(); HttpResponse response = client.execute(request); assertThat(response.getStatusCode()).isEqualTo(404); } @@ -83,11 +86,11 @@ public void itReturns500OnServerError() { @Test public void itReturns405OnMethodNotAllowed() { - HttpRequest request = HttpRequest - .newBuilder() - .setUrl(baseUrl + "/test/server-error") - .setMethod(Method.POST) - .build(); + HttpRequest request = + HttpRequest.newBuilder() + .setUrl(baseUrl + "/test/server-error") + .setMethod(Method.POST) + .build(); HttpResponse response = client.execute(request); assertThat(response.getStatusCode()).isEqualTo(405); } diff --git a/jvm/src/test/java/com/muchq/lunarcat/config/ConfigurationTest.java b/jvm/src/test/java/com/muchq/lunarcat/config/ConfigurationTest.java index 28e334dc..ec22e1c3 100644 --- a/jvm/src/test/java/com/muchq/lunarcat/config/ConfigurationTest.java +++ b/jvm/src/test/java/com/muchq/lunarcat/config/ConfigurationTest.java @@ -15,16 +15,17 @@ public class ConfigurationTest { public void itHasThePropertiesYouSet() { int port = ThreadLocalRandom.current().nextInt(); String appRoot = UUID.randomUUID().toString(); - Module module = new AbstractModule() { - protected void configure() {} - }; - Configuration configuration = Configuration - .newBuilder() - .withBasePackage(getClass().getPackage()) - .withModules(module) - .withAppRoot(appRoot) - .withPort(port) - .build(); + Module module = + new AbstractModule() { + protected void configure() {} + }; + Configuration configuration = + Configuration.newBuilder() + .withBasePackage(getClass().getPackage()) + .withModules(module) + .withAppRoot(appRoot) + .withPort(port) + .build(); assertThat(configuration.getBasePackage()).isSameAs(getClass().getPackage()); assertThat(configuration.getModules()).contains(module).hasSize(1); diff --git a/jvm/src/test/java/com/muchq/lunarcat/util/PublicPreconditionsTest.java b/jvm/src/test/java/com/muchq/lunarcat/util/PublicPreconditionsTest.java index a565d773..21782775 100644 --- a/jvm/src/test/java/com/muchq/lunarcat/util/PublicPreconditionsTest.java +++ b/jvm/src/test/java/com/muchq/lunarcat/util/PublicPreconditionsTest.java @@ -7,8 +7,7 @@ public class PublicPreconditionsTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); + @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void itThrowsBadRequestOnFalsePredicate() { diff --git a/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComGamesToolTest.java b/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComGamesToolTest.java index cc86e161..183dbb74 100644 --- a/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComGamesToolTest.java +++ b/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComGamesToolTest.java @@ -14,103 +14,101 @@ public class ChessComGamesToolTest { - private static class StubChessClient extends ChessClient { - private final Optional response; - - public StubChessClient(Optional response) { - super(null, null); - this.response = response; - } - - @Override - public Optional fetchGames(String player, YearMonth yearMonth) { - return response; - } - } - - private final GamesResponse emptyGamesResponse = new GamesResponse(List.of()); - private final ChessClient stubClient = new StubChessClient(Optional.of(emptyGamesResponse)); - private final ChessComGamesTool tool = new ChessComGamesTool(stubClient, JsonUtils.mapper()); - - @Test - public void testGetName() { - assertThat(tool.getName()).isEqualTo("chess_com_games"); - } + private static class StubChessClient extends ChessClient { + private final Optional response; - @Test - public void testGetDescription() { - assertThat(tool.getDescription()) - .contains("chess.com games") - .contains("month") - .contains("year"); + public StubChessClient(Optional response) { + super(null, null); + this.response = response; } - @Test - public void testGetInputSchema() { - Map schema = tool.getInputSchema(); - assertThat(schema).containsKey("type"); - assertThat(schema).containsKey("properties"); - assertThat(schema).containsKey("required"); - - @SuppressWarnings("unchecked") - Map properties = (Map) schema.get("properties"); - assertThat(properties).containsKey("username"); - assertThat(properties).containsKey("year"); - assertThat(properties).containsKey("month"); + @Override + public Optional fetchGames(String player, YearMonth yearMonth) { + return response; } - - @Test - public void testExecuteWithValidParameters() { - Map arguments = Map.of( + } + + private final GamesResponse emptyGamesResponse = new GamesResponse(List.of()); + private final ChessClient stubClient = new StubChessClient(Optional.of(emptyGamesResponse)); + private final ChessComGamesTool tool = new ChessComGamesTool(stubClient, JsonUtils.mapper()); + + @Test + public void testGetName() { + assertThat(tool.getName()).isEqualTo("chess_com_games"); + } + + @Test + public void testGetDescription() { + assertThat(tool.getDescription()) + .contains("chess.com games") + .contains("month") + .contains("year"); + } + + @Test + public void testGetInputSchema() { + Map schema = tool.getInputSchema(); + assertThat(schema).containsKey("type"); + assertThat(schema).containsKey("properties"); + assertThat(schema).containsKey("required"); + + @SuppressWarnings("unchecked") + Map properties = (Map) schema.get("properties"); + assertThat(properties).containsKey("username"); + assertThat(properties).containsKey("year"); + assertThat(properties).containsKey("month"); + } + + @Test + public void testExecuteWithValidParameters() { + Map arguments = + Map.of( "username", "drawlya", "year", "2026", - "month", "01" - ); - String result = tool.execute(arguments); - assertThat(result).isNotNull(); - } - - @Test - public void testExecuteWithDifferentMonthFormat() { - Map arguments = Map.of( + "month", "01"); + String result = tool.execute(arguments); + assertThat(result).isNotNull(); + } + + @Test + public void testExecuteWithDifferentMonthFormat() { + Map arguments = + Map.of( "username", "hikaru", "year", "2025", - "month", "12" - ); - String result = tool.execute(arguments); - assertThat(result).isNotNull(); - } - - @Test - public void testExecuteWithSingleDigitMonth() { - Map arguments = Map.of( + "month", "12"); + String result = tool.execute(arguments); + assertThat(result).isNotNull(); + } + + @Test + public void testExecuteWithSingleDigitMonth() { + Map arguments = + Map.of( "username", "magnus", "year", "2024", - "month", "5" - ); - String result = tool.execute(arguments); - assertThat(result).isNotNull(); - } - - @Test - public void testExecuteWithInvalidYear() { - Map arguments = Map.of( + "month", "5"); + String result = tool.execute(arguments); + assertThat(result).isNotNull(); + } + + @Test + public void testExecuteWithInvalidYear() { + Map arguments = + Map.of( "username", "testuser", "year", "invalid", - "month", "01" - ); - assertThatThrownBy(() -> tool.execute(arguments)) - .isInstanceOf(NumberFormatException.class); - } - - @Test - public void testExecuteWithInvalidMonth() { - Map arguments = Map.of( + "month", "01"); + assertThatThrownBy(() -> tool.execute(arguments)).isInstanceOf(NumberFormatException.class); + } + + @Test + public void testExecuteWithInvalidMonth() { + Map arguments = + Map.of( "username", "testuser", "year", "2025", - "month", "invalid" - ); - assertThatThrownBy(() -> tool.execute(arguments)) - .isInstanceOf(NumberFormatException.class); - } + "month", "invalid"); + assertThatThrownBy(() -> tool.execute(arguments)).isInstanceOf(NumberFormatException.class); + } } diff --git a/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComPlayerToolTest.java b/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComPlayerToolTest.java index 87df076f..14e0f6ac 100644 --- a/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComPlayerToolTest.java +++ b/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComPlayerToolTest.java @@ -13,85 +13,83 @@ public class ChessComPlayerToolTest { - private static class StubChessClient extends ChessClient { - private final Optional response; + private static class StubChessClient extends ChessClient { + private final Optional response; - public StubChessClient(Optional response) { - super(null, null); - this.response = response; - } + public StubChessClient(Optional response) { + super(null, null); + this.response = response; + } - @Override - public Optional fetchPlayer(String player) { - return response; - } + @Override + public Optional fetchPlayer(String player) { + return response; } + } - private final Player emptyPlayer = new Player( - 1, - "https://api.chess.com/pub/player/testuser", - "https://chess.com/member/testuser", - "Test User", - "testuser", - 100, - "https://api.chess.com/pub/country/US", - Instant.now(), - Instant.now(), - "active", - false, - false, - "bronze", - List.of() - ); - private final ChessClient stubClient = new StubChessClient(Optional.of(emptyPlayer)); - private final ChessComPlayerTool tool = new ChessComPlayerTool(stubClient, JsonUtils.mapper()); + private final Player emptyPlayer = + new Player( + 1, + "https://api.chess.com/pub/player/testuser", + "https://chess.com/member/testuser", + "Test User", + "testuser", + 100, + "https://api.chess.com/pub/country/US", + Instant.now(), + Instant.now(), + "active", + false, + false, + "bronze", + List.of()); + private final ChessClient stubClient = new StubChessClient(Optional.of(emptyPlayer)); + private final ChessComPlayerTool tool = new ChessComPlayerTool(stubClient, JsonUtils.mapper()); - @Test - public void testGetName() { - assertThat(tool.getName()).isEqualTo("chess_com_player"); - } + @Test + public void testGetName() { + assertThat(tool.getName()).isEqualTo("chess_com_player"); + } - @Test - public void testGetDescription() { - assertThat(tool.getDescription()) - .contains("chess.com") - .contains("player"); - } + @Test + public void testGetDescription() { + assertThat(tool.getDescription()).contains("chess.com").contains("player"); + } - @Test - public void testGetInputSchema() { - Map schema = tool.getInputSchema(); - assertThat(schema).containsKey("type"); - assertThat(schema).containsKey("properties"); - assertThat(schema).containsKey("required"); + @Test + public void testGetInputSchema() { + Map schema = tool.getInputSchema(); + assertThat(schema).containsKey("type"); + assertThat(schema).containsKey("properties"); + assertThat(schema).containsKey("required"); - @SuppressWarnings("unchecked") - Map properties = (Map) schema.get("properties"); - assertThat(properties).containsKey("username"); - } + @SuppressWarnings("unchecked") + Map properties = (Map) schema.get("properties"); + assertThat(properties).containsKey("username"); + } - @Test - public void testExecuteWithValidParameters() { - Map arguments = Map.of("username", "hikaru"); - String result = tool.execute(arguments); - assertThat(result).isNotNull(); - assertThat(result).contains("testuser"); - } + @Test + public void testExecuteWithValidParameters() { + Map arguments = Map.of("username", "hikaru"); + String result = tool.execute(arguments); + assertThat(result).isNotNull(); + assertThat(result).contains("testuser"); + } - @Test - public void testExecuteWithDifferentUsername() { - Map arguments = Map.of("username", "magnus"); - String result = tool.execute(arguments); - assertThat(result).isNotNull(); - } + @Test + public void testExecuteWithDifferentUsername() { + Map arguments = Map.of("username", "magnus"); + String result = tool.execute(arguments); + assertThat(result).isNotNull(); + } - @Test - public void testExecuteWithPlayerNotFound() { - ChessClient notFoundClient = new StubChessClient(Optional.empty()); - ChessComPlayerTool notFoundTool = new ChessComPlayerTool(notFoundClient, JsonUtils.mapper()); + @Test + public void testExecuteWithPlayerNotFound() { + ChessClient notFoundClient = new StubChessClient(Optional.empty()); + ChessComPlayerTool notFoundTool = new ChessComPlayerTool(notFoundClient, JsonUtils.mapper()); - Map arguments = Map.of("username", "nonexistent"); - String result = notFoundTool.execute(arguments); - assertThat(result).isEqualTo("player not found"); - } + Map arguments = Map.of("username", "nonexistent"); + String result = notFoundTool.execute(arguments); + assertThat(result).isEqualTo("player not found"); + } } diff --git a/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComStatsToolTest.java b/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComStatsToolTest.java index 72da9bae..9a4fb296 100644 --- a/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComStatsToolTest.java +++ b/jvm/src/test/java/com/muchq/mcpserver/tools/ChessComStatsToolTest.java @@ -11,76 +11,68 @@ public class ChessComStatsToolTest { - private static class StubChessClient extends ChessClient { - private final Optional response; + private static class StubChessClient extends ChessClient { + private final Optional response; - public StubChessClient(Optional response) { - super(null, null); - this.response = response; - } + public StubChessClient(Optional response) { + super(null, null); + this.response = response; + } - @Override - public Optional fetchStats(String player) { - return response; - } + @Override + public Optional fetchStats(String player) { + return response; } + } - private final StatsResponse emptyStatsResponse = new StatsResponse( - null, - null, - null, - null, - 0, - null - ); - private final ChessClient stubClient = new StubChessClient(Optional.of(emptyStatsResponse)); - private final ChessComStatsTool tool = new ChessComStatsTool(stubClient, JsonUtils.mapper()); + private final StatsResponse emptyStatsResponse = + new StatsResponse(null, null, null, null, 0, null); + private final ChessClient stubClient = new StubChessClient(Optional.of(emptyStatsResponse)); + private final ChessComStatsTool tool = new ChessComStatsTool(stubClient, JsonUtils.mapper()); - @Test - public void testGetName() { - assertThat(tool.getName()).isEqualTo("chess_com_stats"); - } + @Test + public void testGetName() { + assertThat(tool.getName()).isEqualTo("chess_com_stats"); + } - @Test - public void testGetDescription() { - assertThat(tool.getDescription()) - .contains("chess.com") - .contains("stats"); - } + @Test + public void testGetDescription() { + assertThat(tool.getDescription()).contains("chess.com").contains("stats"); + } - @Test - public void testGetInputSchema() { - Map schema = tool.getInputSchema(); - assertThat(schema).containsKey("type"); - assertThat(schema).containsKey("properties"); - assertThat(schema).containsKey("required"); + @Test + public void testGetInputSchema() { + Map schema = tool.getInputSchema(); + assertThat(schema).containsKey("type"); + assertThat(schema).containsKey("properties"); + assertThat(schema).containsKey("required"); - @SuppressWarnings("unchecked") - Map properties = (Map) schema.get("properties"); - assertThat(properties).containsKey("username"); - } + @SuppressWarnings("unchecked") + Map properties = (Map) schema.get("properties"); + assertThat(properties).containsKey("username"); + } - @Test - public void testExecuteWithValidParameters() { - Map arguments = Map.of("username", "hikaru"); - String result = tool.execute(arguments); - assertThat(result).isNotNull(); - } + @Test + public void testExecuteWithValidParameters() { + Map arguments = Map.of("username", "hikaru"); + String result = tool.execute(arguments); + assertThat(result).isNotNull(); + } - @Test - public void testExecuteWithDifferentUsername() { - Map arguments = Map.of("username", "magnus"); - String result = tool.execute(arguments); - assertThat(result).isNotNull(); - } + @Test + public void testExecuteWithDifferentUsername() { + Map arguments = Map.of("username", "magnus"); + String result = tool.execute(arguments); + assertThat(result).isNotNull(); + } - @Test - public void testExecuteWithPlayerNotFound() { - ChessClient notFoundClient = new StubChessClient(Optional.empty()); - ChessComStatsTool notFoundTool = new ChessComStatsTool(notFoundClient, JsonUtils.mapper()); + @Test + public void testExecuteWithPlayerNotFound() { + ChessClient notFoundClient = new StubChessClient(Optional.empty()); + ChessComStatsTool notFoundTool = new ChessComStatsTool(notFoundClient, JsonUtils.mapper()); - Map arguments = Map.of("username", "nonexistent"); - String result = notFoundTool.execute(arguments); - assertThat(result).isEqualTo("player not found"); - } + Map arguments = Map.of("username", "nonexistent"); + String result = notFoundTool.execute(arguments); + assertThat(result).isEqualTo("player not found"); + } } diff --git a/jvm/src/test/java/com/muchq/mcpserver/tools/ServerTimeToolTest.java b/jvm/src/test/java/com/muchq/mcpserver/tools/ServerTimeToolTest.java index 106a52b2..b875cd7e 100644 --- a/jvm/src/test/java/com/muchq/mcpserver/tools/ServerTimeToolTest.java +++ b/jvm/src/test/java/com/muchq/mcpserver/tools/ServerTimeToolTest.java @@ -9,52 +9,53 @@ import org.junit.Test; public class ServerTimeToolTest { - private static final Instant FIXED_INSTANT = Instant.parse("2024-01-15T10:30:45.123Z"); - private static final Clock FIXED_CLOCK = Clock.fixed(FIXED_INSTANT, ZoneId.of("UTC")); - - private final ServerTimeTool tool = new ServerTimeTool(FIXED_CLOCK); - - @Test - public void testGetName() { - assertThat(tool.getName()).isEqualTo("server_time"); - } - - @Test - public void testGetDescription() { - assertThat(tool.getDescription()).isEqualTo("Returns the current timestamp according to the server's system clock"); - } - - @Test - public void testGetInputSchema() { - Map schema = tool.getInputSchema(); - assertThat(schema).containsKey("type"); - assertThat(schema).containsKey("properties"); - Map properties = (Map) schema.get("properties"); - assertThat(properties).isEmpty(); - } - - @Test - public void testExecuteReturnsExpectedTimestamp() { - Map arguments = Map.of(); - String result = tool.execute(arguments); - assertThat(result).isEqualTo(String.valueOf(FIXED_INSTANT.toEpochMilli())); - } - - @Test - public void testExecuteReturnsConsistentValue() { - Map arguments = Map.of(); - String result1 = tool.execute(arguments); - String result2 = tool.execute(arguments); - assertThat(result1).isEqualTo(result2); - } - - @Test - public void testExecuteWithSystemClock() { - ServerTimeTool systemTool = new ServerTimeTool(Clock.systemUTC()); - Map arguments = Map.of(); - String result = systemTool.execute(arguments); - long timestamp = Long.parseLong(result); - long now = System.currentTimeMillis(); - assertThat(timestamp).isBetween(now - 1000, now + 1000); - } + private static final Instant FIXED_INSTANT = Instant.parse("2024-01-15T10:30:45.123Z"); + private static final Clock FIXED_CLOCK = Clock.fixed(FIXED_INSTANT, ZoneId.of("UTC")); + + private final ServerTimeTool tool = new ServerTimeTool(FIXED_CLOCK); + + @Test + public void testGetName() { + assertThat(tool.getName()).isEqualTo("server_time"); + } + + @Test + public void testGetDescription() { + assertThat(tool.getDescription()) + .isEqualTo("Returns the current timestamp according to the server's system clock"); + } + + @Test + public void testGetInputSchema() { + Map schema = tool.getInputSchema(); + assertThat(schema).containsKey("type"); + assertThat(schema).containsKey("properties"); + Map properties = (Map) schema.get("properties"); + assertThat(properties).isEmpty(); + } + + @Test + public void testExecuteReturnsExpectedTimestamp() { + Map arguments = Map.of(); + String result = tool.execute(arguments); + assertThat(result).isEqualTo(String.valueOf(FIXED_INSTANT.toEpochMilli())); + } + + @Test + public void testExecuteReturnsConsistentValue() { + Map arguments = Map.of(); + String result1 = tool.execute(arguments); + String result2 = tool.execute(arguments); + assertThat(result1).isEqualTo(result2); + } + + @Test + public void testExecuteWithSystemClock() { + ServerTimeTool systemTool = new ServerTimeTool(Clock.systemUTC()); + Map arguments = Map.of(); + String result = systemTool.execute(arguments); + long timestamp = Long.parseLong(result); + long now = System.currentTimeMillis(); + assertThat(timestamp).isBetween(now - 1000, now + 1000); + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/chessql/compiler/SqlCompilerTest.java b/jvm/src/test/java/com/muchq/one_d4/chessql/compiler/SqlCompilerTest.java index 8770f0fc..995d8156 100644 --- a/jvm/src/test/java/com/muchq/one_d4/chessql/compiler/SqlCompilerTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/chessql/compiler/SqlCompilerTest.java @@ -1,104 +1,103 @@ package com.muchq.one_d4.chessql.compiler; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import com.muchq.one_d4.chessql.ast.Expr; import com.muchq.one_d4.chessql.parser.Parser; -import org.junit.Test; - import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.Test; public class SqlCompilerTest { - private final SqlCompiler compiler = new SqlCompiler(); - - @Test - public void testSimpleComparison() { - CompiledQuery result = compile("white.elo >= 2500"); - assertThat(result.sql()).isEqualTo("white_elo >= ?"); - assertThat(result.parameters()).isEqualTo(List.of(2500)); - } - - @Test - public void testMotif() { - CompiledQuery result = compile("motif(fork)"); - assertThat(result.sql()).isEqualTo("has_fork = TRUE"); - assertThat(result.parameters()).isEmpty(); - } - - @Test - public void testAndExpression() { - CompiledQuery result = compile("white.elo >= 2500 AND motif(fork)"); - assertThat(result.sql()).isEqualTo("(white_elo >= ? AND has_fork = TRUE)"); - assertThat(result.parameters()).isEqualTo(List.of(2500)); - } - - @Test - public void testOrExpression() { - CompiledQuery result = compile("motif(fork) OR motif(pin)"); - assertThat(result.sql()).isEqualTo("(has_fork = TRUE OR has_pin = TRUE)"); - assertThat(result.parameters()).isEmpty(); - } - - @Test - public void testNotExpression() { - CompiledQuery result = compile("NOT motif(pin)"); - assertThat(result.sql()).isEqualTo("(NOT has_pin = TRUE)"); - assertThat(result.parameters()).isEmpty(); - } - - @Test - public void testInExpression() { - CompiledQuery result = compile("platform IN [\"lichess\", \"chess.com\"]"); - assertThat(result.sql()).isEqualTo("platform IN (?, ?)"); - assertThat(result.parameters()).isEqualTo(List.of("lichess", "chess.com")); - } - - @Test - public void testComplexQuery() { - CompiledQuery result = compile("white.elo >= 2500 AND motif(cross_pin)"); - assertThat(result.sql()).isEqualTo("(white_elo >= ? AND has_cross_pin = TRUE)"); - assertThat(result.parameters()).isEqualTo(List.of(2500)); - } - - @Test - public void testEndToEnd() { - CompiledQuery result = compile("white.elo >= 2500 AND motif(fork)"); - assertThat(result.sql()).isEqualTo("(white_elo >= ? AND has_fork = TRUE)"); - assertThat(result.parameters()).isEqualTo(List.of(2500)); - } - - @Test - public void testNestedBooleans() { - CompiledQuery result = compile("(motif(fork) OR motif(pin)) AND white.elo > 2000"); - assertThat(result.sql()).isEqualTo("((has_fork = TRUE OR has_pin = TRUE) AND white_elo > ?)"); - assertThat(result.parameters()).isEqualTo(List.of(2000)); - } - - @Test - public void testUnknownMotif() { - assertThatThrownBy(() -> compile("motif(unknown)")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown motif"); - } - - @Test - public void testUnknownField() { - assertThatThrownBy(() -> compile("bogus_field >= 100")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown field"); - } - - @Test - public void testDirectColumnName() { - CompiledQuery result = compile("white_elo >= 2500"); - assertThat(result.sql()).isEqualTo("white_elo >= ?"); - assertThat(result.parameters()).isEqualTo(List.of(2500)); - } - - private CompiledQuery compile(String input) { - Expr expr = Parser.parse(input); - return compiler.compile(expr); - } + private final SqlCompiler compiler = new SqlCompiler(); + + @Test + public void testSimpleComparison() { + CompiledQuery result = compile("white.elo >= 2500"); + assertThat(result.sql()).isEqualTo("white_elo >= ?"); + assertThat(result.parameters()).isEqualTo(List.of(2500)); + } + + @Test + public void testMotif() { + CompiledQuery result = compile("motif(fork)"); + assertThat(result.sql()).isEqualTo("has_fork = TRUE"); + assertThat(result.parameters()).isEmpty(); + } + + @Test + public void testAndExpression() { + CompiledQuery result = compile("white.elo >= 2500 AND motif(fork)"); + assertThat(result.sql()).isEqualTo("(white_elo >= ? AND has_fork = TRUE)"); + assertThat(result.parameters()).isEqualTo(List.of(2500)); + } + + @Test + public void testOrExpression() { + CompiledQuery result = compile("motif(fork) OR motif(pin)"); + assertThat(result.sql()).isEqualTo("(has_fork = TRUE OR has_pin = TRUE)"); + assertThat(result.parameters()).isEmpty(); + } + + @Test + public void testNotExpression() { + CompiledQuery result = compile("NOT motif(pin)"); + assertThat(result.sql()).isEqualTo("(NOT has_pin = TRUE)"); + assertThat(result.parameters()).isEmpty(); + } + + @Test + public void testInExpression() { + CompiledQuery result = compile("platform IN [\"lichess\", \"chess.com\"]"); + assertThat(result.sql()).isEqualTo("platform IN (?, ?)"); + assertThat(result.parameters()).isEqualTo(List.of("lichess", "chess.com")); + } + + @Test + public void testComplexQuery() { + CompiledQuery result = compile("white.elo >= 2500 AND motif(cross_pin)"); + assertThat(result.sql()).isEqualTo("(white_elo >= ? AND has_cross_pin = TRUE)"); + assertThat(result.parameters()).isEqualTo(List.of(2500)); + } + + @Test + public void testEndToEnd() { + CompiledQuery result = compile("white.elo >= 2500 AND motif(fork)"); + assertThat(result.sql()).isEqualTo("(white_elo >= ? AND has_fork = TRUE)"); + assertThat(result.parameters()).isEqualTo(List.of(2500)); + } + + @Test + public void testNestedBooleans() { + CompiledQuery result = compile("(motif(fork) OR motif(pin)) AND white.elo > 2000"); + assertThat(result.sql()).isEqualTo("((has_fork = TRUE OR has_pin = TRUE) AND white_elo > ?)"); + assertThat(result.parameters()).isEqualTo(List.of(2000)); + } + + @Test + public void testUnknownMotif() { + assertThatThrownBy(() -> compile("motif(unknown)")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown motif"); + } + + @Test + public void testUnknownField() { + assertThatThrownBy(() -> compile("bogus_field >= 100")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown field"); + } + + @Test + public void testDirectColumnName() { + CompiledQuery result = compile("white_elo >= 2500"); + assertThat(result.sql()).isEqualTo("white_elo >= ?"); + assertThat(result.parameters()).isEqualTo(List.of(2500)); + } + + private CompiledQuery compile(String input) { + Expr expr = Parser.parse(input); + return compiler.compile(expr); + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/chessql/lexer/LexerTest.java b/jvm/src/test/java/com/muchq/one_d4/chessql/lexer/LexerTest.java index 82c58086..fda859fb 100644 --- a/jvm/src/test/java/com/muchq/one_d4/chessql/lexer/LexerTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/chessql/lexer/LexerTest.java @@ -1,91 +1,90 @@ package com.muchq.one_d4.chessql.lexer; -import org.junit.Test; - -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.List; +import org.junit.Test; + public class LexerTest { - @Test - public void testSimpleComparison() { - List tokens = new Lexer("white_elo >= 2500").tokenize(); - assertThat(tokens).hasSize(4); // IDENTIFIER, GTE, NUMBER, EOF - assertThat(tokens.get(0).type()).isEqualTo(TokenType.IDENTIFIER); - assertThat(tokens.get(0).value()).isEqualTo("white_elo"); - assertThat(tokens.get(1).type()).isEqualTo(TokenType.GTE); - assertThat(tokens.get(2).type()).isEqualTo(TokenType.NUMBER); - assertThat(tokens.get(2).value()).isEqualTo("2500"); - assertThat(tokens.get(3).type()).isEqualTo(TokenType.EOF); - } + @Test + public void testSimpleComparison() { + List tokens = new Lexer("white_elo >= 2500").tokenize(); + assertThat(tokens).hasSize(4); // IDENTIFIER, GTE, NUMBER, EOF + assertThat(tokens.get(0).type()).isEqualTo(TokenType.IDENTIFIER); + assertThat(tokens.get(0).value()).isEqualTo("white_elo"); + assertThat(tokens.get(1).type()).isEqualTo(TokenType.GTE); + assertThat(tokens.get(2).type()).isEqualTo(TokenType.NUMBER); + assertThat(tokens.get(2).value()).isEqualTo("2500"); + assertThat(tokens.get(3).type()).isEqualTo(TokenType.EOF); + } - @Test - public void testMotifExpression() { - List tokens = new Lexer("motif(fork)").tokenize(); - assertThat(tokens).hasSize(5); // MOTIF, LPAREN, IDENTIFIER, RPAREN, EOF - assertThat(tokens.get(0).type()).isEqualTo(TokenType.MOTIF); - assertThat(tokens.get(1).type()).isEqualTo(TokenType.LPAREN); - assertThat(tokens.get(2).type()).isEqualTo(TokenType.IDENTIFIER); - assertThat(tokens.get(2).value()).isEqualTo("fork"); - assertThat(tokens.get(3).type()).isEqualTo(TokenType.RPAREN); - } + @Test + public void testMotifExpression() { + List tokens = new Lexer("motif(fork)").tokenize(); + assertThat(tokens).hasSize(5); // MOTIF, LPAREN, IDENTIFIER, RPAREN, EOF + assertThat(tokens.get(0).type()).isEqualTo(TokenType.MOTIF); + assertThat(tokens.get(1).type()).isEqualTo(TokenType.LPAREN); + assertThat(tokens.get(2).type()).isEqualTo(TokenType.IDENTIFIER); + assertThat(tokens.get(2).value()).isEqualTo("fork"); + assertThat(tokens.get(3).type()).isEqualTo(TokenType.RPAREN); + } - @Test - public void testKeywords() { - List tokens = new Lexer("AND OR NOT IN motif").tokenize(); - assertThat(tokens.get(0).type()).isEqualTo(TokenType.AND); - assertThat(tokens.get(1).type()).isEqualTo(TokenType.OR); - assertThat(tokens.get(2).type()).isEqualTo(TokenType.NOT); - assertThat(tokens.get(3).type()).isEqualTo(TokenType.IN); - assertThat(tokens.get(4).type()).isEqualTo(TokenType.MOTIF); - } + @Test + public void testKeywords() { + List tokens = new Lexer("AND OR NOT IN motif").tokenize(); + assertThat(tokens.get(0).type()).isEqualTo(TokenType.AND); + assertThat(tokens.get(1).type()).isEqualTo(TokenType.OR); + assertThat(tokens.get(2).type()).isEqualTo(TokenType.NOT); + assertThat(tokens.get(3).type()).isEqualTo(TokenType.IN); + assertThat(tokens.get(4).type()).isEqualTo(TokenType.MOTIF); + } - @Test - public void testStringLiteral() { - List tokens = new Lexer("\"chess.com\"").tokenize(); - assertThat(tokens.get(0).type()).isEqualTo(TokenType.STRING); - assertThat(tokens.get(0).value()).isEqualTo("chess.com"); - } + @Test + public void testStringLiteral() { + List tokens = new Lexer("\"chess.com\"").tokenize(); + assertThat(tokens.get(0).type()).isEqualTo(TokenType.STRING); + assertThat(tokens.get(0).value()).isEqualTo("chess.com"); + } - @Test - public void testAllOperators() { - List tokens = new Lexer("= != < <= > >=").tokenize(); - assertThat(tokens.get(0).type()).isEqualTo(TokenType.EQ); - assertThat(tokens.get(1).type()).isEqualTo(TokenType.NEQ); - assertThat(tokens.get(2).type()).isEqualTo(TokenType.LT); - assertThat(tokens.get(3).type()).isEqualTo(TokenType.LTE); - assertThat(tokens.get(4).type()).isEqualTo(TokenType.GT); - assertThat(tokens.get(5).type()).isEqualTo(TokenType.GTE); - } + @Test + public void testAllOperators() { + List tokens = new Lexer("= != < <= > >=").tokenize(); + assertThat(tokens.get(0).type()).isEqualTo(TokenType.EQ); + assertThat(tokens.get(1).type()).isEqualTo(TokenType.NEQ); + assertThat(tokens.get(2).type()).isEqualTo(TokenType.LT); + assertThat(tokens.get(3).type()).isEqualTo(TokenType.LTE); + assertThat(tokens.get(4).type()).isEqualTo(TokenType.GT); + assertThat(tokens.get(5).type()).isEqualTo(TokenType.GTE); + } - @Test - public void testDottedField() { - List tokens = new Lexer("white.elo").tokenize(); - assertThat(tokens.get(0).type()).isEqualTo(TokenType.IDENTIFIER); - assertThat(tokens.get(0).value()).isEqualTo("white"); - assertThat(tokens.get(1).type()).isEqualTo(TokenType.DOT); - assertThat(tokens.get(2).type()).isEqualTo(TokenType.IDENTIFIER); - assertThat(tokens.get(2).value()).isEqualTo("elo"); - } + @Test + public void testDottedField() { + List tokens = new Lexer("white.elo").tokenize(); + assertThat(tokens.get(0).type()).isEqualTo(TokenType.IDENTIFIER); + assertThat(tokens.get(0).value()).isEqualTo("white"); + assertThat(tokens.get(1).type()).isEqualTo(TokenType.DOT); + assertThat(tokens.get(2).type()).isEqualTo(TokenType.IDENTIFIER); + assertThat(tokens.get(2).value()).isEqualTo("elo"); + } - @Test - public void testInWithBrackets() { - List tokens = new Lexer("platform IN [\"a\", \"b\"]").tokenize(); - assertThat(tokens.get(0).type()).isEqualTo(TokenType.IDENTIFIER); - assertThat(tokens.get(1).type()).isEqualTo(TokenType.IN); - assertThat(tokens.get(2).type()).isEqualTo(TokenType.LBRACKET); - assertThat(tokens.get(3).type()).isEqualTo(TokenType.STRING); - assertThat(tokens.get(4).type()).isEqualTo(TokenType.COMMA); - assertThat(tokens.get(5).type()).isEqualTo(TokenType.STRING); - assertThat(tokens.get(6).type()).isEqualTo(TokenType.RBRACKET); - } + @Test + public void testInWithBrackets() { + List tokens = new Lexer("platform IN [\"a\", \"b\"]").tokenize(); + assertThat(tokens.get(0).type()).isEqualTo(TokenType.IDENTIFIER); + assertThat(tokens.get(1).type()).isEqualTo(TokenType.IN); + assertThat(tokens.get(2).type()).isEqualTo(TokenType.LBRACKET); + assertThat(tokens.get(3).type()).isEqualTo(TokenType.STRING); + assertThat(tokens.get(4).type()).isEqualTo(TokenType.COMMA); + assertThat(tokens.get(5).type()).isEqualTo(TokenType.STRING); + assertThat(tokens.get(6).type()).isEqualTo(TokenType.RBRACKET); + } - @Test - public void testUnterminatedString() { - assertThatThrownBy(() -> new Lexer("\"unterminated").tokenize()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unterminated string"); - } + @Test + public void testUnterminatedString() { + assertThatThrownBy(() -> new Lexer("\"unterminated").tokenize()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unterminated string"); + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/chessql/parser/ParserTest.java b/jvm/src/test/java/com/muchq/one_d4/chessql/parser/ParserTest.java index 17850c38..b8747c11 100644 --- a/jvm/src/test/java/com/muchq/one_d4/chessql/parser/ParserTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/chessql/parser/ParserTest.java @@ -1,5 +1,8 @@ package com.muchq.one_d4.chessql.parser; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import com.muchq.one_d4.chessql.ast.AndExpr; import com.muchq.one_d4.chessql.ast.ComparisonExpr; import com.muchq.one_d4.chessql.ast.Expr; @@ -7,118 +10,113 @@ import com.muchq.one_d4.chessql.ast.MotifExpr; import com.muchq.one_d4.chessql.ast.NotExpr; import com.muchq.one_d4.chessql.ast.OrExpr; -import org.junit.Test; - import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.Test; public class ParserTest { - @Test - public void testSimpleComparison() { - Expr expr = Parser.parse("white_elo >= 2500"); - assertThat(expr).isInstanceOf(ComparisonExpr.class); - ComparisonExpr cmp = (ComparisonExpr) expr; - assertThat(cmp.field()).isEqualTo("white_elo"); - assertThat(cmp.operator()).isEqualTo(">="); - assertThat(cmp.value()).isEqualTo(2500); - } - - @Test - public void testDottedFieldComparison() { - Expr expr = Parser.parse("white.elo >= 2500"); - assertThat(expr).isInstanceOf(ComparisonExpr.class); - ComparisonExpr cmp = (ComparisonExpr) expr; - assertThat(cmp.field()).isEqualTo("white.elo"); - assertThat(cmp.operator()).isEqualTo(">="); - assertThat(cmp.value()).isEqualTo(2500); - } - - @Test - public void testMotifExpression() { - Expr expr = Parser.parse("motif(fork)"); - assertThat(expr).isInstanceOf(MotifExpr.class); - assertThat(((MotifExpr) expr).motifName()).isEqualTo("fork"); - } - - @Test - public void testAndExpression() { - Expr expr = Parser.parse("white.elo >= 2500 AND motif(cross_pin)"); - assertThat(expr).isInstanceOf(AndExpr.class); - AndExpr and = (AndExpr) expr; - assertThat(and.operands()).hasSize(2); - assertThat(and.operands().get(0)).isInstanceOf(ComparisonExpr.class); - assertThat(and.operands().get(1)).isInstanceOf(MotifExpr.class); - } - - @Test - public void testOrExpression() { - Expr expr = Parser.parse("motif(fork) OR motif(pin)"); - assertThat(expr).isInstanceOf(OrExpr.class); - OrExpr or = (OrExpr) expr; - assertThat(or.operands()).hasSize(2); - } - - @Test - public void testNotExpression() { - Expr expr = Parser.parse("NOT motif(pin)"); - assertThat(expr).isInstanceOf(NotExpr.class); - NotExpr not = (NotExpr) expr; - assertThat(not.operand()).isInstanceOf(MotifExpr.class); - } - - @Test - public void testInExpression() { - Expr expr = Parser.parse("platform IN [\"lichess\", \"chess.com\"]"); - assertThat(expr).isInstanceOf(InExpr.class); - InExpr in = (InExpr) expr; - assertThat(in.field()).isEqualTo("platform"); - assertThat(in.values()).isEqualTo(List.of("lichess", "chess.com")); - } - - @Test - public void testComplexExpression() { - Expr expr = Parser.parse("white.elo >= 2500 AND motif(fork) AND NOT motif(pin)"); - assertThat(expr).isInstanceOf(AndExpr.class); - AndExpr and = (AndExpr) expr; - assertThat(and.operands()).hasSize(3); - assertThat(and.operands().get(2)).isInstanceOf(NotExpr.class); - } - - @Test - public void testParenthesizedExpression() { - Expr expr = Parser.parse("(motif(fork) OR motif(pin)) AND white.elo > 2000"); - assertThat(expr).isInstanceOf(AndExpr.class); - AndExpr and = (AndExpr) expr; - assertThat(and.operands().get(0)).isInstanceOf(OrExpr.class); - assertThat(and.operands().get(1)).isInstanceOf(ComparisonExpr.class); - } - - @Test - public void testPrecedence() { - // AND binds tighter than OR - Expr expr = Parser.parse("motif(fork) OR motif(pin) AND white.elo > 2000"); - assertThat(expr).isInstanceOf(OrExpr.class); - OrExpr or = (OrExpr) expr; - assertThat(or.operands()).hasSize(2); - assertThat(or.operands().get(0)).isInstanceOf(MotifExpr.class); - assertThat(or.operands().get(1)).isInstanceOf(AndExpr.class); - } - - @Test - public void testStringComparison() { - Expr expr = Parser.parse("eco = \"B90\""); - assertThat(expr).isInstanceOf(ComparisonExpr.class); - ComparisonExpr cmp = (ComparisonExpr) expr; - assertThat(cmp.field()).isEqualTo("eco"); - assertThat(cmp.value()).isEqualTo("B90"); - } - - @Test - public void testParseError() { - assertThatThrownBy(() -> Parser.parse("AND")) - .isInstanceOf(ParseException.class); - } + @Test + public void testSimpleComparison() { + Expr expr = Parser.parse("white_elo >= 2500"); + assertThat(expr).isInstanceOf(ComparisonExpr.class); + ComparisonExpr cmp = (ComparisonExpr) expr; + assertThat(cmp.field()).isEqualTo("white_elo"); + assertThat(cmp.operator()).isEqualTo(">="); + assertThat(cmp.value()).isEqualTo(2500); + } + + @Test + public void testDottedFieldComparison() { + Expr expr = Parser.parse("white.elo >= 2500"); + assertThat(expr).isInstanceOf(ComparisonExpr.class); + ComparisonExpr cmp = (ComparisonExpr) expr; + assertThat(cmp.field()).isEqualTo("white.elo"); + assertThat(cmp.operator()).isEqualTo(">="); + assertThat(cmp.value()).isEqualTo(2500); + } + + @Test + public void testMotifExpression() { + Expr expr = Parser.parse("motif(fork)"); + assertThat(expr).isInstanceOf(MotifExpr.class); + assertThat(((MotifExpr) expr).motifName()).isEqualTo("fork"); + } + + @Test + public void testAndExpression() { + Expr expr = Parser.parse("white.elo >= 2500 AND motif(cross_pin)"); + assertThat(expr).isInstanceOf(AndExpr.class); + AndExpr and = (AndExpr) expr; + assertThat(and.operands()).hasSize(2); + assertThat(and.operands().get(0)).isInstanceOf(ComparisonExpr.class); + assertThat(and.operands().get(1)).isInstanceOf(MotifExpr.class); + } + + @Test + public void testOrExpression() { + Expr expr = Parser.parse("motif(fork) OR motif(pin)"); + assertThat(expr).isInstanceOf(OrExpr.class); + OrExpr or = (OrExpr) expr; + assertThat(or.operands()).hasSize(2); + } + + @Test + public void testNotExpression() { + Expr expr = Parser.parse("NOT motif(pin)"); + assertThat(expr).isInstanceOf(NotExpr.class); + NotExpr not = (NotExpr) expr; + assertThat(not.operand()).isInstanceOf(MotifExpr.class); + } + + @Test + public void testInExpression() { + Expr expr = Parser.parse("platform IN [\"lichess\", \"chess.com\"]"); + assertThat(expr).isInstanceOf(InExpr.class); + InExpr in = (InExpr) expr; + assertThat(in.field()).isEqualTo("platform"); + assertThat(in.values()).isEqualTo(List.of("lichess", "chess.com")); + } + + @Test + public void testComplexExpression() { + Expr expr = Parser.parse("white.elo >= 2500 AND motif(fork) AND NOT motif(pin)"); + assertThat(expr).isInstanceOf(AndExpr.class); + AndExpr and = (AndExpr) expr; + assertThat(and.operands()).hasSize(3); + assertThat(and.operands().get(2)).isInstanceOf(NotExpr.class); + } + + @Test + public void testParenthesizedExpression() { + Expr expr = Parser.parse("(motif(fork) OR motif(pin)) AND white.elo > 2000"); + assertThat(expr).isInstanceOf(AndExpr.class); + AndExpr and = (AndExpr) expr; + assertThat(and.operands().get(0)).isInstanceOf(OrExpr.class); + assertThat(and.operands().get(1)).isInstanceOf(ComparisonExpr.class); + } + + @Test + public void testPrecedence() { + // AND binds tighter than OR + Expr expr = Parser.parse("motif(fork) OR motif(pin) AND white.elo > 2000"); + assertThat(expr).isInstanceOf(OrExpr.class); + OrExpr or = (OrExpr) expr; + assertThat(or.operands()).hasSize(2); + assertThat(or.operands().get(0)).isInstanceOf(MotifExpr.class); + assertThat(or.operands().get(1)).isInstanceOf(AndExpr.class); + } + + @Test + public void testStringComparison() { + Expr expr = Parser.parse("eco = \"B90\""); + assertThat(expr).isInstanceOf(ComparisonExpr.class); + ComparisonExpr cmp = (ComparisonExpr) expr; + assertThat(cmp.field()).isEqualTo("eco"); + assertThat(cmp.value()).isEqualTo("B90"); + } + + @Test + public void testParseError() { + assertThatThrownBy(() -> Parser.parse("AND")).isInstanceOf(ParseException.class); + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/engine/GameReplayerTest.java b/jvm/src/test/java/com/muchq/one_d4/engine/GameReplayerTest.java index 2eb8bc68..4a9195bd 100644 --- a/jvm/src/test/java/com/muchq/one_d4/engine/GameReplayerTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/engine/GameReplayerTest.java @@ -1,197 +1,198 @@ package com.muchq.one_d4.engine; -import com.muchq.one_d4.engine.model.PositionContext; -import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; +import com.muchq.one_d4.engine.model.PositionContext; import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; public class GameReplayerTest { - private final GameReplayer replayer = new GameReplayer(); - - @Test - public void testEmptyMoveText() { - List positions = replayer.replay(""); - - assertThat(positions).hasSize(1); - assertThat(positions.get(0).moveNumber()).isEqualTo(0); - assertThat(positions.get(0).whiteToMove()).isTrue(); - assertThat(positions.get(0).fen()).startsWith("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"); - } - - @Test - public void testSingleMove() { - List positions = replayer.replay("1. e4"); - - assertThat(positions).hasSize(2); - - // Initial position - assertThat(positions.get(0).moveNumber()).isEqualTo(0); - assertThat(positions.get(0).whiteToMove()).isTrue(); - - // After 1. e4 - assertThat(positions.get(1).moveNumber()).isEqualTo(1); - assertThat(positions.get(1).whiteToMove()).isFalse(); - assertThat(positions.get(1).fen()).contains("PPPP1PPP"); // pawn moved from e2 - } - - @Test - public void testOpeningMoves() { - List positions = replayer.replay("1. e4 e5 2. Nf3 Nc6 3. Bb5"); - - assertThat(positions).hasSize(6); // initial + 5 moves - - // After 1. e4 - move 1, black to move - assertThat(positions.get(1).moveNumber()).isEqualTo(1); - assertThat(positions.get(1).whiteToMove()).isFalse(); - - // After 1... e5 - move 2 (incremented after black moves), white to move - assertThat(positions.get(2).moveNumber()).isEqualTo(2); - assertThat(positions.get(2).whiteToMove()).isTrue(); - - // After 2. Nf3 - still move 2, black to move - assertThat(positions.get(3).moveNumber()).isEqualTo(2); - assertThat(positions.get(3).whiteToMove()).isFalse(); - - // After 3. Bb5 (Ruy Lopez) - move 3, black to move - assertThat(positions.get(5).moveNumber()).isEqualTo(3); - assertThat(positions.get(5).whiteToMove()).isFalse(); - assertThat(positions.get(5).fen()).contains("B"); // bishop on b5 - } - - @Test - public void testCastlingKingside() { - List positions = replayer.replay("1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. O-O"); - - PositionContext afterCastle = positions.get(positions.size() - 1); - // After O-O, king should be on g1 and rook on f1 - // FEN back rank: RNBQ1RK1 (Rook a1, Knight b1, Bishop c1, Queen d1, empty e1, Rook f1, King g1, empty h1) - assertThat(afterCastle.fen()).contains("1RK1 "); // king on g1, rook on f1, h1 empty - } - - @Test - public void testCastlingQueenside() { - List positions = replayer.replay( - "1. d4 d5 2. c4 e6 3. Nc3 Nf6 4. Bg5 Be7 5. e3 O-O 6. Nf3 Nbd7 7. Qc2 c6 8. O-O-O"); - - PositionContext afterCastle = positions.get(positions.size() - 1); - // After O-O-O, white king on c1, rook on d1 - assertThat(afterCastle.fen()).contains("2KR"); // king on c1, rook on d1 - } - - @Test - public void testCapture() { - List positions = replayer.replay("1. e4 d5 2. exd5"); - - PositionContext afterCapture = positions.get(positions.size() - 1); - assertThat(afterCapture.fen()).contains("3P4"); // pawn now on d5 - } - - @Test - public void testPromotion() { - // Simplified position where pawn promotes - List positions = replayer.replay( - "1. e4 d5 2. e5 d4 3. e6 d3 4. exf7+ Kd7 5. fxg8=Q"); - - PositionContext afterPromotion = positions.get(positions.size() - 1); - assertThat(afterPromotion.fen()).contains("Q"); // promoted queen - } - - @Test - public void testCheckNotation() { - List positions = replayer.replay("1. e4 e5 2. Qh5 Nc6 3. Bc4 Nf6 4. Qxf7+"); - - // Should handle + notation without issue - assertThat(positions).hasSize(8); - } - - @Test - public void testCheckmateNotation() { - // Scholar's mate - List positions = replayer.replay("1. e4 e5 2. Qh5 Nc6 3. Bc4 Nf6 4. Qxf7#"); - - assertThat(positions).hasSize(8); - // The move with # should be processed correctly - PositionContext finalPos = positions.get(positions.size() - 1); - assertThat(finalPos.moveNumber()).isEqualTo(4); - } - - @Test - public void testStripsComments() { - List positions = replayer.replay( - "1. e4 {Best by test} e5 {Solid reply} 2. Nf3"); - - assertThat(positions).hasSize(4); // initial + 3 moves - } - - @Test - public void testStripsVariations() { - List positions = replayer.replay( - "1. e4 e5 (1... c5 2. Nf3) 2. Nf3"); - - assertThat(positions).hasSize(4); // initial + 3 moves (variation excluded) - } - - @Test - public void testStripsNagAnnotations() { - List positions = replayer.replay( - "1. e4 $1 e5 $2 2. Nf3 $10"); - - assertThat(positions).hasSize(4); - } - - @Test - public void testIgnoresResultIndicator() { - List positions = replayer.replay("1. e4 e5 2. Nf3 Nc6 1-0"); - - assertThat(positions).hasSize(5); // initial + 4 moves, not including result - } - - @Test - public void testMoveNumbersWithoutSpaces() { - List positions = replayer.replay("1.e4 e5 2.Nf3 Nc6"); - - assertThat(positions).hasSize(5); - } - - @Test - public void testPawnMoveWithoutPieceIndicator() { - List positions = replayer.replay("1. d4 d5 2. c4 e6"); - - assertThat(positions).hasSize(5); - // Verify pawns moved correctly - PositionContext afterD4 = positions.get(1); - assertThat(afterD4.fen()).contains("3P4"); // d4 pawn - } - - @Test - public void testAmbiguousMoveWithFile() { - // Position where two knights can go to same square - List positions = replayer.replay( - "1. e4 e5 2. Nf3 Nc6 3. Nc3 Nf6 4. Nd5 Nxd5 5. exd5 Nd4 6. Nxd4"); - - assertThat(positions).hasSize(12); - } - - @Test - public void testLongerGame() { - // Test that a typical opening sequence replays correctly - List positions = replayer.replay( - "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O"); - - assertThat(positions).hasSize(17); // initial + 16 half-moves - } - - @Test - public void testAmbiguousMoveWithRank() { - // After move 10, white has Ra3 and Rf1. Both can reach a1 (back rank is clear - // since Nc3, Be3, Qd2 have moved those pieces). R3a1 uses rank disambiguation. - List positions = replayer.replay( - "1. e4 e5 2. d3 d6 3. Nf3 Nf6 4. Nc3 Nc6 5. Be2 Be7 6. O-O O-O " + - "7. Be3 Be6 8. Qd2 Qd7 9. a4 a5 10. Ra3 Ra6 11. R3a1"); - - assertThat(positions).hasSize(22); // initial + 21 half-moves - } + private final GameReplayer replayer = new GameReplayer(); + + @Test + public void testEmptyMoveText() { + List positions = replayer.replay(""); + + assertThat(positions).hasSize(1); + assertThat(positions.get(0).moveNumber()).isEqualTo(0); + assertThat(positions.get(0).whiteToMove()).isTrue(); + assertThat(positions.get(0).fen()).startsWith("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"); + } + + @Test + public void testSingleMove() { + List positions = replayer.replay("1. e4"); + + assertThat(positions).hasSize(2); + + // Initial position + assertThat(positions.get(0).moveNumber()).isEqualTo(0); + assertThat(positions.get(0).whiteToMove()).isTrue(); + + // After 1. e4 + assertThat(positions.get(1).moveNumber()).isEqualTo(1); + assertThat(positions.get(1).whiteToMove()).isFalse(); + assertThat(positions.get(1).fen()).contains("PPPP1PPP"); // pawn moved from e2 + } + + @Test + public void testOpeningMoves() { + List positions = replayer.replay("1. e4 e5 2. Nf3 Nc6 3. Bb5"); + + assertThat(positions).hasSize(6); // initial + 5 moves + + // After 1. e4 - move 1, black to move + assertThat(positions.get(1).moveNumber()).isEqualTo(1); + assertThat(positions.get(1).whiteToMove()).isFalse(); + + // After 1... e5 - move 2 (incremented after black moves), white to move + assertThat(positions.get(2).moveNumber()).isEqualTo(2); + assertThat(positions.get(2).whiteToMove()).isTrue(); + + // After 2. Nf3 - still move 2, black to move + assertThat(positions.get(3).moveNumber()).isEqualTo(2); + assertThat(positions.get(3).whiteToMove()).isFalse(); + + // After 3. Bb5 (Ruy Lopez) - move 3, black to move + assertThat(positions.get(5).moveNumber()).isEqualTo(3); + assertThat(positions.get(5).whiteToMove()).isFalse(); + assertThat(positions.get(5).fen()).contains("B"); // bishop on b5 + } + + @Test + public void testCastlingKingside() { + List positions = replayer.replay("1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. O-O"); + + PositionContext afterCastle = positions.get(positions.size() - 1); + // After O-O, king should be on g1 and rook on f1 + // FEN back rank: RNBQ1RK1 (Rook a1, Knight b1, Bishop c1, Queen d1, empty e1, Rook f1, King g1, + // empty h1) + assertThat(afterCastle.fen()).contains("1RK1 "); // king on g1, rook on f1, h1 empty + } + + @Test + public void testCastlingQueenside() { + List positions = + replayer.replay( + "1. d4 d5 2. c4 e6 3. Nc3 Nf6 4. Bg5 Be7 5. e3 O-O 6. Nf3 Nbd7 7. Qc2 c6 8. O-O-O"); + + PositionContext afterCastle = positions.get(positions.size() - 1); + // After O-O-O, white king on c1, rook on d1 + assertThat(afterCastle.fen()).contains("2KR"); // king on c1, rook on d1 + } + + @Test + public void testCapture() { + List positions = replayer.replay("1. e4 d5 2. exd5"); + + PositionContext afterCapture = positions.get(positions.size() - 1); + assertThat(afterCapture.fen()).contains("3P4"); // pawn now on d5 + } + + @Test + public void testPromotion() { + // Simplified position where pawn promotes + List positions = + replayer.replay("1. e4 d5 2. e5 d4 3. e6 d3 4. exf7+ Kd7 5. fxg8=Q"); + + PositionContext afterPromotion = positions.get(positions.size() - 1); + assertThat(afterPromotion.fen()).contains("Q"); // promoted queen + } + + @Test + public void testCheckNotation() { + List positions = replayer.replay("1. e4 e5 2. Qh5 Nc6 3. Bc4 Nf6 4. Qxf7+"); + + // Should handle + notation without issue + assertThat(positions).hasSize(8); + } + + @Test + public void testCheckmateNotation() { + // Scholar's mate + List positions = replayer.replay("1. e4 e5 2. Qh5 Nc6 3. Bc4 Nf6 4. Qxf7#"); + + assertThat(positions).hasSize(8); + // The move with # should be processed correctly + PositionContext finalPos = positions.get(positions.size() - 1); + assertThat(finalPos.moveNumber()).isEqualTo(4); + } + + @Test + public void testStripsComments() { + List positions = + replayer.replay("1. e4 {Best by test} e5 {Solid reply} 2. Nf3"); + + assertThat(positions).hasSize(4); // initial + 3 moves + } + + @Test + public void testStripsVariations() { + List positions = replayer.replay("1. e4 e5 (1... c5 2. Nf3) 2. Nf3"); + + assertThat(positions).hasSize(4); // initial + 3 moves (variation excluded) + } + + @Test + public void testStripsNagAnnotations() { + List positions = replayer.replay("1. e4 $1 e5 $2 2. Nf3 $10"); + + assertThat(positions).hasSize(4); + } + + @Test + public void testIgnoresResultIndicator() { + List positions = replayer.replay("1. e4 e5 2. Nf3 Nc6 1-0"); + + assertThat(positions).hasSize(5); // initial + 4 moves, not including result + } + + @Test + public void testMoveNumbersWithoutSpaces() { + List positions = replayer.replay("1.e4 e5 2.Nf3 Nc6"); + + assertThat(positions).hasSize(5); + } + + @Test + public void testPawnMoveWithoutPieceIndicator() { + List positions = replayer.replay("1. d4 d5 2. c4 e6"); + + assertThat(positions).hasSize(5); + // Verify pawns moved correctly + PositionContext afterD4 = positions.get(1); + assertThat(afterD4.fen()).contains("3P4"); // d4 pawn + } + + @Test + public void testAmbiguousMoveWithFile() { + // Position where two knights can go to same square + List positions = + replayer.replay("1. e4 e5 2. Nf3 Nc6 3. Nc3 Nf6 4. Nd5 Nxd5 5. exd5 Nd4 6. Nxd4"); + + assertThat(positions).hasSize(12); + } + + @Test + public void testLongerGame() { + // Test that a typical opening sequence replays correctly + List positions = + replayer.replay( + "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O"); + + assertThat(positions).hasSize(17); // initial + 16 half-moves + } + + @Test + public void testAmbiguousMoveWithRank() { + // After move 10, white has Ra3 and Rf1. Both can reach a1 (back rank is clear + // since Nc3, Be3, Qd2 have moved those pieces). R3a1 uses rank disambiguation. + List positions = + replayer.replay( + "1. e4 e5 2. d3 d6 3. Nf3 Nf6 4. Nc3 Nc6 5. Be2 Be7 6. O-O O-O " + + "7. Be3 Be6 8. Qd2 Qd7 9. a4 a5 10. Ra3 Ra6 11. R3a1"); + + assertThat(positions).hasSize(22); // initial + 21 half-moves + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/engine/PgnParserTest.java b/jvm/src/test/java/com/muchq/one_d4/engine/PgnParserTest.java index f38f3e23..f5b0ca51 100644 --- a/jvm/src/test/java/com/muchq/one_d4/engine/PgnParserTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/engine/PgnParserTest.java @@ -1,66 +1,69 @@ package com.muchq.one_d4.engine; +import static org.assertj.core.api.Assertions.assertThat; + import com.muchq.one_d4.engine.model.ParsedGame; import org.junit.Test; -import static org.assertj.core.api.Assertions.assertThat; - public class PgnParserTest { - private final PgnParser parser = new PgnParser(); + private final PgnParser parser = new PgnParser(); - @Test - public void testParseHeaders() { - String pgn = """ - [Event "Live Chess"] - [Site "Chess.com"] - [White "hikaru"] - [Black "magnuscarlsen"] - [Result "1-0"] - [ECO "B90"] - [WhiteElo "2850"] - [BlackElo "2830"] + @Test + public void testParseHeaders() { + String pgn = + """ + [Event "Live Chess"] + [Site "Chess.com"] + [White "hikaru"] + [Black "magnuscarlsen"] + [Result "1-0"] + [ECO "B90"] + [WhiteElo "2850"] + [BlackElo "2830"] - 1. e4 c5 2. Nf3 d6 3. d4 cxd4 4. Nxd4 Nf6 5. Nc3 a6 1-0 - """; + 1. e4 c5 2. Nf3 d6 3. d4 cxd4 4. Nxd4 Nf6 5. Nc3 a6 1-0 + """; - ParsedGame game = parser.parse(pgn); - assertThat(game.headers()).containsEntry("Event", "Live Chess"); - assertThat(game.headers()).containsEntry("White", "hikaru"); - assertThat(game.headers()).containsEntry("Black", "magnuscarlsen"); - assertThat(game.headers()).containsEntry("ECO", "B90"); - assertThat(game.headers()).containsEntry("WhiteElo", "2850"); - } + ParsedGame game = parser.parse(pgn); + assertThat(game.headers()).containsEntry("Event", "Live Chess"); + assertThat(game.headers()).containsEntry("White", "hikaru"); + assertThat(game.headers()).containsEntry("Black", "magnuscarlsen"); + assertThat(game.headers()).containsEntry("ECO", "B90"); + assertThat(game.headers()).containsEntry("WhiteElo", "2850"); + } - @Test - public void testParseMoveText() { - String pgn = """ - [Event "Test"] + @Test + public void testParseMoveText() { + String pgn = + """ + [Event "Test"] - 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 1/2-1/2 - """; + 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 1/2-1/2 + """; - ParsedGame game = parser.parse(pgn); - assertThat(game.moveText()).contains("1. e4 e5"); - assertThat(game.moveText()).contains("Bb5"); - } + ParsedGame game = parser.parse(pgn); + assertThat(game.moveText()).contains("1. e4 e5"); + assertThat(game.moveText()).contains("Bb5"); + } - @Test - public void testEmptyPgn() { - ParsedGame game = parser.parse(""); - assertThat(game.headers()).isEmpty(); - assertThat(game.moveText()).isEmpty(); - } + @Test + public void testEmptyPgn() { + ParsedGame game = parser.parse(""); + assertThat(game.headers()).isEmpty(); + assertThat(game.moveText()).isEmpty(); + } - @Test - public void testHeadersOnly() { - String pgn = """ - [Event "Test"] - [White "player1"] - """; + @Test + public void testHeadersOnly() { + String pgn = + """ + [Event "Test"] + [White "player1"] + """; - ParsedGame game = parser.parse(pgn); - assertThat(game.headers()).hasSize(2); - assertThat(game.moveText()).isEmpty(); - } + ParsedGame game = parser.parse(pgn); + assertThat(game.headers()).hasSize(2); + assertThat(game.moveText()).isEmpty(); + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/motifs/ForkDetectorTest.java b/jvm/src/test/java/com/muchq/one_d4/motifs/ForkDetectorTest.java index b807f38f..6b11bf31 100644 --- a/jvm/src/test/java/com/muchq/one_d4/motifs/ForkDetectorTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/motifs/ForkDetectorTest.java @@ -1,194 +1,178 @@ package com.muchq.one_d4.motifs; +import static org.assertj.core.api.Assertions.assertThat; + import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.engine.model.PositionContext; -import org.junit.Test; - import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; public class ForkDetectorTest { - private final ForkDetector detector = new ForkDetector(); - - @Test - public void motifType() { - assertThat(detector.motif()).isEqualTo(Motif.FORK); - } - - // === Knight forks === - - @Test - public void knightFork_attacksKingAndQueen() { - // White knight on e6 forks black king on g7 and black queen on c7 - // Position: 8/2q3k1/4N3/8/8/8/8/4K3 w - - 0 1 - String fen = "8/2q3k1/4N3/8/8/8/8/4K3 w - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, true) - ); - - // After white's move, it's black to move, so attacker is white (!whiteToMove) - // But ForkDetector checks for the side that just moved, which is white here - // Let's set whiteToMove=false to indicate black is to move (white just moved) - positions = List.of(new PositionContext(10, fen, false)); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - assertThat(occurrences.get(0).moveNumber()).isEqualTo(10); - } - - @Test - public void knightFork_attacksKingAndRook() { - // White knight on d5 forks black king on f6 and black rook on b4 - // Position: 8/8/5k2/3N4/1r6/8/8/4K3 b - - 0 1 - String fen = "8/8/5k2/3N4/1r6/8/8/4K3 b - - 0 1"; - List positions = List.of( - new PositionContext(15, fen, false) // black to move, white just forked - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void knightFork_attacksQueenAndRook() { - // White knight on c6 forks black queen on e7 and black rook on a7 - String fen = "8/r3q3/2N5/8/8/8/8/4K2k b - - 0 1"; - List positions = List.of( - new PositionContext(12, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void knightFork_blackKnightForksWhitePieces() { - // Black knight on d4 forks white king on e2 and white queen on f5 - String fen = "8/8/8/5Q2/3n4/8/4K3/7k w - - 0 1"; - List positions = List.of( - new PositionContext(20, fen, true) // white to move, black just forked - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - // === Pawn forks === - - @Test - public void pawnFork_whitePawnForksBlackPieces() { - // White pawn on d5 attacks black knights on c6 and e6 - String fen = "8/8/2n1n3/3P4/8/8/8/4K2k b - - 0 1"; - List positions = List.of( - new PositionContext(8, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void pawnFork_blackPawnForksWhitePieces() { - // Black pawn on e4 attacks white bishop on d3 and white knight on f3 - String fen = "7k/8/8/8/4p3/3B1N2/8/4K3 w - - 0 1"; - List positions = List.of( - new PositionContext(15, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - // === Queen forks === - - @Test - public void queenFork_attacksKingAndRook() { - // White queen on e5 forks black king on g7 and black rook on a5 - String fen = "8/6k1/8/r3Q3/8/8/8/4K3 b - - 0 1"; - List positions = List.of( - new PositionContext(25, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - // === Bishop forks === - - @Test - public void bishopFork_attacksTwoRooks() { - // White bishop on c4 attacks black rooks on a6 and f7 - String fen = "8/5r2/r7/8/2B5/8/8/4K2k b - - 0 1"; - List positions = List.of( - new PositionContext(18, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - // === No fork cases === - - @Test - public void noFork_onlyOnePieceAttacked() { - // White knight on e4 attacks only black queen on f6 - String fen = "8/8/5q2/8/4N3/8/8/4K2k b - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void noFork_attackingPawnsNotCounted() { - // White knight attacks two black pawns - pawns are value 1, not counted as valuable - String fen = "8/8/8/8/3N4/2p1p3/8/4K2k b - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void noFork_emptyPosition() { - // Starting position - no forks - String fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; - List positions = List.of( - new PositionContext(1, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void noFork_emptyList() { - List occurrences = detector.detect(List.of()); - assertThat(occurrences).isEmpty(); - } - - // === Multiple positions === - - @Test - public void multiplePositions_detectsForksInSome() { - List positions = List.of( - // No fork - new PositionContext(1, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", true), - // Fork: knight on e6 forks king g7 and queen c7 - new PositionContext(10, "8/2q3k1/4N3/8/8/8/8/4K3 b - - 0 1", false), - // No fork - new PositionContext(15, "8/8/8/8/8/8/8/4K2k w - - 0 1", true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - assertThat(occurrences.get(0).moveNumber()).isEqualTo(10); - } + private final ForkDetector detector = new ForkDetector(); + + @Test + public void motifType() { + assertThat(detector.motif()).isEqualTo(Motif.FORK); + } + + // === Knight forks === + + @Test + public void knightFork_attacksKingAndQueen() { + // White knight on e6 forks black king on g7 and black queen on c7 + // Position: 8/2q3k1/4N3/8/8/8/8/4K3 w - - 0 1 + String fen = "8/2q3k1/4N3/8/8/8/8/4K3 w - - 0 1"; + List positions = List.of(new PositionContext(10, fen, true)); + + // After white's move, it's black to move, so attacker is white (!whiteToMove) + // But ForkDetector checks for the side that just moved, which is white here + // Let's set whiteToMove=false to indicate black is to move (white just moved) + positions = List.of(new PositionContext(10, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + assertThat(occurrences.get(0).moveNumber()).isEqualTo(10); + } + + @Test + public void knightFork_attacksKingAndRook() { + // White knight on d5 forks black king on f6 and black rook on b4 + // Position: 8/8/5k2/3N4/1r6/8/8/4K3 b - - 0 1 + String fen = "8/8/5k2/3N4/1r6/8/8/4K3 b - - 0 1"; + List positions = + List.of( + new PositionContext(15, fen, false) // black to move, white just forked + ); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void knightFork_attacksQueenAndRook() { + // White knight on c6 forks black queen on e7 and black rook on a7 + String fen = "8/r3q3/2N5/8/8/8/8/4K2k b - - 0 1"; + List positions = List.of(new PositionContext(12, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void knightFork_blackKnightForksWhitePieces() { + // Black knight on d4 forks white king on e2 and white queen on f5 + String fen = "8/8/8/5Q2/3n4/8/4K3/7k w - - 0 1"; + List positions = + List.of( + new PositionContext(20, fen, true) // white to move, black just forked + ); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + // === Pawn forks === + + @Test + public void pawnFork_whitePawnForksBlackPieces() { + // White pawn on d5 attacks black knights on c6 and e6 + String fen = "8/8/2n1n3/3P4/8/8/8/4K2k b - - 0 1"; + List positions = List.of(new PositionContext(8, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void pawnFork_blackPawnForksWhitePieces() { + // Black pawn on e4 attacks white bishop on d3 and white knight on f3 + String fen = "7k/8/8/8/4p3/3B1N2/8/4K3 w - - 0 1"; + List positions = List.of(new PositionContext(15, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + // === Queen forks === + + @Test + public void queenFork_attacksKingAndRook() { + // White queen on e5 forks black king on g7 and black rook on a5 + String fen = "8/6k1/8/r3Q3/8/8/8/4K3 b - - 0 1"; + List positions = List.of(new PositionContext(25, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + // === Bishop forks === + + @Test + public void bishopFork_attacksTwoRooks() { + // White bishop on c4 attacks black rooks on a6 and f7 + String fen = "8/5r2/r7/8/2B5/8/8/4K2k b - - 0 1"; + List positions = List.of(new PositionContext(18, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + // === No fork cases === + + @Test + public void noFork_onlyOnePieceAttacked() { + // White knight on e4 attacks only black queen on f6 + String fen = "8/8/5q2/8/4N3/8/8/4K2k b - - 0 1"; + List positions = List.of(new PositionContext(10, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void noFork_attackingPawnsNotCounted() { + // White knight attacks two black pawns - pawns are value 1, not counted as valuable + String fen = "8/8/8/8/3N4/2p1p3/8/4K2k b - - 0 1"; + List positions = List.of(new PositionContext(10, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void noFork_emptyPosition() { + // Starting position - no forks + String fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + List positions = List.of(new PositionContext(1, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void noFork_emptyList() { + List occurrences = detector.detect(List.of()); + assertThat(occurrences).isEmpty(); + } + + // === Multiple positions === + + @Test + public void multiplePositions_detectsForksInSome() { + List positions = + List.of( + // No fork + new PositionContext( + 1, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", true), + // Fork: knight on e6 forks king g7 and queen c7 + new PositionContext(10, "8/2q3k1/4N3/8/8/8/8/4K3 b - - 0 1", false), + // No fork + new PositionContext(15, "8/8/8/8/8/8/8/4K2k w - - 0 1", true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + assertThat(occurrences.get(0).moveNumber()).isEqualTo(10); + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/motifs/PinDetectorTest.java b/jvm/src/test/java/com/muchq/one_d4/motifs/PinDetectorTest.java index 6e7b6f86..9190a5b8 100644 --- a/jvm/src/test/java/com/muchq/one_d4/motifs/PinDetectorTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/motifs/PinDetectorTest.java @@ -1,182 +1,167 @@ package com.muchq.one_d4.motifs; +import static org.assertj.core.api.Assertions.assertThat; + import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.engine.model.PositionContext; -import org.junit.Test; - import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; public class PinDetectorTest { - private final PinDetector detector = new PinDetector(); - - @Test - public void motifType() { - assertThat(detector.motif()).isEqualTo(Motif.PIN); - } - - // === Absolute pins (to king) === - - @Test - public void absolutePin_rookPinsKnightToKing() { - // Black rook on a4 pins white knight on e4 to white king on h4 - // Position: 8/8/8/8/r3N2K/8/8/7k w - - 0 1 - String fen = "8/8/8/8/r3N2K/8/8/7k w - - 0 1"; - List positions = List.of( - new PositionContext(15, fen, true) // white to move, knight is pinned - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void absolutePin_bishopPinsBishopToKing() { - // Black bishop on a1 pins white bishop on d4 to white king on g7 - String fen = "8/6K1/8/8/3B4/8/8/b6k w - - 0 1"; - List positions = List.of( - new PositionContext(20, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void absolutePin_queenPinsRookToKing() { - // Black queen on a8 pins white rook on d8 to white king on h8 - String fen = "q2R3K/8/8/8/8/8/8/7k w - - 0 1"; - List positions = List.of( - new PositionContext(25, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void absolutePin_blackPieceIsPinned() { - // White rook on a5 pins black knight on e5 to black king on h5 - String fen = "8/8/8/R3n2k/8/8/8/4K3 b - - 0 1"; - List positions = List.of( - new PositionContext(18, fen, false) // black to move, knight is pinned - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void absolutePin_diagonalPin() { - // White bishop on b1 pins black knight on d3 to black king on f5 - String fen = "8/8/8/5k2/8/3n4/8/1B2K3 b - - 0 1"; - List positions = List.of( - new PositionContext(12, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - // === No pin cases === - - @Test - public void noPin_startingPosition() { - String fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; - List positions = List.of( - new PositionContext(1, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void noPin_noPieceBetweenAttackerAndKing() { - // Black rook attacks white king directly, no pin - String fen = "8/8/8/8/r6K/8/8/7k w - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void noPin_twoPiecesBetweenAttackerAndKing() { - // Black rook, two white pieces, white king - not a pin - String fen = "8/8/8/8/r2NN2K/8/8/7k w - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void noPin_knightCannotPin() { - // Knights cannot create pins (don't slide) - String fen = "8/8/2n5/8/3B4/8/8/4K2k w - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void noPin_wrongPieceType() { - // Rook on diagonal cannot pin (rooks don't attack diagonally) - String fen = "8/6K1/8/8/3B4/8/8/r6k w - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void noPin_emptyPositionList() { - List occurrences = detector.detect(List.of()); - assertThat(occurrences).isEmpty(); - } - - // === Helper methods === - - @Test - public void parsePlacement_startingPosition() { - String placement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"; - int[][] board = PinDetector.parsePlacement(placement); - - // Check some squares - assertThat(board[0][0]).isEqualTo(-4); // black rook a8 - assertThat(board[0][4]).isEqualTo(-6); // black king e8 - assertThat(board[7][4]).isEqualTo(6); // white king e1 - assertThat(board[7][3]).isEqualTo(5); // white queen d1 - assertThat(board[4][4]).isEqualTo(0); // empty e4 - } - - @Test - public void pieceValue_allPieces() { - assertThat(PinDetector.pieceValue('K')).isEqualTo(6); - assertThat(PinDetector.pieceValue('Q')).isEqualTo(5); - assertThat(PinDetector.pieceValue('R')).isEqualTo(4); - assertThat(PinDetector.pieceValue('B')).isEqualTo(3); - assertThat(PinDetector.pieceValue('N')).isEqualTo(2); - assertThat(PinDetector.pieceValue('P')).isEqualTo(1); - assertThat(PinDetector.pieceValue('k')).isEqualTo(-6); - assertThat(PinDetector.pieceValue('q')).isEqualTo(-5); - assertThat(PinDetector.pieceValue('r')).isEqualTo(-4); - assertThat(PinDetector.pieceValue('b')).isEqualTo(-3); - assertThat(PinDetector.pieceValue('n')).isEqualTo(-2); - assertThat(PinDetector.pieceValue('p')).isEqualTo(-1); - assertThat(PinDetector.pieceValue('x')).isEqualTo(0); // invalid - } + private final PinDetector detector = new PinDetector(); + + @Test + public void motifType() { + assertThat(detector.motif()).isEqualTo(Motif.PIN); + } + + // === Absolute pins (to king) === + + @Test + public void absolutePin_rookPinsKnightToKing() { + // Black rook on a4 pins white knight on e4 to white king on h4 + // Position: 8/8/8/8/r3N2K/8/8/7k w - - 0 1 + String fen = "8/8/8/8/r3N2K/8/8/7k w - - 0 1"; + List positions = + List.of( + new PositionContext(15, fen, true) // white to move, knight is pinned + ); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void absolutePin_bishopPinsBishopToKing() { + // Black bishop on a1 pins white bishop on d4 to white king on g7 + String fen = "8/6K1/8/8/3B4/8/8/b6k w - - 0 1"; + List positions = List.of(new PositionContext(20, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void absolutePin_queenPinsRookToKing() { + // Black queen on a8 pins white rook on d8 to white king on h8 + String fen = "q2R3K/8/8/8/8/8/8/7k w - - 0 1"; + List positions = List.of(new PositionContext(25, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void absolutePin_blackPieceIsPinned() { + // White rook on a5 pins black knight on e5 to black king on h5 + String fen = "8/8/8/R3n2k/8/8/8/4K3 b - - 0 1"; + List positions = + List.of( + new PositionContext(18, fen, false) // black to move, knight is pinned + ); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void absolutePin_diagonalPin() { + // White bishop on b1 pins black knight on d3 to black king on f5 + String fen = "8/8/8/5k2/8/3n4/8/1B2K3 b - - 0 1"; + List positions = List.of(new PositionContext(12, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + // === No pin cases === + + @Test + public void noPin_startingPosition() { + String fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + List positions = List.of(new PositionContext(1, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void noPin_noPieceBetweenAttackerAndKing() { + // Black rook attacks white king directly, no pin + String fen = "8/8/8/8/r6K/8/8/7k w - - 0 1"; + List positions = List.of(new PositionContext(10, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void noPin_twoPiecesBetweenAttackerAndKing() { + // Black rook, two white pieces, white king - not a pin + String fen = "8/8/8/8/r2NN2K/8/8/7k w - - 0 1"; + List positions = List.of(new PositionContext(10, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void noPin_knightCannotPin() { + // Knights cannot create pins (don't slide) + String fen = "8/8/2n5/8/3B4/8/8/4K2k w - - 0 1"; + List positions = List.of(new PositionContext(10, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void noPin_wrongPieceType() { + // Rook on diagonal cannot pin (rooks don't attack diagonally) + String fen = "8/6K1/8/8/3B4/8/8/r6k w - - 0 1"; + List positions = List.of(new PositionContext(10, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void noPin_emptyPositionList() { + List occurrences = detector.detect(List.of()); + assertThat(occurrences).isEmpty(); + } + + // === Helper methods === + + @Test + public void parsePlacement_startingPosition() { + String placement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"; + int[][] board = PinDetector.parsePlacement(placement); + + // Check some squares + assertThat(board[0][0]).isEqualTo(-4); // black rook a8 + assertThat(board[0][4]).isEqualTo(-6); // black king e8 + assertThat(board[7][4]).isEqualTo(6); // white king e1 + assertThat(board[7][3]).isEqualTo(5); // white queen d1 + assertThat(board[4][4]).isEqualTo(0); // empty e4 + } + + @Test + public void pieceValue_allPieces() { + assertThat(PinDetector.pieceValue('K')).isEqualTo(6); + assertThat(PinDetector.pieceValue('Q')).isEqualTo(5); + assertThat(PinDetector.pieceValue('R')).isEqualTo(4); + assertThat(PinDetector.pieceValue('B')).isEqualTo(3); + assertThat(PinDetector.pieceValue('N')).isEqualTo(2); + assertThat(PinDetector.pieceValue('P')).isEqualTo(1); + assertThat(PinDetector.pieceValue('k')).isEqualTo(-6); + assertThat(PinDetector.pieceValue('q')).isEqualTo(-5); + assertThat(PinDetector.pieceValue('r')).isEqualTo(-4); + assertThat(PinDetector.pieceValue('b')).isEqualTo(-3); + assertThat(PinDetector.pieceValue('n')).isEqualTo(-2); + assertThat(PinDetector.pieceValue('p')).isEqualTo(-1); + assertThat(PinDetector.pieceValue('x')).isEqualTo(0); // invalid + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/motifs/SkewerDetectorTest.java b/jvm/src/test/java/com/muchq/one_d4/motifs/SkewerDetectorTest.java index fdd5ed50..214ffe7d 100644 --- a/jvm/src/test/java/com/muchq/one_d4/motifs/SkewerDetectorTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/motifs/SkewerDetectorTest.java @@ -1,178 +1,159 @@ package com.muchq.one_d4.motifs; +import static org.assertj.core.api.Assertions.assertThat; + import com.muchq.one_d4.engine.model.GameFeatures; import com.muchq.one_d4.engine.model.Motif; import com.muchq.one_d4.engine.model.PositionContext; -import org.junit.Test; - import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; public class SkewerDetectorTest { - private final SkewerDetector detector = new SkewerDetector(); - - @Test - public void motifType() { - assertThat(detector.motif()).isEqualTo(Motif.SKEWER); - } - - // === Skewer cases === - // A skewer is when a more valuable piece is in front and a less valuable piece is behind - - @Test - public void skewer_rookSkewersKingAndRook() { - // White rook on a4 attacks black king on e4, with black rook on h4 behind - // King (6) > Rook (4), so this is a skewer - String fen = "8/8/8/8/R3k2r/8/8/4K3 b - - 0 1"; - List positions = List.of( - new PositionContext(15, fen, false) // black to move, white just skewered - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void skewer_queenSkewersKingAndBishop() { - // White queen on a1 attacks black king on d4, with black bishop on g7 behind - String fen = "8/6b1/8/8/3k4/8/8/Q3K3 b - - 0 1"; - List positions = List.of( - new PositionContext(20, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void skewer_bishopSkewersQueenAndRook() { - // White bishop on b1 attacks black queen on d3, with black rook on f5 behind - // Queen (5) > Rook (4), so this is a skewer - String fen = "8/8/8/5r2/8/3q4/8/1B2K2k b - - 0 1"; - List positions = List.of( - new PositionContext(18, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void skewer_rookSkewersQueenAndKnight() { - // White rook on a5 attacks black queen on d5, with black knight on h5 behind - String fen = "8/8/8/R2q3n/8/8/8/4K2k b - - 0 1"; - List positions = List.of( - new PositionContext(22, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - @Test - public void skewer_blackSkewersWhitePieces() { - // Black rook on h4 attacks white king on e4, with white bishop on b4 behind - String fen = "8/8/8/8/1B2K2r/8/8/7k w - - 0 1"; - List positions = List.of( - new PositionContext(15, fen, true) // white to move, black just skewered - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).hasSize(1); - } - - // === Not a skewer cases === - - @Test - public void notSkewer_lessValuableInFront() { - // This is a PIN, not a skewer: knight in front, king behind - // White rook attacks black knight on e4, with black king on h4 behind - String fen = "8/8/8/8/R3n2k/8/8/4K3 b - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, false) - ); - - // Should NOT detect skewer (this would be detected as a pin) - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void notSkewer_sameValuePieces() { - // Rook attacks rook with rook behind - same value, not a skewer - String fen = "8/8/8/8/R3r2r/8/8/4K2k b - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void notSkewer_onlyOnePieceOnRay() { - // Rook attacks king, but nothing behind - String fen = "8/8/8/8/R3k3/8/8/4K3 b - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void notSkewer_pawnBehind() { - // King skewered to pawn - but pawn value (1) < 2, not counted - String fen = "8/8/8/8/R3k2p/8/8/4K3 b - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void notSkewer_friendlyPieceBlocks() { - // White rook on a4, white knight on c4 blocks any skewer potential - String fen = "8/8/8/8/R1N1k2r/8/8/4K3 b - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void notSkewer_knightCannotSkewer() { - // Knights cannot create skewers (don't slide) - String fen = "8/8/5q2/8/4N3/8/3r4/4K2k b - - 0 1"; - List positions = List.of( - new PositionContext(10, fen, false) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void notSkewer_startingPosition() { - String fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; - List positions = List.of( - new PositionContext(1, fen, true) - ); - - List occurrences = detector.detect(positions); - assertThat(occurrences).isEmpty(); - } - - @Test - public void notSkewer_emptyList() { - List occurrences = detector.detect(List.of()); - assertThat(occurrences).isEmpty(); - } + private final SkewerDetector detector = new SkewerDetector(); + + @Test + public void motifType() { + assertThat(detector.motif()).isEqualTo(Motif.SKEWER); + } + + // === Skewer cases === + // A skewer is when a more valuable piece is in front and a less valuable piece is behind + + @Test + public void skewer_rookSkewersKingAndRook() { + // White rook on a4 attacks black king on e4, with black rook on h4 behind + // King (6) > Rook (4), so this is a skewer + String fen = "8/8/8/8/R3k2r/8/8/4K3 b - - 0 1"; + List positions = + List.of( + new PositionContext(15, fen, false) // black to move, white just skewered + ); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void skewer_queenSkewersKingAndBishop() { + // White queen on a1 attacks black king on d4, with black bishop on g7 behind + String fen = "8/6b1/8/8/3k4/8/8/Q3K3 b - - 0 1"; + List positions = List.of(new PositionContext(20, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void skewer_bishopSkewersQueenAndRook() { + // White bishop on b1 attacks black queen on d3, with black rook on f5 behind + // Queen (5) > Rook (4), so this is a skewer + String fen = "8/8/8/5r2/8/3q4/8/1B2K2k b - - 0 1"; + List positions = List.of(new PositionContext(18, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void skewer_rookSkewersQueenAndKnight() { + // White rook on a5 attacks black queen on d5, with black knight on h5 behind + String fen = "8/8/8/R2q3n/8/8/8/4K2k b - - 0 1"; + List positions = List.of(new PositionContext(22, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + @Test + public void skewer_blackSkewersWhitePieces() { + // Black rook on h4 attacks white king on e4, with white bishop on b4 behind + String fen = "8/8/8/8/1B2K2r/8/8/7k w - - 0 1"; + List positions = + List.of( + new PositionContext(15, fen, true) // white to move, black just skewered + ); + + List occurrences = detector.detect(positions); + assertThat(occurrences).hasSize(1); + } + + // === Not a skewer cases === + + @Test + public void notSkewer_lessValuableInFront() { + // This is a PIN, not a skewer: knight in front, king behind + // White rook attacks black knight on e4, with black king on h4 behind + String fen = "8/8/8/8/R3n2k/8/8/4K3 b - - 0 1"; + List positions = List.of(new PositionContext(10, fen, false)); + + // Should NOT detect skewer (this would be detected as a pin) + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void notSkewer_sameValuePieces() { + // Rook attacks rook with rook behind - same value, not a skewer + String fen = "8/8/8/8/R3r2r/8/8/4K2k b - - 0 1"; + List positions = List.of(new PositionContext(10, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void notSkewer_onlyOnePieceOnRay() { + // Rook attacks king, but nothing behind + String fen = "8/8/8/8/R3k3/8/8/4K3 b - - 0 1"; + List positions = List.of(new PositionContext(10, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void notSkewer_pawnBehind() { + // King skewered to pawn - but pawn value (1) < 2, not counted + String fen = "8/8/8/8/R3k2p/8/8/4K3 b - - 0 1"; + List positions = List.of(new PositionContext(10, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void notSkewer_friendlyPieceBlocks() { + // White rook on a4, white knight on c4 blocks any skewer potential + String fen = "8/8/8/8/R1N1k2r/8/8/4K3 b - - 0 1"; + List positions = List.of(new PositionContext(10, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void notSkewer_knightCannotSkewer() { + // Knights cannot create skewers (don't slide) + String fen = "8/8/5q2/8/4N3/8/3r4/4K2k b - - 0 1"; + List positions = List.of(new PositionContext(10, fen, false)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void notSkewer_startingPosition() { + String fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + List positions = List.of(new PositionContext(1, fen, true)); + + List occurrences = detector.detect(positions); + assertThat(occurrences).isEmpty(); + } + + @Test + public void notSkewer_emptyList() { + List occurrences = detector.detect(List.of()); + assertThat(occurrences).isEmpty(); + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/queue/InMemoryIndexQueueTest.java b/jvm/src/test/java/com/muchq/one_d4/queue/InMemoryIndexQueueTest.java index ec24bbbc..fbac60b7 100644 --- a/jvm/src/test/java/com/muchq/one_d4/queue/InMemoryIndexQueueTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/queue/InMemoryIndexQueueTest.java @@ -1,43 +1,43 @@ package com.muchq.one_d4.queue; -import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; import java.util.Optional; import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; public class InMemoryIndexQueueTest { - @Test - public void testEnqueueAndPoll() { - InMemoryIndexQueue queue = new InMemoryIndexQueue(); - IndexMessage message = new IndexMessage(UUID.randomUUID(), "hikaru", "chess.com", "2024-01", "2024-01"); - - queue.enqueue(message); - assertThat(queue.size()).isEqualTo(1); - - Optional polled = queue.poll(Duration.ofMillis(100)); - assertThat(polled).isPresent(); - assertThat(polled.get().player()).isEqualTo("hikaru"); - assertThat(queue.size()).isEqualTo(0); - } - - @Test - public void testPollEmpty() { - InMemoryIndexQueue queue = new InMemoryIndexQueue(); - Optional polled = queue.poll(Duration.ofMillis(50)); - assertThat(polled).isEmpty(); - } - - @Test - public void testSize() { - InMemoryIndexQueue queue = new InMemoryIndexQueue(); - assertThat(queue.size()).isEqualTo(0); - - queue.enqueue(new IndexMessage(UUID.randomUUID(), "a", "chess.com", "2024-01", "2024-01")); - queue.enqueue(new IndexMessage(UUID.randomUUID(), "b", "chess.com", "2024-01", "2024-01")); - assertThat(queue.size()).isEqualTo(2); - } + @Test + public void testEnqueueAndPoll() { + InMemoryIndexQueue queue = new InMemoryIndexQueue(); + IndexMessage message = + new IndexMessage(UUID.randomUUID(), "hikaru", "chess.com", "2024-01", "2024-01"); + + queue.enqueue(message); + assertThat(queue.size()).isEqualTo(1); + + Optional polled = queue.poll(Duration.ofMillis(100)); + assertThat(polled).isPresent(); + assertThat(polled.get().player()).isEqualTo("hikaru"); + assertThat(queue.size()).isEqualTo(0); + } + + @Test + public void testPollEmpty() { + InMemoryIndexQueue queue = new InMemoryIndexQueue(); + Optional polled = queue.poll(Duration.ofMillis(50)); + assertThat(polled).isEmpty(); + } + + @Test + public void testSize() { + InMemoryIndexQueue queue = new InMemoryIndexQueue(); + assertThat(queue.size()).isEqualTo(0); + + queue.enqueue(new IndexMessage(UUID.randomUUID(), "a", "chess.com", "2024-01", "2024-01")); + queue.enqueue(new IndexMessage(UUID.randomUUID(), "b", "chess.com", "2024-01", "2024-01")); + assertThat(queue.size()).isEqualTo(2); + } } diff --git a/jvm/src/test/java/com/muchq/one_d4/worker/ResultMapperTest.java b/jvm/src/test/java/com/muchq/one_d4/worker/ResultMapperTest.java index 82148283..ba90d786 100644 --- a/jvm/src/test/java/com/muchq/one_d4/worker/ResultMapperTest.java +++ b/jvm/src/test/java/com/muchq/one_d4/worker/ResultMapperTest.java @@ -1,184 +1,185 @@ package com.muchq.one_d4.worker; -import org.junit.Test; - import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; + public class ResultMapperTest { - // === White wins === - - @Test - public void whiteWins_explicitWin() { - assertThat(ResultMapper.mapResult("win", "resigned")).isEqualTo("1-0"); - assertThat(ResultMapper.mapResult("win", "checkmated")).isEqualTo("1-0"); - assertThat(ResultMapper.mapResult("win", "timeout")).isEqualTo("1-0"); - } - - @Test - public void whiteWins_blackResigned() { - assertThat(ResultMapper.mapResult(null, "resigned")).isEqualTo("1-0"); - } - - @Test - public void whiteWins_blackCheckmated() { - assertThat(ResultMapper.mapResult(null, "checkmated")).isEqualTo("1-0"); - } - - @Test - public void whiteWins_blackTimeout() { - assertThat(ResultMapper.mapResult(null, "timeout")).isEqualTo("1-0"); - } - - @Test - public void whiteWins_blackAbandoned() { - assertThat(ResultMapper.mapResult(null, "abandoned")).isEqualTo("1-0"); - } - - @Test - public void whiteWins_blackLose() { - assertThat(ResultMapper.mapResult(null, "lose")).isEqualTo("1-0"); - } - - // === Black wins === - - @Test - public void blackWins_explicitWin() { - assertThat(ResultMapper.mapResult("resigned", "win")).isEqualTo("0-1"); - assertThat(ResultMapper.mapResult("checkmated", "win")).isEqualTo("0-1"); - assertThat(ResultMapper.mapResult("timeout", "win")).isEqualTo("0-1"); - } - - @Test - public void blackWins_whiteResigned() { - assertThat(ResultMapper.mapResult("resigned", null)).isEqualTo("0-1"); - } - - @Test - public void blackWins_whiteCheckmated() { - assertThat(ResultMapper.mapResult("checkmated", null)).isEqualTo("0-1"); - } - - @Test - public void blackWins_whiteTimeout() { - assertThat(ResultMapper.mapResult("timeout", null)).isEqualTo("0-1"); - } - - @Test - public void blackWins_whiteAbandoned() { - assertThat(ResultMapper.mapResult("abandoned", null)).isEqualTo("0-1"); - } - - @Test - public void blackWins_whiteLose() { - assertThat(ResultMapper.mapResult("lose", null)).isEqualTo("0-1"); - } - - // === Draws === - - @Test - public void draw_agreed() { - assertThat(ResultMapper.mapResult("agreed", "agreed")).isEqualTo("1/2-1/2"); - assertThat(ResultMapper.mapResult("agreed", null)).isEqualTo("1/2-1/2"); - assertThat(ResultMapper.mapResult(null, "agreed")).isEqualTo("1/2-1/2"); - } - - @Test - public void draw_repetition() { - assertThat(ResultMapper.mapResult("repetition", "repetition")).isEqualTo("1/2-1/2"); - assertThat(ResultMapper.mapResult("repetition", null)).isEqualTo("1/2-1/2"); - } - - @Test - public void draw_stalemate() { - assertThat(ResultMapper.mapResult("stalemate", "stalemate")).isEqualTo("1/2-1/2"); - } - - @Test - public void draw_insufficient() { - assertThat(ResultMapper.mapResult("insufficient", "insufficient")).isEqualTo("1/2-1/2"); - } - - @Test - public void draw_50move() { - assertThat(ResultMapper.mapResult("50move", "50move")).isEqualTo("1/2-1/2"); - } - - @Test - public void draw_timevsinsufficient() { - assertThat(ResultMapper.mapResult("timevsinsufficient", "timevsinsufficient")).isEqualTo("1/2-1/2"); - } - - @Test - public void draw_drawn() { - assertThat(ResultMapper.mapResult("drawn", null)).isEqualTo("1/2-1/2"); - } - - // === Unknown === - - @Test - public void unknown_bothNull() { - assertThat(ResultMapper.mapResult(null, null)).isEqualTo("unknown"); - } - - @Test - public void unknown_unrecognizedValues() { - assertThat(ResultMapper.mapResult("something", "else")).isEqualTo("unknown"); - } - - // === isDrawResult === - - @Test - public void isDrawResult_recognizesAllDrawTypes() { - assertThat(ResultMapper.isDrawResult("agreed")).isTrue(); - assertThat(ResultMapper.isDrawResult("repetition")).isTrue(); - assertThat(ResultMapper.isDrawResult("stalemate")).isTrue(); - assertThat(ResultMapper.isDrawResult("insufficient")).isTrue(); - assertThat(ResultMapper.isDrawResult("50move")).isTrue(); - assertThat(ResultMapper.isDrawResult("timevsinsufficient")).isTrue(); - assertThat(ResultMapper.isDrawResult("drawn")).isTrue(); - } - - @Test - public void isDrawResult_rejectsNonDraws() { - assertThat(ResultMapper.isDrawResult("win")).isFalse(); - assertThat(ResultMapper.isDrawResult("resigned")).isFalse(); - assertThat(ResultMapper.isDrawResult("checkmated")).isFalse(); - assertThat(ResultMapper.isDrawResult(null)).isFalse(); - } - - // === isLossResult === - - @Test - public void isLossResult_recognizesAllLossTypes() { - assertThat(ResultMapper.isLossResult("resigned")).isTrue(); - assertThat(ResultMapper.isLossResult("checkmated")).isTrue(); - assertThat(ResultMapper.isLossResult("timeout")).isTrue(); - assertThat(ResultMapper.isLossResult("abandoned")).isTrue(); - assertThat(ResultMapper.isLossResult("lose")).isTrue(); - } - - @Test - public void isLossResult_rejectsNonLosses() { - assertThat(ResultMapper.isLossResult("win")).isFalse(); - assertThat(ResultMapper.isLossResult("agreed")).isFalse(); - assertThat(ResultMapper.isLossResult("repetition")).isFalse(); - assertThat(ResultMapper.isLossResult(null)).isFalse(); - } - - // === Edge cases === - - @Test - public void explicitWinTakesPrecedenceOverLoss() { - // If both sides have a result, "win" should be authoritative - assertThat(ResultMapper.mapResult("win", "resigned")).isEqualTo("1-0"); - assertThat(ResultMapper.mapResult("resigned", "win")).isEqualTo("0-1"); - } - - @Test - public void drawTakesPrecedenceOverUnknown() { - // A draw result on either side should yield 1/2-1/2 - assertThat(ResultMapper.mapResult("repetition", "unknown_value")).isEqualTo("1/2-1/2"); - assertThat(ResultMapper.mapResult("unknown_value", "stalemate")).isEqualTo("1/2-1/2"); - } + // === White wins === + + @Test + public void whiteWins_explicitWin() { + assertThat(ResultMapper.mapResult("win", "resigned")).isEqualTo("1-0"); + assertThat(ResultMapper.mapResult("win", "checkmated")).isEqualTo("1-0"); + assertThat(ResultMapper.mapResult("win", "timeout")).isEqualTo("1-0"); + } + + @Test + public void whiteWins_blackResigned() { + assertThat(ResultMapper.mapResult(null, "resigned")).isEqualTo("1-0"); + } + + @Test + public void whiteWins_blackCheckmated() { + assertThat(ResultMapper.mapResult(null, "checkmated")).isEqualTo("1-0"); + } + + @Test + public void whiteWins_blackTimeout() { + assertThat(ResultMapper.mapResult(null, "timeout")).isEqualTo("1-0"); + } + + @Test + public void whiteWins_blackAbandoned() { + assertThat(ResultMapper.mapResult(null, "abandoned")).isEqualTo("1-0"); + } + + @Test + public void whiteWins_blackLose() { + assertThat(ResultMapper.mapResult(null, "lose")).isEqualTo("1-0"); + } + + // === Black wins === + + @Test + public void blackWins_explicitWin() { + assertThat(ResultMapper.mapResult("resigned", "win")).isEqualTo("0-1"); + assertThat(ResultMapper.mapResult("checkmated", "win")).isEqualTo("0-1"); + assertThat(ResultMapper.mapResult("timeout", "win")).isEqualTo("0-1"); + } + + @Test + public void blackWins_whiteResigned() { + assertThat(ResultMapper.mapResult("resigned", null)).isEqualTo("0-1"); + } + + @Test + public void blackWins_whiteCheckmated() { + assertThat(ResultMapper.mapResult("checkmated", null)).isEqualTo("0-1"); + } + + @Test + public void blackWins_whiteTimeout() { + assertThat(ResultMapper.mapResult("timeout", null)).isEqualTo("0-1"); + } + + @Test + public void blackWins_whiteAbandoned() { + assertThat(ResultMapper.mapResult("abandoned", null)).isEqualTo("0-1"); + } + + @Test + public void blackWins_whiteLose() { + assertThat(ResultMapper.mapResult("lose", null)).isEqualTo("0-1"); + } + + // === Draws === + + @Test + public void draw_agreed() { + assertThat(ResultMapper.mapResult("agreed", "agreed")).isEqualTo("1/2-1/2"); + assertThat(ResultMapper.mapResult("agreed", null)).isEqualTo("1/2-1/2"); + assertThat(ResultMapper.mapResult(null, "agreed")).isEqualTo("1/2-1/2"); + } + + @Test + public void draw_repetition() { + assertThat(ResultMapper.mapResult("repetition", "repetition")).isEqualTo("1/2-1/2"); + assertThat(ResultMapper.mapResult("repetition", null)).isEqualTo("1/2-1/2"); + } + + @Test + public void draw_stalemate() { + assertThat(ResultMapper.mapResult("stalemate", "stalemate")).isEqualTo("1/2-1/2"); + } + + @Test + public void draw_insufficient() { + assertThat(ResultMapper.mapResult("insufficient", "insufficient")).isEqualTo("1/2-1/2"); + } + + @Test + public void draw_50move() { + assertThat(ResultMapper.mapResult("50move", "50move")).isEqualTo("1/2-1/2"); + } + + @Test + public void draw_timevsinsufficient() { + assertThat(ResultMapper.mapResult("timevsinsufficient", "timevsinsufficient")) + .isEqualTo("1/2-1/2"); + } + + @Test + public void draw_drawn() { + assertThat(ResultMapper.mapResult("drawn", null)).isEqualTo("1/2-1/2"); + } + + // === Unknown === + + @Test + public void unknown_bothNull() { + assertThat(ResultMapper.mapResult(null, null)).isEqualTo("unknown"); + } + + @Test + public void unknown_unrecognizedValues() { + assertThat(ResultMapper.mapResult("something", "else")).isEqualTo("unknown"); + } + + // === isDrawResult === + + @Test + public void isDrawResult_recognizesAllDrawTypes() { + assertThat(ResultMapper.isDrawResult("agreed")).isTrue(); + assertThat(ResultMapper.isDrawResult("repetition")).isTrue(); + assertThat(ResultMapper.isDrawResult("stalemate")).isTrue(); + assertThat(ResultMapper.isDrawResult("insufficient")).isTrue(); + assertThat(ResultMapper.isDrawResult("50move")).isTrue(); + assertThat(ResultMapper.isDrawResult("timevsinsufficient")).isTrue(); + assertThat(ResultMapper.isDrawResult("drawn")).isTrue(); + } + + @Test + public void isDrawResult_rejectsNonDraws() { + assertThat(ResultMapper.isDrawResult("win")).isFalse(); + assertThat(ResultMapper.isDrawResult("resigned")).isFalse(); + assertThat(ResultMapper.isDrawResult("checkmated")).isFalse(); + assertThat(ResultMapper.isDrawResult(null)).isFalse(); + } + + // === isLossResult === + + @Test + public void isLossResult_recognizesAllLossTypes() { + assertThat(ResultMapper.isLossResult("resigned")).isTrue(); + assertThat(ResultMapper.isLossResult("checkmated")).isTrue(); + assertThat(ResultMapper.isLossResult("timeout")).isTrue(); + assertThat(ResultMapper.isLossResult("abandoned")).isTrue(); + assertThat(ResultMapper.isLossResult("lose")).isTrue(); + } + + @Test + public void isLossResult_rejectsNonLosses() { + assertThat(ResultMapper.isLossResult("win")).isFalse(); + assertThat(ResultMapper.isLossResult("agreed")).isFalse(); + assertThat(ResultMapper.isLossResult("repetition")).isFalse(); + assertThat(ResultMapper.isLossResult(null)).isFalse(); + } + + // === Edge cases === + + @Test + public void explicitWinTakesPrecedenceOverLoss() { + // If both sides have a result, "win" should be authoritative + assertThat(ResultMapper.mapResult("win", "resigned")).isEqualTo("1-0"); + assertThat(ResultMapper.mapResult("resigned", "win")).isEqualTo("0-1"); + } + + @Test + public void drawTakesPrecedenceOverUnknown() { + // A draw result on either side should yield 1/2-1/2 + assertThat(ResultMapper.mapResult("repetition", "unknown_value")).isEqualTo("1/2-1/2"); + assertThat(ResultMapper.mapResult("unknown_value", "stalemate")).isEqualTo("1/2-1/2"); + } } From 98d0787cd2283fb3680717728f1430f13b0af8c9 Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Mon, 2 Feb 2026 23:47:49 -0500 Subject: [PATCH 4/4] fmt --- .../main/java/com/muchq/mcpserver/tools/ChessComGamesTool.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComGamesTool.java b/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComGamesTool.java index 28c1947c..543cf6ce 100644 --- a/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComGamesTool.java +++ b/jvm/src/main/java/com/muchq/mcpserver/tools/ChessComGamesTool.java @@ -25,7 +25,7 @@ public String getName() { @Override public String getDescription() { return "Returns the requested user's chess.com games for the specified month and year. For" - + " example, username: hikaru, year: 2025, month: 01"; + + " example, username: hikaru, year: 2025, month: 01"; } @Override