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,22 @@
1
+ require 'just_chess/errors/error'
2
+
3
+ module JustChess
4
+
5
+ # = NotPlayersTurnError
6
+ #
7
+ # A not players turn error with a message
8
+ class NotPlayersTurnError < Error
9
+
10
+ # New not players turn errors can be instantiated with
11
+ #
12
+ # @option [String] message
13
+ # the message to display.
14
+ #
15
+ # ==== Example:
16
+ # # Instantiates a new NotPlayersTurnError
17
+ # JustChess::NotPlayersTurnError.new("Custom Message")
18
+ def initialize(message="It is not the player's turn yet.")
19
+ super
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'just_chess/errors/error'
2
+
3
+ module JustChess
4
+
5
+ # = OffBoardError
6
+ #
7
+ # An off board error with a message
8
+ class OffBoardError < Error
9
+
10
+ # New off board errors can be instantiated with
11
+ #
12
+ # @option [String] message
13
+ # the message to display.
14
+ #
15
+ # ==== Example:
16
+ # # Instantiates a new OffBoardError
17
+ # JustChess::OffBoardError.new("Custom Message")
18
+ def initialize(message="Cannot move off board.")
19
+ super
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,323 @@
1
+ require 'just_chess/errors/no_piece_error'
2
+ require 'just_chess/errors/not_players_turn_error'
3
+ require 'just_chess/errors/invalid_move_error'
4
+ require 'just_chess/errors/invalid_promotion_error'
5
+ require 'just_chess/errors/moved_into_check_error'
6
+ require 'just_chess/square_set'
7
+
8
+ module JustChess
9
+
10
+ # = Game State
11
+ #
12
+ # Represents a game of Chess in progress.
13
+ class GameState
14
+
15
+ # They piece types that a pawn can promote to.
16
+ PROMOTABLE_PIECE_TYPES = ['queen', 'knight', 'bishop', 'rook']
17
+
18
+ def initialize(current_player_number: , squares: [], last_double_step_pawn_id: nil)
19
+ @current_player_number = current_player_number
20
+ @squares = if squares.is_a?(SquareSet)
21
+ squares
22
+ else
23
+ SquareSet.new(squares: squares)
24
+ end
25
+ @last_double_step_pawn_id = last_double_step_pawn_id
26
+ @last_change = {}
27
+ @errors = []
28
+ end
29
+
30
+ attr_reader :current_player_number, :squares, :last_change, :errors, :last_double_step_pawn_id
31
+
32
+ # Instantiates a new GameState object in the starting position
33
+ #
34
+ # @return [GameState]
35
+ def self.default
36
+ new({
37
+ current_player_number: 1,
38
+ squares: [
39
+ { id: 'a8', x: 0, y: 0, piece: { id: 1, player_number: 2, type: 'rook' } },
40
+ { id: 'b8', x: 1, y: 0, piece: { id: 2, player_number: 2, type: 'knight' } },
41
+ { id: 'c8', x: 2, y: 0, piece: { id: 3, player_number: 2, type: 'bishop' } },
42
+ { id: 'd8', x: 3, y: 0, piece: { id: 4, player_number: 2, type: 'queen' } },
43
+ { id: 'e8', x: 4, y: 0, piece: { id: 5, player_number: 2, type: 'king' } },
44
+ { id: 'f8', x: 5, y: 0, piece: { id: 6, player_number: 2, type: 'bishop' } },
45
+ { id: 'g8', x: 6, y: 0, piece: { id: 7, player_number: 2, type: 'knight' } },
46
+ { id: 'h8', x: 7, y: 0, piece: { id: 8, player_number: 2, type: 'rook' } },
47
+
48
+ { id: 'a7', x: 0, y: 1, piece: { id: 9, player_number: 2, type: 'pawn' } },
49
+ { id: 'b7', x: 1, y: 1, piece: { id: 10, player_number: 2, type: 'pawn' } },
50
+ { id: 'c7', x: 2, y: 1, piece: { id: 11, player_number: 2, type: 'pawn' } },
51
+ { id: 'd7', x: 3, y: 1, piece: { id: 12, player_number: 2, type: 'pawn' } },
52
+ { id: 'e7', x: 4, y: 1, piece: { id: 13, player_number: 2, type: 'pawn' } },
53
+ { id: 'f7', x: 5, y: 1, piece: { id: 14, player_number: 2, type: 'pawn' } },
54
+ { id: 'g7', x: 6, y: 1, piece: { id: 15, player_number: 2, type: 'pawn' } },
55
+ { id: 'h7', x: 7, y: 1, piece: { id: 16, player_number: 2, type: 'pawn' } },
56
+
57
+ { id: 'a6', x: 0, y: 2, piece: nil },
58
+ { id: 'b6', x: 1, y: 2, piece: nil },
59
+ { id: 'c6', x: 2, y: 2, piece: nil },
60
+ { id: 'd6', x: 3, y: 2, piece: nil },
61
+ { id: 'e6', x: 4, y: 2, piece: nil },
62
+ { id: 'f6', x: 5, y: 2, piece: nil },
63
+ { id: 'g6', x: 6, y: 2, piece: nil },
64
+ { id: 'h6', x: 7, y: 2, piece: nil },
65
+
66
+ { id: 'a5', x: 0, y: 3, piece: nil },
67
+ { id: 'b5', x: 1, y: 3, piece: nil },
68
+ { id: 'c5', x: 2, y: 3, piece: nil },
69
+ { id: 'd5', x: 3, y: 3, piece: nil },
70
+ { id: 'e5', x: 4, y: 3, piece: nil },
71
+ { id: 'f5', x: 5, y: 3, piece: nil },
72
+ { id: 'g5', x: 6, y: 3, piece: nil },
73
+ { id: 'h5', x: 7, y: 3, piece: nil },
74
+
75
+ { id: 'a4', x: 0, y: 4, piece: nil },
76
+ { id: 'b4', x: 1, y: 4, piece: nil },
77
+ { id: 'c4', x: 2, y: 4, piece: nil },
78
+ { id: 'd4', x: 3, y: 4, piece: nil },
79
+ { id: 'e4', x: 4, y: 4, piece: nil },
80
+ { id: 'f4', x: 5, y: 4, piece: nil },
81
+ { id: 'g4', x: 6, y: 4, piece: nil },
82
+ { id: 'h4', x: 7, y: 4, piece: nil },
83
+
84
+ { id: 'a3', x: 0, y: 5, piece: nil },
85
+ { id: 'b3', x: 1, y: 5, piece: nil },
86
+ { id: 'c3', x: 2, y: 5, piece: nil },
87
+ { id: 'd3', x: 3, y: 5, piece: nil },
88
+ { id: 'e3', x: 4, y: 5, piece: nil },
89
+ { id: 'f3', x: 5, y: 5, piece: nil },
90
+ { id: 'g3', x: 6, y: 5, piece: nil },
91
+ { id: 'h3', x: 7, y: 5, piece: nil },
92
+
93
+ { id: 'a2', x: 0, y: 6, piece: { id: 17, player_number: 1, type: 'pawn' } },
94
+ { id: 'b2', x: 1, y: 6, piece: { id: 18, player_number: 1, type: 'pawn' } },
95
+ { id: 'c2', x: 2, y: 6, piece: { id: 19, player_number: 1, type: 'pawn' } },
96
+ { id: 'd2', x: 3, y: 6, piece: { id: 20, player_number: 1, type: 'pawn' } },
97
+ { id: 'e2', x: 4, y: 6, piece: { id: 21, player_number: 1, type: 'pawn' } },
98
+ { id: 'f2', x: 5, y: 6, piece: { id: 22, player_number: 1, type: 'pawn' } },
99
+ { id: 'g2', x: 6, y: 6, piece: { id: 23, player_number: 1, type: 'pawn' } },
100
+ { id: 'h2', x: 7, y: 6, piece: { id: 24, player_number: 1, type: 'pawn' } },
101
+
102
+ { id: 'a1', x: 0, y: 7, piece: { id: 25, player_number: 1, type: 'rook' } },
103
+ { id: 'b1', x: 1, y: 7, piece: { id: 26, player_number: 1, type: 'knight' } },
104
+ { id: 'c1', x: 2, y: 7, piece: { id: 27, player_number: 1, type: 'bishop' } },
105
+ { id: 'd1', x: 3, y: 7, piece: { id: 28, player_number: 1, type: 'queen' } },
106
+ { id: 'e1', x: 4, y: 7, piece: { id: 29, player_number: 1, type: 'king' } },
107
+ { id: 'f1', x: 5, y: 7, piece: { id: 30, player_number: 1, type: 'bishop' } },
108
+ { id: 'g1', x: 6, y: 7, piece: { id: 31, player_number: 1, type: 'knight' } },
109
+ { id: 'h1', x: 7, y: 7, piece: { id: 32, player_number: 1, type: 'rook' } },
110
+ ]
111
+ })
112
+ end
113
+
114
+ # serializes the game state as a hash
115
+ #
116
+ # @return [Hash]
117
+ def as_json
118
+ {
119
+ current_player_number: current_player_number,
120
+ squares: squares.as_json,
121
+ last_double_step_pawn_id: last_double_step_pawn_id
122
+ }
123
+ end
124
+
125
+ # deep clone of the game state
126
+ #
127
+ # @return [GameState]
128
+ def clone
129
+ self.class.new(as_json)
130
+ end
131
+
132
+ # Moves a piece owned by the player, from one square, to another, with an optional promotion.
133
+ #
134
+ # It moves the piece and returns true if the move is valid and it's the player's turn.
135
+ # It returns false otherwise.
136
+ #
137
+ # ==== Example:
138
+ # # Moves a piece from a square to perform a move
139
+ # game_state.move(1, 'h7', 'h6')
140
+ #
141
+ # @param [Fixnum] player_number
142
+ # the player number, 1 or 2.
143
+ #
144
+ # @param [String] from_id
145
+ # the id of the from square
146
+ #
147
+ # @param [String] to_id
148
+ # the id of the to square
149
+ #
150
+ # @option [String] promote_to
151
+ # the type of the piece that the pawn will promote to
152
+ #
153
+ # @return [Boolean]
154
+ def move(player_number, from_id, to_id, promote_to=nil)
155
+ @errors = []
156
+
157
+ from = squares.find_by_id(from_id)
158
+ to = squares.find_by_id(to_id)
159
+
160
+ if current_player_number != player_number
161
+ @errors.push JustChess::NotPlayersTurnError.new
162
+ elsif from.unoccupied?
163
+ @errors.push JustChess::NoPieceError.new
164
+ elsif !((0..7).include?(to.x) && (0..7).include?(to.y))
165
+ @errors.push JustChess::OffBoardError.new
166
+ elsif !promote_to.nil? && !PROMOTABLE_PIECE_TYPES.include?(promote_to)
167
+ @errors.push JustChess::InvalidPromotion.new
168
+ elsif from.piece.can_move?(from, to, self)
169
+
170
+ duplicate = self.clone
171
+ duplicate.perform_complete_move(player_number, from_id, to_id, promote_to)
172
+
173
+ if duplicate.in_check?(current_player_number)
174
+ @errors.push JustChess::MovedIntoCheckError.new
175
+ else
176
+ perform_complete_move(player_number, from_id, to_id, promote_to)
177
+ end
178
+ else
179
+ @errors.push JustChess::InvalidMoveError.new
180
+ end
181
+ @errors.empty?
182
+ end
183
+
184
+ # The player number of the winner. It returns nil if there is no winner.
185
+ #
186
+ # @return [Fixnum,NilClass]
187
+ def winner
188
+ case
189
+ when in_checkmate?(1)
190
+ 2
191
+ when in_checkmate?(2)
192
+ 1
193
+ else
194
+ nil
195
+ end
196
+ end
197
+
198
+ # Moves a piece owned by the player, from one square, to another, with an optional promotion without validation
199
+ #
200
+ # It handles castling, en passant and promotion.
201
+ # It moves the piece and returns true if the move is valid and it's the player's turn.
202
+ # It returns false otherwise.
203
+ #
204
+ # ==== Example:
205
+ # # Moves a piece from a square to perform a move
206
+ # game_state.move(1, 'h7', 'h6')
207
+ #
208
+ # @param [Fixnum] player_number
209
+ # the player number, 1 or 2.
210
+ #
211
+ # @param [String] from_id
212
+ # the id of the from square
213
+ #
214
+ # @param [String] to_id
215
+ # the id of the to square
216
+ #
217
+ # @option [String] promote_to
218
+ # the type of the piece that the pawn will promote to
219
+ #
220
+ # @return [Boolean]
221
+ def perform_complete_move(player_number, from_id, to_id, promote_to=nil)
222
+ from = squares.find_by_id(from_id)
223
+ to = squares.find_by_id(to_id)
224
+
225
+ captured = captured_square(from, to)
226
+ double_step_pawn = from.piece.is_a?(JustChess::Pawn) && Vector.new(from,to).magnitude == 2
227
+ @last_double_step_pawn_id = double_step_pawn ? from.piece.id : nil
228
+
229
+ @last_change = { type: 'move', data: {player_number: player_number, from: from_id, to: to_id} }
230
+
231
+ rook_castle = rook_castle_move(from, to)
232
+ perform_move(rook_castle.from, rook_castle.to, nil) if rook_castle
233
+
234
+ perform_move(from, to, captured)
235
+
236
+ promote(to, promote_to) if pawn_moved_to_last_rank(to)
237
+ pass_turn
238
+ end
239
+
240
+ def in_check?(player_number)
241
+ king_square = squares.find_king_for_player(player_number)
242
+ threatened_by = squares.threatened_by(opposing_player_number(player_number), self)
243
+ threatened_by.include?(king_square)
244
+ end
245
+
246
+ def in_checkmate?(player_number)
247
+ in_check?(player_number) && king_cannot_move?(player_number)
248
+ end
249
+
250
+
251
+ def king_cannot_move?(player_number)
252
+ king_square = squares.find_king_for_player(player_number)
253
+ threatened_by = squares.threatened_by(opposing_player_number(player_number), self)
254
+ destinations = king_square.piece.destinations(king_square, self)
255
+ remove_threats = destinations - threatened_by
256
+ remove_threats.none?
257
+ end
258
+
259
+ private
260
+
261
+ def captured_square(from, to)
262
+ if to.occupied?
263
+ to
264
+ else
265
+ if from.piece.is_a?(Pawn) && from.piece.en_passant_square(from, self)
266
+ squares.find_by_piece_id(last_double_step_pawn_id)
267
+ else
268
+ nil
269
+ end
270
+ end
271
+ end
272
+
273
+ def rook_castle_move(from, to)
274
+ if from.occupied? && from.piece.is_a?(King) && from.piece.castle(from, self).include?(to)
275
+ vector = Vector.new(from, to)
276
+
277
+ rook_from_x = vector.direction.x > 0 ? 7 : 0
278
+ rook_from_y = from.y
279
+ rook_from = squares.find_by_x_and_y(rook_from_x, rook_from_y)
280
+
281
+ rook_to_x = vector.direction.x > 0 ? (from.x + 1) : (from.x -1)
282
+ rook_to_y = from.y
283
+ rook_to = squares.find_by_x_and_y(rook_to_x, rook_to_y)
284
+
285
+ Struct.new(:from, :to).new(rook_from, rook_to)
286
+ else
287
+ nil
288
+ end
289
+ end
290
+
291
+ def pawn_moved_to_last_rank(square)
292
+ square.piece && square.piece.is_a?(Pawn) && square.last_rank(square.piece.player_number)
293
+ end
294
+
295
+ def pass_turn
296
+ @current_player_number = opposing_player_number
297
+ end
298
+
299
+ def opposing_player_number(player_number=nil)
300
+ if player_number
301
+ other_player_number(player_number)
302
+ else
303
+ other_player_number(@current_player_number)
304
+ end
305
+ end
306
+
307
+ def other_player_number(player_number)
308
+ (player_number == 1) ? 2 : 1
309
+ end
310
+
311
+ def perform_move(from, to, captured)
312
+ captured.piece = nil if captured
313
+ to.piece = from.piece
314
+ from.piece = nil
315
+ to.piece.moved
316
+ end
317
+
318
+ def promote(square, promote_to)
319
+ new_piece = PieceFactory.new(type: promote_to, id: square.piece.id, player_number: square.piece.player_number).build
320
+ square.piece = new_piece
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,68 @@
1
+ require 'just_chess/pieces/pawn'
2
+ require 'just_chess/pieces/rook'
3
+ require 'just_chess/pieces/knight'
4
+ require 'just_chess/pieces/bishop'
5
+ require 'just_chess/pieces/queen'
6
+ require 'just_chess/pieces/king'
7
+
8
+ module JustChess
9
+
10
+ # = Piece Factory
11
+ #
12
+ # Generates pieces from a hash of arguments
13
+ class PieceFactory
14
+
15
+ # A mapping of type descriptions to classes
16
+ CLASSES = {
17
+ 'pawn' => Pawn,
18
+ 'rook' => Rook,
19
+ 'knight' => Knight,
20
+ 'bishop' => Bishop,
21
+ 'queen' => Queen,
22
+ 'king' => King
23
+ }
24
+
25
+ # New objects can be instantiated by passing in a hash or the piece.
26
+ #
27
+ # @param [Hash,Piece] args
28
+ # the initial attributes of the piece, hash requires type key
29
+ #
30
+ # ==== Example:
31
+ # # Instantiates a new PieceFactory
32
+ # JustChess::PieceFactory.new({
33
+ # type: 'pawn',
34
+ # id: 1,
35
+ # player_number: 2
36
+ # })
37
+ def initialize(args)
38
+ @args = args
39
+ end
40
+
41
+ # Returns a piece based on the initial arguments.
42
+ #
43
+ # @return [Piece]
44
+ def build
45
+ case @args
46
+ when Hash
47
+ build_from_hash
48
+ when Piece
49
+ @args
50
+ when nil
51
+ nil
52
+ else
53
+ raise ArgumentError, "piece must be Hash or NilClass"
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def build_from_hash
60
+ klass = CLASSES[@args[:type]]
61
+ if klass
62
+ klass.new(@args)
63
+ else
64
+ raise ArgumentError, "invalid piece type: #{@args[:type].to_s}"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,23 @@
1
+ require 'just_chess/pieces/piece'
2
+
3
+ module JustChess
4
+
5
+ # = Bishop
6
+ #
7
+ # The piece that moves diagonally any number of squares
8
+ class Bishop < Piece
9
+
10
+ # All the squares that the piece can move to and/or capture.
11
+ #
12
+ # @param [Square] square
13
+ # the origin square.
14
+ #
15
+ # @param [GameState] game_state
16
+ # the current game state.
17
+ #
18
+ # @return [SquareSet]
19
+ def destinations(square, game_state)
20
+ game_state.squares.diagonal(square).unoccupied_or_occupied_by_opponent(player_number).unblocked(square, game_state.squares)
21
+ end
22
+ end
23
+ end