rb_chess 0.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/LICENSE +21 -0
- data/README.md +68 -0
- data/bin/rb_chess +6 -0
- data/lib/rb_chess/board.rb +171 -0
- data/lib/rb_chess/castling_rights.rb +31 -0
- data/lib/rb_chess/cli/cli.rb +237 -0
- data/lib/rb_chess/cli/game_saver.rb +33 -0
- data/lib/rb_chess/cli/players.rb +20 -0
- data/lib/rb_chess/errors.rb +5 -0
- data/lib/rb_chess/fen_parser.rb +150 -0
- data/lib/rb_chess/game.rb +166 -0
- data/lib/rb_chess/game_modules/move_generator.rb +154 -0
- data/lib/rb_chess/game_modules/move_parser.rb +43 -0
- data/lib/rb_chess/game_modules/move_validator.rb +47 -0
- data/lib/rb_chess/letter_display.rb +42 -0
- data/lib/rb_chess/move.rb +33 -0
- data/lib/rb_chess/move_set.rb +18 -0
- data/lib/rb_chess/pieces/bishop.rb +24 -0
- data/lib/rb_chess/pieces/king.rb +26 -0
- data/lib/rb_chess/pieces/knight.rb +25 -0
- data/lib/rb_chess/pieces/pawn.rb +45 -0
- data/lib/rb_chess/pieces/piece.rb +19 -0
- data/lib/rb_chess/pieces/piece_constants.rb +26 -0
- data/lib/rb_chess/pieces/queen.rb +25 -0
- data/lib/rb_chess/pieces/rook.rb +24 -0
- data/lib/rb_chess/position.rb +72 -0
- data/lib/rb_chess.rb +3 -0
- metadata +70 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pry-byebug'
|
|
4
|
+
require_relative 'pieces/piece_constants'
|
|
5
|
+
require_relative 'position'
|
|
6
|
+
require_relative 'castling_rights'
|
|
7
|
+
require_relative 'errors'
|
|
8
|
+
|
|
9
|
+
# A class to parse Chess FEN notation
|
|
10
|
+
module RbChess
|
|
11
|
+
class FENParser
|
|
12
|
+
include PieceConstants
|
|
13
|
+
|
|
14
|
+
BOARD_HEIGHT = 8
|
|
15
|
+
BOARD_WIDTH = 8
|
|
16
|
+
|
|
17
|
+
DEFAULT_NOTATION = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0'
|
|
18
|
+
FEN_TOKEN_REGEX = /[pkqrbn\d]/i.freeze
|
|
19
|
+
|
|
20
|
+
ERROR_MESSAGES = {
|
|
21
|
+
invalid_token: 'Invalid FEN notation, token not valid',
|
|
22
|
+
invalid_board_width: "Invalid FEN notation, Board width is not equal to #{BOARD_WIDTH}",
|
|
23
|
+
invalid_board_height: "Invalid FEN notation, Board height not equal to #{BOARD_HEIGHT}"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def self.board_to_fen(board)
|
|
27
|
+
pieces = pieces_to_fen(board.pieces)
|
|
28
|
+
active = board.active_color == :white ? 'w' : 'b'
|
|
29
|
+
castling_rights = board.castling_rights.to_s
|
|
30
|
+
en_passant_pos = board.en_passant_square.nil? ? '-' : board.en_passant_square.to_s
|
|
31
|
+
halfmove = board.halfmove_clock.to_s
|
|
32
|
+
fullmove = board.fullmove_no.to_s
|
|
33
|
+
|
|
34
|
+
[pieces, active, castling_rights, en_passant_pos, halfmove, fullmove].join(' ')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.pieces_to_fen(pieces)
|
|
38
|
+
(0..7).map { |y| build_rank_fen(pieces, y) }.join('/')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.build_rank_fen(pieces, y)
|
|
42
|
+
rank = ''
|
|
43
|
+
no_of_consecutive_empty_cells = 0
|
|
44
|
+
|
|
45
|
+
0.upto(7) do |x|
|
|
46
|
+
piece = pieces.find { |piece| piece.position.y == y && piece.position.x == x }
|
|
47
|
+
|
|
48
|
+
if piece.nil?
|
|
49
|
+
no_of_consecutive_empty_cells += 1
|
|
50
|
+
rank += no_of_consecutive_empty_cells.to_s if x == (BOARD_WIDTH - 1)
|
|
51
|
+
else
|
|
52
|
+
rank += get_piece_letter(piece, no_of_consecutive_empty_cells)
|
|
53
|
+
no_of_consecutive_empty_cells = 0
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
rank
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.get_piece_letter(piece, no_of_consecutive_empty_cells)
|
|
61
|
+
result = ''
|
|
62
|
+
result += no_of_consecutive_empty_cells.to_s if no_of_consecutive_empty_cells != 0
|
|
63
|
+
|
|
64
|
+
key = piece.class.name.split('::').last.to_sym
|
|
65
|
+
letter = PIECE_CLASS_TO_LETTER[key]
|
|
66
|
+
letter = piece.color == :white ? letter.upcase : letter
|
|
67
|
+
result + letter
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def initialize(fen_notation = DEFAULT_NOTATION)
|
|
71
|
+
segments = fen_notation.split(' ')
|
|
72
|
+
raise ChessError unless segments.size == 6
|
|
73
|
+
|
|
74
|
+
@pieces = segments[0]
|
|
75
|
+
@active = segments[1]
|
|
76
|
+
@castling_rights = segments[2]
|
|
77
|
+
@en_passant_pos = segments[3]
|
|
78
|
+
@halfmove = segments[4]
|
|
79
|
+
@fullmove = segments[5]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def parse
|
|
83
|
+
{
|
|
84
|
+
pieces: parse_pieces,
|
|
85
|
+
active_color: active == 'w' ? :white : :black,
|
|
86
|
+
castling_rights: parse_castling_rights,
|
|
87
|
+
en_passant_square: en_passant_pos == '-' ? nil : Position.parse(en_passant_pos),
|
|
88
|
+
halfmove_clock: halfmove.to_i,
|
|
89
|
+
fullmove_no: fullmove.to_i
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
attr_reader :pieces, :active, :castling_rights, :en_passant_pos, :halfmove, :fullmove
|
|
96
|
+
|
|
97
|
+
def parse_castling_rights
|
|
98
|
+
res = CastlingRights.new
|
|
99
|
+
|
|
100
|
+
res.kingside[:white] = true if castling_rights.include?('K')
|
|
101
|
+
res.kingside[:black] = true if castling_rights.include?('k')
|
|
102
|
+
res.queenside[:white] = true if castling_rights.include?('Q')
|
|
103
|
+
res.queenside[:black] = true if castling_rights.include?('q')
|
|
104
|
+
|
|
105
|
+
res
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def parse_pieces
|
|
109
|
+
ranks = pieces.split('/')
|
|
110
|
+
raise ChessError, ERROR_MESSAGES[:invalid_board_height] if ranks.length != BOARD_HEIGHT
|
|
111
|
+
|
|
112
|
+
ranks.each_with_index.reduce([]) { |res, (rank, y)| res.concat parse_rank_fen(rank, y) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def parse_rank_fen(rank_fen, y)
|
|
116
|
+
tokens = rank_fen.split('')
|
|
117
|
+
pos = Position.new(y: y, x: -1)
|
|
118
|
+
|
|
119
|
+
pieces = tokens.reduce([]) do |res, token|
|
|
120
|
+
piece, pos = parse_token(token, pos)
|
|
121
|
+
res.push(piece)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
raise ChessError, ERROR_MESSAGES[:invalid_board_width] if pos.x + 1 != BOARD_WIDTH
|
|
125
|
+
|
|
126
|
+
pieces.reject(&:nil?)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def parse_token(token, pos)
|
|
130
|
+
raise ChessError, ERROR_MESSAGES[:invalid_token] unless FEN_TOKEN_REGEX.match(token)
|
|
131
|
+
|
|
132
|
+
if is_integer? token
|
|
133
|
+
pos = pos.increment(x: token.to_i)
|
|
134
|
+
piece = nil
|
|
135
|
+
else
|
|
136
|
+
color = token.upcase == token ? :white : :black
|
|
137
|
+
piece_class = LETTER_TO_PIECE_CLASS[token.downcase.to_sym]
|
|
138
|
+
|
|
139
|
+
pos = pos.increment(x: 1)
|
|
140
|
+
piece = piece_class.new(color, pos)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
[piece, pos]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def is_integer?(string)
|
|
147
|
+
/^\d+$/.match string
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'require_all'
|
|
5
|
+
|
|
6
|
+
require_relative 'board'
|
|
7
|
+
require_relative 'errors'
|
|
8
|
+
require_rel 'game_modules'
|
|
9
|
+
|
|
10
|
+
# A class to handle a chess game
|
|
11
|
+
module RbChess
|
|
12
|
+
class Game
|
|
13
|
+
include MoveGenerator
|
|
14
|
+
include MoveParser
|
|
15
|
+
include MoveValidator
|
|
16
|
+
|
|
17
|
+
attr_reader :board
|
|
18
|
+
|
|
19
|
+
def initialize(fen: nil)
|
|
20
|
+
@board = Board.new(fen_notation: fen)
|
|
21
|
+
@starting_board = @board
|
|
22
|
+
@history = [].freeze
|
|
23
|
+
@repetitions_count = Hash.new(0) # for threefold repetition rule
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def make_move(move)
|
|
27
|
+
raise ChessError, 'Game Over' if game_over?
|
|
28
|
+
|
|
29
|
+
move = parse_move move
|
|
30
|
+
raise ChessError, 'Cannot make move king in check' unless legal_move? move
|
|
31
|
+
|
|
32
|
+
@board = board.make_move move
|
|
33
|
+
@history = [*@history, move].freeze
|
|
34
|
+
update_repetitions_count
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def winner
|
|
38
|
+
return unless checkmate?
|
|
39
|
+
|
|
40
|
+
current_player == :white ? :black : :white
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def valid_move?(move)
|
|
44
|
+
move = parse_move move
|
|
45
|
+
legal_move? move
|
|
46
|
+
rescue ChessError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def all_moves
|
|
51
|
+
moves = board.player_pieces(current_player).reduce([]) do |res, piece|
|
|
52
|
+
res.concat moves_for_piece(piece, board)
|
|
53
|
+
end
|
|
54
|
+
moves.filter { |m| legal_move?(m) }.map(&:to_s)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def make_moves(moves)
|
|
58
|
+
moves.each { |m| make_move(m) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def moves_at(pos)
|
|
62
|
+
moves_for_pos(pos, board).filter { |m| legal_move?(m) }.map(&:to_s)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def current_player
|
|
66
|
+
board.active_color
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def stalemate?
|
|
70
|
+
no_moves?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def threefold?
|
|
74
|
+
repetitions_count.values.any? { |e| e >= 3 }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fivefold?
|
|
78
|
+
repetitions_count.values.any? { |e| e >= 5 }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def seventy_five_moves?
|
|
82
|
+
board.halfmove_clock >= 150
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def history
|
|
86
|
+
@history.map(&:to_s)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fifty_moves?
|
|
90
|
+
board.halfmove_clock >= 100
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def checkmate?
|
|
94
|
+
stalemate? && check?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def insufficient_material?
|
|
98
|
+
return true if board.pieces.size == 2 # only kings
|
|
99
|
+
|
|
100
|
+
return true if board.pieces.size == 3 &&
|
|
101
|
+
board.pieces.any? { |p| p.is_a?(Knight) || p.is_a?(Bishop) }
|
|
102
|
+
|
|
103
|
+
if board.pieces.size == 4
|
|
104
|
+
bishops = board.pieces.select { |p| p.is_a? Bishop }
|
|
105
|
+
return bishops.length == 2 &&
|
|
106
|
+
bishops[0].position.square_type == bishops[1].position.square_type
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def draw?
|
|
113
|
+
!checkmate? && (
|
|
114
|
+
stalemate? || fivefold? || insufficient_material? ||
|
|
115
|
+
seventy_five_moves?
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def game_over?
|
|
120
|
+
checkmate? || draw?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def serialize
|
|
124
|
+
Marshal.dump(self)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.deserialize(data)
|
|
128
|
+
Marshal.load(data)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def as_json
|
|
132
|
+
data = {
|
|
133
|
+
moves: history,
|
|
134
|
+
starting_fen: starting_board.to_fen,
|
|
135
|
+
fen: board.to_fen
|
|
136
|
+
}
|
|
137
|
+
JSON.generate(data)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.from_json(json_data, with_history: false)
|
|
141
|
+
data = JSON.parse(json_data)
|
|
142
|
+
return new(fen: data['fen']) unless with_history
|
|
143
|
+
|
|
144
|
+
game = new(fen: data['starting_fen'])
|
|
145
|
+
game.make_moves data['moves']
|
|
146
|
+
game
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def update_repetitions_count
|
|
152
|
+
# removes halfmove and fullmove since they are needed
|
|
153
|
+
board_fen = board.to_fen.split(' ').first(4).join(' ')
|
|
154
|
+
repetitions_count[board_fen] = repetitions_count[board_fen] + 1
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def no_moves?
|
|
158
|
+
moves = board.player_pieces(current_player).reduce([]) do |res, piece|
|
|
159
|
+
res.concat moves_for_piece(piece, board)
|
|
160
|
+
end
|
|
161
|
+
moves.none? { |m| legal_move?(m) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
attr_reader :repetitions_count, :starting_board
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../position'
|
|
4
|
+
require_relative '../pieces/piece_constants'
|
|
5
|
+
require_relative '../move'
|
|
6
|
+
|
|
7
|
+
# a module to generate moves for a piece
|
|
8
|
+
module RbChess
|
|
9
|
+
module MoveGenerator
|
|
10
|
+
include PieceConstants
|
|
11
|
+
|
|
12
|
+
SPECIAL_MOVES = {
|
|
13
|
+
castle: :castle_moves,
|
|
14
|
+
en_passant: :en_passant_moves
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def moves_for_pos(pos, board)
|
|
18
|
+
piece = board.piece_at pos
|
|
19
|
+
return [] unless piece&.color == board.active_color
|
|
20
|
+
|
|
21
|
+
moves_for_piece(piece, board)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def moves_for_piece(piece, board)
|
|
25
|
+
piece.move_sets.reduce([]) do |result, move_set|
|
|
26
|
+
move_set.increments.each do |increment|
|
|
27
|
+
result.concat(
|
|
28
|
+
gen_moves_for_increment(board, move_set, increment, piece)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
result.concat gen_special_moves(board, move_set, piece)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def gen_moves_for_increment(board, move_set, increment, piece)
|
|
39
|
+
x, y = increment.values_at(:x, :y)
|
|
40
|
+
result = []
|
|
41
|
+
|
|
42
|
+
1.upto(move_set.repeat) do |factor|
|
|
43
|
+
new_pos = piece.position.increment(y: y * factor, x: x * factor)
|
|
44
|
+
break if new_pos.out_of_bounds? || blocked?(board, move_set, piece, new_pos)
|
|
45
|
+
|
|
46
|
+
removed = board.piece_at(new_pos).nil? ? nil : new_pos.to_s
|
|
47
|
+
from = piece.position.to_s
|
|
48
|
+
to = new_pos.to_s
|
|
49
|
+
|
|
50
|
+
if move_set.promotable && piece.can_promote?(new_pos)
|
|
51
|
+
result.concat promotion_moves(piece, from: from, to: to, removed: removed)
|
|
52
|
+
else
|
|
53
|
+
result << Move.new(from: from, to: to, removed: removed)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
break unless removed.nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def promotion_moves(piece, from:, to:, removed:)
|
|
63
|
+
piece.promotion_pieces.map do |letter|
|
|
64
|
+
Move.new(from: from, to: to, removed: removed, promotion: letter)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def blocked?(board, move_set, piece, new_pos)
|
|
69
|
+
other_piece = board.piece_at new_pos
|
|
70
|
+
return true if other_piece.nil? && move_set.blocked_by.include?(:empty)
|
|
71
|
+
|
|
72
|
+
return true if other_piece && move_set.blocked_by.include?(:piece)
|
|
73
|
+
|
|
74
|
+
return true if other_piece&.color == piece.color && move_set.blocked_by.include?(:same)
|
|
75
|
+
|
|
76
|
+
return true if other_piece&.color != piece.color && move_set.blocked_by.include?(:enemy)
|
|
77
|
+
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def gen_special_moves(board, move_set, piece)
|
|
82
|
+
result = []
|
|
83
|
+
move_set.special_moves.each do |sym|
|
|
84
|
+
result.concat send(SPECIAL_MOVES[sym], board, piece)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
result.reject(&:nil?)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def castle_moves(board, piece)
|
|
91
|
+
[
|
|
92
|
+
kingside_castle_move(board, piece),
|
|
93
|
+
queenside_castle_move(board, piece)
|
|
94
|
+
].reject(&:nil?)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def en_passant_moves(board, piece)
|
|
98
|
+
moves = [-1, 1].map do |x|
|
|
99
|
+
from = piece.position
|
|
100
|
+
to = piece.position.increment(y: piece.direction, x: x)
|
|
101
|
+
removed = piece.position.increment(x: x)
|
|
102
|
+
next nil unless board.en_passant_square == to
|
|
103
|
+
|
|
104
|
+
Move.new(from: from.to_s, to: to.to_s, removed: removed.to_s)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
moves.reject(&:nil?)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def kingside_castle_move(board, piece)
|
|
111
|
+
return unless board.can_castle_kingside? piece.color
|
|
112
|
+
|
|
113
|
+
return unless kingside_castle_squares_available?(board, piece)
|
|
114
|
+
|
|
115
|
+
king_from = piece.position.to_s
|
|
116
|
+
king_to = piece.position.increment(x: 2).to_s
|
|
117
|
+
rook_from = piece.color == :white ? 'h1' : 'h8'
|
|
118
|
+
rook_to = piece.color == :white ? 'f1' : 'f8'
|
|
119
|
+
|
|
120
|
+
move = Move.new(from: king_from, to: king_to, castle: :kingside)
|
|
121
|
+
move.add_move(from: rook_from, to: rook_to)
|
|
122
|
+
move
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def queenside_castle_move(board, piece)
|
|
126
|
+
return unless board.can_castle_queenside? piece.color
|
|
127
|
+
|
|
128
|
+
return unless queenside_castle_squares_available?(board, piece)
|
|
129
|
+
|
|
130
|
+
king_from = piece.position.to_s
|
|
131
|
+
king_to = piece.position.increment(x: -2).to_s
|
|
132
|
+
rook_from = piece.color == :white ? 'a1' : 'a8'
|
|
133
|
+
rook_to = piece.color == :white ? 'd1' : 'd8'
|
|
134
|
+
|
|
135
|
+
move = Move.new(from: king_from, to: king_to, castle: :queenside)
|
|
136
|
+
move.add_move(from: rook_from, to: rook_to)
|
|
137
|
+
move
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def kingside_castle_squares_available?(board, piece)
|
|
141
|
+
positions = piece.color == :white ? %w[f1 g1] : %w[f8 g8]
|
|
142
|
+
positions.all? do |pos|
|
|
143
|
+
board.piece_at(pos).nil?
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def queenside_castle_squares_available?(board, piece)
|
|
148
|
+
positions = piece.color == :white ? %w[b1 c1 d1] : %w[b8 c8 g8]
|
|
149
|
+
positions.all? do |pos|
|
|
150
|
+
board.piece_at(pos).nil?
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'move_generator'
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
|
|
6
|
+
module RbChess
|
|
7
|
+
module MoveParser
|
|
8
|
+
include MoveGenerator
|
|
9
|
+
|
|
10
|
+
MOVE_REGEX = /^(0-0)|(0-0-0)|(([a-h][1-8]){2}[qrbn]?)$/i.freeze
|
|
11
|
+
|
|
12
|
+
def parse_move(move)
|
|
13
|
+
raise ChessError, "Invalid move format cannot parse: #{move}" unless MOVE_REGEX.match move
|
|
14
|
+
|
|
15
|
+
castling_move?(move) ? castling_move(move) : normal_move(move)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def castling_move?(move)
|
|
19
|
+
/(0-0)|(0-0-0)/.match move
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def castling_move(move)
|
|
23
|
+
pos = current_player == :white ? 'e1' : 'e8'
|
|
24
|
+
type = move == '0-0' ? :kingside : :queenside
|
|
25
|
+
|
|
26
|
+
moves = moves_for_pos(pos, board)
|
|
27
|
+
move = moves.find { |m| m.castle == type }
|
|
28
|
+
|
|
29
|
+
raise ChessError, "Invalid move, cannot castle #{type}" unless move
|
|
30
|
+
|
|
31
|
+
move
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def normal_move(move_str)
|
|
35
|
+
_, pos, to, promotion = * /^(\w{2})(\w{2})(\w?)$/.match(move_str)
|
|
36
|
+
moves = moves_for_pos(pos, board)
|
|
37
|
+
move = moves.find { |m| m.to_s.downcase == move_str.downcase }
|
|
38
|
+
raise ChessError, "Invalid move: #{move}" unless move
|
|
39
|
+
|
|
40
|
+
move
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'move_generator'
|
|
4
|
+
|
|
5
|
+
module RbChess
|
|
6
|
+
module MoveValidator
|
|
7
|
+
include MoveGenerator
|
|
8
|
+
|
|
9
|
+
def legal_move?(move)
|
|
10
|
+
new_board = board.make_move move
|
|
11
|
+
is_check = check?(boardn: new_board, color: current_player)
|
|
12
|
+
|
|
13
|
+
return !is_check && legal_castle_move?(move) if move.castle
|
|
14
|
+
|
|
15
|
+
!is_check
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def check?(boardn: board, color: current_player)
|
|
19
|
+
king = boardn.pieces.find { |piece| piece.is_a?(King) && piece.color == color }
|
|
20
|
+
king ? pos_attacked?(boardn, king.position, color) : false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def legal_castle_move?(move)
|
|
24
|
+
can_castle = move.castle == :kingside ? legal_kingside_castle_move? : legal_queenside_castle_move?
|
|
25
|
+
can_castle && !check?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def legal_kingside_castle_move?
|
|
29
|
+
positions = current_player == :white ? %w[f1 g1] : %w[f8 g8]
|
|
30
|
+
positions.none? { |pos| pos_attacked?(board, Position.parse(pos), current_player) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def legal_queenside_castle_move?
|
|
34
|
+
positions = current_player == :white ? %w[b1 c1 d1] : %w[b8 c8 d8]
|
|
35
|
+
positions.none? { |pos| pos_attacked?(board, Position.parse(pos), current_player) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pos_attacked?(board, pos, color)
|
|
39
|
+
opponent_color = color == :white ? :black : :white
|
|
40
|
+
pieces = board.player_pieces(opponent_color)
|
|
41
|
+
moves = pieces.reduce([]) { |res, piece| res.concat moves_for_piece(piece, board) }
|
|
42
|
+
moves.any? do |move|
|
|
43
|
+
move.moved.any? { |hash| hash[:to] == pos }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'position'
|
|
4
|
+
require_relative 'pieces/piece_constants'
|
|
5
|
+
|
|
6
|
+
# A module to display chess board on console
|
|
7
|
+
module RbChess
|
|
8
|
+
module LetterDisplay
|
|
9
|
+
include PieceConstants
|
|
10
|
+
|
|
11
|
+
COLUMN_LABELS = ' a b c d e f g h'
|
|
12
|
+
SEPARATOR = ' _________________'
|
|
13
|
+
|
|
14
|
+
def ascii
|
|
15
|
+
ranks = (0..7).map { |rank_no| rank_ascii(rank_no) }
|
|
16
|
+
result = [COLUMN_LABELS, SEPARATOR] + ranks + [SEPARATOR, COLUMN_LABELS]
|
|
17
|
+
result.join("\n")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def rank_ascii(rank_no)
|
|
23
|
+
result = (0..7).map do |file_no|
|
|
24
|
+
piece_ascii(rank_no, file_no)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
result.unshift("#{8 - rank_no} ")
|
|
28
|
+
result.push(" #{8 - rank_no}")
|
|
29
|
+
result.join(' ')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def piece_ascii(rank_no, file_no)
|
|
33
|
+
pos = Position.new(y: rank_no, x: file_no)
|
|
34
|
+
piece = piece_at(pos)
|
|
35
|
+
return '-' if piece.nil?
|
|
36
|
+
|
|
37
|
+
key = piece.class.name.split('::').last.to_sym
|
|
38
|
+
result = PIECE_CLASS_TO_LETTER[key]
|
|
39
|
+
piece.color == :black ? result.downcase : result.upcase
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A class to represent a chess move
|
|
4
|
+
module RbChess
|
|
5
|
+
class Move
|
|
6
|
+
attr_reader :moved, :removed, :promotion, :castle
|
|
7
|
+
|
|
8
|
+
def initialize(from:, to:, removed: nil, promotion: nil, castle: nil)
|
|
9
|
+
@promotion = promotion
|
|
10
|
+
@castle = castle
|
|
11
|
+
@removed = Position.parse(removed) if removed
|
|
12
|
+
@moved = [{ from: Position.parse(from), to: Position.parse(to) }].freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_move(from:, to:)
|
|
16
|
+
@moved = [*moved, { from: Position.parse(from), to: Position.parse(to) }].freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def destination_for(from)
|
|
20
|
+
hash = moved.find { |hash| hash[:from] == from }
|
|
21
|
+
hash[:to]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
return '0-0' if castle == :kingside
|
|
26
|
+
|
|
27
|
+
return '0-0-0' if castle == :queenside
|
|
28
|
+
|
|
29
|
+
moves = moved.map { |hash| "#{hash[:from]}#{hash[:to]}#{promotion.to_s.upcase}" }
|
|
30
|
+
moves.join(',')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# frozen_string_literal: true|
|
|
4
|
+
|
|
5
|
+
# A class to represent info about how a piece moves on the body
|
|
6
|
+
module RbChess
|
|
7
|
+
class MoveSet
|
|
8
|
+
attr_accessor :increments, :repeat, :blocked_by, :promotable, :special_moves
|
|
9
|
+
|
|
10
|
+
def initialize(increments:, repeat:, blocked_by: [], special_moves: [], promotable: false)
|
|
11
|
+
@increments = increments
|
|
12
|
+
@repeat = repeat
|
|
13
|
+
@blocked_by = blocked_by
|
|
14
|
+
@special_moves = special_moves
|
|
15
|
+
@promotable = promotable
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../move_set'
|
|
4
|
+
require_relative 'piece'
|
|
5
|
+
|
|
6
|
+
# A class to represent a bishop in a chess game
|
|
7
|
+
module RbChess
|
|
8
|
+
class Bishop < Piece
|
|
9
|
+
attr_reader :move_sets
|
|
10
|
+
|
|
11
|
+
def initialize(color, position)
|
|
12
|
+
super
|
|
13
|
+
increments = [{ y: 1, x: 1 }, { y: -1, x: 1 }, { y: -1, x: -1 },
|
|
14
|
+
{ y: 1, x: -1 }]
|
|
15
|
+
@move_sets = [
|
|
16
|
+
MoveSet.new(
|
|
17
|
+
increments: increments,
|
|
18
|
+
repeat: Float::INFINITY,
|
|
19
|
+
blocked_by: [:same]
|
|
20
|
+
)
|
|
21
|
+
]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../move_set'
|
|
4
|
+
require_relative 'piece'
|
|
5
|
+
|
|
6
|
+
# A class to represent a kong in chess
|
|
7
|
+
module RbChess
|
|
8
|
+
class King < Piece
|
|
9
|
+
attr_reader :move_sets
|
|
10
|
+
|
|
11
|
+
def initialize(color, position)
|
|
12
|
+
super
|
|
13
|
+
increments = [{ y: -1, x: -1 }, { y: 1, x: 1 }, { y: 1, x: -1 },
|
|
14
|
+
{ y: -1, x: 1 }, { y: 0, x: -1 }, { y: 0, x: 1 },
|
|
15
|
+
{ y: -1, x: 0 }, { y: 1, x: 0 }]
|
|
16
|
+
@move_sets = [
|
|
17
|
+
MoveSet.new(
|
|
18
|
+
increments: increments,
|
|
19
|
+
repeat: 1,
|
|
20
|
+
blocked_by: [:same],
|
|
21
|
+
special_moves: %i[castle]
|
|
22
|
+
)
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|