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.
Files changed (7) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +98 -76
  3. data/lib/qi/board.rb +140 -91
  4. data/lib/qi/hands.rb +73 -54
  5. data/lib/qi/styles.rb +38 -54
  6. data/lib/qi.rb +126 -206
  7. 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 validation functions for multi-dimensional board structures.
4
+ # Pure functions for flat board operations.
5
5
  #
6
- # A board is represented as a nested Array where:
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
- # - 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.
10
+ # This module provides three categories of functions:
11
11
  #
12
- # Each leaf element (square) is either +nil+ (empty) or any non-nil
13
- # object (a piece). String normalization of pieces is the responsibility
14
- # of the +Qi+ class, not of this module.
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
- # == Constraints
17
+ # All functions are stateless and side-effect-free.
17
18
  #
18
- # - Maximum dimensionality: 3
19
- # - Maximum size per dimension: 255
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 Validate a 2D board
25
- # Qi::Board.validate([["a", nil], [nil, "b"]]) #=> [4, 2]
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 Validate an empty board
28
- # Qi::Board.validate([[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]]) #=> [9, 0]
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 = 3
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 a board and returns its square and piece counts.
36
+ # Validates board dimensions and returns the total square count.
34
37
  #
35
- # Validation is performed in a single recursive pass that simultaneously
36
- # infers the board shape, verifies structural regularity, checks dimension
37
- # limits, and counts squares and pieces.
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
- # @param board [Object] the board structure to validate.
40
- # @return [Array(Integer, Integer)] +[square_count, piece_count]+.
41
- # @raise [ArgumentError] if the board is structurally invalid.
42
- #
43
- # @example A 2D board
44
- # Qi::Board.validate([["r", nil, nil], [nil, nil, "R"]]) #=> [6, 2]
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 board.empty?
61
- raise ::ArgumentError, "board must not be empty"
52
+ if shape.size > MAX_DIMENSIONS
53
+ raise ::ArgumentError, "board exceeds #{MAX_DIMENSIONS} dimensions (got #{shape.size})"
62
54
  end
63
55
 
64
- validate_recursive(board, nil, 0)
65
- end
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
- # Single-pass recursive validation.
68
- #
69
- # At each level, determines whether this is a leaf rank (contains
70
- # non-Array elements) or an intermediate dimension (contains Arrays).
71
- # Infers expected sizes from the first element at each level,
72
- # validates all siblings match, and enforces dimension limits.
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
- if node.size > MAX_DIMENSION_SIZE
84
- raise ::ArgumentError, "dimension size #{node.size} exceeds maximum of #{MAX_DIMENSION_SIZE}"
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
- if node.first.is_a?(::Array)
88
- # Intermediate dimension: validate depth, then recurse.
89
- if depth >= MAX_DIMENSIONS - 1
90
- raise ::ArgumentError, "board exceeds #{MAX_DIMENSIONS} dimensions"
91
- end
76
+ total
77
+ end
92
78
 
93
- inner_size = node.first.size
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
- if inner_size == 0
96
- raise ::ArgumentError, "board must not be empty"
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
- total_squares = 0
100
- total_pieces = 0
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
- node.each do |sub|
103
- unless sub.is_a?(::Array)
104
- raise ::ArgumentError, "inconsistent board structure: mixed arrays and non-arrays at same level"
105
- end
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
- sq, pc = validate_recursive(sub, inner_size, depth + 1)
108
- total_squares += sq
109
- total_pieces += pc
110
- end
121
+ [new_board, board_piece_count + delta]
122
+ end
111
123
 
112
- [total_squares, total_pieces]
113
- else
114
- # Leaf rank: validate structure, then count pieces.
115
- node.each do |square|
116
- if square.is_a?(::Array)
117
- raise ::ArgumentError, "inconsistent board structure: expected flat squares at this level"
118
- end
119
- end
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
- [node.size, node.count { |sq| !sq.nil? }]
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
- private_class_method :validate_recursive
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
- freeze
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 validation functions for player hands.
4
+ # Pure functions for player hand operations.
5
5
  #
6
- # Hands are represented as a Hash with exactly two keys:
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
- # - +:first+ array of pieces held by the first player.
9
- # - +:second+ array of pieces held by the second player.
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
- # Each piece in a hand can be any non-nil object. String normalization
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 Validate hands with pieces
16
- # Qi::Hands.validate({ first: ["+P", "+P"], second: ["b"] }) #=> 3
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 Validate empty hands
19
- # Qi::Hands.validate({ first: [], second: [] }) #=> 0
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
- # Validates hands structure and returns the total piece count.
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
- # Validation checks shape (exactly two keys), type (both values are
24
- # arrays), and rejects +nil+ elements in each hand.
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
- # @param hands [Object] the hands structure to validate.
27
- # @return [Integer] the total number of pieces across both hands.
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
- # @example Valid hands
31
- # Qi::Hands.validate({ first: ["P", "B"], second: ["p"] }) #=> 3
39
+ # The original hand is not modified.
32
40
  #
33
- # @example Nil piece rejected
34
- # Qi::Hands.validate({ first: [nil], second: [] })
35
- # # => ArgumentError: hand pieces must not be nil
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 Missing key
38
- # Qi::Hands.validate({ first: [] })
39
- # # => ArgumentError: hands must have exactly keys :first and :second
40
- def self.validate(hands)
41
- validate_shape(hands)
42
- validate_arrays(hands)
43
- validate_hand(hands[:first])
44
- validate_hand(hands[:second])
45
- hands[:first].size + hands[:second].size
46
- end
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
- # --- Shape validation -----------------------------------------------------
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
- 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
72
+ next if delta == 0
54
73
 
55
- return if hands.size == 2 && hands.key?(:first) && hands.key?(:second)
74
+ piece = piece_key.is_a?(::Symbol) ? piece_key.name : piece_key
56
75
 
57
- raise ::ArgumentError, "hands must have exactly keys :first and :second"
58
- end
76
+ if piece.bytesize > MAX_PIECE_BYTESIZE
77
+ raise ::ArgumentError, "piece exceeds #{MAX_PIECE_BYTESIZE} bytes (got #{piece.bytesize})"
78
+ end
59
79
 
60
- def self.validate_arrays(hands)
61
- return if hands[:first].is_a?(::Array) && hands[:second].is_a?(::Array)
80
+ current = result[piece] || 0
81
+ new_count = current + delta
62
82
 
63
- raise ::ArgumentError, "each hand must be an Array"
64
- end
83
+ if new_count < 0
84
+ raise ::ArgumentError, "cannot remove #{piece.inspect}: not found in hand"
85
+ end
65
86
 
66
- # --- Piece validation -----------------------------------------------------
87
+ if new_count == 0
88
+ result.delete(piece)
89
+ else
90
+ result[piece] = new_count
91
+ end
67
92
 
68
- def self.validate_hand(pieces)
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
- private_class_method :validate_shape,
75
- :validate_arrays,
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 functions for player styles.
4
+ # Pure validation function for player styles.
5
5
  #
6
- # Styles are represented as a Hash with exactly two keys:
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
- # - +:first+ the style associated with the first player side.
9
- # - +:second+ — the style associated with the second player side.
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
- # Validates the styles structure.
14
+ MAX_STYLE_BYTESIZE = 255
15
+
16
+ # Validates a single player style and returns it.
23
17
  #
24
- # Returns +nil+ if the Hash has exactly keys +:first+ and +:second+ with
25
- # non-nil values, or raises +ArgumentError+ otherwise.
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 styles [Object] the styles structure to validate.
28
- # @return [nil]
29
- # @raise [ArgumentError] if the styles structure is invalid.
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 styles
32
- # Qi::Styles.validate({ first: "S", second: "s" }) #=> nil
28
+ # @example Valid style
29
+ # Qi::Styles.validate(:first, "C") #=> "C"
33
30
  #
34
- # @example Nil first style
35
- # Qi::Styles.validate({ first: nil, second: "c" })
31
+ # @example Nil style
32
+ # Qi::Styles.validate(:first, nil)
36
33
  # # => ArgumentError: first player style must not be nil
37
34
  #
38
- # @example Nil second style
39
- # Qi::Styles.validate({ first: "C", second: nil })
40
- # # => ArgumentError: second player style must not be nil
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 Not a Hash
47
- # Qi::Styles.validate("not a hash")
48
- # # => ArgumentError: styles must be a Hash with keys :first and :second
49
- def self.validate(styles)
50
- validate_shape(styles)
51
- validate_non_nil(styles)
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
- return if styles.size == 2 && styles.key?(:first) && styles.key?(:second)
47
+ unless style.is_a?(::String)
48
+ raise ::ArgumentError, "#{side} player style must be a String"
49
+ end
60
50
 
61
- raise ::ArgumentError, "styles must have exactly keys :first and :second"
62
- end
51
+ if style.bytesize > MAX_STYLE_BYTESIZE
52
+ raise ::ArgumentError, "#{side} player style exceeds #{MAX_STYLE_BYTESIZE} bytes"
53
+ end
63
54
 
64
- def self.validate_non_nil(styles)
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