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