qi 10.0.0.beta9 → 10.0.0.beta11

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 (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -10
  3. data/lib/qi/error/drop.rb +5 -0
  4. data/lib/qi.rb +180 -91
  5. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 807fc7b14621e81663ea476f2d737352d455d4d2ab1d49efbe722c3ce571ed15
4
- data.tar.gz: 9244deacf36f2717897a23b56de132af40ce308cd5977ee92899f57c49368f0e
3
+ metadata.gz: 79ffab5e654d14cf9f3f7dac1c50bc5b5c612452d0fdea35a00d3529337fbff2
4
+ data.tar.gz: b8fecf9ff5dd97923070809b6a906c8b44cebbee9695ea242e314d53afb9fad8
5
5
  SHA512:
6
- metadata.gz: 87cedbb94ddcecb3026647893aac63ce6559c92d37dc65121850efca0eb1ddd7a1616e441a0c40463a2bbe664de59d4498bbcfcfd3aa3caae6075a65851c57c5
7
- data.tar.gz: 62a213dcaeb4d68cc6c4bc01b4d9b4a2df33b3d8bcca09c5e494e6a5b0eb349b236907437caa12c11777c6eaac559402e927f3db972f89acb4cd07f77408c4d9
6
+ metadata.gz: cb36bd477ba3e756b116e3f2ab55bdb15ea50834461cc614865d8fdda56e271eff4675d998172bd766b8ca33e6944be3bbf567a1eba96b6215816234c485f6c9
7
+ data.tar.gz: e56b5f73bf2c0a1a3a256f813acfcf24af3bc3cdd570f2fb6ab1aa2f7ebc194ddd0b82e5ade5cbc052229377a8c768d3f49f45188d811a756ab7781481c69f08
data/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  Add this line to your application's Gemfile:
14
14
 
15
15
  ```ruby
16
- gem "qi", ">= 10.0.0.beta9"
16
+ gem "qi", ">= 10.0.0.beta11"
17
17
  ```
18
18
 
19
19
  And then execute:
@@ -45,30 +45,30 @@ qi0.south_captures # => ["S"]
45
45
  qi0.squares # => {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 43=>"+B"}
46
46
  qi0.north_turn? # => false
47
47
  qi0.south_turn? # => true
48
- qi0.serialize # => "south-turn===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===3:s,4:k,5:s,22:+P,43:+B"
49
- qi0.inspect # => "<Qi south-turn===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===3:s,4:k,5:s,22:+P,43:+B>"
50
-
48
+ qi0.serialize # => "South-turn===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,S===3:s,4:k,5:s,22:+P,43:+B"
49
+ qi0.inspect # => "<Qi South-turn===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,S===3:s,4:k,5:s,22:+P,43:+B>"
51
50
  qi0.to_a
52
51
  # [false,
53
52
  # ["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"],
54
53
  # ["S"],
55
- # {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 43=>"+B"}]
54
+ # {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 43=>"+B"},
55
+ # {}]
56
56
 
57
- qi1 = qi0.commit({ 43 => nil, 13 => "+B" })
57
+ qi1 = qi0.commit(43, 13, "+B", nil)
58
58
 
59
59
  qi1.north_captures # => ["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"]
60
60
  qi1.south_captures # => ["S"]
61
61
  qi1.squares # => {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 13=>"+B"}
62
62
  qi1.north_turn? # => true
63
63
  qi1.south_turn? # => false
64
- qi1.serialize # => "north-turn===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===3:s,4:k,5:s,22:+P,13:+B"
65
- qi1.inspect # => "<Qi north-turn===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===3:s,4:k,5:s,22:+P,13:+B>"
66
-
64
+ qi1.serialize # => "North-turn===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,S===3:s,4:k,5:s,13:+B,22:+P"
65
+ qi1.inspect # => "<Qi North-turn===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,S===3:s,4:k,5:s,13:+B,22:+P>"
67
66
  qi1.to_a
68
67
  # [true,
69
68
  # ["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"],
70
69
  # ["S"],
71
- # {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 13=>"+B"}]
70
+ # {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 13=>"+B"},
71
+ # {}]
72
72
  ```
73
73
 
74
74
  ## License
data/lib/qi/error/drop.rb CHANGED
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Qi
4
+ # The Error module contains custom error classes for the Qi game.
4
5
  module Error
6
+ # The Drop class is a custom error class that inherits from IndexError.
7
+ # It is raised when a drop operation in the game of Qi is invalid.
8
+ #
9
+ # @see IndexError
5
10
  class Drop < ::IndexError
6
11
  end
7
12
  end
data/lib/qi.rb CHANGED
@@ -3,45 +3,84 @@
3
3
  require "digest"
4
4
  require_relative "qi/error/drop"
5
5
 
6
- # A class that represents the state of a game.
6
+ # Qi is a class for representing a state of games like Shogi. It includes
7
+ # information about the current turn, captures, game board, and other game options.
7
8
  class Qi
8
- # @!attribute [r] north_captures
9
- # @return [Array] an array of pieces captured by the north player
10
- # @!attribute [r] south_captures
11
- # @return [Array] an array of pieces captured by the south player
12
- # @!attribute [r] squares
13
- # @return [Hash] a hash of pieces on the board
14
- attr_reader :north_captures, :south_captures, :squares
15
-
16
- # Initializes a new Qi object with the given attributes.
17
- #
18
- # @param is_north_turn [Boolean] a boolean value indicating whose turn it is
19
- # @param north_captures [Array<Object>] an array of pieces captured by the north player
20
- # @param south_captures [Array<Object>] an array of pieces captured by the south player
21
- # @param squares [Hash<Object, Object>] a hash of squares on the board
22
- def initialize(is_north_turn, north_captures, south_captures, squares)
23
- # Assign the parameters to instance variables.
9
+ # Constants for representing the North and South players.
10
+ North = :North
11
+ South = :South
12
+
13
+ # @return [Array] the pieces captured by the north player
14
+ attr_reader :north_captures
15
+
16
+ # @return [Array] the pieces captured by the south player
17
+ attr_reader :south_captures
18
+
19
+ # @return [Hash] the current state of the game board
20
+ attr_reader :squares
21
+
22
+ # @return [Hash] additional game options
23
+ attr_reader :options
24
+
25
+ # Initialize a new Qi object.
26
+ #
27
+ # @example Creating a new Qi object
28
+ # qi = Qi.new(true, ['P', 'G'], ['B', '+B'], {56 => 'P', 3 => 'g', 64 => '+B'}, fullmove_number: 42, is_in_check: false)
29
+ #
30
+ # @param [Boolean] is_north_turn true if it's the north player's turn, false otherwise
31
+ # @param [Array] north_captures the pieces captured by the north player
32
+ # @param [Array] south_captures the pieces captured by the south player
33
+ # @param [Hash] squares the current state of the game board
34
+ # @param [Hash] options additional game options
35
+ def initialize(is_north_turn, north_captures, south_captures, squares, **options)
24
36
  @is_north_turn = is_north_turn
25
37
  @north_captures = north_captures.sort
26
38
  @south_captures = south_captures.sort
27
39
  @squares = squares.compact
40
+ @options = options
28
41
  end
29
42
 
30
- # Returns a new Qi object that represents the state after applying the given changes.
43
+ # Apply a move or a drop on the board.
44
+ #
45
+ # The method creates a new instance of Qi representing the state of the game after the move.
46
+ # This includes updating the captures and the positions of the pieces on the board.
47
+ #
48
+ # @param src_square [Object, nil] the source square, or nil if dropping a piece from hand
49
+ # @param dst_square [Object] the destination square
50
+ # @param piece_name [String] the name of the piece to move
51
+ # @param in_hand [String, nil] the piece in hand, or nil if moving a piece from a square
52
+ # @param options [Hash] options to pass to the new instance
53
+ #
54
+ # @example Applying a move
55
+ # qi = Qi.new(true, ['P', 'B'], ['G', '+B'], {56 => 'P', 64 => '+B'})
56
+ # qi.commit(56, 47, 'P', nil) #=> <Qi South-turn===B,P,+B,G===47:P,64:+B>
31
57
  #
32
- # @param diffs [Hash<Object, Object>] a hash of changes to apply to the squares hash
33
- # @param in_hand [Object, nil] the piece that is in hand or nil if none
34
- # @param is_drop [Boolean] a boolean value indicating whether the in hand piece is dropped or not
35
- # @return [Qi] a new Qi object with modified attributes, where:
36
- # - the turn is switched
37
- # - the captures are updated according to the in hand piece and the drop flag
38
- # - the squares are merged with the diffs hash
39
- def commit(diffs = {}, in_hand = nil, is_drop: false)
40
- modified_squares = squares.merge(diffs)
41
- modified_captures = update_captures(in_hand, is_drop:)
42
- self.class.new(!north_turn?, *modified_captures, modified_squares)
58
+ # @example Applying a capture
59
+ # qi = Qi.new(true, ['P', 'B'], ['G', '+B'], {56 => 'P', 64 => '+B'})
60
+ # qi.commit(56, 47, 'P', 'G') #=> <Qi South-turn===B,G,P,+B,G===47:P,64:+B>
61
+ #
62
+ # @example Applying a drop
63
+ # qi = Qi.new(true, ['P', 'B', 'G'], ['G', '+B'], {56 => 'P', 64 => '+B'})
64
+ # qi.commit(nil, 47, 'G', 'G') #=> <Qi South-turn===B,P,+B,G===47:G,56:P,64:+B>
65
+ #
66
+ # @return [Qi] the new game state after the move
67
+ def commit(src_square, dst_square, piece_name, in_hand, **options)
68
+ raise ::ArgumentError, "Both src_square and in_hand cannot be nil" if src_square.nil? && in_hand.nil?
69
+
70
+ modified_captures = update_captures(src_square, in_hand)
71
+ modified_squares = squares.merge({ src_square => nil, dst_square => piece_name })
72
+ self.class.new(south_turn?, *modified_captures, modified_squares, **options)
43
73
  end
44
74
 
75
+ # Compare this Qi object with another for equality.
76
+ #
77
+ # @example Comparing two Qi objects
78
+ # qi1 = Qi.new(...)
79
+ # qi2 = Qi.new(...)
80
+ # qi1.eql?(qi2) #=> true or false
81
+ #
82
+ # @param [Qi] other the other Qi object to compare with
83
+ # @return [Boolean] true if the two objects represent the same game state, false otherwise
45
84
  def eql?(other)
46
85
  return false unless other.respond_to?(:serialize)
47
86
 
@@ -49,130 +88,180 @@ class Qi
49
88
  end
50
89
  alias == eql?
51
90
 
52
- def other_captures
53
- if north_turn?
54
- south_captures
55
- else
56
- north_captures
57
- end
91
+ # Get the captures of the current player.
92
+ #
93
+ # The method returns the north player's captures when it's their turn,
94
+ # and the south player's captures when it's their turn.
95
+ #
96
+ # @example When it's the north player's turn
97
+ # qi = Qi.new(true, ['P', 'B'], ['G', '+B'], {56 => 'P', 64 => '+B'})
98
+ # qi.current_captures #=> ['B', 'P']
99
+ #
100
+ # @example When it's the south player's turn
101
+ # qi = Qi.new(false, ['P', 'B'], ['G', '+B'], {56 => 'P', 64 => '+B'})
102
+ # qi.current_captures #=> ['+B', 'G']
103
+ #
104
+ # @return [Array] the captures of the current player
105
+ def current_captures
106
+ north_turn? ? north_captures : south_captures
58
107
  end
59
108
 
60
- def owned_captures
61
- if north_turn?
62
- north_captures
63
- else
64
- south_captures
65
- end
109
+ # Get the captures of the opponent player.
110
+ #
111
+ # @return [Array] the captures of the opponent player
112
+ def opponent_captures
113
+ north_turn? ? south_captures : north_captures
66
114
  end
67
115
 
68
- def side_name
69
- if north_turn?
70
- "north"
71
- else
72
- "south"
73
- end
116
+ # Get the current turn.
117
+ #
118
+ # @return [Symbol] :North if it's the north player's turn, :South otherwise
119
+ def current_turn
120
+ north_turn? ? North : South
74
121
  end
75
122
 
76
- # Checks if it is the north turn or not.
123
+ # Get the next turn.
77
124
  #
78
- # @return [Boolean] true if it is the north turn and false otherwise
125
+ # @return [Symbol] :South if it's the north player's turn, :North otherwise
126
+ def next_turn
127
+ north_turn? ? South : North
128
+ end
129
+
130
+ # Check if it's the north player's turn.
131
+ #
132
+ # @return [Boolean] true if it's the north player's turn, false otherwise
79
133
  def north_turn?
80
134
  @is_north_turn
81
135
  end
82
136
 
83
- # Checks if it is not the north turn or not.
137
+ # Check if it's the south player's turn.
84
138
  #
85
- # @return [Boolean] true if it is not the north turn and false otherwise
139
+ # @return [Boolean] true if it's the south player's turn, false otherwise
86
140
  def south_turn?
87
141
  !north_turn?
88
142
  end
89
143
 
90
- # Returns an array representation of the Qi object's attributes.
144
+ # Convert the state to an array.
145
+ #
146
+ # @example Converting the state to an array
147
+ # qi.to_a #=> [true, ['P', 'G'], ['B', '+B'], {56 => 'P', 3 => 'g', 64 => '+B'}, {}]
91
148
  #
92
- # @return [Array(Boolean, Array<Object>, Array<Object>, Hash<Object, Object>)] an array containing four elements:
93
- # - a boolean value indicating whose turn it is
94
- # - an array of pieces captured by the north player
95
- # - an array of pieces captured by the south player
96
- # - a hash of squares on the board
149
+ # @return [Array] an array representing the game state
97
150
  def to_a
98
151
  [
99
152
  north_turn?,
100
153
  north_captures,
101
154
  south_captures,
102
- squares
155
+ squares,
156
+ options
103
157
  ]
104
158
  end
105
159
 
106
- # Returns a hash representation of the Qi object's attributes.
160
+ # Convert the state to a hash.
107
161
  #
108
- # @return [Hash{Symbol => Object}] a hash containing four key-value pairs:
109
- # - is_north_turn: a boolean value indicating whose turn it is
110
- # - north_captures: an array of pieces captured by the north player
111
- # - south_captures: an array of pieces captured by the south player
112
- # - squares: a hash of squares on the board
162
+ # @example Converting the state to a hash
163
+ # qi.to_h #=> {is_north_turn: true, north_captures: ['P', 'G'], south_captures: ['B', '+B'], squares: {56 => 'P', 3 => 'g', 64 => '+B'}, options: {}}
164
+ #
165
+ # @return [Hash] a hash representing the game state
113
166
  def to_h
114
167
  {
115
168
  is_north_turn: north_turn?,
116
169
  north_captures:,
117
170
  south_captures:,
118
- squares:
171
+ squares:,
172
+ options:
119
173
  }
120
174
  end
121
175
 
122
- # Returns the hash-code for the position.
176
+ # Generate a hash value for the game state.
177
+ #
178
+ # @example Generating a hash value
179
+ # qi.hash #=> "10dfb778f6559dd368c510a27e3b00bdb5b4ad88d4d67f38864d7e36de7c2f9c"
180
+ #
181
+ # @return [String] a SHA256 hash of the serialized game state
123
182
  def hash
124
183
  ::Digest::SHA256.hexdigest(serialize)
125
184
  end
126
185
 
127
- # Returns a string representation of the Qi object's attributes.
186
+ # Serialize the game state.
128
187
  #
129
- # @return [String] a string containing three parts separated by "===":
130
- # - the current turn, either "NorthTurn" or "SouthTurn"
131
- # - the captures, sorted and joined by ","
132
- # - the squares, mapped to "coordinate:piece" pairs and joined by ","
188
+ # @example Serializing the game state
189
+ # qi.serialize #=> "North-turn===P,G,B,+B===3:g,56:P,64:+B"
190
+ #
191
+ # @return [String] a string representation of the game state
133
192
  def serialize
134
- serialized_turn = "#{side_name}-turn"
135
- serialized_captures = (north_captures + south_captures).sort.join(",")
136
- serialized_squares = squares.keys.map { |i| "#{i}:#{squares.fetch(i)}" }.join(",")
193
+ serialized_turn = "#{current_turn}-turn"
194
+ serialized_captures = (north_captures + south_captures).join(",")
195
+ serialized_squares = squares.keys.sort.map { |i| "#{i}:#{squares.fetch(i)}" }.join(",")
137
196
 
138
197
  "#{serialized_turn}===#{serialized_captures}===#{serialized_squares}"
139
198
  end
140
199
 
141
- # Returns a human-readable representation of the Qi object.
200
+ # Provide a string representation of the game state for debugging.
201
+ #
202
+ # @example Getting a string representation of the game state
203
+ # qi.inspect #=> "<Qi North-turn===P,G,B,+B===3:g,56:P,64:+B>"
142
204
  #
143
- # @return [String] a string containing the class name and the serialized attributes
205
+ # @return [String] a string representation of the game state
144
206
  def inspect
145
207
  "<#{self.class} #{serialize}>"
146
208
  end
147
209
 
148
210
  private
149
211
 
150
- # Updates the captures arrays based on the piece in hand and whether it is dropped or not.
212
+ # Update captures based on the source square and the piece in hand.
151
213
  #
152
- # @param in_hand [Object, nil] the piece that is in hand or nil if none
153
- # @param is_drop [Boolean] a boolean value indicating whether the in hand piece is dropped or not
154
- # @return [Array] an array containing the updated north and south captures arrays
155
- def update_captures(in_hand, is_drop:)
214
+ # If the source square is not nil and in_hand is nil (i.e., we're moving a piece on the board),
215
+ # the captures remain the same. If the source square is not nil and in_hand is not nil
216
+ # (i.e., we're capturing a piece that will remain in hand), the piece is added to the
217
+ # current player's captures. If the source square is nil (i.e., we're dropping a piece from hand),
218
+ # the piece is removed from the current player's captures.
219
+ #
220
+ # @param src_square [Object, nil] the source square, or nil if dropping a piece from hand
221
+ # @param in_hand [String, nil] the piece in hand, or nil if moving a piece from a square
222
+ #
223
+ # @example When moving a piece on the board
224
+ # qi = Qi.new(true, ['P', 'B'], ['G', '+B'], {56 => 'P', 64 => '+B'})
225
+ # qi.send(:update_captures, 56, nil) #=> [['B', 'P'], ['G', '+B']]
226
+ #
227
+ # @example When capturing a piece that will remain in hand
228
+ # qi = Qi.new(true, ['P', 'B'], ['G', '+B'], {56 => 'P', 64 => '+B'})
229
+ # qi.send(:update_captures, 56, 'G') #=> [['B', 'P', 'G'], ['G', '+B']]
230
+ #
231
+ # @example When dropping a piece from hand
232
+ # qi = Qi.new(true, ['P', 'B', 'G'], ['G', '+B'], {56 => 'P', 64 => '+B'})
233
+ # qi.send(:update_captures, nil, 'G') #=> [['B', 'P'], ['G', '+B']]
234
+ #
235
+ # @return [Array] a two-element array with the updated north and south captures
236
+ def update_captures(src_square, in_hand)
156
237
  return [north_captures, south_captures] if in_hand.nil?
157
238
 
158
- captures = if is_drop
159
- remove_from_captures(in_hand, *owned_captures)
239
+ captures = if src_square.nil?
240
+ remove_from_captures(in_hand, *current_captures)
160
241
  else
161
- owned_captures + [in_hand]
242
+ current_captures + [in_hand]
162
243
  end
163
244
 
164
- north_turn? ? [captures, other_captures] : [other_captures, captures]
245
+ north_turn? ? [captures, opponent_captures] : [opponent_captures, captures]
165
246
  end
166
247
 
167
- # Removes the last occurrence of a piece from an array of captures and returns the modified array.
248
+ # Removes a piece from the captures.
249
+ #
250
+ # If the piece is not found in the captures, an Error::Drop exception is raised.
251
+ #
252
+ # @param piece [String] the piece to remove from the captures
253
+ # @param captures [Array] the captures to update
254
+ #
255
+ # @example
256
+ # qi = Qi.new(true, ['P', 'B', 'G'], ['G', '+B'], {56 => 'P', 64 => '+B'})
257
+ # qi.send(:remove_from_captures, 'G', *qi.current_captures) #=> ['P', 'B']
258
+ #
259
+ # @raise [Error::Drop] if the piece is not found in the captures
168
260
  #
169
- # @param piece [Object] the piece to be removed
170
- # @param captures [Array<Object>] the array of captures
171
- # @return [Array<Object>] the modified array of captures
172
- # @raise [Qi::Error::Drop] if the piece is not found in the array
261
+ # @return [Array] the captures after removing the piece
173
262
  def remove_from_captures(piece, *captures)
174
263
  index = captures.rindex(piece)
175
- raise Error::Drop, "There are no #{piece} in hand." if index.nil?
264
+ raise Error::Drop, "There are no #{piece} in hand" if index.nil?
176
265
 
177
266
  captures.delete_at(index)
178
267
  captures
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qi
3
3
  version: !ruby/object:Gem::Version
4
- version: 10.0.0.beta9
4
+ version: 10.0.0.beta11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-06 00:00:00.000000000 Z
11
+ date: 2023-05-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: An abstraction that could help to update positions for games like Shogi.
14
14
  email: contact@cyril.email
@@ -40,7 +40,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
40
40
  - !ruby/object:Gem::Version
41
41
  version: 1.3.1
42
42
  requirements: []
43
- rubygems_version: 3.4.6
43
+ rubygems_version: 3.4.10
44
44
  signing_key:
45
45
  specification_version: 4
46
46
  summary: An abstraction that could help to update positions for games like Shogi.