pgn2 2.0.0 → 2.0.2
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/release-gems.yml +5 -2
- data/CHANGELOG.md +101 -2
- data/README.md +27 -0
- data/Rakefile +18 -0
- data/TODO.md +25 -107
- data/docs/superpowers/plans/2026-08-15-game-tree-api-plan.md +1014 -0
- data/docs/superpowers/plans/2026-08-15-small-medium-roadmap-plan.md +384 -0
- data/docs/superpowers/specs/2026-08-15-game-tree-api-design.md +387 -0
- data/ext/pgn2_native/pgn2-bitboard/src/board.rs +2 -10
- data/ext/pgn2_native/pgn2-bitboard/src/lib.rs +0 -2
- data/ext/pgn2_native/pgn2_native/src/lib.rs +5 -5
- data/lib/pgn/attack.rb +107 -0
- data/lib/pgn/epd.rb +54 -0
- data/lib/pgn/fen.rb +54 -44
- data/lib/pgn/game.rb +91 -7
- data/lib/pgn/move.rb +4 -4
- data/lib/pgn/node.rb +347 -0
- data/lib/pgn/notation.rb +8 -66
- data/lib/pgn/pgn_parser.rb +30 -31
- data/lib/pgn/pgn_parser.y +14 -15
- data/lib/pgn/position.rb +193 -0
- data/lib/pgn/version.rb +1 -1
- data/lib/pgn.rb +3 -0
- data/spec/castling_normalization_spec.rb +39 -0
- data/spec/comment_round_trip_spec.rb +35 -0
- data/spec/epd_spec.rb +44 -0
- data/spec/fen_spec.rb +6 -0
- data/spec/game_history_spec.rb +46 -0
- data/spec/game_spec.rb +46 -0
- data/spec/movetext_clean_spec.rb +44 -0
- data/spec/node_spec.rb +222 -0
- data/spec/outcome_spec.rb +93 -0
- data/spec/parser_left_recursion_spec.rb +37 -0
- data/spec/position_attack_spec.rb +56 -0
- data/spec/position_legal_spec.rb +123 -0
- data/spec/spec_helper.rb +17 -0
- metadata +18 -3
- data/ext/pgn2_native/pgn2-bitboard/src/moves.rs +0 -121
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fdc6dcb9543c25e2e1102902d031f9d733463780b6f5d16a1a4713c6cfe16b9
|
|
4
|
+
data.tar.gz: 051ba77cdb0e657909ef5a22c1470f229c883e2bcb70cbfd3b9dbfbb83e92b8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3999930da7369748415bfb222108d436a64c67bf8e91c5b0d12d9dc5ab38f64c78f1e1e56abca9454ed8e1a215df0918d152e37b1271808d8e1962088029a0e7
|
|
7
|
+
data.tar.gz: 2dec3d79ee03350d252bd846a789b3b96091f6ec34cd788d60e267506c2837436659395472fef192f3aeec8290ef32e3f7bd235308062a1de12f1add0e110ab7
|
|
@@ -6,9 +6,12 @@ name: release-gems
|
|
|
6
6
|
# toolchain required. Push to RubyGems is gated on a published release;
|
|
7
7
|
# `workflow_dispatch` builds + uploads artifacts without pushing.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
9
|
+
# The target platform list is configured in the Rakefile's
|
|
10
|
+
# Rake::ExtensionTask (ext.cross_platform); keep that list as the single
|
|
11
|
+
# source of truth.
|
|
12
|
+
#
|
|
13
|
+
# Targets:
|
|
10
14
|
# x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin
|
|
11
|
-
# (configure the platform list in Rakefile cross-compile settings.)
|
|
12
15
|
|
|
13
16
|
on:
|
|
14
17
|
release:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
3
|
+
## 2.0.2 (2026-08-18)
|
|
4
|
+
|
|
5
|
+
### Summary
|
|
6
|
+
|
|
7
|
+
Internal refactor and performance pass — no public API changes. The
|
|
8
|
+
`pgn2-bitboard` adapter drops its hand-rolled `Move`/`MoveList`/`uci_parse`
|
|
9
|
+
layer in favor of `chessie`'s own `MoveList` and `Move: PartialEq<str>`
|
|
10
|
+
(UCI comparison), and the Ruby side DRYs shared FEN/EPD parsing, memoizes
|
|
11
|
+
`Position#in_check?`, and maintains `Game`'s position cache incrementally.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`ext/pgn2_native`** — remove `pgn2-bitboard/src/moves.rs`;
|
|
16
|
+
`Board#legal_moves` returns `chessie::MoveList` directly and
|
|
17
|
+
`Engine#legal?` compares via `chessie::Move`'s `PartialEq<str>` (by
|
|
18
|
+
`to_uci()`), eliminating the separate UCI parser.
|
|
19
|
+
- **`PGN::PositionFields`** — shared FEN/EPD module for board-string and
|
|
20
|
+
castling/en-passant field parsing; `FEN` and `EPD` now include it instead
|
|
21
|
+
of duplicating the logic.
|
|
22
|
+
- **`PGN::Attack.attacked?`** — short-circuits pawn/knight/king checks before
|
|
23
|
+
the slider ray-walk (cheapest first), matching the previous result.
|
|
24
|
+
- **`PGN::Position#in_check?`** — memoizes its result on first call.
|
|
25
|
+
- **`PGN::Game`** — `push`/`pop` now grow/shrink the memoized `@positions`
|
|
26
|
+
list in step rather than invalidating it; `threefold?` reuses `#positions`.
|
|
27
|
+
- **`PGN::MoveText.normalize_castling`** — castling `0`→`O` normalization
|
|
28
|
+
extracted as a class method (was a private `Node` helper).
|
|
29
|
+
- **`PGN::Node`** — `promote`/`demote`/`promote_to_main`/`demote_to_last`/
|
|
30
|
+
`delete` share a `mutate_sibling` prologue/epilogue and a `splice_line!`
|
|
31
|
+
helper; behavior unchanged.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2.0.1 (2026-08-15)
|
|
4
36
|
|
|
5
37
|
### Summary
|
|
6
38
|
|
|
@@ -22,6 +54,16 @@ install/packaging contract — hence 2.0.0 even though the Ruby API is
|
|
|
22
54
|
purely additive (`PGN::Bitboard` is new; existing classes are untouched).
|
|
23
55
|
|
|
24
56
|
### Added
|
|
57
|
+
- **`PGN::Node`** — a navigable, mutable game-tree view via
|
|
58
|
+
`PGN::Game#root`: parent/children/variation links, a lazy cached
|
|
59
|
+
pure-Ruby `Node#position` (no native-engine dependency), and variation
|
|
60
|
+
management (`add_variation`, `add_main_variation`, `promote`, `demote`,
|
|
61
|
+
`promote_to_main`, `demote_to_last`, `delete`). `MoveText` remains the
|
|
62
|
+
source of truth; mutations edit it in place and normalize the affected
|
|
63
|
+
branching point to flat sibling storage, so `Game#moves` and the
|
|
64
|
+
serializer stay correct and PGN round-trips remain byte-identical for
|
|
65
|
+
un-mutated games. Non-breaking — `Game#moves` and `MoveText` keep their
|
|
66
|
+
shapes.
|
|
25
67
|
- **`PGN::Bitboard::Engine`** (native): `Engine.new(fen)`, `#perft(depth)`,
|
|
26
68
|
`#legal_moves` (sorted UCI), `#legal?(uci)`. Validates against the
|
|
27
69
|
published perft suite (startpos depth 6 = 119,060,324; Kiwipete depth 5
|
|
@@ -67,10 +109,67 @@ undefined when the extension is absent.
|
|
|
67
109
|
|
|
68
110
|
---
|
|
69
111
|
|
|
70
|
-
##
|
|
112
|
+
## 2.0.0 (2026-08-15)
|
|
71
113
|
|
|
72
114
|
### Summary
|
|
73
115
|
|
|
116
|
+
(Pre-release snapshot of the native Rust bitboard perft backend, before
|
|
117
|
+
the small/medium roadmap and game-tree API work landed in 2.0.1.)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### Small + medium roadmap
|
|
122
|
+
|
|
123
|
+
Small + medium TODO pass: public legal-move and outcome APIs on
|
|
124
|
+
`PGN::Position`/`PGN::Game` delegating to the chessie engine, plus parsing
|
|
125
|
+
and packaging hardening. Serialized PGN/FEN output stays byte-identical
|
|
126
|
+
except where a task explicitly normalizes a quirk (nested comments are now
|
|
127
|
+
escaped on output and unescaped on parse for stable round trips); 311
|
|
128
|
+
specs green, RuboCop clean.
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
- **`PGN::Position#legal?(san_or_uci)`** and **`#legal_moves_san`**:
|
|
132
|
+
public legal-move API. `legal?` accepts SAN or UCI and resolves SAN by
|
|
133
|
+
matching `PGN::Notation.san` against the engine's legal moves (rejecting
|
|
134
|
+
ambiguous SAN). Raises `NameError` when the extension is absent.
|
|
135
|
+
- **`PGN::Attack`** module + **`PGN::Position#in_check?`**, **`#attackers`**,
|
|
136
|
+
**`#mover_color`**, **`#opponent_color`**: attack detection extracted from
|
|
137
|
+
`Notation` private methods and exposed on `Position`.
|
|
138
|
+
- **Game/position outcome detection**: `Position#checkmate?`/`#stalemate?`/
|
|
139
|
+
`#insufficient_material?`/`#fifty_move?`/`#outcome`, and `Game#threefold?`
|
|
140
|
+
(streams Zobrist hashes via `each_position`) / `Game#outcome`.
|
|
141
|
+
- **`PGN::EPD`** read/write + **`PGN::FEN#to_epd`**: EPD shares FEN's first
|
|
142
|
+
four fields and keeps trailing operation fields verbatim.
|
|
143
|
+
- **`PGN::Game#push(san)`** / **`#pop`**: mutable history; `push` validates
|
|
144
|
+
legality (raises `ArgumentError`) and invalidates the memoized position
|
|
145
|
+
list.
|
|
146
|
+
|
|
147
|
+
### Changed
|
|
148
|
+
- **`PGN::Move`**: parses `0-0`/`0-0-0` as castling (alongside `O-O`), so
|
|
149
|
+
UCI-style castling normalizes to canonical SAN through one path.
|
|
150
|
+
- **Parser**: `tag_section` and `variation_list` are now ordinary
|
|
151
|
+
left-recursion with a single explicit `.reverse` (and first-occurrence-wins
|
|
152
|
+
tag merge) where each list is consumed; parse output is byte-identical
|
|
153
|
+
(same 2 shift/reduce conflicts).
|
|
154
|
+
- **`MoveText#clean_text`**: idempotent — strips a single outermost brace
|
|
155
|
+
pair and unescapes `\{`/`\}`/`\\` into the canonical raw comment body, so
|
|
156
|
+
`Serializer` escaping and the lexer are symmetric and nested-comment
|
|
157
|
+
round trips are byte-stable. `Game#moves=` no longer sniffs comments for
|
|
158
|
+
leftover braces.
|
|
159
|
+
- **`FEN#to_position`**: en passant square is no longer dropped (Ruby
|
|
160
|
+
conditional-assignment gotcha).
|
|
161
|
+
- **Rakefile**: `Rake::ExtensionTask` now sets `cross_compile`/`cross_platform`
|
|
162
|
+
(single source of truth for `release-gems.yml`) and adds the `native:clean`
|
|
163
|
+
task that `native:gem` referenced.
|
|
164
|
+
|
|
165
|
+
### Fixed
|
|
166
|
+
- `FEN#to_position` en-passant round-trip regression (position lost the ep
|
|
167
|
+
square, which would have corrupted `legal?` and outcome detection).
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### Attack masks
|
|
172
|
+
|
|
74
173
|
Attack-mask pass: precomputed knight/king on-board target tables on
|
|
75
174
|
`PGN::Board`, used by `Notation` (reaches/attacked/leaper moves) and
|
|
76
175
|
`MoveCalculator` (knight/king origin lookup), plus a retained-memory
|
data/README.md
CHANGED
|
@@ -145,6 +145,33 @@ It handles captures, en passant, promotions, legal-move disambiguation
|
|
|
145
145
|
(file / rank / full square, respecting pins), and check (`+`) / checkmate
|
|
146
146
|
(`#`) suffixes.
|
|
147
147
|
|
|
148
|
+
### Navigating and mutating the game tree
|
|
149
|
+
|
|
150
|
+
{PGN::Game#root} returns a navigable {PGN::Node} tree over the mainline and
|
|
151
|
+
its variations. A node knows its parent, its children (the mainline
|
|
152
|
+
continuation first, then the variations), and the {PGN::Position} it
|
|
153
|
+
represents. Mutations edit the underlying structure in place; call `#root`
|
|
154
|
+
again for a fresh tree after mutating.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
> game = PGN.parse(File.read("./examples/immortal_game.pgn")).first
|
|
158
|
+
> root = game.root
|
|
159
|
+
> root.main_line.map(&:notation) # => ["e4", "e5", ...]
|
|
160
|
+
> root.next.next.children.map(&:notation) # alternatives at that position
|
|
161
|
+
> root.next.next.position.to_fen.to_s # the FEN after 1.e4 e5
|
|
162
|
+
|
|
163
|
+
> root.next.next.add_variation("Nc6") # add a variation
|
|
164
|
+
> root = game.root # fresh tree after mutation
|
|
165
|
+
> root.next.next.children.find { |n| n.notation == "Nc6" }.promote_to_main
|
|
166
|
+
> game.to_pgn # serialized with the new mainline
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`Node#position` is pure-Ruby (no native engine); it replays from the
|
|
170
|
+
starting position and raises on an illegal SAN exactly like
|
|
171
|
+
`Game#positions`. See `spec/node_spec.rb` for the full surface
|
|
172
|
+
(`add_variation`, `add_main_variation`, `promote`, `demote`,
|
|
173
|
+
`promote_to_main`, `demote_to_last`, `delete`).
|
|
174
|
+
|
|
148
175
|
## Benchmarks
|
|
149
176
|
|
|
150
177
|
A reproducible profiling harness lives in `bench/`. It measures the
|
data/Rakefile
CHANGED
|
@@ -6,6 +6,18 @@ spec = Gem::Specification.load('pgn2.gemspec')
|
|
|
6
6
|
Rake::ExtensionTask.new('pgn2_native', spec) do |ext|
|
|
7
7
|
ext.ext_dir = 'ext/pgn2_native'
|
|
8
8
|
ext.lib_dir = 'lib/pgn2_native'
|
|
9
|
+
|
|
10
|
+
# Cross-compile prebuilt platform gems via rake-compiler-dock. The list
|
|
11
|
+
# here is the single source of truth for {release-gems.yml}; keep them in
|
|
12
|
+
# sync. rb-sys's extconf picks up the Rake::ExtensionTask cross env vars.
|
|
13
|
+
ext.cross_compile = true
|
|
14
|
+
ext.cross_platform = %w[
|
|
15
|
+
x86_64-linux
|
|
16
|
+
aarch64-linux
|
|
17
|
+
x86_64-darwin
|
|
18
|
+
aarch64-darwin
|
|
19
|
+
]
|
|
20
|
+
ext.cross_config_options << '--enable-cross'
|
|
9
21
|
end
|
|
10
22
|
|
|
11
23
|
RuboCop::RakeTask.new
|
|
@@ -36,6 +48,12 @@ task bench: ['bench:moves', 'bench:parse']
|
|
|
36
48
|
# Produces fat-binary platform gems so end users (and the chessellence
|
|
37
49
|
# Docker build) need no Rust toolchain. Requires Docker locally.
|
|
38
50
|
namespace :native do
|
|
51
|
+
desc 'Remove cross-compiled build artifacts and pkg/ (Docker build prep)'
|
|
52
|
+
task :clean do
|
|
53
|
+
Rake::Task['clean'].invoke
|
|
54
|
+
rm_rf 'pkg' if Dir.exist?('pkg')
|
|
55
|
+
end
|
|
56
|
+
|
|
39
57
|
desc 'Cross-compile prebuilt platform gems via rake-compiler-dock'
|
|
40
58
|
task :gem do
|
|
41
59
|
require 'rake_compiler_dock'
|
data/TODO.md
CHANGED
|
@@ -3,70 +3,8 @@
|
|
|
3
3
|
## Parsing
|
|
4
4
|
|
|
5
5
|
- Accept a more flexible input format
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Misc
|
|
10
|
-
|
|
11
|
-
- Support converting a game to pgn format
|
|
12
|
-
- Speed up parsing
|
|
13
|
-
- ✓ (done in 1.2.0) Removed `PGN::Lexer`'s per-token `Token` Struct
|
|
14
|
-
allocation on the parser hot path (`next_token_pair`), and collapsed
|
|
15
|
-
`scan_one`'s `[type, m, discarded]` tuple to a single returned string
|
|
16
|
-
(type/discarded stashed in ivars). The full `Token` is kept only for the
|
|
17
|
-
`#tokens` spec helper. Parse allocations −42% (603537 → 347037 / 500 games).
|
|
18
|
-
- Speed up replay via a board-representation rewrite ("Approach B"): done.
|
|
19
|
-
(b) ✓ (done in 1.3.0) Rewrote `Board` internals to the classic 0x88
|
|
20
|
-
representation (128-cell array indexed by `rank*16+file`) and rewrote
|
|
21
|
-
`MoveCalculator` to work entirely in single-integer square indices via
|
|
22
|
-
`Board#at_index`/`#apply!`, so the replay hot path no longer allocates
|
|
23
|
-
`[file,rank]` coordinate arrays or square-name strings. Off-board is a
|
|
24
|
-
single bitmask (`(idx & 0x88).zero?`, ~1.6x faster than a 0..7 bounds
|
|
25
|
-
check) and ray stepping is a single integer add. Algorithm unchanged, so
|
|
26
|
-
output is byte-identical. Measured (immortal game): replay 798→535 µs/i
|
|
27
|
-
(+49% throughput), allocations 1571→976 objects (−38%) / 92440→62064 bytes
|
|
28
|
-
(−33%); parse+replay +21% throughput. 182 specs green, 0 new rubocop
|
|
29
|
-
offenses vs main. The public string/coord API is preserved (additive).
|
|
30
|
-
(c) One related idea was left alone during cleanup rather than "fixed",
|
|
31
|
-
since fixing it would cost more than it's worth right now: `Board#squares`
|
|
32
|
-
rebuilds the full 8x8 array from `@cells` on every call (9 allocations,
|
|
33
|
-
64 reads); it's off the replay hot path by design, but `FEN#to_s`
|
|
34
|
-
round-trips through it on every position-to-FEN call, so FEN generation
|
|
35
|
-
pays that cost repeatedly. Memoizing would mean invalidating the cache
|
|
36
|
-
from `update`/`apply!`, i.e. adding a write to the actual hot path to
|
|
37
|
-
speed up a path that isn't hot -- the wrong trade; if FEN generation
|
|
38
|
-
becomes hot, have it read `@cells` directly instead. Also considered
|
|
39
|
-
and not attempted: column-granularity copy-on-write in `dup` (the pre-0x88
|
|
40
|
-
Board only duplicated touched file-columns on write); the flat 0x88 array
|
|
41
|
-
trades that away for simplicity and the +49% throughput measured above,
|
|
42
|
-
and reintroducing it would need its own A/B before it's worth the
|
|
43
|
-
complexity.
|
|
44
|
-
(a) ✗ (attempted, rejected) A piece-location index (piece → 0x88 indices)
|
|
45
|
-
maintained in `update`/`apply!` and used for O(1) slider/leaper/king
|
|
46
|
-
origin lookups. Implemented on top of (b), all 182 specs green, but it
|
|
47
|
-
**regressed**: replay 526→727 µs/i (+38% slower), allocations 976→1591
|
|
48
|
-
objects (+63%). Root cause: `Board#dup` (called every move) must clone
|
|
49
|
-
the index (`transform_values(&:dup)` ≈ 12 piece arrays) — Board#dup went
|
|
50
|
-
91→676 objects — and every move pays per-update index maintenance
|
|
51
|
-
(`<<`/`delete`) that pawns (the most common move type, whose origins are
|
|
52
|
-
geometry-fixed and can't use the index) pay for no benefit. The index
|
|
53
|
-
helps sliders/leapers (minority of moves) but the dup + maintenance cost
|
|
54
|
-
is paid by every move. Conclusion: a global piece index is a loss for
|
|
55
|
-
replay (where only ONE given move is validated, so ray-scanning from the
|
|
56
|
-
destination is already cheap); it pays in move-_generation_ libraries
|
|
57
|
-
(chess.js/python-chess) that enumerate ALL legal moves. Not worth a COW
|
|
58
|
-
variant either (maintenance + pawns). Reverted; (b) alone is the winner.
|
|
59
|
-
- Replace the right-recursive `tag_section`/`variation_list` rules in
|
|
60
|
-
`pgn_parser.y` with ordinary left-recursion plus one explicit `.reverse`
|
|
61
|
-
at the point each list is consumed, so the legacy whittle-order
|
|
62
|
-
compatibility quirk is a single greppable line instead of implicit in
|
|
63
|
-
recursion direction.
|
|
64
|
-
- Make `MoveText#clean_text` idempotent (or run it exactly once, at
|
|
65
|
-
construction) so `Game#moves=`/`#standardize_castling` doesn't need to
|
|
66
|
-
sniff a comment for leftover `{`/`}` to decide whether a MoveText is safe
|
|
67
|
-
to reuse as-is. The brace check is a bandaid for `clean_text` not fully
|
|
68
|
-
normalizing multi-line/nested comments in one pass; fixing that at the
|
|
69
|
-
source would let `moves=` reuse unconditionally.
|
|
6
|
+
- Tolerant parse mode: collect warnings/errors instead of failing on the
|
|
7
|
+
first bad move.
|
|
70
8
|
|
|
71
9
|
## Roadmap ideas (from pioz/chess + python-chess review)
|
|
72
10
|
|
|
@@ -76,58 +14,38 @@ Out of scope for now: SVG rendering, Chess960, Shredder-FEN, FRC castling.
|
|
|
76
14
|
|
|
77
15
|
- [ ] Streaming/lazy PGN reader: yield games from an `IO` without slurping
|
|
78
16
|
the whole file.
|
|
79
|
-
- [ ] EPD read/write (`EPD#to_position`, `FEN#to_epd`).
|
|
80
|
-
- [ ] Richer game-tree API: node-style mainline/variations, add/promote/
|
|
81
|
-
demote variations.
|
|
82
|
-
- [ ] Nested-comment normalization + brace escaping for byte-perfect round
|
|
83
|
-
trips.
|
|
84
|
-
- [ ] Tolerant parse mode: collect warnings/errors instead of failing on the
|
|
85
|
-
first bad move.
|
|
86
17
|
|
|
87
18
|
### Group 2 — Position Intelligence / Game Rules
|
|
88
19
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
|
|
20
|
+
`PGN::Bitboard::Engine` is a thin adapter over the `chessie` crate, so
|
|
21
|
+
several of these are implemented by delegating from `PGN::Position` to the
|
|
22
|
+
native engine rather than rewriting the logic in pure Ruby.
|
|
23
|
+
|
|
24
|
+
- [ ] Pin-aware helpers beyond `Position#attackers` (e.g. pinned-piece
|
|
25
|
+
detection, discovered-check detection), delegating to chessie where
|
|
26
|
+
useful.
|
|
96
27
|
|
|
97
28
|
### Group 3 — Engine & Analysis Integration
|
|
98
29
|
|
|
99
30
|
- [ ] Lightweight UCI/XBoard engine wrapper (`PGN::Engine`-style).
|
|
100
31
|
- [ ] PGN annotation helpers: auto-generate NAGs/comments from engine info.
|
|
101
32
|
- [ ] Optional Polyglot opening-book reader and/or Syzygy tablebase prober.
|
|
102
|
-
- [ ] UCI-style castling normalization (see pioz/chess).
|
|
103
33
|
|
|
104
|
-
### Group 4 — Performance / Internals
|
|
34
|
+
### Group 4 — Performance / Internals
|
|
105
35
|
|
|
106
|
-
- [ ] FEN serializer that walks the 0x88 `@cells` array directly (skip
|
|
107
|
-
rebuilding `board.squares`).
|
|
108
36
|
- [ ] Incremental Zobrist hashing in `Board`/`Position` for fast repetition /
|
|
109
|
-
transposition checks.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
`
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
(~2× off Stockfish). Keep the make/unmake path for the Ruby
|
|
125
|
-
`#legal_moves` API; only the `perft` hot path switches. Validated by
|
|
126
|
-
the existing perft oracle vs Stockfish (`bench/cross_check.rb`).
|
|
127
|
-
- [ ] Bulk-count at depth 1 (small, low-risk) and per-node `MoveList` buffer
|
|
128
|
-
reuse (avoid the per-node `Vec<Move>` alloc) — incremental wins below
|
|
129
|
-
the pin-aware rewrite.
|
|
130
|
-
- [ ] aarch64 (non-BMI2) falls back to the ray-walker sliders; add magic
|
|
131
|
-
bitboard tables there if its perft nps matters.
|
|
132
|
-
- [ ] Verify the `release-gems.yml` cross-compile (rake-compiler-dock) for
|
|
133
|
-
x86_64/aarch64 linux+darwin before relying on prebuilt gems.
|
|
37
|
+
transposition checks. Note: the previous attempt regressed replay
|
|
38
|
+
because `Bignum` XOR allocations on every move dwarfed the gains;
|
|
39
|
+
incremental update is only worth revisiting if something starts
|
|
40
|
+
consuming hashes on the hot path (e.g. `Game#threefold?` now streams
|
|
41
|
+
Zobrist hashes, so revisit if it shows up in profiles).
|
|
42
|
+
- [ ] Final Docker verification of the `release-gems.yml` cross-compile
|
|
43
|
+
(rake-compiler-dock) for x86_64/aarch64 linux+darwin; the Rakefile
|
|
44
|
+
cross-compile config and `native:clean` task are wired, but the full
|
|
45
|
+
Docker build still wants confirming on a machine with Docker.
|
|
46
|
+
- [ ] `Engine#legal_p`/`legal?` compares candidate moves via
|
|
47
|
+
`chessie::Move`'s `PartialEq<str>`, which allocates a `String` via
|
|
48
|
+
`to_uci()` per candidate scanned. Currently negligible (offset by
|
|
49
|
+
`legal_moves()` now being stack-allocated `ArrayVec` instead of a
|
|
50
|
+
heap `Vec`, and dwarfed by movegen/FFI cost) — only worth a cheap
|
|
51
|
+
numeric comparison if `legal?` ever ends up in a genuine hot loop.
|