#!/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