qi 12.0.0 → 14.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 +98 -76
- data/lib/qi/board.rb +140 -91
- data/lib/qi/hands.rb +73 -54
- data/lib/qi/styles.rb +38 -54
- data/lib/qi.rb +126 -206
- 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: d616536a97f64f4010976e9b6f8f80fbe1fdc61f1211ef95a17e98ebbbc1d2e8
|
|
4
|
+
data.tar.gz: 25b6ce774a649d36fa10f48733d680afd9ef8c93d90844d6fd2d6357726e3dab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5ac9f630af9039dcf96a8a56558dc6acbfec5c6f0836132d2e95cb6a132dcf8c23e520fd8101e3a810f330b5aacd7077461d068a6b65b9396ae093fa95f7e48
|
|
7
|
+
data.tar.gz: 3dde63bd8f57f6901f597046fbe749c7695c2b5b6e77fad2a101f2598eeff7fead2630c42b46d5f552207a6d248ba9548f8c0b94be1d645e568365b351f776e5
|
data/README.md
CHANGED
|
@@ -9,26 +9,25 @@
|
|
|
9
9
|
|
|
10
10
|
## Quick Start
|
|
11
11
|
|
|
12
|
-
```ruby
|
|
13
|
-
gem "qi", "~> 12.0"
|
|
14
|
-
```
|
|
15
|
-
|
|
16
12
|
```ruby
|
|
17
13
|
require "qi"
|
|
18
14
|
|
|
19
|
-
# Create an empty 8×8 board
|
|
20
|
-
|
|
15
|
+
# Create an empty 8×8 board — "C" and "c" are style identifiers
|
|
16
|
+
# (here: Chess uppercase vs Chess lowercase)
|
|
17
|
+
pos = Qi.new([8, 8], first_player_style: "C", second_player_style: "c")
|
|
21
18
|
|
|
22
|
-
# Place pieces
|
|
19
|
+
# Place some pieces using flat indices (row-major order)
|
|
23
20
|
pos2 = pos
|
|
24
|
-
.board_diff(
|
|
25
|
-
.board_diff(
|
|
26
|
-
.toggle
|
|
21
|
+
.board_diff(4 => "K", 60 => "k") # kings on their starting squares
|
|
22
|
+
.board_diff(0 => "R", 63 => "r") # rooks in the corners
|
|
23
|
+
.toggle # switch turn to second player
|
|
27
24
|
|
|
28
25
|
pos2.turn #=> :second
|
|
26
|
+
pos2.board[4] #=> "K"
|
|
27
|
+
pos2.board[60] #=> "k"
|
|
29
28
|
```
|
|
30
29
|
|
|
31
|
-
Every transformation returns a **new
|
|
30
|
+
Every transformation returns a **new instance**. The original is never modified. All returned internal state is frozen — callers cannot corrupt positions through accessors.
|
|
32
31
|
|
|
33
32
|
## Overview
|
|
34
33
|
|
|
@@ -36,18 +35,17 @@ Every transformation returns a **new frozen instance**. The original is never mo
|
|
|
36
35
|
|
|
37
36
|
| Component | Accessors | Description |
|
|
38
37
|
|-----------|-----------|-------------|
|
|
39
|
-
| Board | `board` |
|
|
38
|
+
| Board | `board` | Flat array of squares, indexed in row-major order |
|
|
40
39
|
| Hands | `first_player_hand`, `second_player_hand` | Off-board pieces held by each player |
|
|
41
40
|
| Styles | `first_player_style`, `second_player_style` | One style String per player side |
|
|
42
41
|
| Turn | `turn` | The active player (`:first` or `:second`) |
|
|
43
42
|
|
|
44
43
|
**Pieces and styles are Strings.** Every piece — whether on the board or in a hand — and every style value is stored as a `String`. This aligns naturally with the notation formats in the Sashité ecosystem ([FEEN](https://sashite.dev/specs/feen/1.0.0/), [EPIN](https://sashite.dev/specs/epin/1.0.0/), [PON](https://sashite.dev/specs/pon/1.0.0/), [SIN](https://sashite.dev/specs/sin/1.0.0/)), which all produce string representations. Empty squares are represented by `nil`.
|
|
45
44
|
|
|
46
|
-
**
|
|
45
|
+
**Strings required.** Pieces and styles must be strings (`String`). Non-string values are rejected with an `ArgumentError`. This avoids per-operation coercion overhead on the hot path.
|
|
47
46
|
|
|
48
47
|
```ruby
|
|
49
48
|
pos.board_diff(0 => "K") # String — stored as "K"
|
|
50
|
-
pos.board_diff(0 => :K) # Symbol — normalized to "K"
|
|
51
49
|
pos.board_diff(0 => "C:K") # Namespaced — stored as "C:K"
|
|
52
50
|
pos.board_diff(0 => "+P") # Promoted — stored as "+P"
|
|
53
51
|
```
|
|
@@ -56,7 +54,7 @@ pos.board_diff(0 => "+P") # Promoted — stored as "+P"
|
|
|
56
54
|
|
|
57
55
|
```ruby
|
|
58
56
|
# In your Gemfile
|
|
59
|
-
gem "qi", "~>
|
|
57
|
+
gem "qi", "~> 14.0"
|
|
60
58
|
```
|
|
61
59
|
|
|
62
60
|
Or install manually:
|
|
@@ -73,25 +71,25 @@ gem install qi
|
|
|
73
71
|
|
|
74
72
|
### Construction
|
|
75
73
|
|
|
76
|
-
#### `Qi.new(
|
|
74
|
+
#### `Qi.new(shape, first_player_style:, second_player_style:)` → `Qi`
|
|
77
75
|
|
|
78
|
-
Creates
|
|
76
|
+
Creates a position with an empty board.
|
|
79
77
|
|
|
80
78
|
**Parameters:**
|
|
81
79
|
|
|
82
|
-
-
|
|
83
|
-
- `first_player_style:` — style for the first player (
|
|
84
|
-
- `second_player_style:` — style for the second player (
|
|
80
|
+
- `shape` — an `Array` of one to three `Integer` dimension sizes (each 1–255). The total number of squares (product of dimensions) must not exceed 65,025.
|
|
81
|
+
- `first_player_style:` — style for the first player (non-nil string, at most 255 bytes).
|
|
82
|
+
- `second_player_style:` — style for the second player (non-nil string, at most 255 bytes).
|
|
85
83
|
|
|
86
|
-
The board starts with all squares empty (`nil`), both hands start empty, and the turn defaults to `:first`.
|
|
84
|
+
The board starts with all squares empty (`nil`), both hands start empty, and the turn defaults to `:first`.
|
|
87
85
|
|
|
88
86
|
```ruby
|
|
89
|
-
Qi.new(8, 8, first_player_style: "C", second_player_style: "c")
|
|
90
|
-
Qi.new(8, first_player_style: "G", second_player_style: "g")
|
|
91
|
-
Qi.new(5, 5, 5, first_player_style: "R", second_player_style: "r")
|
|
87
|
+
Qi.new([8, 8], first_player_style: "C", second_player_style: "c") # 2D (8×8)
|
|
88
|
+
Qi.new([8], first_player_style: "G", second_player_style: "g") # 1D
|
|
89
|
+
Qi.new([5, 5, 5], first_player_style: "R", second_player_style: "r") # 3D
|
|
92
90
|
```
|
|
93
91
|
|
|
94
|
-
**Raises** `ArgumentError` if shape constraints are violated or if a style is `nil` (see [Validation Errors](#validation-errors)).
|
|
92
|
+
**Raises** `ArgumentError` if shape constraints are violated, if total squares exceed the limit, or if a style is `nil` or oversized (see [Validation Errors](#validation-errors)).
|
|
95
93
|
|
|
96
94
|
### Constants
|
|
97
95
|
|
|
@@ -99,50 +97,63 @@ Qi.new(5, 5, 5, first_player_style: "R", second_player_style: "r") # 3D
|
|
|
99
97
|
|----------|-------|-------------|
|
|
100
98
|
| `Qi::MAX_DIMENSIONS` | `3` | Maximum number of board dimensions |
|
|
101
99
|
| `Qi::MAX_DIMENSION_SIZE` | `255` | Maximum size of any single dimension |
|
|
100
|
+
| `Qi::MAX_SQUARE_COUNT` | `65025` | Maximum total number of squares on a board |
|
|
101
|
+
| `Qi::MAX_PIECE_BYTESIZE` | `255` | Maximum bytesize of a piece string |
|
|
102
|
+
| `Qi::MAX_STYLE_BYTESIZE` | `255` | Maximum bytesize of a style string |
|
|
102
103
|
|
|
103
104
|
### Accessors
|
|
104
105
|
|
|
105
|
-
All accessors
|
|
106
|
-
|
|
107
|
-
| Method | Returns |
|
|
108
|
-
|
|
109
|
-
| `board` | `Array` |
|
|
110
|
-
| `first_player_hand` | `
|
|
111
|
-
| `second_player_hand` | `
|
|
112
|
-
| `turn` | `Symbol` | `:first` or `:second
|
|
113
|
-
| `first_player_style` | `String` |
|
|
114
|
-
| `second_player_style` | `String` |
|
|
115
|
-
| `shape` | `Array<Integer>` |
|
|
106
|
+
All accessors return frozen internal state — no copies, no allocations. Attempting to mutate a returned object raises `FrozenError`.
|
|
107
|
+
|
|
108
|
+
| Method | Returns | Description |
|
|
109
|
+
|--------|---------|-------------|
|
|
110
|
+
| `board` | `Array` | Flat array of `nil` or `String` (frozen). Indexed in row-major order. |
|
|
111
|
+
| `first_player_hand` | `Hash{String => Integer}` | First player's held pieces as piece → count map (frozen). |
|
|
112
|
+
| `second_player_hand` | `Hash{String => Integer}` | Second player's held pieces as piece → count map (frozen). |
|
|
113
|
+
| `turn` | `Symbol` | `:first` or `:second`. |
|
|
114
|
+
| `first_player_style` | `String` | First player's style. |
|
|
115
|
+
| `second_player_style` | `String` | Second player's style. |
|
|
116
|
+
| `shape` | `Array<Integer>` | Board dimensions (e.g., `[8, 8]`) (frozen). |
|
|
116
117
|
| `inspect` | `String` | Developer-friendly, unstable format. Do not parse. |
|
|
117
118
|
|
|
118
119
|
```ruby
|
|
119
|
-
pos.board #=> [
|
|
120
|
-
pos.first_player_hand #=>
|
|
121
|
-
pos.second_player_hand #=>
|
|
120
|
+
pos.board #=> [nil, "r", "n", "b", "q", "k", nil, nil, ...]
|
|
121
|
+
pos.first_player_hand #=> {}
|
|
122
|
+
pos.second_player_hand #=> {}
|
|
122
123
|
pos.turn #=> :first
|
|
123
124
|
pos.first_player_style #=> "C"
|
|
124
125
|
pos.second_player_style #=> "c"
|
|
125
126
|
pos.shape #=> [8, 8]
|
|
126
|
-
pos.inspect #=> "#<Qi shape=[8, 8] turn=:first>"
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
**
|
|
129
|
+
**Board as nested array.** Use `to_nested` to convert the flat board into a nested array matching the shape. This is an O(n) operation intended for display or serialization, not for the hot path.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
pos.to_nested #=> [["r", "n", "b", ...], ...]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Direct square access.** Read individual squares from the flat board by index — no intermediate structure needed:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
pos.board[0] #=> "r"
|
|
139
|
+
pos.board[63] #=> "R"
|
|
140
|
+
```
|
|
130
141
|
|
|
131
142
|
### Transformations
|
|
132
143
|
|
|
133
|
-
All transformation methods return a **new
|
|
144
|
+
All transformation methods return a **new `Qi` instance**. The original is never modified.
|
|
134
145
|
|
|
135
146
|
#### `board_diff(**squares)` → `Qi`
|
|
136
147
|
|
|
137
148
|
Returns a new position with modified squares.
|
|
138
149
|
|
|
139
|
-
Keys are flat indices (`Integer`, 0-based, row-major order). Values are pieces (
|
|
150
|
+
Keys are flat indices (`Integer`, 0-based, row-major order). Values are pieces (`String`, at most 255 bytes) or `nil` (empty square).
|
|
140
151
|
|
|
141
152
|
```ruby
|
|
142
153
|
pos2 = pos.board_diff(12 => nil, 28 => "P")
|
|
143
154
|
```
|
|
144
155
|
|
|
145
|
-
**Raises** `ArgumentError` if an index is out of range or if the resulting total piece count exceeds the board size.
|
|
156
|
+
**Raises** `ArgumentError` if an index is out of range, if a piece is not a `String`, if a piece exceeds 255 bytes, or if the resulting total piece count exceeds the board size.
|
|
146
157
|
|
|
147
158
|
See [Flat Indexing](#flat-indexing) for computing flat indices from coordinates.
|
|
148
159
|
|
|
@@ -151,7 +162,7 @@ See [Flat Indexing](#flat-indexing) for computing flat indices from coordinates.
|
|
|
151
162
|
|
|
152
163
|
Returns a new position with a modified hand.
|
|
153
164
|
|
|
154
|
-
Keys are piece identifiers (
|
|
165
|
+
Keys are piece identifiers (at most 255 bytes after string normalization); values are integer deltas (positive to add, negative to remove, zero is a no-op).
|
|
155
166
|
|
|
156
167
|
```ruby
|
|
157
168
|
pos2 = pos.first_player_hand_diff("P": 1) # Add one "P"
|
|
@@ -159,9 +170,11 @@ pos3 = pos.first_player_hand_diff("B": -1, "P": 1) # Remove one "B", add one "P
|
|
|
159
170
|
pos4 = pos.second_player_hand_diff("p": 1) # Add one "p" to second hand
|
|
160
171
|
```
|
|
161
172
|
|
|
162
|
-
|
|
173
|
+
Internally, hands are stored as `{piece => count}` hashes. Adding and removing pieces is O(1) per entry.
|
|
174
|
+
|
|
175
|
+
**String normalization of keys:** Ruby keyword arguments produce Symbol keys, so `first_player_hand_diff("P": 1)` passes `{P: 1}` with key `:P` (a Symbol). The implementation normalizes this to the String `"P"` before storing. This is a Ruby-specific concern — the important contract is that the hand always contains strings, matching the board's piece type.
|
|
163
176
|
|
|
164
|
-
**Raises** `ArgumentError` if a delta is not an `Integer`, if removing a piece not present, or if the resulting total piece count exceeds the board size.
|
|
177
|
+
**Raises** `ArgumentError` if a delta is not an `Integer`, if a piece exceeds 255 bytes, if removing a piece not present, or if the resulting total piece count exceeds the board size.
|
|
165
178
|
|
|
166
179
|
#### `toggle` → `Qi`
|
|
167
180
|
|
|
@@ -195,21 +208,21 @@ The Protocol does not prescribe how captures are modeled. In the example above,
|
|
|
195
208
|
|
|
196
209
|
### Shape and Dimensionality
|
|
197
210
|
|
|
198
|
-
The
|
|
211
|
+
The `board` accessor always returns a flat array. Use `to_nested` when a nested structure is needed:
|
|
199
212
|
|
|
200
|
-
| Dimensionality | Constructor | `
|
|
201
|
-
|
|
202
|
-
| 1D | `Qi.new(8, ...)` | `[square, square, ...]` |
|
|
203
|
-
| 2D | `Qi.new(8, 8, ...)` | `[[square, ...], [square, ...], ...]` |
|
|
204
|
-
| 3D | `Qi.new(5, 5, 5, ...)` | `[[[square, ...], ...], ...]` |
|
|
213
|
+
| Dimensionality | Constructor | `to_nested` returns |
|
|
214
|
+
|----------------|-------------|---------------------|
|
|
215
|
+
| 1D | `Qi.new([8], ...)` | `[square, square, ...]` |
|
|
216
|
+
| 2D | `Qi.new([8, 8], ...)` | `[[square, ...], [square, ...], ...]` |
|
|
217
|
+
| 3D | `Qi.new([5, 5, 5], ...)` | `[[[square, ...], ...], ...]` |
|
|
205
218
|
|
|
206
219
|
Each `square` is either `nil` (empty) or a `String` (a piece).
|
|
207
220
|
|
|
208
|
-
For a shape `[D1, D2, ..., DN]`, the total number of squares is `D1 × D2 × ... × DN`.
|
|
221
|
+
For a shape `[D1, D2, ..., DN]`, the total number of squares is `D1 × D2 × ... × DN`. This total must not exceed 65,025 (`MAX_SQUARE_COUNT`).
|
|
209
222
|
|
|
210
223
|
### Flat Indexing
|
|
211
224
|
|
|
212
|
-
`board_diff` addresses squares by **flat index** — a single integer in **row-major order** (C order).
|
|
225
|
+
`board_diff` addresses squares by **flat index** — a single integer in **row-major order** (C order). Individual squares can also be read directly from the flat board via `board[index]`.
|
|
213
226
|
|
|
214
227
|
**1D board** with shape `[F]`:
|
|
215
228
|
|
|
@@ -252,7 +265,7 @@ The total number of pieces across all locations (board squares + both hands) mus
|
|
|
252
265
|
For a board with *n* squares and *p* total pieces: **0 ≤ p ≤ n**.
|
|
253
266
|
|
|
254
267
|
```ruby
|
|
255
|
-
pos = Qi.new(2, first_player_style: "C", second_player_style: "c")
|
|
268
|
+
pos = Qi.new([2], first_player_style: "C", second_player_style: "c")
|
|
256
269
|
.board_diff(0 => "a", 1 => "b") # 2 pieces on 2 squares: OK
|
|
257
270
|
|
|
258
271
|
pos.first_player_hand_diff("c": 1)
|
|
@@ -265,8 +278,8 @@ pos.first_player_hand_diff("c": 1)
|
|
|
265
278
|
|
|
266
279
|
Construction validates fields in a guaranteed order. When multiple errors exist, the **first** failing check determines the error message:
|
|
267
280
|
|
|
268
|
-
1. **Shape** — dimension count, types,
|
|
269
|
-
2. **Styles** — nil checks (first, then second)
|
|
281
|
+
1. **Shape** — dimension count, types, bounds, then total square count
|
|
282
|
+
2. **Styles** — nil checks (first, then second), then type checks, then bytesize checks
|
|
270
283
|
|
|
271
284
|
This order is part of the public API contract.
|
|
272
285
|
|
|
@@ -279,31 +292,42 @@ This order is part of the public API contract.
|
|
|
279
292
|
| `"dimension size must be an Integer, got C"` | Non-integer dimension size |
|
|
280
293
|
| `"dimension size must be at least 1, got N"` | Dimension size is zero or negative |
|
|
281
294
|
| `"dimension size N exceeds maximum of 255"` | Dimension size exceeds 255 |
|
|
295
|
+
| `"board exceeds 65025 squares (got N)"` | Total square count exceeds limit |
|
|
282
296
|
| `"first player style must not be nil"` | First style is `nil` |
|
|
283
297
|
| `"second player style must not be nil"` | Second style is `nil` |
|
|
298
|
+
| `"first player style must be a String"` | First style is not a String |
|
|
299
|
+
| `"second player style must be a String"` | Second style is not a String |
|
|
300
|
+
| `"first player style exceeds 255 bytes"` | First style is too large |
|
|
301
|
+
| `"second player style exceeds 255 bytes"` | Second style is too large |
|
|
284
302
|
|
|
285
303
|
### Transformation Errors
|
|
286
304
|
|
|
287
305
|
| Error message | Method | Cause |
|
|
288
306
|
|---------------|--------|-------|
|
|
289
307
|
| `"invalid flat index: I (board has N squares)"` | `board_diff` | Index out of range or non-integer key |
|
|
308
|
+
| `"piece must be a String, got C"` | `board_diff` | Non-string piece value |
|
|
309
|
+
| `"piece exceeds 255 bytes (got N)"` | `board_diff`, hand diffs | Piece string too large |
|
|
290
310
|
| `"delta must be an Integer, got C for piece P"` | hand diffs | Non-integer delta |
|
|
291
311
|
| `"cannot remove P: not found in hand"` | hand diffs | Removing more pieces than present |
|
|
292
312
|
| `"too many pieces for board size (P pieces, N squares)"` | all | Total pieces would exceed board capacity |
|
|
293
313
|
|
|
294
314
|
## Design Principles
|
|
295
315
|
|
|
296
|
-
**
|
|
316
|
+
**Purely functional.** Every transformation method returns a new `Qi` instance. The original is never modified. This eliminates an entire class of bugs around shared mutable state and makes positions safe to use as hash keys, cache entries, or history snapshots.
|
|
297
317
|
|
|
298
|
-
**
|
|
318
|
+
**Frozen by default.** All mutable containers (board arrays, hand hashes, shape arrays) are frozen after construction. Accessors return these frozen objects directly — no defensive copies, no allocation overhead. Attempting to mutate a returned object raises `FrozenError`, making invariant violations impossible rather than merely documented.
|
|
299
319
|
|
|
300
|
-
**
|
|
320
|
+
**Performance-oriented internals.** The board is stored as a flat array for O(1) random access via `board[index]`. Hands are stored as `{piece => count}` hashes for O(1) additions and removals. Freezing is O(1) per object (a bit flag in Ruby) and happens only in constructors, never on the hot path. String validation replaces coercion to avoid per-operation interpolation.
|
|
321
|
+
|
|
322
|
+
**Bounded resource consumption.** All inputs are bounded: board dimensions (1–255 per axis, 65,025 total squares), piece strings (255 bytes), style strings (255 bytes). No input can trigger unbounded memory allocation. The library is safe to use in an internet-facing service with zero additional sanitization by the caller.
|
|
323
|
+
|
|
324
|
+
**Diff-based transformations.** Rather than rebuilding a full position from scratch, `board_diff` and hand diff methods express changes as deltas against the current state. This keeps the API surface small (four transformation methods cover all possible state transitions) while making the intent of each operation explicit.
|
|
301
325
|
|
|
302
326
|
**Zero dependencies.** `Qi` relies only on the Ruby standard library. No transitive dependency tree to audit, no version conflicts to resolve.
|
|
303
327
|
|
|
304
|
-
##
|
|
328
|
+
## Concurrency
|
|
305
329
|
|
|
306
|
-
`Qi` instances are
|
|
330
|
+
`Qi` instances are never mutated after creation. All internal containers are frozen at construction time. Transformation methods allocate new instances and share unchanged frozen structures by reference. This makes positions safe to share across threads and Ractors without synchronization.
|
|
307
331
|
|
|
308
332
|
## Ecosystem
|
|
309
333
|
|
|
@@ -319,33 +343,31 @@ This section provides guidance for porting `Qi` to other languages.
|
|
|
319
343
|
|
|
320
344
|
The complete public API consists of:
|
|
321
345
|
|
|
322
|
-
- **1 constructor** — `Qi.new(
|
|
346
|
+
- **1 constructor** — `Qi.new(shape, first_player_style:, second_player_style:)`
|
|
323
347
|
- **7 accessors** — `board`, `first_player_hand`, `second_player_hand`, `turn`, `first_player_style`, `second_player_style`, `shape`
|
|
324
|
-
- **
|
|
348
|
+
- **5 methods** — `board_diff`, `first_player_hand_diff`, `second_player_hand_diff`, `toggle`, `to_nested`
|
|
325
349
|
- **1 debug** — `inspect`
|
|
326
|
-
- **
|
|
327
|
-
|
|
328
|
-
### API Naming
|
|
329
|
-
|
|
330
|
-
The Ruby API separates **accessors** (read) from **transformations** (diff) using a `_diff` suffix. In languages where this convention is unusual, alternatives include `with_board(...)` / `with_first_hand(...)` for a fluent style, or `update_board(...)` / `update_hand(side, ...)` for a more imperative style.
|
|
350
|
+
- **5 constants** — `MAX_DIMENSIONS`, `MAX_DIMENSION_SIZE`, `MAX_SQUARE_COUNT`, `MAX_PIECE_BYTESIZE`, `MAX_STYLE_BYTESIZE`
|
|
331
351
|
|
|
332
352
|
### Key Semantic Contracts
|
|
333
353
|
|
|
334
|
-
**Pieces and styles are strings.** Board squares, hand contents, and style values are all stored as strings.
|
|
354
|
+
**Pieces and styles are strings.** Board squares, hand contents, and style values are all stored as strings. Non-string inputs are rejected at the boundary.
|
|
355
|
+
|
|
356
|
+
**All inputs are bounded.** Dimensions are capped at 255, total squares at 65,025, piece strings at 255 bytes, style strings at 255 bytes. No input can trigger unbounded memory allocation. Reimplementations must enforce these same limits to maintain the security properties.
|
|
335
357
|
|
|
336
|
-
**Piece equality is by value**, not by identity. Hand
|
|
358
|
+
**Piece equality is by value**, not by identity. Hand operations use standard Ruby `==` for piece matching. Use the equivalent in your language (`Eq` in Rust, `__eq__` in Python, `equals()` in Java).
|
|
337
359
|
|
|
338
360
|
**Piece cardinality is global.** The constraint `p ≤ n` counts pieces across all locations: board squares plus both hands. A transformation that adds a piece to a hand can exceed the limit even if the board has empty squares.
|
|
339
361
|
|
|
340
362
|
**Nil means empty.** On the board, `nil` (or the language equivalent) represents an empty square. It is never coerced to a string. Styles must not be nil — this is the only nil-related error at construction.
|
|
341
363
|
|
|
342
|
-
**Validation order is guaranteed**: shape → styles. Tests assert which error is reported when multiple inputs are invalid simultaneously.
|
|
364
|
+
**Validation order is guaranteed**: shape (dimensions → total square count) → styles (nil → type → bytesize). Tests assert which error is reported when multiple inputs are invalid simultaneously.
|
|
343
365
|
|
|
344
|
-
**
|
|
366
|
+
**Hands are piece → count maps.** Internally, hands use `{"P" => 2, "B" => 1}` rather than flat lists. This gives O(1) add/remove and makes count queries trivial. Empty entries (count reaching zero) are removed from the map.
|
|
345
367
|
|
|
346
|
-
**
|
|
368
|
+
**The constructor creates an empty position**: board all nil, hands empty, turn is first player. Pieces are added via `board_diff` and hand diff methods.
|
|
347
369
|
|
|
348
|
-
**
|
|
370
|
+
**Accessors return frozen shared state.** In Ruby, all returned arrays and hashes are frozen, making mutation impossible. Languages with immutable data structures (Elixir, Haskell, Clojure) get this for free. In languages with mutable defaults (Python, Java, JavaScript), reimplementations should either freeze/lock returned structures or return defensive copies.
|
|
349
371
|
|
|
350
372
|
### Hand Diff and String Normalization
|
|
351
373
|
|