[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

[SOLUTION] Chess Variants (I) (#35

Paolo Capriotti

6/15/2005 12:44:00 PM

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


3 Answers

Gavin Kistner

6/15/2005 2:10:00 PM

0

My solution can be found at http://phrogz.net/RubyLibs/...

My solution does not allow castling or en-passant. (Castling would be
far easier if you didn't have to determine if any other piece
challenged the intermediary space :p En-passant is annoying because
of the turn-based restriction.) It also does not check for checkmate.

Things I'm proud of:
* Subclassing Piece for each type to delegate the possible moves
calculation.

* Some of the DRY stuff in calculating possible moves along various
axes for everyone but the Pawn

* Some nice robustness and messages when you try to pick an invalid
piece, or send a piece somewhere it can't go. (After you pick the
piece to move, if you give it an invalid destination it tells you
what moves are legal.)

* Subclassing RuntimeError for the first time only and using it to my
advantage. (OutOfBoundsError and IllegalMoveError).

I'm actually not sure if I should be proud of the last one...is it
'good' to run code you know will throw an error and then catch that
as a way of detecting bounds, versus calculating the bounds properly?

When calculating the moves that a rook could do, for example, I moved
along a particular axis until I found another piece, adding each
square to the possible moves. However, if no pieces existed between
me and the edge of the board, I let the algorithm charge off the edge
of the board, waiting for the Move constructor to throw an
OutOfBoundsError as another way to let me know that it was time to stop.




Things I'm not so proud of:
* Sometimes I use 'a2' to access information, sometimes ['a','2'];
consequently a lot of my methods have code like:
def foo( col, row=nil )
col, row = (col.to_s+row.to_s).split('')
so that I can pass the information around as separate arguments,
one string, or an array. Tricky, but hacky.

* The pieces know what they are allowed to do on the board...but
don't know anything about check or checkmate. That's up to the Board.
Consequently, pieces think they can make a move that would result in
self-check, and list those choices among the options. The error
message when you try to make a move that would put your own king in
check is not helpful.

* I use #deep_clone with the Marshal hack to set up a scenario to
detect check without altering the current board. This feels
wrong...the board should be able to try out a move and then undo it
without having to clone itself. (I was concerned about putting pieces
back on the board afterwards.) Further, when I make the clone, I
can't the real #move method to try out the change, because that
method has all sorts of warnings that I'd have to hide. Ugly.


The trickiest part for me (that I ended up not solving) is figuring
out where the logic of the game goes. Shouldn't each piece know what
it can do? But then it needs to know about the entire board. And (for
en-passant) the recent history of moves for each piece. Ugly.


Thanks for a challenging puzzle. Having refactored some pieces twice,
it was a very useful exercise in thinking about roles and
responsibilities, and my own need to plan a little more before coding.


Paolo Capriotti

6/15/2005 5:39:00 PM

0

On 6/15/05, Paolo Capriotti <p.capriotti@gmail.com> wrote:
> Here is my solution.

I've found a bug. Here you can find the new version:
http://linuz.sns.it/~capriotti/ruby...

Paolo Capriotti


James Gray

6/15/2005 6:16:00 PM

0

On Jun 15, 2005, at 7:43 AM, Paolo Capriotti wrote:

> Here is my solution.

My own can be found at:

http://rubyquiz.com/ruby_quiz...

I tried to go the extra mile with tests and documentation to make it
very approachable, in case others would like to use it to try part II
of this quiz...

James Edward Gray II