sashite-gan 3.0.0 → 4.0.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 +323 -89
- data/lib/sashite/gan/actor.rb +185 -0
- data/lib/sashite/gan.rb +66 -54
- data/lib/sashite-gan.rb +12 -1
- metadata +18 -6
- data/lib/sashite/gan/dumper.rb +0 -94
- data/lib/sashite/gan/parser.rb +0 -58
- data/lib/sashite/gan/validator.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5469e58632f24a93eea22a4dd6d48c27028f84a2137469164eae9684274a7af
|
4
|
+
data.tar.gz: fa8fe045e1f5ced019b9bdf33369bc457e94187d7703ed13c7189c040e430502
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 91d7fbc665bce2be60a37921398dea166684efa68ce55e22fd740d7d85907599d8812a491dbd561b73c1bb8aa1c625d453590842adb3edfb86b686e69c1f8ed8
|
7
|
+
data.tar.gz: 7761db8067e89586009fe4c52f06e632e7c153c5a7f370c613abd3066691d3d9b3fe214f71db1de1988a4a0d02df1d841b03a70b9aadb49ad89dcf3b940ef8bd
|
data/README.md
CHANGED
@@ -9,13 +9,9 @@
|
|
9
9
|
|
10
10
|
## What is GAN?
|
11
11
|
|
12
|
-
GAN (General Actor Notation) defines a consistent and rule-agnostic format for
|
12
|
+
GAN (General Actor Notation) defines a consistent and rule-agnostic format for identifying game actors in abstract strategy board games. GAN provides unambiguous identification of pieces by combining Style Name Notation (SNN) with Piece Name Notation (PNN), eliminating collision problems when multiple piece styles are present in the same context.
|
13
13
|
|
14
|
-
This gem implements the [GAN Specification v1.0.0](https://sashite.dev/documents/gan/1.0.0/), providing a Ruby interface for
|
15
|
-
|
16
|
-
- Serializing game actors to GAN strings
|
17
|
-
- Parsing GAN strings into their component parts
|
18
|
-
- Validating GAN strings according to the specification
|
14
|
+
This gem implements the [GAN Specification v1.0.0](https://sashite.dev/documents/gan/1.0.0/), providing a Ruby interface for working with game actors through a clean and modular API that builds upon the existing [sashite-snn](https://rubygems.org/gems/sashite-snn) and [pnn](https://rubygems.org/gems/pnn) gems.
|
19
15
|
|
20
16
|
## Installation
|
21
17
|
|
@@ -32,144 +28,382 @@ gem install sashite-gan
|
|
32
28
|
|
33
29
|
## GAN Format
|
34
30
|
|
35
|
-
A GAN record consists of a
|
31
|
+
A GAN record consists of a style identifier (SNN format), followed by a colon separator, followed by a piece identifier (PNN format):
|
36
32
|
|
37
33
|
```
|
38
|
-
<
|
34
|
+
<style-id>:<piece-id>
|
39
35
|
```
|
40
36
|
|
41
37
|
Where:
|
38
|
+
- `<style-id>` is a Style Name Notation (SNN) identifier conforming to SNN specification
|
39
|
+
- `:` is a literal colon character serving as a separator
|
40
|
+
- `<piece-id>` is a Piece Name Notation (PNN) identifier conforming to PNN specification
|
42
41
|
|
43
|
-
|
44
|
-
- `:` is a literal colon character, serving as a separator.
|
45
|
-
- `<piece-id>` is a piece representation following the PNN specification: `[<prefix>]<letter>[<suffix>]`.
|
42
|
+
## Basic Usage
|
46
43
|
|
47
|
-
|
44
|
+
### Creating Actor Objects
|
48
45
|
|
49
|
-
|
50
|
-
- **Lowercase** game identifiers (e.g., `chess:`) denote pieces belonging to the second player.
|
46
|
+
The primary interface is the `Sashite::Gan::Actor` class, which represents a game actor in GAN format:
|
51
47
|
|
52
|
-
|
48
|
+
```ruby
|
49
|
+
require "sashite/gan"
|
50
|
+
|
51
|
+
# Parse a GAN string into an actor object
|
52
|
+
actor = Sashite::Gan::Actor.parse("CHESS:K")
|
53
|
+
# => #<Sashite::Gan::Actor:0x... @style="CHESS" @piece="K">
|
54
|
+
|
55
|
+
# With piece modifiers
|
56
|
+
enhanced_actor = Sashite::Gan::Actor.parse("SHOGI:+P")
|
57
|
+
# => #<Sashite::Gan::Actor:0x... @style="SHOGI" @piece="+P">
|
58
|
+
|
59
|
+
# Create directly with constructor
|
60
|
+
actor = Sashite::Gan::Actor.new("CHESS", "K")
|
61
|
+
enhanced_actor = Sashite::Gan::Actor.new("SHOGI", "+P")
|
62
|
+
|
63
|
+
# Create with style and piece objects
|
64
|
+
style = Sashite::Snn::Style.new("CHESS")
|
65
|
+
piece = Pnn::Piece.new("K")
|
66
|
+
actor = Sashite::Gan::Actor.new(style, piece)
|
67
|
+
|
68
|
+
# Convenience method
|
69
|
+
actor = Sashite::Gan.actor("CHESS", "K")
|
70
|
+
```
|
71
|
+
|
72
|
+
### Converting to GAN String
|
73
|
+
|
74
|
+
Convert an actor object back to its GAN string representation:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
actor = Sashite::Gan::Actor.parse("CHESS:K")
|
78
|
+
actor.to_s
|
79
|
+
# => "CHESS:K"
|
80
|
+
|
81
|
+
enhanced_actor = Sashite::Gan::Actor.parse("SHOGI:+p'")
|
82
|
+
enhanced_actor.to_s
|
83
|
+
# => "SHOGI:+p'"
|
84
|
+
```
|
85
|
+
|
86
|
+
### Accessing Components
|
53
87
|
|
54
|
-
|
88
|
+
Access the style and piece components of an actor:
|
55
89
|
|
56
|
-
|
90
|
+
```ruby
|
91
|
+
actor = Sashite::Gan::Actor.parse("CHESS:K")
|
92
|
+
|
93
|
+
# Access as strings
|
94
|
+
actor.style_name # => "CHESS"
|
95
|
+
actor.piece_name # => "K"
|
96
|
+
|
97
|
+
# Access as objects
|
98
|
+
actor.style # => #<Sashite::Snn::Style:0x... @identifier="CHESS">
|
99
|
+
actor.piece # => #<Pnn::Piece:0x... @letter="K">
|
100
|
+
|
101
|
+
# Check player associations
|
102
|
+
actor.style.first_player? # => true
|
103
|
+
actor.piece.uppercase? # => true
|
104
|
+
```
|
105
|
+
|
106
|
+
## Casing Combinations and Player Association
|
107
|
+
|
108
|
+
GAN allows all four combinations of case between style and piece identifiers to support dynamic ownership changes:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
# First player's style, first player's piece
|
112
|
+
actor1 = Sashite::Gan::Actor.parse("CHESS:K")
|
113
|
+
actor1.style.first_player? # => true
|
114
|
+
actor1.piece.uppercase? # => true
|
115
|
+
|
116
|
+
# First player's style, second player's piece (piece was captured and converted)
|
117
|
+
actor2 = Sashite::Gan::Actor.parse("CHESS:k")
|
118
|
+
actor2.style.first_player? # => true
|
119
|
+
actor2.piece.lowercase? # => true
|
120
|
+
|
121
|
+
# Second player's style, first player's piece (piece was captured and converted)
|
122
|
+
actor3 = Sashite::Gan::Actor.parse("chess:K")
|
123
|
+
actor3.style.second_player? # => true
|
124
|
+
actor3.piece.uppercase? # => true
|
125
|
+
|
126
|
+
# Second player's style, second player's piece
|
127
|
+
actor4 = Sashite::Gan::Actor.parse("chess:k")
|
128
|
+
actor4.style.second_player? # => true
|
129
|
+
actor4.piece.lowercase? # => true
|
130
|
+
```
|
131
|
+
|
132
|
+
## Dynamic Ownership Changes
|
133
|
+
|
134
|
+
While style assignment remains fixed throughout a game, piece ownership may change during gameplay:
|
57
135
|
|
58
136
|
```ruby
|
59
|
-
|
137
|
+
# Original piece owned by first player
|
138
|
+
original = Sashite::Gan::Actor.parse("SHOGI:P")
|
60
139
|
|
61
|
-
#
|
62
|
-
|
63
|
-
# =>
|
140
|
+
# After capture by second player (modifiers preserved by default)
|
141
|
+
captured = original.change_piece_ownership
|
142
|
+
captured.to_s # => "SHOGI:p"
|
64
143
|
|
65
|
-
#
|
66
|
-
|
67
|
-
# => { game_id: "SHOGI", letter: "P", prefix: "+" }
|
144
|
+
# Or create the captured version directly
|
145
|
+
captured = Sashite::Gan::Actor.new(original.style, "p")
|
68
146
|
|
69
|
-
#
|
70
|
-
|
71
|
-
|
147
|
+
# Example with enhanced piece - modifiers are preserved
|
148
|
+
enhanced = Sashite::Gan::Actor.parse("SHOGI:+P")
|
149
|
+
captured_enhanced = enhanced.change_piece_ownership
|
150
|
+
captured_enhanced.to_s # => "SHOGI:+p" (modifiers preserved)
|
72
151
|
|
73
|
-
#
|
74
|
-
|
75
|
-
# =>
|
152
|
+
# To remove modifiers explicitly (if game rules require it):
|
153
|
+
bare_captured = enhanced.bare_piece.change_piece_ownership
|
154
|
+
bare_captured.to_s # => "SHOGI:p" (modifiers removed)
|
76
155
|
```
|
77
156
|
|
78
|
-
|
157
|
+
## Traditional Same-Style Games
|
79
158
|
|
80
|
-
|
159
|
+
In traditional games where both players use the same piece style:
|
81
160
|
|
82
161
|
```ruby
|
83
|
-
|
162
|
+
# Chess pieces
|
163
|
+
white_king = Sashite::Gan::Actor.parse("CHESS:K")
|
164
|
+
black_king = Sashite::Gan::Actor.parse("chess:k")
|
84
165
|
|
85
|
-
|
86
|
-
|
87
|
-
# => { game_id: "CHESS", letter: "K", suffix: "'" }
|
166
|
+
white_queen = Sashite::Gan::Actor.parse("CHESS:Q")
|
167
|
+
black_queen = Sashite::Gan::Actor.parse("chess:q")
|
88
168
|
|
89
|
-
#
|
90
|
-
|
91
|
-
|
169
|
+
# Shogi pieces
|
170
|
+
first_king = Sashite::Gan::Actor.parse("SHOGI:K")
|
171
|
+
second_king = Sashite::Gan::Actor.parse("shogi:k")
|
172
|
+
|
173
|
+
first_gold = Sashite::Gan::Actor.parse("SHOGI:G")
|
174
|
+
second_gold = Sashite::Gan::Actor.parse("shogi:g")
|
92
175
|
```
|
93
176
|
|
94
|
-
|
177
|
+
## Cross-Style Games
|
95
178
|
|
96
|
-
|
179
|
+
In games where players use different piece styles:
|
97
180
|
|
98
181
|
```ruby
|
99
|
-
|
182
|
+
# Chess vs Makruk
|
183
|
+
chess_king = Sashite::Gan::Actor.parse("CHESS:K")
|
184
|
+
makruk_king = Sashite::Gan::Actor.parse("makruk:k")
|
100
185
|
|
101
|
-
|
102
|
-
Sashite::Gan.
|
103
|
-
|
186
|
+
chess_queen = Sashite::Gan::Actor.parse("CHESS:Q")
|
187
|
+
makruk_queen = Sashite::Gan::Actor.parse("makruk:q")
|
188
|
+
|
189
|
+
# Shogi vs Xiangqi
|
190
|
+
shogi_king = Sashite::Gan::Actor.parse("SHOGI:K")
|
191
|
+
xiangqi_general = Sashite::Gan::Actor.parse("xiangqi:g")
|
192
|
+
|
193
|
+
shogi_gold = Sashite::Gan::Actor.parse("SHOGI:G")
|
194
|
+
xiangqi_advisor = Sashite::Gan::Actor.parse("xiangqi:a")
|
195
|
+
```
|
196
|
+
|
197
|
+
## Pieces with States and Ownership Changes
|
104
198
|
|
105
|
-
|
106
|
-
|
107
|
-
|
199
|
+
```ruby
|
200
|
+
# Original enhanced piece
|
201
|
+
original = Sashite::Gan::Actor.parse("CHESS:R'")
|
202
|
+
|
203
|
+
# After capture (modifiers preserved by default)
|
204
|
+
captured = original.change_piece_ownership
|
205
|
+
captured.to_s # => "chess:R'"
|
206
|
+
|
207
|
+
# If game rules require modifier removal during capture:
|
208
|
+
captured_bare = original.bare_piece.change_piece_ownership
|
209
|
+
captured_bare.to_s # => "chess:R"
|
210
|
+
|
211
|
+
# Promoted shogi piece captured
|
212
|
+
promoted_pawn = Sashite::Gan::Actor.parse("shogi:+p")
|
213
|
+
captured_promoted = promoted_pawn.change_piece_ownership
|
214
|
+
captured_promoted.to_s # => "SHOGI:+p" (modifiers preserved)
|
215
|
+
|
216
|
+
# With explicit modifier removal:
|
217
|
+
captured_demoted = promoted_pawn.bare_piece.change_piece_ownership
|
218
|
+
captured_demoted.to_s # => "SHOGI:p"
|
219
|
+
```
|
220
|
+
|
221
|
+
## Collision Resolution
|
222
|
+
|
223
|
+
GAN resolves naming conflicts between different styles:
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
# All different actors despite similar piece types
|
227
|
+
chess_rook = Sashite::Gan::Actor.parse("CHESS:R")
|
228
|
+
shogi_rook = Sashite::Gan::Actor.parse("SHOGI:R")
|
229
|
+
makruk_rook = Sashite::Gan::Actor.parse("MAKRUK:R")
|
230
|
+
xiangqi_chariot = Sashite::Gan::Actor.parse("xiangqi:r")
|
231
|
+
|
232
|
+
# They can all coexist in the same context
|
233
|
+
pieces = [chess_rook, shogi_rook, makruk_rook, xiangqi_chariot]
|
234
|
+
puts pieces.map(&:to_s)
|
235
|
+
# => ["CHESS:R", "SHOGI:R", "MAKRUK:R", "xiangqi:r"]
|
236
|
+
```
|
237
|
+
|
238
|
+
## Advanced Usage
|
239
|
+
|
240
|
+
### Working with Collections
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
# Group actors by style
|
244
|
+
actors = [
|
245
|
+
Sashite::Gan::Actor.parse("CHESS:K"),
|
246
|
+
Sashite::Gan::Actor.parse("CHESS:Q"),
|
247
|
+
Sashite::Gan::Actor.parse("shogi:k"),
|
248
|
+
Sashite::Gan::Actor.parse("shogi:g")
|
249
|
+
]
|
250
|
+
|
251
|
+
grouped = actors.group_by { |actor| actor.style_name.downcase }
|
252
|
+
# => {"chess" => [...], "shogi" => [...]}
|
253
|
+
|
254
|
+
# Filter by player
|
255
|
+
first_player_actors = actors.select { |actor| actor.style.first_player? }
|
256
|
+
second_player_actors = actors.select { |actor| actor.style.second_player? }
|
257
|
+
|
258
|
+
# Find actors by piece type
|
259
|
+
kings = actors.select { |actor| actor.piece_name.downcase == "k" }
|
260
|
+
```
|
261
|
+
|
262
|
+
### State Manipulation
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
actor = Sashite::Gan::Actor.parse("SHOGI:P")
|
266
|
+
|
267
|
+
# Enhance the piece
|
268
|
+
enhanced = actor.enhance_piece
|
269
|
+
enhanced.to_s # => "SHOGI:+P"
|
270
|
+
|
271
|
+
# Add intermediate state
|
272
|
+
intermediate = actor.set_piece_intermediate
|
273
|
+
intermediate.to_s # => "SHOGI:P'"
|
108
274
|
|
109
|
-
#
|
110
|
-
|
111
|
-
# => "
|
275
|
+
# Chain operations
|
276
|
+
complex = actor.enhance_piece.set_piece_intermediate
|
277
|
+
complex.to_s # => "SHOGI:+P'"
|
112
278
|
|
113
|
-
#
|
114
|
-
|
115
|
-
# => "SHOGI
|
279
|
+
# Remove all modifiers
|
280
|
+
bare = complex.bare_piece
|
281
|
+
bare.to_s # => "SHOGI:P"
|
116
282
|
```
|
117
283
|
|
118
284
|
### Validation
|
119
285
|
|
120
|
-
|
286
|
+
All parsing automatically validates input according to the GAN specification:
|
121
287
|
|
122
288
|
```ruby
|
123
|
-
|
289
|
+
# Valid GAN strings
|
290
|
+
Sashite::Gan::Actor.parse("CHESS:K") # ✓
|
291
|
+
Sashite::Gan::Actor.parse("shogi:+p") # ✓
|
292
|
+
Sashite::Gan::Actor.parse("XIANGQI:r'") # ✓
|
293
|
+
|
294
|
+
# Valid constructor calls
|
295
|
+
Sashite::Gan::Actor.new("CHESS", "K") # ✓
|
296
|
+
Sashite::Gan::Actor.new("shogi", "+p") # ✓
|
297
|
+
|
298
|
+
# Convenience method
|
299
|
+
Sashite::Gan.actor("MAKRUK", "Q") # ✓
|
300
|
+
|
301
|
+
# Check validity
|
302
|
+
Sashite::Gan.valid?("CHESS:K") # => true
|
303
|
+
Sashite::Gan.valid?("Chess:K") # => false (mixed case in style)
|
304
|
+
Sashite::Gan.valid?("CHESS") # => false (missing piece)
|
305
|
+
Sashite::Gan.valid?("") # => false (empty string)
|
306
|
+
|
307
|
+
# Invalid GAN strings raise ArgumentError
|
308
|
+
Sashite::Gan::Actor.parse("") # ✗ ArgumentError
|
309
|
+
Sashite::Gan::Actor.parse("Chess:K") # ✗ ArgumentError (mixed case)
|
310
|
+
Sashite::Gan::Actor.parse("CHESS") # ✗ ArgumentError (missing piece)
|
311
|
+
Sashite::Gan::Actor.parse("CHESS:++K") # ✗ ArgumentError (invalid piece)
|
312
|
+
```
|
124
313
|
|
125
|
-
|
126
|
-
Sashite::Gan.valid?("SHOGI:+P") # => true
|
127
|
-
Sashite::Gan.valid?("CHESS:K'") # => true
|
128
|
-
Sashite::Gan.valid?("chess:k") # => true
|
314
|
+
### Inspection and Debugging
|
129
315
|
|
130
|
-
|
131
|
-
Sashite::Gan.
|
132
|
-
|
133
|
-
|
316
|
+
```ruby
|
317
|
+
actor = Sashite::Gan::Actor.parse("SHOGI:+p'")
|
318
|
+
|
319
|
+
# Get detailed information
|
320
|
+
actor.inspect
|
321
|
+
# => "#<Sashite::Gan::Actor:0x... style=\"SHOGI\" piece=\"+p'\">"
|
322
|
+
|
323
|
+
# Check components
|
324
|
+
actor.style_name # => "SHOGI"
|
325
|
+
actor.piece_name # => "+p'"
|
326
|
+
actor.piece.enhanced? # => true
|
327
|
+
actor.piece.intermediate? # => true
|
134
328
|
```
|
135
329
|
|
136
|
-
##
|
330
|
+
## API Reference
|
331
|
+
|
332
|
+
### Module Methods
|
333
|
+
|
334
|
+
- `Sashite::Gan.valid?(gan_string)` - Check if a string is valid GAN notation
|
335
|
+
- `Sashite::Gan.actor(style, piece)` - Convenience method to create actors
|
336
|
+
|
337
|
+
### Sashite::Gan::Actor Class Methods
|
338
|
+
|
339
|
+
- `Sashite::Gan::Actor.parse(gan_string)` - Parse a GAN string into an actor object
|
340
|
+
- `Sashite::Gan::Actor.new(style, piece)` - Create a new actor instance
|
341
|
+
|
342
|
+
### Instance Methods
|
343
|
+
|
344
|
+
#### Component Access
|
345
|
+
- `#style` - Get the style object (Sashite::Snn::Style)
|
346
|
+
- `#piece` - Get the piece object (Pnn::Piece)
|
347
|
+
- `#style_name` - Get the style name as string
|
348
|
+
- `#piece_name` - Get the piece name as string
|
349
|
+
|
350
|
+
#### Piece State Manipulation
|
351
|
+
- `#enhance_piece` - Create actor with enhanced piece
|
352
|
+
- `#diminish_piece` - Create actor with diminished piece
|
353
|
+
- `#set_piece_intermediate` - Create actor with intermediate piece state
|
354
|
+
- `#bare_piece` - Create actor with piece without modifiers
|
355
|
+
- `#change_piece_ownership` - Create actor with piece ownership flipped
|
356
|
+
|
357
|
+
#### Conversion
|
358
|
+
- `#to_s` - Convert to GAN string representation
|
359
|
+
- `#inspect` - Detailed string representation for debugging
|
360
|
+
|
361
|
+
## Properties of GAN
|
362
|
+
|
363
|
+
* **Rule-agnostic**: GAN does not encode game states, legality, validity, or game-specific conditions
|
364
|
+
* **Unambiguous identification**: Different piece styles can coexist without naming conflicts
|
365
|
+
* **Canonical representation**: Equivalent actors yield identical strings
|
366
|
+
* **Cross-style support**: Enables games where pieces from multiple traditions may be present
|
367
|
+
* **Dynamic ownership**: Supports games where piece ownership can change during gameplay
|
368
|
+
* **Compositional architecture**: Built on independent SNN and PNN specifications
|
369
|
+
|
370
|
+
## Constraints
|
137
371
|
|
138
|
-
|
372
|
+
* GAN supports exactly **two players**
|
373
|
+
* Players are distinguished through the combination of SNN and PNN casing
|
374
|
+
* Style assignment to players remains **fixed throughout a game**
|
375
|
+
* Piece ownership may change during gameplay through casing changes
|
376
|
+
* Both style and piece identifiers must conform to their respective specifications
|
139
377
|
|
140
|
-
|
141
|
-
- **Lowercase** game IDs must have **lowercase** piece letters for the second player
|
378
|
+
## Use Cases
|
142
379
|
|
143
|
-
|
380
|
+
GAN is particularly useful in the following scenarios:
|
144
381
|
|
145
|
-
|
382
|
+
1. **Multi-style environments**: When positions or analyses involve pieces from multiple style traditions
|
383
|
+
2. **Game engine development**: When implementing engines that need to distinguish between similar pieces from different styles while tracking ownership changes
|
384
|
+
3. **Hybrid games**: When creating or analyzing positions from games that combine elements from different piece traditions
|
385
|
+
4. **Database systems**: When storing game data that must avoid naming conflicts between similar pieces from different styles
|
386
|
+
5. **Cross-style analysis**: When comparing or analyzing strategic elements across different piece traditions
|
387
|
+
6. **Capture-conversion games**: When implementing games like shōgi where pieces change ownership and require clear ownership tracking
|
146
388
|
|
147
|
-
|
389
|
+
## Dependencies
|
148
390
|
|
149
|
-
|
150
|
-
|-------|--------------------|--------------------|
|
151
|
-
| `K'` | `CHESS:K'` | `chess:k'` |
|
152
|
-
| `Q` | `CHESS:Q` | `chess:q` |
|
153
|
-
| `R` | `CHESS:R` | `chess:r` |
|
154
|
-
| `B` | `CHESS:B` | `chess:b` |
|
155
|
-
| `N` | `CHESS:N` | `chess:n` |
|
156
|
-
| `P` | `CHESS:P` | `chess:p` |
|
391
|
+
This gem depends on:
|
157
392
|
|
158
|
-
|
393
|
+
- [sashite-snn](https://github.com/sashite/snn.rb) (~> 1.0.0) - Style Name Notation implementation
|
394
|
+
- [pnn](https://github.com/sashite/pnn.rb) (~> 2.0.0) - Piece Name Notation implementation
|
159
395
|
|
160
|
-
|
396
|
+
## Specification
|
161
397
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
| Makruk Rook (white) | `R` | `MAKRUK:R` |
|
166
|
-
| Shogi Rook (sente) | `R` | `SHOGI:R` |
|
167
|
-
| Promoted Shogi Rook (sente) | `+R` | `SHOGI:+R` |
|
398
|
+
- [GAN Specification](https://sashite.dev/documents/gan/1.0.0/)
|
399
|
+
- [SNN Specification](https://sashite.dev/documents/snn/1.0.0/)
|
400
|
+
- [PNN Specification](https://sashite.dev/documents/pnn/1.0.0/)
|
168
401
|
|
169
402
|
## Documentation
|
170
403
|
|
171
|
-
- [
|
172
|
-
- [
|
404
|
+
- [GAN Documentation](https://rubydoc.info/github/sashite/gan.rb/main)
|
405
|
+
- [SNN Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
|
406
|
+
- [PNN Documentation](https://rubydoc.info/github/sashite/pnn.rb/main)
|
173
407
|
|
174
408
|
## License
|
175
409
|
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Gan
|
5
|
+
# Represents a game actor in GAN format
|
6
|
+
#
|
7
|
+
# An actor combines a style identifier (SNN format) with a piece identifier (PNN format)
|
8
|
+
# to create an unambiguous representation of a game piece within its style context.
|
9
|
+
# The casing of both components determines player association and piece ownership:
|
10
|
+
# - Style casing determines which player uses that style tradition (fixed per game)
|
11
|
+
# - Piece casing determines current piece ownership (may change during gameplay)
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # Traditional same-style game
|
15
|
+
# white_king = Sashite::Gan::Actor.new("CHESS", "K") # First player's chess king
|
16
|
+
# black_king = Sashite::Gan::Actor.new("chess", "k") # Second player's chess king
|
17
|
+
#
|
18
|
+
# # Cross-style game
|
19
|
+
# chess_king = Sashite::Gan::Actor.new("CHESS", "K") # First player uses chess
|
20
|
+
# shogi_king = Sashite::Gan::Actor.new("shogi", "k") # Second player uses shogi
|
21
|
+
#
|
22
|
+
# # Dynamic ownership (piece captured and converted)
|
23
|
+
# captured = Sashite::Gan::Actor.new("CHESS", "k") # Chess piece owned by second player
|
24
|
+
class Actor
|
25
|
+
# @return [Sashite::Snn::Style] The style component
|
26
|
+
attr_reader :style
|
27
|
+
|
28
|
+
# @return [Pnn::Piece] The piece component
|
29
|
+
attr_reader :piece
|
30
|
+
|
31
|
+
# Create a new actor instance
|
32
|
+
#
|
33
|
+
# @param style [String, Sashite::Snn::Style] The style identifier or style object
|
34
|
+
# @param piece [String, Pnn::Piece] The piece identifier or piece object
|
35
|
+
# @raise [ArgumentError] if the parameters are invalid
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# # With strings
|
39
|
+
# actor = Sashite::Gan::Actor.new("CHESS", "K")
|
40
|
+
#
|
41
|
+
# # With objects
|
42
|
+
# style = Sashite::Snn::Style.new("CHESS")
|
43
|
+
# piece = Pnn::Piece.new("K")
|
44
|
+
# actor = Sashite::Gan::Actor.new(style, piece)
|
45
|
+
def initialize(style, piece)
|
46
|
+
@style = style.is_a?(Snn::Style) ? style : Snn::Style.new(style.to_s)
|
47
|
+
@piece = piece.is_a?(Pnn::Piece) ? piece : Pnn::Piece.parse(piece.to_s)
|
48
|
+
|
49
|
+
freeze
|
50
|
+
end
|
51
|
+
|
52
|
+
# Parse a GAN string into an actor object
|
53
|
+
#
|
54
|
+
# @param gan_string [String] GAN notation string
|
55
|
+
# @return [Actor] new actor instance
|
56
|
+
# @raise [ArgumentError] if the GAN string is invalid
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# actor = Sashite::Gan::Actor.parse("CHESS:K")
|
60
|
+
# # => #<Sashite::Gan::Actor:0x... style="CHESS" piece="K">
|
61
|
+
#
|
62
|
+
# enhanced = Sashite::Gan::Actor.parse("SHOGI:+p'")
|
63
|
+
# # => #<Sashite::Gan::Actor:0x... style="SHOGI" piece="+p'">
|
64
|
+
def self.parse(gan_string)
|
65
|
+
style_string, piece_string = Gan.parse_components(gan_string)
|
66
|
+
new(style_string, piece_string)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Convert the actor to its GAN string representation
|
70
|
+
#
|
71
|
+
# @return [String] GAN notation string
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# actor.to_s # => "CHESS:K"
|
75
|
+
def to_s
|
76
|
+
"#{style}:#{piece}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get the style name as a string
|
80
|
+
#
|
81
|
+
# @return [String] The style identifier string
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# actor.style_name # => "CHESS"
|
85
|
+
def style_name
|
86
|
+
style.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get the piece name as a string
|
90
|
+
#
|
91
|
+
# @return [String] The piece identifier string
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# actor.piece_name # => "K"
|
95
|
+
def piece_name
|
96
|
+
piece.to_s
|
97
|
+
end
|
98
|
+
|
99
|
+
# Create a new actor with an enhanced piece
|
100
|
+
#
|
101
|
+
# @return [Actor] new actor instance with enhanced piece
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# actor.enhance_piece # SHOGI:P => SHOGI:+P
|
105
|
+
def enhance_piece
|
106
|
+
self.class.new(style, piece.enhance)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Create a new actor with a diminished piece
|
110
|
+
#
|
111
|
+
# @return [Actor] new actor instance with diminished piece
|
112
|
+
#
|
113
|
+
# @example
|
114
|
+
# actor.diminish_piece # CHESS:R => CHESS:-R
|
115
|
+
def diminish_piece
|
116
|
+
self.class.new(style, piece.diminish)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Create a new actor with an intermediate piece state
|
120
|
+
#
|
121
|
+
# @return [Actor] new actor instance with intermediate piece
|
122
|
+
#
|
123
|
+
# @example
|
124
|
+
# actor.set_piece_intermediate # CHESS:R => CHESS:R'
|
125
|
+
def set_piece_intermediate
|
126
|
+
self.class.new(style, piece.intermediate)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Create a new actor with a piece without modifiers
|
130
|
+
#
|
131
|
+
# @return [Actor] new actor instance with bare piece
|
132
|
+
#
|
133
|
+
# @example
|
134
|
+
# actor.bare_piece # SHOGI:+P' => SHOGI:P
|
135
|
+
def bare_piece
|
136
|
+
self.class.new(style, piece.bare)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Create a new actor with piece ownership flipped
|
140
|
+
#
|
141
|
+
# Changes the piece ownership (case) while keeping the style unchanged.
|
142
|
+
# This method is rule-agnostic and preserves all piece modifiers.
|
143
|
+
# If modifier removal is needed, it should be done explicitly.
|
144
|
+
#
|
145
|
+
# @return [Actor] new actor instance with ownership changed
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# actor.change_piece_ownership # SHOGI:P => SHOGI:p
|
149
|
+
# enhanced.change_piece_ownership # SHOGI:+P => SHOGI:+p (modifiers preserved)
|
150
|
+
#
|
151
|
+
# # To remove modifiers explicitly:
|
152
|
+
# actor.bare_piece.change_piece_ownership # SHOGI:+P => SHOGI:p
|
153
|
+
# # or
|
154
|
+
# actor.change_piece_ownership.bare_piece # SHOGI:+P => SHOGI:p
|
155
|
+
def change_piece_ownership
|
156
|
+
self.class.new(style, piece.flip)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Custom equality comparison
|
160
|
+
#
|
161
|
+
# @param other [Object] The object to compare with
|
162
|
+
# @return [Boolean] true if both objects are Actor instances with the same components
|
163
|
+
def ==(other)
|
164
|
+
other.is_a?(Actor) && style == other.style && piece == other.piece
|
165
|
+
end
|
166
|
+
|
167
|
+
# Alias for equality comparison
|
168
|
+
alias eql? ==
|
169
|
+
|
170
|
+
# Hash code for use in hashes and sets
|
171
|
+
#
|
172
|
+
# @return [Integer] The hash code
|
173
|
+
def hash
|
174
|
+
[self.class, style, piece].hash
|
175
|
+
end
|
176
|
+
|
177
|
+
# String representation for debugging
|
178
|
+
#
|
179
|
+
# @return [String] A detailed string representation
|
180
|
+
def inspect
|
181
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} style=#{style_name.inspect} piece=#{piece_name.inspect}>"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
data/lib/sashite/gan.rb
CHANGED
@@ -1,76 +1,88 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
require_relative
|
3
|
+
require "sashite/snn"
|
4
|
+
require "pnn"
|
5
|
+
require_relative "gan/actor"
|
6
6
|
|
7
7
|
module Sashite
|
8
|
-
#
|
9
|
-
# deserialization of game actors in GAN format.
|
8
|
+
# General Actor Notation (GAN) module
|
10
9
|
#
|
11
|
-
# GAN
|
12
|
-
#
|
13
|
-
#
|
10
|
+
# GAN provides a consistent and rule-agnostic format for identifying game actors
|
11
|
+
# in abstract strategy board games. It combines Style Name Notation (SNN) with
|
12
|
+
# Piece Name Notation (PNN) to create unambiguous actor identification that
|
13
|
+
# eliminates collision problems when multiple piece styles are present.
|
14
14
|
#
|
15
|
-
# @see https://sashite.dev/documents/gan/1.0.0/
|
15
|
+
# @see https://sashite.dev/documents/gan/1.0.0/ GAN Specification v1.0.0
|
16
16
|
module Gan
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
# @param piece_params [Hash] Piece parameters as accepted by Pnn.dump
|
21
|
-
# @option piece_params [String] :letter The single ASCII letter identifier (required)
|
22
|
-
# @option piece_params [String, nil] :prefix Optional prefix modifier for the piece ("+", "-")
|
23
|
-
# @option piece_params [String, nil] :suffix Optional suffix modifier for the piece ("'")
|
24
|
-
# @return [String] GAN notation string
|
25
|
-
# @raise [ArgumentError] If any parameter is invalid
|
26
|
-
# @example
|
27
|
-
# Sashite::Gan.dump(game_id: "CHESS", letter: "K", suffix: "'")
|
28
|
-
# # => "CHESS:K'"
|
29
|
-
def self.dump(game_id:, **piece_params)
|
30
|
-
Dumper.dump(game_id:, **piece_params)
|
31
|
-
end
|
17
|
+
# GAN validation regular expression
|
18
|
+
# Matches: <snn>:<pnn> where snn and pnn follow their respective specifications
|
19
|
+
VALIDATION_REGEX = /\A([A-Z][A-Z0-9]*|[a-z][a-z0-9]*):[-+]?[a-zA-Z]'?\z/
|
32
20
|
|
33
|
-
#
|
21
|
+
# Check if a string is valid GAN notation
|
22
|
+
#
|
23
|
+
# @param gan_string [String] The string to validate
|
24
|
+
# @return [Boolean] true if the string is valid GAN notation, false otherwise
|
34
25
|
#
|
35
|
-
# @param gan_string [String] GAN notation string
|
36
|
-
# @return [Hash] Hash containing the parsed actor data with the following keys:
|
37
|
-
# - :game_id [String] - The game identifier
|
38
|
-
# - :letter [String] - The base letter identifier
|
39
|
-
# - :prefix [String, nil] - The prefix modifier if present
|
40
|
-
# - :suffix [String, nil] - The suffix modifier if present
|
41
|
-
# @raise [ArgumentError] If the GAN string is invalid
|
42
26
|
# @example
|
43
|
-
# Sashite::Gan.
|
44
|
-
# # =>
|
45
|
-
|
46
|
-
|
27
|
+
# Sashite::Gan.valid?("CHESS:K") # => true
|
28
|
+
# Sashite::Gan.valid?("shogi:+p'") # => true
|
29
|
+
# Sashite::Gan.valid?("Chess:K") # => false (mixed case in style)
|
30
|
+
# Sashite::Gan.valid?("CHESS") # => false (missing piece)
|
31
|
+
# Sashite::Gan.valid?("") # => false (empty string)
|
32
|
+
def self.valid?(gan_string)
|
33
|
+
return false unless gan_string.is_a?(String)
|
34
|
+
return false if gan_string.empty?
|
35
|
+
|
36
|
+
# Quick regex check first
|
37
|
+
return false unless VALIDATION_REGEX.match?(gan_string)
|
38
|
+
|
39
|
+
# Split and validate components individually for more precise validation
|
40
|
+
parts = gan_string.split(":", 2)
|
41
|
+
return false unless parts.length == 2
|
42
|
+
|
43
|
+
style_part, piece_part = parts
|
44
|
+
|
45
|
+
# Validate SNN and PNN components using their respective libraries
|
46
|
+
Snn.valid?(style_part) && Pnn.valid?(piece_part)
|
47
47
|
end
|
48
48
|
|
49
|
-
#
|
49
|
+
# Convenience method to create an actor object
|
50
|
+
#
|
51
|
+
# @param style [String, Sashite::Snn::Style] The style identifier or style object
|
52
|
+
# @param piece [String, Pnn::Piece] The piece identifier or piece object
|
53
|
+
# @return [Sashite::Gan::Actor] A new actor object
|
54
|
+
# @raise [ArgumentError] if the parameters are invalid
|
50
55
|
#
|
51
|
-
# @param gan_string [String] GAN notation string
|
52
|
-
# @return [Hash, nil] Hash containing the parsed actor data or nil if parsing fails
|
53
56
|
# @example
|
54
|
-
#
|
55
|
-
# Sashite::Gan
|
56
|
-
# # => { game_id: "CHESS", letter: "K", suffix: "'" }
|
57
|
+
# actor = Sashite::Gan.actor("CHESS", "K")
|
58
|
+
# # => #<Sashite::Gan::Actor:0x... style="CHESS" piece="K">
|
57
59
|
#
|
58
|
-
# #
|
59
|
-
# Sashite::
|
60
|
-
#
|
61
|
-
|
62
|
-
|
60
|
+
# # With objects
|
61
|
+
# style = Sashite::Snn::Style.new("CHESS")
|
62
|
+
# piece = Pnn::Piece.new("K")
|
63
|
+
# actor = Sashite::Gan.actor(style, piece)
|
64
|
+
def self.actor(style, piece)
|
65
|
+
Actor.new(style, piece)
|
63
66
|
end
|
64
67
|
|
65
|
-
#
|
68
|
+
# Parse a GAN string into component parts
|
69
|
+
#
|
70
|
+
# @param gan_string [String] The GAN string to parse
|
71
|
+
# @return [Array<String>] An array containing [style_string, piece_string]
|
72
|
+
# @raise [ArgumentError] if the string is invalid GAN notation
|
66
73
|
#
|
67
|
-
# @param gan_string [String] GAN string to validate
|
68
|
-
# @return [Boolean] True if the string is a valid GAN string
|
69
74
|
# @example
|
70
|
-
# Sashite::Gan.
|
71
|
-
#
|
72
|
-
|
73
|
-
|
75
|
+
# Sashite::Gan.parse_components("CHESS:K")
|
76
|
+
# # => ["CHESS", "K"]
|
77
|
+
#
|
78
|
+
# Sashite::Gan.parse_components("shogi:+p'")
|
79
|
+
# # => ["shogi", "+p'"]
|
80
|
+
#
|
81
|
+
# @api private
|
82
|
+
def self.parse_components(gan_string)
|
83
|
+
raise ArgumentError, "Invalid GAN format: #{gan_string.inspect}" unless valid?(gan_string)
|
84
|
+
|
85
|
+
gan_string.split(":", 2)
|
74
86
|
end
|
75
87
|
end
|
76
88
|
end
|
data/lib/sashite-gan.rb
CHANGED
@@ -1,7 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Sashité namespace
|
3
|
+
# Sashité namespace for board game notation libraries
|
4
4
|
module Sashite
|
5
|
+
# General Actor Notation (GAN) implementation for Ruby
|
6
|
+
#
|
7
|
+
# GAN defines a consistent and rule-agnostic format for identifying game actors
|
8
|
+
# in abstract strategy board games. GAN provides unambiguous identification of
|
9
|
+
# pieces by combining Style Name Notation (SNN) with Piece Name Notation (PNN),
|
10
|
+
# eliminating collision problems when multiple piece styles are present in the
|
11
|
+
# same context.
|
12
|
+
#
|
13
|
+
# @see https://sashite.dev/documents/gan/1.0.0/ GAN Specification v1.0.0
|
14
|
+
# @author Sashité
|
15
|
+
# @since 1.0.0
|
5
16
|
end
|
6
17
|
|
7
18
|
require_relative "sashite/gan"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sashite-gan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -15,14 +15,28 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version:
|
18
|
+
version: 2.0.0
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version:
|
25
|
+
version: 2.0.0
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: sashite-snn
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.0.0
|
26
40
|
description: A Ruby interface for serialization and deserialization of game actors
|
27
41
|
in GAN format. GAN is a consistent and rule-agnostic format for representing game
|
28
42
|
actors in abstract strategy board games, providing a standardized way to identify
|
@@ -36,9 +50,7 @@ files:
|
|
36
50
|
- README.md
|
37
51
|
- lib/sashite-gan.rb
|
38
52
|
- lib/sashite/gan.rb
|
39
|
-
- lib/sashite/gan/
|
40
|
-
- lib/sashite/gan/parser.rb
|
41
|
-
- lib/sashite/gan/validator.rb
|
53
|
+
- lib/sashite/gan/actor.rb
|
42
54
|
homepage: https://github.com/sashite/gan.rb
|
43
55
|
licenses:
|
44
56
|
- MIT
|
data/lib/sashite/gan/dumper.rb
DELETED
@@ -1,94 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "pnn"
|
4
|
-
|
5
|
-
module Sashite
|
6
|
-
module Gan
|
7
|
-
# Serializes actor components into GAN (General Actor Notation) strings.
|
8
|
-
#
|
9
|
-
# The dumper transforms piece data and game identifiers into properly
|
10
|
-
# formatted GAN strings, ensuring consistency between game ID casing
|
11
|
-
# and piece letter casing according to the GAN specification.
|
12
|
-
#
|
13
|
-
# According to the specification, game IDs must be either all uppercase
|
14
|
-
# or all lowercase, and their casing must match the casing of the piece letter.
|
15
|
-
class Dumper
|
16
|
-
# Pattern for validating game identifiers - must be all uppercase OR all lowercase
|
17
|
-
GAME_ID_PATTERN = /\A([A-Z]+|[a-z]+)\z/
|
18
|
-
|
19
|
-
# Error message templates
|
20
|
-
INVALID_GAME_ID_ERROR = "Game ID must be a non-empty string containing only ASCII letters and must be either all uppercase or all lowercase: %s"
|
21
|
-
CASING_MISMATCH_ERROR = "Game ID casing (%s) must match piece letter casing (%s)"
|
22
|
-
|
23
|
-
# Serializes actor components into a GAN string
|
24
|
-
#
|
25
|
-
# @param game_id [String] The game identifier (e.g., "CHESS", "shogi")
|
26
|
-
# @param piece_params [Hash] Piece parameters as accepted by Pnn.dump:
|
27
|
-
# @option piece_params [String] :letter The single ASCII letter identifier (required)
|
28
|
-
# @option piece_params [String, nil] :prefix Optional prefix modifier for the piece ("+", "-")
|
29
|
-
# @option piece_params [String, nil] :suffix Optional suffix modifier for the piece ("'")
|
30
|
-
# @return [String] A properly formatted GAN notation string (e.g., "CHESS:K'")
|
31
|
-
# @raise [ArgumentError] If game_id is invalid or casing is inconsistent with piece letter
|
32
|
-
# @example Create a GAN string for a white chess king with castling rights
|
33
|
-
# Dumper.dump(game_id: "CHESS", letter: "K", suffix: "'")
|
34
|
-
# # => "CHESS:K'"
|
35
|
-
# @example Create a GAN string for a promoted shogi pawn
|
36
|
-
# Dumper.dump(game_id: "SHOGI", letter: "P", prefix: "+")
|
37
|
-
# # => "SHOGI:+P"
|
38
|
-
def self.dump(game_id:, **piece_params)
|
39
|
-
game_id = String(game_id)
|
40
|
-
validate_game_id!(game_id)
|
41
|
-
|
42
|
-
# Build the piece string using the PNN gem
|
43
|
-
pnn_string = ::Pnn.dump(**piece_params)
|
44
|
-
|
45
|
-
# Verify casing consistency
|
46
|
-
validate_casing_consistency!(game_id, pnn_string)
|
47
|
-
|
48
|
-
"#{game_id}:#{pnn_string}"
|
49
|
-
end
|
50
|
-
|
51
|
-
# @api private
|
52
|
-
# Validates that the game_id contains only ASCII letters
|
53
|
-
#
|
54
|
-
# @param game_id [String] The game identifier to validate
|
55
|
-
# @return [void]
|
56
|
-
# @raise [ArgumentError] If game_id contains non-letter characters
|
57
|
-
def self.validate_game_id!(game_id)
|
58
|
-
return if game_id.match?(GAME_ID_PATTERN)
|
59
|
-
|
60
|
-
raise ::ArgumentError, format(INVALID_GAME_ID_ERROR, game_id)
|
61
|
-
end
|
62
|
-
private_class_method :validate_game_id!
|
63
|
-
|
64
|
-
# @api private
|
65
|
-
# Validates that the casing of the game_id is consistent with the piece letter
|
66
|
-
#
|
67
|
-
# According to GAN specification, if game_id is uppercase, piece letter must be uppercase,
|
68
|
-
# and if game_id is lowercase, piece letter must be lowercase.
|
69
|
-
#
|
70
|
-
# @param game_id [String] The game identifier
|
71
|
-
# @param pnn_string [String] The PNN string
|
72
|
-
# @return [void]
|
73
|
-
# @raise [ArgumentError] If casing is inconsistent
|
74
|
-
def self.validate_casing_consistency!(game_id, pnn_string)
|
75
|
-
return if casing_consistent?(game_id, pnn_string)
|
76
|
-
|
77
|
-
raise ::ArgumentError, format(CASING_MISMATCH_ERROR, game_id, pnn_string)
|
78
|
-
end
|
79
|
-
private_class_method :validate_casing_consistency!
|
80
|
-
|
81
|
-
# @api private
|
82
|
-
# Verifies that the casing of the game_id matches the casing of the piece letter
|
83
|
-
#
|
84
|
-
# @param game_id [String] The game identifier
|
85
|
-
# @param pnn_string [String] The PNN string
|
86
|
-
# @return [Boolean] True if casing is consistent
|
87
|
-
def self.casing_consistent?(game_id, pnn_string)
|
88
|
-
# Both must be uppercase or both must be lowercase
|
89
|
-
(game_id == game_id.upcase) == (pnn_string == pnn_string.upcase)
|
90
|
-
end
|
91
|
-
private_class_method :casing_consistent?
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
data/lib/sashite/gan/parser.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "pnn"
|
4
|
-
|
5
|
-
module Sashite
|
6
|
-
module Gan
|
7
|
-
# Parses GAN strings into their component parts
|
8
|
-
class Parser
|
9
|
-
# GAN regex pattern for parsing
|
10
|
-
PATTERN = /\A(?<game_id>[a-zA-Z]+):(?<pnn_part>[-+]?[a-zA-Z][']?)\z/
|
11
|
-
|
12
|
-
# Parse a GAN string into its components
|
13
|
-
#
|
14
|
-
# @param gan_string [String] The GAN string to parse
|
15
|
-
# @return [Hash] Hash containing the parsed components
|
16
|
-
# @raise [ArgumentError] If the GAN string is invalid
|
17
|
-
def self.parse(gan_string)
|
18
|
-
gan_string = String(gan_string)
|
19
|
-
|
20
|
-
matches = PATTERN.match(gan_string)
|
21
|
-
raise ArgumentError, "Invalid GAN string: #{gan_string}" if matches.nil?
|
22
|
-
|
23
|
-
game_id = matches[:game_id]
|
24
|
-
pnn_part = matches[:pnn_part]
|
25
|
-
|
26
|
-
# Parse the PNN part using the PNN gem
|
27
|
-
pnn_result = Pnn.parse(pnn_part)
|
28
|
-
|
29
|
-
# Verify casing consistency
|
30
|
-
unless casing_consistent?(game_id, pnn_result[:letter])
|
31
|
-
raise ArgumentError, "Game ID casing (#{game_id}) must match piece letter casing (#{pnn_result[:letter]})"
|
32
|
-
end
|
33
|
-
|
34
|
-
# Merge the game_id with the piece parameters for a flatter structure
|
35
|
-
{ game_id: game_id }.merge(pnn_result)
|
36
|
-
end
|
37
|
-
|
38
|
-
# Safely parse a GAN string without raising exceptions
|
39
|
-
#
|
40
|
-
# @param gan_string [String] The GAN string to parse
|
41
|
-
# @return [Hash, nil] Hash containing the parsed components or nil if invalid
|
42
|
-
def self.safe_parse(gan_string)
|
43
|
-
parse(gan_string)
|
44
|
-
rescue ArgumentError
|
45
|
-
nil
|
46
|
-
end
|
47
|
-
|
48
|
-
# Verifies that the casing of the game_id matches the casing of the piece letter
|
49
|
-
#
|
50
|
-
# @param game_id [String] The game identifier
|
51
|
-
# @param letter [String] The piece letter
|
52
|
-
# @return [Boolean] True if casing is consistent
|
53
|
-
def self.casing_consistent?(game_id, letter)
|
54
|
-
(game_id == game_id.upcase) == (letter == letter.upcase)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "pnn"
|
4
|
-
|
5
|
-
module Sashite
|
6
|
-
module Gan
|
7
|
-
# Validates GAN strings
|
8
|
-
class Validator
|
9
|
-
# GAN regex pattern for validation
|
10
|
-
PATTERN = /\A([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)\z/
|
11
|
-
|
12
|
-
# Validates if the given string is a valid GAN string
|
13
|
-
#
|
14
|
-
# @param gan_string [String] The GAN string to validate
|
15
|
-
# @return [Boolean] True if the string is a valid GAN string
|
16
|
-
def self.valid?(gan_string)
|
17
|
-
return false unless gan_string.is_a?(String)
|
18
|
-
|
19
|
-
PATTERN.match?(gan_string)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|