qi 10.0.0.beta9 → 10.0.0.beta11

Sign up to get free protection for your applications and to get access to all the features.
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.