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.
data/README.md CHANGED
@@ -5,13 +5,19 @@
5
5
  ![Ruby](https://github.com/sashite/gan.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
6
  [![License](https://img.shields.io/github/license/sashite/gan.rb?label=License&logo=github)](https://github.com/sashite/gan.rb/raw/main/LICENSE.md)
7
7
 
8
- > **GAN** (General Actor Notation) support for the Ruby language.
8
+ > **GAN** (General Actor Notation) implementation for the Ruby language.
9
9
 
10
10
  ## What is GAN?
11
11
 
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.
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
- 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.
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
- ## GAN Format
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
- A GAN record consists of a style identifier (SNN format), followed by a colon separator, followed by a piece identifier (PNN format):
118
+ ## Format Specification
32
119
 
120
+ ### Structure
33
121
  ```
34
- <style-id>:<piece-id>
122
+ <snn>:<pin>
35
123
  ```
36
124
 
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
125
+ ### Components
41
126
 
42
- ## Basic Usage
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
- ### Creating Actor Objects
135
+ ### Case Consistency Requirement
45
136
 
46
- The primary interface is the `Sashite::Gan::Actor` class, which represents a game actor in GAN format:
137
+ **Critical Rule**: The case of the SNN component must match the case of the PIN component:
47
138
 
48
139
  ```ruby
49
- require "sashite/gan"
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
- # 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">
152
+ ### Validation Architecture
54
153
 
55
- # With piece modifiers
56
- enhanced_actor = Sashite::Gan::Actor.parse("SHOGI:+P")
57
- # => #<Sashite::Gan::Actor:0x... @style="SHOGI" @piece="+P">
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
- # Create directly with constructor
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
- # 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)
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
- # Convenience method
69
- actor = Sashite::Gan.actor("CHESS", "K")
70
- ```
167
+ ## Game Examples
71
168
 
72
- ### Converting to GAN String
169
+ ### Traditional Same-Style Games
73
170
 
74
- Convert an actor object back to its GAN string representation:
171
+ In traditional games where both players use the same piece style:
75
172
 
76
173
  ```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'"
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
- ### Accessing Components
192
+ ### Cross-Style Games
87
193
 
88
- Access the style and piece components of an actor:
194
+ GAN's explicit style naming enables games where players use different piece traditions:
89
195
 
90
196
  ```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
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
- ## Casing Combinations and Player Association
216
+ ### Capture Mechanics Examples
107
217
 
108
- GAN allows all four combinations of case between style and piece identifiers to support dynamic ownership changes:
218
+ GAN can represent the different capture mechanics described in the specification:
109
219
 
110
220
  ```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
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
- ## Dynamic Ownership Changes
133
-
134
- While style assignment remains fixed throughout a game, piece ownership may change during gameplay:
241
+ ## API Reference
135
242
 
136
- ```ruby
137
- # Original piece owned by first player
138
- original = Sashite::Gan::Actor.parse("SHOGI:P")
243
+ ### Main Module Methods
139
244
 
140
- # After capture by second player (modifiers preserved by default)
141
- captured = original.change_piece_ownership
142
- captured.to_s # => "SHOGI:p"
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
- # Or create the captured version directly
145
- captured = Sashite::Gan::Actor.new(original.style, "p")
249
+ ### Actor Class
146
250
 
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)
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
- # 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)
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
- ## Traditional Same-Style Games
265
+ #### Component Extraction
158
266
 
159
- In traditional games where both players use the same piece style:
267
+ The `to_pin` and `to_snn` methods allow extraction of individual notation components:
160
268
 
161
269
  ```ruby
162
- # Chess pieces
163
- white_king = Sashite::Gan::Actor.parse("CHESS:K")
164
- black_king = Sashite::Gan::Actor.parse("chess:k")
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
- white_queen = Sashite::Gan::Actor.parse("CHESS:Q")
167
- black_queen = Sashite::Gan::Actor.parse("chess:q")
292
+ #### Component Handling
168
293
 
169
- # Shogi pieces
170
- first_king = Sashite::Gan::Actor.parse("SHOGI:K")
171
- second_king = Sashite::Gan::Actor.parse("shogi:k")
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
- first_gold = Sashite::Gan::Actor.parse("SHOGI:G")
174
- second_gold = Sashite::Gan::Actor.parse("shogi:g")
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
- ## Cross-Style Games
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
- In games where players use different piece styles:
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
- # Chess vs Makruk
183
- chess_king = Sashite::Gan::Actor.parse("CHESS:K")
184
- makruk_king = Sashite::Gan::Actor.parse("makruk:k")
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
- chess_queen = Sashite::Gan::Actor.parse("CHESS:Q")
187
- makruk_queen = Sashite::Gan::Actor.parse("makruk:q")
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
- # Shogi vs Xiangqi
190
- shogi_king = Sashite::Gan::Actor.parse("SHOGI:K")
191
- xiangqi_general = Sashite::Gan::Actor.parse("xiangqi:g")
372
+ # Group by style component
373
+ by_style = actors.group_by(&:to_snn)
374
+ # => {"CHESS" => [...], "SHOGI" => [...], "chess" => [...]}
192
375
 
193
- shogi_gold = Sashite::Gan::Actor.parse("SHOGI:G")
194
- xiangqi_advisor = Sashite::Gan::Actor.parse("xiangqi:a")
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
- ## Pieces with States and Ownership Changes
385
+ ### Component Reconstruction Patterns
198
386
 
199
387
  ```ruby
200
- # Original enhanced piece
201
- original = Sashite::Gan::Actor.parse("CHESS:R'")
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
- # After capture (modifiers preserved by default)
204
- captured = original.change_piece_ownership
205
- captured.to_s # => "chess:R'"
406
+ shogi_pieces = apply_style_template(chess_pieces, :Shogi)
407
+ # => [SHOGI:K, shogi:+q]
206
408
 
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"
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
- # 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)
417
+ chess_king = Sashite::Gan.parse("CHESS:K")
418
+ shogi_pawn = Sashite::Gan.parse("shogi:p")
215
419
 
216
- # With explicit modifier removal:
217
- captured_demoted = promoted_pawn.bare_piece.change_piece_ownership
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
- ## Collision Resolution
222
-
223
- GAN resolves naming conflicts between different styles:
224
-
424
+ ### Immutable Transformations
225
425
  ```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"]
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
- ## Advanced Usage
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
- ### Working with Collections
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
- # Group actors by style
536
+ # Working with actor collections
244
537
  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")
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
- 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? }
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
- # Find actors by piece type
259
- kings = actors.select { |actor| actor.piece_name.downcase == "k" }
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
- ### State Manipulation
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
- ```ruby
265
- actor = Sashite::Gan::Actor.parse("SHOGI:P")
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
- # Enhance the piece
268
- enhanced = actor.enhance_piece
269
- enhanced.to_s # => "SHOGI:+P"
563
+ # Transform collections immutably
564
+ enhanced_actors = actors.map(&:enhance)
565
+ enemy_actors = actors.map(&:flip)
270
566
 
271
- # Add intermediate state
272
- intermediate = actor.set_piece_intermediate
273
- intermediate.to_s # => "SHOGI:P'"
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
- ### Validation
571
+ puts "Enemy actors:"
572
+ enemy_actors.each { |a| puts " #{a} (snn: #{a.to_snn}, pin: #{a.to_pin})" }
285
573
 
286
- All parsing automatically validates input according to the GAN specification:
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
- ```ruby
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
- ### Inspection and Debugging
315
-
316
- ```ruby
317
- actor = Sashite::Gan::Actor.parse("SHOGI:+p'")
582
+ ## Protocol Mapping
318
583
 
319
- # Get detailed information
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
- # Check components
324
- actor.style_name # => "SHOGI"
325
- actor.piece_name # => "+p'"
326
- actor.piece.enhanced? # => true
327
- actor.piece.intermediate? # => true
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
- ## API Reference
593
+ ## Properties
331
594
 
332
- ### Module Methods
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
- - `Sashite::Gan.valid?(gan_string)` - Check if a string is valid GAN notation
335
- - `Sashite::Gan.actor(style, piece)` - Convenience method to create actors
606
+ ## Implementation Notes
336
607
 
337
- ### Sashite::Gan::Actor Class Methods
608
+ ### Validation Architecture
338
609
 
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
610
+ GAN follows a modular validation approach that leverages the underlying component libraries:
341
611
 
342
- ### Instance Methods
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
- #### 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
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
- #### 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
624
+ ### Component Handling Convention
356
625
 
357
- #### Conversion
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
- ## Properties of GAN
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
- * **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
632
+ This ensures predictable behavior and consistency across the entire Sashité ecosystem.
369
633
 
370
- ## Constraints
634
+ ## System Constraints
371
635
 
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
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 in the following scenarios:
643
+ GAN is particularly useful for:
381
644
 
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
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) (~> 1.0.0) - Style Name Notation implementation
394
- - [pnn](https://github.com/sashite/pnn.rb) (~> 2.0.0) - Piece Name Notation implementation
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
- ## Specification
661
+ ## Related Specifications
397
662
 
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/)
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
- - [GAN Documentation](https://rubydoc.info/github/sashite/gan.rb/main)
671
+ - [API Documentation](https://rubydoc.info/github/sashite/gan.rb/main)
405
672
  - [SNN Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
406
- - [PNN Documentation](https://rubydoc.info/github/sashite/pnn.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
407
701
 
408
702
  ## License
409
703
 
410
- The [gem](https://rubygems.org/gems/sashite-gan) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
704
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
411
705
 
412
- ## About Sashité
706
+ ## About
413
707
 
414
- This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
708
+ Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.