pgn2 1.5.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 +148 -0
- data/Gemfile +3 -0
- data/NOTICE.md +21 -0
- data/README.md +107 -5
- data/Rakefile +35 -10
- data/TODO.md +64 -0
- 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 +64 -0
- data/lib/pgn/fen.rb +35 -46
- data/lib/pgn/game.rb +22 -13
- data/lib/pgn/move.rb +19 -15
- data/lib/pgn/move_calculator.rb +13 -1
- 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 +97 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9122d51c1216e8cfa7a0dd361fc4c37532dd52c589e21316a841e78a3559eced
|
|
4
|
+
data.tar.gz: c982424532eb4cf9fe84ddcf95bcd7653cde9679a588d050d25ee22f0c4aea64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab449fed5746ca4b41262ca19b2a5e4db55e2faa12ce89b51cb30427383b4b83db622be644a2c0b3234c0827011fed259f60137694a40593e8013f574151f4f7
|
|
7
|
+
data.tar.gz: f3b520c9e0bb48b7c06cd379d77f4336703d2ac212f8b013e2815db1c2b767823e4d0659cba8ff7f6003a56094044ece7339c7be82fb2a324eae169c0240edc7
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -19,19 +19,50 @@ jobs:
|
|
|
19
19
|
with:
|
|
20
20
|
ruby-version: ${{ matrix.ruby }}
|
|
21
21
|
bundler-cache: true
|
|
22
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
23
|
+
with:
|
|
24
|
+
components: rust-src
|
|
25
|
+
- run: bundle exec rake compile
|
|
22
26
|
- run: bundle exec rspec --format documentation
|
|
23
27
|
|
|
24
28
|
rubocop:
|
|
25
|
-
name: RuboCop
|
|
29
|
+
name: RuboCop
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
env:
|
|
32
|
+
RUBOCOP_CACHE_ROOT: tmp/rubocop
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
- uses: ruby/setup-ruby@v1
|
|
36
|
+
with:
|
|
37
|
+
ruby-version: '3.3'
|
|
38
|
+
bundler-cache: true
|
|
39
|
+
|
|
40
|
+
- name: Prepare RuboCop cache
|
|
41
|
+
uses: actions/cache@v4
|
|
42
|
+
env:
|
|
43
|
+
DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', 'Gemfile.lock') }}
|
|
44
|
+
with:
|
|
45
|
+
path: ${{ env.RUBOCOP_CACHE_ROOT }}
|
|
46
|
+
key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }}
|
|
47
|
+
restore-keys: |
|
|
48
|
+
rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-
|
|
49
|
+
|
|
50
|
+
- name: Lint code for consistent style
|
|
51
|
+
run: bundle exec rubocop -f github
|
|
52
|
+
|
|
53
|
+
bundle-audit:
|
|
54
|
+
name: Bundler-audit (gem vulnerabilities)
|
|
26
55
|
runs-on: ubuntu-latest
|
|
27
|
-
continue-on-error: true
|
|
28
56
|
steps:
|
|
29
57
|
- uses: actions/checkout@v4
|
|
30
58
|
- uses: ruby/setup-ruby@v1
|
|
31
59
|
with:
|
|
32
60
|
ruby-version: '3.3'
|
|
33
61
|
bundler-cache: true
|
|
34
|
-
-
|
|
62
|
+
- name: Update the advisory database
|
|
63
|
+
run: bundle exec bundle-audit update
|
|
64
|
+
- name: Check dependencies for known vulnerabilities
|
|
65
|
+
run: bundle exec bundle-audit check
|
|
35
66
|
|
|
36
67
|
# Verifies that lib/pgn/pgn_parser.rb (committed) matches what `racc`
|
|
37
68
|
# regenerates from lib/pgn/pgn_parser.y, so the checked-in parser never
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: native
|
|
2
|
+
|
|
3
|
+
# Builds and tests the required Rust bitboard perft backend (a thin
|
|
4
|
+
# adapter over the `chessie` crate): runs the perft oracle via
|
|
5
|
+
# `cargo test`, compiles the native extension via `rake compile`, and
|
|
6
|
+
# runs the full RSpec suite (which loads the ext and includes the
|
|
7
|
+
# PGN::Bitboard::Engine specs).
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main, 'perf/**', 'feat/**']
|
|
12
|
+
pull_request:
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
test:
|
|
16
|
+
name: cargo test + rake compile + rspec
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: ruby/setup-ruby@v1
|
|
21
|
+
with:
|
|
22
|
+
ruby-version: '3.3'
|
|
23
|
+
bundler-cache: true
|
|
24
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
25
|
+
with:
|
|
26
|
+
components: rust-src
|
|
27
|
+
- name: cargo test (Rust engine + perft oracle)
|
|
28
|
+
run: cargo test --manifest-path ext/pgn2_native/Cargo.toml
|
|
29
|
+
- name: rake compile (build native ext)
|
|
30
|
+
run: bundle exec rake compile
|
|
31
|
+
- name: rspec (with native ext loaded)
|
|
32
|
+
run: bundle exec rspec
|
|
@@ -10,7 +10,9 @@ name: Publish to RubyGems
|
|
|
10
10
|
#
|
|
11
11
|
# To release a new version locally:
|
|
12
12
|
# bump lib/pgn/version.rb -> commit -> tag v<x.y.z> -> git push --tags
|
|
13
|
-
# This workflow then builds and pushes the .gem automatically
|
|
13
|
+
# This workflow then builds and pushes the .gem automatically — but only after
|
|
14
|
+
# the RSpec matrix, RuboCop, bundler-audit, and the racc in-sync check all
|
|
15
|
+
# pass.
|
|
14
16
|
|
|
15
17
|
on:
|
|
16
18
|
push:
|
|
@@ -31,11 +33,51 @@ jobs:
|
|
|
31
33
|
with:
|
|
32
34
|
ruby-version: ${{ matrix.ruby }}
|
|
33
35
|
bundler-cache: true
|
|
36
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
37
|
+
with:
|
|
38
|
+
components: rust-src
|
|
39
|
+
- run: bundle exec rake compile
|
|
34
40
|
- run: bundle exec rspec
|
|
35
41
|
|
|
42
|
+
rubocop:
|
|
43
|
+
name: RuboCop (on tag)
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
- uses: ruby/setup-ruby@v1
|
|
48
|
+
with:
|
|
49
|
+
ruby-version: '3.3'
|
|
50
|
+
bundler-cache: true
|
|
51
|
+
- run: bundle exec rubocop -f github
|
|
52
|
+
|
|
53
|
+
bundle-audit:
|
|
54
|
+
name: Bundler-audit (on tag)
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
- uses: ruby/setup-ruby@v1
|
|
59
|
+
with:
|
|
60
|
+
ruby-version: '3.3'
|
|
61
|
+
bundler-cache: true
|
|
62
|
+
- run: bundle exec bundle-audit update
|
|
63
|
+
- run: bundle exec bundle-audit check
|
|
64
|
+
|
|
65
|
+
parser-reproducible:
|
|
66
|
+
name: Racc parser in sync with grammar (on tag)
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
- uses: ruby/setup-ruby@v1
|
|
71
|
+
with:
|
|
72
|
+
ruby-version: '3.3'
|
|
73
|
+
bundler-cache: true
|
|
74
|
+
- run: |
|
|
75
|
+
bundle exec racc -o /tmp/pgn_parser.check.rb lib/pgn/pgn_parser.y
|
|
76
|
+
diff -u lib/pgn/pgn_parser.rb /tmp/pgn_parser.check.rb
|
|
77
|
+
|
|
36
78
|
publish:
|
|
37
79
|
name: Build & push gem
|
|
38
|
-
needs: test
|
|
80
|
+
needs: [test, rubocop, bundle-audit, parser-reproducible]
|
|
39
81
|
runs-on: ubuntu-latest
|
|
40
82
|
environment: release
|
|
41
83
|
permissions:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: release-gems
|
|
2
|
+
|
|
3
|
+
# Cross-compiles prebuilt native platform gems for the Rust bitboard
|
|
4
|
+
# backend using rake-compiler-dock (which spins up Docker images for each
|
|
5
|
+
# target). End users then `gem install pgn2` and get a binary — no Rust
|
|
6
|
+
# toolchain required. Push to RubyGems is gated on a published release;
|
|
7
|
+
# `workflow_dispatch` builds + uploads artifacts without pushing.
|
|
8
|
+
#
|
|
9
|
+
# Targets (set inside the rake-compiler-dock environment):
|
|
10
|
+
# x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin
|
|
11
|
+
# (configure the platform list in Rakefile cross-compile settings.)
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
release:
|
|
15
|
+
types: [published]
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
ruby-version: '3.3'
|
|
26
|
+
bundler-cache: true
|
|
27
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
28
|
+
- name: Build prebuilt platform gems
|
|
29
|
+
run: bundle exec rake native:gem
|
|
30
|
+
- uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: native-gems
|
|
33
|
+
path: pkg/*.gem
|
|
34
|
+
- name: Push to RubyGems
|
|
35
|
+
if: github.event_name == 'release'
|
|
36
|
+
run: |
|
|
37
|
+
for g in pkg/*.gem; do gem push "$g"; done
|
|
38
|
+
env:
|
|
39
|
+
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
|
40
|
+
GEM_HOST_OTP_CODE: ${{ secrets.RUBYGEMS_OTP }}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
name: Release
|
|
2
2
|
|
|
3
3
|
# Auto-releases on any push to `main` whose version (lib/pgn/version.rb) does
|
|
4
|
-
# not yet have a matching `v<x.y.z>` tag. It gates on the RSpec matrix,
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# not yet have a matching `v<x.y.z>` tag. It gates on the RSpec matrix,
|
|
5
|
+
# RuboCop, bundler-audit, and the racc in-sync check, then creates the tag
|
|
6
|
+
# and pushes the gem to rubygems.org in this same workflow (a tag pushed by
|
|
7
|
+
# the GITHUB_TOKEN does NOT trigger other workflows, so the publish happens
|
|
8
|
+
# here directly to avoid that limitation).
|
|
8
9
|
#
|
|
9
10
|
# Manual releases still work: `git tag vX.Y.Z && git push origin vX.Y.Z` fires
|
|
10
11
|
# publish.yml (human-pushed tags do trigger workflows).
|
|
@@ -51,11 +52,57 @@ jobs:
|
|
|
51
52
|
with:
|
|
52
53
|
ruby-version: ${{ matrix.ruby }}
|
|
53
54
|
bundler-cache: true
|
|
55
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
56
|
+
with:
|
|
57
|
+
components: rust-src
|
|
58
|
+
- run: bundle exec rake compile
|
|
54
59
|
- run: bundle exec rspec
|
|
55
60
|
|
|
61
|
+
rubocop:
|
|
62
|
+
name: RuboCop
|
|
63
|
+
needs: check
|
|
64
|
+
if: needs.check.outputs.should_release == 'true'
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
- uses: ruby/setup-ruby@v1
|
|
69
|
+
with:
|
|
70
|
+
ruby-version: '3.3'
|
|
71
|
+
bundler-cache: true
|
|
72
|
+
- run: bundle exec rubocop -f github
|
|
73
|
+
|
|
74
|
+
bundle-audit:
|
|
75
|
+
name: Bundler-audit
|
|
76
|
+
needs: check
|
|
77
|
+
if: needs.check.outputs.should_release == 'true'
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/checkout@v4
|
|
81
|
+
- uses: ruby/setup-ruby@v1
|
|
82
|
+
with:
|
|
83
|
+
ruby-version: '3.3'
|
|
84
|
+
bundler-cache: true
|
|
85
|
+
- run: bundle exec bundle-audit update
|
|
86
|
+
- run: bundle exec bundle-audit check
|
|
87
|
+
|
|
88
|
+
parser-reproducible:
|
|
89
|
+
name: Racc parser in sync with grammar
|
|
90
|
+
needs: check
|
|
91
|
+
if: needs.check.outputs.should_release == 'true'
|
|
92
|
+
runs-on: ubuntu-latest
|
|
93
|
+
steps:
|
|
94
|
+
- uses: actions/checkout@v4
|
|
95
|
+
- uses: ruby/setup-ruby@v1
|
|
96
|
+
with:
|
|
97
|
+
ruby-version: '3.3'
|
|
98
|
+
bundler-cache: true
|
|
99
|
+
- run: |
|
|
100
|
+
bundle exec racc -o /tmp/pgn_parser.check.rb lib/pgn/pgn_parser.y
|
|
101
|
+
diff -u lib/pgn/pgn_parser.rb /tmp/pgn_parser.check.rb
|
|
102
|
+
|
|
56
103
|
release:
|
|
57
104
|
name: Tag & publish
|
|
58
|
-
needs: [check, test]
|
|
105
|
+
needs: [check, test, rubocop, bundle-audit, parser-reproducible]
|
|
59
106
|
if: needs.check.outputs.should_release == 'true'
|
|
60
107
|
runs-on: ubuntu-latest
|
|
61
108
|
environment: release
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -7,20 +7,30 @@ AllCops:
|
|
|
7
7
|
- "bench/**/*"
|
|
8
8
|
- "vendor/**/*"
|
|
9
9
|
- "pkg/**/*"
|
|
10
|
+
- "tmp/**/*"
|
|
11
|
+
# rake-compiler's staging dir (a copy of the tree under tmp/) is a build
|
|
12
|
+
# artifact; lint it only indirectly via the real source files.
|
|
13
|
+
# lib/pgn/pgn_parser.rb is generated by racc from pgn_parser.y; CI keeps it
|
|
14
|
+
# in sync via the `parser-reproducible` job, so do not lint the generated
|
|
15
|
+
# output (racc emits non-conforming names/heredocs we cannot control).
|
|
16
|
+
- "lib/pgn/pgn_parser.rb"
|
|
10
17
|
|
|
11
|
-
# This is a small, mostly-stable gem; skip mandatory top-level
|
|
18
|
+
# This is a small, mostly-stable gem; skip mandatory top-level doc.
|
|
12
19
|
Style/Documentation:
|
|
13
20
|
Enabled: false
|
|
14
21
|
|
|
15
22
|
Style/StringLiterals:
|
|
16
23
|
EnforcedStyle: single_quotes
|
|
17
24
|
|
|
25
|
+
# Frozen-string-literal comments were previously disabled via an invalid
|
|
26
|
+
# `EnabledForRuby` parameter (which rubocop silently ignored, leaving the cop
|
|
27
|
+
# enabled and reporting across every file). Keep the intended behaviour —
|
|
28
|
+
# disabled — explicitly so the whole tree is consistent without churn.
|
|
18
29
|
Style/FrozenStringLiteralComment:
|
|
19
|
-
|
|
20
|
-
Enabled: true
|
|
30
|
+
Enabled: false
|
|
21
31
|
|
|
22
32
|
Layout/LineLength:
|
|
23
|
-
Max:
|
|
33
|
+
Max: 120
|
|
24
34
|
|
|
25
35
|
# The SAN/PGN grammar and move-calculation logic are inherently branchy;
|
|
26
36
|
# don't fight the domain, just keep an eye on egregious cases.
|
|
@@ -28,11 +38,41 @@ Metrics/MethodLength:
|
|
|
28
38
|
Max: 20
|
|
29
39
|
|
|
30
40
|
Metrics/AbcSize:
|
|
31
|
-
Max:
|
|
41
|
+
Max: 30
|
|
42
|
+
# Test helpers (e.g. expect_moves_equal) iterate over move trees and are
|
|
43
|
+
# naturally branchy; ABC size is not a useful signal there.
|
|
44
|
+
Exclude:
|
|
45
|
+
- "spec/**/*"
|
|
32
46
|
|
|
33
47
|
Metrics/ClassLength:
|
|
34
|
-
Max:
|
|
48
|
+
Max: 220
|
|
49
|
+
# notation.rb is the full SAN encoder; splitting it would hurt cohesion.
|
|
50
|
+
Exclude:
|
|
51
|
+
- "lib/pgn/notation.rb"
|
|
35
52
|
|
|
36
53
|
Metrics/BlockLength:
|
|
37
54
|
Exclude:
|
|
38
55
|
- "spec/**/*"
|
|
56
|
+
- "*.gemspec"
|
|
57
|
+
- "**/*.gemspec"
|
|
58
|
+
|
|
59
|
+
Metrics/CyclomaticComplexity:
|
|
60
|
+
Max: 11
|
|
61
|
+
|
|
62
|
+
Metrics/PerceivedComplexity:
|
|
63
|
+
Max: 11
|
|
64
|
+
|
|
65
|
+
Metrics/ParameterLists:
|
|
66
|
+
Max: 6
|
|
67
|
+
MaxOptionalParameters: 4
|
|
68
|
+
|
|
69
|
+
# `has_extras?` is part of the published public API; renaming to `extras?`
|
|
70
|
+
# would be a breaking change for a released gem, so disable this cop.
|
|
71
|
+
Naming/PredicatePrefix:
|
|
72
|
+
Enabled: false
|
|
73
|
+
|
|
74
|
+
# This gem intentionally declares development dependencies in the gemspec
|
|
75
|
+
# (consumed via `gemspec` in the Gemfile) — the canonical pattern for a
|
|
76
|
+
# distributable gem — rather than in a Gemfile group.
|
|
77
|
+
Gemspec/DevelopmentDependencies:
|
|
78
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,153 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
### Summary
|
|
6
|
+
|
|
7
|
+
Native Rust bitboard perft backend: a `pgn2-bitboard` **adapter over the
|
|
8
|
+
`chessie` crate** (MPL-2.0; magic-bitboard move generation with
|
|
9
|
+
checkmask/pinmask legality) exposed to Ruby as `PGN::Bitboard::Engine`
|
|
10
|
+
via a `magnus`/`rb_sys` `cdylib` (`pgn2_native`), shipped as precompiled
|
|
11
|
+
platform gems. The engine lives in `ext/pgn2_native/` and is fully
|
|
12
|
+
decoupled from the pure-Ruby 0x88 `Board`/`Notation`/`MoveCalculator`,
|
|
13
|
+
which stay byte-identical; all specs green. Fast perft (~90–140 Mnps
|
|
14
|
+
pure-Rust on x86-64/BMI2, ~4–6× the earlier hand-rolled engine) is the
|
|
15
|
+
primary deliverable; a thin `#legal_moves` / `#legal?` UCI API is the
|
|
16
|
+
byproduct. Perft is cross-validated against the published perft suite
|
|
17
|
+
(startpos/Kiwipete/positions 3–6).
|
|
18
|
+
|
|
19
|
+
**Major bump rationale:** the native extension is a *required compiled
|
|
20
|
+
artifact* (no pure-Ruby fallback for the shipped path), which changes the
|
|
21
|
+
install/packaging contract — hence 2.0.0 even though the Ruby API is
|
|
22
|
+
purely additive (`PGN::Bitboard` is new; existing classes are untouched).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **`PGN::Bitboard::Engine`** (native): `Engine.new(fen)`, `#perft(depth)`,
|
|
26
|
+
`#legal_moves` (sorted UCI), `#legal?(uci)`. Validates against the
|
|
27
|
+
published perft suite (startpos depth 6 = 119,060,324; Kiwipete depth 5
|
|
28
|
+
= 193,690,690; positions 3–6).
|
|
29
|
+
- **`ext/pgn2_native/`** Cargo workspace: `pgn2-bitboard` (thin adapter
|
|
30
|
+
over the `chessie` crate — `cargo test` runs the perft oracle) +
|
|
31
|
+
`pgn2_native` (`cdylib`, `magnus` bindings). Built via
|
|
32
|
+
`rb_sys`/`extconf.rb`; `bundle exec rake compile`.
|
|
33
|
+
- **`NOTICE.md`** + `spec.licenses = ['MIT', 'MPL-2.0']`: the gem's own
|
|
34
|
+
code stays MIT; the bundled `chessie`/`chessie_types` are MPL-2.0
|
|
35
|
+
(file-level copyleft, attributed in `NOTICE.md`, source pinned in
|
|
36
|
+
`ext/pgn2_native/Cargo.lock`).
|
|
37
|
+
- **`lib/pgn/bitboard.rb`** load gate: requires the native lib, rescuing
|
|
38
|
+
`LoadError` so the rest of the gem works without it.
|
|
39
|
+
- **`bench/perft.rb`** + `rake bench:perft`: perft nps benchmark.
|
|
40
|
+
- **CI**: `.github/workflows/native.yml` (`cargo test` + `rake compile` +
|
|
41
|
+
`rspec`) and `.github/workflows/release-gems.yml` (cross-compile
|
|
42
|
+
prebuilt platform gems via `rake-compiler-dock`).
|
|
43
|
+
- **`PGN::Position#perft(depth)`** and **`PGN::Position#legal_moves`**
|
|
44
|
+
(sorted UCI): FEN-round-trip delegations to `PGN::Bitboard::Engine`
|
|
45
|
+
(`Engine.new(position.to_fen.to_s)`), so perft/legal-move enumeration is
|
|
46
|
+
available directly on a `Position` without building the `Engine` by hand.
|
|
47
|
+
`#legal_moves` is ~30 µs/call on a middlegame position, measured by
|
|
48
|
+
`bench/legal_moves.rb`. Both raise `NameError` if the extension is absent.
|
|
49
|
+
- **`bench/legal_moves.rb`**: throughput benchmark for the `Position#legal_moves`
|
|
50
|
+
delegation (FEN build + `Engine.new` + native gen + string materialization).
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
- `attacks::init()` now initializes its attack tables through
|
|
54
|
+
`std::sync::Once` (was a bare `static mut bool` flipped under `unsafe` — a
|
|
55
|
+
data race / UB). No behavior change; `init()` remains idempotent. Removed the
|
|
56
|
+
three now-redundant per-method `attacks::init()` calls in the magnus binding
|
|
57
|
+
(`pgn2_native`), since the delegated crate functions self-initialize.
|
|
58
|
+
|
|
59
|
+
### Distribution note
|
|
60
|
+
|
|
61
|
+
This adds a **required compiled extension**. Release artifacts are
|
|
62
|
+
**precompiled platform gems** (x86_64/aarch64, linux/darwin) built in CI;
|
|
63
|
+
end users and the chessellence Docker build need no Rust toolchain. Interim
|
|
64
|
+
source builds install Rust in the Docker build stage (see README). The
|
|
65
|
+
pure-Ruby gem contract is otherwise unchanged; `PGN::Bitboard` is simply
|
|
66
|
+
undefined when the extension is absent.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Unreleased (attack-masks)
|
|
71
|
+
|
|
72
|
+
### Summary
|
|
73
|
+
|
|
74
|
+
Attack-mask pass: precomputed knight/king on-board target tables on
|
|
75
|
+
`PGN::Board`, used by `Notation` (reaches/attacked/leaper moves) and
|
|
76
|
+
`MoveCalculator` (knight/king origin lookup), plus a retained-memory
|
|
77
|
+
benchmark section. No behavior change; serialized PGN/FEN stays
|
|
78
|
+
byte-identical; all 226 specs green.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
- **`PGN::Board::KNIGHT_ATTACKS` / `KING_ATTACKS`**: new frozen 128-entry
|
|
82
|
+
tables mapping each 0x88 index to the frozen Array of on-board target
|
|
83
|
+
indices reachable by that piece (built once at load from the existing
|
|
84
|
+
offsets). Replaces the per-call `OFFS.any? { |o| from + o == to }` +
|
|
85
|
+
`(t & 0x88).zero?` off-board test with a direct array iteration.
|
|
86
|
+
- **`PGN::Notation`**: `#reaches?` (N/K), `#knight_attacked?`,
|
|
87
|
+
`#king_attacked?`, and `#leaper_moves?` now iterate the precomputed masks
|
|
88
|
+
instead of offsets. `leaper_moves?` takes targets directly (no per-call
|
|
89
|
+
off-board test).
|
|
90
|
+
- **`PGN::MoveCalculator`**: K/N origin lookup routes through a new
|
|
91
|
+
`leaper_origins` using `Board::KNIGHT_ATTACKS`/`KING_ATTACKS`; the pawn
|
|
92
|
+
path keeps the offset-based `move_origins`.
|
|
93
|
+
- **Bench** (`bench/profile_moves.rb`): new section 8 (SAN generation
|
|
94
|
+
allocation + throughput) and section 9 (retained memory: lazy
|
|
95
|
+
`each_position` vs eager `positions`). Baselines refreshed.
|
|
96
|
+
|
|
97
|
+
### Performance
|
|
98
|
+
- **SAN generation** (`Notation.san` over the immortal game): throughput
|
|
99
|
+
**+8%** (334 → 361 ips, 2.99 → 2.77 ms/i) from a same-harness A/B
|
|
100
|
+
(pre-mask lib vs masks). Allocations unchanged (1387 objects) — the masks
|
|
101
|
+
only replace arithmetic + an off-board test, no new objects per move.
|
|
102
|
+
- **Replay**: neutral. Same-harness A/B: 930 → 931 objects / +6720 bytes
|
|
103
|
+
with masks; throughput within noise. (The committed replay baseline
|
|
104
|
+
refreshed 976 → 931 objects, which is measurement-environment variance
|
|
105
|
+
between runs of identical pre-mask code, not an effect of the masks.)
|
|
106
|
+
- **Retention** (new section 9): a caller that streams `each_position` and
|
|
107
|
+
keeps the `Game` retains 95 objects / 6280 bytes; a caller that calls
|
|
108
|
+
`positions` (memoizing the full array on the Game) retains 287 objects /
|
|
109
|
+
17232 bytes — ~3x less retained for the streaming pattern.
|
|
110
|
+
|
|
111
|
+
## 1.5.0 (2026-08-13)
|
|
112
|
+
|
|
113
|
+
Performance/internals pass: a direct 0x88 FEN builder, a lazy
|
|
114
|
+
`Game#each_position` enumerator, and a lazily-computed Zobrist position
|
|
115
|
+
hash with `Position#hash`/`#eql?`/`#==`. No public behavior change;
|
|
116
|
+
serialized PGN/FEN output stays byte-identical; all 222 specs green.
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
- **`PGN::Board#fen_board_string`**: new method that serializes the FEN
|
|
120
|
+
board-string portion by walking the 0x88 `@cells` array directly (ranks
|
|
121
|
+
8→1, files a→h, empty-run collapsing), instead of rebuilding the 8x8
|
|
122
|
+
`squares` array and transposing. `FEN#board_string` now delegates to it,
|
|
123
|
+
so every `position.to_fen` / `game.fen_list` call skips the 8x8 rebuild.
|
|
124
|
+
FEN output is byte-identical (the `board_string round-trip` spec pins
|
|
125
|
+
it). Measured (immortal game, 46 positions): 1913 objects / 100464
|
|
126
|
+
bytes for FEN generation; ~1.27k ips.
|
|
127
|
+
- **`PGN::Game#each_position`**: new lazy enumerator (yields each
|
|
128
|
+
`PGN::Position` in order, or returns an `Enumerator` without a block);
|
|
129
|
+
`#positions` is now `each_position.to_a` with the same memoization.
|
|
130
|
+
Callers that only need the last position can stream without
|
|
131
|
+
materializing the full array. `positions` still returns the same
|
|
132
|
+
`Array`. Cost: one `Enumerator` per `#positions` call (parse+replay
|
|
133
|
+
for 500 games: +500 objects, throughput flat).
|
|
134
|
+
- **`PGN::Zobrist`**: new module with a deterministic 64-bit Zobrist key
|
|
135
|
+
table (`table`/`side`/`castling`/`ep_file`) and a `seed(board, player,
|
|
136
|
+
castling, en_passant)` helper, generated once from a frozen seed so
|
|
137
|
+
hashes are stable across processes. Pure Ruby, no native deps.
|
|
138
|
+
- **`PGN::Position`**: new `#zobrist` (the hash, computed lazily on first
|
|
139
|
+
access and cached), `#hash`, `#eql?`, and `#==`. Equality compares
|
|
140
|
+
board cells, side to move, castling rights, and en-passant square —
|
|
141
|
+
halfmove/fullmove counters are ignored, matching threefold-repetition
|
|
142
|
+
semantics. The hash is lazy: the replay hot path (which never asks for
|
|
143
|
+
it) pays nothing (replay stays at 976 objects / 62064 bytes,
|
|
144
|
+
byte-identical to 1.5.0); consumers pay one full seed on demand. An
|
|
145
|
+
incremental per-move update was prototyped and rejected: 64-bit
|
|
146
|
+
Integer XOR allocates a new `Bignum` per operation (~9 per move),
|
|
147
|
+
which regressed replay by +40% allocations / −32% throughput for a
|
|
148
|
+
feature nothing currently consumes — laziness keeps the public API
|
|
149
|
+
with zero hot-path cost.
|
|
150
|
+
|
|
3
151
|
## 1.5.0 (2026-08-13)
|
|
4
152
|
|
|
5
153
|
### Summary
|
data/Gemfile
CHANGED
data/NOTICE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
This gem bundles a compiled Rust extension (`pgn2_native`) that links
|
|
4
|
+
the `chessie` crate.
|
|
5
|
+
|
|
6
|
+
## chessie
|
|
7
|
+
|
|
8
|
+
- Source: https://crates.io/crates/chessie
|
|
9
|
+
- Repository: https://github.com/duck2/chessie
|
|
10
|
+
- Version: 2.0.x (see `ext/pgn2_native/Cargo.lock` for the exact pinned
|
|
11
|
+
version)
|
|
12
|
+
- License: Mozilla Public License 2.0 (MPL-2.0)
|
|
13
|
+
|
|
14
|
+
`chessie` and its dependency `chessie_types` are MPL-2.0. They are used
|
|
15
|
+
unmodified. The MPL-2.0 license is file-level copyleft: it applies to
|
|
16
|
+
`chessie`'s own source files only and does not change the license of
|
|
17
|
+
this gem's code (MIT). Per MPL-2.0 §3.3, the source of the MPL-licensed
|
|
18
|
+
files is available at the repository URL above (and is reproducibly
|
|
19
|
+
pinned in `ext/pgn2_native/Cargo.lock`).
|
|
20
|
+
|
|
21
|
+
pgn2's own code remains MIT-licensed; see `LICENSE.txt`.
|