pgn2 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +34 -3
- data/.github/workflows/native.yml +32 -0
- data/.github/workflows/publish.yml +44 -2
- data/.github/workflows/release-gems.yml +40 -0
- data/.github/workflows/release.yml +52 -5
- data/.gitignore +9 -1
- data/.rubocop.yml +46 -6
- data/CHANGELOG.md +183 -1
- data/Gemfile +3 -0
- data/NOTICE.md +21 -0
- data/README.md +107 -5
- data/Rakefile +35 -10
- data/TODO.md +102 -31
- data/bench/baseline_moves.txt +38 -4
- data/bench/baseline_parse.txt +4 -4
- data/bench/cross_check.rb +61 -0
- data/bench/legal_moves.rb +38 -0
- data/bench/perft.rb +32 -0
- data/bench/profile_moves.rb +93 -0
- data/docs/superpowers/plans/2026-08-13-attack-masks-plan.md +51 -0
- data/docs/superpowers/plans/2026-08-13-perf-internals-plan.md +883 -0
- data/docs/superpowers/plans/2026-08-13-rust-bitboard-perft-plan.md +2442 -0
- data/docs/superpowers/plans/2026-08-14-chessie-migration.md +722 -0
- data/docs/superpowers/plans/2026-08-14-rust-integration-plan.md +444 -0
- data/docs/superpowers/specs/2026-08-13-attack-masks-design.md +57 -0
- data/docs/superpowers/specs/2026-08-13-perf-internals-design.md +111 -0
- data/docs/superpowers/specs/2026-08-13-rust-bitboard-perft-design.md +270 -0
- data/docs/superpowers/specs/2026-08-14-rust-integration-design.md +217 -0
- data/ext/pgn2_native/Cargo.lock +321 -0
- data/ext/pgn2_native/Cargo.toml +19 -0
- data/ext/pgn2_native/extconf.rb +8 -0
- data/ext/pgn2_native/pgn2-bitboard/Cargo.toml +10 -0
- data/ext/pgn2_native/pgn2-bitboard/src/board.rs +32 -0
- data/ext/pgn2_native/pgn2-bitboard/src/lib.rs +12 -0
- data/ext/pgn2_native/pgn2-bitboard/src/moves.rs +121 -0
- data/ext/pgn2_native/pgn2-bitboard/src/perft.rs +81 -0
- data/ext/pgn2_native/pgn2_native/Cargo.toml +11 -0
- data/ext/pgn2_native/pgn2_native/src/lib.rs +54 -0
- data/lib/pgn/bitboard.rb +13 -0
- data/lib/pgn/board.rb +103 -10
- data/lib/pgn/fen.rb +35 -46
- data/lib/pgn/game.rb +22 -13
- data/lib/pgn/lexer.rb +9 -6
- data/lib/pgn/move.rb +19 -15
- data/lib/pgn/move_calculator.rb +46 -20
- data/lib/pgn/notation.rb +24 -29
- data/lib/pgn/position.rb +60 -19
- data/lib/pgn/serializer.rb +13 -18
- data/lib/pgn/version.rb +1 -1
- data/lib/pgn/zobrist.rb +53 -0
- data/lib/pgn.rb +2 -0
- data/pgn2.gemspec +17 -10
- data/spec/bitboard_spec.rb +54 -0
- data/spec/board_spec.rb +53 -0
- data/spec/fen_spec.rb +65 -65
- data/spec/game_spec.rb +52 -15
- data/spec/lexer_spec.rb +5 -5
- data/spec/notation_spec.rb +5 -0
- data/spec/parser_spec.rb +8 -1
- data/spec/position_spec.rb +128 -27
- data/spec/serializer_spec.rb +4 -4
- data/spec/zobrist_spec.rb +46 -0
- metadata +101 -36
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "aho-corasick"
|
|
7
|
+
version = "1.1.5"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"memchr",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "anyhow"
|
|
16
|
+
version = "1.0.104"
|
|
17
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
|
+
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
|
19
|
+
|
|
20
|
+
[[package]]
|
|
21
|
+
name = "arrayvec"
|
|
22
|
+
version = "0.7.8"
|
|
23
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
24
|
+
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
|
25
|
+
|
|
26
|
+
[[package]]
|
|
27
|
+
name = "bindgen"
|
|
28
|
+
version = "0.72.1"
|
|
29
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
30
|
+
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
|
31
|
+
dependencies = [
|
|
32
|
+
"bitflags",
|
|
33
|
+
"cexpr",
|
|
34
|
+
"clang-sys",
|
|
35
|
+
"itertools",
|
|
36
|
+
"proc-macro2",
|
|
37
|
+
"quote",
|
|
38
|
+
"regex",
|
|
39
|
+
"rustc-hash",
|
|
40
|
+
"shlex",
|
|
41
|
+
"syn",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[[package]]
|
|
45
|
+
name = "bitflags"
|
|
46
|
+
version = "2.13.1"
|
|
47
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
48
|
+
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
|
49
|
+
|
|
50
|
+
[[package]]
|
|
51
|
+
name = "cexpr"
|
|
52
|
+
version = "0.6.0"
|
|
53
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
54
|
+
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
|
55
|
+
dependencies = [
|
|
56
|
+
"nom",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[[package]]
|
|
60
|
+
name = "cfg-if"
|
|
61
|
+
version = "1.0.4"
|
|
62
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
63
|
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
64
|
+
|
|
65
|
+
[[package]]
|
|
66
|
+
name = "chessie"
|
|
67
|
+
version = "2.0.0"
|
|
68
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
69
|
+
checksum = "69f7ee9c07973fe2b0afe17de4ae2e1158aede0912d254a3be2d3a3b26386d23"
|
|
70
|
+
dependencies = [
|
|
71
|
+
"anyhow",
|
|
72
|
+
"arrayvec",
|
|
73
|
+
"chessie_types",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[[package]]
|
|
77
|
+
name = "chessie_types"
|
|
78
|
+
version = "2.0.0"
|
|
79
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
80
|
+
checksum = "ccab5c6928f8d27e6133280700b6a76e4e5d23353e8e1864a9a99c0f91efbb31"
|
|
81
|
+
dependencies = [
|
|
82
|
+
"anyhow",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[[package]]
|
|
86
|
+
name = "clang-sys"
|
|
87
|
+
version = "1.9.1"
|
|
88
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
89
|
+
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
|
90
|
+
dependencies = [
|
|
91
|
+
"glob",
|
|
92
|
+
"libc",
|
|
93
|
+
"libloading",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[[package]]
|
|
97
|
+
name = "either"
|
|
98
|
+
version = "1.17.0"
|
|
99
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
100
|
+
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
|
101
|
+
|
|
102
|
+
[[package]]
|
|
103
|
+
name = "glob"
|
|
104
|
+
version = "0.3.4"
|
|
105
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
106
|
+
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
|
107
|
+
|
|
108
|
+
[[package]]
|
|
109
|
+
name = "itertools"
|
|
110
|
+
version = "0.13.0"
|
|
111
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
112
|
+
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
|
113
|
+
dependencies = [
|
|
114
|
+
"either",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
[[package]]
|
|
118
|
+
name = "lazy_static"
|
|
119
|
+
version = "1.5.0"
|
|
120
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
121
|
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|
122
|
+
|
|
123
|
+
[[package]]
|
|
124
|
+
name = "libc"
|
|
125
|
+
version = "0.2.189"
|
|
126
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
127
|
+
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|
128
|
+
|
|
129
|
+
[[package]]
|
|
130
|
+
name = "libloading"
|
|
131
|
+
version = "0.8.9"
|
|
132
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
133
|
+
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
|
134
|
+
dependencies = [
|
|
135
|
+
"cfg-if",
|
|
136
|
+
"windows-link",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
[[package]]
|
|
140
|
+
name = "magnus"
|
|
141
|
+
version = "0.8.2"
|
|
142
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
143
|
+
checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
|
|
144
|
+
dependencies = [
|
|
145
|
+
"magnus-macros",
|
|
146
|
+
"rb-sys",
|
|
147
|
+
"rb-sys-env",
|
|
148
|
+
"seq-macro",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
[[package]]
|
|
152
|
+
name = "magnus-macros"
|
|
153
|
+
version = "0.8.0"
|
|
154
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
155
|
+
checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
|
|
156
|
+
dependencies = [
|
|
157
|
+
"proc-macro2",
|
|
158
|
+
"quote",
|
|
159
|
+
"syn",
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
[[package]]
|
|
163
|
+
name = "memchr"
|
|
164
|
+
version = "2.8.3"
|
|
165
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
166
|
+
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|
167
|
+
|
|
168
|
+
[[package]]
|
|
169
|
+
name = "minimal-lexical"
|
|
170
|
+
version = "0.2.1"
|
|
171
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
172
|
+
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
173
|
+
|
|
174
|
+
[[package]]
|
|
175
|
+
name = "nom"
|
|
176
|
+
version = "7.1.3"
|
|
177
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
178
|
+
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
179
|
+
dependencies = [
|
|
180
|
+
"memchr",
|
|
181
|
+
"minimal-lexical",
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
[[package]]
|
|
185
|
+
name = "pgn2-bitboard"
|
|
186
|
+
version = "0.1.0"
|
|
187
|
+
dependencies = [
|
|
188
|
+
"chessie",
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
[[package]]
|
|
192
|
+
name = "pgn2_native"
|
|
193
|
+
version = "0.1.0"
|
|
194
|
+
dependencies = [
|
|
195
|
+
"magnus",
|
|
196
|
+
"pgn2-bitboard",
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
[[package]]
|
|
200
|
+
name = "proc-macro2"
|
|
201
|
+
version = "1.0.107"
|
|
202
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
203
|
+
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
|
204
|
+
dependencies = [
|
|
205
|
+
"unicode-ident",
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
[[package]]
|
|
209
|
+
name = "quote"
|
|
210
|
+
version = "1.0.47"
|
|
211
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
212
|
+
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
|
213
|
+
dependencies = [
|
|
214
|
+
"proc-macro2",
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
[[package]]
|
|
218
|
+
name = "rb-sys"
|
|
219
|
+
version = "0.9.128"
|
|
220
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
221
|
+
checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
|
|
222
|
+
dependencies = [
|
|
223
|
+
"rb-sys-build",
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
[[package]]
|
|
227
|
+
name = "rb-sys-build"
|
|
228
|
+
version = "0.9.128"
|
|
229
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
230
|
+
checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
|
|
231
|
+
dependencies = [
|
|
232
|
+
"bindgen",
|
|
233
|
+
"lazy_static",
|
|
234
|
+
"proc-macro2",
|
|
235
|
+
"quote",
|
|
236
|
+
"regex",
|
|
237
|
+
"shell-words",
|
|
238
|
+
"syn",
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
[[package]]
|
|
242
|
+
name = "rb-sys-env"
|
|
243
|
+
version = "0.2.3"
|
|
244
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
245
|
+
checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
|
|
246
|
+
|
|
247
|
+
[[package]]
|
|
248
|
+
name = "regex"
|
|
249
|
+
version = "1.13.1"
|
|
250
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
251
|
+
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
|
252
|
+
dependencies = [
|
|
253
|
+
"aho-corasick",
|
|
254
|
+
"memchr",
|
|
255
|
+
"regex-automata",
|
|
256
|
+
"regex-syntax",
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
[[package]]
|
|
260
|
+
name = "regex-automata"
|
|
261
|
+
version = "0.4.18"
|
|
262
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
263
|
+
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
|
264
|
+
dependencies = [
|
|
265
|
+
"aho-corasick",
|
|
266
|
+
"memchr",
|
|
267
|
+
"regex-syntax",
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
[[package]]
|
|
271
|
+
name = "regex-syntax"
|
|
272
|
+
version = "0.8.11"
|
|
273
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
274
|
+
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|
275
|
+
|
|
276
|
+
[[package]]
|
|
277
|
+
name = "rustc-hash"
|
|
278
|
+
version = "2.1.3"
|
|
279
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
280
|
+
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
|
281
|
+
|
|
282
|
+
[[package]]
|
|
283
|
+
name = "seq-macro"
|
|
284
|
+
version = "0.3.6"
|
|
285
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
286
|
+
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
|
287
|
+
|
|
288
|
+
[[package]]
|
|
289
|
+
name = "shell-words"
|
|
290
|
+
version = "1.1.1"
|
|
291
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
292
|
+
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
|
293
|
+
|
|
294
|
+
[[package]]
|
|
295
|
+
name = "shlex"
|
|
296
|
+
version = "1.3.0"
|
|
297
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
298
|
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
299
|
+
|
|
300
|
+
[[package]]
|
|
301
|
+
name = "syn"
|
|
302
|
+
version = "2.0.119"
|
|
303
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
304
|
+
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
|
305
|
+
dependencies = [
|
|
306
|
+
"proc-macro2",
|
|
307
|
+
"quote",
|
|
308
|
+
"unicode-ident",
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
[[package]]
|
|
312
|
+
name = "unicode-ident"
|
|
313
|
+
version = "1.0.24"
|
|
314
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
315
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
316
|
+
|
|
317
|
+
[[package]]
|
|
318
|
+
name = "windows-link"
|
|
319
|
+
version = "0.2.1"
|
|
320
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
321
|
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
members = ["pgn2-bitboard", "pgn2_native"]
|
|
3
|
+
resolver = "2"
|
|
4
|
+
|
|
5
|
+
# The perft oracle does real work; optimize tests so `cargo test` runs
|
|
6
|
+
# in release-like time instead of minutes.
|
|
7
|
+
[profile.test]
|
|
8
|
+
opt-level = 2
|
|
9
|
+
|
|
10
|
+
[profile.dev]
|
|
11
|
+
opt-level = 1
|
|
12
|
+
|
|
13
|
+
# The shipped native gem is built in `release`; squeeze perft nps with LTO
|
|
14
|
+
# and single codegen-unit (longer build, faster code).
|
|
15
|
+
[profile.release]
|
|
16
|
+
opt-level = 3
|
|
17
|
+
lto = true
|
|
18
|
+
codegen-units = 1
|
|
19
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
require 'mkmf'
|
|
2
|
+
require 'rb_sys/mkmf'
|
|
3
|
+
|
|
4
|
+
# The ext lives in a Cargo workspace whose root manifest is virtual, so
|
|
5
|
+
# tell cargo which member to build.
|
|
6
|
+
create_rust_makefile('pgn2_native/pgn2_native') do |builder|
|
|
7
|
+
builder.extra_cargo_args = ['--package', 'pgn2_native']
|
|
8
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
use crate::moves::{Move, MoveList};
|
|
2
|
+
|
|
3
|
+
/// A chess position backed by `chessie::Game`. Holds no chess logic of
|
|
4
|
+
/// its own; every operation delegates. `Copy` because `chessie::Game`
|
|
5
|
+
/// is `Copy`; `Default` because the magnus `Engine` wraps it in a
|
|
6
|
+
/// `RefCell` and `#[derive(Default)]`s it.
|
|
7
|
+
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
|
8
|
+
pub struct Board {
|
|
9
|
+
game: chessie::Game,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
impl Board {
|
|
13
|
+
pub fn from_fen(fen: &str) -> Result<Board, String> {
|
|
14
|
+
chessie::Game::from_fen(fen)
|
|
15
|
+
.map(|game| Board { game })
|
|
16
|
+
.map_err(|e| e.to_string())
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub fn perft(&self, depth: u32) -> u64 {
|
|
20
|
+
self.game.perft(depth as usize)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub fn legal_moves(&self) -> MoveList {
|
|
24
|
+
let moves: Vec<Move> = self
|
|
25
|
+
.game
|
|
26
|
+
.get_legal_moves()
|
|
27
|
+
.into_iter()
|
|
28
|
+
.map(Move::from_chessie)
|
|
29
|
+
.collect();
|
|
30
|
+
MoveList(moves)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//! Thin adapter over the `chessie` crate exposing the small surface the
|
|
2
|
+
//! `pgn2_native` magnus binding consumes. No chess logic lives here —
|
|
3
|
+
//! `chessie` is the engine. Kept as a separate crate so the perft oracle
|
|
4
|
+
//! stays testable in pure Rust (`cargo test -p pgn2-bitboard`) with no
|
|
5
|
+
//! Ruby in the loop.
|
|
6
|
+
|
|
7
|
+
pub mod board;
|
|
8
|
+
pub mod moves;
|
|
9
|
+
pub mod perft;
|
|
10
|
+
|
|
11
|
+
pub use board::Board;
|
|
12
|
+
pub use moves::{Move, MoveList};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/// Adapter move carrying exactly what `to_uci` and `same_target` need:
|
|
2
|
+
/// from-square, to-square, and optional promotion kind. No flag bits,
|
|
3
|
+
/// so a position-free `uci_parse` can produce a comparable token — this
|
|
4
|
+
/// preserves the existing `same_target` semantics (match on
|
|
5
|
+
/// from + to + promo only), so `legal?("e1g1")` finds the castle and
|
|
6
|
+
/// `legal?("e7e8q")` finds that exact promotion.
|
|
7
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
8
|
+
pub struct Move {
|
|
9
|
+
from: u8, // 0..=63, index = rank * 8 + file
|
|
10
|
+
to: u8,
|
|
11
|
+
promo: Option<Promo>,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
15
|
+
enum Promo {
|
|
16
|
+
Knight,
|
|
17
|
+
Bishop,
|
|
18
|
+
Rook,
|
|
19
|
+
Queen,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl Move {
|
|
23
|
+
/// Build an adapter `Move` from a `chessie::Move` (a legal move).
|
|
24
|
+
///
|
|
25
|
+
/// chessie encodes castling internally as "King takes Rook"
|
|
26
|
+
/// (`to` = the rook's square, Chess960 style). `into_standard_castle`
|
|
27
|
+
/// rewrites the `to` square to the king's destination (`e1g1`/`e1c1`),
|
|
28
|
+
/// matching the standard-UCI output the previous engine produced and
|
|
29
|
+
/// that `uci_parse` / `bitboard_spec.rb` expect. No-op for non-castle
|
|
30
|
+
/// moves.
|
|
31
|
+
pub(crate) fn from_chessie(m: chessie::Move) -> Self {
|
|
32
|
+
let m = m.into_standard_castle();
|
|
33
|
+
Move {
|
|
34
|
+
from: m.from().index() as u8,
|
|
35
|
+
to: m.to().index() as u8,
|
|
36
|
+
promo: m.promotion().map(|kind| match kind {
|
|
37
|
+
chessie::PieceKind::Knight => Promo::Knight,
|
|
38
|
+
chessie::PieceKind::Bishop => Promo::Bishop,
|
|
39
|
+
chessie::PieceKind::Rook => Promo::Rook,
|
|
40
|
+
chessie::PieceKind::Queen => Promo::Queen,
|
|
41
|
+
// Pawns/Kings never appear as a promotion kind.
|
|
42
|
+
_ => unreachable!("non-promotion PieceKind in Move::promotion"),
|
|
43
|
+
}),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// UCI string: `"e2e4"`, `"e1g1"` (castle, king from→to),
|
|
48
|
+
/// `"e7e8q"` (promotion). Equal to `chessie::Move::to_uci` for
|
|
49
|
+
/// every legal move (castle and en-passant both reduce to from+to
|
|
50
|
+
/// in UCI), so the sorted-UCI output is unchanged.
|
|
51
|
+
pub fn to_uci(self) -> String {
|
|
52
|
+
let mut s = String::with_capacity(5);
|
|
53
|
+
s.push_str(&sq_name(self.from));
|
|
54
|
+
s.push_str(&sq_name(self.to));
|
|
55
|
+
if let Some(p) = self.promo {
|
|
56
|
+
s.push(match p {
|
|
57
|
+
Promo::Knight => 'n',
|
|
58
|
+
Promo::Bishop => 'b',
|
|
59
|
+
Promo::Rook => 'r',
|
|
60
|
+
Promo::Queen => 'q',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
s
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Match on from + to + promo (the pre-existing semantics).
|
|
67
|
+
pub fn same_target(self, other: Move) -> bool {
|
|
68
|
+
self.from == other.from && self.to == other.to && self.promo == other.promo
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Thin wrapper around `Vec<Move>`; the binding calls `.iter()` on it.
|
|
73
|
+
pub struct MoveList(pub Vec<Move>);
|
|
74
|
+
|
|
75
|
+
impl MoveList {
|
|
76
|
+
pub fn iter(&self) -> std::slice::Iter<'_, Move> {
|
|
77
|
+
self.0.iter()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Parse a UCI string into a `Move` token **without** a position.
|
|
82
|
+
/// Only `to_uci`/`same_target` consume the result, so from + to + promo
|
|
83
|
+
/// is all that is needed. Returns `None` on any malformed input.
|
|
84
|
+
pub fn uci_parse(s: &str) -> Option<Move> {
|
|
85
|
+
let b = s.as_bytes();
|
|
86
|
+
if b.len() < 4 {
|
|
87
|
+
return None;
|
|
88
|
+
}
|
|
89
|
+
let from = parse_sq(&b[0..2])?;
|
|
90
|
+
let to = parse_sq(&b[2..4])?;
|
|
91
|
+
let promo = if b.len() >= 5 {
|
|
92
|
+
Some(match b[4] {
|
|
93
|
+
b'n' => Promo::Knight,
|
|
94
|
+
b'b' => Promo::Bishop,
|
|
95
|
+
b'r' => Promo::Rook,
|
|
96
|
+
b'q' => Promo::Queen,
|
|
97
|
+
_ => return None,
|
|
98
|
+
})
|
|
99
|
+
} else {
|
|
100
|
+
None
|
|
101
|
+
};
|
|
102
|
+
Some(Move { from, to, promo })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn parse_sq(t: &[u8]) -> Option<u8> {
|
|
106
|
+
let f = t[0].checked_sub(b'a')?;
|
|
107
|
+
let r = t[1].checked_sub(b'1')?;
|
|
108
|
+
if f > 7 || r > 7 {
|
|
109
|
+
return None;
|
|
110
|
+
}
|
|
111
|
+
Some(r * 8 + f)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn sq_name(idx: u8) -> String {
|
|
115
|
+
let file = (b'a' + (idx & 7)) as char;
|
|
116
|
+
let rank = (b'1' + (idx >> 3)) as char;
|
|
117
|
+
let mut s = String::with_capacity(2);
|
|
118
|
+
s.push(file);
|
|
119
|
+
s.push(rank);
|
|
120
|
+
s
|
|
121
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//! Perft oracle — the published node counts are the test. The chess
|
|
2
|
+
//! logic lives in `chessie`; this file only exercises the adapter's
|
|
3
|
+
//! `Board::from_fen` + `Board::perft` end-to-end in pure Rust (no Ruby).
|
|
4
|
+
|
|
5
|
+
#[cfg(test)]
|
|
6
|
+
mod tests {
|
|
7
|
+
use crate::board::Board;
|
|
8
|
+
|
|
9
|
+
struct Case {
|
|
10
|
+
fen: &'static str,
|
|
11
|
+
depth: u32,
|
|
12
|
+
nodes: u64,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn run(c: Case) {
|
|
16
|
+
let b = Board::from_fen(c.fen).unwrap();
|
|
17
|
+
assert_eq!(b.perft(c.depth), c.nodes, "fen={} depth={}", c.fen, c.depth);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[test]
|
|
21
|
+
fn perft_startpos() {
|
|
22
|
+
let f = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
23
|
+
run(Case { fen: f, depth: 1, nodes: 20 });
|
|
24
|
+
run(Case { fen: f, depth: 2, nodes: 400 });
|
|
25
|
+
run(Case { fen: f, depth: 3, nodes: 8902 });
|
|
26
|
+
run(Case { fen: f, depth: 4, nodes: 197281 });
|
|
27
|
+
run(Case { fen: f, depth: 5, nodes: 4865609 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[test]
|
|
31
|
+
fn perft_kiwipete() {
|
|
32
|
+
let f = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
|
|
33
|
+
run(Case { fen: f, depth: 1, nodes: 48 });
|
|
34
|
+
run(Case { fen: f, depth: 2, nodes: 2039 });
|
|
35
|
+
run(Case { fen: f, depth: 3, nodes: 97862 });
|
|
36
|
+
run(Case { fen: f, depth: 4, nodes: 4085603 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[test]
|
|
40
|
+
fn perft_pos3() {
|
|
41
|
+
let f = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1";
|
|
42
|
+
run(Case { fen: f, depth: 1, nodes: 14 });
|
|
43
|
+
run(Case { fen: f, depth: 4, nodes: 43238 });
|
|
44
|
+
run(Case { fen: f, depth: 5, nodes: 674624 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[test]
|
|
48
|
+
fn perft_pos4() {
|
|
49
|
+
let f = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1";
|
|
50
|
+
run(Case { fen: f, depth: 1, nodes: 6 });
|
|
51
|
+
run(Case { fen: f, depth: 3, nodes: 9467 });
|
|
52
|
+
run(Case { fen: f, depth: 4, nodes: 422333 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn perft_pos5() {
|
|
57
|
+
let f = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8";
|
|
58
|
+
run(Case { fen: f, depth: 3, nodes: 62379 });
|
|
59
|
+
run(Case { fen: f, depth: 4, nodes: 2103487 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn perft_pos6() {
|
|
64
|
+
let f = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10";
|
|
65
|
+
run(Case { fen: f, depth: 3, nodes: 89890 });
|
|
66
|
+
run(Case { fen: f, depth: 4, nodes: 3894594 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
#[ignore] // slow; run with `cargo test -- --ignored`
|
|
71
|
+
fn perft_deep() {
|
|
72
|
+
let s = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
73
|
+
run(Case { fen: s, depth: 6, nodes: 119060324 });
|
|
74
|
+
let k = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
|
|
75
|
+
run(Case { fen: k, depth: 5, nodes: 193690690 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The old `make_unmake_symmetry_via_perft` test is intentionally
|
|
79
|
+
// removed: it exercised the hand-rolled make/unmake internals, which
|
|
80
|
+
// no longer exist. chessie's own test suite covers its make/unmake.
|
|
81
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
use std::cell::RefCell;
|
|
2
|
+
|
|
3
|
+
use magnus::prelude::*;
|
|
4
|
+
use magnus::{method, Error, Ruby};
|
|
5
|
+
use pgn2_bitboard::Board;
|
|
6
|
+
|
|
7
|
+
/// `PGN::Bitboard::Engine` — a magic/pext bitboard engine holding a position.
|
|
8
|
+
///
|
|
9
|
+
/// Constructed with a FEN string; `#perft(depth)` does a full-width perft.
|
|
10
|
+
/// The wrapped board is held in a `RefCell`; `perft` borrows immutably (it
|
|
11
|
+
/// clones internally for make/unmake), so no `&mut` crosses the FFI boundary.
|
|
12
|
+
#[derive(Default)]
|
|
13
|
+
#[magnus::wrap(class = "PGN::Bitboard::Engine", free_immediately)]
|
|
14
|
+
struct Engine(RefCell<Board>);
|
|
15
|
+
|
|
16
|
+
impl Engine {
|
|
17
|
+
fn initialize(ruby: &Ruby, rb_self: &Self, fen: String) -> Result<(), Error> {
|
|
18
|
+
let board = Board::from_fen(&fen)
|
|
19
|
+
.map_err(|e| Error::new(ruby.exception_arg_error(), e))?;
|
|
20
|
+
*rb_self.0.borrow_mut() = board;
|
|
21
|
+
Ok(())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn perft(&self, depth: u32) -> u64 {
|
|
25
|
+
self.0.borrow().perft(depth)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn legal_moves_ruby(&self) -> Vec<String> {
|
|
29
|
+
let mut v: Vec<String> = self.0.borrow().legal_moves().iter().map(|m| m.to_uci()).collect();
|
|
30
|
+
v.sort();
|
|
31
|
+
v
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn legal_p(&self, uci: String) -> bool {
|
|
35
|
+
match pgn2_bitboard::moves::uci_parse(&uci) {
|
|
36
|
+
Some(parsed) => self.0.borrow().legal_moves().iter().any(|m| m.same_target(parsed)),
|
|
37
|
+
None => false,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[magnus::init]
|
|
43
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
44
|
+
let engine = ruby
|
|
45
|
+
.define_module("PGN")?
|
|
46
|
+
.define_module("Bitboard")?
|
|
47
|
+
.define_class("Engine", ruby.class_object())?;
|
|
48
|
+
engine.define_alloc_func::<Engine>();
|
|
49
|
+
engine.define_method("initialize", method!(Engine::initialize, 1))?;
|
|
50
|
+
engine.define_method("perft", method!(Engine::perft, 1))?;
|
|
51
|
+
engine.define_method("legal_moves", method!(Engine::legal_moves_ruby, 0))?;
|
|
52
|
+
engine.define_method("legal?", method!(Engine::legal_p, 1))?;;
|
|
53
|
+
Ok(())
|
|
54
|
+
}
|
data/lib/pgn/bitboard.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Load the chessie-backed native bitboard engine if the compiled
|
|
4
|
+
# extension is available. End users get the precompiled native gem (no
|
|
5
|
+
# Rust toolchain needed); if the extension is absent (e.g. running from a
|
|
6
|
+
# source checkout without `rake compile`), the load fails silently so
|
|
7
|
+
# the rest of the gem still works. Code that calls PGN::Bitboard::Engine
|
|
8
|
+
# will raise NameError naturally.
|
|
9
|
+
begin
|
|
10
|
+
require 'pgn2_native/pgn2_native'
|
|
11
|
+
rescue StandardError
|
|
12
|
+
LoadError
|
|
13
|
+
end
|