sashite-gan 3.0.0 → 5.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 +623 -95
- data/lib/sashite/gan/actor.rb +507 -0
- data/lib/sashite/gan.rb +57 -57
- data/lib/sashite-gan.rb +10 -3
- metadata +29 -14
- data/lib/sashite/gan/dumper.rb +0 -94
- data/lib/sashite/gan/parser.rb +0 -58
- data/lib/sashite/gan/validator.rb +0 -23
data/README.md
CHANGED
@@ -5,17 +5,19 @@
|
|
5
5
|

|
6
6
|
[](https://github.com/sashite/gan.rb/raw/main/LICENSE.md)
|
7
7
|
|
8
|
-
> **GAN** (General Actor Notation)
|
8
|
+
> **GAN** (General Actor Notation) implementation for the Ruby language.
|
9
9
|
|
10
10
|
## What is GAN?
|
11
11
|
|
12
|
-
GAN (General Actor Notation)
|
12
|
+
GAN (General Actor Notation) provides a rule-agnostic format for identifying game actors in abstract strategy board games by combining [Style Name Notation (SNN)](https://sashite.dev/specs/snn/1.0.0/) and [Piece Identifier Notation (PIN)](https://sashite.dev/specs/pin/1.0.0/) with a colon separator and consistent case encoding.
|
13
13
|
|
14
|
-
|
14
|
+
GAN represents **all four fundamental piece attributes** from the [Game Protocol](https://sashite.dev/game-protocol/):
|
15
|
+
- **Type** → PIN component (ASCII letter choice)
|
16
|
+
- **Side** → Consistent case encoding across both SNN and PIN components
|
17
|
+
- **State** → PIN component (optional prefix modifier)
|
18
|
+
- **Style** → SNN component (explicit style identifier)
|
15
19
|
|
16
|
-
|
17
|
-
- Parsing GAN strings into their component parts
|
18
|
-
- Validating GAN strings according to the specification
|
20
|
+
This gem implements the [GAN Specification v1.0.0](https://sashite.dev/specs/gan/1.0.0/), providing a modern Ruby interface with immutable actor objects and functional programming principles built upon the [sashite-snn](https://rubygems.org/gems/sashite-snn) and [sashite-pin](https://rubygems.org/gems/sashite-pin) gems.
|
19
21
|
|
20
22
|
## Installation
|
21
23
|
|
@@ -30,151 +32,677 @@ Or install manually:
|
|
30
32
|
gem install sashite-gan
|
31
33
|
```
|
32
34
|
|
33
|
-
##
|
35
|
+
## Usage
|
34
36
|
|
35
|
-
|
37
|
+
```ruby
|
38
|
+
require "sashite/gan"
|
39
|
+
|
40
|
+
# Parse GAN strings into actor objects
|
41
|
+
actor = Sashite::Gan.parse("CHESS:K") # => #<Gan::Actor name=:Chess type=:K side=:first state=:normal>
|
42
|
+
actor.to_s # => "CHESS:K"
|
43
|
+
actor.name # => :Chess
|
44
|
+
actor.type # => :K
|
45
|
+
actor.side # => :first
|
46
|
+
actor.state # => :normal
|
47
|
+
|
48
|
+
# Extract individual components
|
49
|
+
actor.to_snn # => "CHESS"
|
50
|
+
actor.to_pin # => "K"
|
51
|
+
|
52
|
+
# Create actors directly
|
53
|
+
actor = Sashite::Gan.actor(:Chess, :K, :first, :normal) # => #<Gan::Actor name=:Chess type=:K side=:first state=:normal>
|
54
|
+
actor = Sashite::Gan::Actor.new(:Shogi, :P, :second, :enhanced) # => #<Gan::Actor name=:Shogi type=:P side=:second state=:enhanced>
|
55
|
+
|
56
|
+
# Validate GAN strings
|
57
|
+
Sashite::Gan.valid?("CHESS:K") # => true
|
58
|
+
Sashite::Gan.valid?("shogi:+p") # => true
|
59
|
+
Sashite::Gan.valid?("Chess:K") # => false (mixed case)
|
60
|
+
Sashite::Gan.valid?("CHESS") # => false (missing piece)
|
61
|
+
|
62
|
+
# Class-level validation (same as module method)
|
63
|
+
Sashite::Gan::Actor.valid?("CHESS:K") # => true
|
64
|
+
Sashite::Gan::Actor.valid?("chess:k") # => true
|
65
|
+
Sashite::Gan::Actor.valid?("Chess:K") # => false (mixed case)
|
66
|
+
Sashite::Gan::Actor.valid?("CHESS:k") # => false (case mismatch)
|
67
|
+
|
68
|
+
# State manipulation (returns new immutable instances)
|
69
|
+
enhanced = actor.enhance # => #<Gan::Actor name=:Chess type=:K side=:first state=:enhanced>
|
70
|
+
enhanced.to_s # => "CHESS:+K"
|
71
|
+
enhanced.to_pin # => "+K"
|
72
|
+
diminished = actor.diminish # => #<Gan::Actor name=:Chess type=:K side=:first state=:diminished>
|
73
|
+
diminished.to_s # => "CHESS:-K"
|
74
|
+
diminished.to_pin # => "-K"
|
75
|
+
|
76
|
+
# Side manipulation
|
77
|
+
flipped = actor.flip # => #<Gan::Actor name=:Chess type=:K side=:second state=:normal>
|
78
|
+
flipped.to_s # => "chess:k"
|
79
|
+
flipped.to_snn # => "chess"
|
80
|
+
flipped.to_pin # => "k"
|
81
|
+
|
82
|
+
# Style manipulation
|
83
|
+
shogi_actor = actor.with_name(:Shogi) # => #<Gan::Actor name=:Shogi type=:K side=:first state=:normal>
|
84
|
+
shogi_actor.to_s # => "SHOGI:K"
|
85
|
+
shogi_actor.to_snn # => "SHOGI"
|
86
|
+
|
87
|
+
# Type manipulation
|
88
|
+
queen = actor.with_type(:Q) # => #<Gan::Actor name=:Chess type=:Q side=:first state=:normal>
|
89
|
+
queen.to_s # => "CHESS:Q"
|
90
|
+
queen.to_pin # => "Q"
|
91
|
+
|
92
|
+
# State queries
|
93
|
+
actor.normal? # => true
|
94
|
+
enhanced.enhanced? # => true
|
95
|
+
diminished.diminished? # => true
|
96
|
+
|
97
|
+
# Side queries
|
98
|
+
actor.first_player? # => true
|
99
|
+
flipped.second_player? # => true
|
100
|
+
|
101
|
+
# Component comparison
|
102
|
+
chess1 = Sashite::Gan.parse("CHESS:K")
|
103
|
+
chess2 = Sashite::Gan.parse("chess:k")
|
104
|
+
shogi = Sashite::Gan.parse("SHOGI:K")
|
105
|
+
|
106
|
+
chess1.same_name?(chess2) # => true (both chess)
|
107
|
+
chess1.same_side?(shogi) # => true (both first player)
|
108
|
+
chess1.same_type?(chess2) # => true (both kings)
|
109
|
+
chess1.same_name?(shogi) # => false (different styles)
|
110
|
+
|
111
|
+
# Functional transformations can be chained
|
112
|
+
black_promoted = Sashite::Gan.parse("CHESS:P").flip.enhance
|
113
|
+
black_promoted.to_s # => "chess:+p"
|
114
|
+
black_promoted.to_snn # => "chess"
|
115
|
+
black_promoted.to_pin # => "+p"
|
116
|
+
```
|
36
117
|
|
118
|
+
## Format Specification
|
119
|
+
|
120
|
+
### Structure
|
37
121
|
```
|
38
|
-
<
|
122
|
+
<snn>:<pin>
|
39
123
|
```
|
40
124
|
|
41
|
-
|
125
|
+
### Components
|
126
|
+
|
127
|
+
- **SNN Component** (Style Name Notation): Style identifier with case-based side encoding
|
128
|
+
- Uppercase: First player styles (`CHESS`, `SHOGI`, `XIANGQI`)
|
129
|
+
- Lowercase: Second player styles (`chess`, `shogi`, `xiangqi`)
|
130
|
+
- **Colon Separator**: Literal `:` character
|
131
|
+
- **PIN Component** (Piece Identifier Notation): Piece with optional state and case-based ownership
|
132
|
+
- Letter case matches SNN case (case consistency requirement)
|
133
|
+
- Optional state prefix: `+` (enhanced), `-` (diminished)
|
42
134
|
|
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>]`.
|
135
|
+
### Case Consistency Requirement
|
46
136
|
|
47
|
-
The
|
137
|
+
**Critical Rule**: The case of the SNN component must match the case of the PIN component:
|
48
138
|
|
49
|
-
|
50
|
-
|
139
|
+
```ruby
|
140
|
+
# ✅ Valid combinations
|
141
|
+
Sashite::Gan.valid?("CHESS:K") # => true (both uppercase = first player)
|
142
|
+
Sashite::Gan.valid?("chess:k") # => true (both lowercase = second player)
|
143
|
+
Sashite::Gan.valid?("SHOGI:+R") # => true (both uppercase = first player)
|
144
|
+
Sashite::Gan.valid?("xiangqi:-g") # => true (both lowercase = second player)
|
145
|
+
|
146
|
+
# ❌ Invalid combinations
|
147
|
+
Sashite::Gan.valid?("CHESS:k") # => false (case mismatch)
|
148
|
+
Sashite::Gan.valid?("chess:K") # => false (case mismatch)
|
149
|
+
Sashite::Gan.valid?("SHOGI:+r") # => false (case mismatch)
|
150
|
+
```
|
51
151
|
|
52
|
-
|
152
|
+
### Validation Architecture
|
53
153
|
|
54
|
-
|
154
|
+
GAN validation delegates to the underlying components for maximum consistency:
|
155
|
+
- **SNN validation**: Uses `Sashite::Snn::Style::SNN_PATTERN` for style validation
|
156
|
+
- **PIN validation**: Uses `Sashite::Pin::Piece::PIN_PATTERN` for piece validation
|
157
|
+
- **Case consistency**: Ensures matching case between SNN and PIN components
|
55
158
|
|
56
|
-
|
159
|
+
This modular approach avoids code duplication and ensures that GAN validation automatically inherits improvements from the underlying SNN and PIN libraries.
|
57
160
|
|
58
|
-
|
59
|
-
|
161
|
+
### Examples
|
162
|
+
- `CHESS:K` - First player chess king
|
163
|
+
- `chess:k` - Second player chess king
|
164
|
+
- `SHOGI:+P` - First player enhanced shōgi pawn
|
165
|
+
- `xiangqi:-g` - Second player diminished xiangqi general
|
60
166
|
|
61
|
-
|
62
|
-
result = Sashite::Gan.parse("CHESS:K")
|
63
|
-
# => { game_id: "CHESS", letter: "K" }
|
167
|
+
## Game Examples
|
64
168
|
|
65
|
-
|
66
|
-
result = Sashite::Gan.parse("SHOGI:+P")
|
67
|
-
# => { game_id: "SHOGI", letter: "P", prefix: "+" }
|
169
|
+
### Traditional Same-Style Games
|
68
170
|
|
69
|
-
|
70
|
-
result = Sashite::Gan.parse("CHESS:K'")
|
71
|
-
# => { game_id: "CHESS", letter: "K", suffix: "'" }
|
171
|
+
In traditional games where both players use the same piece style:
|
72
172
|
|
73
|
-
|
74
|
-
|
75
|
-
|
173
|
+
```ruby
|
174
|
+
# Chess pieces
|
175
|
+
white_king = Sashite::Gan.parse("CHESS:K")
|
176
|
+
black_king = Sashite::Gan.parse("chess:k")
|
177
|
+
white_queen = Sashite::Gan.parse("CHESS:Q")
|
178
|
+
black_queen = Sashite::Gan.parse("chess:q")
|
179
|
+
|
180
|
+
# Shōgi pieces
|
181
|
+
sente_king = Sashite::Gan.parse("SHOGI:K")
|
182
|
+
gote_king = Sashite::Gan.parse("shogi:k")
|
183
|
+
sente_gold = Sashite::Gan.parse("SHOGI:G")
|
184
|
+
gote_gold = Sashite::Gan.parse("shogi:g")
|
185
|
+
|
186
|
+
# Enhanced states for special conditions
|
187
|
+
castling_rook = Sashite::Gan.parse("CHESS:+R") # Castling-eligible rook
|
188
|
+
vulnerable_pawn = Sashite::Gan.parse("CHESS:-P") # En passant vulnerable pawn
|
189
|
+
promoted_pawn = Sashite::Gan.parse("SHOGI:+P") # Tokin (promoted pawn)
|
76
190
|
```
|
77
191
|
|
78
|
-
###
|
192
|
+
### Cross-Style Games
|
79
193
|
|
80
|
-
|
194
|
+
GAN's explicit style naming enables games where players use different piece traditions:
|
81
195
|
|
82
196
|
```ruby
|
83
|
-
|
197
|
+
# Chess vs Shōgi
|
198
|
+
chess_king = Sashite::Gan.parse("CHESS:K")
|
199
|
+
shogi_king = Sashite::Gan.parse("shogi:k")
|
200
|
+
|
201
|
+
# Makruk vs Xiangqi
|
202
|
+
makruk_queen = Sashite::Gan.parse("MAKRUK:M") # Met (Makruk queen)
|
203
|
+
xiangqi_general = Sashite::Gan.parse("xiangqi:g") # Xiangqi general
|
204
|
+
|
205
|
+
# Multi-tradition setup
|
206
|
+
def create_cross_style_game
|
207
|
+
[
|
208
|
+
Sashite::Gan.parse("CHESS:K"), # First player uses chess
|
209
|
+
Sashite::Gan.parse("CHESS:Q"),
|
210
|
+
Sashite::Gan.parse("shogi:k"), # Second player uses shōgi
|
211
|
+
Sashite::Gan.parse("shogi:g")
|
212
|
+
]
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
### Capture Mechanics Examples
|
84
217
|
|
85
|
-
|
86
|
-
result = Sashite::Gan.safe_parse("CHESS:K'")
|
87
|
-
# => { game_id: "CHESS", letter: "K", suffix: "'" }
|
218
|
+
GAN can represent the different capture mechanics described in the specification:
|
88
219
|
|
89
|
-
|
90
|
-
|
91
|
-
|
220
|
+
```ruby
|
221
|
+
# Chess vs Chess (traditional capture)
|
222
|
+
def chess_capture(captured_piece)
|
223
|
+
# In chess, captured pieces retain their identity but become inactive
|
224
|
+
captured_piece # GAN remains unchanged: chess:p stays chess:p
|
225
|
+
end
|
226
|
+
|
227
|
+
# Shōgi vs Shōgi (side-changing capture)
|
228
|
+
def shogi_capture(captured_piece)
|
229
|
+
# In shōgi, captured pieces change sides and lose promotions
|
230
|
+
captured_piece.flip.normalize # shogi:+p becomes SHOGI:P
|
231
|
+
end
|
232
|
+
|
233
|
+
# Cross-style capture (style transformation)
|
234
|
+
def cross_style_capture(captured_piece, capturing_style)
|
235
|
+
# Captured piece transforms to capturing player's style
|
236
|
+
captured_piece.flip.with_name(capturing_style).normalize
|
237
|
+
# chess:q captured by Ōgi player becomes OGI:P
|
238
|
+
end
|
92
239
|
```
|
93
240
|
|
94
|
-
|
241
|
+
## API Reference
|
242
|
+
|
243
|
+
### Main Module Methods
|
244
|
+
|
245
|
+
- `Sashite::Gan.valid?(gan_string)` - Check if string is valid GAN notation
|
246
|
+
- `Sashite::Gan.parse(gan_string)` - Parse GAN string into Actor object
|
247
|
+
- `Sashite::Gan.actor(name, type, side, state = :normal)` - Create actor instance directly
|
248
|
+
|
249
|
+
### Actor Class
|
250
|
+
|
251
|
+
#### Creation and Parsing
|
252
|
+
- `Sashite::Gan::Actor.new(name, type, side, state = :normal)` - Create actor instance
|
253
|
+
- `Sashite::Gan::Actor.parse(gan_string)` - Parse GAN string (same as module method)
|
254
|
+
- `Sashite::Gan::Actor.valid?(gan_string)` - Validate GAN string (class method)
|
255
|
+
|
256
|
+
#### Attribute Access
|
257
|
+
- `#name` - Get style name (symbol with proper capitalization)
|
258
|
+
- `#type` - Get piece type (symbol :A to :Z, always uppercase)
|
259
|
+
- `#side` - Get player side (:first or :second)
|
260
|
+
- `#state` - Get piece state (:normal, :enhanced, or :diminished)
|
261
|
+
- `#to_s` - Convert to GAN string representation
|
262
|
+
- `#to_pin` - Convert to PIN string representation (piece component only)
|
263
|
+
- `#to_snn` - Convert to SNN string representation (style component only)
|
95
264
|
|
96
|
-
|
265
|
+
#### Component Extraction
|
266
|
+
|
267
|
+
The `to_pin` and `to_snn` methods allow extraction of individual notation components:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
actor = Sashite::Gan.parse("CHESS:+K")
|
271
|
+
|
272
|
+
# Full GAN representation
|
273
|
+
actor.to_s # => "CHESS:+K"
|
274
|
+
|
275
|
+
# Individual components
|
276
|
+
actor.to_snn # => "CHESS" (style component)
|
277
|
+
actor.to_pin # => "+K" (piece component)
|
278
|
+
|
279
|
+
# Component transformation example
|
280
|
+
flipped = actor.flip
|
281
|
+
flipped.to_s # => "chess:+k"
|
282
|
+
flipped.to_snn # => "chess" (lowercase for second player)
|
283
|
+
flipped.to_pin # => "+k" (lowercase with state preserved)
|
284
|
+
|
285
|
+
# State manipulation example
|
286
|
+
normalized = actor.normalize
|
287
|
+
normalized.to_s # => "CHESS:K"
|
288
|
+
normalized.to_pin # => "K" (state modifier removed)
|
289
|
+
normalized.to_snn # => "CHESS" (style unchanged)
|
290
|
+
```
|
291
|
+
|
292
|
+
#### Component Handling
|
293
|
+
|
294
|
+
**Important**: Following PIN and SNN conventions:
|
295
|
+
- **Style names** are stored with proper capitalization (`:Chess`, `:Shogi`)
|
296
|
+
- **Piece types** are stored as uppercase symbols (`:K`, `:P`)
|
297
|
+
- **Display case** is determined by `side` during rendering
|
97
298
|
|
98
299
|
```ruby
|
99
|
-
|
300
|
+
# Both create the same internal representation
|
301
|
+
actor1 = Sashite::Gan.parse("CHESS:K") # name: :Chess, type: :K, side: :first
|
302
|
+
actor2 = Sashite::Gan.parse("chess:k") # name: :Chess, type: :K, side: :second
|
303
|
+
|
304
|
+
actor1.name # => :Chess (proper capitalization)
|
305
|
+
actor2.name # => :Chess (same style name)
|
306
|
+
actor1.type # => :K (uppercase type)
|
307
|
+
actor2.type # => :K (same type)
|
308
|
+
|
309
|
+
actor1.to_s # => "CHESS:K" (uppercase display)
|
310
|
+
actor2.to_s # => "chess:k" (lowercase display)
|
311
|
+
actor1.to_snn # => "CHESS" (uppercase style)
|
312
|
+
actor2.to_snn # => "chess" (lowercase style)
|
313
|
+
actor1.to_pin # => "K" (uppercase piece)
|
314
|
+
actor2.to_pin # => "k" (lowercase piece)
|
315
|
+
```
|
316
|
+
|
317
|
+
#### State Queries
|
318
|
+
- `#normal?` - Check if normal state (no modifiers)
|
319
|
+
- `#enhanced?` - Check if enhanced state
|
320
|
+
- `#diminished?` - Check if diminished state
|
100
321
|
|
101
|
-
|
102
|
-
|
103
|
-
|
322
|
+
#### Side Queries
|
323
|
+
- `#first_player?` - Check if first player actor
|
324
|
+
- `#second_player?` - Check if second player actor
|
104
325
|
|
105
|
-
|
106
|
-
|
107
|
-
|
326
|
+
#### State Transformations (immutable - return new instances)
|
327
|
+
- `#enhance` - Create enhanced version
|
328
|
+
- `#diminish` - Create diminished version
|
329
|
+
- `#normalize` - Remove all state modifiers
|
330
|
+
- `#flip` - Switch player (change side)
|
108
331
|
|
109
|
-
|
110
|
-
|
111
|
-
|
332
|
+
#### Attribute Transformations (immutable - return new instances)
|
333
|
+
- `#with_name(new_name)` - Create actor with different style name
|
334
|
+
- `#with_type(new_type)` - Create actor with different piece type
|
335
|
+
- `#with_side(new_side)` - Create actor with different side
|
336
|
+
- `#with_state(new_state)` - Create actor with different state
|
112
337
|
|
113
|
-
|
114
|
-
|
115
|
-
|
338
|
+
#### Comparison Methods
|
339
|
+
- `#same_name?(other)` - Check if same style name
|
340
|
+
- `#same_type?(other)` - Check if same piece type
|
341
|
+
- `#same_side?(other)` - Check if same side
|
342
|
+
- `#same_state?(other)` - Check if same state
|
343
|
+
- `#==(other)` - Full equality comparison
|
344
|
+
|
345
|
+
### Constants
|
346
|
+
- `Sashite::Gan::Actor::SEPARATOR` - Colon separator character
|
347
|
+
|
348
|
+
## Advanced Usage
|
349
|
+
|
350
|
+
### Component Extraction and Manipulation
|
351
|
+
|
352
|
+
The `to_pin` and `to_snn` methods enable powerful component-based operations:
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
# Extract and manipulate components
|
356
|
+
actor = Sashite::Gan.parse("SHOGI:+P")
|
357
|
+
|
358
|
+
# Component extraction
|
359
|
+
style_str = actor.to_snn # => "SHOGI"
|
360
|
+
piece_str = actor.to_pin # => "+P"
|
361
|
+
|
362
|
+
# Reconstruct from components
|
363
|
+
reconstructed = "#{style_str}:#{piece_str}" # => "SHOGI:+P"
|
364
|
+
|
365
|
+
# Cross-component analysis
|
366
|
+
actors = [
|
367
|
+
Sashite::Gan.parse("CHESS:K"),
|
368
|
+
Sashite::Gan.parse("SHOGI:K"),
|
369
|
+
Sashite::Gan.parse("chess:k")
|
370
|
+
]
|
371
|
+
|
372
|
+
# Group by style component
|
373
|
+
by_style = actors.group_by(&:to_snn)
|
374
|
+
# => {"CHESS" => [...], "SHOGI" => [...], "chess" => [...]}
|
375
|
+
|
376
|
+
# Group by piece component
|
377
|
+
by_piece = actors.group_by(&:to_pin)
|
378
|
+
# => {"K" => [...], "k" => [...]}
|
379
|
+
|
380
|
+
# Component-based filtering
|
381
|
+
uppercase_styles = actors.select { |a| a.to_snn == a.to_snn.upcase }
|
382
|
+
enhanced_pieces = actors.select { |a| a.to_pin.start_with?("+") }
|
116
383
|
```
|
117
384
|
|
118
|
-
###
|
385
|
+
### Component Reconstruction Patterns
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
# Template-based reconstruction
|
389
|
+
def apply_style_template(actors, new_style)
|
390
|
+
actors.map do |actor|
|
391
|
+
pin_part = actor.to_pin
|
392
|
+
side = actor.side
|
393
|
+
|
394
|
+
# Apply new style while preserving piece and side
|
395
|
+
new_style_str = side == :first ? new_style.to_s.upcase : new_style.to_s.downcase
|
396
|
+
Sashite::Gan.parse("#{new_style_str}:#{pin_part}")
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# Convert chess pieces to shōgi style
|
401
|
+
chess_pieces = [
|
402
|
+
Sashite::Gan.parse("CHESS:K"),
|
403
|
+
Sashite::Gan.parse("chess:+q")
|
404
|
+
]
|
405
|
+
|
406
|
+
shogi_pieces = apply_style_template(chess_pieces, :Shogi)
|
407
|
+
# => [SHOGI:K, shogi:+q]
|
408
|
+
|
409
|
+
# Component swapping
|
410
|
+
def swap_components(actor1, actor2)
|
411
|
+
[
|
412
|
+
Sashite::Gan.parse("#{actor1.to_snn}:#{actor2.to_pin}"),
|
413
|
+
Sashite::Gan.parse("#{actor2.to_snn}:#{actor1.to_pin}")
|
414
|
+
]
|
415
|
+
end
|
416
|
+
|
417
|
+
chess_king = Sashite::Gan.parse("CHESS:K")
|
418
|
+
shogi_pawn = Sashite::Gan.parse("shogi:p")
|
419
|
+
|
420
|
+
swapped = swap_components(chess_king, shogi_pawn)
|
421
|
+
# => [CHESS:p, shogi:K]
|
422
|
+
```
|
119
423
|
|
120
|
-
|
424
|
+
### Immutable Transformations
|
425
|
+
```ruby
|
426
|
+
# All transformations return new instances
|
427
|
+
original = Sashite::Gan.parse("CHESS:P")
|
428
|
+
enhanced = original.enhance
|
429
|
+
cross_style = original.with_name(:Shogi)
|
430
|
+
enemy = original.flip
|
431
|
+
|
432
|
+
# Original actor is never modified
|
433
|
+
puts original # => "CHESS:P"
|
434
|
+
puts enhanced # => "CHESS:+P"
|
435
|
+
puts cross_style # => "SHOGI:P"
|
436
|
+
puts enemy # => "chess:p"
|
437
|
+
|
438
|
+
# Component extraction shows changes
|
439
|
+
puts enhanced.to_pin # => "+P" (state changed)
|
440
|
+
puts cross_style.to_snn # => "SHOGI" (style changed)
|
441
|
+
puts enemy.to_snn # => "chess" (case changed)
|
442
|
+
puts enemy.to_pin # => "p" (case changed)
|
443
|
+
|
444
|
+
# Transformations can be chained
|
445
|
+
result = original.flip.with_name(:Xiangqi).enhance
|
446
|
+
puts result # => "xiangqi:+p"
|
447
|
+
puts result.to_snn # => "xiangqi"
|
448
|
+
puts result.to_pin # => "+p"
|
449
|
+
```
|
121
450
|
|
451
|
+
### Multi-Style Game Management
|
122
452
|
```ruby
|
123
|
-
|
453
|
+
class CrossStyleGame
|
454
|
+
def initialize
|
455
|
+
@actors = []
|
456
|
+
@style_assignments = {}
|
457
|
+
end
|
458
|
+
|
459
|
+
def assign_style(player, style)
|
460
|
+
side = player == :white ? :first : :second
|
461
|
+
@style_assignments[player] = { style: style, side: side }
|
462
|
+
end
|
463
|
+
|
464
|
+
def create_actor(player, type, state = :normal)
|
465
|
+
assignment = @style_assignments[player]
|
466
|
+
Sashite::Gan::Actor.new(assignment[:style], type, assignment[:side], state)
|
467
|
+
end
|
468
|
+
|
469
|
+
def valid_combination?
|
470
|
+
return true if @style_assignments.size < 2
|
471
|
+
|
472
|
+
sides = @style_assignments.values.map { |a| a[:side] }
|
473
|
+
sides.uniq.size == 2 # Must have different sides
|
474
|
+
end
|
475
|
+
|
476
|
+
def get_player_style_string(player)
|
477
|
+
actor = create_actor(player, :K) # Use king as reference
|
478
|
+
actor.to_snn
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
# Usage
|
483
|
+
game = CrossStyleGame.new
|
484
|
+
game.assign_style(:white, :Chess)
|
485
|
+
game.assign_style(:black, :Shogi)
|
486
|
+
|
487
|
+
white_king = game.create_actor(:white, :K)
|
488
|
+
black_king = game.create_actor(:black, :K)
|
489
|
+
|
490
|
+
puts white_king # => "CHESS:K"
|
491
|
+
puts white_king.to_snn # => "CHESS"
|
492
|
+
puts black_king # => "shogi:k"
|
493
|
+
puts black_king.to_snn # => "shogi"
|
494
|
+
puts game.valid_combination? # => true
|
495
|
+
```
|
124
496
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
497
|
+
### Validation and Error Handling
|
498
|
+
```ruby
|
499
|
+
# Comprehensive validation with both module and class methods
|
500
|
+
def safe_parse(gan_string)
|
501
|
+
# You can use either method for validation
|
502
|
+
return nil unless Sashite::Gan.valid?(gan_string)
|
503
|
+
|
504
|
+
# Alternative: return nil unless Sashite::Gan::Actor.valid?(gan_string)
|
505
|
+
|
506
|
+
Sashite::Gan.parse(gan_string)
|
507
|
+
rescue ArgumentError => e
|
508
|
+
puts "Parse error: #{e.message}"
|
509
|
+
nil
|
510
|
+
end
|
511
|
+
|
512
|
+
# Batch validation with component extraction
|
513
|
+
gan_strings = ["CHESS:K", "Chess:K", "SHOGI:+p", "invalid"]
|
514
|
+
valid_actors = gan_strings.filter_map { |s| safe_parse(s) }
|
515
|
+
|
516
|
+
puts "Valid actors with components:"
|
517
|
+
valid_actors.each do |actor|
|
518
|
+
puts " #{actor} -> style: #{actor.to_snn}, piece: #{actor.to_pin}"
|
519
|
+
end
|
520
|
+
|
521
|
+
# Module-level validation
|
522
|
+
Sashite::Gan.valid?("CHESS:K") # => true
|
523
|
+
Sashite::Gan.valid?("chess:k") # => true
|
524
|
+
Sashite::Gan.valid?("Chess:K") # => false (mixed case)
|
525
|
+
Sashite::Gan.valid?("CHESS") # => false (missing piece)
|
526
|
+
|
527
|
+
# Class-level validation (equivalent to module method)
|
528
|
+
Sashite::Gan::Actor.valid?("CHESS:K") # => true
|
529
|
+
Sashite::Gan::Actor.valid?("chess:k") # => true
|
530
|
+
Sashite::Gan::Actor.valid?("Chess:K") # => false (mixed case)
|
531
|
+
Sashite::Gan::Actor.valid?("CHESS:k") # => false (case mismatch)
|
532
|
+
```
|
129
533
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
534
|
+
### Collection Operations
|
535
|
+
```ruby
|
536
|
+
# Working with actor collections
|
537
|
+
actors = [
|
538
|
+
Sashite::Gan.parse("CHESS:K"),
|
539
|
+
Sashite::Gan.parse("CHESS:Q"),
|
540
|
+
Sashite::Gan.parse("shogi:k"),
|
541
|
+
Sashite::Gan.parse("shogi:g"),
|
542
|
+
Sashite::Gan.parse("XIANGQI:G")
|
543
|
+
]
|
544
|
+
|
545
|
+
# Group by various attributes
|
546
|
+
by_style = actors.group_by(&:name)
|
547
|
+
by_side = actors.group_by(&:side)
|
548
|
+
by_type = actors.group_by(&:type)
|
549
|
+
|
550
|
+
# Group by string components
|
551
|
+
by_style_string = actors.group_by(&:to_snn)
|
552
|
+
by_piece_string = actors.group_by(&:to_pin)
|
553
|
+
|
554
|
+
puts "By style string: #{by_style_string.keys}" # => ["CHESS", "shogi", "XIANGQI"]
|
555
|
+
puts "By piece string: #{by_piece_string.keys}" # => ["K", "Q", "k", "g", "G"]
|
556
|
+
|
557
|
+
# Filter operations
|
558
|
+
first_player_actors = actors.select(&:first_player?)
|
559
|
+
chess_actors = actors.select { |a| a.name == :Chess }
|
560
|
+
kings = actors.select { |a| a.type == :K }
|
561
|
+
uppercase_styles = actors.select { |a| a.to_snn == a.to_snn.upcase }
|
562
|
+
|
563
|
+
# Transform collections immutably
|
564
|
+
enhanced_actors = actors.map(&:enhance)
|
565
|
+
enemy_actors = actors.map(&:flip)
|
566
|
+
|
567
|
+
# Show component changes
|
568
|
+
puts "Enhanced actors:"
|
569
|
+
enhanced_actors.each { |a| puts " #{a} (pin: #{a.to_pin})" }
|
570
|
+
|
571
|
+
puts "Enemy actors:"
|
572
|
+
enemy_actors.each { |a| puts " #{a} (snn: #{a.to_snn}, pin: #{a.to_pin})" }
|
573
|
+
|
574
|
+
# Complex queries
|
575
|
+
cross_style_pairs = actors.combination(2).select do |a1, a2|
|
576
|
+
a1.name != a2.name && a1.side != a2.side
|
577
|
+
end
|
578
|
+
|
579
|
+
puts "Cross-style pairs: #{cross_style_pairs.size}"
|
134
580
|
```
|
135
581
|
|
136
|
-
##
|
582
|
+
## Protocol Mapping
|
583
|
+
|
584
|
+
GAN encodes piece attributes by combining SNN and PIN information:
|
585
|
+
|
586
|
+
| Protocol Attribute | GAN Encoding | Examples | Notes |
|
587
|
+
|-------------------|--------------|----------|-------|
|
588
|
+
| **Type** | PIN letter choice | `CHESS:K` = King, `SHOGI:P` = Pawn | Type stored as uppercase symbol (`:K`, `:P`) |
|
589
|
+
| **Side** | Unified case across components | `CHESS:K` = First player, `chess:k` = Second player | Case consistency enforced |
|
590
|
+
| **State** | PIN prefix modifier | `SHOGI:+P` = Enhanced, `CHESS:-P` = Diminished | |
|
591
|
+
| **Style** | SNN identifier | `CHESS:K` = Chess style, `SHOGI:K` = Shōgi style | Style stored with proper capitalization (`:Chess`, `:Shogi`) |
|
592
|
+
|
593
|
+
## Properties
|
594
|
+
|
595
|
+
* **Rule-Agnostic**: Independent of specific game mechanics
|
596
|
+
* **Complete Identification**: Explicit representation of all four piece attributes
|
597
|
+
* **Cross-Style Support**: Enables multi-tradition gaming environments
|
598
|
+
* **Component Clarity**: Clear separation between style context and piece identity
|
599
|
+
* **Component Extraction**: Individual SNN and PIN components accessible via `to_snn` and `to_pin`
|
600
|
+
* **Unified Case Encoding**: Consistent case across both components for side identification
|
601
|
+
* **Protocol Compliance**: Direct implementation of Sashité piece attributes
|
602
|
+
* **Immutable Design**: All operations return new instances, ensuring thread safety
|
603
|
+
* **Compositional Architecture**: Built on independent SNN and PIN specifications
|
604
|
+
* **Modular Validation**: Delegates validation to underlying components for consistency
|
605
|
+
|
606
|
+
## Implementation Notes
|
607
|
+
|
608
|
+
### Validation Architecture
|
609
|
+
|
610
|
+
GAN follows a modular validation approach that leverages the underlying component libraries:
|
611
|
+
|
612
|
+
1. **Component Splitting**: GAN strings are split on the colon separator
|
613
|
+
2. **Individual Validation**: Each component is validated using its specific regex:
|
614
|
+
- SNN component: `Sashite::Snn::Style::SNN_PATTERN`
|
615
|
+
- PIN component: `Sashite::Pin::Piece::PIN_PATTERN`
|
616
|
+
3. **Case Consistency**: Additional validation ensures matching case between components
|
137
617
|
|
138
|
-
|
618
|
+
This approach:
|
619
|
+
- **Avoids Code Duplication**: No need to maintain a separate GAN regex
|
620
|
+
- **Maintains Consistency**: Automatically inherits validation improvements from SNN and PIN
|
621
|
+
- **Provides Clear Error Messages**: Component-specific validation failures are more informative
|
622
|
+
- **Enables Modularity**: Each library maintains its own validation logic
|
139
623
|
|
140
|
-
|
141
|
-
- **Lowercase** game IDs must have **lowercase** piece letters for the second player
|
624
|
+
### Component Handling Convention
|
142
625
|
|
143
|
-
|
626
|
+
GAN follows the same internal representation conventions as its constituent libraries:
|
144
627
|
|
145
|
-
|
628
|
+
1. **Style Names**: Always stored with proper capitalization (`:Chess`, `:Shogi`)
|
629
|
+
2. **Piece Types**: Always stored as uppercase symbols (`:K`, `:P`)
|
630
|
+
3. **Display Logic**: Case is computed from `side` during string rendering
|
146
631
|
|
147
|
-
|
632
|
+
This ensures predictable behavior and consistency across the entire Sashité ecosystem.
|
148
633
|
|
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` |
|
634
|
+
## System Constraints
|
157
635
|
|
158
|
-
|
636
|
+
- **Case Consistency**: SNN and PIN components must have matching case
|
637
|
+
- **Exactly 2 players**: Distinguished through consistent case encoding
|
638
|
+
- **Style Assignment**: Fixed throughout a game (first/second player styles remain constant)
|
639
|
+
- **Component Validation**: Both SNN and PIN components must be individually valid
|
159
640
|
|
160
|
-
|
641
|
+
## Use Cases
|
161
642
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
643
|
+
GAN is particularly useful for:
|
644
|
+
|
645
|
+
1. **Multi-Style Environments**: Positions involving pieces from multiple style traditions
|
646
|
+
2. **Cross-Style Games**: Games combining elements from different piece traditions
|
647
|
+
3. **Component Analysis**: Extracting and analyzing style and piece information separately
|
648
|
+
4. **Game Engine Development**: Engines needing unambiguous piece identification
|
649
|
+
5. **Database Systems**: Storing game data without naming conflicts
|
650
|
+
6. **Hybrid Analysis**: Comparing strategic elements across different traditions
|
651
|
+
7. **Functional Programming**: Immutable game state representations
|
652
|
+
8. **Format Conversion**: Converting between GAN and individual SNN/PIN representations
|
653
|
+
|
654
|
+
## Dependencies
|
655
|
+
|
656
|
+
This gem depends on:
|
657
|
+
|
658
|
+
- [sashite-snn](https://github.com/sashite/snn.rb) - Style Name Notation implementation
|
659
|
+
- [sashite-pin](https://github.com/sashite/pin.rb) - Piece Identifier Notation implementation
|
660
|
+
|
661
|
+
## Related Specifications
|
662
|
+
|
663
|
+
- [GAN Specification v1.0.0](https://sashite.dev/specs/gan/1.0.0/)
|
664
|
+
- [GAN Examples](https://sashite.dev/specs/gan/1.0.0/examples/)
|
665
|
+
- [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/)
|
666
|
+
- [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/)
|
667
|
+
- [Game Protocol Foundation](https://sashite.dev/game-protocol/)
|
168
668
|
|
169
669
|
## Documentation
|
170
670
|
|
171
|
-
- [Official GAN Specification](https://sashite.dev/documents/gan/1.0.0/)
|
172
671
|
- [API Documentation](https://rubydoc.info/github/sashite/gan.rb/main)
|
672
|
+
- [SNN Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
|
673
|
+
- [PIN Documentation](https://rubydoc.info/github/sashite/pin.rb/main)
|
674
|
+
|
675
|
+
## Development
|
676
|
+
|
677
|
+
```sh
|
678
|
+
# Clone the repository
|
679
|
+
git clone https://github.com/sashite/gan.rb.git
|
680
|
+
cd gan.rb
|
681
|
+
|
682
|
+
# Install dependencies
|
683
|
+
bundle install
|
684
|
+
|
685
|
+
# Run tests
|
686
|
+
ruby test.rb
|
687
|
+
|
688
|
+
# Generate documentation
|
689
|
+
yard doc
|
690
|
+
```
|
691
|
+
|
692
|
+
## Contributing
|
693
|
+
|
694
|
+
1. Fork the repository
|
695
|
+
2. Create a feature branch (`git checkout -b feature/new-feature`)
|
696
|
+
3. Add tests for your changes
|
697
|
+
4. Ensure all tests pass (`ruby test.rb`)
|
698
|
+
5. Commit your changes (`git commit -am 'Add new feature'`)
|
699
|
+
6. Push to the branch (`git push origin feature/new-feature`)
|
700
|
+
7. Create a Pull Request
|
173
701
|
|
174
702
|
## License
|
175
703
|
|
176
|
-
|
704
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
177
705
|
|
178
|
-
## About
|
706
|
+
## About
|
179
707
|
|
180
|
-
|
708
|
+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|