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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/just_chess.gemspec +29 -0
- data/lib/just_chess.rb +8 -0
- data/lib/just_chess/direction.rb +35 -0
- data/lib/just_chess/errors/causes_check_error.rb +22 -0
- data/lib/just_chess/errors/error.rb +20 -0
- data/lib/just_chess/errors/invalid_move_error.rb +22 -0
- data/lib/just_chess/errors/invalid_promotion_error.rb +22 -0
- data/lib/just_chess/errors/moved_into_check_error.rb +22 -0
- data/lib/just_chess/errors/no_piece_error.rb +22 -0
- data/lib/just_chess/errors/not_players_turn_error.rb +22 -0
- data/lib/just_chess/errors/off_board_error.rb +22 -0
- data/lib/just_chess/game_state.rb +323 -0
- data/lib/just_chess/piece_factory.rb +68 -0
- data/lib/just_chess/pieces/bishop.rb +23 -0
- data/lib/just_chess/pieces/king.rb +97 -0
- data/lib/just_chess/pieces/knight.rb +23 -0
- data/lib/just_chess/pieces/pawn.rb +103 -0
- data/lib/just_chess/pieces/piece.rb +121 -0
- data/lib/just_chess/pieces/queen.rb +23 -0
- data/lib/just_chess/pieces/rook.rb +23 -0
- data/lib/just_chess/point.rb +52 -0
- data/lib/just_chess/square.rb +124 -0
- data/lib/just_chess/square_set.rb +407 -0
- data/lib/just_chess/vector.rb +86 -0
- data/lib/just_chess/version.rb +4 -0
- metadata +120 -0
@@ -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
|