Here is my solution. It is not fully designed to support variants and
I don't know if I will have time to adapt it (it seems to me a highly
nontrivial task).
Best regards,
Paolo Capriotti
···
---
class Numeric
def sign
if self > 0
1
else
if self < 0 then -1 else 0 end
end
end
end
class Piece
attr_reader :type, :color
def initialize(color, type)
@type = type
@color = color
end
def type_initial
if @type == :knight
'n'
else
@type.to_s[0,1]
end
end
def ==(other)
return false unless other.respond_to?(:color)
(@type == other.type and @color == other.color)
end
end
class Vector
attr_reader :x, :y
def initialize(x,y)
@x,@y = x,y
end
def +(other)
x = @x + other[0]
y = @y + other[1]
Vector.new(x,y)
end
def -(other)
x = @x - other[0]
y = @y - other[1]
Vector.new(x,y)
end
def *(number)
x = @x * number
y = @y * number
Vector.new(x,y)
end
def ==(other)
return false unless other.respond_to?(:[])
@x == other[0] and @y == other[1]
end
def [](i)
case i
when 0
@x
when 1
@y
end
end
def clone
Vector.new(@x,@y)
end
end
class Board
attr_reader :size, :turn
attr_writer :promotion_piece
def initialize(size_x, size_y)
@size = Vector.new(size_x, size_y)
@board = []
(0...size_x).each do |x|
@board[x] = []
end
@turn = :white
@king_moved = Hash.new(false)
@queen_rook_moved = Hash.new(false)
@king_rook_moved = Hash.new(false)
end
def [](*position)
position = get_pos(position)
@board[position.x][position.y]
end
def []=(*args)
value = args.pop
position = get_pos(args)
@board[position.x][position.y] = value
self
end
def move(from, to)
piece = self[from]
execute_move(from, to)
case from
when king_starting_position
@king_moved[@turn] = true
when king_rook_starting_position
@king_rook_moved[@turn] = true
when queen_rook_starting_position
@queen_rook_moved[@turn] = true
end
if castling(from, to)
if to.x - from.x > 0
self[from+[1,0]] = self[king_rook_starting_position]
self[king_rook_starting_position] = nil
else
self[from+[-1,0]] = self[queen_rook_starting_position]
self[queen_rook_starting_position] = nil
end
end
if to == @en_passant
self[to-[0,pawn_dir]] = nil
end
if piece.type == :pawn and (to.y - from.y) == 2
@en_passant = to-[0,pawn_dir]
else
@en_passant = nil
end
switch_turn
end
def pseudolegal_move(from, to)
return false if from == to
piece = self[from]
return false unless piece
return false unless piece.color == turn
return false if self[to] and self[to].color == piece.color
d = to - from
case piece.type
when :king
if (d.x.abs <= 1 and d.y.abs <= 1)
return true
end
if castling(from, to)
return false if @king_moved[@turn]
if d.x > 0 # king side
return false if @king_rook_moved[@turn]
return false unless (self[from + [1,0]] == nil or self[from
+ [2,0]] == nil)
else
return false if @queen_rook_moved[@turn]
return false unless (from + [-1,0] == nil or from + [-2,0]
== nil or from + [-3,0] == nil)
end
return true
end
when :queen
return ((rook_slide(d) or bishop_slide(d)) and check_free_path(from, to))
when :rook
return (rook_slide(d) and check_free_path(from, to))
when :bishop
return (bishop_slide(d) and check_free_path(from, to))
when :knight
return knight_jump(d)
when :pawn
case d.x.abs
when 1
return (d.y == pawn_dir and ((self[to] and (not self[to].color
== piece.color)) or to == @en_passant))
when 0
case d.y
when pawn_dir
return self[to] == nil
when pawn_dir*2
return (from.y == rank(2) and self[from+[0,pawn_dir]] == nil
and self[to] == nil)
else
return false
end
else
return false
end
end
end
def legal_move(from, to)
return false unless pseudolegal_move(from, to)
old_turn = @turn
switch_turn
res = check_legality(from, to, old_turn)
@turn = old_turn
return res
end
def is_valid(*args)
position = get_pos(args)
position.x >= 0 and position.x < size.x and position.y >= 0 and
position.y < size.y
end
def find_piece(piece)
each_piece do |position, p|
return position if piece == p
end
return nil
end
def each_piece
(0...size.x).each do |x|
(0...size.y).each do |y|
pos = Vector.new(x,y)
piece = self[pos]
yield(Vector.new(x,y), self[x,y]) if self[x,y]
end
end
end
def show
(0...@size.y).each do |y|
(0...@size.x).each do |x|
piece = self[x,y]
if piece
s = piece.type_initial
s.upcase! if (piece.color == :white)
print s, ' '
else
print ' '
end
end
print "\n"
end
print "\n"
end
def promotion(from, to)
to.y == rank(8) and self[from].type == :pawn
end
def capturing(from, to)
return true if self[to]
self[from].type == :pawn and (not from.x == to.x)
end
def game_state
generator = MoveGenerator.new(self)
if generator.unstalled
return :in_game
else
old_turn = @turn
switch_turn
if leaving_king_safe(old_turn)
res = :stalemate
else
res = (@turn == :white ? :white_wins : :black_wins)
end
return res
end
end
def pawn_dir
@turn == :white ? -1 : 1
end
def rank(r)
@turn == :white ? 8 - r : r - 1
end
def possible_starting_points(piece_type, to, capt)
generator = MoveGenerator.new(self) do |from, dest|
if to == dest and self[from].type == piece_type and
pseudolegal_move(from, dest) and capturing(from, to) == capt
from
end
end
generator.generate_all
end
def king_starting_position
Vector.new(4, rank(1))
end
def king_rook_starting_position
Vector.new(7, rank(1))
end
def queen_rook_starting_position
Vector.new(0, rank(1))
end
private
def check_legality(from, to, old_turn)
if castling(from, to)
castling_directions(from, to)+[[0,0]].each do |direction|
return false unless leaving_position_safe(from+direction)
end
end
old_board = []
(0...size.x).each do |x|
old_board[x] = @board[x].clone
end
execute_move(from, to)
res = leaving_king_safe(old_turn)
@board = old_board
res
end
def check_free_path(from, to)
d = to - from
inc = [d.x.sign, d.y.sign]
pos = from.clone
while not pos == to - inc
pos += inc
return false if self[pos]
end
return true
end
def castling(from, to)
from == king_starting_position and (from-to).x.abs == 2
end
def rook_slide(d)
d.x == 0 or d.y == 0
end
def bishop_slide(d)
d.x.abs == d.y.abs
end
def knight_jump(d)
(d.x.abs == 2 and d.y.abs == 1) or (d.x.abs == 1 and d.y.abs == 2)
end
def switch_turn
@turn = (@turn == :white ? :black : :white)
end
def leaving_king_safe(old_turn)
king_pos = find_piece(Piece.new(old_turn, :king))
leaving_position_safe(king_pos)
end
def castling_directions(from, to)
if (to.x > from.x)
return [[1,0]]
else
return [[-1,0],[-2,0]]
end
end
def leaving_position_safe(safepos)
each_piece do |position, piece|
if piece.color == @turn
return false if pseudolegal_move(position, safepos)
end
end
end
def get_pos(v)
case v.size
when 1
v[0]
when 2
Vector.new(v[0], v[1])
end
end
def execute_move(from, to)
if promotion(from, to)
self[to] = Piece.new(@turn, @promotion_piece)
else
self[to] = self[from]
end
self[from] = nil
end
end
class MoveGenerator
def initialize(board, &valid)
@board = board
if block_given?
@valid_move = valid
else
@valid_move = lambda do |from, to|
if @board.is_valid(to) and @board.legal_move(from, to)
return to
end
end
end
end
def unstalled
@fast = true
found = catch(:move_found) do
@board.each_piece do |position, piece|
if piece.color == @board.turn
can_move(position)
end
end
end
found == true
end
def generate_all
@fast = false
move_list = []
@board.each_piece do |position, piece|
if piece.color == @board.turn
move_list += can_move(position)
end
end
move_list
end
def can_move(from)
piece = @board[from]
move_list = []
return [] unless piece
case piece.type
when :king
generate_directions.each do |direction|
move = got_move(from, from + direction)
move_list << move if move
end
when :queen
generate_directions.each do |direction|
move_list += generate_slide(from, direction)
end
when :rook
[[1,0],[-1,0],[0,1],[0,-1]].each do |direction|
move_list += generate_slide(from, direction)
end
when :bishop
[[1,1],[-1,1],[1,-1],[-1,-1]].each do |direction|
move_list += generate_slide(from, direction)
end
when :knight
[[2,1],[2,-1],[-2,1],[-2,-1],[1,2],[1,-2],[-1,2],[-1,-2]].each do |jump|
move = got_move(from, from + jump)
move_list << move if move
end
when :pawn
[[0,@board.pawn_dir],[0,@board.pawn_dir*2],[1,@board.pawn_dir],[-1,@board.pawn_dir]].each
do |jump|
move = got_move(from, from + jump)
move_list << move if move
end
end
return move_list
end
private
def generate_slide(from, direction)
move_list = []
pos = from
while @board.is_valid(pos += direction) do
move = got_move(from, pos)
move_list << move if move
end
move_list
end
def generate_directions
dirs = []
(-1..1).each do |x|
(-1..1).each do |y|
dirs << [x,y] unless x == 0 and y == 0
end
end
return dirs
end
def got_move(from, to)
if move = @valid_move[from, to]
throw(:move_found, true) if @fast
move
else
false
end
end
end
module UI
def ask_move
@promotion_piece = nil
loop do
print ": "
input = gets
raise "no move" unless input
case input.chomp
when /^\s*$/
return nil
when /^(\d)(\d)\s*(\d)(\d)$/
return Vector.new($1.to_i, $2.to_i), Vector.new($3.to_i, $4.to_i)
when /^([RNBQK]?)([a-h1-8]?)(x?)([a-h])([1-8])(=[RNBQK])?[#+]?$/
piece_type = letter2piece($1)
prom = $6
to = Vector.new($4[0]-'a'[0], 8 - $5.to_i)
sp = @board.possible_starting_points(piece_type, to, $3 == 'x')
unless $2.nil? or $2 == ""
letter = $2
if letter =~ /[a-h]/
check = lambda do |possible_from|
possible_from.x == letter[0] - 'a'[0]
end
else
check = lambda do |possible_from|
possible_from.y == 8 - letter.to_i
end
end
sp.reject! do |possible_from|
not check[possible_from]
end
end
case sp.size
when 0
say "incorrect notation"
when 1
from = sp.first
else
say "ambiguous notation"
end
if from
if piece_type == :pawn and to.y == @board.rank(8)
@promotion_piece = letter2piece($5[1,1])
end
if from
return from, to
end
end
when "O-O"
pos = @board.king_starting_position
return pos, pos + [2,0]
when "O-O-O"
pos = @board.queen_starting_position
return pos, pos - [2,0]
end
end
end
def letter2piece(letter)
case letter
when 'R'
return :rook
when 'N'
return :knight
when 'B'
return :bishop
when 'Q'
return :queen
when 'K'
return :king
else
return :pawn
end
end
def ask_promotion_piece
return @promotion_piece if @promotion_piece
print "promote to (default: queen): "
case gets.chomp!
when "rook" || 'r'
return :rook
when "knight" || 'n'
return :knight
when "bishop" || 'b'
return :bishop
else
return :queen || 'q'
end
end
def say(msg)
puts msg.to_s.gsub(/_/) { ' ' }
end
def show_board
say "turn : #{@board.turn.to_s}"
(0...@board.size.y).each do |y|
(0...@board.size.x).each do |x|
piece = @board[x,y]
if piece
s = piece.type_initial
s.upcase! if (piece.color == :white)
print s, ' '
else
print ' '
end
end
print "\n"
end
print "\n"
end
end
class ChessGame
attr_reader :board
include UI
def initialize
@board = Board.new(8,8)
@board.promotion_piece = :queen
(0...8).each do |x|
@board[x,1] = Piece.new( :black, :pawn )
@board[x,6] = Piece.new( :white, :pawn )
end
@board[0,0] = Piece.new( :black, :rook )
@board[1,0] = Piece.new( :black, :knight )
@board[2,0] = Piece.new( :black, :bishop )
@board[3,0] = Piece.new( :black, :queen )
@board[4,0] = Piece.new( :black, :king )
@board[5,0] = Piece.new( :black, :bishop )
@board[6,0] = Piece.new( :black, :knight )
@board[7,0] = Piece.new( :black, :rook )
@board[0,7] = Piece.new( :white, :rook )
@board[1,7] = Piece.new( :white, :knight )
@board[2,7] = Piece.new( :white, :bishop )
@board[3,7] = Piece.new( :white, :queen )
@board[4,7] = Piece.new( :white, :king )
@board[5,7] = Piece.new( :white, :bishop )
@board[6,7] = Piece.new( :white, :knight )
@board[7,7] = Piece.new( :white, :rook )
end
def play
while (state = @board.game_state) == :in_game
begin
move
rescue RuntimeError => err
print "\n"
if err.message == "no move"
say :exiting
else
say err.message
end
return
end
end
show_board
say state
end
def move
loop do
say ""
show_board
from, to = ask_move
raise "no move" unless from
if @board.is_valid(from) and @board.is_valid(to) and
@board.legal_move(from, to)
if @board.promotion(from, to)
@board.promotion_piece = ask_promotion_piece
end
@board.move(from, to)
break
else
say :invalid_move
end
end
end
end
@game = ChessGame.new
@game.play