bchess 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +65 -0
- data/Rakefile +6 -0
- data/bchess.gemspec +29 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/full_test.sh +1 -0
- data/lib/bchess.rb +33 -0
- data/lib/bchess/bishop.rb +17 -0
- data/lib/bchess/board.rb +131 -0
- data/lib/bchess/game.rb +21 -0
- data/lib/bchess/helpers/board_helpers.rb +43 -0
- data/lib/bchess/helpers/castle_helpers.rb +58 -0
- data/lib/bchess/helpers/en_passant_helpers.rb +22 -0
- data/lib/bchess/helpers/fen_helpers.rb +141 -0
- data/lib/bchess/helpers/field_between_helpers.rb +50 -0
- data/lib/bchess/helpers/validations.rb +16 -0
- data/lib/bchess/king.rb +30 -0
- data/lib/bchess/knight.rb +27 -0
- data/lib/bchess/pawn.rb +54 -0
- data/lib/bchess/piece.rb +133 -0
- data/lib/bchess/queen.rb +21 -0
- data/lib/bchess/rook.rb +25 -0
- data/lib/bchess/version.rb +3 -0
- data/lib/pgn/game.rb +18 -0
- data/lib/pgn/game_body.rb +111 -0
- data/lib/pgn/game_header.rb +55 -0
- data/lib/pgn/move_info_parser.rb +106 -0
- data/lib/pgn/parser.rb +42 -0
- data/lib/pgn/pgn_file.rb +16 -0
- data/lib/pgn/pgn_nodes.rb +84 -0
- data/lib/pgn/pgn_rules.treetop +127 -0
- data/test.sh +1 -0
- metadata +153 -0
data/lib/bchess/piece.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
module Bchess
|
2
|
+
class Piece
|
3
|
+
attr_reader :color, :column, :row
|
4
|
+
|
5
|
+
include BoardHelpers
|
6
|
+
include FieldBetweenHelpers
|
7
|
+
|
8
|
+
def initialize(color, column, row)
|
9
|
+
@color = color
|
10
|
+
@column = column
|
11
|
+
@row = row
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
name + field(column, row)
|
16
|
+
end
|
17
|
+
|
18
|
+
def moved
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
def name
|
23
|
+
raise Exception.new("Should be defined in subclass")
|
24
|
+
end
|
25
|
+
|
26
|
+
def at?(dcolumn, drow)
|
27
|
+
column == dcolumn && row == drow
|
28
|
+
end
|
29
|
+
|
30
|
+
def move(dcolumn, drow)
|
31
|
+
@column = dcolumn
|
32
|
+
@row = drow
|
33
|
+
end
|
34
|
+
|
35
|
+
def can_make_move?(info, board)
|
36
|
+
row = to_row(info[:row])
|
37
|
+
column = to_column(info[:column])
|
38
|
+
|
39
|
+
board.at(column, row)&.color != color &&
|
40
|
+
kind_of?(info[:piece_type]) &&
|
41
|
+
info[:piece_color] == color &&
|
42
|
+
can_move_or_take?(column, row, board) &&
|
43
|
+
additional_info?(info[:additional_info])
|
44
|
+
end
|
45
|
+
|
46
|
+
def can_move_or_take?(column, row, board)
|
47
|
+
clear_path?(column, row, board) &&
|
48
|
+
(
|
49
|
+
can_move?(column, row) ||
|
50
|
+
can_take?(column, row, board) ||
|
51
|
+
can_en_passant?(column, row, board)
|
52
|
+
) && valid_position_after?(column, row, board)
|
53
|
+
end
|
54
|
+
|
55
|
+
def clear_path?(column, row, board)
|
56
|
+
fields_between(column, row).none?{|f| board.at(*f)}
|
57
|
+
end
|
58
|
+
|
59
|
+
def can_move?(column, row)
|
60
|
+
can_move_to_field?(column, row)
|
61
|
+
end
|
62
|
+
|
63
|
+
def can_take?(column, row, board, en_passant = false)
|
64
|
+
can_take_on_field?(column, row) && (!!board.at(column, row) || en_passant)
|
65
|
+
end
|
66
|
+
|
67
|
+
def can_en_passant?(column, row, board)
|
68
|
+
board.en_passant == field(column, row) && can_take?(column, row, board, true)
|
69
|
+
end
|
70
|
+
|
71
|
+
def valid_position_after?(dcolumn, drow, board)
|
72
|
+
fen = board.write_fen
|
73
|
+
|
74
|
+
helper_board = Bchess::Board.new(fen)
|
75
|
+
helper_board.read_fen
|
76
|
+
helper_piece = helper_board.at(column, row)
|
77
|
+
helper_board.move(helper_piece, dcolumn, drow, Bchess::Queen) # TODO
|
78
|
+
end
|
79
|
+
|
80
|
+
def additional_info?(info)
|
81
|
+
info.nil? || to_column(info) == column || to_row(info) == row
|
82
|
+
end
|
83
|
+
|
84
|
+
def can_move_to_field?(dcolumn, drow)
|
85
|
+
!(column == dcolumn && drow == row) &&
|
86
|
+
(0..7).include?(dcolumn) && (0..7).include?(drow)
|
87
|
+
end
|
88
|
+
|
89
|
+
def can_take_on_field?(dcolumn, drow)
|
90
|
+
can_move_to_field?(dcolumn, drow)
|
91
|
+
end
|
92
|
+
|
93
|
+
def can_be_promoted?
|
94
|
+
false
|
95
|
+
end
|
96
|
+
|
97
|
+
def valid?
|
98
|
+
(0..7).include?(column) && (0..7).include?(row)
|
99
|
+
end
|
100
|
+
|
101
|
+
def by_line(dcolumn, drow, range)
|
102
|
+
same_row?(drow) && (column - dcolumn).abs <= range ||
|
103
|
+
same_column?(dcolumn) && (row - drow).abs <= range
|
104
|
+
end
|
105
|
+
|
106
|
+
def by_diagonal(dcolumn, drow, range)
|
107
|
+
same_diagonal?(dcolumn, drow) &&
|
108
|
+
(column - dcolumn).abs <= range
|
109
|
+
end
|
110
|
+
|
111
|
+
def white?
|
112
|
+
color == Bchess::WHITE
|
113
|
+
end
|
114
|
+
|
115
|
+
def black?
|
116
|
+
color == Bchess::BLACK
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def same_row?(drow)
|
122
|
+
row == drow
|
123
|
+
end
|
124
|
+
|
125
|
+
def same_column?(dcolumn)
|
126
|
+
column == dcolumn
|
127
|
+
end
|
128
|
+
|
129
|
+
def same_diagonal?(dcolumn, drow)
|
130
|
+
(column - dcolumn).abs == (row - drow).abs
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/bchess/queen.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Bchess
|
2
|
+
class Queen < Piece
|
3
|
+
QUEEN_REACH = 7
|
4
|
+
|
5
|
+
def initiialize(*args)
|
6
|
+
super(args)
|
7
|
+
end
|
8
|
+
|
9
|
+
def name
|
10
|
+
'Q'
|
11
|
+
end
|
12
|
+
|
13
|
+
def can_move_to_field?(dcolumn, drow)
|
14
|
+
super &&
|
15
|
+
(
|
16
|
+
by_line(dcolumn, drow, QUEEN_REACH) ||
|
17
|
+
by_diagonal(dcolumn, drow, QUEEN_REACH)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/bchess/rook.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Bchess
|
2
|
+
class Rook < Piece
|
3
|
+
ROOK_REACH = 7
|
4
|
+
|
5
|
+
attr_accessor :moved
|
6
|
+
|
7
|
+
def initiialize(*args)
|
8
|
+
super(args)
|
9
|
+
@moved = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
'R'
|
14
|
+
end
|
15
|
+
|
16
|
+
def move(dcolumn, drow)
|
17
|
+
super(dcolumn, drow)
|
18
|
+
@moved = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def can_move_to_field?(dcolumn, drow)
|
22
|
+
super && by_line(dcolumn, drow, ROOK_REACH)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/pgn/game.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Bchess
|
2
|
+
module PGN
|
3
|
+
class Game
|
4
|
+
attr_accessor :header, :body, :moves
|
5
|
+
|
6
|
+
def initialize(parsed_game)
|
7
|
+
@header = Bchess::PGN::GameHeader.new(parsed_game.elements.first)
|
8
|
+
@body = Bchess::PGN::GameBody.new(parsed_game.elements.last)
|
9
|
+
end
|
10
|
+
|
11
|
+
def convert_body_to_moves
|
12
|
+
body.extract_moves
|
13
|
+
@moves = body.moves
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Bchess
|
2
|
+
module PGN
|
3
|
+
class GameBody
|
4
|
+
attr_reader :body, :moves, :board
|
5
|
+
|
6
|
+
include BoardHelpers
|
7
|
+
|
8
|
+
C_COLUMN = 2
|
9
|
+
G_COLUMN = 6
|
10
|
+
|
11
|
+
WHITE_ROW = 0
|
12
|
+
BLACK_ROW = 7
|
13
|
+
|
14
|
+
def initialize(body)
|
15
|
+
@body = body
|
16
|
+
@board = Bchess::Board.new
|
17
|
+
@moves = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def extract_moves
|
21
|
+
@board.read_fen
|
22
|
+
|
23
|
+
body.elements&.each do |move|
|
24
|
+
if is_castle?(move)
|
25
|
+
move_info = extract_castle(move)
|
26
|
+
elsif is_move?(move)
|
27
|
+
move_info = extract_move(move)
|
28
|
+
else
|
29
|
+
next
|
30
|
+
end
|
31
|
+
|
32
|
+
board.move(*move_info.values)
|
33
|
+
@moves << move_info
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def extract_move(move)
|
38
|
+
move_text = move.text_value
|
39
|
+
info = return_move_information(move_text)
|
40
|
+
|
41
|
+
piece = get_moving_piece(board, info)
|
42
|
+
{
|
43
|
+
piece: piece,
|
44
|
+
column: to_column(info[:column]),
|
45
|
+
row: to_row(info[:row]),
|
46
|
+
promoted_piece: info[:promoted_piece]
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def get_moving_piece(board, info)
|
53
|
+
pieces = board.get_possible_pieces(info)
|
54
|
+
|
55
|
+
if pieces.size != 1
|
56
|
+
raise Bchess::InvalidMoveException.new("Too many or too few pieces to make move: #{info} : #{pieces.size}")
|
57
|
+
end
|
58
|
+
|
59
|
+
pieces.first
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_castle(move)
|
63
|
+
move_text = move.text_value
|
64
|
+
info = return_move_information(move_text, true)
|
65
|
+
|
66
|
+
if C_COLUMN == move_text.split('-').size
|
67
|
+
column = G_COLUMN
|
68
|
+
row = is_white?(info) ? WHITE_ROW : BLACK_ROW
|
69
|
+
else
|
70
|
+
column = C_COLUMN
|
71
|
+
row = is_white?(info) ? WHITE_ROW : BLACK_ROW
|
72
|
+
end
|
73
|
+
|
74
|
+
{
|
75
|
+
piece: board.king(info[:piece_color]),
|
76
|
+
column: column,
|
77
|
+
row: row,
|
78
|
+
promoted_piece: false
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def return_move_information(move_text,castle=false)
|
83
|
+
Bchess::PGN::MoveInfoParser.new(move_text, castle).parse_move_string
|
84
|
+
end
|
85
|
+
|
86
|
+
def is_move?(move)
|
87
|
+
move.kind_of?(Sexp::PMove)
|
88
|
+
end
|
89
|
+
|
90
|
+
def is_castle?(move)
|
91
|
+
move.kind_of?(Sexp::PCastle)
|
92
|
+
end
|
93
|
+
|
94
|
+
def is_comment?(move)
|
95
|
+
move.kind_of?(Sexp::PCommentWithBracket)
|
96
|
+
end
|
97
|
+
|
98
|
+
def is_variation?(move)
|
99
|
+
move.kind_of?(Sexp::PVariation)
|
100
|
+
end
|
101
|
+
|
102
|
+
def is_white?(info)
|
103
|
+
Bchess::WHITE == info[:piece_color]
|
104
|
+
end
|
105
|
+
|
106
|
+
def promotion?(info)
|
107
|
+
info[:promoted_piece]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Bchess
|
2
|
+
module PGN
|
3
|
+
class GameHeader
|
4
|
+
attr_reader :header, :player_white, :player_black
|
5
|
+
|
6
|
+
def initialize(header)
|
7
|
+
@header = header
|
8
|
+
end
|
9
|
+
|
10
|
+
def player_white
|
11
|
+
@player_white ||= values["White"]
|
12
|
+
end
|
13
|
+
|
14
|
+
def player_black
|
15
|
+
@player_black ||= values["Black"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def elo_white
|
19
|
+
@elo_white ||= values["WhiteElo"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def elo_black
|
23
|
+
@elo_black ||= values["BlackElo"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def event
|
27
|
+
@event ||= values["Event"]
|
28
|
+
end
|
29
|
+
|
30
|
+
def site
|
31
|
+
@site ||= values["Site"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def date
|
35
|
+
@date ||= values["Date"]
|
36
|
+
end
|
37
|
+
|
38
|
+
def round
|
39
|
+
@round ||= values["Round"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def eco
|
43
|
+
@eco ||= values["Eco"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def result
|
47
|
+
@result ||= values["Result"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def values
|
51
|
+
@values ||= header.create_value_hash
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Bchess
|
2
|
+
module PGN
|
3
|
+
class MoveInfoParser
|
4
|
+
attr_accessor :info, :move, :castle, :move_string
|
5
|
+
|
6
|
+
CHECK = "+"
|
7
|
+
TAKE = "x"
|
8
|
+
TAKE_2 = ":"
|
9
|
+
MATE = "#"
|
10
|
+
PROMOTION = "="
|
11
|
+
|
12
|
+
def initialize(move, castle)
|
13
|
+
@move = move
|
14
|
+
@castle = castle
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_move_string
|
18
|
+
info = {}
|
19
|
+
info.merge!(extract_promotion)
|
20
|
+
move_split = @move.split('.')
|
21
|
+
|
22
|
+
info.merge!(basic_move_data(move_split))
|
23
|
+
return info if @castle
|
24
|
+
|
25
|
+
move_string.tr!('+x:#', '')
|
26
|
+
info.merge!(additional_info(move_string))
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def additional_info(move_string)
|
32
|
+
{
|
33
|
+
piece_type: extract_piece_type(move_string),
|
34
|
+
column: extract_column(move_string),
|
35
|
+
row: extract_row(move_string),
|
36
|
+
additional_info: extract_additional_info(move_string)
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def extract_piece_type(move_string)
|
41
|
+
if 2 == move_string.size || move_string.size && pawn_taking?(move_string)
|
42
|
+
Bchess::Pawn
|
43
|
+
else
|
44
|
+
get_piece_from_letter(move_string[0])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def pawn_taking?(move_string)
|
49
|
+
move_string[0].ord >= 'a'.ord && move_string[0].ord <= 'h'.ord
|
50
|
+
end
|
51
|
+
|
52
|
+
def extract_column(move_string)
|
53
|
+
move_string[move_string.size - 2]
|
54
|
+
end
|
55
|
+
|
56
|
+
def extract_row(move_string)
|
57
|
+
move_string[move_string.size - 1]
|
58
|
+
end
|
59
|
+
|
60
|
+
def extract_additional_info(move_string)
|
61
|
+
if (3 == move_string.size) && pawn_taking?(move_string)
|
62
|
+
move_string[0]
|
63
|
+
elsif (4 == move_string.size)
|
64
|
+
move_string[1]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_promotion
|
69
|
+
promotion = @move.split(PROMOTION)
|
70
|
+
|
71
|
+
if promotion.size == 2
|
72
|
+
@move = promotion.first
|
73
|
+
{ promoted_piece: get_piece_from_letter(promotion.last[0]) }
|
74
|
+
else
|
75
|
+
{}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def basic_move_data(move_split)
|
80
|
+
@move_string = move_split.last.strip
|
81
|
+
|
82
|
+
raise InvalidMoveException.new("Wrong move description") if move_string.size < 2
|
83
|
+
color = move_split.size == 2 ? Bchess::WHITE : Bchess::BLACK
|
84
|
+
number = move_split.first.strip if move_split.size == 2
|
85
|
+
{
|
86
|
+
piece_color: color,
|
87
|
+
move_number: number,
|
88
|
+
check: move_string.include?(CHECK),
|
89
|
+
take: move_string.include?(TAKE) || move_string.include?(TAKE_2),
|
90
|
+
mate: move_string.include?(MATE),
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def get_piece_from_letter(letter)
|
96
|
+
{
|
97
|
+
"K" => Bchess::King,
|
98
|
+
"Q" => Bchess::Queen,
|
99
|
+
"R" => Bchess::Rook,
|
100
|
+
"B" => Bchess::Bishop,
|
101
|
+
"N" => Bchess::Knight
|
102
|
+
}[letter]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|