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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3841392373c865dd1e96e3cd8321cb8720767585d815fc16449f05a00c8d0900
4
- data.tar.gz: a00ebca510e8af7ff2e77ea8056009ca2198a43d79885a61fd8ea682f6c9aaf3
3
+ metadata.gz: 6dab117559875291772b7e8f60077f670928755870b39badd3555f4d66fd8357
4
+ data.tar.gz: 2fe3fd39dbb288dc140b10c4c9fec523d2ce00a7d33d7802d8612b95e2268ed3
5
5
  SHA512:
6
- metadata.gz: 9cc52a786d043b1e2ed437d58f9c939268d23f97df82211732dacf955964bbc27fbc24a3ac280c5cb63a84e6aeaf7fb22aa88e659832020802ac1e78e949f6b5
7
- data.tar.gz: 35a6f1b1d9d0f5878ebef7c443f675a0c15027d0895ca96c92c69c1f9a6864c152b7c58fb25e57cbb5ff50223a007003eb994ddb265d1e0e67707fa5b692a6b3
6
+ metadata.gz: 805c62503574bbc3b985cc21cb4dfe7a15e03198d97a5e7f0d91508111fd7290ed7382f93f2db6d653f07593f06a65c2640ac4e027127e0bdd964cdc61edab5c
7
+ data.tar.gz: fc0330900649313e22511f44d391d5a73befdabe4038aa4744de3f8af12684767c410bd6f8aa7e4a88f961cecc6d7be1223e00cfd9736396d92d7c781fa5abea
data/README.md CHANGED
@@ -1,413 +1,147 @@
1
1
  # Ggn.rb
2
2
 
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)
3
+ A Ruby implementation of the General Gameplay Notation (GGN) specification for describing pseudo-legal moves in abstract strategy board games.
7
4
 
8
- > **GGN** (General Gameplay Notation) support for the Ruby language.
5
+ [![Gem Version](https://badge.fury.io/rb/sashite-ggn.svg)](https://badge.fury.io/rb/sashite-ggn)
6
+ [![Ruby](https://github.com/sashite/ggn.rb/workflows/Ruby/badge.svg)](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 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.
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 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.
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
- This gem implements the [GGN Specification v1.0.0](https://sashite.dev/documents/ggn/1.0.0/), providing a Ruby interface for:
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
- - 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
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
- # In your Gemfile
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
- # From file
81
- piece_data = Sashite::Ggn.load_file("chess_moves.json")
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
- # 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)
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
- ### Querying Moves
93
-
94
- Navigate through the GGN structure to find specific moves:
42
+ ### Evaluating Moves
95
43
 
96
44
  ```ruby
97
- require "sashite-ggn"
45
+ # Query specific move
46
+ engine = ruleset.select("CHESS:P").from("e2").to("e4")
98
47
 
99
- piece_data = Sashite::Ggn.load_file("chess_moves.json")
48
+ # Define board state
49
+ board_state = { "e2" => "CHESS:P", "e3" => nil, "e4" => nil }
100
50
 
101
- # Select a piece type
102
- source = piece_data.select("CHESS:P")
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
- ### Evaluating Move Validity
112
-
113
- Check if a move is valid under current board conditions:
56
+ ### Generating All Moves
114
57
 
115
58
  ```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")
59
+ # Get all pseudo-legal moves for current position
60
+ all_moves = ruleset.pseudo_legal_transitions(board_state, "CHESS")
121
61
 
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"
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
- ### Handling Captures
68
+ ## Move Variants
144
69
 
145
- Process moves that capture enemy pieces:
70
+ GGN supports multiple outcomes for a single move (e.g., promotion choices):
146
71
 
147
72
  ```ruby
148
- require "sashite-ggn"
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
- # 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)
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
- ### Piece Drops (Shogi-style)
84
+ ## Complex Moves
171
85
 
172
- Handle dropping pieces from hand onto the board:
86
+ GGN can represent multi-square moves like castling:
173
87
 
174
88
  ```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)
89
+ # Castling (king and rook move simultaneously)
90
+ engine = ruleset.select("CHESS:K").from("e1").to("g1")
185
91
  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
92
+ "e1" => "CHESS:K", "f1" => nil, "g1" => nil, "h1" => "CHESS:R"
189
93
  }
190
94
 
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
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
- ## Validation
99
+ ## Conditional Moves
201
100
 
202
- ### Schema Validation
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
- 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" }
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
- ### Capture with Gain
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
- ### 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"
333
- }
334
- ]
335
- }
336
- }
337
- }
338
- ```
114
+ ## Validation
339
115
 
340
- ### Promotion
341
-
342
- A piece moving and changing to a different piece type:
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
- ## Error Handling
360
-
361
- The library provides comprehensive error handling:
362
-
363
- ```ruby
364
- require "sashite-ggn"
122
+ ## API Reference
365
123
 
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
- ```
124
+ ### Core Classes
381
125
 
382
- ## Performance Considerations
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
- For large GGN files or high-frequency operations:
132
+ ### Key Methods
385
133
 
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
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
- - **[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)
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