sapphire-chess 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +45 -0
- data/LICENSE +21 -0
- data/README.md +24 -0
- data/Rakefile +1 -0
- data/bin/sapphire-chess +5 -0
- data/lib/sapphire-chess/ai.rb +101 -0
- data/lib/sapphire-chess/algebraic_conversion.rb +89 -0
- data/lib/sapphire-chess/board/board_analysis.rb +66 -0
- data/lib/sapphire-chess/board/board_evaluation.rb +25 -0
- data/lib/sapphire-chess/board/board_general.rb +155 -0
- data/lib/sapphire-chess/board/board_provisional_moves.rb +32 -0
- data/lib/sapphire-chess/board/board_renderer.rb +122 -0
- data/lib/sapphire-chess/board.rb +5 -0
- data/lib/sapphire-chess/display.rb +140 -0
- data/lib/sapphire-chess/engine.rb +153 -0
- data/lib/sapphire-chess/human_input_validation.rb +123 -0
- data/lib/sapphire-chess/movement_rules/castling_board_control.rb +37 -0
- data/lib/sapphire-chess/movement_rules/castling_piece_control.rb +9 -0
- data/lib/sapphire-chess/movement_rules/castling_rights.rb +79 -0
- data/lib/sapphire-chess/movement_rules/en_passant_board_control.rb +20 -0
- data/lib/sapphire-chess/movement_rules/en_passant_piece_control.rb +36 -0
- data/lib/sapphire-chess/movement_rules/move_slide_pattern.rb +23 -0
- data/lib/sapphire-chess/movement_rules/move_step_pattern.rb +17 -0
- data/lib/sapphire-chess/movement_rules/pawn_movement_and_promotion.rb +71 -0
- data/lib/sapphire-chess/movement_rules.rb +7 -0
- data/lib/sapphire-chess/pieces/bishop.rb +56 -0
- data/lib/sapphire-chess/pieces/empty_square.rb +13 -0
- data/lib/sapphire-chess/pieces/king.rb +77 -0
- data/lib/sapphire-chess/pieces/knight.rb +57 -0
- data/lib/sapphire-chess/pieces/pawn.rb +82 -0
- data/lib/sapphire-chess/pieces/piece.rb +77 -0
- data/lib/sapphire-chess/pieces/queen.rb +44 -0
- data/lib/sapphire-chess/pieces/rook.rb +62 -0
- data/lib/sapphire-chess/pieces.rb +8 -0
- data/lib/sapphire-chess/player.rb +43 -0
- data/lib/sapphire-chess/version.rb +3 -0
- data/lib/sapphire-chess.rb +9 -0
- data/sapphire-chess.gemspec +29 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a152a756661a6bba522bd8a75bde7074e7386a5b65c1572225da5976acd86cd8
|
4
|
+
data.tar.gz: 98c2040c722a85e5f5caf9a8dce9ee5caeb2bf1871b1b9dceb1e1d330de52e03
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 108d87f43ba5f8c26f6e7e607dca07cbc57e7ae79ea4675b3649d5272995351b2cc37ca9570bac11651d4694b3978bba502b0ff60a1b52e5c30541ee68d77c41
|
7
|
+
data.tar.gz: 7a035c86043ecec355bb910d834eec02c060f49705067b27ed2c5b9f467e5920e7adbf1bf215a31330fda448911797d4a8ded7e227b417bb96aa39832027bdcb
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
sapphire-chess (1.0.0)
|
5
|
+
paint (~> 2.3)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
ast (2.4.2)
|
11
|
+
json (2.6.3)
|
12
|
+
paint (2.3.0)
|
13
|
+
parallel (1.22.1)
|
14
|
+
parser (3.2.0.0)
|
15
|
+
ast (~> 2.4.1)
|
16
|
+
rainbow (3.1.1)
|
17
|
+
rake (13.0.6)
|
18
|
+
regexp_parser (2.6.1)
|
19
|
+
rexml (3.2.5)
|
20
|
+
rubocop (1.43.0)
|
21
|
+
json (~> 2.3)
|
22
|
+
parallel (~> 1.10)
|
23
|
+
parser (>= 3.2.0.0)
|
24
|
+
rainbow (>= 2.2.2, < 4.0)
|
25
|
+
regexp_parser (>= 1.8, < 3.0)
|
26
|
+
rexml (>= 3.2.5, < 4.0)
|
27
|
+
rubocop-ast (>= 1.24.1, < 2.0)
|
28
|
+
ruby-progressbar (~> 1.7)
|
29
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
30
|
+
rubocop-ast (1.24.1)
|
31
|
+
parser (>= 3.1.1.0)
|
32
|
+
ruby-progressbar (1.11.0)
|
33
|
+
unicode-display_width (2.4.2)
|
34
|
+
|
35
|
+
PLATFORMS
|
36
|
+
x86_64-linux
|
37
|
+
|
38
|
+
DEPENDENCIES
|
39
|
+
bundler (~> 2.4)
|
40
|
+
rake (~> 13.0)
|
41
|
+
rubocop (~> 1.43)
|
42
|
+
sapphire-chess!
|
43
|
+
|
44
|
+
BUNDLED WITH
|
45
|
+
2.4.3
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright © 2023, Lucas Sorribes
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Sapphire Chess v0.9.0
|
2
|
+
|
3
|
+
Welcome to Sapphire Chess!
|
4
|
+
|
5
|
+
This is a chess game written in pure Ruby v2.7.5. Other versions have not been tested yet.
|
6
|
+
|
7
|
+
Please, visit https://medium.com/@lucas.sorribes/nostromo-my-ruby-chess-journey-part-i-7ef544b547a5 for a very detailed account of how I wrote this game.
|
8
|
+
|
9
|
+
---
|
10
|
+
|
11
|
+
## Current Features
|
12
|
+
|
13
|
+
* A beautiful board with easy-to-distinguish colors for white and black pieces.
|
14
|
+
* Fully functional AI
|
15
|
+
* Two game modes: human vs. computer, human vs. human.
|
16
|
+
* Three levels of difficulty.
|
17
|
+
* Full chess movement rules implementation, including castling and *en passant*, for both the human and the computer player.
|
18
|
+
* Accepts algebraic notation for movements, with human input validation.
|
19
|
+
* Material score display.
|
20
|
+
* Player's last move display.
|
21
|
+
|
22
|
+
## Screenshot
|
23
|
+
|
24
|
+
![Game screenshot](./screenshot.png)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/bin/sapphire-chess
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
module AI
|
2
|
+
private
|
3
|
+
|
4
|
+
# Chooses move by best possible outcome:
|
5
|
+
def computer_chooses_move
|
6
|
+
possible_moves = board.generate_moves(color)
|
7
|
+
possible_moves << %i[castle king] if castle_rights?(:king)
|
8
|
+
possible_moves << %i[castle queen] if castle_rights?(:queen)
|
9
|
+
|
10
|
+
best_move(possible_moves)
|
11
|
+
end
|
12
|
+
|
13
|
+
def best_move(possible_moves)
|
14
|
+
evaluations = {}
|
15
|
+
anti_loop_filter(possible_moves)
|
16
|
+
|
17
|
+
possible_moves.each do |move|
|
18
|
+
evaluations[move] =
|
19
|
+
minimax(move, depth, -Float::INFINITY, Float::INFINITY, maximizing_player?)
|
20
|
+
end
|
21
|
+
|
22
|
+
move_randomizer(evaluations)
|
23
|
+
end
|
24
|
+
|
25
|
+
def minimax(move, depth, alpha, beta, maximizing_player)
|
26
|
+
return board.evaluate if depth.zero?
|
27
|
+
|
28
|
+
move_final_evaluation =
|
29
|
+
board.provisional(move, color) do
|
30
|
+
# This generates possible outcomes (children) for the provisional move:
|
31
|
+
# Each branch represents the next turn (i.e.: if current player is white
|
32
|
+
# [the maximizing player], it generates every possible movement for the
|
33
|
+
# next player, black [the minimizing player], who will choose the best
|
34
|
+
# possible move, and so on. The best (relative to each player) possible
|
35
|
+
# outcome for each move will determine what move is chosen, `best_evaluation`)
|
36
|
+
# See AI#computer_chooses_move
|
37
|
+
|
38
|
+
# The alpha-beta `prunes` the tree: it makes the search more efficient
|
39
|
+
# removing unnecessary branches, resulting in a faster process.
|
40
|
+
move_final_evaluation =
|
41
|
+
if maximizing_player
|
42
|
+
best_minimizing_evaluation = Float::INFINITY
|
43
|
+
|
44
|
+
board.generate_moves(:black).each do |possible_move|
|
45
|
+
evaluation = minimax(possible_move, depth - 1, alpha, beta, false)
|
46
|
+
best_minimizing_evaluation = [best_minimizing_evaluation, evaluation].min
|
47
|
+
beta = [beta, evaluation].min
|
48
|
+
break if beta <= alpha
|
49
|
+
end
|
50
|
+
|
51
|
+
best_minimizing_evaluation
|
52
|
+
else
|
53
|
+
best_maximizing_evaluation = -Float::INFINITY
|
54
|
+
|
55
|
+
board.generate_moves(:white).each do |possible_move|
|
56
|
+
evaluation = minimax(possible_move, depth - 1, alpha, beta, true)
|
57
|
+
best_maximizing_evaluation = [best_maximizing_evaluation, evaluation].max
|
58
|
+
alpha = [alpha, evaluation].max
|
59
|
+
break if beta <= alpha
|
60
|
+
end
|
61
|
+
|
62
|
+
best_maximizing_evaluation
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
move_final_evaluation
|
67
|
+
end
|
68
|
+
|
69
|
+
# This method randomizes the moves if two or more moves share the best evaluation.
|
70
|
+
# This avoids the Computer to play the same moves every game.
|
71
|
+
def move_randomizer(evaluations)
|
72
|
+
best_evaluation =
|
73
|
+
if color == :white then evaluations.values.max
|
74
|
+
else
|
75
|
+
evaluations.values.min
|
76
|
+
end
|
77
|
+
|
78
|
+
evaluations.select do |_, evaluation|
|
79
|
+
evaluation == best_evaluation
|
80
|
+
end.keys.sample
|
81
|
+
end
|
82
|
+
|
83
|
+
def anti_loop_filter(possible_moves)
|
84
|
+
possible_moves.delete(history[-2]) if possible_moves.include?(history[-2])
|
85
|
+
end
|
86
|
+
|
87
|
+
# For evaluation analysis only:
|
88
|
+
def store_evaluation(move, evaluation)
|
89
|
+
return [move, evaluation] if move.first == :castle
|
90
|
+
|
91
|
+
description = format(
|
92
|
+
'%<piece_class>s %<piece_position>s to %<target_class>s %<target_position>s',
|
93
|
+
piece_class: board[move.first].class,
|
94
|
+
piece_position: move.first,
|
95
|
+
target_class: board[move.last].class,
|
96
|
+
target_position: move.last
|
97
|
+
)
|
98
|
+
|
99
|
+
[description, evaluation]
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module AlgebraicConversion
|
2
|
+
LETTER_RESET_VALUE = 97 # ASCII downcase 'a' numeric value: 'a'.ord
|
3
|
+
ALGEBRAIC_NOTATION_FORMAT = /[a-h]{1}[1-8]{1}/.freeze
|
4
|
+
CASTLING_INPUT_FORMAT = /castle [kq]{1}/.freeze
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def algebraic_input
|
9
|
+
move_input = nil
|
10
|
+
loop do
|
11
|
+
move_input = gets.chomp.strip.downcase
|
12
|
+
break if valid_input_format?(move_input)
|
13
|
+
|
14
|
+
puts 'Please, enter a valid move_input.'
|
15
|
+
end
|
16
|
+
|
17
|
+
convert_algegraic_input(move_input)
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid_input_format?(move_input)
|
21
|
+
(move_input.size == 2 &&
|
22
|
+
move_input.match?(ALGEBRAIC_NOTATION_FORMAT)) ||
|
23
|
+
(move_input.size == 4 &&
|
24
|
+
move_input[0, 2].match?(ALGEBRAIC_NOTATION_FORMAT) &&
|
25
|
+
move_input[2, 2].match?(ALGEBRAIC_NOTATION_FORMAT)) ||
|
26
|
+
move_input.match?(CASTLING_INPUT_FORMAT)
|
27
|
+
end
|
28
|
+
|
29
|
+
def convert_algegraic_input(move_input)
|
30
|
+
case move_input.size
|
31
|
+
when 2 then convert_single_input(move_input)
|
32
|
+
when 4 then convert_double_input(move_input)
|
33
|
+
else convert_castling_input(move_input)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def convert_single_input(move_input)
|
38
|
+
letter = move_input[0]
|
39
|
+
number = move_input[1]
|
40
|
+
|
41
|
+
row = number_to_row(number)
|
42
|
+
column = letter_to_column(letter)
|
43
|
+
|
44
|
+
[row, column]
|
45
|
+
end
|
46
|
+
|
47
|
+
def convert_double_input(move_input)
|
48
|
+
letter = move_input[0]
|
49
|
+
number = move_input[1]
|
50
|
+
letter_end = move_input[2]
|
51
|
+
number_end = move_input[3]
|
52
|
+
|
53
|
+
row = number_to_row(number)
|
54
|
+
column = letter_to_column(letter)
|
55
|
+
|
56
|
+
row_end = number_to_row(number_end)
|
57
|
+
column_end = letter_to_column(letter_end)
|
58
|
+
|
59
|
+
[[row, column], [row_end, column_end]]
|
60
|
+
end
|
61
|
+
|
62
|
+
def number_to_row(number)
|
63
|
+
(number.to_i - Board::SQUARE_ORDER).abs
|
64
|
+
end
|
65
|
+
|
66
|
+
def letter_to_column(letter)
|
67
|
+
letter.ord - LETTER_RESET_VALUE
|
68
|
+
end
|
69
|
+
|
70
|
+
def convert_castling_input(move_input)
|
71
|
+
side = move_input[-1] == 'k' ? :king : :queen
|
72
|
+
[:castle, side]
|
73
|
+
end
|
74
|
+
|
75
|
+
def convert_to_algebraic(square)
|
76
|
+
letter = column_to_letter(square.last)
|
77
|
+
number = row_to_number(square.first)
|
78
|
+
|
79
|
+
"#{letter}#{number}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def column_to_letter(column)
|
83
|
+
(column + LETTER_RESET_VALUE).chr
|
84
|
+
end
|
85
|
+
|
86
|
+
def row_to_number(row)
|
87
|
+
(row - Board::SQUARE_ORDER).abs
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module BoardAnalysis
|
2
|
+
# This is the total material evaluation (pieces value put together)
|
3
|
+
# when a player keeps just a king, a queen and a few pieces, indicating
|
4
|
+
# that the game is now in its last stage (endgame):
|
5
|
+
LAST_STAND_PIECES_VALUE = 21_500
|
6
|
+
|
7
|
+
def in_check?(color)
|
8
|
+
king_position = find_king(color)
|
9
|
+
|
10
|
+
enemy_pieces(color).each do |piece|
|
11
|
+
return true if piece.available_moves.include?(king_position)
|
12
|
+
end
|
13
|
+
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_king(color)
|
18
|
+
king_location = pieces.find do |piece|
|
19
|
+
piece.color == color && piece.is_a?(King)
|
20
|
+
end
|
21
|
+
|
22
|
+
king_location&.location
|
23
|
+
end
|
24
|
+
|
25
|
+
def checkmate?(color)
|
26
|
+
return false unless in_check?(color)
|
27
|
+
|
28
|
+
friendly_pieces(color).all? { |piece| piece.safe_moves.empty? }
|
29
|
+
end
|
30
|
+
|
31
|
+
def no_king?(color)
|
32
|
+
find_king(color).nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
def pieces
|
36
|
+
matrix.flatten.reject { |position| position.is_a?(EmptySquare) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def friendly_pieces(color)
|
40
|
+
pieces.select { |piece| piece.color == color }
|
41
|
+
end
|
42
|
+
|
43
|
+
def enemy_pieces(color)
|
44
|
+
pieces.reject { |piece| piece.color == color }
|
45
|
+
end
|
46
|
+
|
47
|
+
def count(type, color)
|
48
|
+
friendly_pieces(color).select { |piece| piece.instance_of?(type) }.size
|
49
|
+
end
|
50
|
+
|
51
|
+
def promoted_pawns(color)
|
52
|
+
friendly_pieces(color).select do |piece|
|
53
|
+
piece.is_a?(Pawn) && piece.promoted?
|
54
|
+
end.size
|
55
|
+
end
|
56
|
+
|
57
|
+
def end_game?
|
58
|
+
(count(Queen, :white).zero? && count(Queen, :black).zero?) ||
|
59
|
+
(last_stand?(:white) || last_stand?(:black))
|
60
|
+
end
|
61
|
+
|
62
|
+
def last_stand?(color)
|
63
|
+
count(Queen, color).positive? &&
|
64
|
+
friendly_pieces(color).map(&:value).sum <= LAST_STAND_PIECES_VALUE
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module BoardEvaluation
|
2
|
+
def evaluate
|
3
|
+
material_evaluation + piece_location_evaluation
|
4
|
+
end
|
5
|
+
|
6
|
+
def material_evaluation
|
7
|
+
white_evaluation = friendly_pieces(:white).map(&:value).sum
|
8
|
+
black_evaluation = -friendly_pieces(:black).map(&:value).sum
|
9
|
+
|
10
|
+
white_evaluation + black_evaluation
|
11
|
+
end
|
12
|
+
|
13
|
+
def piece_location_evaluation
|
14
|
+
white_evaluation = friendly_pieces(:white).map(&:location_value).sum
|
15
|
+
black_evaluation = -friendly_pieces(:black).map(&:location_value).sum
|
16
|
+
|
17
|
+
white_evaluation + black_evaluation
|
18
|
+
end
|
19
|
+
|
20
|
+
def evaluate_move(move, color)
|
21
|
+
provisional(move, color) do
|
22
|
+
evaluate
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require_relative '../pieces'
|
2
|
+
require_relative '../movement_rules/castling_board_control'
|
3
|
+
require_relative '../movement_rules/en_passant_board_control'
|
4
|
+
require_relative '../board/board_analysis'
|
5
|
+
require_relative '../board/board_evaluation'
|
6
|
+
require_relative '../board/board_provisional_moves'
|
7
|
+
|
8
|
+
class Board
|
9
|
+
SQUARE_ORDER = 8
|
10
|
+
B_PAWN_ROW = 1
|
11
|
+
W_PAWN_ROW = 6
|
12
|
+
FIRST_ROW = 0
|
13
|
+
LAST_ROW = 7
|
14
|
+
PIECES_SEQUENCE = [
|
15
|
+
Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
include BoardAnalysis
|
19
|
+
include BoardEvaluation
|
20
|
+
include ProvisionalMoves
|
21
|
+
include CastlingBoardControl
|
22
|
+
include EnPassantBoardControl
|
23
|
+
|
24
|
+
attr_reader :matrix, :white_player, :black_player, :hard_difficulty
|
25
|
+
|
26
|
+
def self.initialize_board
|
27
|
+
board = new(duplicated: false)
|
28
|
+
|
29
|
+
set_pawns(board)
|
30
|
+
set_pieces(board)
|
31
|
+
|
32
|
+
board
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
private
|
37
|
+
|
38
|
+
def set_pawns(board)
|
39
|
+
[B_PAWN_ROW, W_PAWN_ROW].each do |pawn_row|
|
40
|
+
color = pawn_row == B_PAWN_ROW ? :black : :white
|
41
|
+
|
42
|
+
SQUARE_ORDER.times do |column|
|
43
|
+
board[[pawn_row, column]] = Pawn.new(board, [pawn_row, column], color)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_pieces(board)
|
49
|
+
[[FIRST_ROW, :black], [LAST_ROW, :white]].each do |(row, color)|
|
50
|
+
PIECES_SEQUENCE.each_with_index do |piece, column|
|
51
|
+
board[[row, column]] = piece.new(board, [row, column], color)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(duplicated: false)
|
58
|
+
@matrix = Array.new(SQUARE_ORDER) { Array.new(SQUARE_ORDER, EmptySquare.instance) }
|
59
|
+
@duplicated = duplicated
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_players!(player1, player2)
|
63
|
+
if player1.color == :white
|
64
|
+
@white_player = player1
|
65
|
+
@black_player = player2
|
66
|
+
else
|
67
|
+
@white_player = player2
|
68
|
+
@black_player = player1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def [](square)
|
73
|
+
row, column = square
|
74
|
+
matrix[row][column]
|
75
|
+
end
|
76
|
+
|
77
|
+
def []=(square, piece)
|
78
|
+
row, column = square
|
79
|
+
matrix[row][column] = piece
|
80
|
+
end
|
81
|
+
|
82
|
+
def within_limits?(square)
|
83
|
+
square.none? { |axis| axis >= SQUARE_ORDER || axis.negative? }
|
84
|
+
end
|
85
|
+
|
86
|
+
def empty_square?(square)
|
87
|
+
row, column = square
|
88
|
+
within_limits?(square) && matrix[row][column].is_a?(EmptySquare)
|
89
|
+
end
|
90
|
+
|
91
|
+
def move_piece!(piece, target_square, permanent: false)
|
92
|
+
mark_moved_piece!(piece) if permanent
|
93
|
+
|
94
|
+
self[piece], self[target_square] = EmptySquare.instance, self[piece]
|
95
|
+
|
96
|
+
self[target_square].location = target_square
|
97
|
+
|
98
|
+
return unless permanent && was_en_passant?(piece, target_square)
|
99
|
+
|
100
|
+
capture_passed_pawn!(target_square)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Deep duplication of the board for Piece#safe_moves
|
104
|
+
def duplicate
|
105
|
+
pieces.each_with_object(Board.new(duplicated: true)) do |piece, new_board|
|
106
|
+
new_piece = piece.class.new(new_board, piece.location, piece.color)
|
107
|
+
|
108
|
+
new_board[new_piece.location] = new_piece
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def generate_moves(color)
|
113
|
+
possible_moves =
|
114
|
+
friendly_pieces(color).each_with_object([]) do |piece, possible_moves|
|
115
|
+
location = piece.location
|
116
|
+
|
117
|
+
piece.available_moves.each do |possible_move|
|
118
|
+
possible_moves << [location, possible_move]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
# sort_moves!(possible_moves, color)
|
122
|
+
possible_moves
|
123
|
+
end
|
124
|
+
|
125
|
+
# This method is avoids checking for availability of en passant
|
126
|
+
# moves in duplicate boards
|
127
|
+
def a_duplicate?
|
128
|
+
@duplicated
|
129
|
+
end
|
130
|
+
|
131
|
+
def set_game_difficulty
|
132
|
+
self.hard_difficulty =
|
133
|
+
[white_player, black_player].find do |player|
|
134
|
+
player.is_a?(Computer)
|
135
|
+
end.depth == 3
|
136
|
+
end
|
137
|
+
|
138
|
+
def hard_difficulty?
|
139
|
+
hard_difficulty
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
attr_writer :hard_difficulty
|
145
|
+
# For testing purposes:
|
146
|
+
def sort_moves!(possible_moves, color)
|
147
|
+
possible_moves.sort! do |a, b|
|
148
|
+
if color == :white
|
149
|
+
evaluate_move(a, color) <=> evaluate_move(b, color)
|
150
|
+
else
|
151
|
+
evaluate_move(b, color) <=> evaluate_move(a, color)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ProvisionalMoves
|
2
|
+
def provisional(move, color)
|
3
|
+
piece_buffer = self[move.last] unless move.first == :castle
|
4
|
+
make_provisional!(move, color)
|
5
|
+
from_block = block_given? ? yield : raise(ArgumentError, 'No block given to ProvisionalMoves#provisional')
|
6
|
+
unmake_provisional!(piece_buffer, move, color)
|
7
|
+
from_block
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def make_provisional!(move, color)
|
13
|
+
if move.first == :castle
|
14
|
+
side = move.last
|
15
|
+
castle!(side, color)
|
16
|
+
else
|
17
|
+
start_position, target_position = move
|
18
|
+
move_piece!(start_position, target_position)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def unmake_provisional!(piece_buffer, move, color)
|
23
|
+
if move.first == :castle
|
24
|
+
side = move.last
|
25
|
+
uncastle!(side, color)
|
26
|
+
else
|
27
|
+
start_position, target_position = move
|
28
|
+
move_piece!(target_position, start_position)
|
29
|
+
self[target_position] = piece_buffer
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|