celtic / chess

chess AI

chess / chess.rb
100755 391 lines (320 sloc) 10.526 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
#!/usr/bin/env ruby
 
module Chess
  module Player
    WHITE = 1
    BLACK = 2
 
    def self.other(colour)
      case colour
when WHITE; BLACK
when BLACK; WHITE
      end
    end
  end
 
  class Game
    COMPUTER = 1
    HUMAN = 2
    
    def initialize(white, black, engine)
      @white, @black = white, black
      @board = Chess::Board.new Chess::Engine::FirstEngine
      @board.setup
 
      @num = 1
      @white_to_play = true
      @log = ""
    end
 
    def play
      take_turn while true
    end
 
    def take_turn
      STDOUT.print(@white_to_play ? "White to move: " : "Black to move: ")
      STDOUT.flush
 
      if (@white_to_play && @white == HUMAN) or (!@white_to_play && @black == HUMAN)
begin
move = STDIN.gets.strip.downcase
if move == "quit"
puts "Bye!\n#@log"
exit
end
 
from_pos, to_pos = [move[0] - ?a, move[1] - ?1], [move[2] - ?a, move[3] - ?1]
new_board = @board.apply_move *(from_pos + to_pos)
rescue
STDOUT.print "I didn't get that move. Try again: "
STDOUT.flush
retry
end
      else
begun = Time.now
smartest_moves = @board.legal_smartest_moves(@white_to_play ? Player::WHITE : Player::BLACK)
finished = Time.now
 
puts "#{smartest_moves.length} option(s) found in #{finished - begun} second(s)"
    
# TODO check if our king is threatened, if so, it's checkmate, if not, stalemate
(puts "no moves!? mate! (or draw)\n\n#@log"; exit) if smartest_moves.length.zero?
 
new_board, from_pos, to_pos = smartest_moves.first[0]
      end
 
      this_move = "#{(from_pos[0] + ?a).chr}#{from_pos[1] + 1}#{(to_pos[0] + ?a).chr}#{to_pos[1] + 1}"
      if @white_to_play
puts(@whites_move = "#{@num}.\t#{this_move}")
@log += @whites_move
      else
puts "#{@whites_move}\t#{this_move}"
@log += "\t#{this_move}\n"
      end
 
      @board = new_board
      puts @board
 
      if @white_to_play
@white_to_play = false
      else
@white_to_play = true
@num += 1
      end
      
      gets if @white == @black and @white == Game::COMPUTER
    end
 
    attr_accessor :board
  end
 
  class Board
    def initialize engine
      @pieces = []
      @engine = engine
      metaclass = class << self; self; end
      metaclass.send 'include', engine
    end
 
    def clone_board
      b = Board.new(@engine)
      @pieces.each {|p| b.pieces << Piece.new(b, p.type, p.colour, p.x, p.y)}
      b
    end
 
    def apply_move from_x, from_y, to_x, to_y
      b = clone_board
      src = b.get(from_x, from_y)
      if src.nil?
raise "Tried to apply a move (#{from_x},#{from_y} to #{to_x},#{to_y}) but #{from_x},#{from_y} is empty ...?"
      end
      dst = b.get(to_x, to_y)
      b.pieces.delete dst if dst # good bye, mr. bond
      src.x, src.y = to_x, to_y
      b
    end
 
    # characteristic things
 
    def consistent?
      # not in check, are we?
      these_pieces(Piece::KING).each do |king|
return false if king.threatened?
      end
 
      true
    end
 
    def legal_moves colour
      my_pieces = pieces_owned_by colour
      resulting_boards = []
      my_pieces.each do |p|
p.possible_targets.each do |x, y|
new_board = self.apply_move(p.x, p.y, x, y)
resulting_boards << [new_board, [p.x, p.y], [x, y]] if new_board.consistent?
end
      end
      resulting_boards
    end
 
    def legal_smartest_moves colour
      boards = legal_moves colour
 
      boards.map! {|b, mf, mt| [[b, mf, mt], b.position(colour) - b.position(Player::other(colour))]}
      boards = boards.sort_by {|board, score| -score}
      #boards.reject! {|board, score| score != boards[0][1]}
      #keep the non-"perfect" ones, our algorithms suck
 
      boards
    end
 
    # Returns true if the path is clear from the `from_?' co-ordinates to the `to_?' ones,
    # not including the from co-ordinate itself
    def path_clear? from_x, from_y, to_x, to_y, take=false
      dx, dy = to_x - from_x, to_y - from_y
      no_of_moves = (dx == 0 ? dy : dx).abs
      dx /= no_of_moves
      dy /= no_of_moves
 
      # dx and dy are now both in terms of single square steps.
      cx, cy = from_x, from_y
      until cx == to_x and cy == to_y
cx += dx; cy += dy
return true if cx == to_x and cy == to_y and take
return false if not get(cx, cy).nil?
      end
      true
    end
 
    def take_clear? from_x, from_y, to_x, to_y
      path_clear? from_x, from_y, to_x, to_y, true
    end
 
    def to_s
      s = []
      7.downto(0) do |y|
s << (0..7).to_a.map do |x|
p = get(x, y)
p ? p.to_s : '-'
end.join(' ')
      end
      s.join("\n")
    end
 
    def inspect
      "<Board #{pieces_owned_by(Player::WHITE).length}/#{position(Player::WHITE)} vs. #{pieces_owned_by(Player::BLACK).length}/#{position(Player::BLACK)}>"
    end
 
    # administrative things
 
    def setup
      @pieces = []
      add_symmetry = lambda do |piece, left, bottom|
@pieces << Piece.new(self, piece, Player::WHITE, left, bottom)
@pieces << Piece.new(self, piece, Player::WHITE, 7 - left, bottom)
@pieces << Piece.new(self, piece, Player::BLACK, left, 7 - bottom)
@pieces << Piece.new(self, piece, Player::BLACK, 7 - left, 7 - bottom)
      end
 
      0.upto(3) {|x| add_symmetry.call(Piece::PAWN, x, 1)}
      add_symmetry.call(Piece::ROOK, 0, 0)
      add_symmetry.call(Piece::KNIGHT, 1, 0)
      add_symmetry.call(Piece::BISHOP, 2, 0)
      @pieces << Piece.new(self, Piece::QUEEN, Player::WHITE, 3, 0)
      @pieces << Piece.new(self, Piece::KING, Player::WHITE, 4, 0)
      @pieces << Piece.new(self, Piece::QUEEN, Player::BLACK, 3, 7)
      @pieces << Piece.new(self, Piece::KING, Player::BLACK, 4, 7)
    end
 
    def get x, y
      @pieces.find {|p| p.x == x && p.y == y}
    end
 
    def pieces_owned_by colour
      @pieces.find_all {|p| p.colour == colour}
    end
 
    def these_pieces type
      @pieces.find_all {|p| p.type == type}
    end
 
    def these_pieces_owned_by colour, type
      @pieces.find_all {|p| p.colour == colour && p.type == type}
    end
 
    def calculate_threats
      @pieces.each {|p| p.calculate_threats}
    end
 
    attr_accessor :pieces
  end
 
  class Piece
    PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING = *1..6
    DISPLAY_FORMS = %w{P N B R Q K}
 
    def initialize(board, type, colour, x, y)
      @board = board
      @type, @colour = type, colour
      @x, @y = x, y
      @threatening, @threatened_by = [], []
      @calculated_threats = false
    end
 
    def threatened?
      @board.calculate_threats unless @calculated_threats
      not @threatened_by.length.zero?
    end
 
    # return a list of co-ordinates we can move to at this state
    def possible_targets
      targets = list_possible_targets
      return targets unless not @calculated_threats
 
      calculate_threats(targets)
 
      targets
    end
 
    def calculate_threats(targets=nil)
      return if @calculated_threats
      targets = list_possible_targets if targets.nil?
 
      targets.each do |x,y|
t = @board.get(x, y)
if t && t.colour != @colour
@threatening << t
t.threatened_by << self
end
      end
      @calculated_threats = true
    end
 
    def list_possible_targets
      case @type
when PAWN
direction, home = {Player::WHITE => [1, 1], Player::BLACK => [-1, 6]}[@colour]
targets = if @y == home
[[@x, @y + direction], [@x, @y + 2 * direction]]
else
[[@x, @y + direction]]
end.reject {|x,y,_| !@board.path_clear?(@x, @y, x, y)}
 
# TODO here: pawn en passant
 
# how about takes?
take_left = @board.get(@x - 1, @y + direction)
take_right = @board.get(@x + 1, @y + direction)
targets << [@x - 1, @y + direction] if take_left && take_left.colour != @colour
targets << [@x + 1, @y + direction] if take_right && take_right.colour != @colour
 
#puts "pawn #@x,#@y moves: #{targets.inspect}"
targets
 
when ROOK
xs = (0..(@x - 1)).to_a + ((@x + 1)..7).to_a
ys = (0..(@y - 1)).to_a + ((@y + 1)..7).to_a
targets = xs.map {|x| [x, @y]} + ys.map {|y| [@x, y]}
targets.reject! {|x,y| !@board.path_clear?(@x, @y, x, y) && !(@board.take_clear?(@x, @y, x, y) && @board.get(x, y).colour != @colour)}
 
#puts "rook #@x,#@y moves: #{targets.inspect}"
targets
 
when KNIGHT
targets = [[@x + 1, @y + 2], [@x + 2, @y + 1], [@x + 2, @y - 1], [@x + 1, @y - 2],
[@x - 1, @y - 2], [@x - 2, @y - 1], [@x - 2, @y + 1], [@x - 1, @y + 2]]
targets.reject! {|x,y| !(0..7).include?(x) || !(0..7).include?(y) || (@board.get(x, y) && @board.get(x, y).colour == @colour)}
 
#puts "knight #@x,#@y moves: #{targets.inspect}"
targets
 
when BISHOP
targets = []
[-1, 1].each do |dx|
[-1, 1].each do |dy|
cx, cy = @x + dx, @y + dy
while (0..7).include?(cx) and (0..7).include?(cy)
targets << [cx, cy]
cx += dx; cy += dy
end
end
end
targets.reject! {|x,y| !@board.path_clear?(@x, @y, x, y) && !(@board.take_clear?(@x, @y, x, y) && @board.get(x, y).colour != @colour)}
 
#puts "bishop #@x,#@y moves: #{targets.inspect}"
targets
 
when QUEEN
xs = (0..(@x - 1)).to_a + ((@x + 1)..7).to_a
ys = (0..(@y - 1)).to_a + ((@y + 1)..7).to_a
targets = xs.map {|x| [x, @y]} + ys.map {|y| [@x, y]}
 
[-1, 1].each do |dx|
[-1, 1].each do |dy|
cx, cy = @x + dx, @y + dy
while (0..7).include?(cx) and (0..7).include?(cy)
targets << [cx, cy]
cx += dx; cy += dy
end
end
end
targets.reject! {|x,y| !@board.path_clear?(@x, @y, x, y) && !(@board.take_clear?(@x, @y, x, y) && @board.get(x, y).colour != @colour)}
 
#puts "queen #@x,#@y moves: #{targets.inspect}"
targets
 
when KING
targets = []
[-1, 0, 1].each do |dx|
[-1, 0, 1].each do |dy|
next if (dx == dy && dy == 0) or !(0..7).include?(@x + dx) or !(0..7).include?(@y + dy)
targets << [@x + dx, @y + dy] if @board.get(@x + dx, @y + dy).nil? or @board.get(@x + dx, @y + dy).colour != @colour
end
end
 
#puts "king #@x,#@y moves: #{targets.inspect}"
targets
 
else
raise NotImplementedError, "we don't know how to handle this piece (#@type) in `possible_targets'!"
      end
    end
 
    def to_s
      df = DISPLAY_FORMS[@type - 1]
      if @colour == Player::BLACK
df.downcase
      else
df.upcase
      end
    end
 
    def inspect
      "(#{to_s} #{x},#{y})"
    end
 
    attr_reader :board
    attr_accessor :type
    attr_accessor :colour
    attr_accessor :x
    attr_accessor :y
 
    attr_accessor :threatening
    attr_accessor :threatened_by
  end
end
 
require 'first_engine'
game = Chess::Game.new(Chess::Game::COMPUTER, Chess::Game::COMPUTER, Chess::Engine::FirstEngine)
puts game.board
game.play