Class: ChessValidator::MoveLogic

Inherits:
Object
  • Object
show all
Defined in:
lib/move_logic.rb

Constant Summary collapse

MAX_CACHE_SIZE =
1000

Class Method Summary collapse

Class Method Details

.advance_pawn?(pawn, board, move) ⇒ Boolean

Returns:

  • (Boolean)


254
255
256
257
258
259
260
261
262
263
264
# File 'lib/move_logic.rb', line 254

def advance_pawn?(pawn, board, move)
  if empty_square?(board, forward_by(pawn, 1))
    if (pawn.position[1].to_i - move[1].to_i).abs == 2
      empty_square?(board, forward_by(pawn, 2))
    else
      true
    end
  else
    false
  end
end

.boardsObject



27
28
29
30
31
32
33
# File 'lib/move_logic.rb', line 27

def boards
  if @boards.nil? || @boards.size > MAX_CACHE_SIZE
    @boards = {}
  end

  @boards
end

.can_defend?(piece, board, position, fen) ⇒ Boolean

Returns:

  • (Boolean)


334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/move_logic.rb', line 334

def can_defend?(piece, board, position, fen)
  case piece.piece_type.downcase
  when 'k'
    handle_king(piece, board, position, fen)
  when 'p'
    if piece.position[0] != position[0]
       target_piece = find_piece(board, position)
       valid = (target_piece && target_piece.color != piece.color) || position == fen.en_passant
       valid && king_will_be_safe?(piece, board, position)
    end
  else
    occupied_spaces = get_occupied_spaces(fen.to_s)
    valid_move_path?(piece, position, occupied_spaces) && king_will_be_safe?(piece, board, position)
  end
end

.castled?(piece, move) ⇒ Boolean

Returns:

  • (Boolean)


180
181
182
# File 'lib/move_logic.rb', line 180

def castled?(piece, move)
  piece.piece_type.downcase == 'k' && (piece.position[0].ord - move[0].ord).abs == 2
end

.collision?(position, destination, occupied_spaces, i1, i2) ⇒ Boolean

Returns:

  • (Boolean)


301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/move_logic.rb', line 301

def collision?(position, destination, occupied_spaces, i1, i2)
  start_index = position[i1]
  end_index = destination[i1]

  if start_index > end_index
    start_index = destination[i1]
    end_index = position[i1]
  end

  occupied_spaces.select do |space|
    space[i2] == position[i2] && space[i1] > start_index && space[i1] < end_index
  end.size > 0
end

.defended_pieces_by_square(fen_notation) ⇒ Object



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

def defended_pieces_by_square(fen_notation)
  return defended_sets[fen_notation] if defended_sets[fen_notation]

  board = get_board(fen_notation)

  fen = PGN::FEN.new(fen_notation)
  space_index = {}

  pieces = board.values.map do |piece|
    space_index[piece.position] = piece.piece_type + piece.color
    piece
  end

  defended = Hash.new([])

  pieces.each do |piece|
    moves_for_piece(piece).each do |move|
      if space_index[move] && space_index[move][-1] == piece.color && can_defend?(piece, board, move, fen)
        defended[move] += [piece]
      end
    end
  end

  defended_sets[fen_notation] = defended
  defended
end

.defended_setsObject



51
52
53
54
55
56
57
# File 'lib/move_logic.rb', line 51

def defended_sets
  if @defended_sets.nil? || @defended_sets.size > MAX_CACHE_SIZE
    @defended_sets = {}
  end

  @defended_sets
end

.diagonal_collision?(position, destination, occupied_spaces) ⇒ Boolean

Returns:

  • (Boolean)


315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/move_logic.rb', line 315

def diagonal_collision?(position, destination, occupied_spaces)
  difference = (position[1].to_i - destination[1].to_i).abs - 1

  horizontal_multiplyer = 1
  horizontal_multiplyer = -1 if position[0] > destination[0]

  vertical_multiplyer = 1
  vertical_multiplyer = -1 if position[1] > destination[1]

  move_path = []
  difference.times do |n|
    column = (position[0].ord + ((n + 1) * horizontal_multiplyer)).chr
    row = (position[1].to_i + ((n + 1) * vertical_multiplyer)).to_s
    move_path << column + row
  end

  !(move_path & occupied_spaces).empty?
end

.empty_square?(board, move) ⇒ Boolean

Returns:

  • (Boolean)


284
285
286
# File 'lib/move_logic.rb', line 284

def empty_square?(board, move)
  board.values.none? { |piece| piece.position == move }
end

.en_passant?(piece, move, square) ⇒ Boolean

Returns:

  • (Boolean)


184
185
186
# File 'lib/move_logic.rb', line 184

def en_passant?(piece, move, square)
  piece.piece_type.downcase == 'p' && piece.position[0] != move[0] && !square
end

.find_king_and_spaces(board, color) ⇒ Object



195
196
197
198
199
200
201
202
203
# File 'lib/move_logic.rb', line 195

def find_king_and_spaces(board, color)
  king = nil
  occupied_spaces = board.values.map do |p|
    king = p if p.piece_type.downcase == 'k' && p.color == color
    p.position
  end

  [king, occupied_spaces]
end

.find_piece(board, position) ⇒ Object



280
281
282
# File 'lib/move_logic.rb', line 280

def find_piece(board, position)
  board.values.detect { |piece| piece.position == position }
end

.find_target(board, piece, move, en_passant) ⇒ Object



92
93
94
95
96
97
98
99
100
101
# File 'lib/move_logic.rb', line 92

def find_target(board, piece, move, en_passant)
  if piece.piece_type.downcase == 'p' && piece.position[0] == move[0]
    nil
  elsif board[INDEX_KEY[move]]
    board[INDEX_KEY[move]]
  elsif piece.piece_type.downcase == 'p' && move == en_passant
    en_passant_position = piece.color == 'w' ? move[0] + '5' : move[0] + '4'
    board[INDEX_KEY[en_passant_position]]
  end
end

.forward_by(piece, count) ⇒ Object



266
267
268
269
# File 'lib/move_logic.rb', line 266

def forward_by(piece, count)
  position = piece.position
  piece.color == 'w' ? position[0] + (position[1].to_i + count).to_s : position[0] + (position[1].to_i - count).to_s
end

.get_board(fen_string) ⇒ Object



59
60
61
62
63
64
65
# File 'lib/move_logic.rb', line 59

def get_board(fen_string)
  return boards[fen_string] if boards[fen_string]

  board = BoardLogic.build_board_from_string(fen_string.split.first)
  boards[fen_string] = board
  board
end

.get_occupied_spaces(fen_string) ⇒ Object



68
69
70
71
72
73
74
75
76
77
# File 'lib/move_logic.rb', line 68

def get_occupied_spaces(fen_string)
  return occupied_square_sets[fen_string] if occupied_square_sets[fen_string]
  board = get_board(fen_string)

  occupied_spaces = []
  board.values.each { |p| occupied_spaces << p.position }

  occupied_square_sets[fen_string] = occupied_spaces
  occupied_spaces
end

.handle_castle(board, move) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/move_logic.rb', line 148

def handle_castle(board, move)
  case move
  when 'c1'
    board.delete(57)
    board[60] = Piece.new('R', 60)
  when 'g1'
    board.delete(64)
    board[62] = Piece.new('R', 62)
  when 'c8'
    board.delete(1)
    board[4] = Piece.new('r', 4)
  when 'g8'
    board.delete(8)
    board[6] = Piece.new('r', 6)
  end

  board
end

.handle_en_passant(board, pawn_color, move) ⇒ Object



137
138
139
140
141
142
143
144
145
146
# File 'lib/move_logic.rb', line 137

def handle_en_passant(board, pawn_color, move)
  if pawn_color == 'w'
    index = INDEX_KEY[move[0] + '5']
  else
    index = INDEX_KEY[move[0] + '4']
  end

  board.delete(index)
  board
end

.handle_king(king, board, move, fen) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/move_logic.rb', line 205

def handle_king(king, board, move, fen)
  if (king.position[0].ord - move[0].ord).abs == 2
    empty_b_square = true
    if move[0] == 'c'
      castle_code = 'q'
      between = 'd' + move[1]
      empty_b_square = empty_square?(board, 'b' + move[1])
    else
      castle_code = 'k'
      between = 'f' + move[1]
    end
    occupied_spaces = get_occupied_spaces(fen.to_s)

    (fen.castling.include?(castle_code) && king.color == 'b' || fen.castling.include?(castle_code.upcase) && king.color == 'w') &&
    king_is_safe?(king.color, board, king.position, occupied_spaces) &&
    king_is_safe?(king.color, board, between, occupied_spaces) &&
    king_is_safe?(king.color, board, move, occupied_spaces) &&
    board.values.none? { |piece| [between, move].include?(piece.position) } &&
    empty_b_square
  else
    king_will_be_safe?(king, board, move)
  end
end

.handle_pawn(piece, board, move, fen) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
# File 'lib/move_logic.rb', line 242

def handle_pawn(piece, board, move, fen)
  position = piece.position

  if position[0] == move[0]
    valid = advance_pawn?(piece, board, move)
  else
    target_piece = find_piece(board, move)
    valid = (target_piece && target_piece.color != piece.color) || move == fen.en_passant
  end
  valid && king_will_be_safe?(piece, board, move)
end

.king_is_safe?(king_color, board, king_position, occupied_spaces) ⇒ Boolean

Returns:

  • (Boolean)


229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/move_logic.rb', line 229

def king_is_safe?(king_color, board, king_position, occupied_spaces)
  board.values.none? do |piece|
    piece.color != king_color &&
    moves_for_piece(piece).any? do |move|
      if piece.piece_type.downcase == 'p'
        king_position == move && piece.position[0] != king_position[0]
      else
        king_position == move && valid_move_path?(piece, king_position, occupied_spaces)
      end
    end
  end
end

.king_will_be_safe?(piece, board, move) ⇒ Boolean

Returns:

  • (Boolean)


188
189
190
191
192
193
# File 'lib/move_logic.rb', line 188

def king_will_be_safe?(piece, board, move)
  new_board = with_next_move(piece, board, move)
  king, occupied_spaces = find_king_and_spaces(new_board, piece.color)
  return false if king.nil?
  king_is_safe?(king.color, new_board, king.position, occupied_spaces)
end

.load_move_data(board, piece, fen) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/move_logic.rb', line 80

def load_move_data(board, piece, fen)
  moves_for_piece(piece).each do |move|
    if valid_move?(piece, board, move, fen)
      piece.valid_moves << move
      target = find_target(board, piece, move, fen.en_passant)
      piece.targets << target if target
    else
      piece.move_potential << move
    end
  end
end

.make_move(piece, move, fen_notation) ⇒ Object



173
174
175
176
177
178
# File 'lib/move_logic.rb', line 173

def make_move(piece, move, fen_notation)
  board = get_board(fen_notation)
  new_board = with_next_move(piece, board, move)
  fen = PGN::FEN.new(fen_notation)
  BoardLogic.to_fen_notation(new_board, fen, piece, move, board[INDEX_KEY[move]])
end

.make_random_move(fen_notation, pieces_with_moves) ⇒ Object



167
168
169
170
171
# File 'lib/move_logic.rb', line 167

def make_random_move(fen_notation, pieces_with_moves)
  piece_to_move = pieces_with_moves.sample
  move = piece_to_move.valid_moves.sample
  make_move(piece_to_move, move, fen_notation)
end

.moves_diagonal(vertical, horizontal, position) ⇒ Object



407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/move_logic.rb', line 407

def moves_diagonal(vertical, horizontal, position)
  column = position[0]
  row = position[1]
  possible_moves = []

  while column >= 'a' && column <= 'h' && row >= '1' && row <= '8' do
    column = horizontal == 'left' ? previous_char(column) : column.next
    row = vertical == 'up' ? row.next : previous_char(row)
    possible_moves << column + row
  end
  remove_out_of_bounds(possible_moves)
end

.moves_for_bishop(position) ⇒ Object



398
399
400
401
402
403
404
405
# File 'lib/move_logic.rb', line 398

def moves_for_bishop(position)
  top_right = moves_diagonal('up', 'right', position)
  top_left = moves_diagonal('up', 'left', position)
  bottom_left = moves_diagonal('down', 'left', position)
  bottom_right = moves_diagonal('down', 'right', position)

  top_right + top_left + bottom_left + bottom_right
end

.moves_for_king(position) ⇒ Object



445
446
447
448
# File 'lib/move_logic.rb', line 445

def moves_for_king(position)
  castle_moves = [(position[0].ord - 2).chr + position[1], position[0].next.next + position[1]]
  remove_out_of_bounds(spaces_near_king(position) + castle_moves)
end

.moves_for_knight(position) ⇒ Object



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/move_logic.rb', line 450

def moves_for_knight(position)
  column = position[0].ord
  row = position[1].to_i

  moves = [
    (column - 2).chr + (row + 1).to_s,
    (column - 2).chr + (row - 1).to_s,
    (column + 2).chr + (row + 1).to_s,
    (column + 2).chr + (row - 1).to_s,
    (column - 1).chr + (row + 2).to_s,
    (column - 1).chr + (row - 2).to_s,
    (column + 1).chr + (row + 2).to_s,
    (column + 1).chr + (row - 2).to_s
  ]

  remove_out_of_bounds(moves)
end

.moves_for_pawn(pawn) ⇒ Object



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/move_logic.rb', line 472

def moves_for_pawn(pawn)
  column = pawn.position[0].ord
  row = pawn.color == 'w' ? pawn.position[1].to_i + 1 : pawn.position[1].to_i - 1

  moves = [
    (column - 1).chr + row.to_s,
    (column + 1).chr + row.to_s,
    column.chr + row.to_s
  ]

  if pawn.color == 'w' && pawn.position[1] == '2' || pawn.color == 'b' && pawn.position[1] == '7'
    two_forward = pawn.color == 'w' ? row + 1 : row - 1
    moves << column.chr + two_forward.to_s
  end
  remove_out_of_bounds(moves)
end

.moves_for_piece(piece) ⇒ Object



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/move_logic.rb', line 377

def moves_for_piece(piece)
  case piece.piece_type.downcase
  when 'r'
    moves_for_rook(piece.position)
  when 'b'
    moves_for_bishop(piece.position)
  when 'q'
    moves_for_queen(piece.position)
  when 'k'
    moves_for_king(piece.position)
  when 'n'
    moves_for_knight(piece.position)
  when 'p'
    moves_for_pawn(piece)
  end
end

.moves_for_queen(position) ⇒ Object



424
425
426
# File 'lib/move_logic.rb', line 424

def moves_for_queen(position)
  moves_for_rook(position) + moves_for_bishop(position)
end

.moves_for_rook(position) ⇒ Object



394
395
396
# File 'lib/move_logic.rb', line 394

def moves_for_rook(position)
  moves_horizontal(position) + moves_vertical(position)
end

.moves_horizontal(position) ⇒ Object



489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/move_logic.rb', line 489

def moves_horizontal(position)
  possible_moves = []
  column = 'a'
  row = position[1]

  8.times do
    possible_moves << column + row unless column == position[0]
    column = column.next
  end

  possible_moves
end

.moves_vertical(position) ⇒ Object



502
503
504
505
506
507
508
509
510
511
512
513
# File 'lib/move_logic.rb', line 502

def moves_vertical(position)
  possible_moves = []
  column = position[0]
  row = '1'

  8.times do
    possible_moves << column + row unless row == position[1]
    row = row.next
  end

  possible_moves
end

.next_moves(fen) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/move_logic.rb', line 9

def next_moves(fen)
  fen_notation = fen.to_s
  return next_moves_for[fen_notation] if next_moves_for[fen_notation]

  board = get_board(fen.to_s)
  pieces = []

  board.values.each do |piece|
    if piece.color == fen.active
      load_move_data(board, piece, fen)
      pieces << piece if piece.valid_moves.size > 0
    end
  end
  next_moves_for[fen_notation] = pieces

  pieces
end

.next_moves_forObject



35
36
37
38
39
40
41
# File 'lib/move_logic.rb', line 35

def next_moves_for
  if @next_moves_for.nil? || @next_moves_for.size > MAX_CACHE_SIZE
    @next_moves_for = {}
  end

  @next_moves_for
end

.occupied_square_setsObject



43
44
45
46
47
48
49
# File 'lib/move_logic.rb', line 43

def occupied_square_sets
  if @occupied_square_sets.nil? || @occupied_square_sets.size > MAX_CACHE_SIZE
    @occupied_square_sets = {}
  end

  @occupied_square_sets
end

.previous_char(char) ⇒ Object



420
421
422
# File 'lib/move_logic.rb', line 420

def previous_char(char)
  (char.ord - 1).chr
end

.remove_out_of_bounds(moves) ⇒ Object



468
469
470
# File 'lib/move_logic.rb', line 468

def remove_out_of_bounds(moves)
  moves.select { |move| ('a'..'h').include?(move[0]) && ('1'..'8').include?(move[1..-1]) }
end

.resolve_piece_type(piece_type, move) ⇒ Object



129
130
131
132
133
134
135
# File 'lib/move_logic.rb', line 129

def resolve_piece_type(piece_type, move)
  if piece_type.downcase == 'p' && ['1', '8'].include?(move[1])
    move[1] == '8' ? 'Q' : 'q'
  else
    piece_type
  end
end

.spaces_near_king(position) ⇒ Object



428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/move_logic.rb', line 428

def spaces_near_king(position)
  column = position[0].ord
  row = position[1].to_i

  moves = [
    (column - 1).chr + row.to_s,
    (column + 1).chr + row.to_s,
    (column - 1).chr + (row - 1).to_s,
    (column - 1).chr + (row + 1).to_s,
    (column + 1).chr + (row - 1).to_s,
    (column + 1).chr + (row + 1).to_s,
    column.chr + (row + 1).to_s,
    column.chr + (row - 1).to_s
  ]
  remove_out_of_bounds(moves)
end

.valid_destination?(piece, board, move) ⇒ Boolean

Returns:

  • (Boolean)


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

def valid_destination?(piece, board, move)
  target_piece = find_piece(board, move)
  if target_piece
    target_piece.color != piece.color
  else
    true
  end
end

.valid_move?(piece, board, move, fen) ⇒ Boolean

Returns:

  • (Boolean)


103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/move_logic.rb', line 103

def valid_move?(piece, board, move, fen)
  case piece.piece_type.downcase
  when 'k'
    handle_king(piece, board, move, fen) && valid_destination?(piece, board, move)
  when 'p'
    handle_pawn(piece, board, move, fen)
  else
    occupied_spaces = get_occupied_spaces(fen.to_s)
    valid_move_path?(piece, move, occupied_spaces) &&
    valid_destination?(piece, board, move) &&
    king_will_be_safe?(piece, board, move)
  end
end

.valid_move_path?(piece, move, occupied_spaces) ⇒ Boolean

Returns:

  • (Boolean)


288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/move_logic.rb', line 288

def valid_move_path?(piece, move, occupied_spaces)
  position = piece.position
  if piece.piece_type.downcase == 'n'
    true
  elsif position[0] == move[0]
    !collision?(piece.position, move, occupied_spaces, 1, 0)
  elsif position[1] == move[1]
    !collision?(piece.position, move, occupied_spaces, 0, 1)
  else
    !diagonal_collision?(piece.position, move, occupied_spaces)
  end
end

.with_next_move(piece, board, move) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/move_logic.rb', line 117

def with_next_move(piece, board, move)
  index = INDEX_KEY[move]
  new_board = board.clone
  piece_type = resolve_piece_type(piece.piece_type, move)
  new_piece = Piece.new(piece_type, index)
  new_board.delete(piece.square_index)
  new_board[index] = new_piece
  new_board = handle_castle(new_board, move) if castled?(piece, move)
  new_board = handle_en_passant(new_board, piece.color, move) if en_passant?(piece, move, new_board[index])
  new_board
end