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
data/lib/qi/board.rb
CHANGED
|
@@ -1,129 +1,178 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Qi
|
|
4
|
-
# Pure
|
|
4
|
+
# Pure functions for flat board operations.
|
|
5
5
|
#
|
|
6
|
-
# A board is
|
|
6
|
+
# A board is stored as a flat +Array+ in row-major order where each
|
|
7
|
+
# element is either +nil+ (empty square) or a +String+ (a piece).
|
|
8
|
+
# The board shape (dimensions) is maintained separately.
|
|
7
9
|
#
|
|
8
|
-
#
|
|
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.
|
|
10
|
+
# This module provides three categories of functions:
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# - *Shape validation* — checks dimension count, types, and bounds.
|
|
13
|
+
# - *Diff application* — applies index-to-piece changes to a flat board.
|
|
14
|
+
# - *Nested conversion* — reconstructs a nested array from a flat board
|
|
15
|
+
# and its shape, for display or serialization.
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
+
# All functions are stateless and side-effect-free.
|
|
17
18
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# - At least one square (non-empty board)
|
|
21
|
-
# - Rectangular structure: all sub-arrays at the same depth must have
|
|
22
|
-
# identical length (enforced globally, not just per-sibling).
|
|
19
|
+
# @example Validate a shape
|
|
20
|
+
# Qi::Board.validate_shape([8, 8]) #=> 64
|
|
23
21
|
#
|
|
24
|
-
# @example
|
|
25
|
-
#
|
|
22
|
+
# @example Apply a diff
|
|
23
|
+
# board = Array.new(4)
|
|
24
|
+
# new_board, new_count = Qi::Board.apply_diff(board, 4, 0, { 0 => "K", 3 => "k" })
|
|
25
|
+
# new_board #=> ["K", nil, nil, "k"]
|
|
26
|
+
# new_count #=> 2
|
|
26
27
|
#
|
|
27
|
-
# @example
|
|
28
|
-
# Qi::Board.
|
|
28
|
+
# @example Convert to nested
|
|
29
|
+
# Qi::Board.to_nested(["a", nil, nil, "b"], [2, 2]) #=> [["a", nil], [nil, "b"]]
|
|
29
30
|
module Board
|
|
30
|
-
MAX_DIMENSIONS
|
|
31
|
+
MAX_DIMENSIONS = 3
|
|
31
32
|
MAX_DIMENSION_SIZE = 255
|
|
33
|
+
MAX_SQUARE_COUNT = 65_025
|
|
34
|
+
MAX_PIECE_BYTESIZE = 255
|
|
32
35
|
|
|
33
|
-
# Validates
|
|
36
|
+
# Validates board dimensions and returns the total square count.
|
|
34
37
|
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
+
# @param shape [Array<Integer>] dimension sizes (1 to 3 integers, each 1–255).
|
|
39
|
+
# @return [Integer] the total number of squares.
|
|
40
|
+
# @raise [ArgumentError] if the shape is invalid.
|
|
41
|
+
# @raise [ArgumentError] if the total square count exceeds {MAX_SQUARE_COUNT}.
|
|
38
42
|
#
|
|
39
|
-
# @
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# @example A 1D board
|
|
47
|
-
# Qi::Board.validate(["k", nil, nil, "K"]) #=> [4, 2]
|
|
48
|
-
#
|
|
49
|
-
# @example A 3D board (2 layers × 2 ranks × 2 files)
|
|
50
|
-
# Qi::Board.validate([[["a", nil], [nil, "b"]], [[nil, "c"], ["d", nil]]]) #=> [8, 4]
|
|
51
|
-
#
|
|
52
|
-
# @example Non-rectangular boards are rejected
|
|
53
|
-
# Qi::Board.validate([["a", "b"], ["c"]])
|
|
54
|
-
# # => ArgumentError: non-rectangular board: expected 2 elements, got 1
|
|
55
|
-
def self.validate(board)
|
|
56
|
-
unless board.is_a?(::Array)
|
|
57
|
-
raise ::ArgumentError, "board must be an Array"
|
|
43
|
+
# @example
|
|
44
|
+
# Qi::Board.validate_shape([8, 8]) #=> 64
|
|
45
|
+
# Qi::Board.validate_shape([9, 9]) #=> 81
|
|
46
|
+
# Qi::Board.validate_shape([5, 5, 5]) #=> 125
|
|
47
|
+
def self.validate_shape(shape)
|
|
48
|
+
if shape.empty?
|
|
49
|
+
raise ::ArgumentError, "at least one dimension is required"
|
|
58
50
|
end
|
|
59
51
|
|
|
60
|
-
if
|
|
61
|
-
raise ::ArgumentError, "board
|
|
52
|
+
if shape.size > MAX_DIMENSIONS
|
|
53
|
+
raise ::ArgumentError, "board exceeds #{MAX_DIMENSIONS} dimensions (got #{shape.size})"
|
|
62
54
|
end
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
shape.each do |dim|
|
|
57
|
+
unless dim.is_a?(::Integer)
|
|
58
|
+
raise ::ArgumentError, "dimension size must be an Integer, got #{dim.class}"
|
|
59
|
+
end
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# @param node [Array] the current sub-array being validated.
|
|
75
|
-
# @param expected_size [Integer, nil] expected length (nil if first sibling).
|
|
76
|
-
# @param depth [Integer] current nesting depth (0-based).
|
|
77
|
-
# @return [Array(Integer, Integer)] +[square_count, piece_count]+.
|
|
78
|
-
def self.validate_recursive(node, expected_size, depth)
|
|
79
|
-
if expected_size && node.size != expected_size
|
|
80
|
-
raise ::ArgumentError, "non-rectangular board: expected #{expected_size} elements, got #{node.size}"
|
|
61
|
+
if dim < 1
|
|
62
|
+
raise ::ArgumentError, "dimension size must be at least 1, got #{dim}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if dim > MAX_DIMENSION_SIZE
|
|
66
|
+
raise ::ArgumentError, "dimension size #{dim} exceeds maximum of #{MAX_DIMENSION_SIZE}"
|
|
67
|
+
end
|
|
81
68
|
end
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
|
|
70
|
+
total = shape.reduce(:*)
|
|
71
|
+
|
|
72
|
+
if total > MAX_SQUARE_COUNT
|
|
73
|
+
raise ::ArgumentError, "board exceeds #{MAX_SQUARE_COUNT} squares (got #{total})"
|
|
85
74
|
end
|
|
86
75
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if depth >= MAX_DIMENSIONS - 1
|
|
90
|
-
raise ::ArgumentError, "board exceeds #{MAX_DIMENSIONS} dimensions"
|
|
91
|
-
end
|
|
76
|
+
total
|
|
77
|
+
end
|
|
92
78
|
|
|
93
|
-
|
|
79
|
+
# Applies changes to a flat board, returning a new array and updated piece count.
|
|
80
|
+
#
|
|
81
|
+
# Each change maps a flat index to a piece (+String+) or +nil+ (empty).
|
|
82
|
+
# The original board is not modified.
|
|
83
|
+
#
|
|
84
|
+
# @param board [Array] the current flat board.
|
|
85
|
+
# @param square_count [Integer] number of squares on the board.
|
|
86
|
+
# @param board_piece_count [Integer] current number of pieces on the board.
|
|
87
|
+
# @param changes [Hash{Integer => String, nil}] flat index to piece mapping.
|
|
88
|
+
# @return [Array(Array, Integer)] +[new_board, new_board_piece_count]+.
|
|
89
|
+
# @raise [ArgumentError] if an index is invalid or a piece is not a String.
|
|
90
|
+
# @raise [ArgumentError] if a piece exceeds {MAX_PIECE_BYTESIZE} bytes.
|
|
91
|
+
#
|
|
92
|
+
# @example Place two pieces
|
|
93
|
+
# Qi::Board.apply_diff(Array.new(4), 4, 0, { 0 => "K", 3 => "k" })
|
|
94
|
+
# #=> [["K", nil, nil, "k"], 2]
|
|
95
|
+
#
|
|
96
|
+
# @example Move a piece
|
|
97
|
+
# Qi::Board.apply_diff(["K", nil, nil, nil], 4, 1, { 0 => nil, 2 => "K" })
|
|
98
|
+
# #=> [[nil, nil, "K", nil], 1]
|
|
99
|
+
def self.apply_diff(board, square_count, board_piece_count, changes)
|
|
100
|
+
new_board = board.dup
|
|
101
|
+
delta = 0
|
|
102
|
+
|
|
103
|
+
changes.each do |index, piece|
|
|
104
|
+
unless index.is_a?(::Integer) && index >= 0 && index < square_count
|
|
105
|
+
raise ::ArgumentError, "invalid flat index: #{index} (board has #{square_count} squares)"
|
|
106
|
+
end
|
|
94
107
|
|
|
95
|
-
|
|
96
|
-
raise ::ArgumentError, "
|
|
108
|
+
unless piece.nil? || piece.is_a?(::String)
|
|
109
|
+
raise ::ArgumentError, "piece must be a String, got #{piece.class}"
|
|
97
110
|
end
|
|
98
111
|
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
if piece.is_a?(::String) && piece.bytesize > MAX_PIECE_BYTESIZE
|
|
113
|
+
raise ::ArgumentError, "piece exceeds #{MAX_PIECE_BYTESIZE} bytes (got #{piece.bytesize})"
|
|
114
|
+
end
|
|
101
115
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
old = new_board[index]
|
|
117
|
+
delta += (piece.nil? ? 0 : 1) - (old.nil? ? 0 : 1)
|
|
118
|
+
new_board[index] = piece
|
|
119
|
+
end
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
total_pieces += pc
|
|
110
|
-
end
|
|
121
|
+
[new_board, board_piece_count + delta]
|
|
122
|
+
end
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
# Converts a flat board into a nested array matching the given shape.
|
|
125
|
+
#
|
|
126
|
+
# This is an O(n) operation intended for display or serialization,
|
|
127
|
+
# not for the hot path.
|
|
128
|
+
#
|
|
129
|
+
# @param board [Array] the flat board.
|
|
130
|
+
# @param shape [Array<Integer>] the board dimensions.
|
|
131
|
+
# @return [Array] a nested array. For a 1D shape, returns a flat array copy.
|
|
132
|
+
#
|
|
133
|
+
# @example 1D board
|
|
134
|
+
# Qi::Board.to_nested(["a", nil, "b"], [3])
|
|
135
|
+
# #=> ["a", nil, "b"]
|
|
136
|
+
#
|
|
137
|
+
# @example 2D board
|
|
138
|
+
# Qi::Board.to_nested(["a", nil, nil, "b"], [2, 2])
|
|
139
|
+
# #=> [["a", nil], [nil, "b"]]
|
|
140
|
+
#
|
|
141
|
+
# @example 3D board (2×2×2)
|
|
142
|
+
# flat = ["a", nil, nil, "b", nil, "c", "d", nil]
|
|
143
|
+
# Qi::Board.to_nested(flat, [2, 2, 2])
|
|
144
|
+
# #=> [[["a", nil], [nil, "b"]], [[nil, "c"], ["d", nil]]]
|
|
145
|
+
def self.to_nested(board, shape)
|
|
146
|
+
return board.dup if shape.size == 1
|
|
147
|
+
|
|
148
|
+
nest(board, compute_chunk_sizes(shape), 0)
|
|
149
|
+
end
|
|
120
150
|
|
|
121
|
-
|
|
151
|
+
# Pre-computes chunk sizes for each dimension level.
|
|
152
|
+
#
|
|
153
|
+
# For shape [8, 8], returns [8, 1].
|
|
154
|
+
# For shape [5, 5, 5], returns [25, 5, 1].
|
|
155
|
+
# For shape [8], returns [1].
|
|
156
|
+
def self.compute_chunk_sizes(shape)
|
|
157
|
+
sizes = ::Array.new(shape.size)
|
|
158
|
+
sizes[-1] = 1
|
|
159
|
+
(shape.size - 2).downto(0) do |i|
|
|
160
|
+
sizes[i] = sizes[i + 1] * shape[i + 1]
|
|
122
161
|
end
|
|
162
|
+
sizes
|
|
123
163
|
end
|
|
124
164
|
|
|
125
|
-
|
|
165
|
+
# Recursively slices a flat array into nested sub-arrays.
|
|
166
|
+
def self.nest(flat, chunk_sizes, dim)
|
|
167
|
+
if dim == chunk_sizes.size - 1
|
|
168
|
+
return flat.dup
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
chunk = chunk_sizes[dim]
|
|
172
|
+
flat.each_slice(chunk).map { |slice| nest(slice, chunk_sizes, dim + 1) }
|
|
173
|
+
end
|
|
126
174
|
|
|
127
|
-
|
|
175
|
+
private_class_method :compute_chunk_sizes,
|
|
176
|
+
:nest
|
|
128
177
|
end
|
|
129
178
|
end
|
data/lib/qi/hands.rb
CHANGED
|
@@ -1,80 +1,99 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Qi
|
|
4
|
-
# Pure
|
|
4
|
+
# Pure functions for player hand operations.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
6
|
+
# A hand is represented as a +Hash{String => Integer}+ mapping each
|
|
7
|
+
# piece to its count. An empty hand is +{}+. Entries whose count
|
|
8
|
+
# reaches zero are removed from the hash.
|
|
7
9
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
+
# This representation gives O(1) add, remove, and count queries per
|
|
11
|
+
# piece type, compared to O(n) scans on a flat array.
|
|
10
12
|
#
|
|
11
|
-
#
|
|
12
|
-
# of pieces is the responsibility of the +Qi+ class, not of this module.
|
|
13
|
-
# The ordering of pieces within a hand carries no semantic meaning.
|
|
13
|
+
# All functions are stateless and side-effect-free.
|
|
14
14
|
#
|
|
15
|
-
# @example
|
|
16
|
-
#
|
|
15
|
+
# @example Apply a diff
|
|
16
|
+
# hand = { "P" => 1 }
|
|
17
|
+
# new_hand, count = Qi::Hands.apply_diff(hand, 1, { "P" => 1, "B" => 1 })
|
|
18
|
+
# new_hand #=> { "P" => 2, "B" => 1 }
|
|
19
|
+
# count #=> 3
|
|
17
20
|
#
|
|
18
|
-
# @example
|
|
19
|
-
#
|
|
21
|
+
# @example Remove pieces
|
|
22
|
+
# hand = { "P" => 2, "B" => 1 }
|
|
23
|
+
# new_hand, count = Qi::Hands.apply_diff(hand, 3, { "P" => -1, "B" => -1 })
|
|
24
|
+
# new_hand #=> { "P" => 1 }
|
|
25
|
+
# count #=> 1
|
|
20
26
|
module Hands
|
|
21
|
-
|
|
27
|
+
MAX_PIECE_BYTESIZE = 255
|
|
28
|
+
|
|
29
|
+
# Applies delta changes to a hand, returning the new hash and its
|
|
30
|
+
# piece count.
|
|
22
31
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
32
|
+
# Each change maps a piece (+String+) to an integer delta: positive
|
|
33
|
+
# to add copies, negative to remove, zero is a no-op. Entries whose
|
|
34
|
+
# count reaches zero are removed from the result.
|
|
25
35
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# @raise [ArgumentError] if the hands structure is invalid.
|
|
36
|
+
# The piece count is computed incrementally during the diff — no
|
|
37
|
+
# extra iteration over the result hash is needed.
|
|
29
38
|
#
|
|
30
|
-
#
|
|
31
|
-
# Qi::Hands.validate({ first: ["P", "B"], second: ["p"] }) #=> 3
|
|
39
|
+
# The original hand is not modified.
|
|
32
40
|
#
|
|
33
|
-
# @
|
|
34
|
-
#
|
|
35
|
-
#
|
|
41
|
+
# @param hand [Hash{String => Integer}] the current hand.
|
|
42
|
+
# @param hand_count [Integer] current total piece count of +hand+.
|
|
43
|
+
# @param changes [Hash{String => Integer}] piece to delta mapping.
|
|
44
|
+
# Keys are normalized from Symbol to String (Ruby keyword argument
|
|
45
|
+
# convention).
|
|
46
|
+
# @return [Array(Hash{String => Integer}, Integer)]
|
|
47
|
+
# +[new_hand, new_piece_count]+.
|
|
48
|
+
# @raise [ArgumentError] if a delta is not an Integer.
|
|
49
|
+
# @raise [ArgumentError] if a piece exceeds {MAX_PIECE_BYTESIZE} bytes.
|
|
50
|
+
# @raise [ArgumentError] if removing more pieces than present.
|
|
36
51
|
#
|
|
37
|
-
# @example
|
|
38
|
-
# Qi::Hands.
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
# @example Add pieces
|
|
53
|
+
# Qi::Hands.apply_diff({}, 0, { "P" => 2, "B" => 1 })
|
|
54
|
+
# #=> [{ "P" => 2, "B" => 1 }, 3]
|
|
55
|
+
#
|
|
56
|
+
# @example Remove a piece
|
|
57
|
+
# Qi::Hands.apply_diff({ "P" => 2, "B" => 1 }, 3, { "P" => -1 })
|
|
58
|
+
# #=> [{ "P" => 1, "B" => 1 }, 2]
|
|
59
|
+
#
|
|
60
|
+
# @example Zero delta is a no-op
|
|
61
|
+
# Qi::Hands.apply_diff({ "P" => 1 }, 1, { "P" => 0 })
|
|
62
|
+
# #=> [{ "P" => 1 }, 1]
|
|
63
|
+
def self.apply_diff(hand, hand_count, changes)
|
|
64
|
+
result = hand.dup
|
|
65
|
+
count = hand_count
|
|
47
66
|
|
|
48
|
-
|
|
67
|
+
changes.each do |piece_key, delta|
|
|
68
|
+
unless delta.is_a?(::Integer)
|
|
69
|
+
raise ::ArgumentError, "delta must be an Integer, got #{delta.class} for piece #{piece_key.inspect}"
|
|
70
|
+
end
|
|
49
71
|
|
|
50
|
-
|
|
51
|
-
unless hands.is_a?(::Hash)
|
|
52
|
-
raise ::ArgumentError, "hands must be a Hash with keys :first and :second"
|
|
53
|
-
end
|
|
72
|
+
next if delta == 0
|
|
54
73
|
|
|
55
|
-
|
|
74
|
+
piece = piece_key.is_a?(::Symbol) ? piece_key.name : piece_key
|
|
56
75
|
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
if piece.bytesize > MAX_PIECE_BYTESIZE
|
|
77
|
+
raise ::ArgumentError, "piece exceeds #{MAX_PIECE_BYTESIZE} bytes (got #{piece.bytesize})"
|
|
78
|
+
end
|
|
59
79
|
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
current = result[piece] || 0
|
|
81
|
+
new_count = current + delta
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
if new_count < 0
|
|
84
|
+
raise ::ArgumentError, "cannot remove #{piece.inspect}: not found in hand"
|
|
85
|
+
end
|
|
65
86
|
|
|
66
|
-
|
|
87
|
+
if new_count == 0
|
|
88
|
+
result.delete(piece)
|
|
89
|
+
else
|
|
90
|
+
result[piece] = new_count
|
|
91
|
+
end
|
|
67
92
|
|
|
68
|
-
|
|
69
|
-
pieces.each do |piece|
|
|
70
|
-
raise ::ArgumentError, "hand pieces must not be nil" if piece.nil?
|
|
93
|
+
count += delta
|
|
71
94
|
end
|
|
72
|
-
end
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
:validate_hand
|
|
77
|
-
|
|
78
|
-
freeze
|
|
96
|
+
[result, count]
|
|
97
|
+
end
|
|
79
98
|
end
|
|
80
99
|
end
|
data/lib/qi/styles.rb
CHANGED
|
@@ -1,74 +1,58 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Qi
|
|
4
|
-
# Pure validation
|
|
4
|
+
# Pure validation function for player styles.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
6
|
+
# A style is a +String+ label denoting a movement tradition or game
|
|
7
|
+
# family (e.g., +"C"+, +"S"+, +"X"+). Semantic validation (e.g., SIN
|
|
8
|
+
# compliance) is the responsibility of the encoding layer (FEEN, PON,
|
|
9
|
+
# etc.).
|
|
7
10
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# Style values can be any non-nil object. String normalization is the
|
|
12
|
-
# responsibility of the +Qi+ class, not of this module. Semantic
|
|
13
|
-
# validation (e.g., SIN compliance) is the responsibility of the
|
|
14
|
-
# encoding layer (FEEN, PON, etc.).
|
|
15
|
-
#
|
|
16
|
-
# @example Validate string styles
|
|
17
|
-
# Qi::Styles.validate({ first: "C", second: "c" }) #=> nil
|
|
18
|
-
#
|
|
19
|
-
# @example Validate symbol styles
|
|
20
|
-
# Qi::Styles.validate({ first: :chess, second: :shogi }) #=> nil
|
|
11
|
+
# @example Validate a style
|
|
12
|
+
# Qi::Styles.validate(:first, "C") #=> "C"
|
|
21
13
|
module Styles
|
|
22
|
-
|
|
14
|
+
MAX_STYLE_BYTESIZE = 255
|
|
15
|
+
|
|
16
|
+
# Validates a single player style and returns it.
|
|
23
17
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
18
|
+
# The style must not be +nil+, must be a +String+, and must not
|
|
19
|
+
# exceed {MAX_STYLE_BYTESIZE} bytes. The validated value is
|
|
20
|
+
# returned as-is (no coercion, no allocation).
|
|
26
21
|
#
|
|
27
|
-
# @param
|
|
28
|
-
# @
|
|
29
|
-
# @
|
|
22
|
+
# @param side [Symbol] +:first+ or +:second+, used in error messages.
|
|
23
|
+
# @param style [Object] the style value to validate.
|
|
24
|
+
# @return [String] the validated style.
|
|
25
|
+
# @raise [ArgumentError] if the style is nil or not a String.
|
|
26
|
+
# @raise [ArgumentError] if the style exceeds {MAX_STYLE_BYTESIZE} bytes.
|
|
30
27
|
#
|
|
31
|
-
# @example Valid
|
|
32
|
-
# Qi::Styles.validate(
|
|
28
|
+
# @example Valid style
|
|
29
|
+
# Qi::Styles.validate(:first, "C") #=> "C"
|
|
33
30
|
#
|
|
34
|
-
# @example Nil
|
|
35
|
-
# Qi::Styles.validate(
|
|
31
|
+
# @example Nil style
|
|
32
|
+
# Qi::Styles.validate(:first, nil)
|
|
36
33
|
# # => ArgumentError: first player style must not be nil
|
|
37
34
|
#
|
|
38
|
-
# @example
|
|
39
|
-
# Qi::Styles.validate(
|
|
40
|
-
# # => ArgumentError: second player style must
|
|
41
|
-
#
|
|
42
|
-
# @example Missing key
|
|
43
|
-
# Qi::Styles.validate({ first: "C" })
|
|
44
|
-
# # => ArgumentError: styles must have exactly keys :first and :second
|
|
35
|
+
# @example Non-string style
|
|
36
|
+
# Qi::Styles.validate(:second, :chess)
|
|
37
|
+
# # => ArgumentError: second player style must be a String
|
|
45
38
|
#
|
|
46
|
-
# @example
|
|
47
|
-
# Qi::Styles.validate("
|
|
48
|
-
# # => ArgumentError:
|
|
49
|
-
def self.validate(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def self.validate_shape(styles)
|
|
55
|
-
unless styles.is_a?(::Hash)
|
|
56
|
-
raise ::ArgumentError, "styles must be a Hash with keys :first and :second"
|
|
39
|
+
# @example Oversized style
|
|
40
|
+
# Qi::Styles.validate(:first, "A" * 256)
|
|
41
|
+
# # => ArgumentError: first player style exceeds 255 bytes
|
|
42
|
+
def self.validate(side, style)
|
|
43
|
+
if style.nil?
|
|
44
|
+
raise ::ArgumentError, "#{side} player style must not be nil"
|
|
57
45
|
end
|
|
58
46
|
|
|
59
|
-
|
|
47
|
+
unless style.is_a?(::String)
|
|
48
|
+
raise ::ArgumentError, "#{side} player style must be a String"
|
|
49
|
+
end
|
|
60
50
|
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
if style.bytesize > MAX_STYLE_BYTESIZE
|
|
52
|
+
raise ::ArgumentError, "#{side} player style exceeds #{MAX_STYLE_BYTESIZE} bytes"
|
|
53
|
+
end
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
raise ::ArgumentError, "first player style must not be nil" if styles[:first].nil?
|
|
66
|
-
raise ::ArgumentError, "second player style must not be nil" if styles[:second].nil?
|
|
55
|
+
style
|
|
67
56
|
end
|
|
68
|
-
|
|
69
|
-
private_class_method :validate_shape,
|
|
70
|
-
:validate_non_nil
|
|
71
|
-
|
|
72
|
-
freeze
|
|
73
57
|
end
|
|
74
58
|
end
|