sashite-ggn 0.5.0 → 0.7.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,130 +1,757 @@
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.
4
-
5
3
  [![Gem Version](https://badge.fury.io/rb/sashite-ggn.svg)](https://badge.fury.io/rb/sashite-ggn)
6
4
  [![Ruby](https://github.com/sashite/ggn.rb/workflows/Ruby/badge.svg)](https://github.com/sashite/ggn.rb/actions)
5
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/ggn.rb/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)
7
+
8
+ > A Ruby library for **GGN** (General Gameplay Notation) - a rule-agnostic format for describing pseudo-legal moves in abstract strategy board games.
7
9
 
8
10
  ## What is GGN?
9
11
 
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/).
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.
11
13
 
12
- GGN focuses on basic movement constraints rather than game-specific legality rules, making it suitable for:
14
+ **Key Features:**
13
15
 
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
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
23
 
19
24
  ## Installation
20
25
 
26
+ Add this line to your application's Gemfile:
27
+
21
28
  ```ruby
22
- gem 'sashite-ggn'
29
+ gem "sashite-ggn"
30
+ ```
31
+
32
+ Or install it directly:
33
+
34
+ ```bash
35
+ gem install sashite-ggn
23
36
  ```
24
37
 
25
- ## Basic Usage
38
+ ## Quick Start
26
39
 
27
- ### Loading GGN Data
40
+ ### Basic Example: Loading Move Rules
28
41
 
29
42
  ```ruby
30
43
  require "sashite-ggn"
31
44
 
32
- # Load from file
45
+ # Load GGN data from file (with full validation by default)
33
46
  ruleset = Sashite::Ggn.load_file("chess_moves.json")
34
47
 
35
- # Load from string
36
- json = '{"CHESS:P": {"e2": {"e4": [{"perform": {"e2": null, "e4": "CHESS:P"}}]}}}'
48
+ # Query specific piece movement rules
49
+ pawn_source = ruleset.select("CHESS:P")
50
+ destinations = pawn_source.from("e2")
51
+ engine = destinations.to("e4")
52
+
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
+ }
59
+
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
70
+ ```
71
+
72
+ ### Basic Example: Loading from JSON String
73
+
74
+ ```ruby
75
+ # Simple pawn double move rule
76
+ ggn_json = {
77
+ "CHESS:P" => {
78
+ "e2" => {
79
+ "e4" => [{
80
+ "require" => { "e3" => "empty", "e4" => "empty" },
81
+ "perform" => { "e2" => nil, "e4" => "CHESS:P" }
82
+ }]
83
+ }
84
+ }
85
+ }
86
+
87
+ ruleset = Sashite::Ggn.load_hash(ggn_json)
88
+ puts "Loaded pawn movement rules!"
89
+ ```
90
+
91
+ ## Validation System
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
102
+ ```
103
+
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
+ ```
110
+
111
+ ### Validation Levels
112
+
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 |
118
+
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
127
+
128
+ A GGN document has this structure:
129
+
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
+ }
144
+ ```
145
+
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)
152
+
153
+ ### Occupation States
154
+
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 |
160
+
161
+ ## Complete API Reference
162
+
163
+ ### Core Loading Methods
164
+
165
+ #### `Sashite::Ggn.load_file(filepath, validate: true)`
166
+
167
+ Loads and validates a GGN JSON file.
168
+
169
+ **Parameters:**
170
+ - `filepath` [String] - Path to GGN JSON file
171
+ - `validate` [Boolean] - Whether to perform all validations (default: true)
172
+
173
+ **Returns:** Ruleset instance
174
+
175
+ **Example:**
176
+
177
+ ```ruby
178
+ # Load with full validation (recommended)
179
+ ruleset = Sashite::Ggn.load_file("moves.json")
180
+
181
+ # Load without validation (faster for large files)
182
+ ruleset = Sashite::Ggn.load_file("large_moves.json", validate: false)
183
+ ```
184
+
185
+ #### `Sashite::Ggn.load_string(json_string, validate: true)`
186
+
187
+ Loads GGN data from a JSON string.
188
+
189
+ **Example:**
190
+
191
+ ```ruby
192
+ json = '{"CHESS:K": {"e1": {"e2": [{"perform": {"e1": null, "e2": "CHESS:K"}}]}}}'
37
193
  ruleset = Sashite::Ggn.load_string(json)
38
194
  ```
39
195
 
40
- ### Evaluating Moves
196
+ #### `Sashite::Ggn.load_hash(data, validate: true)`
197
+
198
+ Creates a ruleset from existing Hash data.
199
+
200
+ ### Navigation Methods
201
+
202
+ #### `ruleset.select(piece_identifier)`
203
+
204
+ Retrieves movement rules for a specific piece type.
205
+
206
+ **Returns:** Source instance
207
+
208
+ **Example:**
41
209
 
42
210
  ```ruby
43
- # Query specific move
44
- engine = ruleset.select("CHESS:P").from("e2").to("e4")
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")
216
+ ```
217
+
218
+ #### `source.from(origin_square)`
45
219
 
46
- # Define board state
47
- board_state = { "e2" => "CHESS:P", "e3" => nil, "e4" => nil }
220
+ Gets possible destinations from a source position.
221
+
222
+ **Returns:** Destination instance
223
+
224
+ #### `destination.to(target_square)`
225
+
226
+ Creates an engine for evaluating a specific move.
227
+
228
+ **Returns:** Engine instance
229
+
230
+ #### `engine.where(board_state, active_game)`
231
+
232
+ Evaluates move validity and returns transitions.
233
+
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")
237
+
238
+ **Returns:** Array of Transition objects
239
+
240
+ **Example:**
241
+
242
+ ```ruby
243
+ board = { "e1" => "CHESS:K", "e2" => nil, "f1" => nil }
244
+ transitions = engine.where(board, "CHESS")
48
245
 
49
- # Evaluate move
50
- transitions = engine.where(board_state, {}, "CHESS")
51
- # => [#<Transition diff={"e2"=>nil, "e4"=>"CHESS:P"}>]
246
+ transitions.each do |transition|
247
+ puts "Move result: #{transition.diff}"
248
+ end
52
249
  ```
53
250
 
54
- ### Generating All Moves
251
+ #### `ruleset.pseudo_legal_transitions(board_state, active_game)`
252
+
253
+ Generates ALL possible moves for the current position.
254
+
255
+ **Returns:** Array of `[actor, origin, target, transitions]`
256
+
257
+ **Example:**
55
258
 
56
259
  ```ruby
57
- # Get all pseudo-legal moves for current position
58
- all_moves = ruleset.pseudo_legal_transitions(board_state, captures, "CHESS")
260
+ board = { "e2" => "CHESS:P", "e1" => "CHESS:K" }
261
+ all_moves = ruleset.pseudo_legal_transitions(board, "CHESS")
59
262
 
60
- # Each move is represented as [actor, origin, target, transitions]
61
263
  all_moves.each do |actor, origin, target, transitions|
62
264
  puts "#{actor}: #{origin} → #{target} (#{transitions.size} variants)"
63
265
  end
64
266
  ```
65
267
 
66
- ## Move Variants
268
+ ## Working with Different Move Types
67
269
 
68
- GGN supports multiple outcomes for a single move (e.g., promotion choices):
270
+ ### Simple Piece Movement
69
271
 
70
272
  ```ruby
71
- # Chess pawn promotion
72
- engine = ruleset.select("CHESS:P").from("e7").to("e8")
73
- transitions = engine.where({"e7" => "CHESS:P", "e8" => nil}, {}, "CHESS")
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
+ ```
74
284
 
75
- transitions.each do |transition|
76
- promoted_piece = transition.diff["e8"]
77
- puts "Promote to #{promoted_piece}"
285
+ ### Capturing Moves
286
+
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
+ ```
300
+
301
+ ### Sliding Pieces
302
+
303
+ ```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
+ }
315
+ ```
316
+
317
+ ### Multiple Promotion Choices
318
+
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
+ }
333
+
334
+ # Evaluate promotion
335
+ board = { "e7" => "CHESS:P", "e8" => nil }
336
+ transitions = engine.where(board, "CHESS")
337
+
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
+ ```
344
+
345
+ ### Complex Multi-Square Moves
346
+
347
+ ```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
+ }
359
+
360
+ # Evaluate castling
361
+ board = { "e1" => "CHESS:K", "f1" => nil, "g1" => nil, "h1" => "CHESS:R" }
362
+ transitions = engine.where(board, "CHESS")
363
+
364
+ if transitions.any?
365
+ puts "Castling is possible!"
366
+ puts "Final position: #{transitions.first.diff}"
78
367
  end
79
- # Output: CHESS:Q, CHESS:R, CHESS:B, CHESS:N
80
368
  ```
81
369
 
82
- ## Piece Drops
370
+ ### En Passant Capture
83
371
 
84
- For games supporting piece drops (e.g., Shogi):
372
+ ```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
+ }
384
+ ```
385
+
386
+ ### Conditional Moves with Prevention
387
+
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
+ ```
402
+
403
+ ## Validation and Error Handling
404
+
405
+ ### Schema Validation
406
+
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
415
+
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
+ ```
424
+
425
+ ### Safe Loading for User Input
426
+
427
+ ```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
438
+ ```
439
+
440
+ ### Logical Validation
441
+
442
+ The library automatically detects logical inconsistencies when `validate: true`:
443
+
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
+ }
457
+
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
+ ```
470
+
471
+ ## Working with Different Games
472
+
473
+ ### Chess Integration
474
+
475
+ ```ruby
476
+ # Load chess move rules
477
+ chess_rules = Sashite::Ggn.load_file("chess.json")
478
+
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
+ }
484
+
485
+ all_moves = chess_rules.pseudo_legal_transitions(board, "CHESS")
486
+ puts "White has #{all_moves.size} possible moves"
487
+ ```
488
+
489
+ ### Shōgi Integration
490
+
491
+ ```ruby
492
+ # Load shogi move rules
493
+ shogi_rules = Sashite::Ggn.load_file("shogi.json")
494
+
495
+ # Query promoted piece movement
496
+ promoted_pawn = shogi_rules.select("SHOGI:+P")
497
+ destinations = promoted_pawn.from("5e")
498
+ ```
499
+
500
+ ### Cross-Game Scenarios
501
+
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
+ }
509
+
510
+ ruleset = Sashite::Ggn.load_hash(mixed_data)
511
+
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
+ ```
516
+
517
+ ## Advanced Features
518
+
519
+ ### Performance Optimization
520
+
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
532
+
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
539
+ ```
540
+
541
+ ### Custom Game Development
542
+
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
+ }
555
+
556
+ ruleset = Sashite::Ggn.load_hash(custom_ggn)
557
+ ```
558
+
559
+ ### Database Integration
85
560
 
86
561
  ```ruby
87
- # Drop from hand (origin "*")
88
- engine = ruleset.select("SHOGI:P").from("*").to("5e")
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
89
580
 
90
- captures = { "SHOGI:P" => 1 } # One pawn in hand
91
- board_state = { "5e" => nil } # Empty target square
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
92
585
 
93
- transitions = engine.where(board_state, captures, "SHOGI")
94
- # => [#<Transition diff={"5e"=>"SHOGI:P"} drop="SHOGI:P">]
586
+ moves = db.evaluate_position("chess", board_state, "CHESS")
95
587
  ```
96
588
 
97
- ## Validation
589
+ ## Real-World Examples
590
+
591
+ ### Game Engine Integration
98
592
 
99
593
  ```ruby
100
- # Validate GGN data
101
- Sashite::Ggn.validate!(data) # Raises exception on failure
102
- Sashite::Ggn.valid?(data) # Returns boolean
594
+ class GameEngine
595
+ def initialize(ruleset)
596
+ @ruleset = ruleset
597
+ end
598
+
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)
602
+
603
+ # Filter for actual legality (check, etc.) - game-specific logic
604
+ pseudo_legal.select { |move| actually_legal?(move, board_state) }
605
+ end
606
+
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)
610
+
611
+ return nil if transitions.empty?
612
+
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
617
+
618
+ private
619
+
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
103
626
  ```
104
627
 
105
- ## API Reference
628
+ ### Move Validation Service
629
+
630
+ ```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
653
+
654
+ # Usage
655
+ validator = MoveValidator.new("chess.json", validate_ggn: true)
656
+ result = validator.validate_move("CHESS:P", "e2", "e4", board_state, "CHESS")
657
+
658
+ if result[:valid]
659
+ puts "Move is valid"
660
+ puts "#{result[:transitions].size} possible outcomes"
661
+ else
662
+ puts "Invalid move: #{result[:error]}"
663
+ end
664
+ ```
665
+
666
+ ## Best Practices
667
+
668
+ ### 1. Choose Validation Level Appropriately
669
+
670
+ ```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
+ ```
689
+
690
+ ### 2. Handle Multiple Variants Gracefully
691
+
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
706
+ ```
707
+
708
+ ### 3. Use Consistent Game Identifiers
709
+
710
+ ```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
718
+ ```
719
+
720
+ ### 4. Error Handling Strategy
721
+
722
+ ```ruby
723
+ # Good: Comprehensive error handling
724
+ 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"
732
+ end
733
+ ```
106
734
 
107
- ### Core Classes
735
+ ## Compatibility and Performance
108
736
 
109
- - `Sashite::Ggn::Ruleset` - Main entry point for querying moves
110
- - `Sashite::Ggn::Ruleset::Source` - Piece type with source positions
111
- - `Sashite::Ggn::Ruleset::Source::Destination` - Available destinations
112
- - `Sashite::Ggn::Ruleset::Source::Destination::Engine` - Move evaluator
113
- - `Sashite::Ggn::Ruleset::Source::Destination::Engine::Transition` - Move result
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
114
742
 
115
- ### Key Methods
743
+ ## Related Sashité Specifications
116
744
 
117
- - `#pseudo_legal_transitions(board_state, captures, turn)` - Generate all moves
118
- - `#select(actor).from(origin).to(target)` - Query specific move
119
- - `#where(board_state, captures, turn)` - Evaluate move validity
745
+ GGN works alongside other Sashité notation standards:
120
746
 
121
- ## Related Specifications
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
122
751
 
123
- GGN works alongside other Sashité specifications:
752
+ ## Contributing
124
753
 
125
- - [GAN](https://sashite.dev/documents/gan/1.0.0/) - General Actor Notation for piece identifiers
126
- - [FEEN](https://sashite.dev/documents/feen/1.0.0/) - Board position representation
127
- - [PMN](https://sashite.dev/documents/pmn/1.0.0/) - Move sequence representation
754
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sashite/ggn.rb.
128
755
 
129
756
  ## License
130
757