qi 10.0.0 → 11.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 +155 -75
- data/lib/qi/board.rb +175 -0
- data/lib/qi/hands.rb +84 -0
- data/lib/qi/position.rb +122 -0
- data/lib/qi/styles.rb +71 -0
- data/lib/qi.rb +43 -111
- metadata +11 -11
- data/LICENSE.md +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37ba6c52ebee303a717aa6e218cc7e4c1167658988b883692dbee1691c72df6f
|
|
4
|
+
data.tar.gz: a63f9d02091f7d379d2fa5d7d7dedbaff1df7eb9505f20bd50dcf228f8b6c319
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 347722567e95439f0cb818cf929931315fdc081a72b9f50738aace03981e023c094b46b6010163e766e2f4b67476bc78b6a5718656c3b1c61c1aa31525838744
|
|
7
|
+
data.tar.gz: 4659c8825b38423cbfa539eb0c215b1503f997ce0d0fd972c3ee55cbe71719a6d61d65badb89866ae46cf87b935c4c1d05d4bb95f5f00b6614483548fe2b95cd
|
data/README.md
CHANGED
|
@@ -1,108 +1,188 @@
|
|
|
1
|
-
# Qi
|
|
1
|
+
# Qi
|
|
2
2
|
|
|
3
|
-
[](https://github.com/sashite/qi.rb/raw/main/LICENSE.md)
|
|
3
|
+
[](https://rubygems.org/gems/qi)
|
|
4
|
+
[](https://rubydoc.info/gems/qi)
|
|
5
|
+
[](https://github.com/sashite/qi.rb/actions)
|
|
6
|
+
[](https://github.com/sashite/qi.rb/blob/main/LICENSE)
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
> A minimal, format-agnostic position model for two-player board games.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
## Overview
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
`Qi` provides an immutable `Qi::Position` object that represents the state of a two-player, turn-based board game as defined by the [Sashité Game Protocol](https://sashite.dev/game-protocol/).
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
2. **Flexible Position Representation:** Qi captures the state of the game by recording the pieces in play, their arrangement on the board, the sequence of turns, and other additional states of the game. This enables a comprehensive view of the game at any given point.
|
|
17
|
-
3. **State Manipulation:** Qi allows for manipulation and update of game states through the `commit` method, allowing transitions between game states.
|
|
18
|
-
4. **Equality Checks:** With the `eql?` method, Qi allows for comparisons between different game states, which can be useful for tracking game progress, detecting repeats, or even in creating AI for your games.
|
|
19
|
-
5. **Turn Management:** Qi keeps track of the sequence of turns allowing users to identify whose turn it is currently.
|
|
20
|
-
6. **Access to Game Data:** Qi provides methods to access the current arrangement of pieces on the board (`squares_hash`) and the pieces captured by each player (`captures_hash`), helping users understand the current status of the game. It also allows access to a list of captured pieces (`captures_array`).
|
|
21
|
-
7. **Customizability:** Qi is flexible and allows for customization as per your needs. The keys and values of the `captures_hash` and `squares_hash` can be any kind of object, as well as the items from `turns` and values from `state`.
|
|
14
|
+
A position encodes exactly four things:
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
| Field | Type | Description |
|
|
17
|
+
|----------|---------------------------------------------|---------------------------------------|
|
|
18
|
+
| `board` | nested `Array` (1D to 3D) | Board structure and occupancy |
|
|
19
|
+
| `hands` | `Hash` with `:first` and `:second` keys | Off-board pieces held by each player |
|
|
20
|
+
| `styles` | `Hash` with `:first` and `:second` keys | Player style for each side |
|
|
21
|
+
| `turn` | `:first` or `:second` | The active player's side |
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
Piece and style representations are **intentionally opaque** — `Qi` validates structure, not semantics. This makes the library reusable across [FEEN](https://sashite.dev/specs/feen/1.0.0/), [PON](https://sashite.dev/specs/pon/1.0.0/), or any other encoding that shares the same positional model.
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
### Implementation Constraints
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
| Constraint | Value | Rationale |
|
|
28
|
+
|--------------------|-------|-------------------------------------------|
|
|
29
|
+
| Max dimensions | 3 | Covers 1D, 2D, 3D boards |
|
|
30
|
+
| Max dimension size | 255 | Fits in 8-bit integer; covers 255×255×255 |
|
|
31
|
+
| Board non-empty | n ≥ 1 | A board must contain at least one square |
|
|
32
|
+
| Piece cardinality | p ≤ n | Pieces cannot exceed the number of squares|
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
## Installation
|
|
34
35
|
|
|
35
|
-
```
|
|
36
|
-
|
|
36
|
+
```ruby
|
|
37
|
+
# In your Gemfile
|
|
38
|
+
gem "qi", "~> 11.0"
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
Or install
|
|
41
|
+
Or install manually:
|
|
40
42
|
|
|
41
43
|
```sh
|
|
42
44
|
gem install qi
|
|
43
45
|
```
|
|
44
46
|
|
|
47
|
+
## Dependencies
|
|
48
|
+
|
|
49
|
+
None. `Qi` is a zero-dependency library.
|
|
50
|
+
|
|
45
51
|
## Usage
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
In the provided setup, the attacking side is in possession of a silver general (S), a promoted bishop (+B) positioned on square 43, and a promoted pawn (+P) on square 22.
|
|
53
|
+
### Creating a Position
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
```ruby
|
|
56
|
+
# Chess starting position
|
|
57
|
+
board = [
|
|
58
|
+
[:r, :n, :b, :q, :k, :b, :n, :r],
|
|
59
|
+
[:p, :p, :p, :p, :p, :p, :p, :p],
|
|
60
|
+
[nil, nil, nil, nil, nil, nil, nil, nil],
|
|
61
|
+
[nil, nil, nil, nil, nil, nil, nil, nil],
|
|
62
|
+
[nil, nil, nil, nil, nil, nil, nil, nil],
|
|
63
|
+
[nil, nil, nil, nil, nil, nil, nil, nil],
|
|
64
|
+
[:P, :P, :P, :P, :P, :P, :P, :P],
|
|
65
|
+
[:R, :N, :B, :Q, :K, :B, :N, :R]
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
position = Qi.new(
|
|
69
|
+
board,
|
|
70
|
+
{ first: [], second: [] },
|
|
71
|
+
{ first: "C", second: "c" },
|
|
72
|
+
:first
|
|
73
|
+
)
|
|
74
|
+
```
|
|
51
75
|
|
|
52
|
-
|
|
76
|
+
### Accessing Fields
|
|
53
77
|
|
|
54
78
|
```ruby
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
south_captures = %w[S]
|
|
60
|
-
|
|
61
|
-
# Combine and count each player's captured pieces
|
|
62
|
-
captures = Hash.new(0)
|
|
63
|
-
(north_captures + south_captures).each { |piece| captures[piece] += 1 }
|
|
64
|
-
|
|
65
|
-
# Define the squares occupied by each piece on the board
|
|
66
|
-
squares = { 3 => "s", 4 => "k", 5 => "s", 22 => "+P", 43 => "+B" }
|
|
67
|
-
|
|
68
|
-
# Create a new game position
|
|
69
|
-
qi0 = Qi.new(captures, squares, [0, 1])
|
|
70
|
-
|
|
71
|
-
# Verify the properties of the game position
|
|
72
|
-
qi0.captures_array # => ["S", "b", "g", "g", "g", "g", "n", "n", "n", "n", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "r", "r", "s"]
|
|
73
|
-
qi0.captures_hash # => {"r"=>2, "b"=>1, "g"=>4, "s"=>1, "n"=>4, "p"=>17, "S"=>1}
|
|
74
|
-
qi0.squares_hash # => {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 43=>"+B"}
|
|
75
|
-
qi0.state # => {}
|
|
76
|
-
qi0.turn # => 0
|
|
77
|
-
qi0.turns # => [0, 1]
|
|
78
|
-
qi0.eql?(Qi.new(captures, squares, [0, 1])) # => true
|
|
79
|
-
qi0.eql?(Qi.new(captures, squares, [1, 0])) # => false
|
|
80
|
-
|
|
81
|
-
# Move a piece on the board and check the game state
|
|
82
|
-
qi1 = qi0.commit([], [], { 43 => nil, 13 => "+B" }, in_check: true)
|
|
83
|
-
|
|
84
|
-
qi1.captures_array # => ["S", "b", "g", "g", "g", "g", "n", "n", "n", "n", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "r", "r", "s"]
|
|
85
|
-
qi1.captures_hash # => {"r"=>2, "b"=>1, "g"=>4, "s"=>1, "n"=>4, "p"=>17, "S"=>1}
|
|
86
|
-
qi1.squares_hash # => {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 13=>"+B"}
|
|
87
|
-
qi1.state # => {:in_check=>true}
|
|
88
|
-
qi1.turn # => 1
|
|
89
|
-
qi1.turns # => [1, 0]
|
|
90
|
-
qi1.eql?(Qi.new(captures, squares, [0, 1])) # => false
|
|
91
|
-
qi1.eql?(Qi.new(captures, squares, [1, 0])) # => false
|
|
79
|
+
position.board #=> [[:r, :n, :b, ...], ...]
|
|
80
|
+
position.hands #=> { first: [], second: [] }
|
|
81
|
+
position.styles #=> { first: "C", second: "c" }
|
|
82
|
+
position.turn #=> :first
|
|
92
83
|
```
|
|
93
84
|
|
|
94
|
-
|
|
85
|
+
All accessors return **frozen** objects. A `Qi::Position` is immutable once created.
|
|
95
86
|
|
|
96
|
-
|
|
87
|
+
### Error Handling
|
|
88
|
+
|
|
89
|
+
`Qi.new` raises `ArgumentError` on invalid input:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
begin
|
|
93
|
+
Qi.new([], { first: [], second: [] }, { first: "C", second: "c" }, :first)
|
|
94
|
+
rescue ArgumentError => e
|
|
95
|
+
e.message #=> "board must not be empty"
|
|
96
|
+
end
|
|
97
|
+
```
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
### Pieces as Arbitrary Objects
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
Pieces are not restricted to any specific type. You can use symbols, strings (EPIN tokens), arrays, or any non-nil Ruby object:
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
```ruby
|
|
104
|
+
# Symbols
|
|
105
|
+
Qi.new([:k, :p, nil, :P, :K], { first: [], second: [] }, { first: "C", second: "c" }, :first)
|
|
106
|
+
|
|
107
|
+
# EPIN strings
|
|
108
|
+
Qi.new([["K^", nil], [nil, "k^"]], { first: [], second: [] }, { first: "C", second: "c" }, :first)
|
|
109
|
+
|
|
110
|
+
# Arrays as structured piece representations
|
|
111
|
+
Qi.new(
|
|
112
|
+
[[[:king, :first, true], nil], [nil, [:king, :second, true]]],
|
|
113
|
+
{ first: [], second: [] },
|
|
114
|
+
{ first: :chess, second: :chess },
|
|
115
|
+
:first
|
|
116
|
+
)
|
|
117
|
+
```
|
|
103
118
|
|
|
104
|
-
|
|
119
|
+
### Multi-dimensional Boards
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# 1D board
|
|
123
|
+
Qi.new([:a, nil, :b], { first: [], second: [] }, { first: "G", second: "g" }, :first)
|
|
124
|
+
|
|
125
|
+
# 2D board (standard)
|
|
126
|
+
Qi.new([[nil, nil], [nil, nil]], { first: [], second: [] }, { first: "C", second: "c" }, :first)
|
|
127
|
+
|
|
128
|
+
# 3D board (2 layers × 2 ranks × 2 files)
|
|
129
|
+
board_3d = [
|
|
130
|
+
[[:a, :b], [:c, :d]],
|
|
131
|
+
[[:A, :B], [:C, :D]]
|
|
132
|
+
]
|
|
133
|
+
Qi.new(board_3d, { first: [], second: [] }, { first: "R", second: "r" }, :first)
|
|
134
|
+
```
|
|
105
135
|
|
|
106
|
-
|
|
136
|
+
### Hands with Captured Pieces
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# Shogi-like position with pieces in hand
|
|
140
|
+
Qi.new(
|
|
141
|
+
[[nil, nil, nil], [nil, "K^", nil], [nil, nil, nil]],
|
|
142
|
+
{ first: ["P", "P", "B"], second: ["p"] },
|
|
143
|
+
{ first: "S", second: "s" },
|
|
144
|
+
:first
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Validation Errors
|
|
149
|
+
|
|
150
|
+
| Error message | Cause |
|
|
151
|
+
|----------------------------------------------------------------------|-------------------------------------------------|
|
|
152
|
+
| `"board must be an Array"` | Board is not an Array |
|
|
153
|
+
| `"board must not be empty"` | Board is `[]` |
|
|
154
|
+
| `"board exceeds 3 dimensions (got N)"` | More than 3 nesting levels |
|
|
155
|
+
| `"dimension size N exceeds maximum of 255"` | A dimension has more than 255 elements |
|
|
156
|
+
| `"non-rectangular board: expected N elements, got M"` | Sub-arrays at the same level differ in length |
|
|
157
|
+
| `"inconsistent board structure: mixed arrays and non-arrays at same level"` | Mixed arrays and non-arrays at the same nesting level |
|
|
158
|
+
| `"inconsistent board structure: expected flat squares at this level"` | An array found where a leaf square was expected |
|
|
159
|
+
| `"hands must be a Hash with keys :first and :second"` | Hands is not a Hash |
|
|
160
|
+
| `"hands must have exactly keys :first and :second"` | Hash has missing or extra keys |
|
|
161
|
+
| `"each hand must be an Array"` | Hand value is not an Array |
|
|
162
|
+
| `"hand pieces must not be nil"` | `nil` found in a hand Array |
|
|
163
|
+
| `"styles must be a Hash with keys :first and :second"` | Styles is not a Hash |
|
|
164
|
+
| `"styles must have exactly keys :first and :second"` | Hash has missing or extra keys |
|
|
165
|
+
| `"first player style must not be nil"` | First style value is `nil` |
|
|
166
|
+
| `"second player style must not be nil"` | Second style value is `nil` |
|
|
167
|
+
| `"turn must be :first or :second"` | Invalid turn value |
|
|
168
|
+
| `"too many pieces for board size (P pieces, N squares)"` | Piece cardinality violation |
|
|
169
|
+
|
|
170
|
+
## Design Principles
|
|
171
|
+
|
|
172
|
+
- **Format-agnostic**: No dependency on EPIN, SIN, or any specific encoding.
|
|
173
|
+
- **Protocol-aligned**: Structurally compatible with the Game Protocol's Position model.
|
|
174
|
+
- **Immutable**: Positions are frozen at construction; no mutation is possible.
|
|
175
|
+
- **Validated at construction**: All invariants are enforced when building a position.
|
|
176
|
+
- **Zero dependencies**: Only the Ruby standard library.
|
|
177
|
+
|
|
178
|
+
## Related Specifications
|
|
179
|
+
|
|
180
|
+
- [Game Protocol](https://sashite.dev/game-protocol/) — Conceptual foundation
|
|
181
|
+
- [PON Specification](https://sashite.dev/specs/pon/1.0.0/) — JSON-based position format
|
|
182
|
+
- [FEEN Specification](https://sashite.dev/specs/feen/1.0.0/) — Canonical string-based position format
|
|
183
|
+
- [EPIN Specification](https://sashite.dev/specs/epin/1.0.0/) — Piece token format
|
|
184
|
+
- [SIN Specification](https://sashite.dev/specs/sin/1.0.0/) — Style token format
|
|
185
|
+
|
|
186
|
+
## License
|
|
107
187
|
|
|
108
|
-
|
|
188
|
+
Available as open source under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).
|
data/lib/qi/board.rb
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Qi
|
|
4
|
+
# Pure validation functions for multi-dimensional board structures.
|
|
5
|
+
#
|
|
6
|
+
# A board is represented as a nested Array where:
|
|
7
|
+
#
|
|
8
|
+
# - A *1D board* is a flat array of squares: +[nil, "K^", nil]+
|
|
9
|
+
# - A *2D board* is an array of ranks: +[[nil, nil], ["K^", nil]]+
|
|
10
|
+
# - A *3D board* is an array of layers, each an array of ranks.
|
|
11
|
+
#
|
|
12
|
+
# Each leaf element (square) is either +nil+ (empty) or any non-nil object (a piece).
|
|
13
|
+
#
|
|
14
|
+
# == Constraints
|
|
15
|
+
#
|
|
16
|
+
# - Maximum dimensionality: 3
|
|
17
|
+
# - Maximum size per dimension: 255
|
|
18
|
+
# - At least one square (non-empty board)
|
|
19
|
+
# - Rectangular structure: all sub-arrays at the same depth must have
|
|
20
|
+
# identical length (enforced globally, not just per-sibling).
|
|
21
|
+
#
|
|
22
|
+
# @example Validate a 2D board
|
|
23
|
+
# Qi::Board.validate([[:a, nil], [nil, :b]]) #=> [4, 2]
|
|
24
|
+
#
|
|
25
|
+
# @example Validate an empty board
|
|
26
|
+
# Qi::Board.validate([[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]]) #=> [9, 0]
|
|
27
|
+
module Board
|
|
28
|
+
MAX_DIMENSIONS = 3
|
|
29
|
+
MAX_DIMENSION_SIZE = 255
|
|
30
|
+
|
|
31
|
+
# Validates a board and returns its square and piece counts.
|
|
32
|
+
#
|
|
33
|
+
# Validation proceeds in order of increasing cost: type check, emptiness,
|
|
34
|
+
# shape inference (first-path walk), dimension limits, then a single-pass
|
|
35
|
+
# structural verification with counting.
|
|
36
|
+
#
|
|
37
|
+
# @param board [Object] the board structure to validate.
|
|
38
|
+
# @return [Array(Integer, Integer)] +[square_count, piece_count]+.
|
|
39
|
+
# @raise [ArgumentError] if the board is structurally invalid.
|
|
40
|
+
#
|
|
41
|
+
# @example A 2D board
|
|
42
|
+
# Qi::Board.validate([[:r, nil, nil], [nil, nil, :R]]) #=> [6, 2]
|
|
43
|
+
#
|
|
44
|
+
# @example A 1D board
|
|
45
|
+
# Qi::Board.validate([:k, nil, nil, :K]) #=> [4, 2]
|
|
46
|
+
#
|
|
47
|
+
# @example A 3D board (2 layers × 2 ranks × 2 files)
|
|
48
|
+
# Qi::Board.validate([[[:a, nil], [nil, :b]], [[nil, :c], [:d, nil]]]) #=> [8, 4]
|
|
49
|
+
#
|
|
50
|
+
# @example Non-rectangular boards are rejected
|
|
51
|
+
# Qi::Board.validate([[:a, :b], [:c]])
|
|
52
|
+
# # => ArgumentError: non-rectangular board: expected 2 elements, got 1
|
|
53
|
+
def self.validate(board)
|
|
54
|
+
validate_is_array(board)
|
|
55
|
+
validate_non_empty(board)
|
|
56
|
+
shape = compute_shape(board)
|
|
57
|
+
validate_max_dimensions(shape)
|
|
58
|
+
validate_dimension_sizes(shape)
|
|
59
|
+
verify_and_count(board, shape)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- Step 1: basic type checks -------------------------------------------
|
|
63
|
+
|
|
64
|
+
def self.validate_is_array(board)
|
|
65
|
+
return if board.is_a?(::Array)
|
|
66
|
+
|
|
67
|
+
raise ::ArgumentError, "board must be an Array"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.validate_non_empty(board)
|
|
71
|
+
return unless board.empty?
|
|
72
|
+
|
|
73
|
+
raise ::ArgumentError, "board must not be empty"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# --- Step 2: compute expected shape by walking the first element ----------
|
|
77
|
+
|
|
78
|
+
# The shape is an array of dimension sizes, e.g. [2, 3, 8] for
|
|
79
|
+
# 2 layers × 3 ranks × 8 files. We derive it by following the first
|
|
80
|
+
# child at each nesting level.
|
|
81
|
+
def self.compute_shape(board)
|
|
82
|
+
shape = []
|
|
83
|
+
current = board
|
|
84
|
+
|
|
85
|
+
while current.is_a?(::Array) && current.first.is_a?(::Array)
|
|
86
|
+
shape << current.size
|
|
87
|
+
current = current.first
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
shape << current.size
|
|
91
|
+
shape
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# --- Step 3: validate dimension count and sizes ---------------------------
|
|
95
|
+
|
|
96
|
+
def self.validate_max_dimensions(shape)
|
|
97
|
+
dim = shape.size
|
|
98
|
+
return if dim <= MAX_DIMENSIONS
|
|
99
|
+
|
|
100
|
+
raise ::ArgumentError, "board exceeds #{MAX_DIMENSIONS} dimensions (got #{dim})"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.validate_dimension_sizes(shape)
|
|
104
|
+
oversized = shape.find { |size| size > MAX_DIMENSION_SIZE }
|
|
105
|
+
return unless oversized
|
|
106
|
+
|
|
107
|
+
raise ::ArgumentError, "dimension size #{oversized} exceeds maximum of #{MAX_DIMENSION_SIZE}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# --- Step 4: verify structure and count in a single pass ------------------
|
|
111
|
+
|
|
112
|
+
def self.verify_and_count(board, shape)
|
|
113
|
+
if shape.size == 1
|
|
114
|
+
verify_and_count_rank(board, shape[0])
|
|
115
|
+
else
|
|
116
|
+
verify_and_count_multi(board, shape)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# For a 1D shape [n]: single-pass over the rank, verifying all elements are
|
|
121
|
+
# leaves (not arrays) while counting squares and pieces simultaneously.
|
|
122
|
+
def self.verify_and_count_rank(rank, expected)
|
|
123
|
+
unless rank.size == expected
|
|
124
|
+
raise ::ArgumentError, "non-rectangular board: expected #{expected} elements, got #{rank.size}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
piece_count = 0
|
|
128
|
+
|
|
129
|
+
rank.each do |square|
|
|
130
|
+
if square.is_a?(::Array)
|
|
131
|
+
raise ::ArgumentError, "inconsistent board structure: expected flat squares at this level"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
piece_count += 1 unless square.nil?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
[expected, piece_count]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# For a multi-dimensional shape [n, *rest]: check length, then recurse
|
|
141
|
+
# into each sub-array, accumulating counts.
|
|
142
|
+
def self.verify_and_count_multi(board, shape)
|
|
143
|
+
expected = shape[0]
|
|
144
|
+
rest = shape[1..]
|
|
145
|
+
|
|
146
|
+
unless board.size == expected
|
|
147
|
+
raise ::ArgumentError, "non-rectangular board: expected #{expected} elements, got #{board.size}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
total_squares = 0
|
|
151
|
+
total_pieces = 0
|
|
152
|
+
|
|
153
|
+
board.each do |sub|
|
|
154
|
+
unless sub.is_a?(::Array)
|
|
155
|
+
raise ::ArgumentError, "inconsistent board structure: mixed arrays and non-arrays at same level"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
sq, pc = verify_and_count(sub, rest)
|
|
159
|
+
total_squares += sq
|
|
160
|
+
total_pieces += pc
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
[total_squares, total_pieces]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private_class_method :validate_is_array,
|
|
167
|
+
:validate_non_empty,
|
|
168
|
+
:compute_shape,
|
|
169
|
+
:validate_max_dimensions,
|
|
170
|
+
:validate_dimension_sizes,
|
|
171
|
+
:verify_and_count,
|
|
172
|
+
:verify_and_count_rank,
|
|
173
|
+
:verify_and_count_multi
|
|
174
|
+
end
|
|
175
|
+
end
|
data/lib/qi/hands.rb
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Qi
|
|
4
|
+
# Pure validation functions for player hands.
|
|
5
|
+
#
|
|
6
|
+
# Hands are represented as a Hash with exactly two keys:
|
|
7
|
+
#
|
|
8
|
+
# - +:first+ — array of pieces held by the first player.
|
|
9
|
+
# - +:second+ — array of pieces held by the second player.
|
|
10
|
+
#
|
|
11
|
+
# Each piece in a hand can be any non-nil object. The ordering of pieces
|
|
12
|
+
# within a hand carries no semantic meaning.
|
|
13
|
+
#
|
|
14
|
+
# @example Validate hands with pieces
|
|
15
|
+
# Qi::Hands.validate({ first: ["+P", "+P"], second: ["b"] }) #=> 3
|
|
16
|
+
#
|
|
17
|
+
# @example Validate empty hands
|
|
18
|
+
# Qi::Hands.validate({ first: [], second: [] }) #=> 0
|
|
19
|
+
module Hands
|
|
20
|
+
REQUIRED_KEYS = %i[first second].freeze
|
|
21
|
+
|
|
22
|
+
# Validates hands structure and returns the total piece count.
|
|
23
|
+
#
|
|
24
|
+
# Validation checks shape (exactly two keys), type (both values are arrays),
|
|
25
|
+
# then performs a single pass over each array to reject +nil+ elements and
|
|
26
|
+
# count pieces simultaneously.
|
|
27
|
+
#
|
|
28
|
+
# @param hands [Object] the hands structure to validate.
|
|
29
|
+
# @return [Integer] the total number of pieces across both hands.
|
|
30
|
+
# @raise [ArgumentError] if the hands structure is invalid.
|
|
31
|
+
#
|
|
32
|
+
# @example Valid hands
|
|
33
|
+
# Qi::Hands.validate({ first: [:P, :B], second: [:p] }) #=> 3
|
|
34
|
+
#
|
|
35
|
+
# @example Nil piece rejected
|
|
36
|
+
# Qi::Hands.validate({ first: [nil], second: [] })
|
|
37
|
+
# # => ArgumentError: hand pieces must not be nil
|
|
38
|
+
#
|
|
39
|
+
# @example Missing key
|
|
40
|
+
# Qi::Hands.validate({ first: [] })
|
|
41
|
+
# # => ArgumentError: hands must have exactly keys :first and :second
|
|
42
|
+
def self.validate(hands)
|
|
43
|
+
validate_shape(hands)
|
|
44
|
+
validate_arrays(hands)
|
|
45
|
+
count_hand(hands[:first]) + count_hand(hands[:second])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# --- Shape validation -----------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def self.validate_shape(hands)
|
|
51
|
+
unless hands.is_a?(::Hash)
|
|
52
|
+
raise ::ArgumentError, "hands must be a Hash with keys :first and :second"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return if hands.size == 2 && hands.key?(:first) && hands.key?(:second)
|
|
56
|
+
|
|
57
|
+
raise ::ArgumentError, "hands must have exactly keys :first and :second"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.validate_arrays(hands)
|
|
61
|
+
return if hands[:first].is_a?(::Array) && hands[:second].is_a?(::Array)
|
|
62
|
+
|
|
63
|
+
raise ::ArgumentError, "each hand must be an Array"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# --- Single pass: reject nil and count simultaneously ---------------------
|
|
67
|
+
|
|
68
|
+
def self.count_hand(pieces)
|
|
69
|
+
count = 0
|
|
70
|
+
|
|
71
|
+
pieces.each do |piece|
|
|
72
|
+
raise ::ArgumentError, "hand pieces must not be nil" if piece.nil?
|
|
73
|
+
|
|
74
|
+
count += 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
count
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private_class_method :validate_shape,
|
|
81
|
+
:validate_arrays,
|
|
82
|
+
:count_hand
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/qi/position.rb
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "board"
|
|
4
|
+
require_relative "hands"
|
|
5
|
+
require_relative "styles"
|
|
6
|
+
|
|
7
|
+
module Qi
|
|
8
|
+
# An immutable, validated position for a two-player board game.
|
|
9
|
+
#
|
|
10
|
+
# A +Qi::Position+ is the in-memory representation of a game position
|
|
11
|
+
# as defined by the Sashité Game Protocol. It guarantees that all
|
|
12
|
+
# structural invariants hold at construction time.
|
|
13
|
+
#
|
|
14
|
+
# == Fields
|
|
15
|
+
#
|
|
16
|
+
# - +board+ — multi-dimensional Array representing board structure and occupancy.
|
|
17
|
+
# - +hands+ — +{ first: Array, second: Array }+ of off-board pieces.
|
|
18
|
+
# - +styles+ — +{ first: Object, second: Object }+ of player styles.
|
|
19
|
+
# - +turn+ — +:first+ or +:second+, the active player's side.
|
|
20
|
+
#
|
|
21
|
+
# == Construction
|
|
22
|
+
#
|
|
23
|
+
# Use +Qi.new+ to build positions. Direct instantiation via +Qi::Position.new+
|
|
24
|
+
# is also supported; both perform identical validation.
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# pos = Qi.new([["K^", nil], [nil, "k^"]], { first: [], second: [] }, { first: "C", second: "c" }, :first)
|
|
28
|
+
# pos.board #=> [["K^", nil], [nil, "k^"]]
|
|
29
|
+
# pos.turn #=> :first
|
|
30
|
+
class Position
|
|
31
|
+
VALID_TURNS = %i[first second].freeze
|
|
32
|
+
|
|
33
|
+
# @return [Array] the board structure.
|
|
34
|
+
attr_reader :board
|
|
35
|
+
|
|
36
|
+
# @return [Hash] off-board pieces for each player.
|
|
37
|
+
attr_reader :hands
|
|
38
|
+
|
|
39
|
+
# @return [Hash] style for each player.
|
|
40
|
+
attr_reader :styles
|
|
41
|
+
|
|
42
|
+
# @return [Symbol] the active player's side (+:first+ or +:second+).
|
|
43
|
+
attr_reader :turn
|
|
44
|
+
|
|
45
|
+
# Creates a validated, immutable position.
|
|
46
|
+
#
|
|
47
|
+
# Validation is performed in order of increasing cost: turn (symbol check),
|
|
48
|
+
# board (structural traversal), hands, styles, then cardinality.
|
|
49
|
+
#
|
|
50
|
+
# == Validated invariants
|
|
51
|
+
#
|
|
52
|
+
# - Turn is +:first+ or +:second+.
|
|
53
|
+
# - Board is a non-empty, rectangular, nested Array (1D to 3D).
|
|
54
|
+
# - Each dimension size is at most 255.
|
|
55
|
+
# - Hands is a Hash with +:first+ and +:second+ Arrays of non-nil pieces.
|
|
56
|
+
# - Styles is a Hash with +:first+ and +:second+ non-nil values.
|
|
57
|
+
# - Total piece count does not exceed total square count.
|
|
58
|
+
#
|
|
59
|
+
# @param board [Array] nested array representing the board (1D to 3D).
|
|
60
|
+
# @param hands [Hash] +{ first: Array, second: Array }+ of off-board pieces.
|
|
61
|
+
# @param styles [Hash] +{ first: Object, second: Object }+ of player styles.
|
|
62
|
+
# @param turn [Symbol] +:first+ or +:second+.
|
|
63
|
+
# @return [Qi::Position] a frozen, immutable position.
|
|
64
|
+
# @raise [ArgumentError] if any structural constraint is violated.
|
|
65
|
+
#
|
|
66
|
+
# @example A valid position
|
|
67
|
+
# Qi::Position.new([nil, :k], { first: [], second: [] }, { first: :chess, second: :chess }, :second)
|
|
68
|
+
#
|
|
69
|
+
# @example Invalid turn
|
|
70
|
+
# Qi::Position.new([nil], { first: [], second: [] }, { first: "C", second: "c" }, :third)
|
|
71
|
+
# # => ArgumentError: turn must be :first or :second
|
|
72
|
+
def initialize(board, hands, styles, turn)
|
|
73
|
+
validate_turn(turn)
|
|
74
|
+
square_count, board_piece_count = Board.validate(board)
|
|
75
|
+
hand_piece_count = Hands.validate(hands)
|
|
76
|
+
Styles.validate(styles)
|
|
77
|
+
validate_cardinality(square_count, board_piece_count + hand_piece_count)
|
|
78
|
+
|
|
79
|
+
@board = deep_freeze(board)
|
|
80
|
+
@hands = deep_freeze(hands)
|
|
81
|
+
@styles = deep_freeze(styles)
|
|
82
|
+
@turn = turn
|
|
83
|
+
|
|
84
|
+
freeze
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns a developer-friendly string representation.
|
|
88
|
+
#
|
|
89
|
+
# @return [String]
|
|
90
|
+
def inspect
|
|
91
|
+
"#<#{self.class} board=#{@board.inspect} hands=#{@hands.inspect} styles=#{@styles.inspect} turn=#{@turn.inspect}>"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def validate_turn(turn)
|
|
97
|
+
return if VALID_TURNS.include?(turn)
|
|
98
|
+
|
|
99
|
+
raise ::ArgumentError, "turn must be :first or :second"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_cardinality(square_count, piece_count)
|
|
103
|
+
return if piece_count <= square_count
|
|
104
|
+
|
|
105
|
+
raise ::ArgumentError, "too many pieces for board size (#{piece_count} pieces, #{square_count} squares)"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Recursively freezes an object and all its nested contents.
|
|
109
|
+
def deep_freeze(obj)
|
|
110
|
+
case obj
|
|
111
|
+
when ::Hash
|
|
112
|
+
obj.each_value { |v| deep_freeze(v) }
|
|
113
|
+
obj.freeze
|
|
114
|
+
when ::Array
|
|
115
|
+
obj.each { |e| deep_freeze(e) }
|
|
116
|
+
obj.freeze
|
|
117
|
+
else
|
|
118
|
+
obj.freeze
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/qi/styles.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Qi
|
|
4
|
+
# Pure validation functions for player styles.
|
|
5
|
+
#
|
|
6
|
+
# Styles are represented as a Hash with exactly two keys:
|
|
7
|
+
#
|
|
8
|
+
# - +:first+ — the style associated with the first player side.
|
|
9
|
+
# - +:second+ — the style associated with the second player side.
|
|
10
|
+
#
|
|
11
|
+
# Style values are format-free: any non-nil object is accepted.
|
|
12
|
+
# Semantic validation (e.g., SIN compliance) is the responsibility
|
|
13
|
+
# of the encoding layer (FEEN, PON, etc.), not of Qi.
|
|
14
|
+
#
|
|
15
|
+
# @example Validate string styles
|
|
16
|
+
# Qi::Styles.validate({ first: "C", second: "c" }) #=> nil
|
|
17
|
+
#
|
|
18
|
+
# @example Validate symbol styles
|
|
19
|
+
# Qi::Styles.validate({ first: :chess, second: :shogi }) #=> nil
|
|
20
|
+
module Styles
|
|
21
|
+
# Validates the styles structure.
|
|
22
|
+
#
|
|
23
|
+
# Returns +nil+ if the Hash has exactly keys +:first+ and +:second+ with
|
|
24
|
+
# non-nil values, or raises +ArgumentError+ otherwise.
|
|
25
|
+
#
|
|
26
|
+
# @param styles [Object] the styles structure to validate.
|
|
27
|
+
# @return [nil]
|
|
28
|
+
# @raise [ArgumentError] if the styles structure is invalid.
|
|
29
|
+
#
|
|
30
|
+
# @example Valid styles
|
|
31
|
+
# Qi::Styles.validate({ first: "S", second: "s" }) #=> nil
|
|
32
|
+
#
|
|
33
|
+
# @example Nil first style
|
|
34
|
+
# Qi::Styles.validate({ first: nil, second: "c" })
|
|
35
|
+
# # => ArgumentError: first player style must not be nil
|
|
36
|
+
#
|
|
37
|
+
# @example Nil second style
|
|
38
|
+
# Qi::Styles.validate({ first: "C", second: nil })
|
|
39
|
+
# # => ArgumentError: second player style must not be nil
|
|
40
|
+
#
|
|
41
|
+
# @example Missing key
|
|
42
|
+
# Qi::Styles.validate({ first: "C" })
|
|
43
|
+
# # => ArgumentError: styles must have exactly keys :first and :second
|
|
44
|
+
#
|
|
45
|
+
# @example Not a Hash
|
|
46
|
+
# Qi::Styles.validate("not a hash")
|
|
47
|
+
# # => ArgumentError: styles must be a Hash with keys :first and :second
|
|
48
|
+
def self.validate(styles)
|
|
49
|
+
validate_shape(styles)
|
|
50
|
+
validate_non_nil(styles)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.validate_shape(styles)
|
|
54
|
+
unless styles.is_a?(::Hash)
|
|
55
|
+
raise ::ArgumentError, "styles must be a Hash with keys :first and :second"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return if styles.size == 2 && styles.key?(:first) && styles.key?(:second)
|
|
59
|
+
|
|
60
|
+
raise ::ArgumentError, "styles must have exactly keys :first and :second"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.validate_non_nil(styles)
|
|
64
|
+
raise ::ArgumentError, "first player style must not be nil" if styles[:first].nil?
|
|
65
|
+
raise ::ArgumentError, "second player style must not be nil" if styles[:second].nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private_class_method :validate_shape,
|
|
69
|
+
:validate_non_nil
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/qi.rb
CHANGED
|
@@ -1,118 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
# identifiers and the values represent the piece that will occupy each square.
|
|
34
|
-
# Both the keys and values can be any type of Ruby object, such as integers, strings, symbols, etc.
|
|
35
|
-
# @param turns [Array<Object>] a rotation of turns
|
|
36
|
-
# @param state [Hash<Symbol, Object>] a hash of game states
|
|
37
|
-
#
|
|
38
|
-
# @example
|
|
39
|
-
# captures = Hash.new(0)
|
|
40
|
-
# north_captures = %w[r r b g g g g s n n n n p p p p p p p p p p p p p p p p p]
|
|
41
|
-
# south_captures = %w[S]
|
|
42
|
-
# (north_captures + south_captures).each { |piece| captures[piece] += 1 }
|
|
43
|
-
# squares = { 3 => "s", 4 => "k", 5 => "s", 22 => "+P", 43 => "+B" }
|
|
44
|
-
# Qi.new(captures, squares, [0, 1])
|
|
45
|
-
def initialize(captures_hash, squares_hash, turns, **state)
|
|
46
|
-
@captures_hash = ::Hash.new(0).merge(captures_hash.select { |_, v| v > 0 })
|
|
47
|
-
@squares_hash = squares_hash.compact
|
|
48
|
-
@turns = turns
|
|
49
|
-
@state = state.transform_keys(&:to_sym)
|
|
50
|
-
|
|
51
|
-
freeze
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Return an array of captures containing piece names.
|
|
55
|
-
#
|
|
56
|
-
# @return [Array<Object>] an array of captures
|
|
57
|
-
# @example
|
|
58
|
-
# ["S", "b", "g", "g", "g", "g", "n", "n", "n", "n", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "r", "r", "s"]
|
|
59
|
-
def captures_array
|
|
60
|
-
captures_hash.flat_map { |piece, count| ::Array.new(count, piece) }.sort
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Commit a change to the game state and return a new Qi object.
|
|
3
|
+
# A minimal, format-agnostic library for representing positions in
|
|
4
|
+
# two-player, turn-based board games.
|
|
5
|
+
#
|
|
6
|
+
# Qi models the four components of a position as defined by the
|
|
7
|
+
# Sashité Game Protocol:
|
|
8
|
+
#
|
|
9
|
+
# - *Board* — a multi-dimensional rectangular grid (1D, 2D, or 3D)
|
|
10
|
+
# where each square is either empty (+nil+) or occupied by a piece
|
|
11
|
+
# (any non-nil object).
|
|
12
|
+
# - *Hands* — collections of off-board pieces held by each player.
|
|
13
|
+
# - *Styles* — one style value per player side (format-free).
|
|
14
|
+
# - *Turn* — which player is active (+:first+ or +:second+).
|
|
15
|
+
#
|
|
16
|
+
# Piece and style representations are intentionally opaque: Qi validates
|
|
17
|
+
# structure, not semantics. This makes the library reusable across FEEN,
|
|
18
|
+
# PON, or any other encoding that shares the same positional model.
|
|
19
|
+
#
|
|
20
|
+
# @example A 3×3 board with two kings and a pawn in hand
|
|
21
|
+
# board = [[nil, nil, nil], [nil, "K^", nil], [nil, nil, "k^"]]
|
|
22
|
+
# hands = { first: ["+P"], second: [] }
|
|
23
|
+
# pos = Qi.new(board, hands, { first: "C", second: "c" }, :first)
|
|
24
|
+
# pos.turn #=> :first
|
|
25
|
+
# pos.hands[:first] #=> ["+P"]
|
|
26
|
+
module Qi
|
|
27
|
+
require_relative "qi/board"
|
|
28
|
+
require_relative "qi/hands"
|
|
29
|
+
require_relative "qi/styles"
|
|
30
|
+
require_relative "qi/position"
|
|
31
|
+
|
|
32
|
+
# Creates a new position after validating all structural constraints.
|
|
64
33
|
#
|
|
65
|
-
# @param
|
|
66
|
-
# @param
|
|
67
|
-
# @param
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
# @
|
|
71
|
-
# @return [Qi] a new Qi object representing the updated game state
|
|
72
|
-
# @example
|
|
73
|
-
# qi0.commit([], [], { D43: nil, B13: "+B" }, in_check: true)
|
|
74
|
-
def commit(add_captures_array, del_captures_array, edit_squares_hash, **state)
|
|
75
|
-
self.class.new(
|
|
76
|
-
edit_captures_hash(add_captures_array.compact, del_captures_array.compact, **captures_hash),
|
|
77
|
-
squares_hash.merge(edit_squares_hash),
|
|
78
|
-
turns.rotate,
|
|
79
|
-
**state
|
|
80
|
-
)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Check if the current Qi object is equal to another Qi object.
|
|
34
|
+
# @param board [Array] nested array representing the board (1D to 3D).
|
|
35
|
+
# @param hands [Hash] +{ first: Array, second: Array }+ of off-board pieces.
|
|
36
|
+
# @param styles [Hash] +{ first: Object, second: Object }+ of player styles.
|
|
37
|
+
# @param turn [Symbol] +:first+ or +:second+.
|
|
38
|
+
# @return [Qi::Position] an immutable, validated position.
|
|
39
|
+
# @raise [ArgumentError] if any structural constraint is violated.
|
|
84
40
|
#
|
|
85
|
-
# @
|
|
86
|
-
#
|
|
87
|
-
def eql?(other)
|
|
88
|
-
return false unless other.captures_hash == captures_hash
|
|
89
|
-
return false unless other.squares_hash == squares_hash
|
|
90
|
-
return false unless other.turn == turn
|
|
91
|
-
return false unless other.state == state
|
|
92
|
-
|
|
93
|
-
true
|
|
94
|
-
end
|
|
95
|
-
alias == eql?
|
|
96
|
-
|
|
97
|
-
# Get the current turn.
|
|
41
|
+
# @example A valid position
|
|
42
|
+
# Qi.new([[:a, nil], [nil, :b]], { first: [], second: [] }, { first: "C", second: "c" }, :first)
|
|
98
43
|
#
|
|
99
|
-
# @
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
# Edits the captures hash and returns it.
|
|
107
|
-
#
|
|
108
|
-
# @param add_captures_array [Array<Object>] an array of pieces to be added to captures
|
|
109
|
-
# @param del_captures_array [Array<Object>] an array of pieces to be deleted from captures
|
|
110
|
-
# @param hash [Hash<Object, Integer>] the current captures hash
|
|
111
|
-
# @return [Hash<Object, Integer>] the updated captures hash
|
|
112
|
-
def edit_captures_hash(add_captures_array, del_captures_array, **hash)
|
|
113
|
-
add_captures_array.each { |piece_name| hash[piece_name] += 1 }
|
|
114
|
-
del_captures_array.each { |piece_name| hash[piece_name] -= 1 }
|
|
115
|
-
|
|
116
|
-
hash
|
|
44
|
+
# @example An invalid position (too many pieces for the board)
|
|
45
|
+
# Qi.new([:k], { first: [:P], second: [] }, { first: "C", second: "c" }, :first)
|
|
46
|
+
# # => ArgumentError: too many pieces for board size (2 pieces, 1 squares)
|
|
47
|
+
def self.new(board, hands, styles, turn)
|
|
48
|
+
Position.new(board, hands, styles, turn)
|
|
117
49
|
end
|
|
118
50
|
end
|
metadata
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: qi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 11.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cyril Kato
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
|
-
description: A
|
|
14
|
-
|
|
12
|
+
description: A minimal, format-agnostic library for representing positions in two-player,
|
|
13
|
+
turn-based board games (chess, shogi, xiangqi, and variants).
|
|
15
14
|
email: contact@cyril.email
|
|
16
15
|
executables: []
|
|
17
16
|
extensions: []
|
|
18
17
|
extra_rdoc_files: []
|
|
19
18
|
files:
|
|
20
|
-
- LICENSE.md
|
|
21
19
|
- README.md
|
|
22
20
|
- lib/qi.rb
|
|
21
|
+
- lib/qi/board.rb
|
|
22
|
+
- lib/qi/hands.rb
|
|
23
|
+
- lib/qi/position.rb
|
|
24
|
+
- lib/qi/styles.rb
|
|
23
25
|
homepage: https://github.com/sashite/qi.rb
|
|
24
26
|
licenses:
|
|
25
|
-
-
|
|
27
|
+
- Apache-2.0
|
|
26
28
|
metadata:
|
|
27
29
|
rubygems_mfa_required: 'true'
|
|
28
|
-
post_install_message:
|
|
29
30
|
rdoc_options: []
|
|
30
31
|
require_paths:
|
|
31
32
|
- lib
|
|
@@ -40,8 +41,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
40
41
|
- !ruby/object:Gem::Version
|
|
41
42
|
version: '0'
|
|
42
43
|
requirements: []
|
|
43
|
-
rubygems_version:
|
|
44
|
-
signing_key:
|
|
44
|
+
rubygems_version: 4.0.5
|
|
45
45
|
specification_version: 4
|
|
46
|
-
summary:
|
|
46
|
+
summary: A minimal, format-agnostic position model for two-player board games.
|
|
47
47
|
test_files: []
|
data/LICENSE.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# The MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2015-2023 Sashité
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
|
13
|
-
all copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
THE SOFTWARE.
|