sashite-ggn 0.7.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.
data/README.md CHANGED
@@ -1,762 +1,500 @@
1
1
  # Ggn.rb
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/sashite-ggn.svg)](https://badge.fury.io/rb/sashite-ggn)
4
- [![Ruby](https://github.com/sashite/ggn.rb/workflows/Ruby/badge.svg)](https://github.com/sashite/ggn.rb/actions)
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/ggn.rb?label=Version&logo=github)](https://github.com/sashite/ggn.rb/tags)
5
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
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)
7
7
 
8
- > A Ruby library for **GGN** (General Gameplay Notation) - a rule-agnostic format for describing pseudo-legal moves in abstract strategy board games.
8
+ > **GGN** (General Gameplay Notation) implementation for Ruby — a pure, functional library for evaluating **movement possibilities** in abstract strategy board games.
9
+
10
+ ---
9
11
 
10
12
  ## What is GGN?
11
13
 
12
- GGN is like having a universal "move library" that works across different board games. Think of it as a detailed catalog that answers: **"Can this piece, currently on this square, reach that square?"** - without worrying about specific game rules like check, ko, or castling rights.
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.
13
15
 
14
- **Key Features:**
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.
15
17
 
16
- - **Rule-agnostic**: Works with Chess, Shōgi, Xiangqi, and custom variants
17
- - **Board-focused**: Describes only board transformations (no hand management)
18
- - **Pseudo-legal**: Basic movement constraints, not full game legality
19
- - **JSON-based**: Structured, machine-readable format
20
- - **Performance-optimized**: Pre-computed move libraries for fast evaluation
21
- - **Cross-game compatible**: Supports hybrid games mixing different variants
22
- - **Flexible validation**: Choose between safety and performance
18
+ ### Core Philosophy
23
19
 
24
- ## Installation
20
+ GGN answers the fundamental question:
21
+
22
+ > **Can this piece, currently at this location, reach that location?**
25
23
 
26
- Add this line to your application's Gemfile:
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
+ ---
33
+
34
+ ## Installation
27
35
 
28
36
  ```ruby
37
+ # In your Gemfile
29
38
  gem "sashite-ggn"
30
39
  ```
31
40
 
32
- Or install it directly:
41
+ Or install manually:
33
42
 
34
- ```bash
43
+ ```sh
35
44
  gem install sashite-ggn
36
45
  ```
37
46
 
38
- ## Quick Start
39
-
40
- ### Basic Example: Loading Move Rules
41
-
42
- ```ruby
43
- require "sashite-ggn"
44
-
45
- # Load GGN data from file (with full validation by default)
46
- ruleset = Sashite::Ggn.load_file("chess_moves.json")
47
+ ---
47
48
 
48
- # Query specific piece movement rules
49
- pawn_source = ruleset.select("CHESS:P")
50
- destinations = pawn_source.from("e2")
51
- engine = destinations.to("e4")
49
+ ## Dependencies
52
50
 
53
- # Check if move is valid given current board state
54
- board_state = {
55
- "e2" => "CHESS:P", # White pawn on e2
56
- "e3" => nil, # Empty square
57
- "e4" => nil # Empty square
58
- }
51
+ GGN builds upon foundational Sashité specifications:
59
52
 
60
- transitions = engine.where(board_state, "CHESS")
61
-
62
- if transitions.any?
63
- transition = transitions.first
64
- puts "Move is valid!"
65
- puts "Board changes: #{transition.diff}"
66
- # => { "e2" => nil, "e4" => "CHESS:P" }
67
- else
68
- puts "Move blocked or invalid"
69
- end
53
+ ```ruby
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
70
60
  ```
71
61
 
72
- ### Basic Example: Loading from JSON String
62
+ ---
63
+
64
+ ## Quick Start
73
65
 
74
66
  ```ruby
75
- # Simple pawn double move rule
76
- ggn_json = {
77
- "CHESS:P" => {
67
+ require "sashite/ggn"
68
+
69
+ # Parse GGN data structure
70
+ ggn_data = {
71
+ "C:P" => {
78
72
  "e2" => {
79
- "e4" => [{
80
- "require" => { "e3" => "empty", "e4" => "empty" },
81
- "perform" => { "e2" => nil, "e4" => "CHESS:P" }
82
- }]
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
83
  }
84
84
  }
85
85
  }
86
86
 
87
- ruleset = Sashite::Ggn.load_hash(ggn_json)
88
- puts "Loaded pawn movement rules!"
89
- ```
87
+ ruleset = Sashite::Ggn.parse(ggn_data)
90
88
 
91
- ## Validation System
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
92
 
93
- Ggn.rb offers **flexible validation** with two modes:
94
-
95
- ### Full Validation (Default)
96
- ```ruby
97
- # All validations enabled (recommended for development/safety)
98
- ruleset = Sashite::Ggn.load_file("moves.json")
99
- # ✓ JSON Schema validation
100
- # ✓ Logical contradiction detection
101
- # ✓ Implicit requirement duplication detection
93
+ transitions.any? # => true
102
94
  ```
103
95
 
104
- ### Performance Mode
105
- ```ruby
106
- # All validations disabled (maximum performance)
107
- ruleset = Sashite::Ggn.load_file("moves.json", validate: false)
108
- # ✗ No validation (use with pre-validated data)
109
- ```
96
+ ---
110
97
 
111
- ### Validation Levels
98
+ ## API Reference
112
99
 
113
- | Validation Type | Purpose | When Enabled |
114
- |----------------|---------|--------------|
115
- | **JSON Schema** | Ensures GGN format compliance | `validate: true` in load methods |
116
- | **Logical Contradictions** | Detects impossible require/prevent conditions | `validate: true` in Ruleset.new |
117
- | **Implicit Duplications** | Prevents redundant requirements | `validate: true` in Ruleset.new |
100
+ ### Module Functions
118
101
 
119
- ```ruby
120
- # Selective validation for specific use cases
121
- if Sashite::Ggn.valid?(data) # Quick schema check only
122
- ruleset = Sashite::Ggn::Ruleset.new(data, validate: false) # Skip internal validations
123
- end
124
- ```
125
-
126
- ## Understanding GGN Format
102
+ #### `Sashite::Ggn.parse(data) → Ruleset`
127
103
 
128
- A GGN document has this structure:
104
+ Parses GGN data structure into an immutable Ruleset object.
129
105
 
130
- ```json
131
- {
132
- "<piece_identifier>": {
133
- "<source_square>": {
134
- "<destination_square>": [
135
- {
136
- "require": { "<square>": "<required_state>" },
137
- "prevent": { "<square>": "<forbidden_state>" },
138
- "perform": { "<square>": "<new_state_or_null>" }
139
- }
140
- ]
141
- }
142
- }
143
- }
106
+ ```ruby
107
+ ruleset = Sashite::Ggn.parse(ggn_data)
144
108
  ```
145
109
 
146
- ### Core Concepts
147
-
148
- - **Piece Identifier**: Uses GAN format like `"CHESS:P"` or `"shogi:+p"`
149
- - **require**: Conditions that MUST be true (logical AND)
150
- - **prevent**: Conditions that MUST NOT be true (logical OR)
151
- - **perform**: Board changes after the move (REQUIRED)
110
+ **Parameters:**
111
+ - `data` (Hash): GGN data structure conforming to specification
152
112
 
153
- ### Occupation States
113
+ **Returns:** `Ruleset` — Immutable ruleset object
154
114
 
155
- | State | Meaning |
156
- |-------|---------|
157
- | `"empty"` | Square must be empty |
158
- | `"enemy"` | Square must contain an opposing piece |
159
- | `"CHESS:K"` | Square must contain exactly this piece |
115
+ **Raises:** `ArgumentError` If data structure is invalid
160
116
 
161
- ## Complete API Reference
117
+ ---
162
118
 
163
- ### Core Loading Methods
119
+ #### `Sashite::Ggn.valid?(data) Boolean`
164
120
 
165
- #### `Sashite::Ggn.load_file(filepath, validate: true)`
121
+ Validates GGN data structure against specification.
166
122
 
167
- Loads and validates a GGN JSON file.
123
+ ```ruby
124
+ Sashite::Ggn.valid?(ggn_data) # => true
125
+ ```
168
126
 
169
127
  **Parameters:**
170
- - `filepath` [String] - Path to GGN JSON file
171
- - `validate` [Boolean] - Whether to perform all validations (default: true)
128
+ - `data` (Hash): Data structure to validate
172
129
 
173
- **Returns:** Ruleset instance
130
+ **Returns:** `Boolean` — True if valid, false otherwise
174
131
 
175
- **Example:**
176
-
177
- ```ruby
178
- # Load with full validation (recommended)
179
- ruleset = Sashite::Ggn.load_file("moves.json")
132
+ ---
180
133
 
181
- # Load without validation (faster for large files)
182
- ruleset = Sashite::Ggn.load_file("large_moves.json", validate: false)
183
- ```
134
+ ### `Sashite::Ggn::Ruleset` Class
184
135
 
185
- #### `Sashite::Ggn.load_string(json_string, validate: true)`
136
+ Immutable container for GGN movement rules.
186
137
 
187
- Loads GGN data from a JSON string.
138
+ #### `#select(piece) Source`
188
139
 
189
- **Example:**
140
+ Selects movement rules for a specific piece type.
190
141
 
191
142
  ```ruby
192
- json = '{"CHESS:K": {"e1": {"e2": [{"perform": {"e1": null, "e2": "CHESS:K"}}]}}}'
193
- ruleset = Sashite::Ggn.load_string(json)
143
+ source = ruleset.select("C:K")
194
144
  ```
195
145
 
196
- #### `Sashite::Ggn.load_hash(data, validate: true)`
197
-
198
- Creates a ruleset from existing Hash data.
146
+ **Parameters:**
147
+ - `piece` (String): QPI piece identifier
199
148
 
200
- ### Navigation Methods
149
+ **Returns:** `Source` — Source selector object
201
150
 
202
- #### `ruleset.select(piece_identifier)`
151
+ **Raises:** `KeyError` — If piece not found in ruleset
203
152
 
204
- Retrieves movement rules for a specific piece type.
153
+ ---
205
154
 
206
- **Returns:** Source instance
155
+ #### `#pseudo_legal_transitions(feen) → Array<Array>`
207
156
 
208
- **Example:**
157
+ Generates all pseudo-legal moves for the given position.
209
158
 
210
159
  ```ruby
211
- # Get chess king movement rules
212
- king_source = ruleset.select("CHESS:K")
213
-
214
- # Get promoted shogi pawn rules
215
- promoted_pawn = ruleset.select("SHOGI:+P")
160
+ moves = ruleset.pseudo_legal_transitions(feen)
161
+ # => [["C:P", "e2", "e4", [#<Transition...>]], ...]
216
162
  ```
217
163
 
218
- #### `source.from(origin_square)`
164
+ **Parameters:**
165
+ - `feen` (String): Position in FEEN format
219
166
 
220
- Gets possible destinations from a source position.
167
+ **Returns:** `Array<Array>` Array of `[piece, source, destination, transitions]` tuples
221
168
 
222
- **Returns:** Destination instance
169
+ ---
223
170
 
224
- #### `destination.to(target_square)`
171
+ #### `#piece?(piece) → Boolean`
225
172
 
226
- Creates an engine for evaluating a specific move.
173
+ Checks if ruleset contains movement rules for specified piece.
227
174
 
228
- **Returns:** Engine instance
175
+ ```ruby
176
+ ruleset.piece?("C:K") # => true
177
+ ```
229
178
 
230
- #### `engine.where(board_state, active_game)`
179
+ **Parameters:**
180
+ - `piece` (String): QPI piece identifier
231
181
 
232
- Evaluates move validity and returns transitions.
182
+ **Returns:** `Boolean`
233
183
 
234
- **Parameters:**
235
- - `board_state` [Hash] - Current board: `{"square" => "piece_or_nil"}`
236
- - `active_game` [String] - Current player's game identifier (e.g., "CHESS", "shogi")
184
+ ---
237
185
 
238
- **Returns:** Array of Transition objects
186
+ #### `#pieces Array<String>`
239
187
 
240
- **Example:**
188
+ Returns all piece identifiers in ruleset.
241
189
 
242
190
  ```ruby
243
- board = { "e1" => "CHESS:K", "e2" => nil, "f1" => nil }
244
- transitions = engine.where(board, "CHESS")
245
-
246
- transitions.each do |transition|
247
- puts "Move result: #{transition.diff}"
248
- end
191
+ ruleset.pieces # => ["C:K", "C:Q", "C:P", ...]
249
192
  ```
250
193
 
251
- #### `ruleset.pseudo_legal_transitions(board_state, active_game)`
194
+ **Returns:** `Array<String>` — QPI piece identifiers
252
195
 
253
- Generates ALL possible moves for the current position.
196
+ ---
254
197
 
255
- **Returns:** Array of `[actor, origin, target, transitions]`
198
+ #### `#to_h Hash`
256
199
 
257
- **Example:**
200
+ Converts ruleset to hash representation.
258
201
 
259
202
  ```ruby
260
- board = { "e2" => "CHESS:P", "e1" => "CHESS:K" }
261
- all_moves = ruleset.pseudo_legal_transitions(board, "CHESS")
262
-
263
- all_moves.each do |actor, origin, target, transitions|
264
- puts "#{actor}: #{origin} → #{target} (#{transitions.size} variants)"
265
- end
203
+ ruleset.to_h # => { "C:K" => { "e1" => { "e2" => [...] } } }
266
204
  ```
267
205
 
268
- ## Working with Different Move Types
206
+ **Returns:** `Hash` GGN data structure
269
207
 
270
- ### Simple Piece Movement
208
+ ---
271
209
 
272
- ```ruby
273
- # King moves one square in any direction
274
- {
275
- "CHESS:K" => {
276
- "e1" => {
277
- "e2" => [{ "require" => { "e2" => "empty" }, "perform" => { "e1" => nil, "e2" => "CHESS:K" } }],
278
- "f1" => [{ "require" => { "f1" => "empty" }, "perform" => { "e1" => nil, "f1" => "CHESS:K" } }],
279
- "d1" => [{ "require" => { "d1" => "empty" }, "perform" => { "e1" => nil, "d1" => "CHESS:K" } }]
280
- }
281
- }
282
- }
283
- ```
210
+ ### `Sashite::Ggn::Ruleset::Source` Class
284
211
 
285
- ### Capturing Moves
212
+ Represents movement possibilities for a piece type.
286
213
 
287
- ```ruby
288
- # Pawn captures diagonally
289
- {
290
- "CHESS:P" => {
291
- "e4" => {
292
- "f5" => [{
293
- "require" => { "f5" => "enemy" },
294
- "perform" => { "e4" => nil, "f5" => "CHESS:P" }
295
- }]
296
- }
297
- }
298
- }
299
- ```
214
+ #### `#from(source) → Destination`
300
215
 
301
- ### Sliding Pieces
216
+ Specifies the source location for the piece.
302
217
 
303
218
  ```ruby
304
- # Rook moves along empty file
305
- {
306
- "CHESS:R" => {
307
- "a1" => {
308
- "a3" => [{
309
- "require" => { "a2" => "empty", "a3" => "empty" },
310
- "perform" => { "a1" => nil, "a3" => "CHESS:R" }
311
- }]
312
- }
313
- }
314
- }
219
+ destination = source.from("e1")
315
220
  ```
316
221
 
317
- ### Multiple Promotion Choices
222
+ **Parameters:**
223
+ - `source` (String): Source location (CELL coordinate or HAND "*")
318
224
 
319
- ```ruby
320
- # Chess pawn promotion offers 4 choices
321
- {
322
- "CHESS:P" => {
323
- "e7" => {
324
- "e8" => [
325
- { "require" => { "e8" => "empty" }, "perform" => { "e7" => nil, "e8" => "CHESS:Q" } },
326
- { "require" => { "e8" => "empty" }, "perform" => { "e7" => nil, "e8" => "CHESS:R" } },
327
- { "require" => { "e8" => "empty" }, "perform" => { "e7" => nil, "e8" => "CHESS:B" } },
328
- { "require" => { "e8" => "empty" }, "perform" => { "e7" => nil, "e8" => "CHESS:N" } }
329
- ]
330
- }
331
- }
332
- }
225
+ **Returns:** `Destination` — Destination selector object
333
226
 
334
- # Evaluate promotion
335
- board = { "e7" => "CHESS:P", "e8" => nil }
336
- transitions = engine.where(board, "CHESS")
227
+ **Raises:** `KeyError` — If source not found for this piece
337
228
 
338
- puts "#{transitions.size} promotion choices available"
339
- transitions.each_with_index do |t, i|
340
- piece = t.diff["e8"]
341
- puts "Choice #{i + 1}: Promote to #{piece}"
342
- end
343
- ```
229
+ ---
230
+
231
+ #### `#sources → Array<String>`
344
232
 
345
- ### Complex Multi-Square Moves
233
+ Returns all valid source locations for this piece.
346
234
 
347
235
  ```ruby
348
- # Castling involves both king and rook
349
- {
350
- "CHESS:K" => {
351
- "e1" => {
352
- "g1" => [{
353
- "require" => { "f1" => "empty", "g1" => "empty", "h1" => "CHESS:R" },
354
- "perform" => { "e1" => nil, "f1" => "CHESS:R", "g1" => "CHESS:K", "h1" => nil }
355
- }]
356
- }
357
- }
358
- }
236
+ source.sources # => ["e1", "d1", "*"]
237
+ ```
359
238
 
360
- # Evaluate castling
361
- board = { "e1" => "CHESS:K", "f1" => nil, "g1" => nil, "h1" => "CHESS:R" }
362
- transitions = engine.where(board, "CHESS")
239
+ **Returns:** `Array<String>` — Source locations
363
240
 
364
- if transitions.any?
365
- puts "Castling is possible!"
366
- puts "Final position: #{transitions.first.diff}"
367
- end
368
- ```
241
+ ---
242
+
243
+ #### `#source?(location) Boolean`
369
244
 
370
- ### En Passant Capture
245
+ Checks if location is a valid source for this piece.
371
246
 
372
247
  ```ruby
373
- # Pawn captures en passant (removes piece from different square)
374
- {
375
- "CHESS:P" => {
376
- "d5" => {
377
- "e6" => [{
378
- "require" => { "e5" => "chess:p", "e6" => "empty" },
379
- "perform" => { "d5" => nil, "e5" => nil, "e6" => "CHESS:P" }
380
- }]
381
- }
382
- }
383
- }
248
+ source.source?("e1") # => true
384
249
  ```
385
250
 
386
- ### Conditional Moves with Prevention
251
+ **Parameters:**
252
+ - `location` (String): Source location
387
253
 
388
- ```ruby
389
- # Move that's blocked by certain pieces
390
- {
391
- "GAME:B" => {
392
- "c1" => {
393
- "f4" => [{
394
- "require" => { "d2" => "empty", "e3" => "empty" },
395
- "prevent" => { "g5" => "GAME:K", "h6" => "GAME:Q" }, # Blocked if these pieces present
396
- "perform" => { "c1" => nil, "f4" => "GAME:B" }
397
- }]
398
- }
399
- }
400
- }
401
- ```
254
+ **Returns:** `Boolean`
402
255
 
403
- ## Validation and Error Handling
256
+ ---
404
257
 
405
- ### Schema Validation
258
+ ### `Sashite::Ggn::Ruleset::Source::Destination` Class
406
259
 
407
- ```ruby
408
- # Validate GGN data structure
409
- if Sashite::Ggn.valid?(ggn_data)
410
- puts "Valid GGN format"
411
- else
412
- errors = Sashite::Ggn.validation_errors(ggn_data)
413
- puts "Validation errors: #{errors}"
414
- end
260
+ Represents movement possibilities from a specific source.
415
261
 
416
- # Validate and raise exception on failure
417
- begin
418
- Sashite::Ggn.validate!(ggn_data)
419
- puts "Data is valid"
420
- rescue Sashite::Ggn::ValidationError => e
421
- puts "Invalid: #{e.message}"
422
- end
423
- ```
262
+ #### `#to(destination) Engine`
424
263
 
425
- ### Safe Loading for User Input
264
+ Specifies the destination location.
426
265
 
427
266
  ```ruby
428
- def load_user_ggn_file(filepath, environment = :development)
429
- validate = (environment == :development) # Full validation in dev only
430
-
431
- ruleset = Sashite::Ggn.load_file(filepath, validate: validate)
432
- puts "Successfully loaded #{filepath}"
433
- ruleset
434
- rescue Sashite::Ggn::ValidationError => e
435
- puts "Failed to load #{filepath}: #{e.message}"
436
- nil
437
- end
267
+ engine = destination.to("e2")
438
268
  ```
439
269
 
440
- ### Logical Validation
270
+ **Parameters:**
271
+ - `destination` (String): Destination location (CELL coordinate or HAND "*")
441
272
 
442
- The library automatically detects logical inconsistencies when `validate: true`:
273
+ **Returns:** `Engine` Movement evaluation engine
443
274
 
444
- ```ruby
445
- # ❌ This will raise ValidationError - logical contradiction
446
- invalid_data = {
447
- "CHESS:B" => {
448
- "c1" => {
449
- "f4" => [{
450
- "require" => { "d2" => "empty" },
451
- "prevent" => { "d2" => "empty" }, # Contradiction!
452
- "perform" => { "c1" => nil, "f4" => "CHESS:B" }
453
- }]
454
- }
455
- }
456
- }
275
+ **Raises:** `KeyError` — If destination not found from this source
457
276
 
458
- # ❌ This will raise ValidationError - redundant implicit requirement
459
- invalid_data = {
460
- "CHESS:K" => {
461
- "e1" => {
462
- "e2" => [{
463
- "require" => { "e1" => "CHESS:K" }, # Redundant!
464
- "perform" => { "e1" => nil, "e2" => "CHESS:K" }
465
- }]
466
- }
467
- }
468
- }
469
- ```
277
+ ---
470
278
 
471
- ## Working with Different Games
279
+ #### `#destinations Array<String>`
472
280
 
473
- ### Chess Integration
281
+ Returns all valid destinations from this source.
474
282
 
475
283
  ```ruby
476
- # Load chess move rules
477
- chess_rules = Sashite::Ggn.load_file("chess.json")
284
+ destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]
285
+ ```
478
286
 
479
- # Evaluate specific chess position
480
- board = {
481
- "e1" => "CHESS:K", "d1" => "CHESS:Q", "a1" => "CHESS:R", "h1" => "CHESS:R",
482
- "e2" => "CHESS:P", "d2" => "CHESS:P", "f2" => "CHESS:P", "g2" => "CHESS:P"
483
- }
287
+ **Returns:** `Array<String>` Destination locations
484
288
 
485
- all_moves = chess_rules.pseudo_legal_transitions(board, "CHESS")
486
- puts "White has #{all_moves.size} possible moves"
487
- ```
289
+ ---
488
290
 
489
- ### Shōgi Integration
291
+ #### `#destination?(location) → Boolean`
490
292
 
491
- ```ruby
492
- # Load shogi move rules
493
- shogi_rules = Sashite::Ggn.load_file("shogi.json")
293
+ Checks if location is a valid destination from this source.
494
294
 
495
- # Query promoted piece movement
496
- promoted_pawn = shogi_rules.select("SHOGI:+P")
497
- destinations = promoted_pawn.from("5e")
295
+ ```ruby
296
+ destination.destination?("e2") # => true
498
297
  ```
499
298
 
500
- ### Cross-Game Scenarios
299
+ **Parameters:**
300
+ - `location` (String): Destination location
501
301
 
502
- ```ruby
503
- # Hybrid game with pieces from different variants
504
- mixed_data = {
505
- "CHESS:K" => { /* chess king rules */ },
506
- "SHOGI:G" => { /* shogi gold rules */ },
507
- "XIANGQI:E" => { /* xiangqi elephant rules */ }
508
- }
302
+ **Returns:** `Boolean`
509
303
 
510
- ruleset = Sashite::Ggn.load_hash(mixed_data)
304
+ ---
511
305
 
512
- # All uppercase pieces controlled by same player
513
- board = { "e1" => "CHESS:K", "f1" => "SHOGI:G", "g1" => "XIANGQI:E" }
514
- moves = ruleset.pseudo_legal_transitions(board, "MIXED")
515
- ```
306
+ ### `Sashite::Ggn::Ruleset::Source::Destination::Engine` Class
516
307
 
517
- ## Advanced Features
308
+ Evaluates movement possibility under given position conditions.
518
309
 
519
- ### Performance Optimization
310
+ #### `#where(feen) → Array<Transition>`
520
311
 
521
- ```ruby
522
- # Choose validation level based on your needs
523
- def load_ggn_optimized(filepath, trusted_source: false)
524
- if trusted_source
525
- # Maximum performance for pre-validated data
526
- Sashite::Ggn.load_file(filepath, validate: false)
527
- else
528
- # Full validation for safety
529
- Sashite::Ggn.load_file(filepath, validate: true)
530
- end
531
- end
312
+ Evaluates movement against position and returns valid transitions.
532
313
 
533
- # Pre-validate once, then use fast loading
534
- if Sashite::Ggn.valid?(data)
535
- fast_ruleset = Sashite::Ggn.load_hash(data, validate: false)
536
- else
537
- puts "Invalid data detected"
538
- end
314
+ ```ruby
315
+ transitions = engine.where(feen)
539
316
  ```
540
317
 
541
- ### Custom Game Development
318
+ **Parameters:**
319
+ - `feen` (String): Position in FEEN format
542
320
 
543
- ```ruby
544
- # Define movement rules for custom game pieces
545
- custom_ggn = {
546
- "MYGAME:X" => {
547
- "a1" => {
548
- "c3" => [{
549
- "require" => { "b2" => "empty" },
550
- "perform" => { "a1" => nil, "c3" => "MYGAME:X" }
551
- }]
552
- }
553
- }
554
- }
321
+ **Returns:** `Array<Sashite::Stn::Transition>` — Valid state transitions (may be empty)
555
322
 
556
- ruleset = Sashite::Ggn.load_hash(custom_ggn)
557
- ```
323
+ ---
324
+
325
+ #### `#possibilities → Array<Hash>`
558
326
 
559
- ### Database Integration
327
+ Returns raw movement possibility rules.
560
328
 
561
329
  ```ruby
562
- class MoveDatabase
563
- def initialize
564
- @rulesets = {}
565
- end
566
-
567
- def load_game_rules(game_name, filepath, validate: true)
568
- @rulesets[game_name] = Sashite::Ggn.load_file(filepath, validate: validate)
569
- rescue Sashite::Ggn::ValidationError => e
570
- warn "Failed to load #{game_name}: #{e.message}"
571
- end
572
-
573
- def evaluate_position(game_name, board_state, active_player)
574
- ruleset = @rulesets[game_name]
575
- return [] unless ruleset
576
-
577
- ruleset.pseudo_legal_transitions(board_state, active_player)
578
- end
579
- end
330
+ engine.possibilities
331
+ # => [{ "must" => {...}, "deny" => {...}, "diff" => {...} }]
332
+ ```
580
333
 
581
- # Usage
582
- db = MoveDatabase.new
583
- db.load_game_rules("chess", "rules/chess.json", validate: true) # Full validation
584
- db.load_game_rules("shogi", "rules/shogi.json", validate: false) # Fast loading
334
+ **Returns:** `Array<Hash>` — Movement possibility specifications
585
335
 
586
- moves = db.evaluate_position("chess", board_state, "CHESS")
587
- ```
336
+ ---
588
337
 
589
- ## Real-World Examples
338
+ ## GGN Format
590
339
 
591
- ### Game Engine Integration
340
+ ### Structure
592
341
 
593
342
  ```ruby
594
- class GameEngine
595
- def initialize(ruleset)
596
- @ruleset = ruleset
597
- end
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
+ }
355
+ }
356
+ ```
598
357
 
599
- def legal_moves(board_state, active_player)
600
- # Get all pseudo-legal moves from GGN
601
- pseudo_legal = @ruleset.pseudo_legal_transitions(board_state, active_player)
358
+ ### Field Specifications
602
359
 
603
- # Filter for actual legality (check, etc.) - game-specific logic
604
- pseudo_legal.select { |move| actually_legal?(move, board_state) }
605
- end
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 |
606
368
 
607
- def make_move(actor, origin, target, board_state, active_player)
608
- engine = @ruleset.select(actor).from(origin).to(target)
609
- transitions = engine.where(board_state, active_player)
369
+ ---
610
370
 
611
- return nil if transitions.empty?
371
+ ## Usage Examples
612
372
 
613
- # Apply the first valid transition (or let user choose for promotions)
614
- transition = transitions.first
615
- apply_transition(board_state, transition.diff)
616
- end
373
+ ### Method Chaining
617
374
 
618
- private
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"
619
378
 
620
- def apply_transition(board_state, diff)
621
- new_board = board_state.dup
622
- diff.each { |square, piece| new_board[square] = piece }
623
- new_board
624
- end
625
- end
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" }
626
387
  ```
627
388
 
628
- ### Move Validation Service
389
+ ### Generate All Pseudo-Legal Moves
629
390
 
630
391
  ```ruby
631
- class MoveValidator
632
- def initialize(ggn_filepath, validate_ggn: true)
633
- @ruleset = Sashite::Ggn.load_file(ggn_filepath, validate: validate_ggn)
634
- end
635
-
636
- def validate_move(piece, from, to, board, player)
637
- begin
638
- engine = @ruleset.select(piece).from(from).to(to)
639
- transitions = engine.where(board, player)
640
-
641
- {
642
- valid: transitions.any?,
643
- transitions: transitions,
644
- error: nil
645
- }
646
- rescue KeyError
647
- { valid: false, transitions: [], error: "Unknown piece or position" }
648
- rescue => e
649
- { valid: false, transitions: [], error: e.message }
650
- end
651
- end
652
- end
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"
653
393
 
654
- # Usage
655
- validator = MoveValidator.new("chess.json", validate_ggn: true)
656
- result = validator.validate_move("CHESS:P", "e2", "e4", board_state, "CHESS")
394
+ all_moves = ruleset.pseudo_legal_transitions(feen)
657
395
 
658
- if result[:valid]
659
- puts "Move is valid"
660
- puts "#{result[:transitions].size} possible outcomes"
661
- else
662
- puts "Invalid move: #{result[:error]}"
396
+ all_moves.each do |piece, source, destination, transitions|
397
+ puts "#{piece}: #{source} → #{destination} (#{transitions.size} variants)"
663
398
  end
664
399
  ```
665
400
 
666
- ## Best Practices
667
-
668
- ### 1. Choose Validation Level Appropriately
401
+ ### Existence Checks
669
402
 
670
403
  ```ruby
671
- # Development: Always validate for safety
672
- ruleset = Sashite::Ggn.load_file(filepath, validate: true)
673
-
674
- # Production with trusted data: Optimize for performance
675
- ruleset = Sashite::Ggn.load_file(filepath, validate: false)
676
-
677
- # Production with untrusted data: Validate first, then cache
678
- def load_rules_safely(filepath)
679
- # Validate once during deployment
680
- Sashite::Ggn.validate!(JSON.parse(File.read(filepath)))
681
-
682
- # Then use fast loading in runtime
683
- Sashite::Ggn.load_file(filepath, validate: false)
684
- rescue Sashite::Ggn::ValidationError => e
685
- puts "GGN validation failed: #{e.message}"
686
- exit(1)
687
- end
688
- ```
404
+ # Check if piece exists in ruleset
405
+ ruleset.piece?("C:K") # => true
689
406
 
690
- ### 2. Handle Multiple Variants Gracefully
407
+ # Check valid sources
408
+ source = ruleset.select("C:K")
409
+ source.source?("e1") # => true
691
410
 
692
- ```ruby
693
- # Good: Let users choose promotion pieces
694
- def handle_promotion(transitions)
695
- return transitions.first if transitions.size == 1
696
-
697
- puts "Choose promotion:"
698
- transitions.each_with_index do |t, i|
699
- piece = t.diff.values.find { |v| v&.include?(":") }
700
- puts "#{i + 1}. #{piece}"
701
- end
702
-
703
- choice = gets.to_i - 1
704
- transitions[choice] if choice.between?(0, transitions.size - 1)
705
- end
411
+ # Check valid destinations
412
+ destination = source.from("e1")
413
+ destination.destination?("e2") # => true
706
414
  ```
707
415
 
708
- ### 3. Use Consistent Game Identifiers
416
+ ### Introspection
709
417
 
710
418
  ```ruby
711
- # Good: Clear, consistent naming
712
- GAME_IDENTIFIERS = {
713
- chess_white: "CHESS",
714
- chess_black: "chess",
715
- shogi_sente: "SHOGI",
716
- shogi_gote: "shogi"
717
- }.freeze
419
+ # List all pieces
420
+ ruleset.pieces # => ["C:K", "C:Q", "C:R", ...]
421
+
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" => {...} }]
718
431
  ```
719
432
 
720
- ### 4. Error Handling Strategy
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
721
447
 
722
448
  ```ruby
723
- # Good: Comprehensive error handling
449
+ # Handle missing piece
724
450
  begin
725
- ruleset = Sashite::Ggn.load_file(filepath, validate: validate_level)
726
- rescue Sashite::Ggn::ValidationError => e
727
- logger.error "GGN validation failed: #{e.message}"
728
- raise GameLoadError, "Invalid move rules file"
729
- rescue Errno::ENOENT
730
- logger.error "Move rules file not found: #{filepath}"
731
- raise GameLoadError, "Move rules file missing"
451
+ source = ruleset.select("INVALID:X")
452
+ rescue KeyError => e
453
+ puts "Piece not found: #{e.message}"
732
454
  end
733
- ```
734
455
 
735
- ## Compatibility and Performance
456
+ # Handle missing source
457
+ begin
458
+ destination = source.from("z9")
459
+ rescue KeyError => e
460
+ puts "Source not found: #{e.message}"
461
+ end
736
462
 
737
- - **Ruby Version**: >= 3.2.0
738
- - **Thread Safety**: All operations are thread-safe
739
- - **Memory**: Efficient hash-based lookup
740
- - **Performance**: O(1) piece selection, O(n) move generation
741
- - **Validation**: Flexible validation system for different use cases
463
+ # Handle missing destination
464
+ begin
465
+ engine = destination.to("z9")
466
+ rescue KeyError => e
467
+ puts "Destination not found: #{e.message}"
468
+ end
742
469
 
743
- ## Related Sashité Specifications
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
+ ```
744
477
 
745
- GGN works alongside other Sashité notation standards:
478
+ ---
746
479
 
747
- - [GAN v1.0.0](https://sashite.dev/documents/gan/1.0.0/) - General Actor Notation for piece identifiers
748
- - [FEEN v1.0.0](https://sashite.dev/documents/feen/1.0.0/) - Board position representation
749
- - [PNN v1.0.0](https://sashite.dev/documents/pnn/1.0.0/) - Piece notation with state modifiers
750
- - [PMN v1.0.0](https://sashite.dev/documents/pmn/1.0.0/) - Portable move notation for game sequences
480
+ ## Related Specifications
751
481
 
752
- ## Contributing
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
753
489
 
754
- Bug reports and pull requests are welcome on GitHub at https://github.com/sashite/ggn.rb.
490
+ ---
755
491
 
756
492
  ## License
757
493
 
758
- 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
+ ---
759
497
 
760
- ## About Sashité
498
+ ## About
761
499
 
762
- 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.