sashite-cell 1.0.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 +165 -242
- data/lib/sashite/cell.rb +230 -39
- data/lib/sashite-cell.rb +0 -7
- metadata +7 -9
- data/lib/sashite/cell/location/hand_char.rb +0 -16
- data/lib/sashite/cell/location.rb +0 -136
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4fefcdbade2adc694035b6fbff032fb8e03466e35406a67f2a9d1c61b9fd600
|
4
|
+
data.tar.gz: 42c3150a201310805948eaa2e9e6bf05fc9c198925024d7a203f0093d6ee969a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 185910faaa10494b20d39b58ac3f25007d432d63d230de76cf666762a215ab8177283c0fe167efc28fdc24b4abcc55bdb25211fc9d216856b62d889005e57c8c
|
7
|
+
data.tar.gz: 43fe0766f8fe932e2a57dec85c89a07db4f30a543319f035b7e893a8122cfaf30ed90a635515bb9867a1509dc9e374ecf3c0b16c6efb39b419413a4811aebfe2
|
data/README.md
CHANGED
@@ -5,13 +5,13 @@
|
|
5
5
|

|
6
6
|
[](https://github.com/sashite/cell.rb/raw/main/LICENSE.md)
|
7
7
|
|
8
|
-
> **CELL** (
|
8
|
+
> **CELL** (Cell Encoding Location Label) support for the Ruby language.
|
9
9
|
|
10
10
|
## What is CELL?
|
11
11
|
|
12
|
-
CELL (
|
12
|
+
CELL (Cell Encoding Location Label) is a standardized format for representing coordinates on multi-dimensional game boards using a cyclical ASCII character system. CELL supports unlimited dimensional coordinate systems through the systematic repetition of three distinct character sets.
|
13
13
|
|
14
|
-
This gem implements the [CELL Specification v1.0.0](https://sashite.dev/documents/cell/1.0.0/), providing a Ruby interface for working with game
|
14
|
+
This gem implements the [CELL Specification v1.0.0](https://sashite.dev/documents/cell/1.0.0/), providing a Ruby interface for working with multi-dimensional game coordinates through a clean, functional API.
|
15
15
|
|
16
16
|
## Installation
|
17
17
|
|
@@ -28,319 +28,242 @@ gem install sashite-cell
|
|
28
28
|
|
29
29
|
## CELL Format
|
30
30
|
|
31
|
-
|
31
|
+
CELL uses a cyclical three-character-set system that repeats indefinitely based on dimensional position:
|
32
32
|
|
33
|
-
|
33
|
+
**Dimension (n % 3 = 1)**: Latin Lowercase Letters
|
34
|
+
- `a`, `b`, `c`, ..., `z`, `aa`, `ab`, ..., `zz`, `aaa`, ...
|
34
35
|
|
35
|
-
|
36
|
+
**Dimension (n % 3 = 2)**: Arabic Numerals
|
37
|
+
- `1`, `2`, `3`, ..., `25`, `26`, ...
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
5c # Shōgi notation
|
40
|
-
A3a # 3D coordinate
|
41
|
-
center # Custom coordinate
|
42
|
-
```
|
43
|
-
|
44
|
-
### Hand/Reserve Location
|
45
|
-
|
46
|
-
The reserved character `*` represents pieces held off-board:
|
47
|
-
```
|
48
|
-
* # Hand/reserve location
|
49
|
-
```
|
39
|
+
**Dimension (n % 3 = 0)**: Latin Uppercase Letters
|
40
|
+
- `A`, `B`, `C`, ..., `Z`, `AA`, `AB`, ..., `ZZ`, `AAA`, ...
|
50
41
|
|
51
42
|
## Basic Usage
|
52
43
|
|
53
|
-
###
|
44
|
+
### Validation
|
54
45
|
|
55
|
-
The primary
|
46
|
+
The primary functionality is validating CELL coordinates:
|
56
47
|
|
57
48
|
```ruby
|
58
49
|
require "sashite/cell"
|
59
50
|
|
60
|
-
#
|
61
|
-
|
62
|
-
# =>
|
63
|
-
|
64
|
-
|
65
|
-
# =>
|
66
|
-
|
67
|
-
#
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
#
|
72
|
-
location = Sashite::Cell.location("e4")
|
51
|
+
# Check if a string represents a valid CELL coordinate
|
52
|
+
Sashite::Cell.valid?("a1") # => true (2D coordinate)
|
53
|
+
Sashite::Cell.valid?("a1A") # => true (3D coordinate)
|
54
|
+
Sashite::Cell.valid?("e4") # => true (2D coordinate)
|
55
|
+
Sashite::Cell.valid?("h8Hh8") # => true (5D coordinate)
|
56
|
+
Sashite::Cell.valid?("*") # => false (not a CELL coordinate)
|
57
|
+
Sashite::Cell.valid?("a0") # => false (invalid numeral)
|
58
|
+
Sashite::Cell.valid?("") # => false (empty string)
|
59
|
+
|
60
|
+
# Alias for convenience
|
61
|
+
Cell = Sashite::Cell
|
62
|
+
Cell.valid?("a1") # => true
|
73
63
|
```
|
74
64
|
|
75
|
-
###
|
76
|
-
|
77
|
-
Convert a location object back to its CELL string representation:
|
65
|
+
### Dimensional Analysis
|
78
66
|
|
79
67
|
```ruby
|
80
|
-
|
81
|
-
|
82
|
-
# =>
|
83
|
-
|
84
|
-
|
85
|
-
hand.to_s
|
86
|
-
# => "*"
|
87
|
-
```
|
88
|
-
|
89
|
-
### Checking Location Types
|
90
|
-
|
91
|
-
Distinguish between board coordinates and hand/reserve locations:
|
68
|
+
# Get the number of dimensions in a coordinate
|
69
|
+
Sashite::Cell.dimensions("a1") # => 2
|
70
|
+
Sashite::Cell.dimensions("a1A") # => 3
|
71
|
+
Sashite::Cell.dimensions("h8Hh8") # => 5
|
72
|
+
Sashite::Cell.dimensions("foobar") # => 1
|
92
73
|
|
93
|
-
|
94
|
-
|
95
|
-
|
74
|
+
# Parse coordinate into dimensional components
|
75
|
+
Sashite::Cell.parse("a1A")
|
76
|
+
# => ["a", "1", "A"]
|
96
77
|
|
97
|
-
|
98
|
-
|
78
|
+
Sashite::Cell.parse("h8Hh8")
|
79
|
+
# => ["h", "8", "H", "h", "8"]
|
99
80
|
|
100
|
-
|
101
|
-
|
81
|
+
Sashite::Cell.parse("foobar")
|
82
|
+
# => ["foobar"]
|
102
83
|
```
|
103
84
|
|
104
|
-
|
105
|
-
|
106
|
-
### Chess
|
85
|
+
### Coordinate Conversion
|
107
86
|
|
108
87
|
```ruby
|
109
|
-
#
|
110
|
-
|
88
|
+
# Convert coordinates to arrays of integers (0-indexed)
|
89
|
+
Sashite::Cell.to_indices("a1")
|
90
|
+
# => [0, 0]
|
111
91
|
|
112
|
-
|
113
|
-
|
114
|
-
return false unless location.board?
|
92
|
+
Sashite::Cell.to_indices("e4")
|
93
|
+
# => [4, 3]
|
115
94
|
|
116
|
-
|
117
|
-
|
118
|
-
coord[0].between?("a", "h") &&
|
119
|
-
coord[1].between?("1", "8")
|
120
|
-
end
|
95
|
+
Sashite::Cell.to_indices("a1A")
|
96
|
+
# => [0, 0, 0]
|
121
97
|
|
122
|
-
|
123
|
-
|
124
|
-
|
98
|
+
# Convert arrays of integers back to CELL coordinates
|
99
|
+
Sashite::Cell.from_indices(0, 0)
|
100
|
+
# => "a1"
|
125
101
|
|
126
|
-
|
102
|
+
Sashite::Cell.from_indices(4, 3)
|
103
|
+
# => "e4"
|
127
104
|
|
128
|
-
|
129
|
-
#
|
130
|
-
board_positions = %w[9a 5e 1i].map { |coord| Sashite::Cell::Location.parse(coord) }
|
131
|
-
hand_position = Sashite::Cell::Location.parse("*")
|
132
|
-
|
133
|
-
# Group by location type
|
134
|
-
positions = board_positions + [hand_position]
|
135
|
-
grouped = positions.group_by(&:hand?)
|
136
|
-
# => {false => [board positions], true => [hand position]}
|
105
|
+
Sashite::Cell.from_indices(0, 0, 0)
|
106
|
+
# => "a1A"
|
137
107
|
```
|
138
108
|
|
139
|
-
|
109
|
+
## Usage Examples
|
140
110
|
|
141
|
-
|
142
|
-
# Go coordinates (traditional notation)
|
143
|
-
go_positions = %w[A1 T19 K10].map { |coord| Sashite::Cell::Location.parse(coord) }
|
111
|
+
### Chess Board (8x8)
|
144
112
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
113
|
+
```ruby
|
114
|
+
# Standard chess notation mapping
|
115
|
+
chess_squares = %w[a1 b1 c1 d1 e1 f1 g1 h1
|
116
|
+
a2 b2 c2 d2 e2 f2 g2 h2
|
117
|
+
a3 b3 c3 d3 e3 f3 g3 h3
|
118
|
+
a4 b4 c4 d4 e4 f4 g4 h4
|
119
|
+
a5 b5 c5 d5 e5 f5 g5 h5
|
120
|
+
a6 b6 c6 d6 e6 f6 g6 h6
|
121
|
+
a7 b7 c7 d7 e7 f7 g7 h7
|
122
|
+
a8 b8 c8 d8 e8 f8 g8 h8]
|
123
|
+
|
124
|
+
chess_squares.all? { |square| Sashite::Cell.valid?(square) }
|
125
|
+
# => true
|
158
126
|
```
|
159
127
|
|
160
|
-
###
|
128
|
+
### Shogi Board (9x9)
|
161
129
|
|
162
130
|
```ruby
|
163
|
-
#
|
164
|
-
|
165
|
-
|
166
|
-
#
|
167
|
-
|
168
|
-
corner = Sashite::Cell::Location.parse("NE")
|
169
|
-
|
170
|
-
# Hexagonal coordinates
|
171
|
-
hex_coord = Sashite::Cell::Location.parse("Q3R7")
|
131
|
+
# Japanese shogi uses 9x9 board
|
132
|
+
shogi_position = "5e" # 5th file, e rank
|
133
|
+
Sashite::Cell.valid?(shogi_position) # => true
|
134
|
+
Sashite::Cell.dimensions(shogi_position) # => 2
|
135
|
+
Sashite::Cell.to_indices(shogi_position) # => [4, 4]
|
172
136
|
```
|
173
137
|
|
174
|
-
|
175
|
-
|
176
|
-
### Working with Collections
|
138
|
+
### 3D Tic-Tac-Toe (3x3x3)
|
177
139
|
|
178
140
|
```ruby
|
179
|
-
#
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
Sashite::Cell::Location.parse("*"),
|
184
|
-
Sashite::Cell::Location.parse("a1")
|
185
|
-
]
|
186
|
-
|
187
|
-
# Separate board from hand locations
|
188
|
-
board_locations = locations.select(&:board?)
|
189
|
-
hand_locations = locations.select(&:hand?)
|
190
|
-
|
191
|
-
# Convert collection to strings
|
192
|
-
coordinates = locations.map(&:to_s)
|
193
|
-
# => ["e4", "d5", "*", "a1"]
|
141
|
+
# Three-dimensional game coordinates
|
142
|
+
positions_3d = %w[a1A b2B c3C a2B b3C c1A]
|
143
|
+
positions_3d.all? { |pos| Sashite::Cell.valid?(pos) && Sashite::Cell.dimensions(pos) == 3 }
|
144
|
+
# => true
|
194
145
|
```
|
195
146
|
|
196
|
-
###
|
147
|
+
### Multi-dimensional Coordinates
|
197
148
|
|
198
149
|
```ruby
|
199
|
-
#
|
200
|
-
|
201
|
-
|
202
|
-
"black_king" => Sashite::Cell::Location.parse("e8"),
|
203
|
-
"white_rook" => Sashite::Cell::Location.parse("a1"),
|
204
|
-
"captured_pieces" => Sashite::Cell::Location.parse("*")
|
205
|
-
}
|
206
|
-
|
207
|
-
# Find pieces on specific ranks/files
|
208
|
-
def pieces_on_file(locations, file)
|
209
|
-
locations.select do |piece, location|
|
210
|
-
location.board? && location.to_s.start_with?(file)
|
211
|
-
end
|
212
|
-
end
|
150
|
+
# Higher dimensional coordinates
|
151
|
+
coord_4d = "a1Aa"
|
152
|
+
coord_5d = "b2Bb2"
|
213
153
|
|
214
|
-
|
154
|
+
Sashite::Cell.dimensions(coord_4d) # => 4
|
155
|
+
Sashite::Cell.dimensions(coord_5d) # => 5
|
156
|
+
|
157
|
+
# Parse into components
|
158
|
+
Sashite::Cell.parse(coord_4d) # => ["a", "1", "A", "a"]
|
159
|
+
Sashite::Cell.parse(coord_5d) # => ["b", "2", "B", "b", "2"]
|
215
160
|
```
|
216
161
|
|
217
|
-
|
162
|
+
## API Reference
|
218
163
|
|
219
|
-
|
220
|
-
# Check validity before parsing
|
221
|
-
Sashite::Cell.valid?("e4") # => true
|
222
|
-
Sashite::Cell.valid?("*") # => true
|
223
|
-
Sashite::Cell.valid?("") # => false
|
224
|
-
Sashite::Cell.valid?("e-4") # => false
|
225
|
-
Sashite::Cell.valid?("@") # => false
|
226
|
-
|
227
|
-
# Safe parsing
|
228
|
-
def safe_parse(coord_string)
|
229
|
-
return nil unless Sashite::Cell.valid?(coord_string)
|
230
|
-
|
231
|
-
Sashite::Cell::Location.parse(coord_string)
|
232
|
-
rescue ArgumentError
|
233
|
-
nil
|
234
|
-
end
|
164
|
+
### Module Methods
|
235
165
|
|
236
|
-
|
237
|
-
|
238
|
-
Sashite::Cell::Location.parse("")
|
239
|
-
rescue ArgumentError => e
|
240
|
-
puts "Invalid coordinate: #{e.message}"
|
241
|
-
end
|
242
|
-
```
|
166
|
+
#### Validation
|
167
|
+
- `Sashite::Cell.valid?(string)` - Check if string represents a valid CELL coordinate
|
243
168
|
|
244
|
-
|
169
|
+
#### Analysis
|
170
|
+
- `Sashite::Cell.dimensions(string)` - Get number of dimensions
|
171
|
+
- `Sashite::Cell.parse(string)` - Parse coordinate into dimensional components array
|
245
172
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
def initialize(from, to)
|
250
|
-
@from = Sashite::Cell::Location.parse(from)
|
251
|
-
@to = Sashite::Cell::Location.parse(to)
|
252
|
-
end
|
253
|
-
|
254
|
-
def from_board?
|
255
|
-
@from.board?
|
256
|
-
end
|
257
|
-
|
258
|
-
def to_board?
|
259
|
-
@to.board?
|
260
|
-
end
|
261
|
-
|
262
|
-
def drop_move?
|
263
|
-
@from.hand? && @to.board?
|
264
|
-
end
|
265
|
-
|
266
|
-
def capture_move?
|
267
|
-
@from.board? && @to.board?
|
268
|
-
end
|
269
|
-
|
270
|
-
def to_s
|
271
|
-
"#{@from}→#{@to}"
|
272
|
-
end
|
273
|
-
end
|
173
|
+
#### Conversion
|
174
|
+
- `Sashite::Cell.to_indices(string)` - Convert CELL coordinate to 0-indexed integer array
|
175
|
+
- `Sashite::Cell.from_indices(*indices)` - Convert splat indices to CELL coordinate
|
274
176
|
|
275
|
-
|
276
|
-
|
277
|
-
drop = SimpleMove.new("*", "e4") # Drop from hand
|
278
|
-
```
|
177
|
+
#### Utilities
|
178
|
+
- `Sashite::Cell.regex` - Get the validation regular expression
|
279
179
|
|
280
|
-
|
180
|
+
### Constants
|
281
181
|
|
282
|
-
|
182
|
+
- `Sashite::Cell::REGEX` - Regular expression for CELL validation: `/\A(?:[a-z]+|[1-9]\d*|[A-Z]+)+\z/`
|
283
183
|
|
284
|
-
|
285
|
-
- `Sashite::Cell.location(coordinate)` - Convenience method to create locations
|
184
|
+
## Properties of CELL
|
286
185
|
|
287
|
-
|
186
|
+
* **Multi-dimensional**: Supports unlimited dimensional coordinate systems
|
187
|
+
* **Cyclical**: Uses systematic three-character-set repetition
|
188
|
+
* **ASCII-based**: Pure ASCII characters for universal compatibility
|
189
|
+
* **Unambiguous**: Each coordinate maps to exactly one location
|
190
|
+
* **Scalable**: Extends naturally from 1D to unlimited dimensions
|
191
|
+
* **Functional**: Provides a clean, stateless API for coordinate operations
|
288
192
|
|
289
|
-
|
290
|
-
- `Sashite::Cell::Location.new(coordinate)` - Create a new location instance
|
193
|
+
## Character Set Details
|
291
194
|
|
292
|
-
###
|
195
|
+
### Latin Lowercase (Dimensions 1, 4, 7, ...)
|
196
|
+
Single letters: `a` through `z` (positions 0-25)
|
197
|
+
Double letters: `aa` through `zz` (positions 26-701)
|
198
|
+
Triple letters: `aaa` through `zzz` (positions 702-18277)
|
199
|
+
And so on...
|
293
200
|
|
294
|
-
|
295
|
-
|
296
|
-
|
201
|
+
### Arabic Numerals (Dimensions 2, 5, 8, ...)
|
202
|
+
Standard decimal notation: `1`, `2`, `3`, ... (1-indexed)
|
203
|
+
No leading zeros, unlimited range
|
297
204
|
|
298
|
-
|
299
|
-
|
300
|
-
|
205
|
+
### Latin Uppercase (Dimensions 3, 6, 9, ...)
|
206
|
+
Single letters: `A` through `Z` (positions 0-25)
|
207
|
+
Double letters: `AA` through `ZZ` (positions 26-701)
|
208
|
+
Triple letters: `AAA` through `ZZZ` (positions 702-18277)
|
209
|
+
And so on...
|
301
210
|
|
302
|
-
|
303
|
-
- `#==` - Compare locations for equality
|
304
|
-
- `#eql?` - Strict equality comparison
|
305
|
-
- `#hash` - Hash value for use in collections
|
211
|
+
## Integration with DROP
|
306
212
|
|
307
|
-
|
213
|
+
CELL complements the DROP specification for complete location coverage:
|
308
214
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
215
|
+
```ruby
|
216
|
+
# Combined location validation
|
217
|
+
def valid_game_location?(location)
|
218
|
+
Sashite::Cell.valid?(location) || Sashite::Drop.reserve?(location)
|
219
|
+
end
|
314
220
|
|
315
|
-
|
221
|
+
valid_game_location?("a1") # => true (board position)
|
222
|
+
valid_game_location?("*") # => true (reserve position)
|
223
|
+
valid_game_location?("$") # => false (invalid)
|
224
|
+
```
|
316
225
|
|
317
|
-
|
318
|
-
* Board coordinates must be non-empty strings
|
319
|
-
* Hand/reserve locations must be exactly the character `*`
|
320
|
-
* No other characters or formats are permitted
|
226
|
+
## Examples in Different Games
|
321
227
|
|
322
|
-
|
228
|
+
### Chess
|
323
229
|
|
324
|
-
|
230
|
+
```ruby
|
231
|
+
# Standard algebraic notation positions
|
232
|
+
start_position = "e2"
|
233
|
+
end_position = "e4"
|
325
234
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
6. **User interface development**: When translating between display coordinates and logical positions
|
235
|
+
Sashite::Cell.valid?(start_position) # => true
|
236
|
+
Sashite::Cell.valid?(end_position) # => true
|
237
|
+
```
|
238
|
+
|
239
|
+
### Go (19x19)
|
332
240
|
|
333
|
-
|
241
|
+
```ruby
|
242
|
+
# Go board positions
|
243
|
+
corner = "a1" # Corner position
|
244
|
+
edge = "j1" # Edge position
|
245
|
+
tengen = "j10" # Center point (tengen) on 19x19 board
|
334
246
|
|
335
|
-
|
247
|
+
[corner, edge, tengen].all? { |pos| Sashite::Cell.valid?(pos) }
|
248
|
+
# => true
|
249
|
+
```
|
336
250
|
|
337
|
-
|
251
|
+
### Abstract Strategy Games
|
338
252
|
|
339
|
-
|
253
|
+
```ruby
|
254
|
+
# Multi-dimensional abstract games
|
255
|
+
hypercube_4d = "a1Aa"
|
256
|
+
tesseract_pos = "h8Hh8"
|
257
|
+
|
258
|
+
# Validate high-dimensional coordinates
|
259
|
+
Sashite::Cell.valid?(hypercube_4d) # => true
|
260
|
+
Sashite::Cell.dimensions(tesseract_pos) # => 5
|
261
|
+
```
|
340
262
|
|
341
263
|
## Documentation
|
342
264
|
|
343
|
-
- [CELL
|
265
|
+
- [Official CELL Specification](https://sashite.dev/documents/cell/1.0.0/)
|
266
|
+
- [API Documentation](https://rubydoc.info/github/sashite/cell.rb/main)
|
344
267
|
|
345
268
|
## License
|
346
269
|
|
data/lib/sashite/cell.rb
CHANGED
@@ -1,61 +1,252 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "cell/location"
|
4
|
-
|
5
|
-
# Sashité module providing implementations of various game notation specifications
|
6
|
-
#
|
7
|
-
# @see https://sashite.com/ Sashité
|
8
3
|
module Sashite
|
9
|
-
# CELL (
|
4
|
+
# CELL (Cell Encoding Location Label) implementation for Ruby
|
10
5
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# positions on game boards and pieces held in hand/reserve.
|
6
|
+
# Provides functionality for working with multi-dimensional game board coordinates
|
7
|
+
# using a cyclical ASCII character system.
|
14
8
|
#
|
15
9
|
# @see https://sashite.dev/documents/cell/1.0.0/ CELL Specification v1.0.0
|
16
10
|
module Cell
|
17
|
-
# Regular expression for validating CELL
|
11
|
+
# Regular expression for validating CELL coordinates according to specification
|
12
|
+
# This is the exact regex from the CELL specification v1.0.0
|
13
|
+
# Using non-capturing groups to avoid Ruby's nested quantifier warning
|
14
|
+
REGEX = /\A[a-z]+(?:[1-9]\d*[A-Z]+[a-z]+)*(?:[1-9]\d*(?:[A-Z]*))?\z/
|
15
|
+
|
16
|
+
# Check if a string represents a valid CELL coordinate
|
17
|
+
#
|
18
|
+
# @param string [String] the string to validate
|
19
|
+
# @return [Boolean] true if the string is a valid CELL coordinate
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# Sashite::Cell.valid?("a1") # => true
|
23
|
+
# Sashite::Cell.valid?("a1A") # => true
|
24
|
+
# Sashite::Cell.valid?("*") # => false
|
25
|
+
# Sashite::Cell.valid?("a0") # => false
|
26
|
+
def self.valid?(string)
|
27
|
+
return false unless string.is_a?(String)
|
28
|
+
return false if string.empty?
|
29
|
+
|
30
|
+
# Use the formal regex for validation
|
31
|
+
string.match?(REGEX)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get the number of dimensions in a coordinate
|
18
35
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# - Board coordinate: one or more alphanumeric characters [a-zA-Z0-9]
|
36
|
+
# @param string [String] the coordinate string
|
37
|
+
# @return [Integer] the number of dimensions
|
22
38
|
#
|
23
|
-
# @
|
24
|
-
|
39
|
+
# @example
|
40
|
+
# Sashite::Cell.dimensions("a1") # => 2
|
41
|
+
# Sashite::Cell.dimensions("a1A") # => 3
|
42
|
+
# Sashite::Cell.dimensions("foobar") # => 1
|
43
|
+
def self.dimensions(string)
|
44
|
+
return 0 unless valid?(string)
|
45
|
+
|
46
|
+
parse(string).length
|
47
|
+
end
|
25
48
|
|
26
|
-
#
|
49
|
+
# Parse a coordinate string into dimensional components
|
27
50
|
#
|
28
|
-
# @param
|
29
|
-
# @return [
|
51
|
+
# @param string [String] the coordinate string to parse
|
52
|
+
# @return [Array<String>] array of dimensional components
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# Sashite::Cell.parse("a1A") # => ["a", "1", "A"]
|
56
|
+
# Sashite::Cell.parse("h8Hh8") # => ["h", "8", "H", "h", "8"]
|
57
|
+
# Sashite::Cell.parse("foobar") # => ["foobar"] (if valid single dimension)
|
58
|
+
def self.parse(string)
|
59
|
+
return [] unless string.is_a?(::String)
|
60
|
+
return [] if string.empty?
|
61
|
+
return [] unless valid?(string)
|
62
|
+
|
63
|
+
parse_recursive(string, 1)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Convert a CELL coordinate to an array of 0-indexed integers
|
30
67
|
#
|
31
|
-
# @
|
32
|
-
#
|
33
|
-
# Sashite::Cell.valid?("*") # => true
|
34
|
-
# Sashite::Cell.valid?("A3a") # => true
|
35
|
-
# Sashite::Cell.valid?("center") # => true
|
68
|
+
# @param string [String] the CELL coordinate
|
69
|
+
# @return [Array<Integer>] array of 0-indexed positions
|
36
70
|
#
|
37
|
-
# @example
|
38
|
-
# Sashite::Cell.
|
39
|
-
# Sashite::Cell.
|
40
|
-
# Sashite::Cell.
|
41
|
-
|
42
|
-
|
43
|
-
return false unless cell_string.is_a?(::String)
|
71
|
+
# @example
|
72
|
+
# Sashite::Cell.to_indices("a1") # => [0, 0]
|
73
|
+
# Sashite::Cell.to_indices("e4") # => [4, 3]
|
74
|
+
# Sashite::Cell.to_indices("a1A") # => [0, 0, 0]
|
75
|
+
def self.to_indices(string)
|
76
|
+
return [] unless valid?(string)
|
44
77
|
|
45
|
-
|
78
|
+
parse(string).map.with_index do |component, index|
|
79
|
+
dimension_type = dimension_type(index + 1)
|
80
|
+
component_to_index(component, dimension_type)
|
81
|
+
end
|
46
82
|
end
|
47
83
|
|
48
|
-
#
|
84
|
+
# Convert an array of 0-indexed integers to a CELL coordinate
|
49
85
|
#
|
50
|
-
# @param
|
51
|
-
# @return [
|
52
|
-
# @raise [ArgumentError] if the coordinate is invalid
|
86
|
+
# @param indices [Array<Integer>] splat arguments of 0-indexed positions
|
87
|
+
# @return [String] the CELL coordinate
|
53
88
|
#
|
54
89
|
# @example
|
55
|
-
#
|
56
|
-
#
|
57
|
-
|
58
|
-
|
90
|
+
# Sashite::Cell.from_indices(0, 0) # => "a1"
|
91
|
+
# Sashite::Cell.from_indices(4, 3) # => "e4"
|
92
|
+
# Sashite::Cell.from_indices(0, 0, 0) # => "a1A"
|
93
|
+
def self.from_indices(*indices)
|
94
|
+
return "" if indices.empty?
|
95
|
+
|
96
|
+
result = indices.map.with_index do |index, dimension|
|
97
|
+
dimension_type = dimension_type(dimension + 1)
|
98
|
+
index_to_component(index, dimension_type)
|
99
|
+
end.join
|
100
|
+
|
101
|
+
# Verify the result is valid according to CELL specification
|
102
|
+
valid?(result) ? result : ""
|
103
|
+
end
|
104
|
+
|
105
|
+
# Get the validation regular expression
|
106
|
+
#
|
107
|
+
# @return [Regexp] the CELL validation regex
|
108
|
+
def self.regex
|
109
|
+
REGEX
|
110
|
+
end
|
111
|
+
|
112
|
+
# Recursively parse a coordinate string into components
|
113
|
+
# following the strict CELL specification pattern
|
114
|
+
#
|
115
|
+
# @param string [String] the remaining string to parse
|
116
|
+
# @param dimension [Integer] the current dimension (1-indexed)
|
117
|
+
# @return [Array<String>] array of dimensional components
|
118
|
+
def self.parse_recursive(string, dimension)
|
119
|
+
return [] if string.empty?
|
120
|
+
|
121
|
+
expected_type = dimension_type(dimension)
|
122
|
+
component = extract_component(string, expected_type)
|
123
|
+
|
124
|
+
return [] if component.nil?
|
125
|
+
|
126
|
+
# Invalid format according to CELL specification
|
127
|
+
|
128
|
+
# Extract component and recursively parse the rest
|
129
|
+
remaining = string[component.length..]
|
130
|
+
[component] + parse_recursive(remaining, dimension + 1)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Determine the character set type for a given dimension
|
134
|
+
# Following CELL specification: dimension n % 3 determines character set
|
135
|
+
#
|
136
|
+
# @param dimension [Integer] the dimension number (1-indexed)
|
137
|
+
# @return [Symbol] :lowercase, :numeric, or :uppercase
|
138
|
+
def self.dimension_type(dimension)
|
139
|
+
case dimension % 3
|
140
|
+
when 1 then :lowercase
|
141
|
+
when 2 then :numeric
|
142
|
+
when 0 then :uppercase
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Extract the next component from a string based on expected type
|
147
|
+
# Strictly follows CELL specification patterns
|
148
|
+
#
|
149
|
+
# @param string [String] the string to extract from
|
150
|
+
# @param type [Symbol] the expected component type
|
151
|
+
# @return [String, nil] the extracted component or nil if invalid
|
152
|
+
def self.extract_component(string, type)
|
153
|
+
case type
|
154
|
+
when :lowercase
|
155
|
+
match = string.match(/\A([a-z]+)/)
|
156
|
+
match ? match[1] : nil
|
157
|
+
when :numeric
|
158
|
+
# CELL specification requires positive integers only (no zero)
|
159
|
+
match = string.match(/\A([1-9]\d*)/)
|
160
|
+
match ? match[1] : nil
|
161
|
+
when :uppercase
|
162
|
+
match = string.match(/\A([A-Z]+)/)
|
163
|
+
match ? match[1] : nil
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Convert a component to its 0-indexed position
|
168
|
+
#
|
169
|
+
# @param component [String] the component
|
170
|
+
# @param type [Symbol] the component type
|
171
|
+
# @return [Integer] the 0-indexed position
|
172
|
+
def self.component_to_index(component, type)
|
173
|
+
case type
|
174
|
+
when :lowercase
|
175
|
+
letters_to_index(component)
|
176
|
+
when :numeric
|
177
|
+
component.to_i - 1
|
178
|
+
when :uppercase
|
179
|
+
letters_to_index(component.downcase)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Convert a 0-indexed position to a component
|
184
|
+
#
|
185
|
+
# @param index [Integer] the 0-indexed position
|
186
|
+
# @param type [Symbol] the component type
|
187
|
+
# @return [String] the component
|
188
|
+
def self.index_to_component(index, type)
|
189
|
+
case type
|
190
|
+
when :lowercase
|
191
|
+
index_to_letters(index)
|
192
|
+
when :numeric
|
193
|
+
(index + 1).to_s
|
194
|
+
when :uppercase
|
195
|
+
index_to_letters(index).upcase
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Convert letter sequence to 0-indexed position
|
200
|
+
# Extended alphabet: a=0, b=1, ..., z=25, aa=26, ab=27, ..., zz=701, aaa=702, etc.
|
201
|
+
#
|
202
|
+
# @param letters [String] the letter sequence
|
203
|
+
# @return [Integer] the 0-indexed position
|
204
|
+
def self.letters_to_index(letters)
|
205
|
+
length = letters.length
|
206
|
+
index = 0
|
207
|
+
|
208
|
+
# Add positions from shorter sequences
|
209
|
+
(1...length).each do |len|
|
210
|
+
index += 26**len
|
211
|
+
end
|
212
|
+
|
213
|
+
# Add position within current length
|
214
|
+
letters.each_char.with_index do |char, pos|
|
215
|
+
index += (char.ord - 97) * (26**(length - pos - 1))
|
216
|
+
end
|
217
|
+
|
218
|
+
index
|
219
|
+
end
|
220
|
+
|
221
|
+
# Convert 0-indexed position to letter sequence
|
222
|
+
# Extended alphabet: 0=a, 1=b, ..., 25=z, 26=aa, 27=ab, ..., 701=zz, 702=aaa, etc.
|
223
|
+
#
|
224
|
+
# @param index [Integer] the 0-indexed position
|
225
|
+
# @return [String] the letter sequence
|
226
|
+
def self.index_to_letters(index)
|
227
|
+
# Find the length of the result
|
228
|
+
length = 1
|
229
|
+
base = 0
|
230
|
+
|
231
|
+
loop do
|
232
|
+
range_size = 26**length
|
233
|
+
break if index < base + range_size
|
234
|
+
|
235
|
+
base += range_size
|
236
|
+
length += 1
|
237
|
+
end
|
238
|
+
|
239
|
+
# Convert within the found length
|
240
|
+
adjusted_index = index - base
|
241
|
+
result = ""
|
242
|
+
|
243
|
+
length.times do |pos|
|
244
|
+
char_index = adjusted_index / (26**(length - pos - 1))
|
245
|
+
result += (char_index + 97).chr
|
246
|
+
adjusted_index %= (26**(length - pos - 1))
|
247
|
+
end
|
248
|
+
|
249
|
+
result
|
59
250
|
end
|
60
251
|
end
|
61
252
|
end
|
data/lib/sashite-cell.rb
CHANGED
@@ -2,13 +2,6 @@
|
|
2
2
|
|
3
3
|
# Sashité namespace for board game notation libraries
|
4
4
|
module Sashite
|
5
|
-
# Coordinate Expression Location Label (CELL) implementation for Ruby
|
6
|
-
#
|
7
|
-
# CELL defines a consistent and rule-agnostic format for representing locations
|
8
|
-
# in abstract strategy board games. CELL provides a standardized way to identify
|
9
|
-
# positions on game boards and pieces held in hand/reserve, establishing a
|
10
|
-
# common foundation for location reference across the Sashité notation ecosystem.
|
11
|
-
#
|
12
5
|
# @see https://sashite.dev/documents/cell/1.0.0/ CELL Specification v1.0.0
|
13
6
|
end
|
14
7
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sashite-cell
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -9,10 +9,10 @@ bindir: bin
|
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
|
-
description: CELL defines a
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
description: CELL defines a standardized format for representing coordinates on multi-dimensional
|
13
|
+
game boards using diverse writing systems from around the world. This gem provides
|
14
|
+
a Ruby interface for working with multi-dimensional game coordinates through a clean,
|
15
|
+
functional API.
|
16
16
|
email: contact@cyril.email
|
17
17
|
executables: []
|
18
18
|
extensions: []
|
@@ -22,8 +22,6 @@ files:
|
|
22
22
|
- README.md
|
23
23
|
- lib/sashite-cell.rb
|
24
24
|
- lib/sashite/cell.rb
|
25
|
-
- lib/sashite/cell/location.rb
|
26
|
-
- lib/sashite/cell/location/hand_char.rb
|
27
25
|
homepage: https://github.com/sashite/cell.rb
|
28
26
|
licenses:
|
29
27
|
- MIT
|
@@ -48,7 +46,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
46
|
- !ruby/object:Gem::Version
|
49
47
|
version: '0'
|
50
48
|
requirements: []
|
51
|
-
rubygems_version: 3.6.
|
49
|
+
rubygems_version: 3.6.9
|
52
50
|
specification_version: 4
|
53
|
-
summary: CELL (
|
51
|
+
summary: CELL (Cell Encoding Location Label) implementation for Ruby
|
54
52
|
test_files: []
|
@@ -1,16 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sashite
|
4
|
-
module Cell
|
5
|
-
class Location
|
6
|
-
# Character representing hand/reserve location in CELL notation
|
7
|
-
#
|
8
|
-
# This character is used to identify pieces held off-board in a player's
|
9
|
-
# hand or reserve, as opposed to pieces positioned on the game board.
|
10
|
-
#
|
11
|
-
# @return [String] the hand/reserve character
|
12
|
-
# @see https://sashite.dev/documents/cell/1.0.0/ CELL Specification v1.0.0
|
13
|
-
HAND_CHAR = "*"
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
@@ -1,136 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "location/hand_char"
|
4
|
-
|
5
|
-
module Sashite
|
6
|
-
module Cell
|
7
|
-
# Represents a game location in CELL format
|
8
|
-
#
|
9
|
-
# A Location object encapsulates either a board coordinate (alphanumeric string)
|
10
|
-
# or a hand/reserve location (the "*" character). This class provides methods
|
11
|
-
# to distinguish between location types and convert to string representation.
|
12
|
-
#
|
13
|
-
# @see https://sashite.dev/documents/cell/1.0.0/ CELL Specification v1.0.0
|
14
|
-
class Location
|
15
|
-
# The coordinate string for this location
|
16
|
-
# @return [String] the coordinate in CELL format
|
17
|
-
attr_reader :coordinate
|
18
|
-
|
19
|
-
# Create a new Location object
|
20
|
-
#
|
21
|
-
# @param coordinate [String] the coordinate string in CELL format
|
22
|
-
# @raise [ArgumentError] if the coordinate is not valid CELL notation
|
23
|
-
#
|
24
|
-
# @example Create board locations
|
25
|
-
# Location.new("e4") # Chess square
|
26
|
-
# Location.new("5c") # Shōgi square
|
27
|
-
# Location.new("A3a") # 3D coordinate
|
28
|
-
# Location.new("center") # Custom coordinate
|
29
|
-
#
|
30
|
-
# @example Create hand/reserve location
|
31
|
-
# Location.new("*") # Hand/reserve
|
32
|
-
def initialize(coordinate)
|
33
|
-
raise ::ArgumentError, "Invalid CELL coordinate: #{coordinate.inspect}" unless Sashite::Cell.valid?(coordinate)
|
34
|
-
|
35
|
-
@coordinate = coordinate.freeze
|
36
|
-
|
37
|
-
freeze
|
38
|
-
end
|
39
|
-
|
40
|
-
# Parse a CELL string into a Location object
|
41
|
-
#
|
42
|
-
# @param cell_string [String] the CELL string to parse
|
43
|
-
# @return [Location] a new Location object
|
44
|
-
# @raise [ArgumentError] if the string is not valid CELL notation
|
45
|
-
#
|
46
|
-
# @example
|
47
|
-
# location = Location.parse("e4")
|
48
|
-
# hand = Location.parse("*")
|
49
|
-
def self.parse(cell_string)
|
50
|
-
new(cell_string)
|
51
|
-
end
|
52
|
-
|
53
|
-
# Check if this location represents a board coordinate
|
54
|
-
#
|
55
|
-
# @return [Boolean] true if this is a board coordinate, false if hand/reserve
|
56
|
-
#
|
57
|
-
# @example
|
58
|
-
# Location.new("e4").board? # => true
|
59
|
-
# Location.new("*").board? # => false
|
60
|
-
def board?
|
61
|
-
@coordinate != HAND_CHAR
|
62
|
-
end
|
63
|
-
|
64
|
-
# Check if this location represents a hand/reserve location
|
65
|
-
#
|
66
|
-
# @return [Boolean] true if this is hand/reserve, false if board coordinate
|
67
|
-
#
|
68
|
-
# @example
|
69
|
-
# Location.new("e4").hand? # => false
|
70
|
-
# Location.new("*").hand? # => true
|
71
|
-
def hand?
|
72
|
-
@coordinate == HAND_CHAR
|
73
|
-
end
|
74
|
-
|
75
|
-
# Convert to CELL string representation
|
76
|
-
#
|
77
|
-
# @return [String] the coordinate in CELL format
|
78
|
-
#
|
79
|
-
# @example
|
80
|
-
# Location.new("e4").to_s # => "e4"
|
81
|
-
# Location.new("*").to_s # => "*"
|
82
|
-
def to_s
|
83
|
-
@coordinate
|
84
|
-
end
|
85
|
-
|
86
|
-
# Detailed string representation for debugging
|
87
|
-
#
|
88
|
-
# @return [String] detailed representation showing class and coordinate
|
89
|
-
#
|
90
|
-
# @example
|
91
|
-
# Location.new("e4").inspect
|
92
|
-
# # => "#<Sashite::Cell::Location:0x... @coordinate=\"e4\">"
|
93
|
-
def inspect
|
94
|
-
"#<#{self.class}:0x#{object_id.to_s(16)} @coordinate=#{@coordinate.inspect}>"
|
95
|
-
end
|
96
|
-
|
97
|
-
# Compare locations for equality
|
98
|
-
#
|
99
|
-
# Two locations are equal if they have the same coordinate string.
|
100
|
-
#
|
101
|
-
# @param other [Object] the object to compare with
|
102
|
-
# @return [Boolean] true if locations are equal
|
103
|
-
#
|
104
|
-
# @example
|
105
|
-
# loc1 = Location.new("e4")
|
106
|
-
# loc2 = Location.new("e4")
|
107
|
-
# loc1 == loc2 # => true
|
108
|
-
def ==(other)
|
109
|
-
other.is_a?(Location) && @coordinate == other.coordinate
|
110
|
-
end
|
111
|
-
|
112
|
-
# Strict equality comparison
|
113
|
-
#
|
114
|
-
# @param other [Object] the object to compare with
|
115
|
-
# @return [Boolean] true if objects are equal
|
116
|
-
def eql?(other)
|
117
|
-
self == other
|
118
|
-
end
|
119
|
-
|
120
|
-
# Hash value for use in collections
|
121
|
-
#
|
122
|
-
# Ensures that equal locations have the same hash value.
|
123
|
-
#
|
124
|
-
# @return [Integer] hash value based on coordinate
|
125
|
-
#
|
126
|
-
# @example
|
127
|
-
# locations = Set.new
|
128
|
-
# locations << Location.new("e4")
|
129
|
-
# locations << Location.new("e4") # Won't be added (same hash)
|
130
|
-
# locations.size # => 1
|
131
|
-
def hash
|
132
|
-
[self.class, @coordinate].hash
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|