just_chess 0.1.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.
@@ -0,0 +1,124 @@
1
+ require 'just_chess/piece_factory'
2
+ require 'just_chess/point'
3
+
4
+ module JustChess
5
+
6
+ # = Square
7
+ #
8
+ # A Square on a checker board
9
+ class Square
10
+
11
+ # New objects can be instantiated by passing in a hash with
12
+ #
13
+ # @param [String] id
14
+ # the unique identifier of the square.
15
+ #
16
+ # @param [Fixnum] x
17
+ # the x co-ordinate of the square.
18
+ #
19
+ # @param [Fixnum] y
20
+ # the y co-ordinate of the square.
21
+ #
22
+ # @option [Piece,Hash,NilClass] piece
23
+ # The piece on the square, can be a piece object or hash or nil.
24
+ #
25
+ # ==== Example:
26
+ # # Instantiates a new Square
27
+ # JustChess::Square.new({
28
+ # id: 'a1',
29
+ # x: 1,
30
+ # y: 0,
31
+ # piece: { player_number: 1, direction: 1, king: false }
32
+ # })
33
+ def initialize(id: , x: , y: , piece: nil)
34
+ @id = id
35
+ @x = x
36
+ @y = y
37
+ @piece = PieceFactory.new(piece).build
38
+ end
39
+
40
+ # @return [String] the unique identifier of the square.
41
+ attr_reader :id
42
+
43
+ # @return [Fixnum] the x co-ordinate of the square.
44
+ attr_reader :x
45
+
46
+ # @return [Fixnum] the y co-ordinate of the square.
47
+ attr_reader :y
48
+
49
+ # @return [Piece,NilClass] The piece on the square if any.
50
+ attr_accessor :piece
51
+
52
+ # Is the square occupied by a piece?
53
+ #
54
+ # @return [Boolean]
55
+ def occupied?
56
+ !!piece
57
+ end
58
+
59
+ # Is the square unoccupied by a piece?
60
+ #
61
+ # @return [Boolean]
62
+ def unoccupied?
63
+ !piece
64
+ end
65
+
66
+ # Is the square occupied by the specified player?
67
+ #
68
+ # @return [Boolean]
69
+ def occupied_by_player?(player_number)
70
+ piece && piece.player_number == player_number
71
+ end
72
+
73
+ # Is the square occupied by the opponent of the specified player?
74
+ #
75
+ # @return [Boolean]
76
+ def occupied_by_opponent?(player_number)
77
+ piece && piece.player_number != player_number
78
+ end
79
+
80
+ # returns the rank number of the square for the specified player
81
+ #
82
+ # @return [Fixnum]
83
+ def rank_number(player_number)
84
+ if player_number == 1
85
+ 8 - @y
86
+ else
87
+ @y + 1
88
+ end
89
+ end
90
+
91
+ # Is the square the last rank for the specified player?
92
+ #
93
+ # @return [Boolean]
94
+ def last_rank(player_number)
95
+ rank_number(player_number) == 8
96
+ end
97
+
98
+ # Is the square the same as another one?
99
+ #
100
+ # @return [Boolean]
101
+ def ==(other)
102
+ self.id == other.id
103
+ end
104
+
105
+ # A point object with the square's co-ordinates.
106
+ #
107
+ # @return [Point]
108
+ def point
109
+ Point.new(x, y)
110
+ end
111
+
112
+ # A serialized version of the square as a hash
113
+ #
114
+ # @return [Hash]
115
+ def as_json
116
+ {
117
+ id: id,
118
+ x: x,
119
+ y: y,
120
+ piece: piece && piece.as_json
121
+ }
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,407 @@
1
+ require 'forwardable'
2
+ require 'just_chess/square'
3
+
4
+ module JustChess
5
+
6
+ # = Square Set
7
+ #
8
+ # A collection of Squares with useful filtering functions
9
+ class SquareSet
10
+ extend Forwardable
11
+
12
+ # New objects can be instantiated by passing in a hash with squares.
13
+ # They can be square objects or hashes.
14
+ #
15
+ # @param [Array<Square,Hash>] squares
16
+ # An array of squares, each with x and y co-ordinates and a piece.
17
+ #
18
+ # ==== Example:
19
+ # # Instantiates a new Square Set
20
+ # JustChess::SquareSet.new({
21
+ # squares: [
22
+ # { x: 1, y: 0, piece: { player_number: 1, direction: 1, king: false }}
23
+ # ]
24
+ # })
25
+ def initialize(squares: )
26
+ @squares = if squares.all? { |s| s.instance_of?(Hash) }
27
+ squares.map { |s| Square.new(s) }
28
+ elsif squares.all? { |s| s.instance_of?(Square) }
29
+ squares
30
+ else
31
+ raise ArgumentError, "all squares must have the same class"
32
+ end
33
+ end
34
+
35
+ # @return [Array<Square>] The squares in the set.
36
+ attr_reader :squares
37
+
38
+ def_delegator :squares, :find
39
+ def_delegator :squares, :size
40
+ def_delegator :squares, :any?
41
+ def_delegator :squares, :all?
42
+ def_delegator :squares, :none?
43
+ def_delegator :squares, :include?
44
+ def_delegator :squares, :map
45
+ def_delegator :squares, :empty?
46
+
47
+ # Concat two SquareSets together
48
+ #
49
+ # @param [SquareSet] other
50
+ # the second SquareSet
51
+ #
52
+ # @return [SquareSet]
53
+ # ==== Example:
54
+ # # Concat two SquareSets together
55
+ # square_set + other
56
+ def +(other)
57
+ _squares = squares + other.squares
58
+
59
+ self.class.new(squares: _squares)
60
+ end
61
+
62
+ # Remove squares from one SquareSet from another
63
+ #
64
+ # @param [SquareSet] other
65
+ # the second SquareSet
66
+ #
67
+ # @return [SquareSet]
68
+ # ==== Example:
69
+ # # Remove squares from one SquareSet
70
+ # square_set - other
71
+ def -(other)
72
+ _squares = squares - other.squares
73
+
74
+ self.class.new(squares: _squares)
75
+ end
76
+
77
+ # Push a Square onto a SquareSet
78
+ #
79
+ # @param [Square] square
80
+ # the square being pushed on
81
+ #
82
+ # @return [SquareSet]
83
+ # ==== Example:
84
+ # # Push a Square onto a SquareSet
85
+ # square_set << square
86
+ def <<(square)
87
+ _squares = squares << square
88
+
89
+ self.class.new(squares: _squares)
90
+ end
91
+
92
+ # Find the intersection of Squares between sets
93
+ #
94
+ # @param [SquareSet] other
95
+ # the second SquareSet
96
+ #
97
+ # @return [SquareSet]
98
+ # ==== Example:
99
+ # # Find the intersection of Squares
100
+ # square_set & other
101
+ def &(other)
102
+ select { |square| other.include?(square) }
103
+ end
104
+
105
+ # Filter the squares with a block and behaves like Enumerable#select.
106
+ # It returns a SquareSet with the filtered squares.
107
+ #
108
+ # @return [SquareSet]
109
+ def select(&block)
110
+ _squares = squares.select(&block)
111
+
112
+ self.class.new(squares: _squares)
113
+ end
114
+
115
+ # Find the square with the matching unique identifier
116
+ #
117
+ # @param [Fixnum] id
118
+ # the unique identifier.
119
+ #
120
+ # @return [Square]
121
+ # ==== Example:
122
+ # # Find the square with id 4
123
+ # square_set.find_by_id(4)
124
+ def find_by_id(id)
125
+ find { |s| s.id == id }
126
+ end
127
+
128
+ # Find the square with the matching x and y co-ordinates
129
+ #
130
+ # @param [Fixnum] x
131
+ # the x co-ordinate.
132
+ #
133
+ # @param [Fixnum] y
134
+ # the y co-ordinate.
135
+ #
136
+ # @return [Square]
137
+ # ==== Example:
138
+ # # Find the square at 4,2
139
+ # square_set.find_by_x_and_y(4, 2)
140
+ def find_by_x_and_y(x, y)
141
+ find { |s| s.x == x && s.y == y }
142
+ end
143
+
144
+ # Find the square with the matching piece identifier
145
+ #
146
+ # @param [Fixnum] piece_id
147
+ # the unique identifier of the piece.
148
+ #
149
+ # @return [Square]
150
+ # ==== Example:
151
+ # # Find the square with a piece of id 4
152
+ # square_set.find_by_piece_id(4)
153
+ def find_by_piece_id(piece_id)
154
+ find { |s| s.piece && s.piece.id == piece_id }
155
+ end
156
+
157
+ # Find the square occupied by the player's king
158
+ #
159
+ # @param [Fixnum] player_number
160
+ # the number of the player
161
+ #
162
+ # @return [Square]
163
+ # ==== Example:
164
+ # # Find the square occupied by player 2's king
165
+ # square_set.find_king_for_player(2)
166
+ def find_king_for_player(player_number)
167
+ find { |s| s.piece && s.piece.is_a?(JustChess::King) && s.occupied_by_player?(player_number) }
168
+ end
169
+
170
+ # Returns squares between a and b.
171
+ # Only squares that are in the same diagonal will return squares.
172
+ #
173
+ # @param [Square] a
174
+ # a square.
175
+ #
176
+ # @param [Square] b
177
+ # another square.
178
+ #
179
+ # @return [SquareSet]
180
+ #
181
+ # ==== Example:
182
+ # # Get all squares between square_a and square_b
183
+ # square_set.between(square_a, square_b)
184
+ def between(a, b)
185
+ vector = Vector.new(a, b)
186
+
187
+ if vector.diagonal? || vector.orthogonal?
188
+ point_counter = a.point
189
+ direction = vector.direction
190
+ _squares = []
191
+
192
+ while point_counter != b.point
193
+ point_counter = point_counter + direction
194
+ square = find_by_x_and_y(point_counter.x, point_counter.y)
195
+ if square && square.point != b.point
196
+ _squares.push(square)
197
+ end
198
+ end
199
+ else
200
+ _squares = []
201
+ end
202
+
203
+ self.class.new(squares: _squares)
204
+ end
205
+
206
+ # Find all squares within distance of square
207
+ #
208
+ # @param [Square] square
209
+ # the originating square
210
+ #
211
+ # @param [Fixnum] distance
212
+ # the specified distance from the square
213
+ #
214
+ # @return [SquareSet]
215
+ # ==== Example:
216
+ # # Get all squares within 2 squares of square_a
217
+ # square_set.in_range(square_a, 2)
218
+ def in_range(square, distance)
219
+ select { |s| Vector.new(square, s).magnitude.abs <= distance }
220
+ end
221
+
222
+ # Find all squares at distance of square
223
+ #
224
+ # @param [Square] square
225
+ # the originating square
226
+ #
227
+ # @param [Fixnum] distance
228
+ # the specified distance from the square
229
+ #
230
+ # @return [SquareSet]
231
+ # ==== Example:
232
+ # # Get all squares at 2 squares from square_a
233
+ # square_set.at_range(square_a, 2)
234
+ def at_range(square, distance)
235
+ select { |s| Vector.new(square, s).magnitude.abs == distance }
236
+ end
237
+
238
+ # Find all squares in the y direction of square
239
+ #
240
+ # @param [Square] square
241
+ # the originating square
242
+ #
243
+ # @param [Fixnum] direction_y
244
+ # the direction, either up (-1) or down (1)
245
+ #
246
+ # @return [SquareSet]
247
+ # ==== Example:
248
+ # # Get all squares up from square_a
249
+ # square_set.in_direction(square_a, -1)
250
+ def in_direction(square, direction_y)
251
+ select { |s| Vector.new(square, s).direction.y == direction_y }
252
+ end
253
+
254
+ # Find all squares orthogonal from square
255
+ #
256
+ # @param [Square] square
257
+ # the originating square
258
+ #
259
+ # @return [SquareSet]
260
+ # ==== Example:
261
+ # # Get all squares orthogonal from square_a
262
+ # square_set.orthogonal(square_a)
263
+ def orthogonal(square)
264
+ select { |s| Vector.new(square, s).orthogonal? }
265
+ end
266
+
267
+ # Find all squares diagonal from square
268
+ #
269
+ # @param [Square] square
270
+ # the originating square
271
+ #
272
+ # @return [SquareSet]
273
+ # ==== Example:
274
+ # # Get all squares diagonal from square_a
275
+ # square_set.diagonal(square_a)
276
+ def diagonal(square)
277
+ select { |s| Vector.new(square, s).diagonal? }
278
+ end
279
+
280
+ # Find all squares orthogonal or diagonal from square
281
+ #
282
+ # @param [Square] square
283
+ # the originating square
284
+ #
285
+ # @return [SquareSet]
286
+ # ==== Example:
287
+ # # Get all squares orthogonal or diagonal from square_a
288
+ # square_set.orthogonal_or_diagonal(square_a)
289
+ def orthogonal_or_diagonal(square)
290
+ select { |s| Vector.new(square, s).orthogonal_or_diagonal? }
291
+ end
292
+
293
+ # Find all squares not orthogonal or diagonal from square
294
+ #
295
+ # @param [Square] square
296
+ # the originating square
297
+ #
298
+ # @return [SquareSet]
299
+ # ==== Example:
300
+ # # Get all squares not orthogonal or diagonal from square_a
301
+ # square_set.not_orthogonal_or_diagonal(square_a)
302
+ def not_orthogonal_or_diagonal(square)
303
+ select { |s| Vector.new(square, s).not_orthogonal_or_diagonal? }
304
+ end
305
+
306
+ # Find all squares without pieces on them.
307
+ #
308
+ # @return [SquareSet]
309
+ def unoccupied
310
+ select { |s| s.unoccupied? }
311
+ end
312
+
313
+ # Takes a player number and returns all squares occupied by the player
314
+ #
315
+ # @param [Fixnum] player_number
316
+ # the player's number.
317
+ #
318
+ # @return [SquareSet]
319
+ def occupied_by_player(player_number)
320
+ select { |s| s.occupied_by_player?(player_number) }
321
+ end
322
+
323
+ # Takes a player number and returns all squares occupied by the opponent of the player
324
+ #
325
+ # @param [Fixnum] player_number
326
+ # the player's number.
327
+ #
328
+ # @return [SquareSet]
329
+ def occupied_by_opponent(player_number)
330
+ select { |s| s.occupied_by_opponent?(player_number) }
331
+ end
332
+
333
+ # Takes a player number and returns all squares unoccupied or occupied by the opponent of the player
334
+ #
335
+ # @param [Fixnum] player_number
336
+ # the player's number.
337
+ #
338
+ # @return [SquareSet]
339
+ def unoccupied_or_occupied_by_opponent(player_number)
340
+ select { |s| s.unoccupied? || s.occupied_by_opponent?(player_number) }
341
+ end
342
+
343
+ # Find all squares occupied by a piece of a particular type
344
+ #
345
+ # @param [Class] piece_type
346
+ # the class of the piece.
347
+ #
348
+ # @return [SquareSet]
349
+ def occupied_by_piece(piece_type)
350
+ select { |s| s.piece && s.piece.is_a?(piece_type) }
351
+ end
352
+
353
+ # Find all squares occupied by a piece not of a particular type
354
+ #
355
+ # @param [Class] piece_type
356
+ # the class of the piece.
357
+ #
358
+ # @return [SquareSet]
359
+ def excluding_piece(piece_type)
360
+ select { |s| s.piece && !s.piece.is_a?(piece_type) }
361
+ end
362
+
363
+ # Returns destination from the origin that have a clear path
364
+ #
365
+ # @param [Square] origin
366
+ # the originating square.
367
+ #
368
+ # @param [SquareSet] square_set
369
+ # the board position.
370
+ #
371
+ # @return [SquareSet]
372
+ def unblocked(origin, square_set)
373
+ select { |destination| square_set.between(origin, destination).all?(&:unoccupied?) }
374
+ end
375
+
376
+ # Returns all squares with pieces that haven't moved
377
+ #
378
+ # @return [SquareSet]
379
+ def unmoved
380
+ select { |s| s.piece && s.piece.has_not_moved? }
381
+ end
382
+
383
+ # Returns all squares threatened by the specified player
384
+ #
385
+ # @param [Fixnum] player_number
386
+ # the player's number.
387
+ #
388
+ # @param [GameState] game_state
389
+ # the current game state.
390
+ #
391
+ # @return [SquareSet]
392
+ def threatened_by(player_number, game_state)
393
+ _squares = occupied_by_player(player_number).map do |s|
394
+ s.piece.capture_squares(s, game_state).squares
395
+ end.flatten.uniq
396
+
397
+ self.class.new(squares: _squares)
398
+ end
399
+
400
+ # serializes the squares as a hash
401
+ #
402
+ # @return [Hash]
403
+ def as_json
404
+ squares.map(&:as_json)
405
+ end
406
+ end
407
+ end