bangkok 0.1.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.
@@ -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