Class: Board

Inherits:
Object
  • Object
show all
Includes:
Move, Printer
Defined in:
lib/board.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Move

#castle, #constants, #get_col_from_index, #get_row_from_index, #move_diagonal, #move_knight, #move_lateral, #move_pawn, #moved?, #not_king, #piece_color, #possible_moves, #unoccupied?

Methods included from Printer

#clear_all, #piece_to_string, #print_board, #print_end_of_row, #print_footer, #print_header, #print_start_of_row, #printer, #substitute_pieces

Constructor Details

#initializeBoard

Returns a new instance of Board.



19
20
21
22
23
24
25
26
27
28
# File 'lib/board.rb', line 19

def initialize
  @piece_locations_buffer = Hash.new
  @piece_locations = Hash.new
  @row_mappings    = Hash[("A".."H").zip(1..8)]
  @taken_pieces    = Array.new
  @player_turn     = :black
  @checkmate       = false

  setup_board
end

Instance Attribute Details

#checkmateObject (readonly)

Returns the value of attribute checkmate.



13
14
15
# File 'lib/board.rb', line 13

def checkmate
  @checkmate
end

#piece_locationsObject (readonly) Also known as: piece_manifest

Returns the value of attribute piece_locations.



13
14
15
# File 'lib/board.rb', line 13

def piece_locations
  @piece_locations
end

#player_turnObject (readonly)

Returns the value of attribute player_turn.



13
14
15
# File 'lib/board.rb', line 13

def player_turn
  @player_turn
end

#taken_piecesObject (readonly)

Returns the value of attribute taken_pieces.



14
15
16
# File 'lib/board.rb', line 14

def taken_pieces
  @taken_pieces
end

Instance Method Details

#attack_vectors(color = @player_turn, proposed_manifest = @piece_locations) ⇒ Object

Board spaces that are attackable by opposing pieces

TODO: check? method should use this function


270
271
272
273
274
275
276
277
278
279
# File 'lib/board.rb', line 270

def attack_vectors(color = @player_turn, proposed_manifest = @piece_locations)
  enemy_color = opposing_color(color)
  kill_zone = Array.new

  proposed_manifest.each do |piece, details|
    kill_zone << possible_moves(piece, proposed_manifest) if details.fetch(:color) == enemy_color
  end

  kill_zone.flatten.uniq
end

#check?(color, proposed_manifest = @piece_locations, recurse_for_checkmate = false) ⇒ Boolean

Return whether the player of a specified color has their king currently in check by checking the attack vectors of all the opponents players against the king location Also, check whether king currently in check, has all of their valid moves within their opponents attack vectors, and therefore are in checkmate (@checkmate)

Returns:

  • (Boolean)


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
# File 'lib/board.rb', line 200

def check?(color, proposed_manifest = @piece_locations, recurse_for_checkmate = false)

  enemy_attack_vectors  = {}
  player_attack_vectors = {}

  king_loc    = []
  enemy_color = opposing_color(color)

  proposed_manifest.each do |piece, details|

    if details[:color] == enemy_color
      enemy_attack_vectors[piece] = possible_moves(piece, proposed_manifest)

    elsif details[:color] == color
      begin
        player_attack_vectors[piece] = possible_moves(piece, proposed_manifest)
      rescue
        # TODO: Fix possible_moves() so it doesn't throw exceptions
        # This happens because it is searching board for where pieces
        # will be, as as a result some pieces are nil
      end
    end

    king_loc = piece if details[:color] == color && details[:type] == :king
  end

  danger_vector  = enemy_attack_vectors.values.flatten.uniq
  defence_vector = player_attack_vectors.values.flatten.uniq
  king_positions = possible_moves(king_loc, proposed_manifest)

  # The King is in the attackable locations by the opposing player
  return false unless danger_vector.include? king_loc

  # If all the positions the king piece can move to is also attackable by the opposing player
  if recurse_for_checkmate && ((king_positions - danger_vector).length == 0)

    is_in_check = []
    player_attack_vectors.each do |piece_index, piece_valid_moves|
      piece_valid_moves.each do |possible_new_location|

        # Check if board is still in check after piece moves to its new location
        @new_piece_locations = @piece_locations.clone
        @new_piece_locations[possible_new_location] = @new_piece_locations[piece_index]
        @new_piece_locations[piece_index] = {
          :type   => nil,
          :number => nil,
          :color  => nil
        }

        is_in_check << check?(color, @new_piece_locations)
      end
    end

    if is_in_check.include?(false)
      return false
    else
      @checkmate = true
    end
  end

  true
end

#checkmate?(color, manifest) ⇒ Boolean

Returns:

  • (Boolean)


192
193
194
# File 'lib/board.rb', line 192

def checkmate?(color, manifest)
  check?(color, manifest, recurse_for_checkmate=true) && @checkmate
end

#commit_board_piece_movementObject



103
104
105
106
107
# File 'lib/board.rb', line 103

def commit_board_piece_movement
  @piece_locations = @piece_locations_buffer
  @player_turn = opposing_color(@player_turn)
  display_board
end

#create_board_after_piece_moved(p1, p2) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/board.rb', line 110

def create_board_after_piece_moved(p1, p2)
  # Store state of board after proposed move in @piece_locations_buffer
  @piece_locations_buffer = @piece_locations.clone
  @piece_locations_buffer[p2] = @piece_locations_buffer[p1]
  @piece_locations_buffer[p2][:moved] = true
  @piece_locations_buffer[p1] = {
    :type   => nil,
    :number => nil,
    :color  => nil
  }
end

#display_boardObject

Reprint the board. Called after every valid piece move



283
284
285
# File 'lib/board.rb', line 283

def display_board
  print_board @piece_locations
end

#get_index_from_rowcol(row_col) ⇒ Object

Convert index [A-H] => (1 - 64)



289
290
291
# File 'lib/board.rb', line 289

def get_index_from_rowcol(row_col)
  (row_col[1].to_i - 1) * 8 + @row_mappings.fetch(row_col[0]).to_i
end

#get_rowcol_from_index(index) ⇒ Object

Convert index (1 - 64) => [A-H]



295
296
297
298
299
300
# File 'lib/board.rb', line 295

def get_rowcol_from_index(index)
  letter = get_col_from_index(index)
  number = get_row_from_index(index)

  "#{letter}#{number}"
end

#king_positionsObject

Search piece manifest for kings. Remove them from the list of positions returned from the Move module (so that players cannot take the “king” type piece)



143
144
145
146
147
148
149
150
151
# File 'lib/board.rb', line 143

def king_positions
  king_locations = []

  @piece_locations.each do |piece, details|
    king_locations << piece if details.fetch(:type) == :king
  end

  king_locations
end

#move(p1, p2) ⇒ Object

Game logic



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
# File 'lib/board.rb', line 31

def move(p1, p2)

  manifest = piece_manifest()

  p1 = get_index_from_rowcol(p1.to_s)
  p2 = get_index_from_rowcol(p2.to_s)

  # Find valid positions and subtract king current position as nobody can directly take king piece
  valid_positions = possible_moves(p1, manifest, true)
  valid_positions -= king_positions

  # If player is moving out of turn, display message
  # `return p ...` is so we print value and return it from the function
  #  this is so the unit tests can get the value directly. There are better ways to do this
  unless @player_turn == @piece_locations[p1][:color]
    return p "It is #{@player_turn}'s turn. Please move a #{@player_turn} piece."
  end

  return p "Please select a valid destination." unless valid_positions.include?(p2)

  create_board_after_piece_moved(p1, p2)

  # If player is in check with the new proposed board, disallow the movement
  if check?(@player_turn, @piece_locations_buffer)
    return p "Please move #{@player_turn} king out of check to continue"
  end

  # At this point, the move appears to be valid
  @taken_pieces << @piece_locations[p2] unless @piece_locations[p2][:type].nil?

  # Check for Pawn Promotion (if pawn reaches end of the board, promote it)
  if @piece_locations_buffer[p2][:type] == :pawn
    if p2 < 9 && @piece_locations_buffer[p2][:color] == :red
      promote(p2)
    elsif p2 > 56 && @piece_locations_buffer[p2][:color] == :black
      promote(p2)
    end
  end

  # Check for Castling - https://en.m.wikipedia.org/wiki/Castling
  if @piece_locations_buffer[p2][:type] == :king && (p2 - p1).abs == 2

    p2 < 9 ? y_offset = 0 : y_offset = 56

    if p2 > p1
      @piece_locations_buffer[6+y_offset] = @piece_locations_buffer[8+y_offset]
      @piece_locations_buffer[8+y_offset] = {
        :type   => nil,
        :number => nil,
        :color  => nil
      }
    else
      @piece_locations_buffer[4+y_offset] = @piece_locations_buffer[1+y_offset]
      @piece_locations_buffer[1+y_offset] = {
        :type   => nil,
        :number => nil,
        :color  => nil
      }
    end
  end

  commit_board_piece_movement

  if (winner = player_in_checkmate(@piece_locations))
    return p Messages.black_winner if winner == :black
    return p Messages.red_winner   if winner == :red
  end

  return p Messages.piece_moved
end

#opposing_color(player_color) ⇒ Object



264
265
266
# File 'lib/board.rb', line 264

def opposing_color(player_color)
  ([:black, :red] - [player_color]).first
end

#place_first_row(color) ⇒ Object



344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/board.rb', line 344

def place_first_row(color)
  row_offset = color == :black ? 0 : 56

  @piece_locations[row_offset + 1] = {:type => :rook,   :number => 1, :color => color, :moved => false}
  @piece_locations[row_offset + 2] = {:type => :knight, :number => 1, :color => color, :moved => false}
  @piece_locations[row_offset + 3] = {:type => :bishop, :number => 1, :color => color, :moved => false}
  @piece_locations[row_offset + 4] = {:type => :queen,  :number => 1, :color => color, :moved => false}
  @piece_locations[row_offset + 5] = {:type => :king,   :number => 1, :color => color, :moved => false}
  @piece_locations[row_offset + 6] = {:type => :bishop, :number => 2, :color => color, :moved => false}
  @piece_locations[row_offset + 7] = {:type => :knight, :number => 2, :color => color, :moved => false}
  @piece_locations[row_offset + 8] = {:type => :rook,   :number => 2, :color => color, :moved => false}
end

#place_pawns(color) ⇒ Object



330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/board.rb', line 330

def place_pawns(color)
  offset = color == :black ? 8 : 48

  (1..8).each do |piece_num|
    @piece_locations[piece_num + offset] = {
      :type   => :pawn,
      :number => piece_num,
      :color  => color,
      :moved  => false
    }
  end
end

#place_player_pieces(color) ⇒ Object



323
324
325
326
327
# File 'lib/board.rb', line 323

def place_player_pieces(color)
  # Place pieces on chess board
  place_first_row(color)
  place_pawns(color)
end

#player_in_checkmate(manifest = @piece_locations) ⇒ Object



187
188
189
190
# File 'lib/board.rb', line 187

def player_in_checkmate(manifest = @piece_locations)
  return :red   if checkmate?(:black, manifest)
  return :black if checkmate?(:red,   manifest)
end

#setup_boardObject



303
304
305
306
307
308
# File 'lib/board.rb', line 303

def setup_board
  # Intial setup of board. Put pieces into the expected locations
  setup_empty_tiles
  place_player_pieces(:black)
  place_player_pieces(:red)
end

#setup_empty_tilesObject



311
312
313
314
315
316
317
318
319
320
# File 'lib/board.rb', line 311

def setup_empty_tiles
  # Initialize chess board tiles without any pieces
  (1..64).each do |tile_num|
    @piece_locations[tile_num] = {
      :type   => nil,
      :number => nil,
      :color  => nil
    }
  end
end

#valid_destinations(current_pos) ⇒ Object

Return the valid positions for piece at current_pos to move in readable format [A-H]



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/board.rb', line 124

def valid_destinations(current_pos)
  readable_positions = []
  manifest = piece_manifest
  p1 = get_index_from_rowcol(current_pos.to_s)

  valid_positions = possible_moves(p1, manifest, true)

  valid_positions.each do |pos|
    grid_pos = get_rowcol_from_index(pos)
    # Map first string character 1-8 to [A-H], for column, and then add second string character as [1-8]
    readable_positions << (@row_mappings.key(grid_pos[0].to_i) + grid_pos[1].to_s)
  end

  readable_positions.sort
end