sashite-ggn 0.3.0 → 0.6.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 +4 -4
- data/README.md +73 -339
- data/lib/sashite/ggn/move_validator.rb +208 -0
- data/lib/sashite/ggn/ruleset/source/destination/engine/transition.rb +81 -0
- data/lib/sashite/ggn/ruleset/source/destination/engine.rb +374 -0
- data/lib/sashite/ggn/ruleset/source/destination.rb +111 -0
- data/lib/sashite/ggn/{piece → ruleset}/source.rb +43 -15
- data/lib/sashite/ggn/ruleset.rb +311 -0
- data/lib/sashite/ggn/schema.rb +96 -77
- data/lib/sashite/ggn/validation_error.rb +26 -1
- data/lib/sashite/ggn.rb +36 -35
- data/lib/sashite-ggn.rb +48 -34
- metadata +13 -11
- data/lib/sashite/ggn/piece/source/destination/engine/transition.rb +0 -90
- data/lib/sashite/ggn/piece/source/destination/engine.rb +0 -407
- data/lib/sashite/ggn/piece/source/destination.rb +0 -65
- data/lib/sashite/ggn/piece.rb +0 -77
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6dab117559875291772b7e8f60077f670928755870b39badd3555f4d66fd8357
|
4
|
+
data.tar.gz: 2fe3fd39dbb288dc140b10c4c9fec523d2ce00a7d33d7802d8612b95e2268ed3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 805c62503574bbc3b985cc21cb4dfe7a15e03198d97a5e7f0d91508111fd7290ed7382f93f2db6d653f07593f06a65c2640ac4e027127e0bdd964cdc61edab5c
|
7
|
+
data.tar.gz: fc0330900649313e22511f44d391d5a73befdabe4038aa4744de3f8af12684767c410bd6f8aa7e4a88f961cecc6d7be1223e00cfd9736396d92d7c781fa5abea
|
data/README.md
CHANGED
@@ -1,413 +1,147 @@
|
|
1
1
|
# Ggn.rb
|
2
2
|
|
3
|
-
|
4
|
-
[](https://rubydoc.info/github/sashite/ggn.rb/main)
|
5
|
-

|
6
|
-
[](https://github.com/sashite/ggn.rb/raw/main/LICENSE.md)
|
3
|
+
A Ruby implementation of the General Gameplay Notation (GGN) specification for describing pseudo-legal moves in abstract strategy board games.
|
7
4
|
|
8
|
-
|
5
|
+
[](https://badge.fury.io/rb/sashite-ggn)
|
6
|
+
[](https://github.com/sashite/ggn.rb/actions)
|
9
7
|
|
10
8
|
## What is GGN?
|
11
9
|
|
12
|
-
GGN (General Gameplay Notation) is a rule-agnostic, JSON-based format for
|
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/).
|
13
11
|
|
14
|
-
GGN
|
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:
|
15
13
|
|
16
|
-
|
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
|
17
18
|
|
18
|
-
-
|
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
|
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.
|
22
20
|
|
23
21
|
## Installation
|
24
22
|
|
25
23
|
```ruby
|
26
|
-
|
27
|
-
gem "sashite-ggn"
|
28
|
-
```
|
29
|
-
|
30
|
-
Or install manually:
|
31
|
-
|
32
|
-
```sh
|
33
|
-
gem install sashite-ggn
|
34
|
-
```
|
35
|
-
|
36
|
-
## GGN Format
|
37
|
-
|
38
|
-
A single GGN **entry** answers the question:
|
39
|
-
|
40
|
-
> Can this piece, currently on this square, reach that square?
|
41
|
-
|
42
|
-
It encodes:
|
43
|
-
|
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`)
|
50
|
-
|
51
|
-
### JSON Structure
|
52
|
-
|
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
|
64
|
-
}
|
65
|
-
]
|
66
|
-
}
|
67
|
-
}
|
68
|
-
}
|
24
|
+
gem 'sashite-ggn'
|
69
25
|
```
|
70
26
|
|
71
27
|
## Basic Usage
|
72
28
|
|
73
29
|
### Loading GGN Data
|
74
30
|
|
75
|
-
Load GGN data from various sources:
|
76
|
-
|
77
31
|
```ruby
|
78
32
|
require "sashite-ggn"
|
79
33
|
|
80
|
-
#
|
81
|
-
|
82
|
-
|
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)
|
34
|
+
# Load from file
|
35
|
+
ruleset = Sashite::Ggn.load_file("chess_moves.json")
|
86
36
|
|
87
|
-
#
|
88
|
-
|
89
|
-
|
37
|
+
# Load from string
|
38
|
+
json = '{"CHESS:P": {"e2": {"e4": [{"perform": {"e2": null, "e4": "CHESS:P"}}]}}}'
|
39
|
+
ruleset = Sashite::Ggn.load_string(json)
|
90
40
|
```
|
91
41
|
|
92
|
-
###
|
93
|
-
|
94
|
-
Navigate through the GGN structure to find specific moves:
|
42
|
+
### Evaluating Moves
|
95
43
|
|
96
44
|
```ruby
|
97
|
-
|
45
|
+
# Query specific move
|
46
|
+
engine = ruleset.select("CHESS:P").from("e2").to("e4")
|
98
47
|
|
99
|
-
|
48
|
+
# Define board state
|
49
|
+
board_state = { "e2" => "CHESS:P", "e3" => nil, "e4" => nil }
|
100
50
|
|
101
|
-
#
|
102
|
-
|
103
|
-
|
104
|
-
# Get destinations from a specific source square
|
105
|
-
destinations = source.from("e2")
|
106
|
-
|
107
|
-
# Get the engine for a specific target square
|
108
|
-
engine = destinations.to("e4")
|
51
|
+
# Evaluate move
|
52
|
+
transitions = engine.where(board_state, "CHESS")
|
53
|
+
# => [#<Transition diff={"e2"=>nil, "e4"=>"CHESS:P"}>]
|
109
54
|
```
|
110
55
|
|
111
|
-
###
|
112
|
-
|
113
|
-
Check if a move is valid under current board conditions:
|
56
|
+
### Generating All Moves
|
114
57
|
|
115
58
|
```ruby
|
116
|
-
|
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")
|
59
|
+
# Get all pseudo-legal moves for current position
|
60
|
+
all_moves = ruleset.pseudo_legal_transitions(board_state, "CHESS")
|
121
61
|
|
122
|
-
#
|
123
|
-
|
124
|
-
|
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"
|
62
|
+
# Each move is represented as [actor, origin, target, transitions]
|
63
|
+
all_moves.each do |actor, origin, target, transitions|
|
64
|
+
puts "#{actor}: #{origin} → #{target} (#{transitions.size} variants)"
|
140
65
|
end
|
141
66
|
```
|
142
67
|
|
143
|
-
|
68
|
+
## Move Variants
|
144
69
|
|
145
|
-
|
70
|
+
GGN supports multiple outcomes for a single move (e.g., promotion choices):
|
146
71
|
|
147
72
|
```ruby
|
148
|
-
|
73
|
+
# Chess pawn promotion
|
74
|
+
engine = ruleset.select("CHESS:P").from("e7").to("e8")
|
75
|
+
transitions = engine.where({"e7" => "CHESS:P", "e8" => nil}, "CHESS")
|
149
76
|
|
150
|
-
|
151
|
-
|
152
|
-
|
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)
|
77
|
+
transitions.each do |transition|
|
78
|
+
promoted_piece = transition.diff["e8"]
|
79
|
+
puts "Promote to #{promoted_piece}"
|
167
80
|
end
|
81
|
+
# Output: CHESS:Q, CHESS:R, CHESS:B, CHESS:N
|
168
82
|
```
|
169
83
|
|
170
|
-
|
84
|
+
## Complex Moves
|
171
85
|
|
172
|
-
|
86
|
+
GGN can represent multi-square moves like castling:
|
173
87
|
|
174
88
|
```ruby
|
175
|
-
|
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)
|
89
|
+
# Castling (king and rook move simultaneously)
|
90
|
+
engine = ruleset.select("CHESS:K").from("e1").to("g1")
|
185
91
|
board_state = {
|
186
|
-
"
|
187
|
-
"5a" => nil, "5b" => nil, "5c" => nil, "5d" => nil,
|
188
|
-
"5f" => nil, "5g" => nil, "5h" => nil, "5i" => nil
|
92
|
+
"e1" => "CHESS:K", "f1" => nil, "g1" => nil, "h1" => "CHESS:R"
|
189
93
|
}
|
190
94
|
|
191
|
-
|
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
|
95
|
+
transitions = engine.where(board_state, "CHESS")
|
96
|
+
# => [#<Transition diff={"e1"=>nil, "f1"=>"CHESS:R", "g1"=>"CHESS:K", "h1"=>nil}>]
|
198
97
|
```
|
199
98
|
|
200
|
-
##
|
99
|
+
## Conditional Moves
|
201
100
|
|
202
|
-
|
203
|
-
|
204
|
-
Validate GGN data against the official JSON Schema:
|
101
|
+
GGN supports moves with complex requirements and prevent conditions:
|
205
102
|
|
206
103
|
```ruby
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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" }
|
269
|
-
}
|
270
|
-
]
|
271
|
-
}
|
272
|
-
}
|
273
|
-
}
|
274
|
-
```
|
275
|
-
|
276
|
-
### Sliding Move
|
277
|
-
|
278
|
-
A piece that slides along empty squares:
|
279
|
-
|
280
|
-
```json
|
281
|
-
{
|
282
|
-
"CHESS:R": {
|
283
|
-
"a1": {
|
284
|
-
"a3": [
|
285
|
-
{
|
286
|
-
"require": { "a2": "empty", "a3": "empty" },
|
287
|
-
"perform": { "a1": null, "a3": "CHESS:R" }
|
288
|
-
}
|
289
|
-
]
|
290
|
-
}
|
291
|
-
}
|
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
|
292
108
|
}
|
293
|
-
```
|
294
109
|
|
295
|
-
|
296
|
-
|
297
|
-
A piece capturing an enemy and gaining it in hand:
|
298
|
-
|
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"
|
308
|
-
}
|
309
|
-
]
|
310
|
-
}
|
311
|
-
}
|
312
|
-
}
|
110
|
+
transitions = engine.where(board_state, "CHESS")
|
111
|
+
# => [#<Transition diff={"d5"=>nil, "e5"=>nil, "e6"=>"CHESS:P"}>]
|
313
112
|
```
|
314
113
|
|
315
|
-
|
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"
|
333
|
-
}
|
334
|
-
]
|
335
|
-
}
|
336
|
-
}
|
337
|
-
}
|
338
|
-
```
|
114
|
+
## Validation
|
339
115
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
```json
|
345
|
-
{
|
346
|
-
"CHESS:P": {
|
347
|
-
"g7": {
|
348
|
-
"g8": [
|
349
|
-
{
|
350
|
-
"require": { "g8": "empty" },
|
351
|
-
"perform": { "g7": null, "g8": "CHESS:Q" }
|
352
|
-
}
|
353
|
-
]
|
354
|
-
}
|
355
|
-
}
|
356
|
-
}
|
116
|
+
```ruby
|
117
|
+
# Validate GGN data
|
118
|
+
Sashite::Ggn.validate!(data) # Raises exception on failure
|
119
|
+
Sashite::Ggn.valid?(data) # Returns boolean
|
357
120
|
```
|
358
121
|
|
359
|
-
##
|
360
|
-
|
361
|
-
The library provides comprehensive error handling:
|
362
|
-
|
363
|
-
```ruby
|
364
|
-
require "sashite-ggn"
|
122
|
+
## API Reference
|
365
123
|
|
366
|
-
|
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
|
-
```
|
124
|
+
### Core Classes
|
381
125
|
|
382
|
-
|
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
|
383
131
|
|
384
|
-
|
132
|
+
### Key Methods
|
385
133
|
|
386
|
-
|
387
|
-
|
388
|
-
|
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
|
396
|
-
```
|
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
|
397
137
|
|
398
138
|
## Related Specifications
|
399
139
|
|
400
140
|
GGN works alongside other Sashité specifications:
|
401
141
|
|
402
|
-
-
|
403
|
-
-
|
404
|
-
-
|
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)
|
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
|
411
145
|
|
412
146
|
## License
|
413
147
|
|