pgn2 1.2.1 → 1.3.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/CHANGELOG.md +52 -0
- data/README.md +31 -8
- data/TODO.md +27 -15
- data/bench/baseline_moves.txt +6 -6
- data/bench/baseline_parse.txt +4 -4
- data/lib/pgn/board.rb +92 -25
- data/lib/pgn/move_calculator.rb +157 -162
- data/lib/pgn/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33036a2920baf0e170efc5e1c387c2a53ae91fe19ad19cf745d4d6f049c9fe20
|
|
4
|
+
data.tar.gz: 93ccbe0289a72f4af5fb8367a4d1f5bdc0c1d5eb77f1a65e3381b7d17f5d3822
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25ed517772982909d896b4b372308d235574db54aa44f3ff98f5201f9055120fc64d19fcd634c6676f7bfb66cf5e4de483a2a9ae13293b3162781339434bfac1
|
|
7
|
+
data.tar.gz: bf2a935f6aa4fedffb5976b0febc5eb351354165275d8b248eac99f7a713d74cf6acb3cb9e0be51c6030a55481663173ab5c9008cf5dfa67904bd09ac4e271f5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0 (2026-08-13)
|
|
4
|
+
|
|
5
|
+
### Summary
|
|
6
|
+
|
|
7
|
+
Replay (move-application) board-representation rewrite: `PGN::Board`
|
|
8
|
+
internals move to the classic 0x88 scheme (a 128-cell array indexed by
|
|
9
|
+
`rank*16+file`) and `PGN::MoveCalculator` works entirely in single-integer
|
|
10
|
+
square indices, so the replay hot path no longer allocates `[file,rank]`
|
|
11
|
+
coordinate arrays or square-name strings. No public API changes; serialized
|
|
12
|
+
PGN/FEN output stays byte-identical.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **`PGN::Board`**: internals rewritten to a 0x88 array (`@cells`, 128
|
|
16
|
+
entries). The public file-major `squares` 8x8 API, `at` (string/coord
|
|
17
|
+
overloads), `update`, `change!`, `position_for`, `coordinates_for`, and
|
|
18
|
+
`dup` are preserved (computed/translated at the API boundary, off the
|
|
19
|
+
hot path). New internal 0x88 accessors `index_of`, `index_for`, `at_index`,
|
|
20
|
+
`update_index`, and `apply!` (integer-keyed batch update) serve the hot
|
|
21
|
+
path. `dup` copies the 128-cell array (cheaper than the prior column COW:
|
|
22
|
+
136 → 91 objects / 10336 → 3856 bytes per 45 dupes).
|
|
23
|
+
- **`PGN::MoveCalculator`**: rewritten to address squares as 0x88 integer
|
|
24
|
+
indices throughout. `#compute_origin` returns an index; `#changes` is an
|
|
25
|
+
integer-keyed hash applied via `Board#apply!`; ray stepping (`first_piece`)
|
|
26
|
+
and off-board checks use a single integer add and the `(idx & 0x88).zero?`
|
|
27
|
+
bitmask (≈1.6x faster than a 0..7 four-integer bounds check, measured);
|
|
28
|
+
`castling_restrictions`/`en_passant_*` use integer corner indices. The
|
|
29
|
+
public `#origin` reader still returns an algebraic square string. Scan and
|
|
30
|
+
disambiguation algorithms are unchanged, so output is byte-identical.
|
|
31
|
+
- **Plan B (piece-location index) — attempted, rejected**: a `piece → 0x88
|
|
32
|
+
indices` index maintained in `update`/`apply!`, used for O(1)
|
|
33
|
+
slider/leaper/king origin lookups, was implemented on top of the 0x88
|
|
34
|
+
board and passed all 182 specs, but regressed: replay 526 → 727 µs/i
|
|
35
|
+
(+38% slower), allocations 976 → 1591 objects (+63%). `Board#dup` (called
|
|
36
|
+
every move) must clone the index (≈12 piece arrays: Board#dup 91 → 676
|
|
37
|
+
objects), and every move pays per-update index maintenance that pawns —
|
|
38
|
+
the most common move type, whose origins are geometry-fixed and can't use
|
|
39
|
+
the index — pay for no benefit. The index helps move-*generation*
|
|
40
|
+
libraries (chess.js, python-chess) that enumerate all legal moves, but
|
|
41
|
+
not replay, which validates one given move where ray-scanning from the
|
|
42
|
+
destination is already cheap. Reverted; the 0x88 board alone is the
|
|
43
|
+
winner. Rationale recorded in `TODO.md`.
|
|
44
|
+
- Performance vs 1.2.1 (immortal game, 45 plies; A/B batched median, 200
|
|
45
|
+
replies × 7, fresh process each): replay throughput 798 → 535 µs/i
|
|
46
|
+
(+49%); replay allocations 1571 → 976 objects (−38%) and 92440 → 62064
|
|
47
|
+
bytes (−33%). Full pipeline (`bench/profile_parse.rb`, 500 games):
|
|
48
|
+
parse+replay throughput 659 → 534 ms/i (+23%), allocations 1101586 →
|
|
49
|
+
831530 objects (−24.5%) and 62164136 → 47966112 bytes (−22.8%);
|
|
50
|
+
parse-only unchanged. 182 specs pass; byte-identical FEN/PGN output;
|
|
51
|
+
zero new rubocop offenses vs 1.2.1 (the rewrite is shorter on
|
|
52
|
+
ClassLength/AbcSize and leaves the same pre-existing metric offenses on
|
|
53
|
+
the same methods).
|
|
54
|
+
|
|
3
55
|
## 1.2.1 (2026-08-13)
|
|
4
56
|
|
|
5
57
|
### Summary
|
data/README.md
CHANGED
|
@@ -169,13 +169,13 @@ Move pipeline — immortal game, 45 plies (`bench/profile_moves.rb`):
|
|
|
169
169
|
|
|
170
170
|
| Metric | original `pgn` | pgn2 | Δ |
|
|
171
171
|
|---|---:|---:|---:|
|
|
172
|
-
| Replay allocations (objects) | 5124 |
|
|
173
|
-
| Replay allocations (bytes) | 262608 |
|
|
174
|
-
| `Board#dup` x45 (objects) | 451 |
|
|
175
|
-
| `Board#dup` x45 (bytes) | 43096 |
|
|
172
|
+
| Replay allocations (objects) | 5124 | 976 | -4148 (-80.9%) |
|
|
173
|
+
| Replay allocations (bytes) | 262608 | 62064 | -200544 (-76.4%) |
|
|
174
|
+
| `Board#dup` x45 (objects) | 451 | 91 | -360 (-79.8%) |
|
|
175
|
+
| `Board#dup` x45 (bytes) | 43096 | 3856 | -39240 (-91.1%) |
|
|
176
176
|
| `Board#at(str)` x1000 (objects) | 6000 | 0 | -6000 (-100%) |
|
|
177
177
|
| `Board#at(str)` x1000 (bytes) | 240000 | 0 | -240000 (-100%) |
|
|
178
|
-
| Replay throughput | 841 µs/i |
|
|
178
|
+
| Replay throughput | 841 µs/i | 517 µs/i | ~1.63x faster |
|
|
179
179
|
|
|
180
180
|
Parser — 500 immortal games (`bench/profile_parse.rb`):
|
|
181
181
|
|
|
@@ -183,10 +183,10 @@ Parser — 500 immortal games (`bench/profile_parse.rb`):
|
|
|
183
183
|
|---|---:|---:|---:|
|
|
184
184
|
| Parse-only allocations (objects) | 1248065 | 347037 | -901028 (-72.2%) |
|
|
185
185
|
| Parse-only allocations (bytes) | 120370470 | 17977414 | -102393056 (-85.1%) |
|
|
186
|
-
| Parse + replay allocations (objects) | 3778073 |
|
|
187
|
-
| Parse + replay allocations (bytes) | 249570048 |
|
|
186
|
+
| Parse + replay allocations (objects) | 3778073 | 831530 | -2946543 (-78.0%) |
|
|
187
|
+
| Parse + replay allocations (bytes) | 249570048 | 47966112 | -201603936 (-80.8%) |
|
|
188
188
|
| Parse-only throughput | 1461 ms/i | 305 ms/i | ~4.8x faster |
|
|
189
|
-
| Parse + replay throughput | 1938 ms/i |
|
|
189
|
+
| Parse + replay throughput | 1938 ms/i | 534 ms/i | ~3.6x faster |
|
|
190
190
|
|
|
191
191
|
What changed to get there:
|
|
192
192
|
|
|
@@ -216,6 +216,29 @@ What changed to get there:
|
|
|
216
216
|
tuple, so the parser hot path now allocates only the single `[type, value]`
|
|
217
217
|
array Racc requires per token. Cuts parse allocations ~42% (603537 → 347037
|
|
218
218
|
objects for 500 games).
|
|
219
|
+
13. `PGN::MoveCalculator#valid_square?` — integer bounds (`file >= 0 && file < 8`)
|
|
220
|
+
instead of `(0..7).include?` (≈3.4x faster per call, zero-alloc, in the
|
|
221
|
+
board-scan inner loops); `#compute_origin` — string `case` dispatch instead
|
|
222
|
+
of regex `/[brq]/i` matches; `#first_piece` — returns only the `[file, rank]`
|
|
223
|
+
square via a `piece_at` helper instead of a `[piece, square]` tuple. Replay
|
|
224
|
+
throughput +8.2% (766 → 741 µs/i), replay allocations −8% (1710 → 1571
|
|
225
|
+
objects). Board-scanning origin lookup is ~46% of replay CPU; the
|
|
226
|
+
piece-location-index rewrite that would cut it remains deferred.
|
|
227
|
+
14. `PGN::Board` / `PGN::MoveCalculator` — 0x88 board representation. `Board`
|
|
228
|
+
internals are now a 128-cell array indexed by `rank*16+file` (the classic
|
|
229
|
+
0x88 scheme), and `MoveCalculator` works entirely in single-integer square
|
|
230
|
+
indices via `Board#at_index`/`#apply!`, so the replay hot path no longer
|
|
231
|
+
allocates `[file,rank]` coordinate arrays or square-name strings.
|
|
232
|
+
Off-board is a single bitmask (`(idx & 0x88).zero?`, ~1.6x faster than a
|
|
233
|
+
0..7 bounds check) and ray stepping is a single integer add. Algorithm
|
|
234
|
+
unchanged → byte-identical output. Replay throughput +49% (798 → 517 µs/i),
|
|
235
|
+
replay allocations −38% (1571 → 976 objects) / −33% (92440 → 62064 bytes),
|
|
236
|
+
parse+replay +21% throughput. A companion piece-location index (piece →
|
|
237
|
+
0x88 indices for O(1) origin/king lookups) was implemented on top, passed
|
|
238
|
+
all specs, but *regressed* (replay 526→727 µs/i, allocations +63%): `dup`
|
|
239
|
+
must clone the index every move and every move pays maintenance that pawns
|
|
240
|
+
(the common case, geometry-fixed origins) can't use — so it was rejected
|
|
241
|
+
and reverted. The 0x88 board alone is the winner.
|
|
219
242
|
|
|
220
243
|
Public output (FEN, PGN) is byte-identical to the original gem; the full
|
|
221
244
|
suite (182 examples) stays green. See `bench/IMPROVEMENTS.md` for the per-step
|
data/TODO.md
CHANGED
|
@@ -15,21 +15,33 @@
|
|
|
15
15
|
`scan_one`'s `[type, m, discarded]` tuple to a single returned string
|
|
16
16
|
(type/discarded stashed in ivars). The full `Token` is kept only for the
|
|
17
17
|
`#tokens` spec helper. Parse allocations −42% (603537 → 347037 / 500 games).
|
|
18
|
-
- Speed up replay via a board-representation rewrite (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
(a) ✗ (attempted, rejected) A piece-location index (piece → 0x88 indices)
|
|
31
|
+
maintained in `update`/`apply!` and used for O(1) slider/leaper/king
|
|
32
|
+
origin lookups. Implemented on top of (b), all 182 specs green, but it
|
|
33
|
+
**regressed**: replay 526→727 µs/i (+38% slower), allocations 976→1591
|
|
34
|
+
objects (+63%). Root cause: `Board#dup` (called every move) must clone
|
|
35
|
+
the index (`transform_values(&:dup)` ≈ 12 piece arrays) — Board#dup went
|
|
36
|
+
91→676 objects — and every move pays per-update index maintenance
|
|
37
|
+
(`<<`/`delete`) that pawns (the most common move type, whose origins are
|
|
38
|
+
geometry-fixed and can't use the index) pay for no benefit. The index
|
|
39
|
+
helps sliders/leapers (minority of moves) but the dup + maintenance cost
|
|
40
|
+
is paid by every move. Conclusion: a global piece index is a loss for
|
|
41
|
+
replay (where only ONE given move is validated, so ray-scanning from the
|
|
42
|
+
destination is already cheap); it pays in move-*generation* libraries
|
|
43
|
+
(chess.js/python-chess) that enumerate ALL legal moves. Not worth a COW
|
|
44
|
+
variant either (maintenance + pawns). Reverted; (b) alone is the winner.
|
|
33
45
|
- Replace the right-recursive `tag_section`/`variation_list` rules in
|
|
34
46
|
`pgn_parser.y` with ordinary left-recursion plus one explicit `.reverse`
|
|
35
47
|
at the point each list is consumed, so the legacy whittle-order
|
data/bench/baseline_moves.txt
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Workload: immortal game, 45 plies
|
|
2
2
|
|
|
3
3
|
=== 1. Replay allocations (45 plies, no parse) ===
|
|
4
|
-
total_allocated objects:
|
|
5
|
-
total_allocated bytes:
|
|
4
|
+
total_allocated objects: 976
|
|
5
|
+
total_allocated bytes: 62064
|
|
6
6
|
|
|
7
7
|
=== 2. Board#dup x45 (target of flat-board COW) ===
|
|
8
|
-
total_allocated objects:
|
|
9
|
-
total_allocated bytes:
|
|
8
|
+
total_allocated objects: 91
|
|
9
|
+
total_allocated bytes: 3856
|
|
10
10
|
|
|
11
11
|
=== 3. Board#at(str) x1000 (target of coord-arithmetic at) ===
|
|
12
12
|
total_allocated objects: 0
|
|
@@ -15,8 +15,8 @@ total_allocated bytes: 0
|
|
|
15
15
|
=== 4. Replay throughput (ips, excluding parse) ===
|
|
16
16
|
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
|
|
17
17
|
Warming up --------------------------------------
|
|
18
|
-
replay immortal
|
|
18
|
+
replay immortal 193.000 i/100ms
|
|
19
19
|
Calculating -------------------------------------
|
|
20
|
-
replay immortal 1.
|
|
20
|
+
replay immortal 1.894k (± 2.9%) i/s (528.02 μs/i) - 9.650k in 5.095389s
|
|
21
21
|
|
|
22
22
|
Done. Compare this file against bench/baseline_moves.txt after optimizations.
|
data/bench/baseline_parse.txt
CHANGED
|
@@ -5,21 +5,21 @@ total_allocated objects: 347037
|
|
|
5
5
|
total_allocated bytes: 17977414
|
|
6
6
|
|
|
7
7
|
=== 2. Parse + replay allocations (500 games) ===
|
|
8
|
-
total_allocated objects:
|
|
9
|
-
total_allocated bytes:
|
|
8
|
+
total_allocated objects: 831530
|
|
9
|
+
total_allocated bytes: 47966112
|
|
10
10
|
|
|
11
11
|
=== 3. Parse-only throughput (ips) ===
|
|
12
12
|
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
|
|
13
13
|
Warming up --------------------------------------
|
|
14
14
|
parse 500 games 1.000 i/100ms
|
|
15
15
|
Calculating -------------------------------------
|
|
16
|
-
parse 500 games 4.
|
|
16
|
+
parse 500 games 4.053 (± 0.0%) i/s (246.70 ms/i) - 21.000 in 5.180770s
|
|
17
17
|
|
|
18
18
|
=== 4. Parse + replay throughput (ips) ===
|
|
19
19
|
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
|
|
20
20
|
Warming up --------------------------------------
|
|
21
21
|
parse+replay 500 games 1.000 i/100ms
|
|
22
22
|
Calculating -------------------------------------
|
|
23
|
-
parse+replay 500 games 1.
|
|
23
|
+
parse+replay 500 games 1.872 (± 0.0%) i/s (534.26 ms/i) - 10.000 in 5.342615s
|
|
24
24
|
|
|
25
25
|
Done. Compare this file against bench/baseline_parse.txt after optimizations.
|
data/lib/pgn/board.rb
CHANGED
|
@@ -50,7 +50,17 @@ module PGN
|
|
|
50
50
|
nil => '_'
|
|
51
51
|
}.freeze
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
# 0x88 board representation (see chess.js / the classic 0x88 move-generation
|
|
54
|
+
# algorithm). A square is addressed by a single integer index
|
|
55
|
+
# `rank * 16 + file`; the extra files/ranks make off-board detection a
|
|
56
|
+
# single bitmask test -- `(idx & 0x88) != 0` -- which is faster than the
|
|
57
|
+
# four-integer comparison a 0..7 bounds check needs, and lets ray
|
|
58
|
+
# stepping be a single integer add. The public `squares` 8x8 API is built
|
|
59
|
+
# from this array on demand (it is off the replay hot path), and the
|
|
60
|
+
# MoveCalculator hot path works entirely in integer indices.
|
|
61
|
+
#
|
|
62
|
+
# file = idx & 0x0F (0..7)
|
|
63
|
+
# rank = idx >> 4 (0..7)
|
|
54
64
|
|
|
55
65
|
# @return [PGN::Board] a board in the starting position
|
|
56
66
|
#
|
|
@@ -75,7 +85,24 @@ module PGN
|
|
|
75
85
|
#
|
|
76
86
|
def initialize(squares)
|
|
77
87
|
self.squares = squares
|
|
78
|
-
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Array<Array<String>>] the board as a file-major 8x8 array
|
|
91
|
+
# (squares[file][rank]). Built on demand from the 0x88 array; equality
|
|
92
|
+
# with the START constant and other boards is preserved.
|
|
93
|
+
#
|
|
94
|
+
def squares
|
|
95
|
+
(0..7).map { |f| (0..7).map { |r| @cells[(r * 16) + f] } }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def squares=(squares)
|
|
99
|
+
@cells = Array.new(128)
|
|
100
|
+
8.times do |f|
|
|
101
|
+
8.times do |r|
|
|
102
|
+
@cells[(r * 16) + f] = squares[f][r]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
@cells
|
|
79
106
|
end
|
|
80
107
|
|
|
81
108
|
# @overload at(str)
|
|
@@ -91,12 +118,10 @@ module PGN
|
|
|
91
118
|
# board.at(4,3) #=> "P"
|
|
92
119
|
# board.at("e4") #=> "P"
|
|
93
120
|
#
|
|
94
|
-
# String squares are parsed with getbyte arithmetic (a=0x61, '1'=0x31)
|
|
95
|
-
# so the common string lookup allocates nothing.
|
|
96
121
|
def at(arg0, arg1 = nil)
|
|
97
|
-
return
|
|
122
|
+
return @cells[(arg1 * 16) + arg0] unless arg1.nil?
|
|
98
123
|
|
|
99
|
-
|
|
124
|
+
@cells[(rank_of(arg0) * 16) + file_of(arg0)]
|
|
100
125
|
end
|
|
101
126
|
|
|
102
127
|
# @param changes [Hash<String, <String, nil>>] changes to make to the board
|
|
@@ -105,9 +130,7 @@ module PGN
|
|
|
105
130
|
# board.change!({"e2" => nil, "e4" => "P"})
|
|
106
131
|
#
|
|
107
132
|
def change!(changes)
|
|
108
|
-
changes.each
|
|
109
|
-
update(square, piece)
|
|
110
|
-
end
|
|
133
|
+
changes.each { |square, piece| update(square, piece) }
|
|
111
134
|
self
|
|
112
135
|
end
|
|
113
136
|
|
|
@@ -117,16 +140,8 @@ module PGN
|
|
|
117
140
|
# @example
|
|
118
141
|
# board.update("e4", "P")
|
|
119
142
|
#
|
|
120
|
-
# Copy-on-write: clone only the column being mutated, and only once per
|
|
121
|
-
# instance, so unchanged columns stay shared with any board this one was
|
|
122
|
-
# duped from.
|
|
123
143
|
def update(square, piece)
|
|
124
|
-
|
|
125
|
-
unless @owned[file]
|
|
126
|
-
squares[file] = squares[file].dup
|
|
127
|
-
@owned[file] = true
|
|
128
|
-
end
|
|
129
|
-
squares[file][rank_of(square)] = piece
|
|
144
|
+
@cells[(rank_of(square) * 16) + file_of(square)] = piece
|
|
130
145
|
self
|
|
131
146
|
end
|
|
132
147
|
|
|
@@ -146,9 +161,7 @@ module PGN
|
|
|
146
161
|
#
|
|
147
162
|
def position_for(coordinates)
|
|
148
163
|
file, rank = coordinates
|
|
149
|
-
|
|
150
|
-
rank_chr = INDEX_TO_RANK[rank]
|
|
151
|
-
[file_chr, rank_chr].join('')
|
|
164
|
+
INDEX_TO_FILE[file] + INDEX_TO_RANK[rank]
|
|
152
165
|
end
|
|
153
166
|
|
|
154
167
|
# @return [String] the board in human readable format with unicode
|
|
@@ -160,12 +173,66 @@ module PGN
|
|
|
160
173
|
end.join("\n")
|
|
161
174
|
end
|
|
162
175
|
|
|
163
|
-
# @return [PGN::Board] a copy of self.
|
|
164
|
-
#
|
|
165
|
-
# mutation (copy-on-write).
|
|
176
|
+
# @return [PGN::Board] a copy of self. Copies the 128-cell 0x88 array;
|
|
177
|
+
# mutations to the copy do not affect the original.
|
|
166
178
|
#
|
|
167
179
|
def dup
|
|
168
|
-
PGN::Board.
|
|
180
|
+
copy = PGN::Board.allocate
|
|
181
|
+
copy.instance_variable_set(:@cells, @cells.dup)
|
|
182
|
+
copy
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# -- 0x88 hot-path API (integer indices) ---------------------------------
|
|
186
|
+
|
|
187
|
+
# The 0x88 index of an algebraic square name.
|
|
188
|
+
#
|
|
189
|
+
# @param square [String] e.g. "e4"
|
|
190
|
+
# @return [Integer] idx = rank * 16 + file
|
|
191
|
+
#
|
|
192
|
+
def index_of(square)
|
|
193
|
+
(rank_of(square) * 16) + file_of(square)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The 0x88 index of zero-indexed file/rank coordinates.
|
|
197
|
+
#
|
|
198
|
+
# @return [Integer] idx = rank * 16 + file
|
|
199
|
+
#
|
|
200
|
+
def index_for(file, rank)
|
|
201
|
+
(rank * 16) + file
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Looks up a piece by 0x88 index. The caller is responsible for having
|
|
205
|
+
# already verified the index is on-board (`(idx & 0x88).zero?`); reading
|
|
206
|
+
# an off-board index simply returns nil.
|
|
207
|
+
#
|
|
208
|
+
# @param idx [Integer] a 0x88 square index
|
|
209
|
+
# @return [String, nil] the piece on that square
|
|
210
|
+
#
|
|
211
|
+
def at_index(idx)
|
|
212
|
+
@cells[idx]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Places a piece on a 0x88 index. Returns self.
|
|
216
|
+
#
|
|
217
|
+
# @param idx [Integer] a 0x88 square index
|
|
218
|
+
# @param piece [String, nil]
|
|
219
|
+
# @return [self]
|
|
220
|
+
#
|
|
221
|
+
def update_index(idx, piece)
|
|
222
|
+
@cells[idx] = piece
|
|
223
|
+
self
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Applies a batch of integer-indexed changes. The replay hot path uses
|
|
227
|
+
# this so it never allocates square-name strings or `[file, rank]`
|
|
228
|
+
# coordinate arrays.
|
|
229
|
+
#
|
|
230
|
+
# @param changes [Hash<Integer, <String, nil>>]
|
|
231
|
+
# @return [self]
|
|
232
|
+
#
|
|
233
|
+
def apply!(changes)
|
|
234
|
+
changes.each { |idx, piece| @cells[idx] = piece }
|
|
235
|
+
self
|
|
169
236
|
end
|
|
170
237
|
|
|
171
238
|
private
|
data/lib/pgn/move_calculator.rb
CHANGED
|
@@ -6,88 +6,70 @@ module PGN
|
|
|
6
6
|
# the board need to be updated, new castling restrictions, the en passant
|
|
7
7
|
# square and whether to update fullmove and halfmove counters.
|
|
8
8
|
#
|
|
9
|
+
# Squares are addressed as 0x88 integer indices (see {PGN::Board}); this
|
|
10
|
+
# keeps the replay hot path free of `[file, rank]` coordinate arrays and
|
|
11
|
+
# square-name string allocations. The public {#origin} reader still returns
|
|
12
|
+
# an algebraic square string for API compatibility.
|
|
13
|
+
#
|
|
9
14
|
# @!attribute board
|
|
10
15
|
# @return [PGN::Board] the current board
|
|
11
16
|
#
|
|
12
17
|
# @!attribute move
|
|
13
18
|
# @return [PGN::Move] the current move
|
|
14
19
|
#
|
|
15
|
-
# @!attribute origin
|
|
16
|
-
# @return [String, nil] the origin square in SAN
|
|
17
|
-
#
|
|
18
20
|
class MoveCalculator
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# board.
|
|
21
|
+
# 0x88 ray-step offsets for sliding pieces. A step is a single integer
|
|
22
|
+
# add; off-board is `(idx & 0x88) != 0`, which also catches file wraparound.
|
|
22
23
|
#
|
|
23
|
-
|
|
24
|
-
'b' => [
|
|
25
|
-
'r' => [
|
|
26
|
-
'q' => [
|
|
27
|
-
[-1, 0], [1, 0], [0, -1], [0, 1]]
|
|
24
|
+
SLIDE = {
|
|
25
|
+
'b' => [-15, 15, -17, 17],
|
|
26
|
+
'r' => [-1, 1, -16, 16],
|
|
27
|
+
'q' => [-1, 1, -16, 16, -15, 15, -17, 17]
|
|
28
28
|
}.freeze
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
# they are allowed to make.
|
|
30
|
+
# 0x88 single-step offsets for knight and king.
|
|
32
31
|
#
|
|
33
|
-
|
|
34
|
-
'k' => [
|
|
35
|
-
|
|
36
|
-
'n' => [[-1, -2], [-1, 2], [1, -2], [1, 2],
|
|
37
|
-
[-2, -1], [2, -1], [-2, 1], [2, 1]]
|
|
32
|
+
STEP = {
|
|
33
|
+
'k' => [-1, 1, -16, 16, -15, 15, -17, 17],
|
|
34
|
+
'n' => [33, 31, -31, -33, 18, 14, -14, -18]
|
|
38
35
|
}.freeze
|
|
39
36
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
37
|
+
# Possible pawn origins, expressed as offsets from the destination square
|
|
38
|
+
# (pawn moves are computed backwards from where the pawn landed).
|
|
42
39
|
#
|
|
43
|
-
|
|
44
|
-
'P' => {
|
|
45
|
-
|
|
46
|
-
normal: [[0, -1]],
|
|
47
|
-
double: [[0, -2]]
|
|
48
|
-
},
|
|
49
|
-
'p' => {
|
|
50
|
-
capture: [[-1, 1], [1, 1]],
|
|
51
|
-
normal: [[0, 1]],
|
|
52
|
-
double: [[0, 2]]
|
|
53
|
-
}
|
|
40
|
+
PAWN_OFFSETS = {
|
|
41
|
+
'P' => { capture: [-17, -15], normal: [-16], double: [-32] },
|
|
42
|
+
'p' => { capture: [15, 17], normal: [16], double: [32] }
|
|
54
43
|
}.freeze
|
|
55
44
|
|
|
56
|
-
# The squares to update for each
|
|
45
|
+
# The squares to update for each castling move, keyed by 0x88 index.
|
|
57
46
|
#
|
|
58
47
|
CASTLING = {
|
|
59
|
-
'Q' => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'e1' => nil
|
|
64
|
-
},
|
|
65
|
-
'K' => {
|
|
66
|
-
'e1' => nil,
|
|
67
|
-
'f1' => 'R',
|
|
68
|
-
'g1' => 'K',
|
|
69
|
-
'h1' => nil
|
|
70
|
-
},
|
|
71
|
-
'q' => {
|
|
72
|
-
'a8' => nil,
|
|
73
|
-
'c8' => 'k',
|
|
74
|
-
'd8' => 'r',
|
|
75
|
-
'e8' => nil
|
|
76
|
-
},
|
|
77
|
-
'k' => {
|
|
78
|
-
'e8' => nil,
|
|
79
|
-
'f8' => 'r',
|
|
80
|
-
'g8' => 'k',
|
|
81
|
-
'h8' => nil
|
|
82
|
-
}
|
|
48
|
+
'Q' => { 0 => nil, 2 => 'K', 3 => 'R', 4 => nil },
|
|
49
|
+
'K' => { 4 => nil, 5 => 'R', 6 => 'K', 7 => nil },
|
|
50
|
+
'q' => { 112 => nil, 114 => 'k', 115 => 'r', 116 => nil },
|
|
51
|
+
'k' => { 116 => nil, 117 => 'r', 118 => 'k', 119 => nil }
|
|
83
52
|
}.freeze
|
|
84
53
|
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
|
|
54
|
+
# Corner-square 0x88 indices, used for castling-restriction bookkeeping
|
|
55
|
+
# (a rook leaving or being captured on a corner drops the matching right).
|
|
56
|
+
#
|
|
57
|
+
A1 = 0
|
|
58
|
+
H1 = 7
|
|
59
|
+
A8 = 112
|
|
60
|
+
H8 = 119
|
|
61
|
+
|
|
62
|
+
# rook-origin (0x88 index) -> castling restriction it drops.
|
|
63
|
+
#
|
|
64
|
+
ROOK_RESTRICTIONS = { A1 => 'Q', H1 => 'K', A8 => 'q', H8 => 'k' }.freeze
|
|
65
|
+
|
|
66
|
+
# Castling-move characters by side, for the "castling occurs" restriction.
|
|
67
|
+
# Frozen so {Array#include?} does not allocate per call.
|
|
68
|
+
#
|
|
69
|
+
WHITE_CASTLE = %w[K Q].freeze
|
|
70
|
+
BLACK_CASTLE = %w[k q].freeze
|
|
89
71
|
|
|
90
|
-
attr_accessor :board, :move
|
|
72
|
+
attr_accessor :board, :move
|
|
91
73
|
|
|
92
74
|
# @param board [PGN::Board] the current board
|
|
93
75
|
# @param move [PGN::Move] the current move
|
|
@@ -95,14 +77,24 @@ module PGN
|
|
|
95
77
|
def initialize(board, move)
|
|
96
78
|
self.board = board
|
|
97
79
|
self.move = move
|
|
98
|
-
|
|
80
|
+
@origin_idx = compute_origin
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [String, nil] the origin square in algebraic notation, for API
|
|
84
|
+
# compatibility. Internally the calculator works with the 0x88 index
|
|
85
|
+
# (see {#origin_idx}); this reader materialises the string on demand.
|
|
86
|
+
#
|
|
87
|
+
def origin
|
|
88
|
+
return nil if @origin_idx.nil?
|
|
89
|
+
|
|
90
|
+
board.position_for([@origin_idx & 0x0F, @origin_idx >> 4])
|
|
99
91
|
end
|
|
100
92
|
|
|
101
93
|
# @return [PGN::Board] the board after the move is made
|
|
102
94
|
#
|
|
103
95
|
def result_board
|
|
104
96
|
new_board = board.dup
|
|
105
|
-
new_board.
|
|
97
|
+
new_board.apply!(changes)
|
|
106
98
|
|
|
107
99
|
new_board
|
|
108
100
|
end
|
|
@@ -112,25 +104,28 @@ module PGN
|
|
|
112
104
|
def castling_restrictions
|
|
113
105
|
restrict = []
|
|
114
106
|
|
|
115
|
-
# when a king or rook is moved
|
|
116
107
|
case move.piece
|
|
117
108
|
when 'K'
|
|
118
|
-
restrict
|
|
109
|
+
restrict << 'K' << 'Q'
|
|
119
110
|
when 'k'
|
|
120
|
-
restrict
|
|
111
|
+
restrict << 'k' << 'q'
|
|
121
112
|
when 'R', 'r'
|
|
122
|
-
restrict << ROOK_RESTRICTIONS[
|
|
113
|
+
restrict << ROOK_RESTRICTIONS[@origin_idx]
|
|
123
114
|
end
|
|
124
115
|
|
|
125
116
|
# when castling occurs
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
if WHITE_CASTLE.include?(move.castle)
|
|
118
|
+
restrict << 'K' << 'Q'
|
|
119
|
+
elsif BLACK_CASTLE.include?(move.castle)
|
|
120
|
+
restrict << 'k' << 'q'
|
|
121
|
+
end
|
|
128
122
|
|
|
129
123
|
# when a rook is taken
|
|
130
|
-
|
|
131
|
-
restrict << '
|
|
132
|
-
restrict << '
|
|
133
|
-
restrict << '
|
|
124
|
+
dest = dest_idx
|
|
125
|
+
restrict << 'Q' if dest == A1
|
|
126
|
+
restrict << 'q' if dest == A8
|
|
127
|
+
restrict << 'K' if dest == H1
|
|
128
|
+
restrict << 'k' if dest == H8
|
|
134
129
|
|
|
135
130
|
restrict.empty? ? restrict : restrict.compact.uniq
|
|
136
131
|
end
|
|
@@ -151,35 +146,31 @@ module PGN
|
|
|
151
146
|
#
|
|
152
147
|
def en_passant_square
|
|
153
148
|
return nil if move.castle
|
|
149
|
+
return nil unless move.pawn? && ((origin_rank - dest_rank).abs == 2)
|
|
154
150
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if move.white?
|
|
158
|
-
"#{origin[0]}3"
|
|
159
|
-
else
|
|
160
|
-
"#{origin[0]}6"
|
|
161
|
-
end
|
|
151
|
+
Board::INDEX_TO_FILE[origin_file] + (move.white? ? '3' : '6')
|
|
162
152
|
end
|
|
163
153
|
|
|
164
154
|
private
|
|
165
155
|
|
|
156
|
+
# The integer-indexed changes to apply to the board. Keys are 0x88
|
|
157
|
+
# indices, so no square-name strings are allocated on the hot path.
|
|
158
|
+
#
|
|
166
159
|
def changes
|
|
167
160
|
changes = {}
|
|
168
161
|
changes.merge!(CASTLING[move.castle]) if move.castle
|
|
169
|
-
changes
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
changes[move.destination] = move.promotion if move.promotion
|
|
162
|
+
changes[@origin_idx] = nil
|
|
163
|
+
changes[dest_idx] = move.piece
|
|
164
|
+
changes[en_passant_capture] = nil
|
|
165
|
+
changes[dest_idx] = move.promotion if move.promotion
|
|
175
166
|
|
|
176
|
-
changes.reject! { |
|
|
167
|
+
changes.reject! { |idx, _| idx.nil? }
|
|
177
168
|
|
|
178
169
|
changes
|
|
179
170
|
end
|
|
180
171
|
|
|
181
172
|
# Using the current position and move, figure out where the piece
|
|
182
|
-
# came from.
|
|
173
|
+
# came from (as a 0x88 index).
|
|
183
174
|
#
|
|
184
175
|
def compute_origin
|
|
185
176
|
return nil if move.castle
|
|
@@ -194,58 +185,55 @@ module PGN
|
|
|
194
185
|
|
|
195
186
|
possibilities = disambiguate(possibilities) if possibilities.length > 1
|
|
196
187
|
|
|
197
|
-
|
|
188
|
+
possibilities.first
|
|
198
189
|
end
|
|
199
190
|
|
|
200
|
-
# From the destination square,
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
# check the next direction.
|
|
191
|
+
# From the destination square, walk each slider direction until the first
|
|
192
|
+
# occupied square. If that piece is the moving piece, the square it sits
|
|
193
|
+
# on is a possible origin.
|
|
204
194
|
#
|
|
205
195
|
def direction_origins
|
|
206
|
-
|
|
207
|
-
|
|
196
|
+
offsets = SLIDE[move.piece.downcase]
|
|
197
|
+
dest = dest_idx
|
|
208
198
|
|
|
209
|
-
|
|
210
|
-
|
|
199
|
+
possibilities = []
|
|
200
|
+
offsets.each do |off|
|
|
201
|
+
square = first_piece(dest, off)
|
|
211
202
|
possibilities << square if piece_at(square) == move.piece
|
|
212
203
|
end
|
|
213
204
|
|
|
214
205
|
possibilities
|
|
215
206
|
end
|
|
216
207
|
|
|
217
|
-
# From the destination square,
|
|
218
|
-
# square and
|
|
219
|
-
#
|
|
208
|
+
# From the destination square, apply each single-step offset. If the
|
|
209
|
+
# target square is on the board and holds the moving piece, it is a
|
|
210
|
+
# possible origin.
|
|
220
211
|
#
|
|
221
|
-
def move_origins(
|
|
222
|
-
|
|
223
|
-
possibilities = []
|
|
224
|
-
file, rank = destination_coords
|
|
212
|
+
def move_origins(offsets = STEP[move.piece.downcase])
|
|
213
|
+
dest = dest_idx
|
|
225
214
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
215
|
+
possibilities = []
|
|
216
|
+
offsets.each do |off|
|
|
217
|
+
target = dest + off
|
|
218
|
+
next unless (target & 0x88).zero? # rubocop:disable Style/BitwisePredicate
|
|
229
219
|
|
|
230
|
-
possibilities <<
|
|
220
|
+
possibilities << target if board.at_index(target) == move.piece
|
|
231
221
|
end
|
|
232
222
|
|
|
233
223
|
possibilities
|
|
234
224
|
end
|
|
235
225
|
|
|
236
|
-
# Computes the
|
|
226
|
+
# Computes the possible pawn origins based on the destination square
|
|
237
227
|
# and whether or not the move is a capture.
|
|
238
228
|
#
|
|
239
229
|
def pawn_origins
|
|
240
|
-
|
|
241
|
-
double_rank = (rank == 3 && move.white?) || (rank == 4 && move.black?)
|
|
242
|
-
|
|
243
|
-
pawn_moves = PAWN_MOVES[move.piece]
|
|
230
|
+
double = (dest_rank == 3 && move.white?) || (dest_rank == 4 && move.black?)
|
|
244
231
|
|
|
245
|
-
|
|
246
|
-
|
|
232
|
+
pawn_moves = PAWN_OFFSETS[move.piece]
|
|
233
|
+
offsets = move.capture ? pawn_moves[:capture] : pawn_moves[:normal]
|
|
234
|
+
offsets += pawn_moves[:double] if double
|
|
247
235
|
|
|
248
|
-
move_origins(
|
|
236
|
+
move_origins(offsets)
|
|
249
237
|
end
|
|
250
238
|
|
|
251
239
|
def disambiguate(possibilities)
|
|
@@ -259,36 +247,36 @@ module PGN
|
|
|
259
247
|
# Try to disambiguate based on the standard algebraic notation.
|
|
260
248
|
#
|
|
261
249
|
def disambiguate_san(possibilities)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
250
|
+
return possibilities unless move.disambiguation
|
|
251
|
+
|
|
252
|
+
possibilities.select do |idx|
|
|
253
|
+
board.position_for([idx & 0x0F, idx >> 4]).match(move.disambiguation)
|
|
266
254
|
end
|
|
267
255
|
end
|
|
268
256
|
|
|
269
|
-
# A pawn can't move two spaces if there is a pawn in front of it.
|
|
257
|
+
# A pawn can't move two spaces if there is a pawn in front of it. A
|
|
258
|
+
# double-push origin sits on rank 2 (white) or 7 (black); reject those
|
|
259
|
+
# candidates when more than one pawn could have reached the destination.
|
|
270
260
|
#
|
|
271
261
|
def disambiguate_pawns(possibilities)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
possibilities
|
|
276
|
-
end
|
|
262
|
+
return possibilities unless move.piece.match?(/p/i) && !move.capture
|
|
263
|
+
|
|
264
|
+
possibilities.reject { |idx| (idx >> 4) == 1 || (idx >> 4) == 6 }
|
|
277
265
|
end
|
|
278
266
|
|
|
279
267
|
# A piece can't move if it would result in a discovered check.
|
|
280
268
|
#
|
|
281
269
|
def disambiguate_discovered_check(possibilities)
|
|
282
|
-
|
|
270
|
+
king_idx = king_position
|
|
283
271
|
|
|
284
|
-
|
|
272
|
+
SLIDE.each do |attacking_piece, offsets|
|
|
285
273
|
attacking_piece = attacking_piece.upcase if move.black?
|
|
286
274
|
|
|
287
|
-
|
|
288
|
-
square = first_piece(
|
|
275
|
+
offsets.each do |off|
|
|
276
|
+
square = first_piece(king_idx, off)
|
|
289
277
|
next unless piece_at(square) == move.piece && possibilities.include?(square)
|
|
290
278
|
|
|
291
|
-
next_square = first_piece(square,
|
|
279
|
+
next_square = first_piece(square, off)
|
|
292
280
|
possibilities.reject! { |p| p == square } if piece_at(next_square) == attacking_piece
|
|
293
281
|
end
|
|
294
282
|
end
|
|
@@ -296,62 +284,69 @@ module PGN
|
|
|
296
284
|
possibilities
|
|
297
285
|
end
|
|
298
286
|
|
|
299
|
-
# Walks from `
|
|
300
|
-
# or the first occupied square. Returns that square's
|
|
301
|
-
#
|
|
302
|
-
# caller reads the piece off the board itself, so this avoids allocating
|
|
303
|
-
# the `[piece, square]` wrapper tuple per direction scan.
|
|
287
|
+
# Walks from `idx` in the 0x88 direction `off` until it reaches the edge
|
|
288
|
+
# of the board or the first occupied square. Returns that square's 0x88
|
|
289
|
+
# index, or nil if no piece was encountered before the edge.
|
|
304
290
|
#
|
|
305
|
-
def first_piece(
|
|
306
|
-
|
|
307
|
-
|
|
291
|
+
def first_piece(idx, off)
|
|
292
|
+
idx += off
|
|
293
|
+
while (idx & 0x88).zero? # rubocop:disable Style/BitwisePredicate
|
|
294
|
+
square = board.at_index(idx)
|
|
295
|
+
return idx if square
|
|
308
296
|
|
|
309
|
-
|
|
310
|
-
file += i
|
|
311
|
-
rank += j
|
|
312
|
-
return nil if file.negative? || file > 7 || rank.negative? || rank > 7
|
|
313
|
-
|
|
314
|
-
square = [file, rank]
|
|
315
|
-
return square if board.at(file, rank)
|
|
297
|
+
idx += off
|
|
316
298
|
end
|
|
299
|
+
nil
|
|
317
300
|
end
|
|
318
301
|
|
|
319
|
-
# Reads the piece
|
|
320
|
-
# {#
|
|
321
|
-
#
|
|
322
|
-
# tuple per direction scan.
|
|
302
|
+
# Reads the piece at a 0x88 index, returning nil for an off-board (nil)
|
|
303
|
+
# index. Keeps {#disambiguate_discovered_check} within the configured
|
|
304
|
+
# complexity limits.
|
|
323
305
|
#
|
|
324
|
-
def piece_at(
|
|
325
|
-
|
|
306
|
+
def piece_at(idx)
|
|
307
|
+
idx && board.at_index(idx)
|
|
326
308
|
end
|
|
327
309
|
|
|
328
|
-
# If the move is a capture and there is no piece on the
|
|
329
|
-
#
|
|
310
|
+
# If the move is a capture and there is no piece on the destination
|
|
311
|
+
# square, it must be an en passant capture. The captured pawn sits on the
|
|
312
|
+
# destination file and the moving pawn's origin rank.
|
|
330
313
|
#
|
|
331
314
|
def en_passant_capture
|
|
332
315
|
return nil if move.castle
|
|
316
|
+
return nil unless move.capture && board.at_index(dest_idx).nil?
|
|
333
317
|
|
|
334
|
-
|
|
318
|
+
(origin_rank * 16) + (dest_idx & 0x0F)
|
|
335
319
|
end
|
|
336
320
|
|
|
337
321
|
def king_position
|
|
338
322
|
king = move.white? ? 'K' : 'k'
|
|
339
323
|
|
|
340
|
-
0.upto(7) do |
|
|
341
|
-
0.upto(7) do |
|
|
342
|
-
|
|
324
|
+
0.upto(7) do |rank|
|
|
325
|
+
0.upto(7) do |file|
|
|
326
|
+
idx = (rank * 16) + file
|
|
327
|
+
return idx if board.at_index(idx) == king
|
|
343
328
|
end
|
|
344
329
|
end
|
|
345
330
|
|
|
346
331
|
nil
|
|
347
332
|
end
|
|
348
333
|
|
|
349
|
-
|
|
350
|
-
|
|
334
|
+
# -- 0x88 index helpers --------------------------------------------------
|
|
335
|
+
|
|
336
|
+
def dest_idx
|
|
337
|
+
@dest_idx ||= move.destination && board.index_of(move.destination)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def origin_file
|
|
341
|
+
@origin_idx & 0x0F
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def origin_rank
|
|
345
|
+
@origin_idx >> 4
|
|
351
346
|
end
|
|
352
347
|
|
|
353
|
-
def
|
|
354
|
-
|
|
348
|
+
def dest_rank
|
|
349
|
+
dest_idx >> 4
|
|
355
350
|
end
|
|
356
351
|
end
|
|
357
352
|
end
|
data/lib/pgn/version.rb
CHANGED