sashite-pin 1.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 +7 -0
- data/LICENSE.md +22 -0
- data/README.md +403 -0
- data/lib/sashite/pin/piece.rb +329 -0
- data/lib/sashite/pin.rb +56 -0
- data/lib/sashite-pin.rb +20 -0
- metadata +57 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a65bc46335f587c689efc712bc26efc23c236ecf4aeaf26ab6fe2a47fc6420a6
|
4
|
+
data.tar.gz: 5c1cde47d08f270ab0594917001b0427792d5b931a7de60327e59fc6764f53d8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0c115d775f83ad50597f66478139720d9b6d1dfe970384d615ea1c25e96427b803dde10581a4349de1c0707064974d6f138ede38de175ed7168c5982482be9ea
|
7
|
+
data.tar.gz: ed8959e794d0d62ec73697b95a0ed18b19e150f9493e0db57d78c17d7bce1b122723847ef92470f528606eedec10900f3fce32a75360732e628b156da771f0ad
|
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2025 Cyril Kato
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,403 @@
|
|
1
|
+
# Pin.rb
|
2
|
+
|
3
|
+
[](https://github.com/sashite/pin.rb/tags)
|
4
|
+
[](https://rubydoc.info/github/sashite/pin.rb/main)
|
5
|
+

|
6
|
+
[](https://github.com/sashite/pin.rb/raw/main/LICENSE.md)
|
7
|
+
|
8
|
+
> **PIN** (Piece Identifier Notation) implementation for the Ruby language.
|
9
|
+
|
10
|
+
## What is PIN?
|
11
|
+
|
12
|
+
PIN (Piece Identifier Notation) provides an ASCII-based format for representing pieces in abstract strategy board games. PIN translates piece attributes from the [Game Protocol](https://sashite.dev/game-protocol/) into a compact, portable notation system.
|
13
|
+
|
14
|
+
This gem implements the [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/), providing a modern Ruby interface with immutable piece objects and functional programming principles.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# In your Gemfile
|
20
|
+
gem "sashite-pin"
|
21
|
+
```
|
22
|
+
|
23
|
+
Or install manually:
|
24
|
+
|
25
|
+
```sh
|
26
|
+
gem install sashite-pin
|
27
|
+
```
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
require "sashite-pin"
|
33
|
+
|
34
|
+
# Parse PIN strings into piece objects
|
35
|
+
piece = Sashite::Pin.parse("K") # => #<Pin::Piece letter="K" type="K" player=first>
|
36
|
+
piece.to_s # => "K"
|
37
|
+
piece.first_player? # => true
|
38
|
+
piece.type # => "K"
|
39
|
+
piece.side # => :first
|
40
|
+
|
41
|
+
# Validate PIN strings
|
42
|
+
Sashite::Pin.valid?("K") # => true
|
43
|
+
Sashite::Pin.valid?("+R") # => true
|
44
|
+
Sashite::Pin.valid?("invalid") # => false
|
45
|
+
|
46
|
+
# State manipulation (returns new immutable instances)
|
47
|
+
enhanced = piece.enhance # => #<Pin::Piece letter="K" type="K" player=first enhanced=true>
|
48
|
+
enhanced.to_s # => "+K"
|
49
|
+
diminished = piece.diminish # => #<Pin::Piece letter="K" type="K" player=first diminished=true>
|
50
|
+
diminished.to_s # => "-K"
|
51
|
+
|
52
|
+
# Player manipulation
|
53
|
+
flipped = piece.flip # => #<Pin::Piece letter="k" type="K" player=second>
|
54
|
+
flipped.to_s # => "k"
|
55
|
+
|
56
|
+
# State queries
|
57
|
+
piece.normal? # => true
|
58
|
+
enhanced.enhanced? # => true
|
59
|
+
diminished.diminished? # => true
|
60
|
+
|
61
|
+
# Type and player comparison
|
62
|
+
king1 = Sashite::Pin.parse("K")
|
63
|
+
king2 = Sashite::Pin.parse("k")
|
64
|
+
queen = Sashite::Pin.parse("Q")
|
65
|
+
|
66
|
+
king1.same_type?(king2) # => true (both kings)
|
67
|
+
king1.same_player?(queen) # => true (both first player)
|
68
|
+
king1.same_type?(queen) # => false (different types)
|
69
|
+
|
70
|
+
# Functional transformations can be chained
|
71
|
+
pawn = Sashite::Pin.parse("P")
|
72
|
+
enemy_promoted = pawn.flip.enhance # => "+p" (black promoted pawn)
|
73
|
+
```
|
74
|
+
|
75
|
+
## Format Specification
|
76
|
+
|
77
|
+
### Structure
|
78
|
+
```
|
79
|
+
[<state>]<letter>
|
80
|
+
```
|
81
|
+
|
82
|
+
### Components
|
83
|
+
|
84
|
+
- **Letter** (`A-Z`, `a-z`): Represents piece type and player
|
85
|
+
- Uppercase: First player pieces
|
86
|
+
- Lowercase: Second player pieces
|
87
|
+
- **State** (optional prefix):
|
88
|
+
- `+`: Enhanced state (promoted, upgraded, empowered)
|
89
|
+
- `-`: Diminished state (weakened, restricted, temporary)
|
90
|
+
- No prefix: Normal state
|
91
|
+
|
92
|
+
### Regular Expression
|
93
|
+
```ruby
|
94
|
+
/\A[-+]?[A-Za-z]\z/
|
95
|
+
```
|
96
|
+
|
97
|
+
### Examples
|
98
|
+
- `K` - First player king (normal state)
|
99
|
+
- `k` - Second player king (normal state)
|
100
|
+
- `+R` - First player rook (enhanced state)
|
101
|
+
- `-p` - Second player pawn (diminished state)
|
102
|
+
|
103
|
+
## Game Examples
|
104
|
+
|
105
|
+
### Western Chess
|
106
|
+
```ruby
|
107
|
+
# Standard pieces
|
108
|
+
king = Sashite::Pin.parse("K") # => white king
|
109
|
+
king.first_player? # => true
|
110
|
+
king.type # => "K"
|
111
|
+
|
112
|
+
# State modifiers for special conditions
|
113
|
+
castling_king = king.enhance # => castling-eligible king
|
114
|
+
castling_king.to_s # => "+K"
|
115
|
+
|
116
|
+
vulnerable_pawn = Sashite::Pin.parse("P").diminish # => en passant vulnerable
|
117
|
+
vulnerable_pawn.to_s # => "-P"
|
118
|
+
|
119
|
+
# 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
|
122
|
+
```
|
123
|
+
|
124
|
+
### Japanese Chess (Shōgi)
|
125
|
+
```ruby
|
126
|
+
# Basic pieces
|
127
|
+
rook = Sashite::Pin.parse("R") # => white rook
|
128
|
+
bishop = Sashite::Pin.parse("B") # => white bishop
|
129
|
+
|
130
|
+
# Promoted pieces (enhanced state)
|
131
|
+
dragon_king = rook.enhance # => promoted rook (Dragon King)
|
132
|
+
dragon_king.to_s # => "+R"
|
133
|
+
|
134
|
+
dragon_horse = bishop.enhance # => promoted bishop (Dragon Horse)
|
135
|
+
dragon_horse.to_s # => "+B"
|
136
|
+
|
137
|
+
# Promoted pawn
|
138
|
+
pawn = Sashite::Pin.parse("P")
|
139
|
+
tokin = pawn.enhance # => promoted pawn (Tokin)
|
140
|
+
tokin.to_s # => "+P"
|
141
|
+
|
142
|
+
# All promotable pieces can use the same pattern
|
143
|
+
promotable = %w[R B S N L P].map { |type| Sashite::Pin.parse(type) }
|
144
|
+
promoted = promotable.map(&:enhance)
|
145
|
+
```
|
146
|
+
|
147
|
+
### Thai Chess (Makruk)
|
148
|
+
```ruby
|
149
|
+
# Basic pieces
|
150
|
+
met = Sashite::Pin.parse("M") # => white Met (queen)
|
151
|
+
pawn = Sashite::Pin.parse("P") # => white Bia (pawn)
|
152
|
+
|
153
|
+
# Promoted pawns
|
154
|
+
bia_kaew = pawn.enhance # => promoted pawn (Bia Kaew)
|
155
|
+
bia_kaew.to_s # => "+P"
|
156
|
+
|
157
|
+
# Makruk pieces
|
158
|
+
makruk_pieces = %w[K M R B N P].map { |type| Sashite::Pin.parse(type) }
|
159
|
+
```
|
160
|
+
|
161
|
+
### Chinese Chess (Xiangqi)
|
162
|
+
```ruby
|
163
|
+
# 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"
|
167
|
+
|
168
|
+
# 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"
|
172
|
+
```
|
173
|
+
|
174
|
+
## API Reference
|
175
|
+
|
176
|
+
### Main Module Methods
|
177
|
+
|
178
|
+
- `Sashite::Pin.valid?(pin_string)` - Check if string is valid PIN notation
|
179
|
+
- `Sashite::Pin.parse(pin_string)` - Parse PIN string into Piece object
|
180
|
+
|
181
|
+
### Piece Class
|
182
|
+
|
183
|
+
#### Creation and Parsing
|
184
|
+
- `Sashite::Pin::Piece.new(letter, enhanced: false, diminished: false)` - Create piece instance
|
185
|
+
- `Sashite::Pin::Piece.parse(pin_string)` - Parse PIN string (same as module method)
|
186
|
+
|
187
|
+
#### String Representation
|
188
|
+
- `#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
|
+
|
194
|
+
#### State Queries
|
195
|
+
- `#normal?` - Check if normal state (no modifiers)
|
196
|
+
- `#enhanced?` - Check if enhanced state
|
197
|
+
- `#diminished?` - Check if diminished state
|
198
|
+
|
199
|
+
#### Player Queries
|
200
|
+
- `#first_player?` - Check if first player piece
|
201
|
+
- `#second_player?` - Check if second player piece
|
202
|
+
|
203
|
+
#### State Transformations (immutable - return new instances)
|
204
|
+
- `#enhance` - Create enhanced version
|
205
|
+
- `#unenhance` - Remove enhanced state
|
206
|
+
- `#diminish` - Create diminished version
|
207
|
+
- `#undiminish` - Remove diminished state
|
208
|
+
- `#normalize` - Remove all state modifiers
|
209
|
+
- `#flip` - Switch player (change case)
|
210
|
+
|
211
|
+
#### Comparison Methods
|
212
|
+
- `#same_type?(other)` - Check if same piece type
|
213
|
+
- `#same_player?(other)` - Check if same player
|
214
|
+
- `#==(other)` - Full equality comparison
|
215
|
+
|
216
|
+
### Constants
|
217
|
+
- `Sashite::Pin::PIN_REGEX` - Regular expression for PIN validation
|
218
|
+
|
219
|
+
## Advanced Usage
|
220
|
+
|
221
|
+
### Immutable Transformations
|
222
|
+
```ruby
|
223
|
+
# All transformations return new instances
|
224
|
+
original = Sashite::Pin.parse("K")
|
225
|
+
enhanced = original.enhance
|
226
|
+
diminished = original.diminish
|
227
|
+
|
228
|
+
# Original piece is never modified
|
229
|
+
puts original.to_s # => "K"
|
230
|
+
puts enhanced.to_s # => "+K"
|
231
|
+
puts diminished.to_s # => "-K"
|
232
|
+
|
233
|
+
# Transformations can be chained
|
234
|
+
result = original.flip.enhance.flip.diminish
|
235
|
+
puts result.to_s # => "-K"
|
236
|
+
```
|
237
|
+
|
238
|
+
### Game State Management
|
239
|
+
```ruby
|
240
|
+
class GameBoard
|
241
|
+
def initialize
|
242
|
+
@pieces = {}
|
243
|
+
end
|
244
|
+
|
245
|
+
def place(square, pin_string)
|
246
|
+
@pieces[square] = Sashite::Pin.parse(pin_string)
|
247
|
+
end
|
248
|
+
|
249
|
+
def promote(square)
|
250
|
+
piece = @pieces[square]
|
251
|
+
return nil unless piece&.normal? # Can only promote normal pieces
|
252
|
+
|
253
|
+
@pieces[square] = piece.enhance
|
254
|
+
end
|
255
|
+
|
256
|
+
def capture(from_square, to_square)
|
257
|
+
captured = @pieces[to_square]
|
258
|
+
@pieces[to_square] = @pieces.delete(from_square)
|
259
|
+
captured
|
260
|
+
end
|
261
|
+
|
262
|
+
def pieces_by_player(first_player: true)
|
263
|
+
@pieces.select { |_, piece| piece.first_player? == first_player }
|
264
|
+
end
|
265
|
+
|
266
|
+
def promoted_pieces
|
267
|
+
@pieces.select { |_, piece| piece.enhanced? }
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Usage
|
272
|
+
board = GameBoard.new
|
273
|
+
board.place("e1", "K")
|
274
|
+
board.place("e8", "k")
|
275
|
+
board.place("a7", "P")
|
276
|
+
|
277
|
+
# Promote pawn
|
278
|
+
board.promote("a7")
|
279
|
+
promoted = board.promoted_pieces
|
280
|
+
puts promoted.values.first.to_s # => "+P"
|
281
|
+
```
|
282
|
+
|
283
|
+
### Piece Analysis
|
284
|
+
```ruby
|
285
|
+
def analyze_pieces(pin_strings)
|
286
|
+
pieces = pin_strings.map { |pin| Sashite::Pin.parse(pin) }
|
287
|
+
|
288
|
+
{
|
289
|
+
total: pieces.size,
|
290
|
+
by_player: pieces.group_by(&:side),
|
291
|
+
by_type: pieces.group_by(&:type),
|
292
|
+
by_state: pieces.group_by(&:state),
|
293
|
+
promoted: pieces.count(&:enhanced?),
|
294
|
+
weakened: pieces.count(&:diminished?)
|
295
|
+
}
|
296
|
+
end
|
297
|
+
|
298
|
+
pins = %w[K Q +R B N P k q r +b n -p]
|
299
|
+
analysis = analyze_pieces(pins)
|
300
|
+
puts analysis[:by_player][:first].size # => 6
|
301
|
+
puts analysis[:promoted] # => 2
|
302
|
+
```
|
303
|
+
|
304
|
+
### Move Validation Example
|
305
|
+
```ruby
|
306
|
+
def can_promote?(piece, target_rank)
|
307
|
+
return false unless piece.normal? # Already promoted pieces can't promote again
|
308
|
+
|
309
|
+
case piece.type
|
310
|
+
when "P" # Pawn
|
311
|
+
(piece.first_player? && target_rank == 8) ||
|
312
|
+
(piece.second_player? && target_rank == 1)
|
313
|
+
when "R", "B", "S", "N", "L" # Shōgi pieces that can promote
|
314
|
+
true
|
315
|
+
else
|
316
|
+
false
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
pawn = Sashite::Pin.parse("P")
|
321
|
+
puts can_promote?(pawn, 8) # => true
|
322
|
+
|
323
|
+
promoted_pawn = pawn.enhance
|
324
|
+
puts can_promote?(promoted_pawn, 8) # => false (already promoted)
|
325
|
+
```
|
326
|
+
|
327
|
+
## Protocol Mapping
|
328
|
+
|
329
|
+
Following the [Game Protocol](https://sashite.dev/game-protocol/):
|
330
|
+
|
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 |
|
336
|
+
|
337
|
+
**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
|
+
|
339
|
+
## Properties
|
340
|
+
|
341
|
+
* **ASCII Compatible**: Maximum portability across systems
|
342
|
+
* **Rule-Agnostic**: Independent of specific game mechanics
|
343
|
+
* **Compact Format**: 1-2 characters per piece
|
344
|
+
* **Visual Distinction**: Clear player differentiation through case
|
345
|
+
* **Protocol Compliant**: Direct implementation of Sashité piece attributes
|
346
|
+
* **Immutable**: All piece instances are frozen and transformations return new objects
|
347
|
+
* **Functional**: Pure functions with no side effects
|
348
|
+
|
349
|
+
## System Constraints
|
350
|
+
|
351
|
+
- **Maximum 26 piece types** per game system (one per ASCII letter)
|
352
|
+
- **Exactly 2 players** (uppercase/lowercase distinction)
|
353
|
+
- **3 state levels** (enhanced, normal, diminished)
|
354
|
+
|
355
|
+
## Related Specifications
|
356
|
+
|
357
|
+
- [Game Protocol](https://sashite.dev/game-protocol/) - Conceptual foundation for abstract strategy board games
|
358
|
+
- [PNN](https://sashite.dev/specs/pnn/) - Piece Name Notation (style-aware piece representation)
|
359
|
+
- [CELL](https://sashite.dev/specs/cell/) - Board position coordinates
|
360
|
+
- [HAND](https://sashite.dev/specs/hand/) - Reserve location notation
|
361
|
+
- [PMN](https://sashite.dev/specs/pmn/) - Portable Move Notation
|
362
|
+
|
363
|
+
## Documentation
|
364
|
+
|
365
|
+
- [Official PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/)
|
366
|
+
- [PIN Examples Documentation](https://sashite.dev/specs/pin/1.0.0/examples/)
|
367
|
+
- [Game Protocol Foundation](https://sashite.dev/game-protocol/)
|
368
|
+
- [API Documentation](https://rubydoc.info/github/sashite/pin.rb/main)
|
369
|
+
|
370
|
+
## Development
|
371
|
+
|
372
|
+
```sh
|
373
|
+
# Clone the repository
|
374
|
+
git clone https://github.com/sashite/pin.rb.git
|
375
|
+
cd pin.rb
|
376
|
+
|
377
|
+
# Install dependencies
|
378
|
+
bundle install
|
379
|
+
|
380
|
+
# Run tests
|
381
|
+
ruby test.rb
|
382
|
+
|
383
|
+
# Generate documentation
|
384
|
+
yard doc
|
385
|
+
```
|
386
|
+
|
387
|
+
## Contributing
|
388
|
+
|
389
|
+
1. Fork the repository
|
390
|
+
2. Create a feature branch (`git checkout -b feature/new-feature`)
|
391
|
+
3. Add tests for your changes
|
392
|
+
4. Ensure all tests pass (`ruby test.rb`)
|
393
|
+
5. Commit your changes (`git commit -am 'Add new feature'`)
|
394
|
+
6. Push to the branch (`git push origin feature/new-feature`)
|
395
|
+
7. Create a Pull Request
|
396
|
+
|
397
|
+
## License
|
398
|
+
|
399
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
400
|
+
|
401
|
+
## About
|
402
|
+
|
403
|
+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|
@@ -0,0 +1,329 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Pin
|
5
|
+
# Represents a piece in PIN (Piece Identifier Notation) format.
|
6
|
+
#
|
7
|
+
# A piece consists of a single ASCII letter with optional state modifiers:
|
8
|
+
# - Enhanced state: prefix '+'
|
9
|
+
# - Diminished state: prefix '-'
|
10
|
+
# - Normal state: no modifier
|
11
|
+
#
|
12
|
+
# The case of the letter determines ownership:
|
13
|
+
# - Uppercase (A-Z): first player
|
14
|
+
# - Lowercase (a-z): second player
|
15
|
+
#
|
16
|
+
# All instances are immutable - state manipulation methods return new instances.
|
17
|
+
# This follows the Game Protocol's piece model with Type, Side, and State attributes.
|
18
|
+
class Piece
|
19
|
+
# PIN validation pattern matching the specification
|
20
|
+
PIN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])\z/
|
21
|
+
|
22
|
+
# Valid state modifiers
|
23
|
+
ENHANCED_PREFIX = "+"
|
24
|
+
DIMINISHED_PREFIX = "-"
|
25
|
+
|
26
|
+
# Error messages
|
27
|
+
ERROR_INVALID_PIN = "Invalid PIN string: %s"
|
28
|
+
ERROR_INVALID_LETTER = "Letter must be a single ASCII letter (a-z or A-Z): %s"
|
29
|
+
|
30
|
+
# @return [String] the base letter identifier (type + side)
|
31
|
+
attr_reader :letter
|
32
|
+
|
33
|
+
# Create a new piece instance
|
34
|
+
#
|
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
|
38
|
+
# @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)
|
42
|
+
|
43
|
+
@letter = letter.freeze
|
44
|
+
@enhanced = enhanced
|
45
|
+
@diminished = diminished
|
46
|
+
|
47
|
+
freeze
|
48
|
+
end
|
49
|
+
|
50
|
+
# Parse a PIN string into a Piece object
|
51
|
+
#
|
52
|
+
# @param pin_string [String] PIN notation string
|
53
|
+
# @return [Piece] new piece instance
|
54
|
+
# @raise [ArgumentError] if the PIN string is invalid
|
55
|
+
# @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>
|
59
|
+
def self.parse(pin_string)
|
60
|
+
string_value = String(pin_string)
|
61
|
+
matches = match_pattern(string_value)
|
62
|
+
|
63
|
+
letter = matches[:letter]
|
64
|
+
enhanced = matches[:prefix] == ENHANCED_PREFIX
|
65
|
+
diminished = matches[:prefix] == DIMINISHED_PREFIX
|
66
|
+
|
67
|
+
new(
|
68
|
+
letter,
|
69
|
+
enhanced: enhanced,
|
70
|
+
diminished: diminished
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Convert the piece to its PIN string representation
|
75
|
+
#
|
76
|
+
# @return [String] PIN notation string
|
77
|
+
# @example
|
78
|
+
# piece.to_s # => "+R"
|
79
|
+
# piece.to_s # => "-p"
|
80
|
+
# piece.to_s # => "K"
|
81
|
+
def to_s
|
82
|
+
prefix = if @enhanced
|
83
|
+
ENHANCED_PREFIX
|
84
|
+
else
|
85
|
+
(@diminished ? DIMINISHED_PREFIX : "")
|
86
|
+
end
|
87
|
+
|
88
|
+
"#{prefix}#{letter}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Create a new piece with enhanced state
|
92
|
+
#
|
93
|
+
# @return [Piece] new piece instance with enhanced state
|
94
|
+
# @example
|
95
|
+
# piece.enhance # k => +k
|
96
|
+
def enhance
|
97
|
+
return self if enhanced?
|
98
|
+
|
99
|
+
self.class.new(
|
100
|
+
letter,
|
101
|
+
enhanced: true,
|
102
|
+
diminished: false
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Create a new piece without enhanced state
|
107
|
+
#
|
108
|
+
# @return [Piece] new piece instance without enhanced state
|
109
|
+
# @example
|
110
|
+
# piece.unenhance # +k => k
|
111
|
+
def unenhance
|
112
|
+
return self unless enhanced?
|
113
|
+
|
114
|
+
self.class.new(
|
115
|
+
letter,
|
116
|
+
enhanced: false,
|
117
|
+
diminished: @diminished
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Create a new piece with diminished state
|
122
|
+
#
|
123
|
+
# @return [Piece] new piece instance with diminished state
|
124
|
+
# @example
|
125
|
+
# piece.diminish # k => -k
|
126
|
+
def diminish
|
127
|
+
return self if diminished?
|
128
|
+
|
129
|
+
self.class.new(
|
130
|
+
letter,
|
131
|
+
enhanced: false,
|
132
|
+
diminished: true
|
133
|
+
)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Create a new piece without diminished state
|
137
|
+
#
|
138
|
+
# @return [Piece] new piece instance without diminished state
|
139
|
+
# @example
|
140
|
+
# piece.undiminish # -k => k
|
141
|
+
def undiminish
|
142
|
+
return self unless diminished?
|
143
|
+
|
144
|
+
self.class.new(
|
145
|
+
letter,
|
146
|
+
enhanced: @enhanced,
|
147
|
+
diminished: false
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Create a new piece with normal state (no modifiers)
|
152
|
+
#
|
153
|
+
# @return [Piece] new piece instance with normal state
|
154
|
+
# @example
|
155
|
+
# piece.normalize # +k => k, -k => k
|
156
|
+
def normalize
|
157
|
+
return self if normal?
|
158
|
+
|
159
|
+
self.class.new(letter)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Create a new piece with opposite ownership (case)
|
163
|
+
#
|
164
|
+
# @return [Piece] new piece instance with flipped case
|
165
|
+
# @example
|
166
|
+
# piece.flip # K => k, k => K
|
167
|
+
def flip
|
168
|
+
flipped_letter = letter.swapcase
|
169
|
+
|
170
|
+
self.class.new(
|
171
|
+
flipped_letter,
|
172
|
+
enhanced: @enhanced,
|
173
|
+
diminished: @diminished
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Check if the piece has enhanced state
|
178
|
+
#
|
179
|
+
# @return [Boolean] true if enhanced
|
180
|
+
def enhanced?
|
181
|
+
@enhanced
|
182
|
+
end
|
183
|
+
|
184
|
+
# Check if the piece has diminished state
|
185
|
+
#
|
186
|
+
# @return [Boolean] true if diminished
|
187
|
+
def diminished?
|
188
|
+
@diminished
|
189
|
+
end
|
190
|
+
|
191
|
+
# Check if the piece has normal state (no modifiers)
|
192
|
+
#
|
193
|
+
# @return [Boolean] true if no modifiers are present
|
194
|
+
def normal?
|
195
|
+
!enhanced? && !diminished?
|
196
|
+
end
|
197
|
+
|
198
|
+
# Check if the piece belongs to the first player (uppercase)
|
199
|
+
#
|
200
|
+
# @return [Boolean] true if uppercase letter
|
201
|
+
def first_player?
|
202
|
+
letter == letter.upcase
|
203
|
+
end
|
204
|
+
|
205
|
+
# Check if the piece belongs to the second player (lowercase)
|
206
|
+
#
|
207
|
+
# @return [Boolean] true if lowercase letter
|
208
|
+
def second_player?
|
209
|
+
letter == letter.downcase
|
210
|
+
end
|
211
|
+
|
212
|
+
# Get the piece type (uppercase letter regardless of player)
|
213
|
+
#
|
214
|
+
# @return [String] uppercase letter representing the piece type
|
215
|
+
# @example
|
216
|
+
# piece.type # "k" => "K", "R" => "R", "+p" => "P"
|
217
|
+
def type
|
218
|
+
letter.upcase
|
219
|
+
end
|
220
|
+
|
221
|
+
# Get the player side based on letter case
|
222
|
+
#
|
223
|
+
# @return [Symbol] :first or :second
|
224
|
+
def side
|
225
|
+
first_player? ? :first : :second
|
226
|
+
end
|
227
|
+
|
228
|
+
# Get the state as a symbol
|
229
|
+
#
|
230
|
+
# @return [Symbol] :enhanced, :diminished, or :normal
|
231
|
+
def state
|
232
|
+
return :enhanced if enhanced?
|
233
|
+
return :diminished if diminished?
|
234
|
+
|
235
|
+
:normal
|
236
|
+
end
|
237
|
+
|
238
|
+
# Check if this piece is the same type as another (ignoring player and state)
|
239
|
+
#
|
240
|
+
# @param other [Piece] piece to compare with
|
241
|
+
# @return [Boolean] true if same type
|
242
|
+
# @example
|
243
|
+
# king1.same_type?(king2) # K and k => true, K and Q => false
|
244
|
+
def same_type?(other)
|
245
|
+
return false unless other.is_a?(self.class)
|
246
|
+
|
247
|
+
type == other.type
|
248
|
+
end
|
249
|
+
|
250
|
+
# Check if this piece belongs to the same player as another
|
251
|
+
#
|
252
|
+
# @param other [Piece] piece to compare with
|
253
|
+
# @return [Boolean] true if same player
|
254
|
+
def same_player?(other)
|
255
|
+
return false unless other.is_a?(self.class)
|
256
|
+
|
257
|
+
side == other.side
|
258
|
+
end
|
259
|
+
|
260
|
+
# Custom equality comparison
|
261
|
+
#
|
262
|
+
# @param other [Object] object to compare with
|
263
|
+
# @return [Boolean] true if pieces are equal
|
264
|
+
def ==(other)
|
265
|
+
return false unless other.is_a?(self.class)
|
266
|
+
|
267
|
+
letter == other.letter &&
|
268
|
+
enhanced? == other.enhanced? &&
|
269
|
+
diminished? == other.diminished?
|
270
|
+
end
|
271
|
+
|
272
|
+
# Custom hash implementation for use in collections
|
273
|
+
#
|
274
|
+
# @return [Integer] hash value
|
275
|
+
def hash
|
276
|
+
[letter, @enhanced, @diminished].hash
|
277
|
+
end
|
278
|
+
|
279
|
+
# Custom inspection for debugging
|
280
|
+
#
|
281
|
+
# @return [String] detailed string representation
|
282
|
+
def inspect
|
283
|
+
modifiers = []
|
284
|
+
modifiers << "enhanced=true" if enhanced?
|
285
|
+
modifiers << "diminished=true" if diminished?
|
286
|
+
|
287
|
+
modifier_str = modifiers.empty? ? "" : " #{modifiers.join(' ')}"
|
288
|
+
player = first_player? ? "first" : "second"
|
289
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} letter='#{letter}' type='#{type}' player=#{player}#{modifier_str}>"
|
290
|
+
end
|
291
|
+
|
292
|
+
# Validate that the letter is a single ASCII letter
|
293
|
+
#
|
294
|
+
# @param letter [String] the letter to validate
|
295
|
+
# @raise [ArgumentError] if invalid
|
296
|
+
def self.validate_letter(letter)
|
297
|
+
letter_str = String(letter)
|
298
|
+
return if letter_str.match?(/\A[a-zA-Z]\z/)
|
299
|
+
|
300
|
+
raise ::ArgumentError, format(ERROR_INVALID_LETTER, letter_str)
|
301
|
+
end
|
302
|
+
|
303
|
+
# Validate that enhanced and diminished states are not both true
|
304
|
+
#
|
305
|
+
# @param enhanced [Boolean] enhanced state
|
306
|
+
# @param diminished [Boolean] diminished state
|
307
|
+
# @raise [ArgumentError] if both are true
|
308
|
+
def self.validate_state_combination(enhanced, diminished)
|
309
|
+
return unless enhanced && diminished
|
310
|
+
|
311
|
+
raise ::ArgumentError, "A piece cannot be both enhanced and diminished"
|
312
|
+
end
|
313
|
+
|
314
|
+
# Match PIN pattern against string
|
315
|
+
#
|
316
|
+
# @param string [String] string to match
|
317
|
+
# @return [MatchData] match data
|
318
|
+
# @raise [ArgumentError] if string doesn't match
|
319
|
+
def self.match_pattern(string)
|
320
|
+
matches = PIN_PATTERN.match(string)
|
321
|
+
return matches if matches
|
322
|
+
|
323
|
+
raise ::ArgumentError, format(ERROR_INVALID_PIN, string)
|
324
|
+
end
|
325
|
+
|
326
|
+
private_class_method :match_pattern
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
data/lib/sashite/pin.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "pin/piece"
|
4
|
+
|
5
|
+
module Sashite
|
6
|
+
# PIN (Piece Identifier Notation) implementation for Ruby
|
7
|
+
#
|
8
|
+
# Provides ASCII-based format for representing pieces in abstract strategy board games.
|
9
|
+
# PIN translates piece attributes from the Game Protocol into a compact, portable notation system.
|
10
|
+
#
|
11
|
+
# Format: [<state>]<letter>
|
12
|
+
# - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
|
13
|
+
# - Letter: A-Z (first player), a-z (second player)
|
14
|
+
#
|
15
|
+
# Examples:
|
16
|
+
# "K" - First player king (normal state)
|
17
|
+
# "k" - Second player king (normal state)
|
18
|
+
# "+R" - First player rook (enhanced state)
|
19
|
+
# "-p" - Second player pawn (diminished state)
|
20
|
+
#
|
21
|
+
# See: https://sashite.dev/specs/pin/1.0.0/
|
22
|
+
module Pin
|
23
|
+
# Regular expression for PIN validation
|
24
|
+
# Matches: optional state modifier followed by a single letter
|
25
|
+
PIN_REGEX = /\A[-+]?[A-Za-z]\z/
|
26
|
+
|
27
|
+
# Check if a string is a valid PIN notation
|
28
|
+
#
|
29
|
+
# @param pin [String] The string to validate
|
30
|
+
# @return [Boolean] true if valid PIN, false otherwise
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# Sashite::Pin.valid?("K") # => true
|
34
|
+
# Sashite::Pin.valid?("+R") # => true
|
35
|
+
# Sashite::Pin.valid?("-p") # => true
|
36
|
+
# Sashite::Pin.valid?("KK") # => false
|
37
|
+
# Sashite::Pin.valid?("++K") # => false
|
38
|
+
def self.valid?(pin)
|
39
|
+
return false unless pin.is_a?(::String)
|
40
|
+
|
41
|
+
pin.match?(PIN_REGEX)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Parse a PIN string into a Piece object
|
45
|
+
#
|
46
|
+
# @param pin_string [String] PIN notation string
|
47
|
+
# @return [Pin::Piece] new piece instance
|
48
|
+
# @raise [ArgumentError] if the PIN string is invalid
|
49
|
+
# @example
|
50
|
+
# Sashite::Pin.parse("K") # => #<Pin::Piece letter="K">
|
51
|
+
# Sashite::Pin.parse("+R") # => #<Pin::Piece letter="R" enhanced=true>
|
52
|
+
def self.parse(pin_string)
|
53
|
+
Piece.parse(pin_string)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/sashite-pin.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sashité namespace for board game notation libraries
|
4
|
+
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
|
+
end
|
19
|
+
|
20
|
+
require_relative "sashite/pin"
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sashite-pin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cyril Kato
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: |
|
13
|
+
PIN (Piece Identifier Notation) provides an ASCII-based format for representing pieces
|
14
|
+
in abstract strategy board games. This gem implements the PIN Specification v1.0.0 with
|
15
|
+
a modern Ruby interface featuring immutable piece objects and functional programming
|
16
|
+
principles. PIN translates piece attributes from the Game Protocol into a compact,
|
17
|
+
portable notation system using ASCII letters with optional state modifiers and
|
18
|
+
case-based player group classification.
|
19
|
+
email: contact@cyril.email
|
20
|
+
executables: []
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- LICENSE.md
|
25
|
+
- README.md
|
26
|
+
- lib/sashite-pin.rb
|
27
|
+
- lib/sashite/pin.rb
|
28
|
+
- lib/sashite/pin/piece.rb
|
29
|
+
homepage: https://github.com/sashite/pin.rb
|
30
|
+
licenses:
|
31
|
+
- MIT
|
32
|
+
metadata:
|
33
|
+
bug_tracker_uri: https://github.com/sashite/pin.rb/issues
|
34
|
+
documentation_uri: https://rubydoc.info/github/sashite/pin.rb/main
|
35
|
+
homepage_uri: https://github.com/sashite/pin.rb
|
36
|
+
source_code_uri: https://github.com/sashite/pin.rb
|
37
|
+
specification_uri: https://sashite.dev/specs/pin/1.0.0/
|
38
|
+
rubygems_mfa_required: 'true'
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.2.0
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubygems_version: 3.6.9
|
54
|
+
specification_version: 4
|
55
|
+
summary: PIN (Piece Identifier Notation) implementation for Ruby with immutable piece
|
56
|
+
objects
|
57
|
+
test_files: []
|