sashite-pin 1.1.0 → 2.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 +103 -75
- data/lib/sashite/pin/piece.rb +173 -114
- data/lib/sashite/pin.rb +18 -2
- data/lib/sashite-pin.rb +9 -15
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a140d90c9c73aeb4773d2e94c0825cdddfe6ce5dff722325eb58bbea896ba62e
|
4
|
+
data.tar.gz: 273fbe7adfa54613f4d582feb053690724d686f2f1d966d0f6871d33e77450b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dcdd60202c06776b8243d1330c54f93d6a38d919f9d5201660287967c987c69a119c9f1884a0c8bf3f3099d14b715b3c2d656c8725b39092df352f6ea9d5a2d8
|
7
|
+
data.tar.gz: 4e87b25c1180e9cebcf47756cce651022d5c5b3227200010f2de408744910f20f596ee80bd34d6bf0ee597982189c92c22aed88907de30fea4c6c2455ac92072
|
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
|
35
|
+
piece = Sashite::Pin.parse("K") # => #<Pin::Piece type=:K side=:first state=:normal>
|
36
36
|
piece.to_s # => "K"
|
37
|
-
piece.
|
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
|
51
|
+
enhanced = piece.enhance # => #<Pin::Piece type=:K side=:first state=:enhanced>
|
48
52
|
enhanced.to_s # => "+K"
|
49
|
-
diminished = piece.diminish # => #<Pin::Piece
|
53
|
+
diminished = piece.diminish # => #<Pin::Piece type=:K side=:first state=:diminished>
|
50
54
|
diminished.to_s # => "-K"
|
51
55
|
|
52
|
-
#
|
53
|
-
flipped = piece.flip # => #<Pin::Piece
|
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
|
-
#
|
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.
|
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" (
|
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
|
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.
|
109
|
-
king.first_player?
|
110
|
-
king.type
|
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
|
114
|
-
castling_king.to_s
|
130
|
+
castling_king = king.enhance # => castling-eligible king
|
131
|
+
castling_king.to_s # => "+K"
|
115
132
|
|
116
|
-
vulnerable_pawn = Sashite::Pin.
|
117
|
-
vulnerable_pawn.to_s
|
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
|
-
|
121
|
-
|
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.
|
128
|
-
bishop = Sashite::Pin.
|
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
|
132
|
-
dragon_king.to_s
|
149
|
+
dragon_king = rook.enhance # => promoted rook (Dragon King)
|
150
|
+
dragon_king.to_s # => "+R"
|
133
151
|
|
134
|
-
dragon_horse = bishop.enhance
|
135
|
-
dragon_horse.to_s
|
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.
|
139
|
-
tokin = pawn.enhance
|
140
|
-
tokin.to_s
|
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
|
-
|
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.
|
151
|
-
pawn = Sashite::Pin.
|
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
|
155
|
-
bia_kaew.to_s
|
173
|
+
bia_kaew = pawn.enhance # => promoted pawn (Bia Kaew)
|
174
|
+
bia_kaew.to_s # => "+P"
|
156
175
|
|
157
176
|
# Makruk pieces
|
158
|
-
|
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.
|
165
|
-
flying_general = general.enhance
|
166
|
-
flying_general.to_s
|
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.
|
170
|
-
crossed_soldier = soldier.enhance
|
171
|
-
crossed_soldier.to_s
|
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,28 @@ 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(
|
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
|
-
####
|
208
|
+
#### Attribute Access
|
209
|
+
- `#type` - Get piece type (symbol :A to :Z)
|
210
|
+
- `#side` - Get player side (:first or :second)
|
211
|
+
- `#state` - Get state (:normal, :enhanced, or :diminished)
|
212
|
+
- `#letter` - Get letter representation (string)
|
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`)
|
193
215
|
|
194
216
|
#### State Queries
|
195
217
|
- `#normal?` - Check if normal state (no modifiers)
|
196
218
|
- `#enhanced?` - Check if enhanced state
|
197
219
|
- `#diminished?` - Check if diminished state
|
198
220
|
|
199
|
-
####
|
221
|
+
#### Side Queries
|
200
222
|
- `#first_player?` - Check if first player piece
|
201
223
|
- `#second_player?` - Check if second player piece
|
202
224
|
|
@@ -206,11 +228,17 @@ crossed_soldier.to_s # => "+P"
|
|
206
228
|
- `#diminish` - Create diminished version
|
207
229
|
- `#undiminish` - Remove diminished state
|
208
230
|
- `#normalize` - Remove all state modifiers
|
209
|
-
- `#flip` - Switch player (change
|
231
|
+
- `#flip` - Switch player (change side)
|
232
|
+
|
233
|
+
#### Attribute Transformations (immutable - return new instances)
|
234
|
+
- `#with_type(new_type)` - Create piece with different type
|
235
|
+
- `#with_side(new_side)` - Create piece with different side
|
236
|
+
- `#with_state(new_state)` - Create piece with different state
|
210
237
|
|
211
238
|
#### Comparison Methods
|
212
239
|
- `#same_type?(other)` - Check if same piece type
|
213
|
-
- `#
|
240
|
+
- `#same_side?(other)` - Check if same side
|
241
|
+
- `#same_state?(other)` - Check if same state
|
214
242
|
- `#==(other)` - Full equality comparison
|
215
243
|
|
216
244
|
### Constants
|
@@ -221,7 +249,7 @@ crossed_soldier.to_s # => "+P"
|
|
221
249
|
### Immutable Transformations
|
222
250
|
```ruby
|
223
251
|
# All transformations return new instances
|
224
|
-
original = Sashite::Pin.
|
252
|
+
original = Sashite::Pin.piece(:K, :first, :normal)
|
225
253
|
enhanced = original.enhance
|
226
254
|
diminished = original.diminish
|
227
255
|
|
@@ -231,8 +259,8 @@ puts enhanced.to_s # => "+K"
|
|
231
259
|
puts diminished.to_s # => "-K"
|
232
260
|
|
233
261
|
# Transformations can be chained
|
234
|
-
result = original.flip.enhance.
|
235
|
-
puts result.to_s # => "
|
262
|
+
result = original.flip.enhance.with_type(:Q)
|
263
|
+
puts result.to_s # => "+q"
|
236
264
|
```
|
237
265
|
|
238
266
|
### Game State Management
|
@@ -242,15 +270,15 @@ class GameBoard
|
|
242
270
|
@pieces = {}
|
243
271
|
end
|
244
272
|
|
245
|
-
def place(square,
|
246
|
-
@pieces[square] =
|
273
|
+
def place(square, piece)
|
274
|
+
@pieces[square] = piece
|
247
275
|
end
|
248
276
|
|
249
|
-
def promote(square)
|
277
|
+
def promote(square, new_type = :Q)
|
250
278
|
piece = @pieces[square]
|
251
279
|
return nil unless piece&.normal? # Can only promote normal pieces
|
252
280
|
|
253
|
-
@pieces[square] = piece.enhance
|
281
|
+
@pieces[square] = piece.with_type(new_type).enhance
|
254
282
|
end
|
255
283
|
|
256
284
|
def capture(from_square, to_square)
|
@@ -259,8 +287,8 @@ class GameBoard
|
|
259
287
|
captured
|
260
288
|
end
|
261
289
|
|
262
|
-
def
|
263
|
-
@pieces.select { |_, piece| piece.
|
290
|
+
def pieces_by_side(side)
|
291
|
+
@pieces.select { |_, piece| piece.side == side }
|
264
292
|
end
|
265
293
|
|
266
294
|
def promoted_pieces
|
@@ -270,24 +298,24 @@ end
|
|
270
298
|
|
271
299
|
# Usage
|
272
300
|
board = GameBoard.new
|
273
|
-
board.place("e1",
|
274
|
-
board.place("e8",
|
275
|
-
board.place("a7",
|
301
|
+
board.place("e1", Sashite::Pin.piece(:K, :first, :normal))
|
302
|
+
board.place("e8", Sashite::Pin.piece(:K, :second, :normal))
|
303
|
+
board.place("a7", Sashite::Pin.piece(:P, :first, :normal))
|
276
304
|
|
277
305
|
# Promote pawn
|
278
|
-
board.promote("a7")
|
306
|
+
board.promote("a7", :Q)
|
279
307
|
promoted = board.promoted_pieces
|
280
|
-
puts promoted.values.first.to_s # => "+
|
308
|
+
puts promoted.values.first.to_s # => "+Q"
|
281
309
|
```
|
282
310
|
|
283
311
|
### Piece Analysis
|
284
312
|
```ruby
|
285
|
-
def analyze_pieces(
|
286
|
-
pieces =
|
313
|
+
def analyze_pieces(pins)
|
314
|
+
pieces = pins.map { |pin| Sashite::Pin.parse(pin) }
|
287
315
|
|
288
316
|
{
|
289
317
|
total: pieces.size,
|
290
|
-
|
318
|
+
by_side: pieces.group_by(&:side),
|
291
319
|
by_type: pieces.group_by(&:type),
|
292
320
|
by_state: pieces.group_by(&:state),
|
293
321
|
promoted: pieces.count(&:enhanced?),
|
@@ -297,8 +325,8 @@ end
|
|
297
325
|
|
298
326
|
pins = %w[K Q +R B N P k q r +b n -p]
|
299
327
|
analysis = analyze_pieces(pins)
|
300
|
-
puts analysis[:
|
301
|
-
puts analysis[:promoted]
|
328
|
+
puts analysis[:by_side][:first].size # => 6
|
329
|
+
puts analysis[:promoted] # => 2
|
302
330
|
```
|
303
331
|
|
304
332
|
### Move Validation Example
|
@@ -307,17 +335,17 @@ def can_promote?(piece, target_rank)
|
|
307
335
|
return false unless piece.normal? # Already promoted pieces can't promote again
|
308
336
|
|
309
337
|
case piece.type
|
310
|
-
when
|
338
|
+
when :P # Pawn
|
311
339
|
(piece.first_player? && target_rank == 8) ||
|
312
340
|
(piece.second_player? && target_rank == 1)
|
313
|
-
when
|
341
|
+
when :R, :B, :S, :N, :L # Shōgi pieces that can promote
|
314
342
|
true
|
315
343
|
else
|
316
344
|
false
|
317
345
|
end
|
318
346
|
end
|
319
347
|
|
320
|
-
pawn = Sashite::Pin.
|
348
|
+
pawn = Sashite::Pin.piece(:P, :first, :normal)
|
321
349
|
puts can_promote?(pawn, 8) # => true
|
322
350
|
|
323
351
|
promoted_pawn = pawn.enhance
|
@@ -330,9 +358,9 @@ Following the [Game Protocol](https://sashite.dev/game-protocol/):
|
|
330
358
|
|
331
359
|
| Protocol Attribute | PIN Encoding | Examples |
|
332
360
|
|-------------------|--------------|----------|
|
333
|
-
| **Type** |
|
334
|
-
| **Side** |
|
335
|
-
| **State** |
|
361
|
+
| **Type** | Symbol choice | `:K`/`:k` = King, `:P`/`:p` = Pawn |
|
362
|
+
| **Side** | Symbol value | `:first` = First player, `:second` = Second player |
|
363
|
+
| **State** | Symbol value | `:enhanced` = Enhanced, `:diminished` = Diminished, `:normal` = Normal |
|
336
364
|
|
337
365
|
**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
366
|
|
data/lib/sashite/pin/piece.rb
CHANGED
@@ -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
|
-
|
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 [
|
31
|
-
attr_reader :
|
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
|
36
|
-
# @param
|
37
|
-
# @param
|
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(
|
40
|
-
self.class.
|
41
|
-
self.class.
|
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
|
-
@
|
44
|
-
@
|
45
|
-
@
|
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
|
57
|
-
# Pin::Piece.parse("+R") # => #<Pin::Piece
|
58
|
-
# Pin::Piece.parse("-p") # => #<Pin::Piece
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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,198 @@ 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 #
|
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 #
|
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 #
|
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 #
|
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 #
|
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(
|
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
|
197
|
+
# piece.flip # (:K, :first, :normal) => (:K, :second, :normal)
|
166
198
|
def flip
|
167
|
-
|
199
|
+
new_side = first_player? ? SECOND_PLAYER : FIRST_PLAYER
|
200
|
+
self.class.new(type, new_side, state)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Create a new piece with a different type (keeping same side and state)
|
204
|
+
#
|
205
|
+
# @param new_type [Symbol] new type (:A to :Z)
|
206
|
+
# @return [Piece] new piece instance with different type
|
207
|
+
# @example
|
208
|
+
# piece.with_type(:Q) # (:K, :first, :normal) => (:Q, :first, :normal)
|
209
|
+
def with_type(new_type)
|
210
|
+
self.class.validate_type(new_type)
|
211
|
+
return self if type == new_type
|
168
212
|
|
169
|
-
self.class.new(
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
213
|
+
self.class.new(new_type, side, state)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Create a new piece with a different side (keeping same type and state)
|
217
|
+
#
|
218
|
+
# @param new_side [Symbol] :first or :second
|
219
|
+
# @return [Piece] new piece instance with different side
|
220
|
+
# @example
|
221
|
+
# piece.with_side(:second) # (:K, :first, :normal) => (:K, :second, :normal)
|
222
|
+
def with_side(new_side)
|
223
|
+
self.class.validate_side(new_side)
|
224
|
+
return self if side == new_side
|
225
|
+
|
226
|
+
self.class.new(type, new_side, state)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Create a new piece with a different state (keeping same type and side)
|
230
|
+
#
|
231
|
+
# @param new_state [Symbol] :normal, :enhanced, or :diminished
|
232
|
+
# @return [Piece] new piece instance with different state
|
233
|
+
# @example
|
234
|
+
# piece.with_state(:enhanced) # (:K, :first, :normal) => (:K, :first, :enhanced)
|
235
|
+
def with_state(new_state)
|
236
|
+
self.class.validate_state(new_state)
|
237
|
+
return self if state == new_state
|
238
|
+
|
239
|
+
self.class.new(type, side, new_state)
|
174
240
|
end
|
175
241
|
|
176
242
|
# Check if the piece has enhanced state
|
177
243
|
#
|
178
244
|
# @return [Boolean] true if enhanced
|
179
245
|
def enhanced?
|
180
|
-
|
246
|
+
state == ENHANCED_STATE
|
181
247
|
end
|
182
248
|
|
183
249
|
# Check if the piece has diminished state
|
184
250
|
#
|
185
251
|
# @return [Boolean] true if diminished
|
186
252
|
def diminished?
|
187
|
-
|
253
|
+
state == DIMINISHED_STATE
|
188
254
|
end
|
189
255
|
|
190
256
|
# Check if the piece has normal state (no modifiers)
|
191
257
|
#
|
192
258
|
# @return [Boolean] true if no modifiers are present
|
193
259
|
def normal?
|
194
|
-
|
260
|
+
state == NORMAL_STATE
|
195
261
|
end
|
196
262
|
|
197
|
-
# Check if the piece belongs to the first player
|
263
|
+
# Check if the piece belongs to the first player
|
198
264
|
#
|
199
|
-
# @return [Boolean] true if
|
265
|
+
# @return [Boolean] true if first player
|
200
266
|
def first_player?
|
201
|
-
|
267
|
+
side == FIRST_PLAYER
|
202
268
|
end
|
203
269
|
|
204
|
-
# Check if the piece belongs to the second player
|
270
|
+
# Check if the piece belongs to the second player
|
205
271
|
#
|
206
|
-
# @return [Boolean] true if
|
272
|
+
# @return [Boolean] true if second player
|
207
273
|
def second_player?
|
208
|
-
|
274
|
+
side == SECOND_PLAYER
|
209
275
|
end
|
210
276
|
|
211
|
-
#
|
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)
|
277
|
+
# Check if this piece is the same type as another (ignoring side and state)
|
237
278
|
#
|
238
279
|
# @param other [Piece] piece to compare with
|
239
280
|
# @return [Boolean] true if same type
|
240
281
|
# @example
|
241
|
-
# king1.same_type?(king2) # K
|
282
|
+
# king1.same_type?(king2) # (:K, :first, :normal) and (:K, :second, :enhanced) => true
|
242
283
|
def same_type?(other)
|
243
284
|
return false unless other.is_a?(self.class)
|
285
|
+
|
244
286
|
type == other.type
|
245
287
|
end
|
246
288
|
|
247
|
-
# Check if this piece belongs to the same
|
289
|
+
# Check if this piece belongs to the same side as another
|
248
290
|
#
|
249
291
|
# @param other [Piece] piece to compare with
|
250
|
-
# @return [Boolean] true if same
|
251
|
-
def
|
292
|
+
# @return [Boolean] true if same side
|
293
|
+
def same_side?(other)
|
252
294
|
return false unless other.is_a?(self.class)
|
295
|
+
|
253
296
|
side == other.side
|
254
297
|
end
|
255
298
|
|
299
|
+
# Check if this piece has the same state as another
|
300
|
+
#
|
301
|
+
# @param other [Piece] piece to compare with
|
302
|
+
# @return [Boolean] true if same state
|
303
|
+
def same_state?(other)
|
304
|
+
return false unless other.is_a?(self.class)
|
305
|
+
|
306
|
+
state == other.state
|
307
|
+
end
|
308
|
+
|
256
309
|
# Custom equality comparison
|
257
310
|
#
|
258
311
|
# @param other [Object] object to compare with
|
@@ -260,9 +313,7 @@ module Sashite
|
|
260
313
|
def ==(other)
|
261
314
|
return false unless other.is_a?(self.class)
|
262
315
|
|
263
|
-
|
264
|
-
enhanced? == other.enhanced? &&
|
265
|
-
diminished? == other.diminished?
|
316
|
+
type == other.type && side == other.side && state == other.state
|
266
317
|
end
|
267
318
|
|
268
319
|
# Alias for == to ensure Set functionality works correctly
|
@@ -272,29 +323,37 @@ module Sashite
|
|
272
323
|
#
|
273
324
|
# @return [Integer] hash value
|
274
325
|
def hash
|
275
|
-
[self.class,
|
326
|
+
[self.class, type, side, state].hash
|
276
327
|
end
|
277
328
|
|
278
|
-
# Validate that the
|
329
|
+
# Validate that the type is a valid symbol
|
279
330
|
#
|
280
|
-
# @param
|
331
|
+
# @param type [Symbol] the type to validate
|
281
332
|
# @raise [ArgumentError] if invalid
|
282
|
-
def self.
|
283
|
-
|
284
|
-
return if letter_str.match?(/\A[a-zA-Z]\z/)
|
333
|
+
def self.validate_type(type)
|
334
|
+
return if VALID_TYPES.include?(type)
|
285
335
|
|
286
|
-
raise ::ArgumentError, format(
|
336
|
+
raise ::ArgumentError, format(ERROR_INVALID_TYPE, type.inspect)
|
287
337
|
end
|
288
338
|
|
289
|
-
# Validate that
|
339
|
+
# Validate that the side is a valid symbol
|
290
340
|
#
|
291
|
-
# @param
|
292
|
-
# @
|
293
|
-
|
294
|
-
|
295
|
-
|
341
|
+
# @param side [Symbol] the side to validate
|
342
|
+
# @raise [ArgumentError] if invalid
|
343
|
+
def self.validate_side(side)
|
344
|
+
return if VALID_SIDES.include?(side)
|
345
|
+
|
346
|
+
raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
|
347
|
+
end
|
348
|
+
|
349
|
+
# Validate that the state is a valid symbol
|
350
|
+
#
|
351
|
+
# @param state [Symbol] the state to validate
|
352
|
+
# @raise [ArgumentError] if invalid
|
353
|
+
def self.validate_state(state)
|
354
|
+
return if VALID_STATES.include?(state)
|
296
355
|
|
297
|
-
raise ::ArgumentError,
|
356
|
+
raise ::ArgumentError, format(ERROR_INVALID_STATE, state.inspect)
|
298
357
|
end
|
299
358
|
|
300
359
|
# Match PIN pattern against string
|
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
|
51
|
-
# Sashite::Pin.parse("+R") # => #<Pin::Piece
|
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"
|