sashite-ggn 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dab117559875291772b7e8f60077f670928755870b39badd3555f4d66fd8357
4
- data.tar.gz: 2fe3fd39dbb288dc140b10c4c9fec523d2ce00a7d33d7802d8612b95e2268ed3
3
+ metadata.gz: f2933dadd55b72474ca9ab12a6012cdd5dfa7acc6cf82d2f38ed5968c2ee3778
4
+ data.tar.gz: 49b9c26e3089a2ea0ab46a78129b9d680a9efbfd781437e147d4e7976ff4b2bd
5
5
  SHA512:
6
- metadata.gz: 805c62503574bbc3b985cc21cb4dfe7a15e03198d97a5e7f0d91508111fd7290ed7382f93f2db6d653f07593f06a65c2640ac4e027127e0bdd964cdc61edab5c
7
- data.tar.gz: fc0330900649313e22511f44d391d5a73befdabe4038aa4744de3f8af12684767c410bd6f8aa7e4a88f961cecc6d7be1223e00cfd9736396d92d7c781fa5abea
6
+ metadata.gz: b665fff9818a2f41dd5623190c69b2904e20bf053730a5f7ca5c48cf51d81a42c62813cd34500d3e13bfaee8bcc41196d8456feaa319bd22d60324e61d04cf5a
7
+ data.tar.gz: ed68e3ff5e50d0e32908b3d78a96448f5258b071948d3644514dab995f2e3c461367e3bcab65e748c57e6c23f9bebe27f36c112215706991549feedd941e9f39
data/README.md CHANGED
@@ -1,147 +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
7
 
8
- ## What is GGN?
8
+ > A Ruby library for **GGN** (General Gameplay Notation) - a rule-agnostic format for describing pseudo-legal moves in abstract strategy board games.
9
9
 
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/).
10
+ ## What is GGN?
11
11
 
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:
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.
13
13
 
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
14
+ **Key Features:**
18
15
 
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.
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
20
23
 
21
24
  ## Installation
22
25
 
26
+ Add this line to your application's Gemfile:
27
+
23
28
  ```ruby
24
- gem 'sashite-ggn'
29
+ gem "sashite-ggn"
30
+ ```
31
+
32
+ Or install it directly:
33
+
34
+ ```bash
35
+ gem install sashite-ggn
25
36
  ```
26
37
 
27
- ## Basic Usage
38
+ ## Quick Start
28
39
 
29
- ### Loading GGN Data
40
+ ### Basic Example: Loading Move Rules
30
41
 
31
42
  ```ruby
32
43
  require "sashite-ggn"
33
44
 
34
- # Load from file
45
+ # Load GGN data from file (with full validation by default)
35
46
  ruleset = Sashite::Ggn.load_file("chess_moves.json")
36
47
 
37
- # Load from string
38
- 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"}}]}}}'
39
193
  ruleset = Sashite::Ggn.load_string(json)
40
194
  ```
41
195
 
42
- ### 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:**
43
209
 
44
210
  ```ruby
45
- # Query specific move
46
- 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
+ ```
47
217
 
48
- # Define board state
49
- board_state = { "e2" => "CHESS:P", "e3" => nil, "e4" => nil }
218
+ #### `source.from(origin_square)`
50
219
 
51
- # Evaluate move
52
- transitions = engine.where(board_state, "CHESS")
53
- # => [#<Transition diff={"e2"=>nil, "e4"=>"CHESS:P"}>]
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")
245
+
246
+ transitions.each do |transition|
247
+ puts "Move result: #{transition.diff}"
248
+ end
54
249
  ```
55
250
 
56
- ### 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:**
57
258
 
58
259
  ```ruby
59
- # Get all pseudo-legal moves for current position
60
- all_moves = ruleset.pseudo_legal_transitions(board_state, "CHESS")
260
+ board = { "e2" => "CHESS:P", "e1" => "CHESS:K" }
261
+ all_moves = ruleset.pseudo_legal_transitions(board, "CHESS")
61
262
 
62
- # Each move is represented as [actor, origin, target, transitions]
63
263
  all_moves.each do |actor, origin, target, transitions|
64
264
  puts "#{actor}: #{origin} → #{target} (#{transitions.size} variants)"
65
265
  end
66
266
  ```
67
267
 
68
- ## Move Variants
268
+ ## Working with Different Move Types
69
269
 
70
- GGN supports multiple outcomes for a single move (e.g., promotion choices):
270
+ ### Simple Piece Movement
71
271
 
72
272
  ```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")
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
+ ```
76
284
 
77
- transitions.each do |transition|
78
- promoted_piece = transition.diff["e8"]
79
- 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}"
80
367
  end
81
- # Output: CHESS:Q, CHESS:R, CHESS:B, CHESS:N
82
368
  ```
83
369
 
84
- ## Complex Moves
370
+ ### En Passant Capture
85
371
 
86
- GGN can represent multi-square moves like castling:
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
87
387
 
88
388
  ```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"
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
+ }
93
400
  }
401
+ ```
94
402
 
95
- transitions = engine.where(board_state, "CHESS")
96
- # => [#<Transition diff={"e1"=>nil, "f1"=>"CHESS:R", "g1"=>"CHESS:K", "h1"=>nil}>]
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
97
423
  ```
98
424
 
99
- ## Conditional Moves
425
+ ### Safe Loading for User Input
100
426
 
101
- GGN supports moves with complex requirements and prevent conditions:
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`:
102
443
 
103
444
  ```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
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
+ }
108
456
  }
109
457
 
110
- transitions = engine.where(board_state, "CHESS")
111
- # => [#<Transition diff={"d5"=>nil, "e5"=>nil, "e6"=>"CHESS:P"}>]
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
560
+
561
+ ```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
580
+
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
585
+
586
+ moves = db.evaluate_position("chess", board_state, "CHESS")
587
+ ```
588
+
589
+ ## Real-World Examples
590
+
591
+ ### Game Engine Integration
592
+
593
+ ```ruby
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
626
+ ```
627
+
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
112
688
  ```
113
689
 
114
- ## Validation
690
+ ### 2. Handle Multiple Variants Gracefully
115
691
 
116
692
  ```ruby
117
- # Validate GGN data
118
- Sashite::Ggn.validate!(data) # Raises exception on failure
119
- Sashite::Ggn.valid?(data) # Returns boolean
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
120
706
  ```
121
707
 
122
- ## API Reference
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
+ ```
123
734
 
124
- ### Core Classes
735
+ ## Compatibility and Performance
125
736
 
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
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
131
742
 
132
- ### Key Methods
743
+ ## Related Sashité Specifications
133
744
 
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
745
+ GGN works alongside other Sashité notation standards:
137
746
 
138
- ## 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
139
751
 
140
- GGN works alongside other Sashité specifications:
752
+ ## Contributing
141
753
 
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
754
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sashite/ggn.rb.
145
755
 
146
756
  ## License
147
757
 
@@ -21,6 +21,14 @@ module Sashite
21
21
  # GGN focuses exclusively on board-to-board transformations. All moves
22
22
  # represent pieces moving, capturing, or transforming on the game board.
23
23
  #
24
+ # = Validation Behavior
25
+ #
26
+ # When `validate: true` (default), performs:
27
+ # - Logical contradiction detection in require/prevent conditions
28
+ # - Implicit requirement duplication detection
29
+ #
30
+ # When `validate: false`, skips all internal validations for maximum performance.
31
+ #
24
32
  # @example Basic usage
25
33
  # piece_data = Sashite::Ggn.load_file('chess.json')
26
34
  # chess_king = piece_data.select('CHESS:K')
@@ -51,17 +59,31 @@ module Sashite
51
59
  #
52
60
  # @param data [Hash] The parsed GGN JSON data structure, where keys are
53
61
  # GAN identifiers and values contain the movement definitions.
62
+ # @param validate [Boolean] Whether to perform internal validations (default: true).
63
+ # When false, skips logical contradiction and implicit requirement checks
64
+ # for maximum performance.
54
65
  #
55
66
  # @raise [ArgumentError] If data is not a Hash
67
+ # @raise [ValidationError] If validation is enabled and logical issues are found
56
68
  #
57
- # @example Creating from parsed JSON data
69
+ # @example Creating from parsed JSON data with full validation
58
70
  # ggn_data = JSON.parse(File.read('chess.json'))
59
- # ruleset = Ruleset.new(ggn_data)
60
- def initialize(data)
71
+ # ruleset = Ruleset.new(ggn_data) # validate: true by default
72
+ #
73
+ # @example Creating without validation for performance
74
+ # ggn_data = JSON.parse(File.read('large_dataset.json'))
75
+ # ruleset = Ruleset.new(ggn_data, validate: false)
76
+ def initialize(data, validate: true)
61
77
  raise ::ArgumentError, "Expected Hash, got #{data.class}" unless data.is_a?(::Hash)
62
78
 
63
79
  @data = data
64
80
 
81
+ if validate
82
+ # Perform enhanced validations for logical consistency
83
+ validate_no_implicit_requirement_duplications!
84
+ validate_no_logical_contradictions!
85
+ end
86
+
65
87
  freeze
66
88
  end
67
89
 
@@ -92,7 +114,7 @@ module Sashite
92
114
  # GAME:piece (e.g., CHESS:K, shogi:p, XIANGQI:E)
93
115
  def select(actor)
94
116
  data = @data.fetch(actor)
95
- Source.new(data, actor:)
117
+ Source.new(data, actor: actor)
96
118
  end
97
119
 
98
120
  # Returns all pseudo-legal move transitions for the given position.
@@ -283,26 +305,162 @@ module Sashite
283
305
  raise ::ArgumentError, "Invalid active_game format: #{active_game.inspect}. Must be a valid game identifier (alphabetic characters only, e.g., 'CHESS', 'shogi')."
284
306
  end
285
307
 
286
- # Validate board_state structure (optional deep validation)
287
- validate_board_state_structure!(board_state) if ENV['GGN_STRICT_VALIDATION']
308
+ # Validate board_state structure
309
+ validate_board_state_structure!(board_state)
288
310
  end
289
311
 
290
- # Validates board_state structure in strict mode.
312
+ # Validates board_state structure.
291
313
  #
292
- # This optional validation can be enabled via environment variable
293
- # to catch malformed board states during development and testing.
314
+ # Ensures all square labels are valid strings and all pieces are either nil
315
+ # or valid strings. This validation helps catch common integration errors
316
+ # where malformed board states are passed to the GGN engine.
294
317
  #
295
318
  # @param board_state [Hash] Board state to validate
296
319
  #
297
320
  # @raise [ArgumentError] If board_state contains invalid data
321
+ #
322
+ # @example Valid board state
323
+ # { "e1" => "CHESS:K", "e2" => nil, "d1" => "CHESS:Q" }
324
+ #
325
+ # @example Invalid board states (would raise ArgumentError)
326
+ # { 123 => "CHESS:K" } # Invalid square label
327
+ # { "e1" => "" } # Empty piece string
328
+ # { "e1" => 456 } # Non-string piece
298
329
  def validate_board_state_structure!(board_state)
299
330
  board_state.each do |square, piece|
300
331
  unless square.is_a?(::String) && !square.empty?
301
- raise ::ArgumentError, "Invalid square label: #{square.inspect}"
332
+ raise ::ArgumentError, "Invalid square label: #{square.inspect}. Must be a non-empty String."
302
333
  end
303
334
 
304
335
  if piece && (!piece.is_a?(::String) || piece.empty?)
305
- raise ::ArgumentError, "Invalid piece at #{square}: #{piece.inspect}"
336
+ raise ::ArgumentError, "Invalid piece at #{square}: #{piece.inspect}. Must be a String or nil."
337
+ end
338
+ end
339
+ end
340
+
341
+ # Validates that transitions don't duplicate implicit requirements in the require field.
342
+ #
343
+ # According to GGN specification, implicit requirements (like the source piece
344
+ # being present at the source square) should NOT be explicitly specified in
345
+ # the require field, as this creates redundancy and potential inconsistency.
346
+ #
347
+ # @raise [ValidationError] If any transition duplicates implicit requirements
348
+ #
349
+ # @example Invalid GGN that would be rejected
350
+ # {
351
+ # "CHESS:K": {
352
+ # "e1": {
353
+ # "e2": [{
354
+ # "require": { "e1": "CHESS:K" }, # ❌ Redundant implicit requirement
355
+ # "perform": { "e1": null, "e2": "CHESS:K" }
356
+ # }]
357
+ # }
358
+ # }
359
+ # }
360
+ def validate_no_implicit_requirement_duplications!
361
+ @data.each do |actor, source_data|
362
+ source_data.each do |origin, destination_data|
363
+ destination_data.each do |target, transition_list|
364
+ transition_list.each_with_index do |transition, index|
365
+ validate_single_transition_implicit_requirements!(
366
+ transition, actor, origin, target, index
367
+ )
368
+ end
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+ # Validates a single transition for implicit requirement duplication.
375
+ #
376
+ # @param transition [Hash] The transition rule to validate
377
+ # @param actor [String] GAN identifier of the piece
378
+ # @param origin [String] Source square
379
+ # @param target [String] Destination square
380
+ # @param index [Integer] Index of transition for error reporting
381
+ #
382
+ # @raise [ValidationError] If implicit requirements are duplicated
383
+ def validate_single_transition_implicit_requirements!(transition, actor, origin, target, index)
384
+ return unless transition.is_a?(::Hash) && transition["require"].is_a?(::Hash)
385
+
386
+ require_conditions = transition["require"]
387
+
388
+ # Check if the source square requirement is explicitly specified
389
+ if require_conditions.key?(origin) && require_conditions[origin] == actor
390
+ raise ValidationError,
391
+ "Implicit requirement duplication detected in #{actor} from #{origin} to #{target} " \
392
+ "(transition #{index}): 'require' field explicitly specifies that #{origin} contains #{actor}, " \
393
+ "but this is already implicit from the move structure. Remove this redundant requirement."
394
+ end
395
+ end
396
+
397
+ # Validates that transitions don't contain logical contradictions between require and prevent.
398
+ #
399
+ # A logical contradiction occurs when the same square is required to be in
400
+ # the same state in both require and prevent fields. This creates an impossible
401
+ # condition that can never be satisfied.
402
+ #
403
+ # @raise [ValidationError] If any transition contains logical contradictions
404
+ #
405
+ # @example Invalid GGN that would be rejected
406
+ # {
407
+ # "CHESS:B": {
408
+ # "c1": {
409
+ # "f4": [{
410
+ # "require": { "d2": "empty" },
411
+ # "prevent": { "d2": "empty" }, # ❌ Logical contradiction
412
+ # "perform": { "c1": null, "f4": "CHESS:B" }
413
+ # }]
414
+ # }
415
+ # }
416
+ # }
417
+ def validate_no_logical_contradictions!
418
+ @data.each do |actor, source_data|
419
+ source_data.each do |origin, destination_data|
420
+ destination_data.each do |target, transition_list|
421
+ transition_list.each_with_index do |transition, index|
422
+ validate_single_transition_logical_consistency!(
423
+ transition, actor, origin, target, index
424
+ )
425
+ end
426
+ end
427
+ end
428
+ end
429
+ end
430
+
431
+ # Validates a single transition for logical contradictions.
432
+ #
433
+ # @param transition [Hash] The transition rule to validate
434
+ # @param actor [String] GAN identifier of the piece
435
+ # @param origin [String] Source square
436
+ # @param target [String] Destination square
437
+ # @param index [Integer] Index of transition for error reporting
438
+ #
439
+ # @raise [ValidationError] If logical contradictions are found
440
+ def validate_single_transition_logical_consistency!(transition, actor, origin, target, index)
441
+ return unless transition.is_a?(::Hash)
442
+
443
+ require_conditions = transition["require"]
444
+ prevent_conditions = transition["prevent"]
445
+
446
+ # Skip if either field is missing or not a hash
447
+ return unless require_conditions.is_a?(::Hash) && prevent_conditions.is_a?(::Hash)
448
+
449
+ # Find squares that appear in both require and prevent
450
+ conflicting_squares = require_conditions.keys & prevent_conditions.keys
451
+
452
+ # Check each conflicting square for state contradictions
453
+ conflicting_squares.each do |square|
454
+ required_state = require_conditions[square]
455
+ prevented_state = prevent_conditions[square]
456
+
457
+ # Logical contradiction: same state required and prevented
458
+ if required_state == prevented_state
459
+ raise ValidationError,
460
+ "Logical contradiction detected in #{actor} from #{origin} to #{target} " \
461
+ "(transition #{index}): square #{square} cannot simultaneously " \
462
+ "require state '#{required_state}' and prevent state '#{prevented_state}'. " \
463
+ "This creates an impossible condition that can never be satisfied."
306
464
  end
307
465
  end
308
466
  end
data/lib/sashite/ggn.rb CHANGED
@@ -23,10 +23,19 @@ module Sashite
23
23
  # - **Board-focused**: Describes only board transformations, no hand management
24
24
  # - **Pseudo-legal** focus: Describes basic movement constraints only
25
25
  # - **JSON-based**: Structured, machine-readable format
26
- # - **Validation** support: Built-in schema validation
26
+ # - **Validation** support: Built-in schema validation and logical consistency checks
27
27
  # - **Performance** optimized: Optional validation for large datasets
28
28
  # - **Cross-game** compatible: Supports hybrid games and variants
29
29
  #
30
+ # = Validation Levels
31
+ #
32
+ # When `validate: true` (default), performs:
33
+ # - JSON Schema validation against GGN specification
34
+ # - Logical contradiction detection in require/prevent conditions
35
+ # - Implicit requirement duplication detection
36
+ #
37
+ # When `validate: false`, skips all validations for maximum performance.
38
+ #
30
39
  # = Related Specifications
31
40
  #
32
41
  # GGN works alongside other Sashité specifications:
@@ -46,16 +55,18 @@ module Sashite
46
55
  # 1. Reads the JSON file from the filesystem with proper encoding
47
56
  # 2. Parses the JSON content into a Ruby Hash with error handling
48
57
  # 3. Optionally validates the structure against the GGN JSON Schema
49
- # 4. Creates and returns a Ruleset instance for querying moves
58
+ # 4. Optionally performs logical consistency validation
59
+ # 5. Creates and returns a Ruleset instance for querying moves
50
60
  #
51
61
  # @param filepath [String, Pathname] Path to the GGN JSON file to load.
52
62
  # Supports both relative and absolute paths.
53
- # @param validate [Boolean] Whether to validate against GGN schema (default: true).
54
- # Set to false to skip validation for improved performance on large documents.
63
+ # @param validate [Boolean] Whether to perform all validations (default: true).
64
+ # When false, skips JSON schema validation AND internal logical validations
65
+ # for maximum performance.
55
66
  # @param encoding [String] File encoding to use when reading (default: 'UTF-8').
56
67
  # Most GGN files should use UTF-8 encoding.
57
68
  #
58
- # @return [Ruleset] A Ruleset instance containing the parsed and validated GGN data.
69
+ # @return [Ruleset] A Ruleset instance containing the parsed GGN data.
59
70
  # Use this instance to query pseudo-legal moves for specific pieces and positions.
60
71
  #
61
72
  # @raise [ValidationError] If any of the following conditions occur:
@@ -63,6 +74,7 @@ module Sashite
63
74
  # - File contains invalid JSON syntax
64
75
  # - File permissions prevent reading
65
76
  # - When validation is enabled: data doesn't conform to GGN schema
77
+ # - When validation is enabled: logical contradictions or implicit duplications found
66
78
  #
67
79
  # @example Loading a chess piece definition with full validation
68
80
  # begin
@@ -89,7 +101,7 @@ module Sashite
89
101
  #
90
102
  # @example Loading large datasets without validation for performance
91
103
  # begin
92
- # # Skip validation for large files to improve loading performance
104
+ # # Skip all validations for large files to improve loading performance
93
105
  # large_dataset = Sashite::Ggn.load_file('data/all_variants.json', validate: false)
94
106
  # puts "Loaded GGN data without validation"
95
107
  # rescue Sashite::Ggn::ValidationError => e
@@ -123,8 +135,8 @@ module Sashite
123
135
  # Validate against GGN schema if requested
124
136
  validate_schema(data, file_path) if validate
125
137
 
126
- # Create and return Ruleset instance
127
- Ruleset.new(data)
138
+ # Create and return Ruleset instance with validation option
139
+ Ruleset.new(data, validate: validate)
128
140
  end
129
141
 
130
142
  # Loads GGN data directly from a JSON string.
@@ -133,7 +145,8 @@ module Sashite
133
145
  # database, API response, or embedded in your application) rather than a file.
134
146
  #
135
147
  # @param json_string [String] JSON string containing GGN data
136
- # @param validate [Boolean] Whether to validate against GGN schema (default: true)
148
+ # @param validate [Boolean] Whether to perform all validations (default: true).
149
+ # When false, skips JSON schema validation AND internal logical validations.
137
150
  #
138
151
  # @return [Ruleset] A Ruleset instance containing the parsed GGN data
139
152
  #
@@ -164,8 +177,8 @@ module Sashite
164
177
  # Validate against GGN schema if requested
165
178
  validate_schema(data, "<string>") if validate
166
179
 
167
- # Create and return Ruleset instance
168
- Ruleset.new(data)
180
+ # Create and return Ruleset instance with validation option
181
+ Ruleset.new(data, validate: validate)
169
182
  end
170
183
 
171
184
  # Loads GGN data from a Ruby Hash.
@@ -174,7 +187,8 @@ module Sashite
174
187
  # and want to create a GGN Ruleset instance with optional validation.
175
188
  #
176
189
  # @param data [Hash] Ruby Hash containing GGN data structure
177
- # @param validate [Boolean] Whether to validate against GGN schema (default: true)
190
+ # @param validate [Boolean] Whether to perform all validations (default: true).
191
+ # When false, skips JSON schema validation AND internal logical validations.
178
192
  #
179
193
  # @return [Ruleset] A Ruleset instance containing the GGN data
180
194
  #
@@ -200,14 +214,16 @@ module Sashite
200
214
  # Validate against GGN schema if requested
201
215
  validate_schema(data, "<hash>") if validate
202
216
 
203
- # Create and return Ruleset instance
204
- Ruleset.new(data)
217
+ # Create and return Ruleset instance with validation option
218
+ Ruleset.new(data, validate: validate)
205
219
  end
206
220
 
207
221
  # Validates a data structure against the GGN JSON Schema.
208
222
  #
209
223
  # This method can be used independently to validate GGN data without
210
224
  # creating a Ruleset instance. Useful for pre-validation or testing.
225
+ # Note: This only performs JSON Schema validation, not the internal
226
+ # logical consistency checks that Ruleset.new performs.
211
227
  #
212
228
  # @param data [Hash] The data structure to validate
213
229
  # @param context [String] Context information for error messages (default: "<data>")
@@ -230,6 +246,9 @@ module Sashite
230
246
 
231
247
  # Checks if a data structure is valid GGN format.
232
248
  #
249
+ # Note: This only performs JSON Schema validation, not the internal
250
+ # logical consistency checks that Ruleset.new performs.
251
+ #
233
252
  # @param data [Hash] The data structure to validate
234
253
  #
235
254
  # @return [Boolean] true if valid, false otherwise
@@ -247,6 +266,9 @@ module Sashite
247
266
 
248
267
  # Returns detailed validation errors for a data structure.
249
268
  #
269
+ # Note: This only performs JSON Schema validation, not the internal
270
+ # logical consistency checks that Ruleset.new performs.
271
+ #
250
272
  # @param data [Hash] The data structure to validate
251
273
  #
252
274
  # @return [Array<String>] Array of validation error messages (empty if valid)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-ggn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -27,8 +27,9 @@ description: A Ruby implementation of the General Gameplay Notation (GGN) specif
27
27
  GGN is a rule-agnostic, JSON-based format for describing pseudo-legal board-to-board
28
28
  transformations in abstract strategy board games. This library provides parsing,
29
29
  validation, and evaluation capabilities for GGN documents, focusing exclusively
30
- on piece movements, captures, and transformations on the game board. Supports Chess,
31
- Shogi, Xiangqi, and custom variants without hand management or piece drops.
30
+ on piece movements, captures, and transformations on the game board. Features flexible
31
+ validation system for both safety and performance. Supports Chess, Shogi, Xiangqi,
32
+ and custom variants without hand management or piece drops.
32
33
  email: contact@cyril.email
33
34
  executables: []
34
35
  extensions: []
@@ -56,6 +57,9 @@ metadata:
56
57
  source_code_uri: https://github.com/sashite/ggn.rb
57
58
  specification_uri: https://sashite.dev/documents/ggn/1.0.0/
58
59
  rubygems_mfa_required: 'true'
60
+ keywords: board-game, chess, game, gameplay, json, makruk, notation, performance,
61
+ pseudo-legal-move, rule-agnostic, serialization, shogi, strategy, validation,
62
+ xiangqi
59
63
  rdoc_options: []
60
64
  require_paths:
61
65
  - lib