[SUMMARY] Chess Variants (I) (#35)

As Gavin Kistner pointed out, this quiz was too much work. There's nothing that
hard about a chess game, as long as you don't need an AI, but there's just a lot
of things you have to take care of.

You have to define moves for six different pieces, build a board with some
helper methods that can handle chess geometry, and handle edge cases like
en-passant, castling and pawn promotion. That's just a lot of tedious work.

Check is the real challenge, for me anyway, because it affects so much of the
game. You must get out of check when you're in check. You can never make a
move that puts you in check. You can't even castle through a square if you
would be in check in that square. All that eventually adds up.

What I should have done with this quiz was provide a chess library and then ask
for the variations, which is the interesting part of the challenge. Luckily,
Ruby Quiz solvers are always making me look good, and that's basically what we
have now, thanks to their efforts. Three people have submitted libraries and
another person has submitted an example of how to use an existing library to
solve this problem, with very little code even. Now, if anyone else wants to
give round two a try, my hope is that you'll use these libraries as jumping off
points and still be able to join in on the fun.

The three libraries are surprisingly similar. We all had about the same idea,
build a class hierarchy for the pieces and create a board. I believe even
Bangkok, the library used by Jim Menard's example works that way. The idea is
that common chess piece behavior goes in a Piece class. Then you subclass Piece
for Pawn, Knight, etc., adding the unique behaviors. In chess, this generally
amounts to the piece's unique moves.

Paolo Capriotti skipped the chess subclasses and rolled the move types into the
Board class. This isn't unreasonable. Knowing what moves a piece can make at
any point in the game requires knowledge of the board. Those of us who use
piece classes pass the board down to the piece to account for this. Paolo just
reverses that.

The other essential part of a chess library is a Board object, as I said before.
Chess definitely has its own geometry and you need an object that encompasses
that. One question is how to refer to the square themselves. You can go with a
two dimensional array style notation, wrap those x and y pairs in a class, or
lean on chess notation and accept things like "e4". When dealing with chess
positions, we often need information about ranks, files, diagonals, those wacky
L-shaped knight jumps, etc. Board provides this information as well.

Obviously, Board also needs to provide piece moving routines and this can be
heavy lifting. These methods need to be aware of castling, pawn promotion,
en-passant capture, and check. That's the real guts of a chess game.

I'm not going to dump entire chess libraries in here. Instead, I'll show usage.
Here's Paolo's chess game portion of rchess.rb:

  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

You can see that Paolo's library also includes a UI module, which this game
object makes use of. The constructor is straight forward, it sets up some
initial state information, including a board, and places the pieces in their
starting positions. Notice that the Board object is indexed as a
multidimensional array and the pieces can be constructed from just a color and a
type.

The play() method is the game itself. It's really just a loop looking for an
end game condition. Aside from a little error checking and displaying the final
state, it simply calls move() again and again.

Which brings us to the move() method. It shows the board (with the help of the
UI method show_board()) and then asks for a move (another UI helper). You can
see that the move is validated using the Board object, and then Board.move() is
called to advance the game.

The final two lines kick off the methods we just examined. All the details are
handled by the library itself.

Here's the same thing using my own library:

  #!/usr/local/bin/ruby -w

  # chess

···

#
  # Created by James Edward Gray II on 2005-06-14.
  # Copyright 2005 Gray Productions. All rights reserved.

  require "chess"

  board = Chess::Board.new

  puts
  puts "Welcome to Ruby Quiz Chess."

  # player move loop
  loop do
    # show board
    puts
    puts board
    puts

    # watch for end conditions
    if board.in_checkmate?
      puts "Checkmate! " +
           "It's #{board.turn == :white ? 'Black' : 'White'}'s game."
      puts
      break
    elsif board.in_stalemate?
      puts "Stalemate."
      puts
      break
    elsif board.in_check?
      puts "Check."
    end

    # move input loop
    move = nil
    loop do
      print "#{board.turn.to_s.capitalize}'s Move (from to): "
      move = $stdin.gets.chomp
      
      # validate move
      moves = board.moves
      if move !~ /^\s*([a-h][1-8])\s*([a-h][1-8])\s*$/
        puts "Invalid move format. Use from to. (Example: e2 e4.)"
      elsif board[$1].nil?
        puts "No piece on that square."
      elsif board[$1].color != board.turn
        puts "That's not your piece to move."
      elsif board.in_check? and ( (m = moves.assoc($1)).nil? or
                                not m.last.include?($2) )
        puts "You must move out of check."
      elsif not (board[$1].captures + board[$1].moves).include?($2)
        puts "That piece can't move to that square."
      elsif ((m = moves.assoc($1)).nil? or not m.last.include?($2))
        puts "You can't move into check."
      else
        break
      end
    end
    
    # make move, with promotion if needed
    if board[$1].is_a?(Chess::Pawn) and $2[1, 1] == "8"
      from, to = $1, $2

      print "Promote to (k, b, r, or q)? "
      promote = $stdin.gets.chomp
      
      case promote.downcase[0, 1]
      when "k"
        board.move($1, $2, Chess::Knight)
      when "b"
        board.move($1, $2, Chess::Bishop)
      when "r"
        board.move($1, $2, Chess::Rook)
      else
        board.move($1, $2, Chess::Queen)
      end
    else
      board.move($1, $2)
    end
  end

I pull in my chess library, and create a Chess::Board. Next, I display a
welcome message then launch into my game loop which begins by printing the
board. My Board object defines to_s(), so you can just print it as needed.

My chess game then checks for game end conditions using helper methods on Board
like in_checkmate?() and in_stalemate?(). When found, the code prints messages
and breaks out of the game loop.

The next loop is my move input loop. It looks like a lot of code but there are
two good reasons for that. One, I wanted good error messages, so I'm checking
every little thing that could have gone wrong and printing a custom message for
it. Two, I avoiding using HighLine to simplify the process, so I wouldn't add
the dependancy to the library. So really I'm just reading input and printing
error messages here, nothing exciting.

The final chunk of code checks to see if the requested move is a pawn promotion.
When it is, the user is prompted to choose the new piece type. Either way, the
requested move is passed along to Board.move(), which handles the rest of the
game.

One last example. Let's look at Gavin Kistner's code:

  if $0 == __FILE__
    include GKChess
    require "rubygems"
    require "highline/import"
    board = Board.new
    while !board.game_over?
      puts "\n#{board}\n\n"
      puts "Move ##{board.move_number}, #{board.turn}'s turn"
      #puts "(#{@turn} is in check)" if board.king_in_check?( @turn )

      piece = ask( "\tPiece to move: ",
                   lambda { |loc| board[ loc ] } ){ |q|
        q.responses[ :not_valid ] = ""
        q.validate = lambda { |loc|
          case loc
            when /[a-h][1-8]/i
              if piece = board[ loc ]
                if piece.color == board.turn
                  if !piece.possible_moves.empty?
                    true
                  else
                    puts "That #{piece.name} has no " +
                         "legal moves available."
                    false
                  end
                else
                  puts "The #{piece.name} at #{loc} " +
                       "does not belong to #{board.turn}!"
                  false
                end
              else
                puts "There is no piece at #{loc}!"
                false
              end
            else
              puts "(Please enter the location such as " +
                   "a8 or c3)"
              false
          end
        }
      }

      valid_locations = piece.possible_moves.collect{ |move|
        move.colrow
      }

      dest = ask( "\tMove #{piece.name} to: " ){ |q|
        q.responses[ :not_valid ] = "The #{piece.name} cannot " +
          "move there. Valid moves: " +
          "#{valid_locations.sort.join(', ')}."
        q.validate = lambda { |loc|
          valid_locations.include?( loc.downcase )
        }
      }

      board.move( piece.colrow, dest )
    end
  end

Gavin start's by pulling in the GKChess namespace and the HighLine library to
ease the input fetching process. The code then creates a Board and loops
looking for Board.game_over?(). It prints the board and turn, much like my own
code did, then fetches a move from the player.

Again, this looks like a lot of code, mainly because of the error messages. The
player is asked to select a piece (which HighLine fetches and returns), then the
code loads all the valid moves for that piece (using possible_moves()).
Finally, the player is asked to select one of those moves for the piece and
Board.move() is called to make it happen.

As I said, all three solutions were surprisingly similar.

Now we have to think about how we would adapt these to chess variations.
Obviously, you need to get a little familiar with your library of choice. Get
to know the methods it provides. Then, you'll probably need to subclass some of
the objects and override them with some special behavior. At that point, you
should be able to plug your chess variant objects into something like the above
examples. Of course, any code handles some changes better than others and
that's were the real fun comes in.

I hope you'll consider giving next week's challenge a shot, even if you didn't
suffer through this week's torture. Grab a library and start adapting. See how
it goes.

I apologize for not estimating this challenge better and presenting it in a more
digestible format. Thanks so much to Gavin Kistner, Jim Menard, and Paolo
Capriotti for for trying it anyway!

Err... uhhh... it seems I missed the deadline. Merrhh. Oh well, here's my incomplete chess program. :slight_smile:

I was basically completing the rules from top to bottom on the web page, and so I haven't yet done castling, or validating for check, stalemate, or (seems hardest to me) mate.

But it does to a little REP loop and draw a board and all that.

Probably the most significant thing about this "solution" is that I decided to ignore your initial statement about introducing plugins & whatnot for the purpose of expansion. Rather, I used this as an opportunity to practice TDD, refactoring, and YAGNI. I was hoping to see this pay off when I had to implement the variants.

There are a bunch of cute tricks in the code (at least, for a ruby nuby such as I).

I am proud of:
- doing all sorts of metaprogramming without resorting to 'eval'
- TDDing the whole d*mn thing (almost)
- producing so much code, so quickly, on a language so foreign to me, and still understanding what I wrote afterwards

I am not proud of:
- well, check the code. chances are, if there's a comment, it's a description of some code debt.
- I don't like using 'break' to get out of loops. Too much like GOTO. But I did. Twice.

TDD, by the way, proved to be incredibly successful, not because it helped with design (in fact, I found it quite difficult -- often, I would write the code I wanted in a comment, and then struggle to write tests to make that code appear), but because it was really easy to make sure I didn't break anything, and it really felt good to see all the tests pass.

Hope there's something of value here for somebody out there! It was certainly valuable practice for me, and I'll probably go on to finish the code.

Devin

rubyquiz35.zip (13.9 KB)

Err... uhhh... it seems I missed the deadline.

There is no such thing. There's only James's schedule, which forces him to write when he has time. Don't let that hinder you though.

Oh well, here's my incomplete chess program. :slight_smile:

Thanks for the submission. I enjoyed looking it over. I hope you'll consider trying to adapt it to tomorrow's challenge...

James Edward Gray II

···

On Jun 16, 2005, at 9:04 AM, Devin Mullins wrote: