Journey through TOP - Part 2 - Ruby
My journey through The Odin Project goes on, and I have just finished the second part focusing on the Ruby programming language.
This part has been a great challenge. I struggled many times, but at the end I learned a lot and I feel much more confident.
Ruby Ruby Ruby Rubyyy
It begins with some simple programs to write, as a way to familiarize yourself with Ruby. You even have to rewrite some existing methods from the Enumerable class. And it’s a great way to learn how Ruby works in the inside. Then serious stuff begin. OOP, serialization, API interaction, recursion, data structures and TDD. But you never feel lost. The curriculum is really well made, you tackle all these subjects one at a time and going through each problem is an incredible source of motivation for braving the next one!
It took me 2 months to achieve this part, and it wasn’t wasting time.
Let’s talk about the final ruby project I realized, a command line chess game.
Chess Game
In this final project, I had to create a player vs player chess game that you can play on the command line. You can see all the functionalities implemented in the following program that launches the chess game.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Chess
#
# Player vs Player
# Script to launch a new game
#
# Functionalities implemented:
#
# - Prints "Check!" when a king is in check
# The player in check has no choice but to move his king on a safe case, to move a
# piece between his king and the adverse piece, or to take this adverse piece.
#
# - Prints "Checmate!" when a king is in checkmate and exit the game
# Happens when the player's king is in check and has no solution to avoid the same
# situation the following turn.
#
# - Prints "Draw!" if the game finishes in a draw
# When only the kings remain on the board.
#
# - Prints "Slatemate!" if the game finishes with a slatemate
# When the player has got only his king remaining on the board, and can't move it
# without putting it in check.
#
# - Pawn can double case when at initial location
#
# - Pawn can be changed in another piece when reaches opposite side
# Piece selected by player.
#
# - Castling can be made
# If the player's king is not in check and can't be on the way to castle. And if the
# king and the rook concerned have not moved since beginning of game.
#
# - Game can be saved at beginning of every turn. If so, exit the game. Player chooses to
# load a game or create a new one at beginning of game.
require './lib/game.rb'
game = Game.new
game.start
Basically, all the usual functionalities of a chess game have been implemented!
I’m a chess player, so I wanted to be able to play chess with the true rules. I would have found it really annoying if I couldn’t castle because it’s part of the strategy of the game. Now, let’s have a look at the design.
OOP, check!
Before doing this chess game, I already had to create some command line games, like a Tic Tac Toe or a Connect Four. So I knew how I wanted to design my chess game.
This one would be divided in four classes:
Player class
1
2
3
4
5
6
7
8
9
# Class for the players. Their name and which color they play.
class Player
attr_reader :name, :color
def initialize (name, color)
@name = name
@color = color
end
end
The Player class is really basic. Its purpose is to store the player’s name and the color he plays, white or black.
Board class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# Class for the board. Allows to display the chess board and to get and set squares.
class Board
attr_reader :grid
def initialize(grid = nil)
@grid = grid || default_grid
end
# Fill the board with all the pieces in their intitial location
def default_grid
array = Array.new(8) { Array.new(8) }
array[0][0] = Rook.new('white', [0,0], 'slide')
array[1][0] = Knight.new('white', [1,0], 'step')
array[2][0] = Bishop.new('white', [2,0], 'slide')
array[3][0] = Queen.new('white', [3,0], 'slide')
array[4][0] = King.new('white', [4,0], 'step')
array[5][0] = Bishop.new('white', [5,0], 'slide')
array[6][0] = Knight.new('white', [6,0], 'step')
array[7][0] = Rook.new('white', [7,0], 'slide')
array[0..7].each_with_index { |column, index|
column[1] = Pawn.new('white', [index,1], 'step') }
array[0][7] = Rook.new('black', [0,7], 'slide')
array[1][7] = Knight.new('black', [1,7], 'step')
array[2][7] = Bishop.new('black', [2,7], 'slide')
array[3][7] = Queen.new('black', [3,7], 'slide')
array[4][7] = King.new('black', [4,7], 'step')
array[5][7] = Bishop.new('black', [5,7], 'slide')
array[6][7] = Knight.new('black', [6,7], 'step')
array[7][7] = Rook.new('black', [7,7], 'slide')
array[0..7].each_with_index { |column, index|
column[6] = Pawn.new('black', [index,6], 'step') }
array
end
# Display the board
def display
puts
puts " |---------------------------------------|"
8.downto(1) do |row|
print " #{row} | "
8.times do |col|
print grid[col][row-1] ? grid[col][row-1].unicode.encode('utf-8') : ' '
print ' | '
end
puts
puts " |----|----|----|----|----|----|----|----|"
end
puts " A B C D E F G H"
puts
end
# Return the piece selected
def get_case(case_selected)
grid[case_selected[0]][case_selected[1]]
end
# Fill the case given by the piece given
def set_case(case_selected, piece)
grid[case_selected[0]][case_selected[1]] = piece
unless piece.nil?
piece.location = case_selected
end
end
end
The Board class aim is to create a chess board and be able to display it. the get_case
and set_case
methods allow to read and update a case from a Board object.
Pieces class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# Parent class of all pieces.
class Pieces
attr_reader :unicode, :color, :type
attr_accessor :location, :path, :counter
def initialize (color, location, type, counter = 0)
@color = color
@location = location
@type = type
@counter = counter
end
end
class King < Pieces
def initialize (color, location, type, counter = 0)
super
@color == 'white' ? @unicode = "\u2654" : @unicode = "\u265A"
end
def possible_moves
moves = [[-1, 0], [-1, 1], [ 0, 1], [ 1, 1],
[ 1, 0], [ 1,-1], [ 0,-1], [-1,-1],
[-2, 0], [ 2, 0]]
moves
end
end
class Queen < Pieces
def initialize (color, location, type, counter = 0)
super
@color == 'white' ? @unicode = "\u2655" : @unicode = "\u265B"
@path = []
end
def possible_moves
moves = [[-1, 0], [-1, 1], [ 0, 1], [ 1, 1],
[ 1, 0], [ 1,-1], [ 0,-1], [-1,-1]]
moves
end
end
class Rook < Pieces
def initialize (color, location, type, counter = 0)
super
@color == 'white' ? @unicode = "\u2656" : @unicode = "\u265C"
@path = []
end
def possible_moves
moves = [[-1, 0], [ 0, 1], [ 1, 0], [ 0,-1]]
moves
end
end
class Bishop < Pieces
def initialize (color, location, type, counter = 0)
super
@color == 'white' ? @unicode = "\u2657" : @unicode = "\u265D"
@path = []
end
def possible_moves
moves = [[-1, 1], [ 1, 1], [ 1,-1], [-1,-1]]
moves
end
end
class Knight < Pieces
def initialize (color, location, type, counter = 0)
super
@color == 'white' ? @unicode = "\u2658" : @unicode = "\u265E"
end
def possible_moves
moves = [[-2,-1], [-2, 1], [-1, 2], [ 1, 2],
[ 2, 1], [ 2,-1], [-1,-2], [ 1,-2]]
moves
end
end
class Pawn < Pieces
def initialize (color, location, type, counter = 0)
super
@color == 'white' ? @unicode = "\u2659" : @unicode = "\u265F"
end
def possible_moves
if color == 'white'
moves = [[-1, 1], [ 0, 1], [ 1, 1], [ 0, 2]]
else
moves = [[ 1,-1], [ 0,-1], [-1,-1], [ 0,-2]]
end
moves
end
end
The Pieces class is a parent class for all the chess pieces. I initialized the attributes and some instances in it, avoiding repeatition in the following nested classes (DRY, very important!).
In the pieces classes are stored the unicodes, and a method that returns the way the pieces move on a board.
Game class
The heart of the game. Let’s see it in more details.
Game class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
require_relative 'board.rb'
require_relative 'pieces.rb'
require_relative 'player.rb'
require 'yaml'
# Main class with all the logic of the chess
class Game
attr_accessor :current_player, :other_player, :board
def initialize
@board = Board.new
end
# Launch chess game.
# Exit only if one player won, if there is a draw or if there is stalemate.
def start
welcome_players
board.display
until victory? || draw? || stalemate?
next_turn
end
end
##################
# WELCOME PLAYERS
##################
# Possibility to load a saved game
# Otherwise Ask players names and which color they want to play
def welcome_players
puts
puts "Welcome to this chess game!"
puts "Load a game? 'y' to load:"
Dir.mkdir("save") unless Dir.exists? "save"
choice = gets.chomp.downcase
if choice.include? "y"
load_game
return
end
puts "Give me the name of the player who wants to be White:"
@current_player = Player.new(gets.chomp.downcase, 'white')
puts "Give me the name of the player who wants to be Black:"
@other_player = Player.new(gets.chomp.downcase, 'black')
puts "Ok let's start!"
end
############
# NEXT TURN
############
# Play next player's turn:
# The player has to select the case from which he wants to move the piece.
# Then, he has to select the case where he wants to move the piece.
# If the move is authorized, the piece is moved, the board displayed
# and the other player becomes the current one.
# 'C' allows the player to start again his turn.
def next_turn
puts
puts "#{current_player.name.capitalize}, your turn. Select a piece or 'save' to save:"
case_from = select_case
save_game if case_from == 'save'
return if case_from == 'C' || case_not_valid?(case_from, 'from')
puts "Where do you want to move it? 'C' to select another piece."
case_to = select_case
return if case_to == 'C' || case_not_valid?(case_to, 'to') || case_to == 'save'
answer = move_possible(case_from, case_to)
if !answer[0]
puts answer[1]
return
end
move_piece(case_from, case_to) if board.get_case(case_from) # If castling hasn't been done
board.get_case(case_to).counter += 1 # Memorize number of times a piece moved
board.display
change_players
end
# Change order of players
def change_players
temp = self.current_player
self.current_player = self.other_player
self.other_player = temp
end
# Return the selected case by the current player in an array
# ex: e4 => [4,3]
def select_case
selection = mapping(gets.chomp.downcase)
unless selection
puts "Sorry wrong input. Try again: (ex: e4)"
selection = select_case
end
return selection
end
# Map the selected case to the actual board case
def mapping(input)
mapping = {
'a1'=>[0,0], 'a2'=>[0,1], 'a3'=>[0,2], 'a4'=>[0,3], 'a5'=>[0,4], 'a6'=>[0,5],
'a7'=>[0,6], 'a8'=>[0,7],
'b1'=>[1,0], 'b2'=>[1,1], 'b3'=>[1,2], 'b4'=>[1,3], 'b5'=>[1,4], 'b6'=>[1,5],
'b7'=>[1,6], 'b8'=>[1,7],
'c1'=>[2,0], 'c2'=>[2,1], 'c3'=>[2,2], 'c4'=>[2,3], 'c5'=>[2,4], 'c6'=>[2,5],
'c7'=>[2,6], 'c8'=>[2,7],
'd1'=>[3,0], 'd2'=>[3,1], 'd3'=>[3,2], 'd4'=>[3,3], 'd5'=>[3,4], 'd6'=>[3,5],
'd7'=>[3,6], 'd8'=>[3,7],
'e1'=>[4,0], 'e2'=>[4,1], 'e3'=>[4,2], 'e4'=>[4,3], 'e5'=>[4,4], 'e6'=>[4,5],
'e7'=>[4,6], 'e8'=>[4,7],
'f1'=>[5,0], 'f2'=>[5,1], 'f3'=>[5,2], 'f4'=>[5,3], 'f5'=>[5,4], 'f6'=>[5,5],
'f7'=>[5,6], 'f8'=>[5,7],
'g1'=>[6,0], 'g2'=>[6,1], 'g3'=>[6,2], 'g4'=>[6,3], 'g5'=>[6,4], 'g6'=>[6,5],
'g7'=>[6,6], 'g8'=>[6,7],
'h1'=>[7,0], 'h2'=>[7,1], 'h3'=>[7,2], 'h4'=>[7,3], 'h5'=>[7,4], 'h6'=>[7,5],
'h7'=>[7,6], 'h8'=>[7,7],
'c' => 'C', 'save' => 'save'
}
mapping[input]
end
# Return true if the player is not allowed to play the selected case
def case_not_valid?(case_selected, input)
piece = board.get_case(case_selected)
# case_from
if input == 'from'
if piece.nil?
puts "There is no piece on this case. Start again."
return true
elsif piece.color != current_player.color
puts "This is not your piece! Start again."
return true
end
# case_to
elsif input == 'to'
if piece.nil?
return false
elsif piece.color == current_player.color
puts "This is your piece! Start again."
return true
end
end
return false
end
# Return an array of 2 elements:
# - a boolean, true if the piece can move from and to the selected case
# - a string describing the error if piece can't move
def move_possible(case_from, case_to)
piece = board.get_case(case_from)
move = [case_to[0] - case_from[0], case_to[1] - case_from[1]]
# Check if castling
if piece.is_a?(King)
if move == [-2, 0] || move == [2, 0]
if can_castle?(piece, move)
answer = [true]
else
answer = [false, "Can't castle sorry."]
end
return answer
end
end
# Check if move is coherent with piece's way of displacement
if piece.type == 'step' && !piece.possible_moves.include?(move)
answer = [false, "This piece can't move like that! Start again."]
return answer
end
# Check if obstruction on path for sliding pieces
if piece.type == 'slide' && obstruction?(piece, case_from, case_to)
answer = [false, "Obstruction in the path. Start again."]
return answer
end
# Check if a pawn can go in diag and can move up to 2 cases
if piece.is_a?(Pawn)
answer = [false, "Pawn can't move like that."]
if move[0] != 0
return answer if pawn_cant_diag(piece, case_to)
elsif move[1] == 2 || move[1] == -2
return answer if pawn_cant_double(piece, case_from)
end
end
# Temporary move for the next tests
board.set_case(case_from, nil)
temp_piece = board.get_case(case_to)
board.set_case(case_to, piece)
answer = [true]
# Check if kings stand one case apart
if kings_too_close?
answer = [false, "Kings can't be that close. Start again."]
end
# Check if king of current's player would be in check
if king_check?(find_king(current_player))
answer = [false, "If you do that, your king will be in check! Start again."]
end
# Come back to previous state before temporary move
board.set_case(case_from, piece)
board.set_case(case_to, temp_piece)
return answer
end
# Move the piece to the selected case
def move_piece(case_from, case_to)
piece = board.get_case(case_from)
# Case when pawn reaches last line
piece = change_pawn(piece) if pawn_reached_end?(piece, case_to)
board.set_case(case_to, piece)
board.set_case(case_from, nil)
end
# Return true if there are obstacles on the path of a sliding piece
def obstruction?(piece, case_from, case_to)
piece.possible_moves.each do |coord|
next_case = case_from
loop do
next_case = [next_case[0] + coord[0], next_case[1] + coord[1]]
return false if next_case == case_to
break if offboard(next_case) || !empty?(next_case)
end
end
end
# Return true if pawn reached the last line
def pawn_reached_end?(piece, case_to)
if piece.is_a?(Pawn)
return true if case_to[1] == 0 || case_to[1] == 7
end
return false
end
# Ask the player if he wants to change the pawn in another piece and return it
def change_pawn(piece)
puts "Do you want to change the piece in another piece?"
puts "'Q' for Queen, 'R' for Rook, 'B' for Bishop, 'K' for knight"
puts "or 'P' if you want to keep a Pawn"
loop do
input = gets.chomp.upcase
case input
when 'Q'
new_piece = Queen.new(piece.color, piece.location, 'slide')
when 'R'
new_piece = Rook.new(piece.color, piece.location, 'slide')
when 'B'
new_piece = Bishop.new(piece.color, piece.location, 'slide')
when 'K'
new_piece = Knight.new(piece.color, piece.location, 'slide')
when 'P'
new_piece = piece
else
puts "Wrong input. Try again"
next
end
return new_piece
end
end
# Return true if pawn can't move in diag
def pawn_cant_diag(piece, case_to)
return true if empty?(case_to)
case_selected = board.get_case(case_to)
return true if case_selected.color == piece.color
return false
end
# Return true if pawn can move up to 2 cases
def pawn_cant_double(piece, case_from)
if piece.color == 'white'
return true if case_from[1] != 1
elsif piece.color == 'black'
return true if case_from[1] != 6
end
return false
end
# Return true if the case selected is free of pieces
def empty?(case_selected)
if board.get_case(case_selected) == nil
return true
end
return false
end
# Return true if the case selected is offboard
def offboard(coord)
return true if coord[0] < 0 || coord[0] > 7 ||
coord[1] < 0 || coord[1] > 7
end
#############################
# VICTORY, DRAW OR SLATEMATE
#############################
# Return true if one player won
def victory?
king = find_king(current_player)
the_bad = king_check?(king)
if the_bad
if king_checkmate?(king, the_bad)
puts "Checkmate!"
puts "Well done #{other_player.name} you won!"
return true
else
puts "Check!"
end
end
return false
end
# Return true if there is a draw
def draw?
counter = 0
board.grid.each do |col|
col.each do |cell|
counter +=1 if cell
end
end
if counter < 3
puts "Draw!"
return true
end
return false
end
# Return true if there is a stalemate
def stalemate?
pieces = find_pieces(current_player.color)
if pieces.size == 1
unless king_can_move?(pieces[0])
puts "Slatemate!"
return true
end
end
return false
end
# Return king of given player
def find_king(player)
board.grid.each do |col|
col.each do |cell|
if cell
return cell if cell.is_a?(King) && cell.color == player.color
end
end
end
end
# Return an array with all the pieces of the given color
def find_pieces(color)
array = []
board.grid.each do |column|
column.each do |piece|
next if !piece
array.push(piece) if piece.color == color
end
end
array
end
# Return true if the given king is in check
def king_check?(king)
x = king.location[0]
y = king.location[1]
return check_slide?(x, y, 'diag') || check_slide?(x, y, 'line') ||
check_step?(x, y, 'knight') || check_step?(x, y, 'pawn')
end
# Return true if king is in check by a sliding piece
def check_slide?(x, y, input)
diag = [[-1, 1], [1, 1], [1,-1], [-1,-1]]
line = [[ 0, 1], [ 1, 0], [ 0,-1], [-1, 0]]
input == 'diag' ? direction = diag : direction = line
direction.each do |coord|
next_case = [x, y]
path = []
loop do
next_case = [next_case[0] + coord[0], next_case[1] + coord[1]]
break if offboard(next_case)
if empty?(next_case)
path.push(next_case)
next
end
piece = board.get_case(next_case)
if piece.color == other_player.color
if direction == diag
if piece.is_a?(Queen) || piece.is_a?(Bishop)
piece.path = path
return piece
end
elsif direction == line
if piece.is_a?(Queen) || piece.is_a?(Rook)
piece.path = path
return piece
end
end
else
break
end
end
end
return false
end
# Return true if king is in check by a stepping piece
def check_step?(x, y, input)
knight = [[-2,-1], [-2, 1], [-1, 2], [ 1, 2],
[ 2, 1], [ 2,-1], [ 1,-2], [-1,-2]]
pawn = [[-1, 1], [ 1, 1]] if current_player.color == 'white'
pawn = [[-1,-1], [ 1,-1]] if current_player.color == 'black'
input == 'knight' ? direction = knight : direction = pawn
direction.each do |coord|
next_case = [coord[0] + x, coord[1] + y]
next if offboard(next_case)
next if empty?(next_case)
piece = board.get_case(next_case)
if piece.color == other_player.color
return piece if piece.is_a?(Knight) && direction == knight
return piece if piece.is_a?(Pawn) && direction == pawn
else
next
end
end
return false
end
# Return true if the given king is checkmate
def king_checkmate?(king, the_bad)
# Not checkmate if king can move
return false if king_can_move?(king)
# Not checkmate if adverse piece can be taken or path obstructed
if the_bad.type == 'step'
return false if can_be_taken?(the_bad)
elsif the_bad.type == 'slide'
return false if can_be_taken?(the_bad) || can_be_obstructed?(the_bad)
end
return true
end
# Return true if there is at least one empty case around the king
def king_can_move?(king)
current_case = king.location
king.possible_moves[0..-3].each do |coord| # [0..-3] to avoid castle moves
next_case = [current_case[0] + coord[0], current_case[1] + coord[1]]
next if offboard(next_case)
if empty?(next_case)
move_piece(current_case, next_case)
if king_check?(king)
move_piece(next_case, current_case)
next
else
move_piece(next_case, current_case)
return true
end
end
end
return false
end
# Return true if piece can be taken by adverse piece
def can_be_taken?(the_bad)
the_bad.color == 'white' ? color = 'black' : color = 'white'
case_to = the_bad.location
pieces = find_pieces(color)
pieces.each do |piece|
case_from = piece.location
answer = move_possible(case_from, case_to)
return true if answer[0]
end
return false
end
# Return true if piece can be obstructed by adverse piece
def can_be_obstructed?(the_bad)
the_bad.color == 'white' ? color = 'black' : color = 'white'
pieces = find_pieces(color)
the_bad.path.each do |case_to|
pieces.each do |piece|
case_from = piece.location
answer = move_possible(case_from, case_to)
return true if answer[0]
end
end
return false
end
# Return true if kings are one case apart
def kings_too_close?
king1 = find_king(current_player)
king2 = find_king(other_player)
king1.possible_moves.each do |coord|
next_case = [king1.location[0] + coord[0], king1.location[1] + coord[1]]
return true if next_case == king2.location
end
return false
end
# Return true if king can castle
def can_castle?(king, move)
if move == [-2, 0]
path = [[4,0], [3,0], [2,0], [1,0], [0,0]] if king.color == 'white'
path = [[4,7], [3,7], [2,7], [1,7], [0,7]] if king.color == 'black'
elsif move == [ 2, 0]
path = [[4,0], [5,0], [6,0], [7,0]] if king.color == 'white'
path = [[4,7], [5,7], [6,7], [7,7]] if king.color == 'black'
end
piece1 = board.get_case(path.first)
piece2 = board.get_case(path.last)
# King and Rook should be at their initial location
return false if empty?(path.first) || empty?(path.last)
# Squares in between should be empty
path[1..-2].each { |square| return false unless empty?(square) }
# King & Rook shouldn't have mooved
return false if piece1.counter > 0 || piece2.counter > 0
# King shouldn't be in check on the path
2.times do |nb|
move_piece(path[nb], path[nb+1])
if king_check?(king)
move_piece(path[nb+1], path[0])
return false
end
end
move_piece(path.last, path[1])
return true
end
################################
# LOAD AND SAVE FUNCTIONALITIES
################################
# Load saved game if exists
def load_game
Dir.chdir("save")
if File.exists?("save.txt")
data = YAML::load(File.read("save.txt"))
self.current_player = data.current_player
self.other_player = data.other_player
self.board = data.board
puts "Game Loaded!"
puts
Dir.chdir("..")
else
puts "You have no saved games."
puts ""
Dir.chdir("..")
end
end
# Save game
def save_game
Dir.chdir("save")
File.open("save.txt", 'w') { |file| file.write(YAML::dump(self))}
puts "Game Saved!"
Dir.chdir("..")
exit
end
end
The method #start
is the one used to launch a new game. From there, the player can choose to load a game or start a new one. If he starts a new one, he will be asked his name and which color he wants to play. Same for other player. Then we have a loop that allows the program to play the same instructions till we’ve got a winner, a draw or a slatemate (equals a draw).
This class is really long so I won’t describe everything. Regards to the design, I tried to keep methods short and with one purpose. It’s not always easy but I thing it really pays off. It makes the code cleaner, tidy, and much more easily to read. Furthermore I try to keep as much comments as possible, without disturbing the clearness of the code.
Writing Specs, Check!
Using RSpec, I wrote some tests for the main methods of my Game class. And I really have to confess: testing seemed first a waste of time. But you see quickly how efficient it can be. These tests allowed me to correct some glitches in my code I wouldn’t have found without. Or maybe I would, but 25 games later! So it’s definitly not a waste of time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
require './lib/game.rb'
describe Game do
let (:game) { Game.new }
let (:flo) { Player.new("flo", "white") }
let (:ginny) { Player.new("ginny", "black") }
describe '#case_not_valid?' do
context "when input is 'from'" do
let (:input) { 'from'}
it "returns false if a selected case is valide" do
case_selected = [1,1]
game.stub(:current_player) { flo }
expect(game.case_not_valid?(case_selected, input)).to eq false
end
it "returns true if a case is empty" do
case_selected = [3,3]
game.stub(:current_player) { flo }
expect(game.case_not_valid?(case_selected, input)).to eq true
end
it "returns true if the case is taken by an adverse piece" do
case_selected = [7,7]
game.stub(:current_player) { flo }
expect(game.case_not_valid?(case_selected, input)).to eq true
end
end
context "when input is 'to'" do
let (:input) { 'to'}
it "returns false if a selected case is valide" do
case_selected = [3,3]
expect(game.case_not_valid?(case_selected, input)).to eq false
end
it "returns true if the case is already taken by own piece" do
case_selected = [1,1]
game.stub(:current_player) { flo }
expect(game.case_not_valid?(case_selected, input)).to eq true
end
end
end
describe '#move_possible' do
context "regards to piece's way of displacement" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[0][0] = Rook.new('white', [0,0], 'slide')
array[1][0] = Knight.new('white', [1,0], 'step')
array[2][0] = Bishop.new('white', [2,0], 'slide')
array[3][0] = Queen.new('white', [3,0], 'slide')
array[4][0] = King.new('white', [4,0], 'step')
array[5][1] = Pawn.new('white', [5,1], 'step')
array[7][7] = King.new('black', [7,7], 'step')
Board.new(array)
end
it "returns an array with 1st element eq to true if rook can move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([0,0], [0,1]).first).to eq true
end
it "returns an array with 1st element eq to false if rook can't move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([0,0], [1,1]).first).to eq false
end
it "returns an array with 1st element eq to true if knight can move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([1,0], [0,2]).first).to eq true
end
it "returns an array with 1st element eq to false if knight can't move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([1,0], [0,1]).first).to eq false
end
it "returns an array with 1st element eq to true if bishop can move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([2,0], [3,1]).first).to eq true
end
it "returns an array with 1st element eq to false if bishop can't move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([2,0], [2,1]).first).to eq false
end
it "returns an array with 1st element eq to true if queen can move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([3,0], [3,3]).first).to eq true
end
it "returns an array with 1st element eq to false if queen can't move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([3,0], [4,2]).first).to eq false
end
it "returns an array with 1st element eq to true if king can move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([4,0], [4,1]).first).to eq true
end
it "returns an array with 1st element eq to false if king can't move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([4,0], [4,2]).first).to eq false
end
it "returns an array with 1st element eq to true if pawn can move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([5,1], [5,2]).first).to eq true
end
it "returns an array with 1st element eq to false if pawn can't move" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([5,1], [6,1]).first).to eq false
end
end
context "regards to sliding pieces" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[3][1] = Rook.new('white', [3,1], 'slide')
array[3][2] = Bishop.new('white', [3,2], 'slide')
array[4][1] = Queen.new('white', [4,1], 'slide')
array[7][7] = King.new('black', [7,7], 'step')
Board.new(array)
end
it "returns an array with 1st element eq to false if obstacle on the path" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([3,1], [5,1]).first).to eq false
expect(game.move_possible([3,2], [5,0]).first).to eq false
expect(game.move_possible([4,1], [2,1]).first).to eq false
end
end
context "regards to the pawn" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[0][0] = King.new('white', [0,0], 'step')
array[2][1] = Pawn.new('white', [2,1], 'step')
array[3][2] = Pawn.new('white', [3,2], 'step')
array[4][3] = Pawn.new('black', [4,3], 'step')
array[7][7] = King.new('black', [7,7], 'step')
Board.new(array)
end
it "returns an array with 1st element eq to true if pawn can double displacement" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([2,1], [2,3]).first).to eq true
end
it "returns an array with 1st element eq to true if pawn can go in diag" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([3,2], [4,3]).first).to eq true
end
it "returns an array with 1st element eq to false if pawn can't double displacement" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([3,2], [3,4]).first).to eq false
end
it "returns an array with 1st element eq to false if pawn can't go in diag" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([2,1], [1,2]).first).to eq false
end
end
context "regards to kings location" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[3][3] = King.new('white', [3,3], 'step')
array[3][5] = King.new('black', [3,5], 'step')
Board.new(array)
end
it "returns an array with 1st element eq to false if kings are one case apart" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([3,5], [3,4]).first).to eq false
end
end
context "regards to own king" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[0][0] = King.new('white', [0,0], 'step')
array[7][7] = King.new('black', [7,7], 'step')
array[6][0] = Queen.new('white', [6,0], 'slide')
Board.new(array)
end
it "returns an array with 1st element eq to false if king becomes in check" do
game.stub(:board) { board }
game.stub(:current_player) { ginny }
game.stub(:other_player) { flo }
expect(game.move_possible([7,7], [6,7]).first).to eq false
end
end
context "regards to castling, left bottom" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[0][0] = Rook.new('white', [0,0], 'slide')
array[7][0] = Rook.new('white', [7,0], 'slide')
array[4][7] = King.new('black', [4,7], 'step')
array[0][7] = Rook.new('black', [0,7], 'slide')
array[7][7] = Rook.new('black', [7,7], 'slide')
Board.new(array)
end
it "returns an array with 1st element eq to true if king can castle" do
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
game.stub(:board) { board }
expect(game.move_possible([4,0], [2,0]).first).to eq true
end
end
context "regards to castling, right bottom" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[0][0] = Rook.new('white', [0,0], 'slide')
array[7][0] = Rook.new('white', [7,0], 'slide')
array[4][7] = King.new('black', [4,7], 'step')
array[0][7] = Rook.new('black', [0,7], 'slide')
array[7][7] = Rook.new('black', [7,7], 'slide')
Board.new(array)
end
it "returns an array with 1st element eq to true if king can castle" do
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
game.stub(:board) { board }
expect(game.move_possible([4,0], [6,0]).first).to eq true
end
end
context "regards to castling, left top" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[0][0] = Rook.new('white', [0,0], 'slide')
array[7][0] = Rook.new('white', [7,0], 'slide')
array[4][7] = King.new('black', [4,7], 'step')
array[0][7] = Rook.new('black', [0,7], 'slide')
array[7][7] = Rook.new('black', [7,7], 'slide')
Board.new(array)
end
it "returns an array with 1st element eq to true if king can castle" do
game.stub(:current_player) { ginny }
game.stub(:other_player) { flo }
game.stub(:board) { board }
expect(game.move_possible([4,7], [2,7]).first).to eq true
end
end
context "regards to castling, right up" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[0][0] = Rook.new('white', [0,0], 'slide')
array[7][0] = Rook.new('white', [7,0], 'slide')
array[4][7] = King.new('black', [4,7], 'step')
array[0][7] = Rook.new('black', [0,7], 'slide')
array[7][7] = Rook.new('black', [7,7], 'slide')
Board.new(array)
end
it "returns an array with 1st element eq to true if king can castle" do
game.stub(:current_player) { ginny }
game.stub(:other_player) { flo }
game.stub(:board) { board }
expect(game.move_possible([4,7], [6,7]).first).to eq true
end
end
context "regards to castling" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[0][0] = Rook.new('white', [0,0], 'slide')
array[5][0] = Queen.new('white', [5,0], 'slide')
array[4][7] = King.new('black', [4,7], 'step')
array[0][7] = Rook.new('black', [0,7], 'slide')
array[3][7] = Queen.new('black', [3,7], 'slide')
Board.new(array)
end
it "returns an array with 1st element eq to false if king can't castle" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.move_possible([4,0], [2,0]).first).to eq false
end
end
end
describe '#king_check?' do
context "if check by a rook" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[4][7] = King.new('black', [4,7], 'step')
array[0][0] = Rook.new('black', [0,0], 'slide')
Board.new(array)
end
it "returns an array" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
king = board.get_case([4,0])
expect(game.king_check?(king)).to be_kind_of(Rook)
end
end
context "if check by a knight" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[4][7] = King.new('black', [4,7], 'step')
array[3][2] = Knight.new('black', [3,2], 'step')
Board.new(array)
end
it "returns an array" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
king = board.get_case([4,0])
expect(game.king_check?(king)).to be_kind_of(Knight)
end
end
context "if check by a bishop" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[4][7] = King.new('black', [4,7], 'step')
array[3][1] = Bishop.new('black', [3,1], 'slide')
Board.new(array)
end
it "returns an array" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
king = board.get_case([4,0])
expect(game.king_check?(king)).to be_kind_of(Bishop)
end
end
context "if check by a queen" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[4][7] = King.new('black', [4,7], 'step')
array[4][4] = Queen.new('black', [4,4], 'slide')
Board.new(array)
end
it "returns an array" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
king = board.get_case([4,0])
expect(game.king_check?(king)).to be_kind_of(Queen)
end
end
context "if check by a bishop" do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[4][0] = King.new('white', [4,0], 'step')
array[4][7] = King.new('black', [4,7], 'step')
array[3][1] = Pawn.new('black', [3,1], 'step')
Board.new(array)
end
it "returns an array" do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
king = board.get_case([4,0])
expect(game.king_check?(king)).to be_kind_of(Pawn)
end
end
end
describe '#victory' do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[0][1] = Pawn.new('white', [0,1], 'step')
array[1][1] = Pawn.new('white', [1,1], 'step')
array[2][1] = Pawn.new('white', [2,1], 'step')
array[0][0] = King.new('white', [0,0], 'step')
array[4][7] = King.new('black', [4,7], 'step')
array[5][0] = Queen.new('black', [5,0], 'slide')
Board.new(array)
end
it 'returns true if there is chekmate' do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.victory?).to be true
end
end
describe '#draw' do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[0][0] = King.new('white', [0,0], 'step')
array[4][7] = King.new('black', [4,7], 'step')
Board.new(array)
end
it 'returns true if draw' do
game.stub(:board) { board }
game.stub(:current_player) { ginny }
game.stub(:other_player) { flo }
expect(game.draw?).to be true
end
end
describe '#stalemate' do
let(:board) do
array = Array.new(8) { Array.new(8) }
array[0][0] = King.new('white', [0,0], 'step')
array[4][7] = King.new('black', [4,7], 'step')
array[1][7] = Queen.new('black', [1,7], 'slide')
array[5][1] = Rook.new('black', [5,1], 'slide')
Board.new(array)
end
it 'returns true if stalemate' do
game.stub(:board) { board }
game.stub(:current_player) { flo }
game.stub(:other_player) { ginny }
expect(game.stalemate?).to be true
end
end
end
And regards to design, it pushes you to keep a sort of independance between methods and classes. And this is something really great. That way, if you have to change a functionality in the future, it won’t disturb other parts of the code. I’m really greatful to TDD for having learned me that.
Level up, checkmate!
This feeling when after looping throught debugging and implementing new functionalities, you start a new game and everything works fine.. First it’s surprise! I didn’t really believe that my game was actually working fine. I have never done a program that long before.
Then it’s joy. You’ve done it!
This second part of The Odin Project wasn’t a waste of time. I learned how to use Ruby, but more than that I learned how I should design my code. I learned how to use data structures and algorithms to make it more efficient. I learned how to test my code and the beauty of debugging. And I learned how important it is to keep a core clean, tidy and commented.
Next part, Interaction with the real world: Rails !