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.
- checksums.yaml +5 -5
- data/LICENSE.md +17 -18
- data/README.md +356 -506
- data/lib/sashite/ggn/piece/source/destination/engine/transition.rb +90 -0
- data/lib/sashite/ggn/piece/source/destination/engine.rb +407 -0
- data/lib/sashite/ggn/piece/source/destination.rb +65 -0
- data/lib/sashite/ggn/piece/source.rb +71 -0
- data/lib/sashite/ggn/piece.rb +77 -0
- data/lib/sashite/ggn/schema.rb +152 -0
- data/lib/sashite/ggn/validation_error.rb +31 -0
- data/lib/sashite/ggn.rb +317 -5
- data/lib/sashite-ggn.rb +112 -1
- metadata +31 -151
- data/.gitignore +0 -22
- data/.ruby-version +0 -1
- data/.travis.yml +0 -3
- data/Gemfile +0 -2
- data/Rakefile +0 -7
- data/VERSION.semver +0 -1
- data/lib/sashite/ggn/ability.rb +0 -29
- data/lib/sashite/ggn/actor.rb +0 -20
- data/lib/sashite/ggn/ally.rb +0 -20
- data/lib/sashite/ggn/area.rb +0 -17
- data/lib/sashite/ggn/attacked.rb +0 -20
- data/lib/sashite/ggn/boolean.rb +0 -17
- data/lib/sashite/ggn/digit.rb +0 -20
- data/lib/sashite/ggn/digit_excluding_zero.rb +0 -17
- data/lib/sashite/ggn/direction.rb +0 -19
- data/lib/sashite/ggn/gameplay.rb +0 -20
- data/lib/sashite/ggn/gameplay_into_base64.rb +0 -21
- data/lib/sashite/ggn/integer.rb +0 -20
- data/lib/sashite/ggn/last_moved_actor.rb +0 -20
- data/lib/sashite/ggn/maximum_magnitude.rb +0 -20
- data/lib/sashite/ggn/name.rb +0 -17
- data/lib/sashite/ggn/negative_integer.rb +0 -19
- data/lib/sashite/ggn/null.rb +0 -21
- data/lib/sashite/ggn/object.rb +0 -28
- data/lib/sashite/ggn/occupied.rb +0 -29
- data/lib/sashite/ggn/pattern.rb +0 -20
- data/lib/sashite/ggn/previous_moves_counter.rb +0 -20
- data/lib/sashite/ggn/promotable_into_actors.rb +0 -23
- data/lib/sashite/ggn/required.rb +0 -19
- data/lib/sashite/ggn/self.rb +0 -21
- data/lib/sashite/ggn/square.rb +0 -29
- data/lib/sashite/ggn/state.rb +0 -26
- data/lib/sashite/ggn/subject.rb +0 -29
- data/lib/sashite/ggn/unsigned_integer.rb +0 -20
- data/lib/sashite/ggn/unsigned_integer_excluding_zero.rb +0 -20
- data/lib/sashite/ggn/verb.rb +0 -33
- data/lib/sashite/ggn/zero.rb +0 -21
- data/sashite-ggn.gemspec +0 -19
- data/test/_test_helper.rb +0 -2
- data/test/test_ggn.rb +0 -552
- data/test/test_ggn_ability.rb +0 -51
- data/test/test_ggn_actor.rb +0 -571
- data/test/test_ggn_ally.rb +0 -35
- data/test/test_ggn_area.rb +0 -21
- data/test/test_ggn_attacked.rb +0 -35
- data/test/test_ggn_boolean.rb +0 -21
- data/test/test_ggn_digit.rb +0 -21
- data/test/test_ggn_digit_excluding_zero.rb +0 -21
- data/test/test_ggn_direction.rb +0 -21
- data/test/test_ggn_gameplay.rb +0 -557
- data/test/test_ggn_gameplay_into_base64.rb +0 -555
- data/test/test_ggn_integer.rb +0 -39
- data/test/test_ggn_last_moved_actor.rb +0 -35
- data/test/test_ggn_maximum_magnitude.rb +0 -39
- data/test/test_ggn_name.rb +0 -21
- data/test/test_ggn_negative_integer.rb +0 -21
- data/test/test_ggn_null.rb +0 -21
- data/test/test_ggn_object.rb +0 -33
- data/test/test_ggn_occupied.rb +0 -78
- data/test/test_ggn_pattern.rb +0 -84
- data/test/test_ggn_previous_moves_counter.rb +0 -39
- data/test/test_ggn_promotable_into_actors.rb +0 -578
- data/test/test_ggn_required.rb +0 -21
- data/test/test_ggn_self.rb +0 -21
- data/test/test_ggn_square.rb +0 -25
- data/test/test_ggn_state.rb +0 -24
- data/test/test_ggn_subject.rb +0 -28
- data/test/test_ggn_unsigned_integer.rb +0 -39
- data/test/test_ggn_unsigned_integer_excluding_zero.rb +0 -25
- data/test/test_ggn_verb.rb +0 -27
- data/test/test_ggn_zero.rb +0 -21
data/README.md
CHANGED
@@ -1,568 +1,418 @@
|
|
1
|
-
#
|
1
|
+
# Ggn.rb
|
2
2
|
|
3
|
-
|
3
|
+
[](https://github.com/sashite/ggn.rb/tags)
|
4
|
+
[](https://rubydoc.info/github/sashite/ggn.rb/main)
|
5
|
+

|
6
|
+
[](https://github.com/sashite/ggn.rb/raw/main/LICENSE.md)
|
4
7
|
|
5
|
-
|
8
|
+
> **GGN** (General Gameplay Notation) support for the Ruby language.
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
25
|
+
```ruby
|
26
|
+
# In your Gemfile
|
27
|
+
gem "sashite-ggn"
|
28
|
+
```
|
29
|
+
|
30
|
+
Or install manually:
|
14
31
|
|
15
|
-
|
32
|
+
```sh
|
33
|
+
gem install sashite-ggn
|
34
|
+
```
|
16
35
|
|
17
|
-
|
36
|
+
## GGN Format
|
18
37
|
|
19
|
-
|
38
|
+
A single GGN **entry** answers the question:
|
20
39
|
|
21
|
-
|
40
|
+
> Can this piece, currently on this square, reach that square?
|
22
41
|
|
23
|
-
|
42
|
+
It encodes:
|
24
43
|
|
25
|
-
|
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
|
-
|
51
|
+
### JSON Structure
|
28
52
|
|
29
|
-
```
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
:"
|
372
|
-
:"
|
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
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
:"
|
406
|
-
:"
|
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
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
:"
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
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
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
:"
|
469
|
-
:"
|
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
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
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
|
-
|
559
|
-
|
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
|
-
##
|
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
|
-
|
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.
|