sashite-pin 1.1.0 → 2.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6affbdb49961967a41cadbd94ced4b32f27279d2fbd7fcc06f74fe58e400184b
4
- data.tar.gz: 5d3ea8362f3d4329cf748f36713d89456e624cc2de0dc238fb784421811e5e81
3
+ metadata.gz: 0335d119541a7fa271689c6235a9c785a4c46f6c70e9577d4cddcc597d520dd5
4
+ data.tar.gz: 7d95e04c803457156561bfe0fd8e270db5df1929b58721b9ed8266646f60e7e1
5
5
  SHA512:
6
- metadata.gz: 71ef6fc371601e9845b667085eed4222f3bd2e03bcac2d076a653632dd535bd2eb139ca59d3e3e257a9fdb33ba6d1ce7435ea2dafe34efe7423cdbefef5d5ede
7
- data.tar.gz: 70684742e2d2406f35a6259d34788ad3545880feb5ba407e993544c78da98f4930dbaccd7a1943579061cf8ab26e0737e8e5eb5dad03b55c7b422a314bc19c8c
6
+ metadata.gz: 6904c80bee041595400f4074ed48a975ad6337c3c5a8c66f9d18ad7864f19e23d04e34b30c725d37ce9c4bfde58667328a82bbd1c38208c123cb44a9da00b283
7
+ data.tar.gz: 32289d6453d81ff2e1920aeb7d69eedfac6bc61f9acdd1929e7ca4bc09c6988bb2a64bc3620130cfb63024977e0508b7c5ffce695590382eac3420d85454c67a
data/README.md CHANGED
@@ -32,11 +32,15 @@ gem install sashite-pin
32
32
  require "sashite/pin"
33
33
 
34
34
  # Parse PIN strings into piece objects
35
- piece = Sashite::Pin.parse("K") # => #<Pin::Piece letter="K" type="K" player=first>
35
+ piece = Sashite::Pin.parse("K") # => #<Pin::Piece type=:K side=:first state=:normal>
36
36
  piece.to_s # => "K"
37
- piece.first_player? # => true
38
- piece.type # => "K"
37
+ piece.type # => :K
39
38
  piece.side # => :first
39
+ piece.state # => :normal
40
+
41
+ # Create pieces directly
42
+ piece = Sashite::Pin.piece(:K, :first, :normal) # => #<Pin::Piece type=:K side=:first state=:normal>
43
+ piece = Sashite::Pin::Piece.new(:R, :second, :enhanced) # => #<Pin::Piece type=:R side=:second state=:enhanced>
40
44
 
41
45
  # Validate PIN strings
42
46
  Sashite::Pin.valid?("K") # => true
@@ -44,32 +48,45 @@ Sashite::Pin.valid?("+R") # => true
44
48
  Sashite::Pin.valid?("invalid") # => false
45
49
 
46
50
  # State manipulation (returns new immutable instances)
47
- enhanced = piece.enhance # => #<Pin::Piece letter="K" type="K" player=first enhanced=true>
51
+ enhanced = piece.enhance # => #<Pin::Piece type=:K side=:first state=:enhanced>
48
52
  enhanced.to_s # => "+K"
49
- diminished = piece.diminish # => #<Pin::Piece letter="K" type="K" player=first diminished=true>
53
+ diminished = piece.diminish # => #<Pin::Piece type=:K side=:first state=:diminished>
50
54
  diminished.to_s # => "-K"
51
55
 
52
- # Player manipulation
53
- flipped = piece.flip # => #<Pin::Piece letter="k" type="K" player=second>
56
+ # Side manipulation
57
+ flipped = piece.flip # => #<Pin::Piece type=:K side=:second state=:normal>
54
58
  flipped.to_s # => "k"
55
59
 
60
+ # Type manipulation
61
+ queen = piece.with_type(:Q) # => #<Pin::Piece type=:Q side=:first state=:normal>
62
+ queen.to_s # => "Q"
63
+
56
64
  # State queries
57
65
  piece.normal? # => true
58
66
  enhanced.enhanced? # => true
59
67
  diminished.diminished? # => true
60
68
 
61
- # Type and player comparison
69
+ # Side queries
70
+ piece.first_player? # => true
71
+ flipped.second_player? # => true
72
+
73
+ # Attribute access
74
+ piece.letter # => "K"
75
+ enhanced.prefix # => "+"
76
+ piece.prefix # => ""
77
+
78
+ # Type and side comparison
62
79
  king1 = Sashite::Pin.parse("K")
63
80
  king2 = Sashite::Pin.parse("k")
64
81
  queen = Sashite::Pin.parse("Q")
65
82
 
66
83
  king1.same_type?(king2) # => true (both kings)
67
- king1.same_player?(queen) # => true (both first player)
84
+ king1.same_side?(queen) # => true (both first player)
68
85
  king1.same_type?(queen) # => false (different types)
69
86
 
70
87
  # Functional transformations can be chained
71
88
  pawn = Sashite::Pin.parse("P")
72
- enemy_promoted = pawn.flip.enhance # => "+p" (black promoted pawn)
89
+ enemy_promoted = pawn.flip.enhance # => "+p" (second player promoted pawn)
73
90
  ```
74
91
 
75
92
  ## Format Specification
@@ -81,7 +98,7 @@ enemy_promoted = pawn.flip.enhance # => "+p" (black promoted pawn)
81
98
 
82
99
  ### Components
83
100
 
84
- - **Letter** (`A-Z`, `a-z`): Represents piece type and player
101
+ - **Letter** (`A-Z`, `a-z`): Represents piece type and side
85
102
  - Uppercase: First player pieces
86
103
  - Lowercase: Second player pieces
87
104
  - **State** (optional prefix):
@@ -105,70 +122,73 @@ enemy_promoted = pawn.flip.enhance # => "+p" (black promoted pawn)
105
122
  ### Western Chess
106
123
  ```ruby
107
124
  # Standard pieces
108
- king = Sashite::Pin.parse("K") # => white king
109
- king.first_player? # => true
110
- king.type # => "K"
125
+ king = Sashite::Pin.piece(:K, :first, :normal) # => white king
126
+ king.first_player? # => true
127
+ king.type # => :K
111
128
 
112
129
  # State modifiers for special conditions
113
- castling_king = king.enhance # => castling-eligible king
114
- castling_king.to_s # => "+K"
130
+ castling_king = king.enhance # => castling-eligible king
131
+ castling_king.to_s # => "+K"
115
132
 
116
- vulnerable_pawn = Sashite::Pin.parse("P").diminish # => en passant vulnerable
117
- vulnerable_pawn.to_s # => "-P"
133
+ vulnerable_pawn = Sashite::Pin.piece(:P, :first, :diminished) # => en passant vulnerable
134
+ vulnerable_pawn.to_s # => "-P"
118
135
 
119
136
  # All piece types
120
- pieces = %w[K Q R B N P].map { |type| Sashite::Pin.parse(type) }
121
- black_pieces = pieces.map(&:flip) # Convert to black pieces
137
+ piece_types = [:K, :Q, :R, :B, :N, :P]
138
+ white_pieces = piece_types.map { |type| Sashite::Pin.piece(type, :first, :normal) }
139
+ black_pieces = white_pieces.map(&:flip) # Convert to black pieces
122
140
  ```
123
141
 
124
142
  ### Japanese Chess (Shōgi)
125
143
  ```ruby
126
144
  # Basic pieces
127
- rook = Sashite::Pin.parse("R") # => white rook
128
- bishop = Sashite::Pin.parse("B") # => white bishop
145
+ rook = Sashite::Pin.piece(:R, :first, :normal) # => white rook
146
+ bishop = Sashite::Pin.piece(:B, :first, :normal) # => white bishop
129
147
 
130
148
  # Promoted pieces (enhanced state)
131
- dragon_king = rook.enhance # => promoted rook (Dragon King)
132
- dragon_king.to_s # => "+R"
149
+ dragon_king = rook.enhance # => promoted rook (Dragon King)
150
+ dragon_king.to_s # => "+R"
133
151
 
134
- dragon_horse = bishop.enhance # => promoted bishop (Dragon Horse)
135
- dragon_horse.to_s # => "+B"
152
+ dragon_horse = bishop.enhance # => promoted bishop (Dragon Horse)
153
+ dragon_horse.to_s # => "+B"
136
154
 
137
155
  # Promoted pawn
138
- pawn = Sashite::Pin.parse("P")
139
- tokin = pawn.enhance # => promoted pawn (Tokin)
140
- tokin.to_s # => "+P"
156
+ pawn = Sashite::Pin.piece(:P, :first, :normal)
157
+ tokin = pawn.enhance # => promoted pawn (Tokin)
158
+ tokin.to_s # => "+P"
141
159
 
142
160
  # All promotable pieces can use the same pattern
143
- promotable = %w[R B S N L P].map { |type| Sashite::Pin.parse(type) }
161
+ promotable_types = [:R, :B, :S, :N, :L, :P]
162
+ promotable = promotable_types.map { |type| Sashite::Pin.piece(type, :first, :normal) }
144
163
  promoted = promotable.map(&:enhance)
145
164
  ```
146
165
 
147
166
  ### Thai Chess (Makruk)
148
167
  ```ruby
149
168
  # Basic pieces
150
- met = Sashite::Pin.parse("M") # => white Met (queen)
151
- pawn = Sashite::Pin.parse("P") # => white Bia (pawn)
169
+ met = Sashite::Pin.piece(:M, :first, :normal) # => white Met (queen)
170
+ pawn = Sashite::Pin.piece(:P, :first, :normal) # => white Bia (pawn)
152
171
 
153
172
  # Promoted pawns
154
- bia_kaew = pawn.enhance # => promoted pawn (Bia Kaew)
155
- bia_kaew.to_s # => "+P"
173
+ bia_kaew = pawn.enhance # => promoted pawn (Bia Kaew)
174
+ bia_kaew.to_s # => "+P"
156
175
 
157
176
  # Makruk pieces
158
- makruk_pieces = %w[K M R B N P].map { |type| Sashite::Pin.parse(type) }
177
+ makruk_types = [:K, :M, :R, :B, :N, :P]
178
+ makruk_pieces = makruk_types.map { |type| Sashite::Pin.piece(type, :first, :normal) }
159
179
  ```
160
180
 
161
181
  ### Chinese Chess (Xiangqi)
162
182
  ```ruby
163
183
  # Pieces with positional states
164
- general = Sashite::Pin.parse("G") # => red general
165
- flying_general = general.enhance # => flying general (special state)
166
- flying_general.to_s # => "+G"
184
+ general = Sashite::Pin.piece(:G, :first, :normal) # => red general
185
+ flying_general = general.enhance # => flying general (special state)
186
+ flying_general.to_s # => "+G"
167
187
 
168
188
  # Soldiers that crossed the river
169
- soldier = Sashite::Pin.parse("P")
170
- crossed_soldier = soldier.enhance # => soldier with enhanced movement
171
- crossed_soldier.to_s # => "+P"
189
+ soldier = Sashite::Pin.piece(:P, :first, :normal)
190
+ crossed_soldier = soldier.enhance # => soldier with enhanced movement
191
+ crossed_soldier.to_s # => "+P"
172
192
  ```
173
193
 
174
194
  ## API Reference
@@ -177,26 +197,44 @@ crossed_soldier.to_s # => "+P"
177
197
 
178
198
  - `Sashite::Pin.valid?(pin_string)` - Check if string is valid PIN notation
179
199
  - `Sashite::Pin.parse(pin_string)` - Parse PIN string into Piece object
200
+ - `Sashite::Pin.piece(type, side, state = :normal)` - Create piece instance directly
180
201
 
181
202
  ### Piece Class
182
203
 
183
204
  #### Creation and Parsing
184
- - `Sashite::Pin::Piece.new(letter, enhanced: false, diminished: false)` - Create piece instance
205
+ - `Sashite::Pin::Piece.new(type, side, state = :normal)` - Create piece instance
185
206
  - `Sashite::Pin::Piece.parse(pin_string)` - Parse PIN string (same as module method)
186
207
 
187
- #### String Representation
208
+ #### Attribute Access
209
+ - `#type` - Get piece type (symbol :A to :Z, always uppercase)
210
+ - `#side` - Get player side (:first or :second)
211
+ - `#state` - Get state (:normal, :enhanced, or :diminished)
212
+ - `#letter` - Get letter representation (string, case determined by side)
213
+ - `#prefix` - Get state prefix (string: "+", "-", or "")
188
214
  - `#to_s` - Convert to PIN string representation
189
- - `#letter` - Get the letter (type + side)
190
- - `#type` - Get piece type (uppercase letter)
191
- - `#side` - Get player side (`:first` or `:second`)
192
- - `#state` - Get state (`:normal`, `:enhanced`, or `:diminished`)
215
+
216
+ #### Type and Case Handling
217
+
218
+ **Important**: The `type` attribute is always stored as an uppercase symbol (`:A` to `:Z`), regardless of the input case when parsing. The display case in `#letter` and `#to_s` is determined by the `side` attribute:
219
+
220
+ ```ruby
221
+ # Both create the same internal type representation
222
+ piece1 = Sashite::Pin.parse("K") # type: :K, side: :first
223
+ piece2 = Sashite::Pin.parse("k") # type: :K, side: :second
224
+
225
+ piece1.type # => :K (uppercase symbol)
226
+ piece2.type # => :K (same uppercase symbol)
227
+
228
+ piece1.letter # => "K" (uppercase display)
229
+ piece2.letter # => "k" (lowercase display)
230
+ ```
193
231
 
194
232
  #### State Queries
195
233
  - `#normal?` - Check if normal state (no modifiers)
196
234
  - `#enhanced?` - Check if enhanced state
197
235
  - `#diminished?` - Check if diminished state
198
236
 
199
- #### Player Queries
237
+ #### Side Queries
200
238
  - `#first_player?` - Check if first player piece
201
239
  - `#second_player?` - Check if second player piece
202
240
 
@@ -206,11 +244,17 @@ crossed_soldier.to_s # => "+P"
206
244
  - `#diminish` - Create diminished version
207
245
  - `#undiminish` - Remove diminished state
208
246
  - `#normalize` - Remove all state modifiers
209
- - `#flip` - Switch player (change case)
247
+ - `#flip` - Switch player (change side)
248
+
249
+ #### Attribute Transformations (immutable - return new instances)
250
+ - `#with_type(new_type)` - Create piece with different type
251
+ - `#with_side(new_side)` - Create piece with different side
252
+ - `#with_state(new_state)` - Create piece with different state
210
253
 
211
254
  #### Comparison Methods
212
255
  - `#same_type?(other)` - Check if same piece type
213
- - `#same_player?(other)` - Check if same player
256
+ - `#same_side?(other)` - Check if same side
257
+ - `#same_state?(other)` - Check if same state
214
258
  - `#==(other)` - Full equality comparison
215
259
 
216
260
  ### Constants
@@ -218,10 +262,34 @@ crossed_soldier.to_s # => "+P"
218
262
 
219
263
  ## Advanced Usage
220
264
 
265
+ ### Type Normalization Examples
266
+
267
+ ```ruby
268
+ # Parsing different cases results in same type
269
+ white_king = Sashite::Pin.parse("K")
270
+ black_king = Sashite::Pin.parse("k")
271
+
272
+ # Types are normalized to uppercase
273
+ white_king.type # => :K
274
+ black_king.type # => :K (same type!)
275
+
276
+ # Sides are different
277
+ white_king.side # => :first
278
+ black_king.side # => :second
279
+
280
+ # Display follows side convention
281
+ white_king.letter # => "K"
282
+ black_king.letter # => "k"
283
+
284
+ # Same type, different sides
285
+ white_king.same_type?(black_king) # => true
286
+ white_king.same_side?(black_king) # => false
287
+ ```
288
+
221
289
  ### Immutable Transformations
222
290
  ```ruby
223
291
  # All transformations return new instances
224
- original = Sashite::Pin.parse("K")
292
+ original = Sashite::Pin.piece(:K, :first, :normal)
225
293
  enhanced = original.enhance
226
294
  diminished = original.diminish
227
295
 
@@ -231,8 +299,8 @@ puts enhanced.to_s # => "+K"
231
299
  puts diminished.to_s # => "-K"
232
300
 
233
301
  # Transformations can be chained
234
- result = original.flip.enhance.flip.diminish
235
- puts result.to_s # => "-K"
302
+ result = original.flip.enhance.with_type(:Q)
303
+ puts result.to_s # => "+q"
236
304
  ```
237
305
 
238
306
  ### Game State Management
@@ -242,15 +310,15 @@ class GameBoard
242
310
  @pieces = {}
243
311
  end
244
312
 
245
- def place(square, pin_string)
246
- @pieces[square] = Sashite::Pin.parse(pin_string)
313
+ def place(square, piece)
314
+ @pieces[square] = piece
247
315
  end
248
316
 
249
- def promote(square)
317
+ def promote(square, new_type = :Q)
250
318
  piece = @pieces[square]
251
319
  return nil unless piece&.normal? # Can only promote normal pieces
252
320
 
253
- @pieces[square] = piece.enhance
321
+ @pieces[square] = piece.with_type(new_type).enhance
254
322
  end
255
323
 
256
324
  def capture(from_square, to_square)
@@ -259,8 +327,8 @@ class GameBoard
259
327
  captured
260
328
  end
261
329
 
262
- def pieces_by_player(first_player: true)
263
- @pieces.select { |_, piece| piece.first_player? == first_player }
330
+ def pieces_by_side(side)
331
+ @pieces.select { |_, piece| piece.side == side }
264
332
  end
265
333
 
266
334
  def promoted_pieces
@@ -270,24 +338,24 @@ end
270
338
 
271
339
  # Usage
272
340
  board = GameBoard.new
273
- board.place("e1", "K")
274
- board.place("e8", "k")
275
- board.place("a7", "P")
341
+ board.place("e1", Sashite::Pin.piece(:K, :first, :normal))
342
+ board.place("e8", Sashite::Pin.piece(:K, :second, :normal))
343
+ board.place("a7", Sashite::Pin.piece(:P, :first, :normal))
276
344
 
277
345
  # Promote pawn
278
- board.promote("a7")
346
+ board.promote("a7", :Q)
279
347
  promoted = board.promoted_pieces
280
- puts promoted.values.first.to_s # => "+P"
348
+ puts promoted.values.first.to_s # => "+Q"
281
349
  ```
282
350
 
283
351
  ### Piece Analysis
284
352
  ```ruby
285
- def analyze_pieces(pin_strings)
286
- pieces = pin_strings.map { |pin| Sashite::Pin.parse(pin) }
353
+ def analyze_pieces(pins)
354
+ pieces = pins.map { |pin| Sashite::Pin.parse(pin) }
287
355
 
288
356
  {
289
357
  total: pieces.size,
290
- by_player: pieces.group_by(&:side),
358
+ by_side: pieces.group_by(&:side),
291
359
  by_type: pieces.group_by(&:type),
292
360
  by_state: pieces.group_by(&:state),
293
361
  promoted: pieces.count(&:enhanced?),
@@ -297,8 +365,8 @@ end
297
365
 
298
366
  pins = %w[K Q +R B N P k q r +b n -p]
299
367
  analysis = analyze_pieces(pins)
300
- puts analysis[:by_player][:first].size # => 6
301
- puts analysis[:promoted] # => 2
368
+ puts analysis[:by_side][:first].size # => 6
369
+ puts analysis[:promoted] # => 2
302
370
  ```
303
371
 
304
372
  ### Move Validation Example
@@ -307,17 +375,17 @@ def can_promote?(piece, target_rank)
307
375
  return false unless piece.normal? # Already promoted pieces can't promote again
308
376
 
309
377
  case piece.type
310
- when "P" # Pawn
378
+ when :P # Pawn
311
379
  (piece.first_player? && target_rank == 8) ||
312
380
  (piece.second_player? && target_rank == 1)
313
- when "R", "B", "S", "N", "L" # Shōgi pieces that can promote
381
+ when :R, :B, :S, :N, :L # Shōgi pieces that can promote
314
382
  true
315
383
  else
316
384
  false
317
385
  end
318
386
  end
319
387
 
320
- pawn = Sashite::Pin.parse("P")
388
+ pawn = Sashite::Pin.piece(:P, :first, :normal)
321
389
  puts can_promote?(pawn, 8) # => true
322
390
 
323
391
  promoted_pawn = pawn.enhance
@@ -328,11 +396,15 @@ puts can_promote?(promoted_pawn, 8) # => false (already promoted)
328
396
 
329
397
  Following the [Game Protocol](https://sashite.dev/game-protocol/):
330
398
 
331
- | Protocol Attribute | PIN Encoding | Examples |
332
- |-------------------|--------------|----------|
333
- | **Type** | ASCII letter choice | `K`/`k` = King, `P`/`p` = Pawn |
334
- | **Side** | Letter case | `K` = First player, `k` = Second player |
335
- | **State** | Optional prefix | `+K` = Enhanced, `-K` = Diminished, `K` = Normal |
399
+ | Protocol Attribute | PIN Encoding | Examples | Notes |
400
+ |-------------------|--------------|----------|-------|
401
+ | **Type** | ASCII letter choice | `K`/`k` = King, `P`/`p` = Pawn | Type is always stored as uppercase symbol (`:K`, `:P`) |
402
+ | **Side** | Letter case in display | `K` = First player, `k` = Second player | Case is determined by side during rendering |
403
+ | **State** | Optional prefix | `+K` = Enhanced, `-K` = Diminished, `K` = Normal | |
404
+
405
+ **Type Convention**: All piece types are internally represented as uppercase symbols (`:A` to `:Z`). The display case is determined by the `side` attribute: first player pieces display as uppercase, second player pieces as lowercase.
406
+
407
+ **Canonical principle**: Identical pieces must have identical PIN representations.
336
408
 
337
409
  **Note**: PIN does not represent the **Style** attribute from the Game Protocol. For style-aware piece notation, see [Piece Name Notation (PNN)](https://sashite.dev/specs/pnn/).
338
410
 
@@ -342,10 +414,41 @@ Following the [Game Protocol](https://sashite.dev/game-protocol/):
342
414
  * **Rule-Agnostic**: Independent of specific game mechanics
343
415
  * **Compact Format**: 1-2 characters per piece
344
416
  * **Visual Distinction**: Clear player differentiation through case
417
+ * **Type Normalization**: Consistent uppercase type representation internally
345
418
  * **Protocol Compliant**: Direct implementation of Sashité piece attributes
346
419
  * **Immutable**: All piece instances are frozen and transformations return new objects
347
420
  * **Functional**: Pure functions with no side effects
348
421
 
422
+ ## Implementation Notes
423
+
424
+ ### Type Normalization Convention
425
+
426
+ PIN follows a strict type normalization convention:
427
+
428
+ 1. **Internal Storage**: All piece types are stored as uppercase symbols (`:A` to `:Z`)
429
+ 2. **Input Flexibility**: Both `"K"` and `"k"` are valid input during parsing
430
+ 3. **Case Semantics**: Input case determines the `side` attribute, not the `type`
431
+ 4. **Display Logic**: Output case is computed from `side` during rendering
432
+
433
+ This design ensures:
434
+ - Consistent internal representation regardless of input format
435
+ - Clear separation between piece identity (type) and ownership (side)
436
+ - Predictable behavior when comparing pieces of the same type
437
+
438
+ ### Example Flow
439
+
440
+ ```ruby
441
+ # Input: "k" (lowercase)
442
+ # ↓ Parsing
443
+ # type: :K (normalized to uppercase)
444
+ # side: :second (inferred from lowercase input)
445
+ # ↓ Display
446
+ # letter: "k" (computed from type + side)
447
+ # PIN: "k" (final representation)
448
+ ```
449
+
450
+ This ensures that `parse(pin).to_s == pin` for all valid PIN strings while maintaining internal consistency.
451
+
349
452
  ## System Constraints
350
453
 
351
454
  - **Maximum 26 piece types** per game system (one per ASCII letter)
@@ -22,27 +22,55 @@ module Sashite
22
22
  # Valid state modifiers
23
23
  ENHANCED_PREFIX = "+"
24
24
  DIMINISHED_PREFIX = "-"
25
+ NORMAL_PREFIX = ""
26
+
27
+ # State constants
28
+ ENHANCED_STATE = :enhanced
29
+ DIMINISHED_STATE = :diminished
30
+ NORMAL_STATE = :normal
31
+
32
+ # Player side constants
33
+ FIRST_PLAYER = :first
34
+ SECOND_PLAYER = :second
35
+
36
+ # Valid types (A-Z)
37
+ VALID_TYPES = (:A..:Z).to_a.freeze
38
+
39
+ # Valid sides
40
+ VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
41
+
42
+ # Valid states
43
+ VALID_STATES = [NORMAL_STATE, ENHANCED_STATE, DIMINISHED_STATE].freeze
25
44
 
26
45
  # Error messages
27
46
  ERROR_INVALID_PIN = "Invalid PIN string: %s"
28
- ERROR_INVALID_LETTER = "Letter must be a single ASCII letter (a-z or A-Z): %s"
47
+ ERROR_INVALID_TYPE = "Type must be a symbol from :A to :Z, got: %s"
48
+ ERROR_INVALID_SIDE = "Side must be :first or :second, got: %s"
49
+ ERROR_INVALID_STATE = "State must be :normal, :enhanced, or :diminished, got: %s"
50
+
51
+ # @return [Symbol] the piece type (:A to :Z)
52
+ attr_reader :type
53
+
54
+ # @return [Symbol] the player side (:first or :second)
55
+ attr_reader :side
29
56
 
30
- # @return [String] the base letter identifier (type + side)
31
- attr_reader :letter
57
+ # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
58
+ attr_reader :state
32
59
 
33
60
  # Create a new piece instance
34
61
  #
35
- # @param letter [String] single ASCII letter (a-z or A-Z)
36
- # @param enhanced [Boolean] whether the piece has enhanced state
37
- # @param diminished [Boolean] whether the piece has diminished state
62
+ # @param type [Symbol] piece type (:A to :Z)
63
+ # @param side [Symbol] player side (:first or :second)
64
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
38
65
  # @raise [ArgumentError] if parameters are invalid
39
- def initialize(letter, enhanced: false, diminished: false)
40
- self.class.validate_letter(letter)
41
- self.class.validate_state_combination(enhanced, diminished)
66
+ def initialize(type, side, state = NORMAL_STATE)
67
+ self.class.validate_type(type)
68
+ self.class.validate_side(side)
69
+ self.class.validate_state(state)
42
70
 
43
- @letter = letter.freeze
44
- @enhanced = enhanced
45
- @diminished = diminished
71
+ @type = type
72
+ @side = side
73
+ @state = state
46
74
 
47
75
  freeze
48
76
  end
@@ -53,9 +81,9 @@ module Sashite
53
81
  # @return [Piece] new piece instance
54
82
  # @raise [ArgumentError] if the PIN string is invalid
55
83
  # @example
56
- # Pin::Piece.parse("k") # => #<Pin::Piece letter="k">
57
- # Pin::Piece.parse("+R") # => #<Pin::Piece letter="R" enhanced=true>
58
- # Pin::Piece.parse("-p") # => #<Pin::Piece letter="p" diminished=true>
84
+ # Pin::Piece.parse("k") # => #<Pin::Piece type=:K side=:second state=:normal>
85
+ # Pin::Piece.parse("+R") # => #<Pin::Piece type=:R side=:first state=:enhanced>
86
+ # Pin::Piece.parse("-p") # => #<Pin::Piece type=:P side=:second state=:diminished>
59
87
  def self.parse(pin_string)
60
88
  string_value = String(pin_string)
61
89
  matches = match_pattern(string_value)
@@ -64,11 +92,18 @@ module Sashite
64
92
  enhanced = matches[:prefix] == ENHANCED_PREFIX
65
93
  diminished = matches[:prefix] == DIMINISHED_PREFIX
66
94
 
67
- new(
68
- letter,
69
- enhanced: enhanced,
70
- diminished: diminished
71
- )
95
+ # Extract type and side from letter
96
+ piece_type = letter.upcase.to_sym
97
+ piece_side = letter == letter.upcase ? FIRST_PLAYER : SECOND_PLAYER
98
+ piece_state = if enhanced
99
+ ENHANCED_STATE
100
+ elsif diminished
101
+ DIMINISHED_STATE
102
+ else
103
+ NORMAL_STATE
104
+ end
105
+
106
+ new(piece_type, piece_side, piece_state)
72
107
  end
73
108
 
74
109
  # Convert the piece to its PIN string representation
@@ -79,180 +114,197 @@ module Sashite
79
114
  # piece.to_s # => "-p"
80
115
  # piece.to_s # => "K"
81
116
  def to_s
82
- prefix = if @enhanced
83
- ENHANCED_PREFIX
84
- else
85
- (@diminished ? DIMINISHED_PREFIX : "")
86
- end
87
117
  "#{prefix}#{letter}"
88
118
  end
89
119
 
120
+ # Get the letter representation
121
+ #
122
+ # @return [String] letter representation combining type and side
123
+ def letter
124
+ first_player? ? type.to_s.upcase : type.to_s.downcase
125
+ end
126
+
127
+ # Get the prefix representation
128
+ #
129
+ # @return [String] prefix representing the state
130
+ def prefix
131
+ case state
132
+ when ENHANCED_STATE then ENHANCED_PREFIX
133
+ when DIMINISHED_STATE then DIMINISHED_PREFIX
134
+ else NORMAL_PREFIX
135
+ end
136
+ end
137
+
90
138
  # Create a new piece with enhanced state
91
139
  #
92
140
  # @return [Piece] new piece instance with enhanced state
93
141
  # @example
94
- # piece.enhance # k => +k
142
+ # piece.enhance # (:K, :first, :normal) => (:K, :first, :enhanced)
95
143
  def enhance
96
144
  return self if enhanced?
97
145
 
98
- self.class.new(
99
- letter,
100
- enhanced: true,
101
- diminished: false
102
- )
146
+ self.class.new(type, side, ENHANCED_STATE)
103
147
  end
104
148
 
105
149
  # Create a new piece without enhanced state
106
150
  #
107
151
  # @return [Piece] new piece instance without enhanced state
108
152
  # @example
109
- # piece.unenhance # +k => k
153
+ # piece.unenhance # (:K, :first, :enhanced) => (:K, :first, :normal)
110
154
  def unenhance
111
155
  return self unless enhanced?
112
156
 
113
- self.class.new(
114
- letter,
115
- enhanced: false,
116
- diminished: @diminished
117
- )
157
+ self.class.new(type, side, NORMAL_STATE)
118
158
  end
119
159
 
120
160
  # Create a new piece with diminished state
121
161
  #
122
162
  # @return [Piece] new piece instance with diminished state
123
163
  # @example
124
- # piece.diminish # k => -k
164
+ # piece.diminish # (:K, :first, :normal) => (:K, :first, :diminished)
125
165
  def diminish
126
166
  return self if diminished?
127
167
 
128
- self.class.new(
129
- letter,
130
- enhanced: false,
131
- diminished: true
132
- )
168
+ self.class.new(type, side, DIMINISHED_STATE)
133
169
  end
134
170
 
135
171
  # Create a new piece without diminished state
136
172
  #
137
173
  # @return [Piece] new piece instance without diminished state
138
174
  # @example
139
- # piece.undiminish # -k => k
175
+ # piece.undiminish # (:K, :first, :diminished) => (:K, :first, :normal)
140
176
  def undiminish
141
177
  return self unless diminished?
142
178
 
143
- self.class.new(
144
- letter,
145
- enhanced: @enhanced,
146
- diminished: false
147
- )
179
+ self.class.new(type, side, NORMAL_STATE)
148
180
  end
149
181
 
150
182
  # Create a new piece with normal state (no modifiers)
151
183
  #
152
184
  # @return [Piece] new piece instance with normal state
153
185
  # @example
154
- # piece.normalize # +k => k, -k => k
186
+ # piece.normalize # (:K, :first, :enhanced) => (:K, :first, :normal)
155
187
  def normalize
156
188
  return self if normal?
157
189
 
158
- self.class.new(letter)
190
+ self.class.new(type, side, NORMAL_STATE)
159
191
  end
160
192
 
161
193
  # Create a new piece with opposite ownership (case)
162
194
  #
163
195
  # @return [Piece] new piece instance with flipped case
164
196
  # @example
165
- # piece.flip # K => k, k => K
197
+ # piece.flip # (:K, :first, :normal) => (:K, :second, :normal)
166
198
  def flip
167
- flipped_letter = letter.swapcase
199
+ self.class.new(type, opposite_side, state)
200
+ end
168
201
 
169
- self.class.new(
170
- flipped_letter,
171
- enhanced: @enhanced,
172
- diminished: @diminished
173
- )
202
+ # Create a new piece with a different type (keeping same side and state)
203
+ #
204
+ # @param new_type [Symbol] new type (:A to :Z)
205
+ # @return [Piece] new piece instance with different type
206
+ # @example
207
+ # piece.with_type(:Q) # (:K, :first, :normal) => (:Q, :first, :normal)
208
+ def with_type(new_type)
209
+ self.class.validate_type(new_type)
210
+ return self if type == new_type
211
+
212
+ self.class.new(new_type, side, state)
213
+ end
214
+
215
+ # Create a new piece with a different side (keeping same type and state)
216
+ #
217
+ # @param new_side [Symbol] :first or :second
218
+ # @return [Piece] new piece instance with different side
219
+ # @example
220
+ # piece.with_side(:second) # (:K, :first, :normal) => (:K, :second, :normal)
221
+ def with_side(new_side)
222
+ self.class.validate_side(new_side)
223
+ return self if side == new_side
224
+
225
+ self.class.new(type, new_side, state)
226
+ end
227
+
228
+ # Create a new piece with a different state (keeping same type and side)
229
+ #
230
+ # @param new_state [Symbol] :normal, :enhanced, or :diminished
231
+ # @return [Piece] new piece instance with different state
232
+ # @example
233
+ # piece.with_state(:enhanced) # (:K, :first, :normal) => (:K, :first, :enhanced)
234
+ def with_state(new_state)
235
+ self.class.validate_state(new_state)
236
+ return self if state == new_state
237
+
238
+ self.class.new(type, side, new_state)
174
239
  end
175
240
 
176
241
  # Check if the piece has enhanced state
177
242
  #
178
243
  # @return [Boolean] true if enhanced
179
244
  def enhanced?
180
- @enhanced
245
+ state == ENHANCED_STATE
181
246
  end
182
247
 
183
248
  # Check if the piece has diminished state
184
249
  #
185
250
  # @return [Boolean] true if diminished
186
251
  def diminished?
187
- @diminished
252
+ state == DIMINISHED_STATE
188
253
  end
189
254
 
190
255
  # Check if the piece has normal state (no modifiers)
191
256
  #
192
257
  # @return [Boolean] true if no modifiers are present
193
258
  def normal?
194
- !enhanced? && !diminished?
259
+ state == NORMAL_STATE
195
260
  end
196
261
 
197
- # Check if the piece belongs to the first player (uppercase)
262
+ # Check if the piece belongs to the first player
198
263
  #
199
- # @return [Boolean] true if uppercase letter
264
+ # @return [Boolean] true if first player
200
265
  def first_player?
201
- letter == letter.upcase
266
+ side == FIRST_PLAYER
202
267
  end
203
268
 
204
- # Check if the piece belongs to the second player (lowercase)
269
+ # Check if the piece belongs to the second player
205
270
  #
206
- # @return [Boolean] true if lowercase letter
271
+ # @return [Boolean] true if second player
207
272
  def second_player?
208
- letter == letter.downcase
273
+ side == SECOND_PLAYER
209
274
  end
210
275
 
211
- # Get the piece type (uppercase letter regardless of player)
212
- #
213
- # @return [String] uppercase letter representing the piece type
214
- # @example
215
- # piece.type # "k" => "K", "R" => "R", "+p" => "P"
216
- def type
217
- letter.upcase
218
- end
219
-
220
- # Get the player side based on letter case
221
- #
222
- # @return [Symbol] :first or :second
223
- def side
224
- first_player? ? :first : :second
225
- end
226
-
227
- # Get the state as a symbol
228
- #
229
- # @return [Symbol] :enhanced, :diminished, or :normal
230
- def state
231
- return :enhanced if enhanced?
232
- return :diminished if diminished?
233
- :normal
234
- end
235
-
236
- # Check if this piece is the same type as another (ignoring player and state)
276
+ # Check if this piece is the same type as another (ignoring side and state)
237
277
  #
238
278
  # @param other [Piece] piece to compare with
239
279
  # @return [Boolean] true if same type
240
280
  # @example
241
- # king1.same_type?(king2) # K and k => true, K and Q => false
281
+ # king1.same_type?(king2) # (:K, :first, :normal) and (:K, :second, :enhanced) => true
242
282
  def same_type?(other)
243
283
  return false unless other.is_a?(self.class)
284
+
244
285
  type == other.type
245
286
  end
246
287
 
247
- # Check if this piece belongs to the same player as another
288
+ # Check if this piece belongs to the same side as another
248
289
  #
249
290
  # @param other [Piece] piece to compare with
250
- # @return [Boolean] true if same player
251
- def same_player?(other)
291
+ # @return [Boolean] true if same side
292
+ def same_side?(other)
252
293
  return false unless other.is_a?(self.class)
294
+
253
295
  side == other.side
254
296
  end
255
297
 
298
+ # Check if this piece has the same state as another
299
+ #
300
+ # @param other [Piece] piece to compare with
301
+ # @return [Boolean] true if same state
302
+ def same_state?(other)
303
+ return false unless other.is_a?(self.class)
304
+
305
+ state == other.state
306
+ end
307
+
256
308
  # Custom equality comparison
257
309
  #
258
310
  # @param other [Object] object to compare with
@@ -260,9 +312,7 @@ module Sashite
260
312
  def ==(other)
261
313
  return false unless other.is_a?(self.class)
262
314
 
263
- letter == other.letter &&
264
- enhanced? == other.enhanced? &&
265
- diminished? == other.diminished?
315
+ type == other.type && side == other.side && state == other.state
266
316
  end
267
317
 
268
318
  # Alias for == to ensure Set functionality works correctly
@@ -272,29 +322,37 @@ module Sashite
272
322
  #
273
323
  # @return [Integer] hash value
274
324
  def hash
275
- [self.class, @letter, @enhanced, @diminished].hash
325
+ [self.class, type, side, state].hash
276
326
  end
277
327
 
278
- # Validate that the letter is a single ASCII letter
328
+ # Validate that the type is a valid symbol
279
329
  #
280
- # @param letter [String] the letter to validate
330
+ # @param type [Symbol] the type to validate
281
331
  # @raise [ArgumentError] if invalid
282
- def self.validate_letter(letter)
283
- letter_str = String(letter)
284
- return if letter_str.match?(/\A[a-zA-Z]\z/)
332
+ def self.validate_type(type)
333
+ return if VALID_TYPES.include?(type)
285
334
 
286
- raise ::ArgumentError, format(ERROR_INVALID_LETTER, letter_str)
335
+ raise ::ArgumentError, format(ERROR_INVALID_TYPE, type.inspect)
287
336
  end
288
337
 
289
- # Validate that enhanced and diminished states are not both true
338
+ # Validate that the side is a valid symbol
290
339
  #
291
- # @param enhanced [Boolean] enhanced state
292
- # @param diminished [Boolean] diminished state
293
- # @raise [ArgumentError] if both are true
294
- def self.validate_state_combination(enhanced, diminished)
295
- return unless enhanced && diminished
340
+ # @param side [Symbol] the side to validate
341
+ # @raise [ArgumentError] if invalid
342
+ def self.validate_side(side)
343
+ return if VALID_SIDES.include?(side)
296
344
 
297
- raise ::ArgumentError, "A piece cannot be both enhanced and diminished"
345
+ raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
346
+ end
347
+
348
+ # Validate that the state is a valid symbol
349
+ #
350
+ # @param state [Symbol] the state to validate
351
+ # @raise [ArgumentError] if invalid
352
+ def self.validate_state(state)
353
+ return if VALID_STATES.include?(state)
354
+
355
+ raise ::ArgumentError, format(ERROR_INVALID_STATE, state.inspect)
298
356
  end
299
357
 
300
358
  # Match PIN pattern against string
@@ -310,6 +368,15 @@ module Sashite
310
368
  end
311
369
 
312
370
  private_class_method :match_pattern
371
+
372
+ private
373
+
374
+ # Get the opposite side of the current piece
375
+ #
376
+ # @return [Symbol] :first if current side is :second, :second if current side is :first
377
+ def opposite_side
378
+ first_player? ? SECOND_PLAYER : FIRST_PLAYER
379
+ end
313
380
  end
314
381
  end
315
382
  end
data/lib/sashite/pin.rb CHANGED
@@ -47,10 +47,26 @@ module Sashite
47
47
  # @return [Pin::Piece] new piece instance
48
48
  # @raise [ArgumentError] if the PIN string is invalid
49
49
  # @example
50
- # Sashite::Pin.parse("K") # => #<Pin::Piece letter="K">
51
- # Sashite::Pin.parse("+R") # => #<Pin::Piece letter="R" enhanced=true>
50
+ # Sashite::Pin.parse("K") # => #<Pin::Piece type=:K side=:first state=:normal>
51
+ # Sashite::Pin.parse("+R") # => #<Pin::Piece type=:R side=:first state=:enhanced>
52
+ # Sashite::Pin.parse("-p") # => #<Pin::Piece type=:P side=:second state=:diminished>
52
53
  def self.parse(pin_string)
53
54
  Piece.parse(pin_string)
54
55
  end
56
+
57
+ # Create a new piece instance
58
+ #
59
+ # @param type [Symbol] piece type (:A to :Z)
60
+ # @param side [Symbol] player side (:first or :second)
61
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
62
+ # @return [Pin::Piece] new piece instance
63
+ # @raise [ArgumentError] if parameters are invalid
64
+ # @example
65
+ # Sashite::Pin.piece(:K, :first, :normal) # => #<Pin::Piece type=:K side=:first state=:normal>
66
+ # Sashite::Pin.piece(:R, :first, :enhanced) # => #<Pin::Piece type=:R side=:first state=:enhanced>
67
+ # Sashite::Pin.piece(:P, :second, :diminished) # => #<Pin::Piece type=:P side=:second state=:diminished>
68
+ def self.piece(type, side, state = :normal)
69
+ Piece.new(type, side, state)
70
+ end
55
71
  end
56
72
  end
data/lib/sashite-pin.rb CHANGED
@@ -1,20 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "sashite/pin"
4
+
3
5
  # Sashité namespace for board game notation libraries
6
+ #
7
+ # Sashité provides a collection of libraries for representing and manipulating
8
+ # board game concepts according to the Game Protocol specifications.
9
+ #
10
+ # @see https://sashite.dev/game-protocol/ Game Protocol Foundation
11
+ # @see https://sashite.dev/specs/ Sashité Specifications
12
+ # @author Sashité
4
13
  module Sashite
5
- # Piece Identifier Notation (PIN) implementation for Ruby
6
- #
7
- # PIN provides an ASCII-based format for representing pieces in abstract
8
- # strategy board games. PIN translates piece attributes from the Game Protocol
9
- # into a compact, portable notation system using ASCII letters with optional
10
- # state modifiers and case-based player group classification.
11
- #
12
- # Format: [<state>]<letter>
13
- # - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
14
- # - Letter: A-Z (first player), a-z (second player)
15
- #
16
- # @see https://sashite.dev/specs/pin/1.0.0/ PIN Specification v1.0.0
17
- # @author Sashité
18
14
  end
19
-
20
- require_relative "sashite/pin"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-pin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -15,7 +15,8 @@ description: |
15
15
  a modern Ruby interface featuring immutable piece objects and functional programming
16
16
  principles. PIN translates piece attributes from the Game Protocol into a compact,
17
17
  portable notation system using ASCII letters with optional state modifiers and
18
- case-based player group classification.
18
+ case-based side encoding. Perfect for game engines, board game notation systems,
19
+ and multi-game environments.
19
20
  email: contact@cyril.email
20
21
  executables: []
21
22
  extensions: []