sashite-ggn 0.6.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dab117559875291772b7e8f60077f670928755870b39badd3555f4d66fd8357
4
- data.tar.gz: 2fe3fd39dbb288dc140b10c4c9fec523d2ce00a7d33d7802d8612b95e2268ed3
3
+ metadata.gz: 71b56f3fe3aa4fe158bc428cb92880751133bfc769ac9a300f78026e51c95ef6
4
+ data.tar.gz: 779187aa54e75e9ef0d679bc0617b4a6230052e5ab24018409932654cd351be7
5
5
  SHA512:
6
- metadata.gz: 805c62503574bbc3b985cc21cb4dfe7a15e03198d97a5e7f0d91508111fd7290ed7382f93f2db6d653f07593f06a65c2640ac4e027127e0bdd964cdc61edab5c
7
- data.tar.gz: fc0330900649313e22511f44d391d5a73befdabe4038aa4744de3f8af12684767c410bd6f8aa7e4a88f961cecc6d7be1223e00cfd9736396d92d7c781fa5abea
6
+ metadata.gz: 94c8dd3916c7032479547f949e75a6b1865f95a19d1baba421947931b7bb2dd700dc8672b0297734c677ae02152dfd7526eec1a6cfed549d6f484cc3d97dbba3
7
+ data.tar.gz: 818b73b8a42e82e968e02aac3b9e8cade1f46a59765e56d19f5f3e8fe68478395a8d3134a074e0037890a61eadbc39989a406e7ff86a580f8defda5abb0d1d0d
data/README.md CHANGED
@@ -1,152 +1,500 @@
1
1
  # Ggn.rb
2
2
 
3
- A Ruby implementation of the General Gameplay Notation (GGN) specification for describing pseudo-legal moves in abstract strategy board games.
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/ggn.rb?label=Version&logo=github)](https://github.com/sashite/ggn.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/ggn.rb/main)
5
+ ![Ruby](https://github.com/sashite/ggn.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/ggn.rb?label=License&logo=github)](https://github.com/sashite/ggn.rb/raw/main/LICENSE.md)
4
7
 
5
- [![Gem Version](https://badge.fury.io/rb/sashite-ggn.svg)](https://badge.fury.io/rb/sashite-ggn)
6
- [![Ruby](https://github.com/sashite/ggn.rb/workflows/Ruby/badge.svg)](https://github.com/sashite/ggn.rb/actions)
8
+ > **GGN** (General Gameplay Notation) implementation for Ruby — a pure, functional library for evaluating **movement possibilities** in abstract strategy board games.
9
+
10
+ ---
7
11
 
8
12
  ## What is GGN?
9
13
 
10
- GGN (General Gameplay Notation) is a rule-agnostic, JSON-based format for representing pseudo-legal moves in abstract strategy board games. This gem implements the [GGN Specification v1.0.0](https://sashite.dev/documents/ggn/1.0.0/).
14
+ GGN (General Gameplay Notation) is a rule-agnostic format for describing **pseudo-legal moves** in abstract strategy board games. GGN serves as a **movement possibility oracle**: given a movement context (piece and source location) plus a destination location, it determines if the movement is feasible under specified pre-conditions.
15
+
16
+ This gem implements the [GGN Specification v1.0.0](https://sashite.dev/specs/ggn/1.0.0/), providing complete movement possibility evaluation with environmental constraint checking.
17
+
18
+ ### Core Philosophy
11
19
 
12
- GGN focuses exclusively on **board-to-board transformations**: pieces moving, capturing, or transforming on the game board. It describes basic movement constraints rather than game-specific legality rules, making it suitable for:
20
+ GGN answers the fundamental question:
13
21
 
14
- - Cross-game move analysis and engine development
15
- - Hybrid games combining elements from different chess variants
16
- - Database systems requiring piece disambiguation across game types
17
- - Performance-critical applications with pre-computed move libraries
22
+ > **Can this piece, currently at this location, reach that location?**
18
23
 
19
- **Note**: GGN does not support hand management, piece drops, or captures-to-hand. These mechanics should be handled at a higher level by game engines.
24
+ It encodes:
25
+ - **Which piece** (via QPI format)
26
+ - **From where** (source location using CELL or HAND)
27
+ - **To where** (destination location using CELL or HAND)
28
+ - **Which environmental pre-conditions** must hold (`must`)
29
+ - **Which environmental pre-conditions** must not hold (`deny`)
30
+ - **What changes occur** if executed (`diff` in STN format)
31
+
32
+ ---
20
33
 
21
34
  ## Installation
22
35
 
23
36
  ```ruby
24
- gem 'sashite-ggn'
37
+ # In your Gemfile
38
+ gem "sashite-ggn"
25
39
  ```
26
40
 
27
- ## Basic Usage
41
+ Or install manually:
42
+
43
+ ```sh
44
+ gem install sashite-ggn
45
+ ```
28
46
 
29
- ### Loading GGN Data
47
+ ---
48
+
49
+ ## Dependencies
50
+
51
+ GGN builds upon foundational Sashité specifications:
30
52
 
31
53
  ```ruby
32
- require "sashite-ggn"
54
+ gem "sashite-cell" # Coordinate Encoding for Layered Locations
55
+ gem "sashite-feen" # Forsyth–Edwards Enhanced Notation
56
+ gem "sashite-hand" # Hold And Notation Designator
57
+ gem "sashite-lcn" # Location Condition Notation
58
+ gem "sashite-qpi" # Qualified Piece Identifier
59
+ gem "sashite-stn" # State Transition Notation
60
+ ```
33
61
 
34
- # Load from file
35
- ruleset = Sashite::Ggn.load_file("chess_moves.json")
62
+ ---
36
63
 
37
- # Load from string
38
- json = '{"CHESS:P": {"e2": {"e4": [{"perform": {"e2": null, "e4": "CHESS:P"}}]}}}'
39
- ruleset = Sashite::Ggn.load_string(json)
64
+ ## Quick Start
65
+
66
+ ```ruby
67
+ require "sashite/ggn"
68
+
69
+ # Parse GGN data structure
70
+ ggn_data = {
71
+ "C:P" => {
72
+ "e2" => {
73
+ "e4" => [
74
+ {
75
+ "must" => { "e3" => "empty", "e4" => "empty" },
76
+ "deny" => {},
77
+ "diff" => {
78
+ "board" => { "e2" => nil, "e4" => "C:P" },
79
+ "toggle" => true
80
+ }
81
+ }
82
+ ]
83
+ }
84
+ }
85
+ }
86
+
87
+ ruleset = Sashite::Ggn.parse(ggn_data)
88
+
89
+ # Query movement possibility through method chaining
90
+ feen = "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
91
+ transitions = ruleset.select("C:P").from("e2").to("e4").where(feen)
92
+
93
+ transitions.any? # => true
40
94
  ```
41
95
 
42
- ### Evaluating Moves
96
+ ---
97
+
98
+ ## API Reference
99
+
100
+ ### Module Functions
101
+
102
+ #### `Sashite::Ggn.parse(data) → Ruleset`
103
+
104
+ Parses GGN data structure into an immutable Ruleset object.
43
105
 
44
106
  ```ruby
45
- # Query specific move
46
- engine = ruleset.select("CHESS:P").from("e2").to("e4")
107
+ ruleset = Sashite::Ggn.parse(ggn_data)
108
+ ```
47
109
 
48
- # Define board state
49
- board_state = { "e2" => "CHESS:P", "e3" => nil, "e4" => nil }
110
+ **Parameters:**
111
+ - `data` (Hash): GGN data structure conforming to specification
50
112
 
51
- # Evaluate move
52
- transitions = engine.where(board_state, "CHESS")
53
- # => [#<Transition diff={"e2"=>nil, "e4"=>"CHESS:P"}>]
113
+ **Returns:** `Ruleset` — Immutable ruleset object
114
+
115
+ **Raises:** `ArgumentError` If data structure is invalid
116
+
117
+ ---
118
+
119
+ #### `Sashite::Ggn.valid?(data) → Boolean`
120
+
121
+ Validates GGN data structure against specification.
122
+
123
+ ```ruby
124
+ Sashite::Ggn.valid?(ggn_data) # => true
54
125
  ```
55
126
 
56
- ### Generating All Moves
127
+ **Parameters:**
128
+ - `data` (Hash): Data structure to validate
129
+
130
+ **Returns:** `Boolean` — True if valid, false otherwise
131
+
132
+ ---
133
+
134
+ ### `Sashite::Ggn::Ruleset` Class
135
+
136
+ Immutable container for GGN movement rules.
137
+
138
+ #### `#select(piece) → Source`
139
+
140
+ Selects movement rules for a specific piece type.
57
141
 
58
142
  ```ruby
59
- # Get all pseudo-legal moves for current position
60
- all_moves = ruleset.pseudo_legal_transitions(board_state, "CHESS")
143
+ source = ruleset.select("C:K")
144
+ ```
61
145
 
62
- # Each move is represented as [actor, origin, target, transitions]
63
- all_moves.each do |actor, origin, target, transitions|
64
- puts "#{actor}: #{origin} → #{target} (#{transitions.size} variants)"
65
- end
146
+ **Parameters:**
147
+ - `piece` (String): QPI piece identifier
148
+
149
+ **Returns:** `Source` — Source selector object
150
+
151
+ **Raises:** `KeyError` — If piece not found in ruleset
152
+
153
+ ---
154
+
155
+ #### `#pseudo_legal_transitions(feen) → Array<Array>`
156
+
157
+ Generates all pseudo-legal moves for the given position.
158
+
159
+ ```ruby
160
+ moves = ruleset.pseudo_legal_transitions(feen)
161
+ # => [["C:P", "e2", "e4", [#<Transition...>]], ...]
66
162
  ```
67
163
 
68
- ## Move Variants
164
+ **Parameters:**
165
+ - `feen` (String): Position in FEEN format
166
+
167
+ **Returns:** `Array<Array>` — Array of `[piece, source, destination, transitions]` tuples
168
+
169
+ ---
69
170
 
70
- GGN supports multiple outcomes for a single move (e.g., promotion choices):
171
+ #### `#piece?(piece) Boolean`
172
+
173
+ Checks if ruleset contains movement rules for specified piece.
71
174
 
72
175
  ```ruby
73
- # Chess pawn promotion
74
- engine = ruleset.select("CHESS:P").from("e7").to("e8")
75
- transitions = engine.where({"e7" => "CHESS:P", "e8" => nil}, "CHESS")
176
+ ruleset.piece?("C:K") # => true
177
+ ```
76
178
 
77
- transitions.each do |transition|
78
- promoted_piece = transition.diff["e8"]
79
- puts "Promote to #{promoted_piece}"
80
- end
81
- # Output: CHESS:Q, CHESS:R, CHESS:B, CHESS:N
179
+ **Parameters:**
180
+ - `piece` (String): QPI piece identifier
181
+
182
+ **Returns:** `Boolean`
183
+
184
+ ---
185
+
186
+ #### `#pieces → Array<String>`
187
+
188
+ Returns all piece identifiers in ruleset.
189
+
190
+ ```ruby
191
+ ruleset.pieces # => ["C:K", "C:Q", "C:P", ...]
82
192
  ```
83
193
 
84
- ## Complex Moves
194
+ **Returns:** `Array<String>` — QPI piece identifiers
195
+
196
+ ---
197
+
198
+ #### `#to_h → Hash`
85
199
 
86
- GGN can represent multi-square moves like castling:
200
+ Converts ruleset to hash representation.
87
201
 
88
202
  ```ruby
89
- # Castling (king and rook move simultaneously)
90
- engine = ruleset.select("CHESS:K").from("e1").to("g1")
91
- board_state = {
92
- "e1" => "CHESS:K", "f1" => nil, "g1" => nil, "h1" => "CHESS:R"
93
- }
203
+ ruleset.to_h # => { "C:K" => { "e1" => { "e2" => [...] } } }
204
+ ```
205
+
206
+ **Returns:** `Hash` GGN data structure
207
+
208
+ ---
209
+
210
+ ### `Sashite::Ggn::Ruleset::Source` Class
211
+
212
+ Represents movement possibilities for a piece type.
213
+
214
+ #### `#from(source) → Destination`
215
+
216
+ Specifies the source location for the piece.
217
+
218
+ ```ruby
219
+ destination = source.from("e1")
220
+ ```
221
+
222
+ **Parameters:**
223
+ - `source` (String): Source location (CELL coordinate or HAND "*")
224
+
225
+ **Returns:** `Destination` — Destination selector object
226
+
227
+ **Raises:** `KeyError` — If source not found for this piece
228
+
229
+ ---
230
+
231
+ #### `#sources → Array<String>`
232
+
233
+ Returns all valid source locations for this piece.
234
+
235
+ ```ruby
236
+ source.sources # => ["e1", "d1", "*"]
237
+ ```
238
+
239
+ **Returns:** `Array<String>` — Source locations
240
+
241
+ ---
242
+
243
+ #### `#source?(location) → Boolean`
244
+
245
+ Checks if location is a valid source for this piece.
246
+
247
+ ```ruby
248
+ source.source?("e1") # => true
249
+ ```
94
250
 
95
- transitions = engine.where(board_state, "CHESS")
96
- # => [#<Transition diff={"e1"=>nil, "f1"=>"CHESS:R", "g1"=>"CHESS:K", "h1"=>nil}>]
251
+ **Parameters:**
252
+ - `location` (String): Source location
253
+
254
+ **Returns:** `Boolean`
255
+
256
+ ---
257
+
258
+ ### `Sashite::Ggn::Ruleset::Source::Destination` Class
259
+
260
+ Represents movement possibilities from a specific source.
261
+
262
+ #### `#to(destination) → Engine`
263
+
264
+ Specifies the destination location.
265
+
266
+ ```ruby
267
+ engine = destination.to("e2")
268
+ ```
269
+
270
+ **Parameters:**
271
+ - `destination` (String): Destination location (CELL coordinate or HAND "*")
272
+
273
+ **Returns:** `Engine` — Movement evaluation engine
274
+
275
+ **Raises:** `KeyError` — If destination not found from this source
276
+
277
+ ---
278
+
279
+ #### `#destinations → Array<String>`
280
+
281
+ Returns all valid destinations from this source.
282
+
283
+ ```ruby
284
+ destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]
285
+ ```
286
+
287
+ **Returns:** `Array<String>` — Destination locations
288
+
289
+ ---
290
+
291
+ #### `#destination?(location) → Boolean`
292
+
293
+ Checks if location is a valid destination from this source.
294
+
295
+ ```ruby
296
+ destination.destination?("e2") # => true
297
+ ```
298
+
299
+ **Parameters:**
300
+ - `location` (String): Destination location
301
+
302
+ **Returns:** `Boolean`
303
+
304
+ ---
305
+
306
+ ### `Sashite::Ggn::Ruleset::Source::Destination::Engine` Class
307
+
308
+ Evaluates movement possibility under given position conditions.
309
+
310
+ #### `#where(feen) → Array<Transition>`
311
+
312
+ Evaluates movement against position and returns valid transitions.
313
+
314
+ ```ruby
315
+ transitions = engine.where(feen)
316
+ ```
317
+
318
+ **Parameters:**
319
+ - `feen` (String): Position in FEEN format
320
+
321
+ **Returns:** `Array<Sashite::Stn::Transition>` — Valid state transitions (may be empty)
322
+
323
+ ---
324
+
325
+ #### `#possibilities → Array<Hash>`
326
+
327
+ Returns raw movement possibility rules.
328
+
329
+ ```ruby
330
+ engine.possibilities
331
+ # => [{ "must" => {...}, "deny" => {...}, "diff" => {...} }]
97
332
  ```
98
333
 
99
- ## Conditional Moves
334
+ **Returns:** `Array<Hash>` — Movement possibility specifications
335
+
336
+ ---
337
+
338
+ ## GGN Format
100
339
 
101
- GGN supports moves with complex requirements and prevent conditions:
340
+ ### Structure
102
341
 
103
342
  ```ruby
104
- # En passant capture (removes pawn from different square)
105
- engine = ruleset.select("CHESS:P").from("d5").to("e6")
106
- board_state = {
107
- "d5" => "CHESS:P", "e5" => "chess:p", "e6" => nil
343
+ {
344
+ "<qpi-piece>" => {
345
+ "<source-location>" => {
346
+ "<destination-location>" => [
347
+ {
348
+ "must" => { /* LCN format */ },
349
+ "deny" => { /* LCN format */ },
350
+ "diff" => { /* STN format */ }
351
+ }
352
+ ]
353
+ }
354
+ }
108
355
  }
356
+ ```
357
+
358
+ ### Field Specifications
359
+
360
+ | Field | Type | Description |
361
+ |-------|------|-------------|
362
+ | **Piece** | String (QPI) | Piece identifier (e.g., `"C:K"`, `"s:+p"`) |
363
+ | **Source** | String (CELL/HAND) | Origin location (e.g., `"e2"`, `"*"`) |
364
+ | **Destination** | String (CELL/HAND) | Target location (e.g., `"e4"`, `"*"`) |
365
+ | **must** | Hash (LCN) | Pre-conditions that must be satisfied |
366
+ | **deny** | Hash (LCN) | Pre-conditions that must not be satisfied |
367
+ | **diff** | Hash (STN) | State transition specification |
368
+
369
+ ---
370
+
371
+ ## Usage Examples
372
+
373
+ ### Method Chaining
374
+
375
+ ```ruby
376
+ # Query specific movement
377
+ feen = "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
109
378
 
110
- transitions = engine.where(board_state, "CHESS")
111
- # => [#<Transition diff={"d5"=>nil, "e5"=>nil, "e6"=>"CHESS:P"}>]
379
+ transitions = ruleset
380
+ .select("C:P")
381
+ .from("e2")
382
+ .to("e4")
383
+ .where(feen)
384
+
385
+ transitions.size # => 1
386
+ transitions.first.board_changes # => { "e2" => nil, "e4" => "C:P" }
112
387
  ```
113
388
 
114
- ## Validation
389
+ ### Generate All Pseudo-Legal Moves
115
390
 
116
391
  ```ruby
117
- # Validate GGN data
118
- Sashite::Ggn.validate!(data) # Raises exception on failure
119
- Sashite::Ggn.valid?(data) # Returns boolean
392
+ feen = "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
393
+
394
+ all_moves = ruleset.pseudo_legal_transitions(feen)
395
+
396
+ all_moves.each do |piece, source, destination, transitions|
397
+ puts "#{piece}: #{source} → #{destination} (#{transitions.size} variants)"
398
+ end
120
399
  ```
121
400
 
122
- ## API Reference
401
+ ### Existence Checks
123
402
 
124
- ### Core Classes
403
+ ```ruby
404
+ # Check if piece exists in ruleset
405
+ ruleset.piece?("C:K") # => true
406
+
407
+ # Check valid sources
408
+ source = ruleset.select("C:K")
409
+ source.source?("e1") # => true
125
410
 
126
- - `Sashite::Ggn::Ruleset` - Main entry point for querying moves
127
- - `Sashite::Ggn::Ruleset::Source` - Piece type with source positions
128
- - `Sashite::Ggn::Ruleset::Source::Destination` - Available destinations
129
- - `Sashite::Ggn::Ruleset::Source::Destination::Engine` - Move evaluator
130
- - `Sashite::Ggn::Ruleset::Source::Destination::Engine::Transition` - Move result
411
+ # Check valid destinations
412
+ destination = source.from("e1")
413
+ destination.destination?("e2") # => true
414
+ ```
131
415
 
132
- ### Key Methods
416
+ ### Introspection
417
+
418
+ ```ruby
419
+ # List all pieces
420
+ ruleset.pieces # => ["C:K", "C:Q", "C:R", ...]
133
421
 
134
- - `#pseudo_legal_transitions(board_state, active_game)` - Generate all moves
135
- - `#select(actor).from(origin).to(target)` - Query specific move
136
- - `#where(board_state, active_game)` - Evaluate move validity
422
+ # List sources for a piece
423
+ source.sources # => ["e1", "d1", "f1", ...]
424
+
425
+ # List destinations from a source
426
+ destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]
427
+
428
+ # Access raw possibilities
429
+ engine.possibilities
430
+ # => [{ "must" => {...}, "deny" => {...}, "diff" => {...} }]
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Design Properties
436
+
437
+ - **Functional**: Pure functions with no side effects
438
+ - **Immutable**: All data structures frozen and unchangeable
439
+ - **Composable**: Clean method chaining for natural query flow
440
+ - **Type-safe**: Strict validation of all inputs
441
+ - **Delegative**: Leverages CELL, FEEN, HAND, LCN, QPI, STN specifications
442
+ - **Spec-compliant**: Strictly follows GGN v1.0.0 specification
443
+
444
+ ---
445
+
446
+ ## Error Handling
447
+
448
+ ```ruby
449
+ # Handle missing piece
450
+ begin
451
+ source = ruleset.select("INVALID:X")
452
+ rescue KeyError => e
453
+ puts "Piece not found: #{e.message}"
454
+ end
455
+
456
+ # Handle missing source
457
+ begin
458
+ destination = source.from("z9")
459
+ rescue KeyError => e
460
+ puts "Source not found: #{e.message}"
461
+ end
462
+
463
+ # Handle missing destination
464
+ begin
465
+ engine = destination.to("z9")
466
+ rescue KeyError => e
467
+ puts "Destination not found: #{e.message}"
468
+ end
469
+
470
+ # Safe validation before parsing
471
+ if Sashite::Ggn.valid?(data)
472
+ ruleset = Sashite::Ggn.parse(data)
473
+ else
474
+ puts "Invalid GGN structure"
475
+ end
476
+ ```
477
+
478
+ ---
137
479
 
138
480
  ## Related Specifications
139
481
 
140
- GGN works alongside other Sashité specifications:
482
+ - [GGN v1.0.0](https://sashite.dev/specs/ggn/1.0.0/) General Gameplay Notation specification
483
+ - [CELL v1.0.0](https://sashite.dev/specs/cell/1.0.0/) — Coordinate encoding
484
+ - [FEEN v1.0.0](https://sashite.dev/specs/feen/1.0.0/) — Position notation
485
+ - [HAND v1.0.0](https://sashite.dev/specs/hand/1.0.0/) — Reserve notation
486
+ - [LCN v1.0.0](https://sashite.dev/specs/lcn/1.0.0/) — Location conditions
487
+ - [QPI v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) — Piece identification
488
+ - [STN v1.0.0](https://sashite.dev/specs/stn/1.0.0/) — State transitions
141
489
 
142
- - [GAN](https://sashite.dev/documents/gan/1.0.0/) - General Actor Notation for piece identifiers
143
- - [FEEN](https://sashite.dev/documents/feen/1.0.0/) - Board position representation
144
- - [PMN](https://sashite.dev/documents/pmn/1.0.0/) - Move sequence representation
490
+ ---
145
491
 
146
492
  ## License
147
493
 
148
- The [gem](https://rubygems.org/gems/sashite-ggn) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
494
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
495
+
496
+ ---
149
497
 
150
- ## About Sashité
498
+ ## About
151
499
 
152
- This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
500
+ Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.