bangkok 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ VERSION_MAJOR = 0
2
+ VERSION_MINOR = 1
3
+ VERSION_TWEAK = 0
4
+
5
+ Version = "#{VERSION_MAJOR}.#{VERSION_MINOR}.#{VERSION_TWEAK}"
6
+ Copyright = 'Copyright (c) 2005 by Jim Menard <jimm@io.com>'
@@ -0,0 +1,106 @@
1
+ require 'bangkok/square'
2
+
3
+
4
+ # A Move is a single piece's move in a chess match. There are two Move objects
5
+ # created for each chess match move: one for white and one for black.
6
+ class Move
7
+ attr_reader :color, :piece, :square, :from_rank_or_file, :modifier
8
+
9
+ # Parse the chess piece move +text+ and set piece, square, and modifier.
10
+ def initialize(color, text)
11
+ @color = color # :white or :black
12
+ @orig_text = text
13
+
14
+ # Note: I don't have to worry about "e.p." en passant notation; the data
15
+ # files do not use that.
16
+ case text
17
+ when 'O-O-O', 'O-O', '0-0-0', '0-0'
18
+ @modifier = text.gsub(/0/, 'O') # zeroes to capital o's
19
+ @piece = 'K'
20
+ @square = Square::OFF_BOARD # Not really, of course; square is never used
21
+ when /^([KQRBNa-h1-8])x([a-h][1-8])(.*)$/
22
+ piece_or_from_rank_or_file, file_and_rank, @modifier = $1, $2, $3
23
+ @square = Square.new(file_and_rank)
24
+ case piece_or_from_rank_or_file
25
+ when /[a-h1-8]/ # first char is file or rank; it's a pawn
26
+ @piece = 'P'
27
+ @from_rank_or_file = Square.new(piece_or_from_rank_or_file)
28
+ else # first char is piece name
29
+ @piece = piece_or_from_rank_or_file
30
+ end
31
+ @modifier ||= ''
32
+ @modifier << 'x'
33
+ when /^([KQRBN]?)([a-h1-8]?)([a-h][1-8])(.*)$/
34
+ @piece, from_rank_or_file, file_and_rank, @modifier = $1, $2, $3, $4
35
+ @square = Square.new(file_and_rank)
36
+ unless from_rank_or_file.empty?
37
+ @from_rank_or_file = Square.new(from_rank_or_file)
38
+ end
39
+ else
40
+ raise "I can't understand the move \"#{@orig_text}\""
41
+ end
42
+
43
+ @piece = 'P' if @piece.empty?
44
+ @piece = @piece.intern
45
+ end
46
+
47
+ def to_s
48
+ return @orig_text
49
+ end
50
+
51
+ # Returns true if @modifier is not null and includes +str+.
52
+ def has_modifier?(str)
53
+ return false unless @modifier
54
+ return @modifier.include?(str)
55
+ end
56
+
57
+ # Returns true if this is a capture
58
+ def capture?
59
+ has_modifier?('x')
60
+ end
61
+
62
+ # Returns true if this is a castle (either side)
63
+ def castle?
64
+ has_modifier?('O-O') # Also true if O-O-O
65
+ end
66
+
67
+ # Returns true if this is a queenside castle
68
+ def queenside_castle?
69
+ has_modifier?('O-O-O')
70
+ end
71
+
72
+ # Returns true if this is a kingside castle
73
+ def kingside_castle?
74
+ has_modifier?('O-O') && !queenside_castle?
75
+ end
76
+
77
+ # Returns true if this move results in a pawn promotion
78
+ def pawn_promotion?
79
+ has_modifier?('Q')
80
+ end
81
+
82
+ # Returns true if this move results in a check
83
+ def check?
84
+ has_modifier?('+')
85
+ end
86
+
87
+ # Returns true if this move results in a checkmate
88
+ def checkmate?
89
+ has_modifier?('#')
90
+ end
91
+
92
+ # Returns true if this move was a good one
93
+ def good_move?
94
+ has_modifier?('!')
95
+ end
96
+
97
+ # Returns true if this move was a bad one
98
+ def bad_move?
99
+ has_modifier?('?') && !blunder?
100
+ end
101
+
102
+ # Returns true if this move was a blunder
103
+ def blunder?
104
+ has_modifier?('??')
105
+ end
106
+ end
@@ -0,0 +1,243 @@
1
+ require 'bangkok/square'
2
+
3
+ class Piece
4
+ attr_reader :color, :piece, :square
5
+
6
+ # Factory method that creates a piece of the proper subclass
7
+ def Piece.create(board, listener, color, piece_sym, square)
8
+ return case piece_sym
9
+ when :K
10
+ King.new(board, listener, color, square)
11
+ when :Q
12
+ Queen.new(board, listener, color, square)
13
+ when :B
14
+ Bishop.new(board, listener, color, square)
15
+ when :N
16
+ Knight.new(board, listener, color, square)
17
+ when :R
18
+ Rook.new(board, listener, color, square)
19
+ when :P
20
+ Pawn.new(board, listener, color, square)
21
+ end
22
+ end
23
+
24
+ def initialize(board, listener, color, piece, square)
25
+ @board, @listener, @color, @piece, @square =
26
+ board, listener, color, piece, square
27
+ end
28
+
29
+ def move_to(square)
30
+ puts "#{self} moving to #{square}" if $verbose
31
+ @listener.move(self, @square, square)
32
+ @square = square
33
+ end
34
+
35
+ def move_off_board
36
+ return move_to(Square::OFF_BOARD)
37
+ end
38
+
39
+ # Make sure this piece can perform +move+. This implementation checks the
40
+ # basics (the color and type of this piece and the emptiness or color of the
41
+ # piece at the destination square); subclasses add further checks.
42
+ def could_perform_move(move)
43
+ p = @board.at(move.square)
44
+ return @color == move.color && piece == move.piece &&
45
+ (p.nil? || p.color != @color)
46
+ end
47
+
48
+ # Checks diagonals and straight horizontal/vertical lines. Won't work
49
+ # correctly for anything else.
50
+ def clear_to?(square)
51
+ curr_file = @square.file
52
+ end_file = square.file
53
+ file_delta = @square.file < square.file ? 1 :
54
+ (@square.file == square.file ? 0 : -1)
55
+ curr_file += file_delta unless curr_file == end_file # Skip current loc
56
+
57
+ curr_rank = @square.rank
58
+ end_rank = square.rank
59
+ rank_delta = @square.rank < square.rank ? 1 :
60
+ (@square.rank == square.rank ? 0 : -1)
61
+ curr_rank += rank_delta unless curr_rank == end_rank # Skip current loc
62
+
63
+ if file_delta == 0 && rank_delta == 0
64
+ raise "error: trying to move to same space #{file}#{rank}"
65
+ end
66
+
67
+ while curr_file != end_file || curr_rank != end_rank
68
+ return false unless @board.empty_at?(Square.new(curr_file, curr_rank))
69
+ curr_file += file_delta
70
+ curr_rank += rank_delta
71
+ end
72
+
73
+ return true
74
+ end
75
+
76
+ def to_s
77
+ str = "#{@color.to_s.capitalize} #@piece "
78
+ str << "at " if @square.on_board?
79
+ str << @square.to_s
80
+ str
81
+ end
82
+ end
83
+
84
+ class King < Piece
85
+ def initialize(board, listener, color, square)
86
+ super(board, listener, color, :K, square)
87
+ end
88
+
89
+ # There is no King#could_move_to method because it would only be called if
90
+ # there were more than one King of the same color on the board, which can
91
+ # not happen.
92
+ end
93
+
94
+ class Queen < Piece
95
+ def initialize(board, listener, color, square)
96
+ super(board, listener, color, :Q, square)
97
+ end
98
+
99
+ # There can be more than one queen on the board, thus this method must be
100
+ # implemented.
101
+ def could_perform_move(move)
102
+ return false unless super
103
+
104
+ # Check for horizontal or vertical
105
+ square = move.square
106
+ if @square.file == square.file || @square.rank == square.rank
107
+ return clear_to?(square)
108
+ end
109
+
110
+ # Check for diagonal
111
+ return false unless square.color == @square.color
112
+ d_file = (@square.file - square.file).abs
113
+ d_rank = (@square.rank - square.rank).abs
114
+ return false unless d_file == d_rank # diagonal
115
+ return clear_to?(square)
116
+ end
117
+ end
118
+
119
+ class Bishop < Piece
120
+ def initialize(board, listener, color, square)
121
+ super(board, listener, color, :B, square)
122
+ end
123
+
124
+ def could_perform_move(move)
125
+ return false unless super
126
+
127
+ # Quick square color check
128
+ square = move.square
129
+ return false unless square.color == @square.color
130
+
131
+ d_file = (@square.file - square.file).abs
132
+ d_rank = (@square.rank - square.rank).abs
133
+ return false unless d_file == d_rank # diagonal
134
+ return clear_to?(square)
135
+ end
136
+ end
137
+
138
+ class Knight < Piece
139
+ def initialize(board, listener, color, square)
140
+ super(board, listener, color, :N, square)
141
+ end
142
+
143
+ def could_perform_move(move)
144
+ return false unless super
145
+
146
+ square = move.square
147
+ d_file = (@square.file - square.file).abs
148
+ d_rank = (@square.rank - square.rank).abs
149
+ return (d_file == 2 && d_rank == 1) || (d_file == 1 && d_rank == 2)
150
+ end
151
+ end
152
+
153
+ class Rook < Piece
154
+ def initialize(board, listener, color, square)
155
+ super(board, listener, color, :R, square)
156
+ end
157
+
158
+ def could_perform_move(move)
159
+ return false unless super
160
+
161
+ square = move.square
162
+ return false if square.file != @square.file && square.rank != @square.rank
163
+ return clear_to?(square)
164
+ end
165
+ end
166
+
167
+ class Pawn < Piece
168
+ attr_reader :moved
169
+
170
+ def initialize(board, listener, color, square)
171
+ super(board, listener, color, :P, square)
172
+ @moved = false
173
+ end
174
+
175
+ def move_to(square)
176
+ @moved = true
177
+ return super
178
+ end
179
+
180
+ def could_perform_move(move)
181
+ return false unless super
182
+
183
+ square = move.square
184
+ if @color == :white
185
+ if square.file == @square.file && square.rank == @square.rank + 1
186
+ # single step forwards
187
+ return @board.empty_at?(square)
188
+ elsif square.file + 1 == @square.file && square.rank == @square.rank + 1
189
+ # take a piece queenside
190
+ return !@board.empty_at?(square)
191
+ elsif square.file == @square.file + 1 && square.rank == @square.rank + 1
192
+ # take a piece kingside
193
+ return !@board.empty_at?(square)
194
+ elsif !@moved && square.file == @square.file &&
195
+ square.rank == @square.rank + 2
196
+ # first move: 2 squares forward
197
+ return @board.empty_at?(Square.new(@square.file, @square.rank + 1)) &&
198
+ @board.empty_at?(square)
199
+
200
+ # TODO Implement en passant checking. The following code was wrong.
201
+
202
+ # elsif !@moved && square.file + 1 == @square.file &&
203
+ # square.rank == @square.rank + 2
204
+ # # e.p. queenside
205
+ # return !@board.empty_at?(square)
206
+ # elsif !@moved && square.file == @square.file + 1 &&
207
+ # square.rank == @square.rank + 2
208
+ # # e.p. kingside
209
+ # return !@board.empty_at?(square)
210
+
211
+ end
212
+ else
213
+ # black
214
+ if square.file == @square.file && square.rank == @square.rank - 1
215
+ # single step forwards
216
+ return @board.empty_at?(square)
217
+ elsif square.file + 1 == @square.file && square.rank == @square.rank - 1
218
+ # take a piece queenside
219
+ return !@board.empty_at?(square)
220
+ elsif square.file == @square.file + 1 && square.rank == @square.rank - 1
221
+ # take a piece kingside
222
+ return !@board.empty_at?(square)
223
+ elsif !@moved && square.file == @square.file &&
224
+ square.rank == @square.rank - 2
225
+ # first move: 2 spaces forward
226
+ return @board.empty_at?(Square.new(@square.file, @square.rank - 1)) &&
227
+ @board.empty_at?(square)
228
+
229
+ # TODO Implement en passant checking. The following code was wrong.
230
+
231
+ # elsif !@moved && square.file + 1 == @square.file &&
232
+ # square.rank == @square.rank - 2
233
+ # # en passant queenside
234
+ # return !@board.empty_at?(square)
235
+ # elsif !@moved && square.file == @square.file + 1 &&
236
+ # square.rank == @square.rank - 2
237
+ # # en passant kingside
238
+ # return !@board.empty_at?(square)
239
+
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,47 @@
1
+ # Represents a location on the board. Immutable.
2
+ class Square
3
+ attr_reader :file, :rank # Always 0-7
4
+ attr_reader :color # :white or :black
5
+
6
+ def initialize(*args)
7
+ @file = @rank = @color = nil
8
+ case args[0]
9
+ when Square # copy values from another Square
10
+ @file = args[0].file
11
+ @rank = args[0].rank
12
+ @color = args[0].color
13
+ when Numeric # (file number, rank number)
14
+ @file = args[0].to_i # If floating point, make integer
15
+ @rank = args[1].to_i
16
+ @color = (((@file & 1) == (@rank & 1)) ? :black : :white) if on_board?
17
+ when /[a-h][1-8]/ # a3, d8
18
+ @file = args[0][0] - ?a
19
+ @rank = args[0][1,1].to_i - 1
20
+ @color = ((@file & 1) == (@rank & 1)) ? :black : :white
21
+ when /[a-h]/ # Either (file letter, rank) or a file 'a'
22
+ @file = args[0][0] - ?a
23
+ @rank = (args[1].to_i - 1) if args[1]
24
+ when /[1-8]/ # 1, 5
25
+ @rank = args[0].to_i - 1
26
+ when nil
27
+ # both file and rank are nil
28
+ else
29
+ raise "don't understand Square ctor args (#{args.join(',')})"
30
+ end
31
+ end
32
+
33
+ def ==(square)
34
+ @file == square.file && @rank == square.rank
35
+ end
36
+
37
+ def on_board?
38
+ return @file && @rank
39
+ end
40
+
41
+ def to_s
42
+ return "<off-board>" if @file.nil? || @rank.nil?
43
+ return "#{@file ? (?a + file).chr : ''}#{@rank + 1}"
44
+ end
45
+
46
+ OFF_BOARD = Square.new
47
+ end
@@ -0,0 +1,22 @@
1
+ class MockGameListener
2
+ def start_game
3
+ end
4
+
5
+ def end_game
6
+ end
7
+
8
+ def move(piece, from, to)
9
+ end
10
+
11
+ def capture(attacker, loser)
12
+ end
13
+
14
+ def check
15
+ end
16
+
17
+ def checkmate
18
+ end
19
+
20
+ def pawn_to_queen(pawn)
21
+ end
22
+ end
@@ -0,0 +1,106 @@
1
+ require 'test/unit'
2
+ $LOAD_PATH[0, 0] = File.dirname(__FILE__)
3
+ $LOAD_PATH[0, 0] = File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'bangkok/piece'
5
+ require 'bangkok/square'
6
+ require 'bangkok/move'
7
+ require 'bangkok/board'
8
+ require 'mock_game_listener'
9
+
10
+ class PieceTest < Test::Unit::TestCase
11
+
12
+ def setup
13
+ @listener = MockGameListener.new
14
+ @board = Board.new(@listener)
15
+ @w_pawn = @board.at(Square.new('a2'))
16
+ @b_pawn = @board.at(Square.new('e7'))
17
+ end
18
+
19
+ def test_create
20
+ assert_instance_of(King, Piece.create(@board, @listener, :white, :K,
21
+ Square::OFF_BOARD))
22
+ assert_instance_of(Queen, Piece.create(@board, @listener, :white, :Q,
23
+ Square::OFF_BOARD))
24
+ assert_instance_of(Bishop, Piece.create(@board, @listener, :white, :B,
25
+ Square::OFF_BOARD))
26
+ assert_instance_of(Knight, Piece.create(@board, @listener, :white, :N,
27
+ Square::OFF_BOARD))
28
+ assert_instance_of(Rook, Piece.create(@board, @listener, :white, :R,
29
+ Square::OFF_BOARD))
30
+ assert_instance_of(Pawn, Piece.create(@board, @listener, :white, :P,
31
+ Square::OFF_BOARD))
32
+ assert_nil(Piece.create(@board, @listener, :white, nil, Square::OFF_BOARD))
33
+ end
34
+
35
+ def test_moved
36
+ assert(!@w_pawn.moved)
37
+ @w_pawn.move_to(Square.new('a4'))
38
+ assert(@w_pawn.moved)
39
+ end
40
+
41
+ def test_move_to
42
+ assert_equal(Square.new('a2'), @w_pawn.square)
43
+
44
+ dest = Square.new('b3')
45
+ @w_pawn.move_to(dest)
46
+ assert_equal(dest, @w_pawn.square)
47
+
48
+ dest = Square::OFF_BOARD
49
+ @w_pawn.move_off_board
50
+ assert_equal(dest, @w_pawn.square)
51
+ end
52
+
53
+ def test_pawn_could_move_to
54
+ assert(@w_pawn.could_perform_move(Move.new(:white, 'a3')))
55
+ assert(@w_pawn.could_perform_move(Move.new(:white, 'a4')))
56
+
57
+ diag = Move.new(:white, 'b3')
58
+ assert(!@w_pawn.could_perform_move(diag)) # no piece there
59
+
60
+ # put a black piece there so we can capture it
61
+ @board.at(Square.new('d8')).move_to(Square.new('b3'))
62
+ assert(@w_pawn.could_perform_move(diag)) # now we can capture it
63
+
64
+ assert(@b_pawn.could_perform_move(Move.new(:black, 'e6')))
65
+ assert(@b_pawn.could_perform_move(Move.new(:black, 'e5')))
66
+
67
+ diag = Move.new(:black, 'd6')
68
+ assert(!@b_pawn.could_perform_move(diag)) # no piece there
69
+
70
+ # put a white piece there so we can capture it
71
+ @board.at(Square.new('d1')).move_to(Square.new('d6'))
72
+ assert(@b_pawn.could_perform_move(diag)) # now we can capture it
73
+ end
74
+
75
+ def test_pawn_en_passant
76
+ # Warning: en passant is not handled yet by the code
77
+ end
78
+
79
+ def test_rook_could_move_to
80
+ rook = @board.at(Square.new('a8')) # black rook
81
+ assert(!rook.could_perform_move(Move.new(:black, 'Ra7')))
82
+ @board.at(Square.new('a7')).move_off_board # remove pawn in front
83
+ (3..7).each { | rank |
84
+ assert(rook.could_perform_move(Move.new(:black, 'Ra' + rank.to_s)))
85
+ }
86
+
87
+ assert_not_nil(@board.at(Square.new('a2')))
88
+ assert(rook.could_perform_move(Move.new(:black, 'Ra2'))) # can capture
89
+
90
+ assert_not_nil(@board.at(Square.new('a1')))
91
+ assert(!rook.could_perform_move(Move.new(:black, 'Ra1'))) # can't get there
92
+ end
93
+
94
+ def test_knight_could_move_to
95
+ end
96
+
97
+ def test_bishop_could_move_to
98
+ end
99
+
100
+ def test_queen_could_move_to
101
+ end
102
+
103
+ def test_king_could_move_to
104
+ end
105
+
106
+ end