sashite-ggn 0.2.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 +398 -28
- 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 -82
- 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 -11
- data/lib/sashite/ggn/gameplay.rb +0 -9
- data/lib/sashite/ggn/object.rb +0 -9
- data/lib/sashite/ggn/pattern.rb +0 -9
- data/lib/sashite/ggn/square.rb +0 -7
- data/lib/sashite/ggn/state.rb +0 -7
- data/lib/sashite/ggn/subject.rb +0 -9
- data/lib/sashite/ggn/verb.rb +0 -7
- data/sashite-ggn.gemspec +0 -19
- data/test/_test_helper.rb +0 -2
- data/test/test_ggn.rb +0 -15
- data/test/test_ggn_ability.rb +0 -41
- data/test/test_ggn_gameplay.rb +0 -17
- data/test/test_ggn_object.rb +0 -41
- data/test/test_ggn_pattern.rb +0 -17
- data/test/test_ggn_square.rb +0 -41
- data/test/test_ggn_state.rb +0 -29
- data/test/test_ggn_subject.rb +0 -41
- data/test/test_ggn_verb.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3841392373c865dd1e96e3cd8321cb8720767585d815fc16449f05a00c8d0900
|
4
|
+
data.tar.gz: a00ebca510e8af7ff2e77ea8056009ca2198a43d79885a61fd8ea682f6c9aaf3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9cc52a786d043b1e2ed437d58f9c939268d23f97df82211732dacf955964bbc27fbc24a3ac280c5cb63a84e6aeaf7fb22aa88e659832020802ac1e78e949f6b5
|
7
|
+
data.tar.gz: 35a6f1b1d9d0f5878ebef7c443f675a0c15027d0895ca96c92c69c1f9a6864c152b7c58fb25e57cbb5ff50223a007003eb994ddb265d1e0e67707fa5b692a6b3
|
data/LICENSE.md
CHANGED
@@ -1,22 +1,21 @@
|
|
1
|
-
|
1
|
+
# The MIT License
|
2
2
|
|
3
|
-
|
3
|
+
Copyright (c) 2014-2025 Sashité
|
4
4
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
the following conditions:
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
12
11
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
14
|
-
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
15
14
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
OF
|
22
|
-
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,48 +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:
|
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
|
+
}
|
69
|
+
```
|
70
|
+
|
71
|
+
## Basic Usage
|
72
|
+
|
73
|
+
### Loading GGN Data
|
74
|
+
|
75
|
+
Load GGN data from various sources:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
require "sashite-ggn"
|
79
|
+
|
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)
|
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
|
+
```
|
91
|
+
|
92
|
+
### Querying Moves
|
14
93
|
|
15
|
-
|
94
|
+
Navigate through the GGN structure to find specific moves:
|
16
95
|
|
17
|
-
|
96
|
+
```ruby
|
97
|
+
require "sashite-ggn"
|
98
|
+
|
99
|
+
piece_data = Sashite::Ggn.load_file("chess_moves.json")
|
100
|
+
|
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")
|
109
|
+
```
|
110
|
+
|
111
|
+
### Evaluating Move Validity
|
18
112
|
|
19
|
-
|
113
|
+
Check if a move is valid under current board conditions:
|
20
114
|
|
21
|
-
|
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")
|
22
121
|
|
23
|
-
|
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
|
+
}
|
24
128
|
|
25
|
-
|
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
|
+
```
|
26
142
|
|
27
|
-
|
143
|
+
### Handling Captures
|
144
|
+
|
145
|
+
Process moves that capture enemy pieces:
|
28
146
|
|
29
147
|
```ruby
|
30
|
-
require
|
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
|
+
}
|
31
159
|
|
32
|
-
|
33
|
-
state.last_moved_actor = nil
|
34
|
-
state.previous_moves_counter = nil
|
160
|
+
result = engine.where(board_state, {}, "CHESS")
|
35
161
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
40
168
|
```
|
41
169
|
|
42
|
-
|
170
|
+
### Piece Drops (Shogi-style)
|
171
|
+
|
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" }
|
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
|
+
}
|
292
|
+
}
|
293
|
+
```
|
294
|
+
|
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
|
+
}
|
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"
|
333
|
+
}
|
334
|
+
]
|
335
|
+
}
|
336
|
+
}
|
337
|
+
}
|
338
|
+
```
|
339
|
+
|
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
|
+
}
|
357
|
+
```
|
358
|
+
|
359
|
+
## Error Handling
|
360
|
+
|
361
|
+
The library provides comprehensive error handling:
|
362
|
+
|
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
|
383
|
+
|
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
|
396
|
+
```
|
397
|
+
|
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é
|
43
417
|
|
44
|
-
|
45
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
46
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
47
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
48
|
-
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.
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Ggn
|
5
|
+
class Piece
|
6
|
+
class Source
|
7
|
+
class Destination
|
8
|
+
class Engine
|
9
|
+
# Represents the result of a valid pseudo-legal move evaluation.
|
10
|
+
#
|
11
|
+
# A Transition encapsulates the changes that occur when a move is executed:
|
12
|
+
# - Board state changes (pieces moving, appearing, or disappearing)
|
13
|
+
# - Pieces gained in hand (from captures)
|
14
|
+
# - Pieces dropped from hand (for drop moves)
|
15
|
+
#
|
16
|
+
# @example Basic move (pawn advance)
|
17
|
+
# transition = Transition.new(nil, nil, "e2" => nil, "e4" => "CHESS:P")
|
18
|
+
# transition.diff # => { "e2" => nil, "e4" => "CHESS:P" }
|
19
|
+
# transition.gain # => nil
|
20
|
+
# transition.drop # => nil
|
21
|
+
#
|
22
|
+
# @example Capture with piece gain
|
23
|
+
# transition = Transition.new("CHESS:R", nil, "g7" => nil, "h8" => "CHESS:Q")
|
24
|
+
# transition.gain # => "CHESS:R" (captured rook goes to hand)
|
25
|
+
#
|
26
|
+
# @example Piece drop from hand
|
27
|
+
# transition = Transition.new(nil, "SHOGI:P", "5e" => "SHOGI:P")
|
28
|
+
# transition.drop # => "SHOGI:P" (pawn removed from hand)
|
29
|
+
class Transition
|
30
|
+
# @return [Hash<String, String|nil>] Board state changes after the move.
|
31
|
+
# Keys are square labels, values are piece identifiers or nil for empty squares.
|
32
|
+
attr_reader :diff
|
33
|
+
|
34
|
+
# @return [String, nil] Piece identifier added to the current player's hand,
|
35
|
+
# typically from a capture. Nil if no piece is gained.
|
36
|
+
attr_reader :gain
|
37
|
+
|
38
|
+
# @return [String, nil] Piece identifier removed from the current player's hand
|
39
|
+
# for drop moves. Nil if no piece is dropped.
|
40
|
+
attr_reader :drop
|
41
|
+
|
42
|
+
# Creates a new Transition with the specified changes.
|
43
|
+
#
|
44
|
+
# @param gain [String, nil] Piece gained in hand (usually from capture)
|
45
|
+
# @param drop [String, nil] Piece dropped from hand (for drop moves)
|
46
|
+
# @param diff [Hash] Board state changes as keyword arguments.
|
47
|
+
# Keys should be square labels, values should be piece identifiers or nil.
|
48
|
+
#
|
49
|
+
# @example Creating a simple move transition
|
50
|
+
# Transition.new(nil, nil, "e2" => nil, "e4" => "CHESS:P")
|
51
|
+
#
|
52
|
+
# @example Creating a capture transition
|
53
|
+
# Transition.new("CHESS:R", nil, "d4" => nil, "e5" => "CHESS:P")
|
54
|
+
#
|
55
|
+
# @example Creating a drop transition
|
56
|
+
# Transition.new(nil, "SHOGI:P", "3c" => "SHOGI:P")
|
57
|
+
def initialize(gain, drop, **diff)
|
58
|
+
@gain = gain
|
59
|
+
@drop = drop
|
60
|
+
@diff = diff
|
61
|
+
|
62
|
+
freeze
|
63
|
+
end
|
64
|
+
|
65
|
+
# Checks if this transition involves gaining a piece.
|
66
|
+
#
|
67
|
+
# @return [Boolean] true if a piece is gained (typically from capture)
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# transition.gain? # => true if @gain is not nil
|
71
|
+
def gain?
|
72
|
+
!@gain.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Checks if this transition involves dropping a piece from hand.
|
76
|
+
#
|
77
|
+
# @return [Boolean] true if a piece is dropped from hand
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# transition.drop? # => true if @drop is not nil
|
81
|
+
def drop?
|
82
|
+
!@drop.nil?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|