sapphire-chess 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+

|
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
|