sashite-gan 4.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 +568 -274
- data/lib/sashite/gan/actor.rb +420 -98
- data/lib/sashite/gan.rb +50 -62
- data/lib/sashite-gan.rb +9 -13
- metadata +16 -13
data/README.md
CHANGED
@@ -5,13 +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)
|
19
|
+
|
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.
|
15
21
|
|
16
22
|
## Installation
|
17
23
|
|
@@ -26,389 +32,677 @@ Or install manually:
|
|
26
32
|
gem install sashite-gan
|
27
33
|
```
|
28
34
|
|
29
|
-
##
|
35
|
+
## Usage
|
36
|
+
|
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
|
+
```
|
30
117
|
|
31
|
-
|
118
|
+
## Format Specification
|
32
119
|
|
120
|
+
### Structure
|
33
121
|
```
|
34
|
-
<
|
122
|
+
<snn>:<pin>
|
35
123
|
```
|
36
124
|
|
37
|
-
|
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
|
125
|
+
### Components
|
41
126
|
|
42
|
-
|
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)
|
43
134
|
|
44
|
-
###
|
135
|
+
### Case Consistency Requirement
|
45
136
|
|
46
|
-
The
|
137
|
+
**Critical Rule**: The case of the SNN component must match the case of the PIN component:
|
47
138
|
|
48
139
|
```ruby
|
49
|
-
|
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
|
+
```
|
50
151
|
|
51
|
-
|
52
|
-
actor = Sashite::Gan::Actor.parse("CHESS:K")
|
53
|
-
# => #<Sashite::Gan::Actor:0x... @style="CHESS" @piece="K">
|
152
|
+
### Validation Architecture
|
54
153
|
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
58
158
|
|
59
|
-
|
60
|
-
actor = Sashite::Gan::Actor.new("CHESS", "K")
|
61
|
-
enhanced_actor = Sashite::Gan::Actor.new("SHOGI", "+P")
|
159
|
+
This modular approach avoids code duplication and ensures that GAN validation automatically inherits improvements from the underlying SNN and PIN libraries.
|
62
160
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
67
166
|
|
68
|
-
|
69
|
-
actor = Sashite::Gan.actor("CHESS", "K")
|
70
|
-
```
|
167
|
+
## Game Examples
|
71
168
|
|
72
|
-
###
|
169
|
+
### Traditional Same-Style Games
|
73
170
|
|
74
|
-
|
171
|
+
In traditional games where both players use the same piece style:
|
75
172
|
|
76
173
|
```ruby
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
#
|
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)
|
84
190
|
```
|
85
191
|
|
86
|
-
###
|
192
|
+
### Cross-Style Games
|
87
193
|
|
88
|
-
|
194
|
+
GAN's explicit style naming enables games where players use different piece traditions:
|
89
195
|
|
90
196
|
```ruby
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
#
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
104
214
|
```
|
105
215
|
|
106
|
-
|
216
|
+
### Capture Mechanics Examples
|
107
217
|
|
108
|
-
GAN
|
218
|
+
GAN can represent the different capture mechanics described in the specification:
|
109
219
|
|
110
220
|
```ruby
|
111
|
-
#
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
actor4.piece.lowercase? # => true
|
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
|
130
239
|
```
|
131
240
|
|
132
|
-
##
|
133
|
-
|
134
|
-
While style assignment remains fixed throughout a game, piece ownership may change during gameplay:
|
241
|
+
## API Reference
|
135
242
|
|
136
|
-
|
137
|
-
# Original piece owned by first player
|
138
|
-
original = Sashite::Gan::Actor.parse("SHOGI:P")
|
243
|
+
### Main Module Methods
|
139
244
|
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
143
248
|
|
144
|
-
|
145
|
-
captured = Sashite::Gan::Actor.new(original.style, "p")
|
249
|
+
### Actor Class
|
146
250
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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)
|
151
255
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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)
|
156
264
|
|
157
|
-
|
265
|
+
#### Component Extraction
|
158
266
|
|
159
|
-
|
267
|
+
The `to_pin` and `to_snn` methods allow extraction of individual notation components:
|
160
268
|
|
161
269
|
```ruby
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
+
```
|
165
291
|
|
166
|
-
|
167
|
-
black_queen = Sashite::Gan::Actor.parse("chess:q")
|
292
|
+
#### Component Handling
|
168
293
|
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
172
298
|
|
173
|
-
|
174
|
-
|
299
|
+
```ruby
|
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)
|
175
315
|
```
|
176
316
|
|
177
|
-
|
317
|
+
#### State Queries
|
318
|
+
- `#normal?` - Check if normal state (no modifiers)
|
319
|
+
- `#enhanced?` - Check if enhanced state
|
320
|
+
- `#diminished?` - Check if diminished state
|
321
|
+
|
322
|
+
#### Side Queries
|
323
|
+
- `#first_player?` - Check if first player actor
|
324
|
+
- `#second_player?` - Check if second player actor
|
325
|
+
|
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)
|
331
|
+
|
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
|
337
|
+
|
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
|
178
347
|
|
179
|
-
|
348
|
+
## Advanced Usage
|
349
|
+
|
350
|
+
### Component Extraction and Manipulation
|
351
|
+
|
352
|
+
The `to_pin` and `to_snn` methods enable powerful component-based operations:
|
180
353
|
|
181
354
|
```ruby
|
182
|
-
#
|
183
|
-
|
184
|
-
|
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"
|
185
361
|
|
186
|
-
|
187
|
-
|
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
|
+
]
|
188
371
|
|
189
|
-
#
|
190
|
-
|
191
|
-
|
372
|
+
# Group by style component
|
373
|
+
by_style = actors.group_by(&:to_snn)
|
374
|
+
# => {"CHESS" => [...], "SHOGI" => [...], "chess" => [...]}
|
192
375
|
|
193
|
-
|
194
|
-
|
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?("+") }
|
195
383
|
```
|
196
384
|
|
197
|
-
|
385
|
+
### Component Reconstruction Patterns
|
198
386
|
|
199
387
|
```ruby
|
200
|
-
#
|
201
|
-
|
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
|
+
]
|
202
405
|
|
203
|
-
|
204
|
-
|
205
|
-
captured.to_s # => "chess:R'"
|
406
|
+
shogi_pieces = apply_style_template(chess_pieces, :Shogi)
|
407
|
+
# => [SHOGI:K, shogi:+q]
|
206
408
|
|
207
|
-
#
|
208
|
-
|
209
|
-
|
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
|
210
416
|
|
211
|
-
|
212
|
-
|
213
|
-
captured_promoted = promoted_pawn.change_piece_ownership
|
214
|
-
captured_promoted.to_s # => "SHOGI:+p" (modifiers preserved)
|
417
|
+
chess_king = Sashite::Gan.parse("CHESS:K")
|
418
|
+
shogi_pawn = Sashite::Gan.parse("shogi:p")
|
215
419
|
|
216
|
-
|
217
|
-
|
218
|
-
captured_demoted.to_s # => "SHOGI:p"
|
420
|
+
swapped = swap_components(chess_king, shogi_pawn)
|
421
|
+
# => [CHESS:p, shogi:K]
|
219
422
|
```
|
220
423
|
|
221
|
-
|
222
|
-
|
223
|
-
GAN resolves naming conflicts between different styles:
|
224
|
-
|
424
|
+
### Immutable Transformations
|
225
425
|
```ruby
|
226
|
-
# All
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
#
|
233
|
-
|
234
|
-
puts
|
235
|
-
# =>
|
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"
|
236
449
|
```
|
237
450
|
|
238
|
-
|
451
|
+
### Multi-Style Game Management
|
452
|
+
```ruby
|
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
|
+
```
|
239
496
|
|
240
|
-
###
|
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
|
+
```
|
241
533
|
|
534
|
+
### Collection Operations
|
242
535
|
```ruby
|
243
|
-
#
|
536
|
+
# Working with actor collections
|
244
537
|
actors = [
|
245
|
-
Sashite::Gan
|
246
|
-
Sashite::Gan
|
247
|
-
Sashite::Gan
|
248
|
-
Sashite::Gan
|
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")
|
249
543
|
]
|
250
544
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
first_player_actors = actors.select { |actor| actor.style.first_player? }
|
256
|
-
second_player_actors = actors.select { |actor| actor.style.second_player? }
|
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)
|
257
549
|
|
258
|
-
#
|
259
|
-
|
260
|
-
|
550
|
+
# Group by string components
|
551
|
+
by_style_string = actors.group_by(&:to_snn)
|
552
|
+
by_piece_string = actors.group_by(&:to_pin)
|
261
553
|
|
262
|
-
|
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"]
|
263
556
|
|
264
|
-
|
265
|
-
|
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 }
|
266
562
|
|
267
|
-
#
|
268
|
-
|
269
|
-
|
563
|
+
# Transform collections immutably
|
564
|
+
enhanced_actors = actors.map(&:enhance)
|
565
|
+
enemy_actors = actors.map(&:flip)
|
270
566
|
|
271
|
-
#
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
# Chain operations
|
276
|
-
complex = actor.enhance_piece.set_piece_intermediate
|
277
|
-
complex.to_s # => "SHOGI:+P'"
|
278
|
-
|
279
|
-
# Remove all modifiers
|
280
|
-
bare = complex.bare_piece
|
281
|
-
bare.to_s # => "SHOGI:P"
|
282
|
-
```
|
567
|
+
# Show component changes
|
568
|
+
puts "Enhanced actors:"
|
569
|
+
enhanced_actors.each { |a| puts " #{a} (pin: #{a.to_pin})" }
|
283
570
|
|
284
|
-
|
571
|
+
puts "Enemy actors:"
|
572
|
+
enemy_actors.each { |a| puts " #{a} (snn: #{a.to_snn}, pin: #{a.to_pin})" }
|
285
573
|
|
286
|
-
|
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
|
287
578
|
|
288
|
-
|
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)
|
579
|
+
puts "Cross-style pairs: #{cross_style_pairs.size}"
|
312
580
|
```
|
313
581
|
|
314
|
-
|
315
|
-
|
316
|
-
```ruby
|
317
|
-
actor = Sashite::Gan::Actor.parse("SHOGI:+p'")
|
582
|
+
## Protocol Mapping
|
318
583
|
|
319
|
-
|
320
|
-
actor.inspect
|
321
|
-
# => "#<Sashite::Gan::Actor:0x... style=\"SHOGI\" piece=\"+p'\">"
|
584
|
+
GAN encodes piece attributes by combining SNN and PIN information:
|
322
585
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
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`) |
|
329
592
|
|
330
|
-
##
|
593
|
+
## Properties
|
331
594
|
|
332
|
-
|
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
|
333
605
|
|
334
|
-
|
335
|
-
- `Sashite::Gan.actor(style, piece)` - Convenience method to create actors
|
606
|
+
## Implementation Notes
|
336
607
|
|
337
|
-
###
|
608
|
+
### Validation Architecture
|
338
609
|
|
339
|
-
|
340
|
-
- `Sashite::Gan::Actor.new(style, piece)` - Create a new actor instance
|
610
|
+
GAN follows a modular validation approach that leverages the underlying component libraries:
|
341
611
|
|
342
|
-
|
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
|
343
617
|
|
344
|
-
|
345
|
-
-
|
346
|
-
-
|
347
|
-
-
|
348
|
-
-
|
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
|
349
623
|
|
350
|
-
|
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
|
624
|
+
### Component Handling Convention
|
356
625
|
|
357
|
-
|
358
|
-
- `#to_s` - Convert to GAN string representation
|
359
|
-
- `#inspect` - Detailed string representation for debugging
|
626
|
+
GAN follows the same internal representation conventions as its constituent libraries:
|
360
627
|
|
361
|
-
|
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
|
362
631
|
|
363
|
-
|
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
|
632
|
+
This ensures predictable behavior and consistency across the entire Sashité ecosystem.
|
369
633
|
|
370
|
-
## Constraints
|
634
|
+
## System Constraints
|
371
635
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
* Both style and piece identifiers must conform to their respective specifications
|
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
|
377
640
|
|
378
641
|
## Use Cases
|
379
642
|
|
380
|
-
GAN is particularly useful
|
643
|
+
GAN is particularly useful for:
|
381
644
|
|
382
|
-
1. **Multi-
|
383
|
-
2. **
|
384
|
-
3. **
|
385
|
-
4. **
|
386
|
-
5. **
|
387
|
-
6. **
|
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
|
388
653
|
|
389
654
|
## Dependencies
|
390
655
|
|
391
656
|
This gem depends on:
|
392
657
|
|
393
|
-
- [sashite-snn](https://github.com/sashite/snn.rb)
|
394
|
-
- [
|
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
|
395
660
|
|
396
|
-
##
|
661
|
+
## Related Specifications
|
397
662
|
|
398
|
-
- [GAN Specification](https://sashite.dev/
|
399
|
-
- [
|
400
|
-
- [
|
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/)
|
401
668
|
|
402
669
|
## Documentation
|
403
670
|
|
404
|
-
- [
|
671
|
+
- [API Documentation](https://rubydoc.info/github/sashite/gan.rb/main)
|
405
672
|
- [SNN Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
|
406
|
-
- [
|
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
|
407
701
|
|
408
702
|
## License
|
409
703
|
|
410
|
-
|
704
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
411
705
|
|
412
|
-
## About
|
706
|
+
## About
|
413
707
|
|
414
|
-
|
708
|
+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|