sashite-ggn 0.1.0 → 0.3.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.
Files changed (84) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.md +17 -18
  3. data/README.md +356 -506
  4. data/lib/sashite/ggn/piece/source/destination/engine/transition.rb +90 -0
  5. data/lib/sashite/ggn/piece/source/destination/engine.rb +407 -0
  6. data/lib/sashite/ggn/piece/source/destination.rb +65 -0
  7. data/lib/sashite/ggn/piece/source.rb +71 -0
  8. data/lib/sashite/ggn/piece.rb +77 -0
  9. data/lib/sashite/ggn/schema.rb +152 -0
  10. data/lib/sashite/ggn/validation_error.rb +31 -0
  11. data/lib/sashite/ggn.rb +317 -5
  12. data/lib/sashite-ggn.rb +112 -1
  13. metadata +31 -151
  14. data/.gitignore +0 -22
  15. data/.ruby-version +0 -1
  16. data/.travis.yml +0 -3
  17. data/Gemfile +0 -2
  18. data/Rakefile +0 -7
  19. data/VERSION.semver +0 -1
  20. data/lib/sashite/ggn/ability.rb +0 -29
  21. data/lib/sashite/ggn/actor.rb +0 -20
  22. data/lib/sashite/ggn/ally.rb +0 -20
  23. data/lib/sashite/ggn/area.rb +0 -17
  24. data/lib/sashite/ggn/attacked.rb +0 -20
  25. data/lib/sashite/ggn/boolean.rb +0 -17
  26. data/lib/sashite/ggn/digit.rb +0 -20
  27. data/lib/sashite/ggn/digit_excluding_zero.rb +0 -17
  28. data/lib/sashite/ggn/direction.rb +0 -19
  29. data/lib/sashite/ggn/gameplay.rb +0 -20
  30. data/lib/sashite/ggn/gameplay_into_base64.rb +0 -21
  31. data/lib/sashite/ggn/integer.rb +0 -20
  32. data/lib/sashite/ggn/last_moved_actor.rb +0 -20
  33. data/lib/sashite/ggn/maximum_magnitude.rb +0 -20
  34. data/lib/sashite/ggn/name.rb +0 -17
  35. data/lib/sashite/ggn/negative_integer.rb +0 -19
  36. data/lib/sashite/ggn/null.rb +0 -21
  37. data/lib/sashite/ggn/object.rb +0 -28
  38. data/lib/sashite/ggn/occupied.rb +0 -29
  39. data/lib/sashite/ggn/pattern.rb +0 -20
  40. data/lib/sashite/ggn/previous_moves_counter.rb +0 -20
  41. data/lib/sashite/ggn/promotable_into_actors.rb +0 -23
  42. data/lib/sashite/ggn/required.rb +0 -19
  43. data/lib/sashite/ggn/self.rb +0 -21
  44. data/lib/sashite/ggn/square.rb +0 -29
  45. data/lib/sashite/ggn/state.rb +0 -26
  46. data/lib/sashite/ggn/subject.rb +0 -29
  47. data/lib/sashite/ggn/unsigned_integer.rb +0 -20
  48. data/lib/sashite/ggn/unsigned_integer_excluding_zero.rb +0 -20
  49. data/lib/sashite/ggn/verb.rb +0 -33
  50. data/lib/sashite/ggn/zero.rb +0 -21
  51. data/sashite-ggn.gemspec +0 -19
  52. data/test/_test_helper.rb +0 -2
  53. data/test/test_ggn.rb +0 -552
  54. data/test/test_ggn_ability.rb +0 -51
  55. data/test/test_ggn_actor.rb +0 -571
  56. data/test/test_ggn_ally.rb +0 -35
  57. data/test/test_ggn_area.rb +0 -21
  58. data/test/test_ggn_attacked.rb +0 -35
  59. data/test/test_ggn_boolean.rb +0 -21
  60. data/test/test_ggn_digit.rb +0 -21
  61. data/test/test_ggn_digit_excluding_zero.rb +0 -21
  62. data/test/test_ggn_direction.rb +0 -21
  63. data/test/test_ggn_gameplay.rb +0 -557
  64. data/test/test_ggn_gameplay_into_base64.rb +0 -555
  65. data/test/test_ggn_integer.rb +0 -39
  66. data/test/test_ggn_last_moved_actor.rb +0 -35
  67. data/test/test_ggn_maximum_magnitude.rb +0 -39
  68. data/test/test_ggn_name.rb +0 -21
  69. data/test/test_ggn_negative_integer.rb +0 -21
  70. data/test/test_ggn_null.rb +0 -21
  71. data/test/test_ggn_object.rb +0 -33
  72. data/test/test_ggn_occupied.rb +0 -78
  73. data/test/test_ggn_pattern.rb +0 -84
  74. data/test/test_ggn_previous_moves_counter.rb +0 -39
  75. data/test/test_ggn_promotable_into_actors.rb +0 -578
  76. data/test/test_ggn_required.rb +0 -21
  77. data/test/test_ggn_self.rb +0 -21
  78. data/test/test_ggn_square.rb +0 -25
  79. data/test/test_ggn_state.rb +0 -24
  80. data/test/test_ggn_subject.rb +0 -28
  81. data/test/test_ggn_unsigned_integer.rb +0 -39
  82. data/test/test_ggn_unsigned_integer_excluding_zero.rb +0 -25
  83. data/test/test_ggn_verb.rb +0 -27
  84. data/test/test_ggn_zero.rb +0 -21
data/README.md CHANGED
@@ -1,568 +1,418 @@
1
- # Sashite::GGN
1
+ # Ggn.rb
2
2
 
3
- This module provides a Ruby interface for data serialization in [GGN](http://sashite.wiki/General_Gameplay_Notation) format.
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/ggn.rb?label=Version&logo=github)](https://github.com/sashite/ggn.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/ggn.rb/main)
5
+ ![Ruby](https://github.com/sashite/ggn.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/ggn.rb?label=License&logo=github)](https://github.com/sashite/ggn.rb/raw/main/LICENSE.md)
4
7
 
5
- ## Status
8
+ > **GGN** (General Gameplay Notation) support for the Ruby language.
6
9
 
7
- * [![Gem Version](https://badge.fury.io/rb/sashite-ggn.svg)](//badge.fury.io/rb/sashite-ggn)
8
- * [![Build Status](https://secure.travis-ci.org/sashite/ggn.rb.svg?branch=master)](//travis-ci.org/sashite/ggn.rb?branch=master)
9
- * [![Dependency Status](https://gemnasium.com/sashite/ggn.rb.svg)](//gemnasium.com/sashite/ggn.rb)
10
+ ## What is GGN?
11
+
12
+ GGN (General Gameplay Notation) is a rule-agnostic, JSON-based format for describing **pseudo-legal moves** in abstract strategy board games. Unlike move notations that express *what* a move does, GGN expresses *whether* that move is **possible** under basic movement constraints.
13
+
14
+ GGN is deliberately silent about higher-level, game-specific legality questions (e.g., check, ko, repetition, castling paths). This neutrality makes the format universal: any engine can pre-compute and share a library of pseudo-legal moves for any mix of games.
15
+
16
+ This gem implements the [GGN Specification v1.0.0](https://sashite.dev/documents/ggn/1.0.0/), providing a Ruby interface for:
17
+
18
+ - Loading and validating GGN JSON documents
19
+ - Querying pseudo-legal moves for specific pieces and positions
20
+ - Evaluating move validity under current board conditions
21
+ - Processing complex move conditions including captures, drops, and promotions
10
22
 
11
23
  ## Installation
12
24
 
13
- Add this line to your application's Gemfile:
25
+ ```ruby
26
+ # In your Gemfile
27
+ gem "sashite-ggn"
28
+ ```
29
+
30
+ Or install manually:
14
31
 
15
- gem 'sashite-ggn'
32
+ ```sh
33
+ gem install sashite-ggn
34
+ ```
16
35
 
17
- And then execute:
36
+ ## GGN Format
18
37
 
19
- $ bundle
38
+ A single GGN **entry** answers the question:
20
39
 
21
- Or install it yourself as:
40
+ > Can this piece, currently on this square, reach that square?
22
41
 
23
- $ gem install sashite-ggn
42
+ It encodes:
24
43
 
25
- ## Usage
44
+ 1. **Which piece** (via GAN identifier)
45
+ 2. **From where** (source square label, or "`*`" for off-board)
46
+ 3. **To where** (destination square label)
47
+ 4. **Which pre-conditions** must hold (`require`)
48
+ 5. **Which pre-conditions** must not hold (`prevent`)
49
+ 6. **Which post-conditions** result (`perform`, plus optional `gain` or `drop`)
26
50
 
27
- Working with GGN can be very simple, for example:
51
+ ### JSON Structure
28
52
 
29
- ```ruby
30
- require 'sashite-ggn'
31
-
32
- # Parse a GGN string
33
-
34
- ggn_obj = [
35
- [
36
- {
37
- :"subject" => {
38
- :"...ally?" => true,
39
- :"actor" => :self,
40
- :"state" => {
41
- :"...last_moved_actor?" => nil,
42
- :"...previous_moves_counter" => nil
53
+ ```json
54
+ {
55
+ "<Source piece GAN>": {
56
+ "<Source square>": {
57
+ "<Destination square>": [
58
+ {
59
+ "require": { "<square>": "<required state>", … },
60
+ "prevent": { "<square>": "<forbidden state>", … },
61
+ "perform": { "<square>": "<new state | null>", … },
62
+ "gain": "<piece GAN>" | null,
63
+ "drop": "<piece GAN>" | null
43
64
  }
44
- },
45
-
46
- :"verb" => {
47
- :"name" => :remove,
48
- :"vector" => {:"...maximum_magnitude" => 1, direction: [-1,0]}
49
- },
50
-
51
- :"object" => {
52
- :"src_square" => {
53
- :"...attacked?" => nil,
54
- :"...occupied!" => false,
55
- :"area" => :all
56
- },
57
- :"dst_square" => {
58
- :"...attacked?" => nil,
59
- :"...occupied!" => :an_enemy_actor,
60
- :"area" => :all
61
- },
62
- :"promotable_into_actors" => [:self]
63
- }
65
+ ]
64
66
  }
65
- ],
67
+ }
68
+ }
69
+ ```
66
70
 
71
+ ## Basic Usage
67
72
 
73
+ ### Loading GGN Data
68
74
 
69
- [
70
- {
71
- :"subject" => {
72
- :"...ally?" => true,
73
- :"actor" => :self,
74
- :"state" => {
75
- :"...last_moved_actor?" => nil,
76
- :"...previous_moves_counter" => nil
77
- }
78
- },
79
-
80
- :"verb" => {
81
- :"name" => :remove,
82
- :"vector" => {:"...maximum_magnitude" => 1, direction: [0,-1]}
83
- },
84
-
85
- :"object" => {
86
- :"src_square" => {
87
- :"...attacked?" => nil,
88
- :"...occupied!" => false,
89
- :"area" => :all
90
- },
91
- :"dst_square" => {
92
- :"...attacked?" => nil,
93
- :"...occupied!" => :an_enemy_actor,
94
- :"area" => :all
95
- },
96
- :"promotable_into_actors" => [:self]
97
- }
98
- }
99
- ],
75
+ Load GGN data from various sources:
100
76
 
77
+ ```ruby
78
+ require "sashite-ggn"
101
79
 
80
+ # From file
81
+ piece_data = Sashite::Ggn.load_file("chess_moves.json")
102
82
 
103
- [
104
- {
105
- :"subject" => {
106
- :"...ally?" => true,
107
- :"actor" => :self,
108
- :"state" => {
109
- :"...last_moved_actor?" => nil,
110
- :"...previous_moves_counter" => nil
111
- }
112
- },
113
-
114
- :"verb" => {
115
- :"name" => :remove,
116
- :"vector" => {:"...maximum_magnitude" => 1, direction: [0,1]}
117
- },
118
-
119
- :"object" => {
120
- :"src_square" => {
121
- :"...attacked?" => nil,
122
- :"...occupied!" => false,
123
- :"area" => :all
124
- },
125
- :"dst_square" => {
126
- :"...attacked?" => nil,
127
- :"...occupied!" => :an_enemy_actor,
128
- :"area" => :all
129
- },
130
- :"promotable_into_actors" => [:self]
131
- }
132
- }
133
- ],
83
+ # From JSON string
84
+ json_string = '{"CHESS:P": {"e2": {"e4": [{"require": {"e3": "empty", "e4": "empty"}, "perform": {"e2": null, "e4": "CHESS:P"}}]}}}'
85
+ piece_data = Sashite::Ggn.load_string(json_string)
134
86
 
87
+ # From Hash
88
+ ggn_hash = { "CHESS:P" => { "e2" => { "e4" => [{ "require" => { "e3" => "empty", "e4" => "empty" }, "perform" => { "e2" => nil, "e4" => "CHESS:P" } }] } } }
89
+ piece_data = Sashite::Ggn.load_hash(ggn_hash)
90
+ ```
135
91
 
92
+ ### Querying Moves
136
93
 
137
- [
138
- {
139
- :"subject" => {
140
- :"...ally?" => true,
141
- :"actor" => :self,
142
- :"state" => {
143
- :"...last_moved_actor?" => nil,
144
- :"...previous_moves_counter" => nil
145
- }
146
- },
147
-
148
- :"verb" => {
149
- :"name" => :remove,
150
- :"vector" => {:"...maximum_magnitude" => 1, direction: [1,0]}
151
- },
152
-
153
- :"object" => {
154
- :"src_square" => {
155
- :"...attacked?" => nil,
156
- :"...occupied!" => false,
157
- :"area" => :all
158
- },
159
- :"dst_square" => {
160
- :"...attacked?" => nil,
161
- :"...occupied!" => :an_enemy_actor,
162
- :"area" => :all
163
- },
164
- :"promotable_into_actors" => [:self]
165
- }
166
- }
167
- ],
94
+ Navigate through the GGN structure to find specific moves:
168
95
 
96
+ ```ruby
97
+ require "sashite-ggn"
169
98
 
99
+ piece_data = Sashite::Ggn.load_file("chess_moves.json")
170
100
 
171
- [
172
- {
173
- :"subject" => {
174
- :"...ally?" => true,
175
- :"actor" => :self,
176
- :"state" => {
177
- :"...last_moved_actor?" => nil,
178
- :"...previous_moves_counter" => nil
179
- }
180
- },
181
-
182
- :"verb" => {
183
- :"name" => :shift,
184
- :"vector" => {:"...maximum_magnitude" => nil, direction: [-1,0]}
185
- },
186
-
187
- :"object" => {
188
- :"src_square" => {
189
- :"...attacked?" => nil,
190
- :"...occupied!" => false,
191
- :"area" => :all
192
- },
193
- :"dst_square" => {
194
- :"...attacked?" => nil,
195
- :"...occupied!" => false,
196
- :"area" => :all
197
- },
198
- :"promotable_into_actors" => [:self]
199
- }
200
- }
201
- ],
101
+ # Select a piece type
102
+ source = piece_data.select("CHESS:P")
202
103
 
104
+ # Get destinations from a specific source square
105
+ destinations = source.from("e2")
203
106
 
107
+ # Get the engine for a specific target square
108
+ engine = destinations.to("e4")
109
+ ```
204
110
 
205
- [
206
- {
207
- :"subject" => {
208
- :"...ally?" => true,
209
- :"actor" => :self,
210
- :"state" => {
211
- :"...last_moved_actor?" => nil,
212
- :"...previous_moves_counter" => nil
213
- }
214
- },
215
-
216
- :"verb" => {
217
- :"name" => :shift,
218
- :"vector" => {:"...maximum_magnitude" => nil, direction: [-1,0]}
219
- },
220
-
221
- :"object" => {
222
- :"src_square" => {
223
- :"...attacked?" => nil,
224
- :"...occupied!" => false,
225
- :"area" => :all
226
- },
227
- :"dst_square" => {
228
- :"...attacked?" => nil,
229
- :"...occupied!" => false,
230
- :"area" => :all
231
- },
232
- :"promotable_into_actors" => [:self]
233
- }
234
- },
235
- {
236
- :"subject" => {
237
- :"...ally?" => true,
238
- :"actor" => :self,
239
- :"state" => {
240
- :"...last_moved_actor?" => nil,
241
- :"...previous_moves_counter" => nil
242
- }
243
- },
244
-
245
- :"verb" => {
246
- :"name" => :remove,
247
- :"vector" => {:"...maximum_magnitude" => 1, direction: [-1,0]}
248
- },
249
-
250
- :"object" => {
251
- :"src_square" => {
252
- :"...attacked?" => nil,
253
- :"...occupied!" => false,
254
- :"area" => :all
255
- },
256
- :"dst_square" => {
257
- :"...attacked?" => nil,
258
- :"...occupied!" => :an_enemy_actor,
259
- :"area" => :all
260
- },
261
- :"promotable_into_actors" => [:self]
262
- }
263
- }
264
- ],
111
+ ### Evaluating Move Validity
265
112
 
113
+ Check if a move is valid under current board conditions:
266
114
 
115
+ ```ruby
116
+ require "sashite-ggn"
117
+
118
+ # Load piece data and get the movement engine
119
+ piece_data = Sashite::Ggn.load_file("chess_moves.json")
120
+ engine = piece_data.select("CHESS:P").from("e2").to("e4")
121
+
122
+ # Define current board state
123
+ board_state = {
124
+ "e2" => "CHESS:P", # White pawn on e2
125
+ "e3" => nil, # Empty square
126
+ "e4" => nil # Empty square
127
+ }
128
+
129
+ # Evaluate the move
130
+ result = engine.where(board_state, {}, "CHESS")
131
+
132
+ if result
133
+ puts "Move is valid!"
134
+ puts "Board changes: #{result.diff}"
135
+ # => { "e2" => nil, "e4" => "CHESS:P" }
136
+ puts "Piece gained: #{result.gain}" # => nil (no capture)
137
+ puts "Piece dropped: #{result.drop}" # => nil (not a drop move)
138
+ else
139
+ puts "Move is not valid under current conditions"
140
+ end
141
+ ```
267
142
 
268
- [
269
- {
270
- :"subject" => {
271
- :"...ally?" => true,
272
- :"actor" => :self,
273
- :"state" => {
274
- :"...last_moved_actor?" => nil,
275
- :"...previous_moves_counter" => nil
276
- }
277
- },
278
-
279
- :"verb" => {
280
- :"name" => :shift,
281
- :"vector" => {:"...maximum_magnitude" => nil, direction: [0,-1]}
282
- },
283
-
284
- :"object" => {
285
- :"src_square" => {
286
- :"...attacked?" => nil,
287
- :"...occupied!" => false,
288
- :"area" => :all
289
- },
290
- :"dst_square" => {
291
- :"...attacked?" => nil,
292
- :"...occupied!" => false,
293
- :"area" => :all
294
- },
295
- :"promotable_into_actors" => [:self]
296
- }
297
- }
298
- ],
143
+ ### Handling Captures
144
+
145
+ Process moves that capture enemy pieces:
299
146
 
147
+ ```ruby
148
+ require "sashite-ggn"
149
+
150
+ # Load piece data for a capture move
151
+ piece_data = Sashite::Ggn.load_file("chess_moves.json")
152
+ engine = piece_data.select("CHESS:P").from("e5").to("d6")
153
+
154
+ # Board state with enemy piece to capture
155
+ board_state = {
156
+ "e5" => "CHESS:P", # Our pawn
157
+ "d6" => "chess:p" # Enemy pawn (lowercase = opponent)
158
+ }
159
+
160
+ result = engine.where(board_state, {}, "CHESS")
161
+
162
+ if result
163
+ puts "Capture is valid!"
164
+ puts "Board changes: #{result.diff}"
165
+ # => { "e5" => nil, "d6" => "CHESS:P" }
166
+ puts "Captured piece: #{result.gain}" # => "CHESS:P" (gained in hand)
167
+ end
168
+ ```
300
169
 
170
+ ### Piece Drops (Shogi-style)
301
171
 
302
- [
303
- {
304
- :"subject" => {
305
- :"...ally?" => true,
306
- :"actor" => :self,
307
- :"state" => {
308
- :"...last_moved_actor?" => nil,
309
- :"...previous_moves_counter" => nil
310
- }
311
- },
312
-
313
- :"verb" => {
314
- :"name" => :shift,
315
- :"vector" => {:"...maximum_magnitude" => nil, direction: [0,-1]}
316
- },
317
-
318
- :"object" => {
319
- :"src_square" => {
320
- :"...attacked?" => nil,
321
- :"...occupied!" => false,
322
- :"area" => :all
323
- },
324
- :"dst_square" => {
325
- :"...attacked?" => nil,
326
- :"...occupied!" => false,
327
- :"area" => :all
328
- },
329
- :"promotable_into_actors" => [:self]
330
- }
331
- },
332
- {
333
- :"subject" => {
334
- :"...ally?" => true,
335
- :"actor" => :self,
336
- :"state" => {
337
- :"...last_moved_actor?" => nil,
338
- :"...previous_moves_counter" => nil
172
+ Handle dropping pieces from hand onto the board:
173
+
174
+ ```ruby
175
+ require "sashite-ggn"
176
+
177
+ # Load Shogi piece data
178
+ piece_data = Sashite::Ggn.load_file("shogi_moves.json")
179
+ engine = piece_data.select("SHOGI:P").from("*").to("5e")
180
+
181
+ # Player has captured pawns available
182
+ captures = { "SHOGI:P" => 2 }
183
+
184
+ # Current board state (5th file is clear of unpromoted pawns)
185
+ board_state = {
186
+ "5e" => nil, # Target square is empty
187
+ "5a" => nil, "5b" => nil, "5c" => nil, "5d" => nil,
188
+ "5f" => nil, "5g" => nil, "5h" => nil, "5i" => nil
189
+ }
190
+
191
+ result = engine.where(board_state, captures, "SHOGI")
192
+
193
+ if result
194
+ puts "Pawn drop is valid!"
195
+ puts "Board changes: #{result.diff}" # => { "5e" => "SHOGI:P" }
196
+ puts "Piece dropped from hand: #{result.drop}" # => "SHOGI:P"
197
+ end
198
+ ```
199
+
200
+ ## Validation
201
+
202
+ ### Schema Validation
203
+
204
+ Validate GGN data against the official JSON Schema:
205
+
206
+ ```ruby
207
+ require "sashite-ggn"
208
+
209
+ # Validate during loading (default behavior)
210
+ begin
211
+ piece_data = Sashite::Ggn.load_file("moves.json")
212
+ puts "GGN data is valid!"
213
+ rescue Sashite::Ggn::ValidationError => e
214
+ puts "Validation failed: #{e.message}"
215
+ end
216
+
217
+ # Skip validation for performance (large files)
218
+ piece_data = Sashite::Ggn.load_file("large_moves.json", validate: false)
219
+
220
+ # Validate manually
221
+ begin
222
+ Sashite::Ggn.validate!(my_data)
223
+ puts "Data is valid"
224
+ rescue Sashite::Ggn::ValidationError => e
225
+ puts "Invalid: #{e.message}"
226
+ end
227
+
228
+ # Check validity without exceptions
229
+ if Sashite::Ggn.valid?(my_data)
230
+ puts "Data is valid"
231
+ else
232
+ errors = Sashite::Ggn.validation_errors(my_data)
233
+ puts "Validation errors: #{errors.join(', ')}"
234
+ end
235
+ ```
236
+
237
+ ## Occupation States
238
+
239
+ GGN recognizes several occupation states for move conditions:
240
+
241
+ | State | Meaning |
242
+ | ---------------- | ---------------------------------------------------------------------------- |
243
+ | `"empty"` | Square must be empty |
244
+ | `"enemy"` | Square must contain a standard opposing piece |
245
+ | *GAN identifier* | Square must contain **exactly** the specified piece |
246
+
247
+ ### Implicit States
248
+
249
+ Through the `prevent` field, additional states can be expressed:
250
+
251
+ | Implicit State | Expression | Meaning |
252
+ | ---------------- | ---------------------------- | -------------------------------------------------------- |
253
+ | `"occupied"` | `"prevent": { "a1": "empty" }` | Square must be occupied by any piece |
254
+ | `"ally"` | `"prevent": { "a1": "enemy" }` | Square must contain a friendly piece |
255
+
256
+ ## Examples
257
+
258
+ ### Simple Move
259
+
260
+ A piece moving from one square to another without conditions:
261
+
262
+ ```json
263
+ {
264
+ "CHESS:K": {
265
+ "e1": {
266
+ "e2": [
267
+ {
268
+ "perform": { "e1": null, "e2": "CHESS:K" }
339
269
  }
340
- },
341
-
342
- :"verb" => {
343
- :"name" => :remove,
344
- :"vector" => {:"...maximum_magnitude" => 1, direction: [0,-1]}
345
- },
346
-
347
- :"object" => {
348
- :"src_square" => {
349
- :"...attacked?" => nil,
350
- :"...occupied!" => false,
351
- :"area" => :all
352
- },
353
- :"dst_square" => {
354
- :"...attacked?" => nil,
355
- :"...occupied!" => :an_enemy_actor,
356
- :"area" => :all
357
- },
358
- :"promotable_into_actors" => [:self]
359
- }
270
+ ]
360
271
  }
361
- ],
272
+ }
273
+ }
274
+ ```
362
275
 
276
+ ### Sliding Move
363
277
 
278
+ A piece that slides along empty squares:
364
279
 
365
- [
366
- {
367
- :"subject" => {
368
- :"...ally?" => true,
369
- :"actor" => :self,
370
- :"state" => {
371
- :"...last_moved_actor?" => nil,
372
- :"...previous_moves_counter" => nil
280
+ ```json
281
+ {
282
+ "CHESS:R": {
283
+ "a1": {
284
+ "a3": [
285
+ {
286
+ "require": { "a2": "empty", "a3": "empty" },
287
+ "perform": { "a1": null, "a3": "CHESS:R" }
373
288
  }
374
- },
375
-
376
- :"verb" => {
377
- :"name" => :shift,
378
- :"vector" => {:"...maximum_magnitude" => nil, direction: [0,1]}
379
- },
380
-
381
- :"object" => {
382
- :"src_square" => {
383
- :"...attacked?" => nil,
384
- :"...occupied!" => false,
385
- :"area" => :all
386
- },
387
- :"dst_square" => {
388
- :"...attacked?" => nil,
389
- :"...occupied!" => false,
390
- :"area" => :all
391
- },
392
- :"promotable_into_actors" => [:self]
393
- }
289
+ ]
394
290
  }
395
- ],
291
+ }
292
+ }
293
+ ```
396
294
 
295
+ ### Capture with Gain
397
296
 
297
+ A piece capturing an enemy and gaining it in hand:
398
298
 
399
- [
400
- {
401
- :"subject" => {
402
- :"...ally?" => true,
403
- :"actor" => :self,
404
- :"state" => {
405
- :"...last_moved_actor?" => nil,
406
- :"...previous_moves_counter" => nil
299
+ ```json
300
+ {
301
+ "SHOGI:P": {
302
+ "5f": {
303
+ "5e": [
304
+ {
305
+ "require": { "5e": "enemy" },
306
+ "perform": { "5f": null, "5e": "SHOGI:P" },
307
+ "gain": "SHOGI:P"
407
308
  }
408
- },
409
-
410
- :"verb" => {
411
- :"name" => :shift,
412
- :"vector" => {:"...maximum_magnitude" => nil, direction: [0,1]}
413
- },
414
-
415
- :"object" => {
416
- :"src_square" => {
417
- :"...attacked?" => nil,
418
- :"...occupied!" => false,
419
- :"area" => :all
420
- },
421
- :"dst_square" => {
422
- :"...attacked?" => nil,
423
- :"...occupied!" => false,
424
- :"area" => :all
425
- },
426
- :"promotable_into_actors" => [:self]
427
- }
428
- },
429
- {
430
- :"subject" => {
431
- :"...ally?" => true,
432
- :"actor" => :self,
433
- :"state" => {
434
- :"...last_moved_actor?" => nil,
435
- :"...previous_moves_counter" => nil
309
+ ]
310
+ }
311
+ }
312
+ }
313
+ ```
314
+
315
+ ### Piece Drop
316
+
317
+ Dropping a piece from hand onto the board:
318
+
319
+ ```json
320
+ {
321
+ "SHOGI:P": {
322
+ "*": {
323
+ "5e": [
324
+ {
325
+ "require": { "5e": "empty" },
326
+ "prevent": {
327
+ "5a": "SHOGI:P", "5b": "SHOGI:P", "5c": "SHOGI:P",
328
+ "5d": "SHOGI:P", "5f": "SHOGI:P", "5g": "SHOGI:P",
329
+ "5h": "SHOGI:P", "5i": "SHOGI:P"
330
+ },
331
+ "perform": { "5e": "SHOGI:P" },
332
+ "drop": "SHOGI:P"
436
333
  }
437
- },
438
-
439
- :"verb" => {
440
- :"name" => :remove,
441
- :"vector" => {:"...maximum_magnitude" => 1, direction: [0,1]}
442
- },
443
-
444
- :"object" => {
445
- :"src_square" => {
446
- :"...attacked?" => nil,
447
- :"...occupied!" => false,
448
- :"area" => :all
449
- },
450
- :"dst_square" => {
451
- :"...attacked?" => nil,
452
- :"...occupied!" => :an_enemy_actor,
453
- :"area" => :all
454
- },
455
- :"promotable_into_actors" => [:self]
456
- }
334
+ ]
457
335
  }
458
- ],
336
+ }
337
+ }
338
+ ```
459
339
 
340
+ ### Promotion
460
341
 
342
+ A piece moving and changing to a different piece type:
461
343
 
462
- [
463
- {
464
- :"subject" => {
465
- :"...ally?" => true,
466
- :"actor" => :self,
467
- :"state" => {
468
- :"...last_moved_actor?" => nil,
469
- :"...previous_moves_counter" => nil
344
+ ```json
345
+ {
346
+ "CHESS:P": {
347
+ "g7": {
348
+ "g8": [
349
+ {
350
+ "require": { "g8": "empty" },
351
+ "perform": { "g7": null, "g8": "CHESS:Q" }
470
352
  }
471
- },
472
-
473
- :"verb" => {
474
- :"name" => :shift,
475
- :"vector" => {:"...maximum_magnitude" => nil, direction: [1,0]}
476
- },
477
-
478
- :"object" => {
479
- :"src_square" => {
480
- :"...attacked?" => nil,
481
- :"...occupied!" => false,
482
- :"area" => :all
483
- },
484
- :"dst_square" => {
485
- :"...attacked?" => nil,
486
- :"...occupied!" => false,
487
- :"area" => :all
488
- },
489
- :"promotable_into_actors" => [:self]
490
- }
353
+ ]
491
354
  }
492
- ],
355
+ }
356
+ }
357
+ ```
493
358
 
359
+ ## Error Handling
494
360
 
361
+ The library provides comprehensive error handling:
495
362
 
496
- [
497
- {
498
- :"subject" => {
499
- :"...ally?" => true,
500
- :"actor" => :self,
501
- :"state" => {
502
- :"...last_moved_actor?" => nil,
503
- :"...previous_moves_counter" => nil
504
- }
505
- },
506
-
507
- :"verb" => {
508
- :"name" => :shift,
509
- :"vector" => {:"...maximum_magnitude" => nil, direction: [1,0]}
510
- },
511
-
512
- :"object" => {
513
- :"src_square" => {
514
- :"...attacked?" => nil,
515
- :"...occupied!" => false,
516
- :"area" => :all
517
- },
518
- :"dst_square" => {
519
- :"...attacked?" => nil,
520
- :"...occupied!" => false,
521
- :"area" => :all
522
- },
523
- :"promotable_into_actors" => [:self]
524
- }
525
- },
526
- {
527
- :"subject" => {
528
- :"...ally?" => true,
529
- :"actor" => :self,
530
- :"state" => {
531
- :"...last_moved_actor?" => nil,
532
- :"...previous_moves_counter" => nil
533
- }
534
- },
535
-
536
- :"verb" => {
537
- :"name" => :remove,
538
- :"vector" => {:"...maximum_magnitude" => 1, direction: [1,0]}
539
- },
540
-
541
- :"object" => {
542
- :"src_square" => {
543
- :"...attacked?" => nil,
544
- :"...occupied!" => false,
545
- :"area" => :all
546
- },
547
- :"dst_square" => {
548
- :"...attacked?" => nil,
549
- :"...occupied!" => :an_enemy_actor,
550
- :"area" => :all
551
- },
552
- :"promotable_into_actors" => [:self]
553
- }
554
- }
555
- ]
556
- ]
363
+ ```ruby
364
+ require "sashite-ggn"
365
+
366
+ begin
367
+ # Various operations that might fail
368
+ piece_data = Sashite::Ggn.load_file("nonexistent.json")
369
+ source = piece_data.select("INVALID:PIECE")
370
+ destinations = source.from("invalid_square")
371
+ engine = destinations.to("another_invalid")
372
+ result = engine.where({}, {}, "")
373
+ rescue Sashite::Ggn::ValidationError => e
374
+ puts "GGN validation error: #{e.message}"
375
+ rescue KeyError => e
376
+ puts "Key not found: #{e.message}"
377
+ rescue ArgumentError => e
378
+ puts "Invalid argument: #{e.message}"
379
+ end
380
+ ```
381
+
382
+ ## Performance Considerations
557
383
 
558
- Sashite::GGN.load(ggn_obj)
559
- # => "t<self>_&_^remove[-1,0]1/t=_@f+all~_@an_enemy_actor+all%self. t<self>_&_^remove[0,-1]1/t=_@f+all~_@an_enemy_actor+all%self. t<self>_&_^remove[0,1]1/t=_@f+all~_@an_enemy_actor+all%self. t<self>_&_^remove[1,0]1/t=_@f+all~_@an_enemy_actor+all%self. t<self>_&_^shift[-1,0]_/t=_@f+all~_@f+all%self. t<self>_&_^shift[-1,0]_/t=_@f+all~_@f+all%self; t<self>_&_^remove[-1,0]1/t=_@f+all~_@an_enemy_actor+all%self. t<self>_&_^shift[0,-1]_/t=_@f+all~_@f+all%self. t<self>_&_^shift[0,-1]_/t=_@f+all~_@f+all%self; t<self>_&_^remove[0,-1]1/t=_@f+all~_@an_enemy_actor+all%self. t<self>_&_^shift[0,1]_/t=_@f+all~_@f+all%self. t<self>_&_^shift[0,1]_/t=_@f+all~_@f+all%self; t<self>_&_^remove[0,1]1/t=_@f+all~_@an_enemy_actor+all%self. t<self>_&_^shift[1,0]_/t=_@f+all~_@f+all%self. t<self>_&_^shift[1,0]_/t=_@f+all~_@f+all%self; t<self>_&_^remove[1,0]1/t=_@f+all~_@an_enemy_actor+all%self."
384
+ For large GGN files or high-frequency operations:
385
+
386
+ ```ruby
387
+ # Skip validation for better performance
388
+ piece_data = Sashite::Ggn.load_file("large_dataset.json", validate: false)
389
+
390
+ # Cache frequently used engines
391
+ @engines = {}
392
+ def get_engine(piece, from, to)
393
+ key = "#{piece}:#{from}:#{to}"
394
+ @engines[key] ||= piece_data.select(piece).from(from).to(to)
395
+ end
560
396
  ```
561
397
 
562
- ## Contributing
398
+ ## Related Specifications
399
+
400
+ GGN works alongside other Sashité specifications:
401
+
402
+ - **[GAN](https://sashite.dev/documents/gan/1.0.0/)** (General Actor Notation): Unique piece identifiers
403
+ - **[FEEN](https://sashite.dev/documents/feen/1.0.0/)** (Forsyth-Edwards Enhanced Notation): Board position representation
404
+ - **[PMN](https://sashite.dev/documents/pmn/1.0.0/)** (Portable Move Notation): Move sequence representation
405
+
406
+ ## Documentation
407
+
408
+ - [Official GGN Specification](https://sashite.dev/documents/ggn/1.0.0/)
409
+ - [JSON Schema](https://sashite.dev/schemas/ggn/1.0.0/schema.json)
410
+ - [API Documentation](https://rubydoc.info/github/sashite/ggn.rb/main)
411
+
412
+ ## License
413
+
414
+ 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).
415
+
416
+ ## About Sashité
563
417
 
564
- 1. Fork it
565
- 2. Create your feature branch (`git checkout -b my-new-feature`)
566
- 3. Commit your changes (`git commit -am 'Add some feature'`)
567
- 4. Push to the branch (`git push origin my-new-feature`)
568
- 5. Create a new Pull Request
418
+ This project is maintained by [Sashité](https://sashite.com/) promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.