Current File : //opt/RZphp73/includes/Games/Chess.php
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP version 4                                                        |
// +----------------------------------------------------------------------+
// | Copyright (c) 2003 The PHP Group                                     |
// +----------------------------------------------------------------------+
// | This source file is subject to version 3.0 of the PHP license,       |
// | that is bundled with this package in the file LICENSE, and is        |
// | available through the world-wide-web at                              |
// | http://www.php.net/license/3_0.txt.                                  |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | license@php.net so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Authors: Gregory Beaver <cellog@php.net>                             |
// +----------------------------------------------------------------------+
//
// $Id: Chess.php,v 1.21 2007/06/17 05:46:43 cellog Exp $
/**
 * The Games_Chess Package
 *
 * The logic of handling a chessboard and parsing standard
 * FEN (Forsyth-Edwards Notation) for describing a position as well as SAN
 * (Standard Algebraic Notation) for describing individual moves is handled.  This
 * class can be used as a backend driver for playing chess, or for validating
 * and/or creating PGN files using the File_ChessPGN package.
rn *
 * Although this package is alpha, it is fully unit-tested.  The code works, but
 * the API is fluid, and may change dramatically as it is put into use and better
 * ways are found to use it.  When the API stabilizes, the stability will increase.
 *
 * To learn how to play chess, there are many sites online, try searching for
 * "chess."  To play online, I use the Internet Chess Club at
 * {@link http://www.chessclub.com} as CelloJi, look me up sometime :).  Don't
 * worry, I'm not very good.
 * @todo implement special class Games_Chess_Chess960 for Fischer Random Chess
 * @todo implement special class Games_Chess_Wild23 for ICC Wild variant 23
 * @author Gregory Beaver <cellog@php.net>
 * @copyright 2003
 * @license http://www.php.net/license/3_0.txt PHP License 3.0
 * @version @VER@
 */
/**#@+
 * Move constants
 */
/**
 * Castling move (O-O or O-O-O)
 */
define('GAMES_CHESS_CASTLE', 1);
/**
 * Pawn move (e4, e8=Q, exd5)
 */
define('GAMES_CHESS_PAWNMOVE', 2);
/**
 * Piece move (Qa4, Nfe6, Bxe5, Re2xe6)
 */
define('GAMES_CHESS_PIECEMOVE', 3);
/**
 * Special move type used in Wild23 like P@a4 (place a pawn at a4)
 */
define('GAMES_CHESS_PIECEPLACEMENT', 4);
/**#@-*/

/**#@+
 * Error Constants
 */
/**
 * Invalid Standard Algebraic Notation was used
 */
define('GAMES_CHESS_ERROR_INVALID_SAN', 1);
/**
 * The number of space-separated fields in a FEN passed to {@internal 
 * {@link _parseFen()} through }} {@link resetGame()} was incorrect, should be 6
 */
define('GAMES_CHESS_ERROR_FEN_COUNT', 2);
/**
 * A FEN containing multiple spaces in a row was parsed {@internal by
 * {@link _parseFen()}}}
 */
define('GAMES_CHESS_ERROR_EMPTY_FEN', 3);
/**
 * Too many pieces were passed in for the chessboard to fit them in a FEN
 * {@internal passed to {@link _parseFen()}}}
 */
define('GAMES_CHESS_ERROR_FEN_TOOMUCH', 4);
/**
 * The indicator of which side to move in a FEN was neither "w" nor "b"
 */
define('GAMES_CHESS_ERROR_FEN_TOMOVEWRONG', 5);
/**
 * The list of castling indicators was too long (longest is KQkq) of a FEN
 */
define('GAMES_CHESS_ERROR_FEN_CASTLETOOLONG', 6);
/**
 * Something other than K, Q, k or q was in the castling indicators of a FEN
 */
define('GAMES_CHESS_ERROR_FEN_CASTLEWRONG', 7);
/**
 * The en passant square was neither "-" nor an algebraic square in a FEN
 */
define('GAMES_CHESS_ERROR_FEN_INVALID_EP', 8);
/**
 * The ply count (number of half-moves) was not a number in a FEN
 */
define('GAMES_CHESS_ERROR_FEN_INVALID_PLY', 9);
/**
 * The move count (pairs of white/black moves) was not a number in a FEN
 */
define('GAMES_CHESS_ERROR_FEN_INVALID_MOVENUMBER', 10);
/**
 * An illegal move was attempted, the king is in check
 */
define('GAMES_CHESS_ERROR_IN_CHECK', 11);
/**
 * Can't castle kingside, either king or rook has moved
 */
define('GAMES_CHESS_ERROR_CANT_CK', 12);
/**
 * Can't castle kingside, pieces are in the way on the f and/or g files
 */
define('GAMES_CHESS_ERROR_CK_PIECES_IN_WAY', 13);
/**
 * Can't castle kingside, either king or rook has moved
 */
define('GAMES_CHESS_ERROR_CANT_CQ', 14);
/**
 * Can't castle queenside, pieces are in the way on the d, c and/or b files
 */
define('GAMES_CHESS_ERROR_CQ_PIECES_IN_WAY', 15);
/**
 * Castling would place the king in check, which is illegal
 */
define('GAMES_CHESS_ERROR_CASTLE_WOULD_CHECK', 16);
/**
 * Performing a requested move would place the king in check
 */
define('GAMES_CHESS_ERROR_MOVE_WOULD_CHECK', 17);
/**
 * The requested move does not remove a check on the king
 */
define('GAMES_CHESS_ERROR_STILL_IN_CHECK', 18);
/**
 * An attempt (however misguided) was made to capture one's own piece, illegal
 */
define('GAMES_CHESS_ERROR_CANT_CAPTURE_OWN', 19);
/**
 * An attempt was made to capture a piece on a square that does not contain a piece
 */
define('GAMES_CHESS_ERROR_NO_PIECE', 20);
/**
 * A attempt to move an opponent's piece was made, illegal
 */
define('GAMES_CHESS_ERROR_WRONG_COLOR', 21);
/**
 * A request was made to move a piece from one square to another, but it can't
 * move to that square legally
 */
define('GAMES_CHESS_ERROR_CANT_MOVE_THAT_WAY', 22);
/**
 * An attempt was made to add a piece to the chessboard, but there are too many
 * pieces of that type already on the chessboard
 */
define('GAMES_CHESS_ERROR_MULTIPIECE', 23);
/**
 * An attempt was made to add a piece to the chessboard through the parsing of
 * a FEN, but there are too many pieces of that type already on the chessboard
 */
define('GAMES_CHESS_ERROR_FEN_MULTIPIECE', 24);
/**
 * An attempt was made to add a piece to the chessboard on top of an existing piece
 */
define('GAMES_CHESS_ERROR_DUPESQUARE', 25);
/**
 * An invalid piece indicator was used in a FEN
 */
define('GAMES_CHESS_ERROR_FEN_INVALIDPIECE', 26);
/**
 * Not enough piece data was passed into the FEN to explain every square on the board
 */
define('GAMES_CHESS_ERROR_FEN_TOOLITTLE', 27);
/**
 * Something other than "W" or "B" was passed to a method needing a color
 */
define('GAMES_CHESS_ERROR_INVALID_COLOR', 28);
/**
 * Something that isn't SAN ([a-h][1-8]) was passed to a function requiring a
 * square location
 */
define('GAMES_CHESS_ERROR_INVALID_SQUARE', 29);
/**
 * Something other than "P", "Q", "R", "B", "N" or "K" was passed to a method
 * needing a piece type
 */
define('GAMES_CHESS_ERROR_INVALID_PIECE', 30);
/**
 * Something other than "Q", "R", "B", or "N" was passed to a method
 * needing a piece type for pawn promotion
 */
define('GAMES_CHESS_ERROR_INVALID_PROMOTE', 31);
/**
 * SAN was passed in that is too ambiguous - multiple pieces could execute
 * the move, and no disambiguation (like Naf3 or Bf3xe4) was used
 */
define('GAMES_CHESS_ERROR_TOO_AMBIGUOUS', 32);
/**
 * No piece of the current color can execute the SAN (as in, if Na3 is passed
 * in, but there are no knights that can reach a3
 */
define('GAMES_CHESS_ERROR_NOPIECE_CANDOTHAT', 33);
/**
 * In loser's chess, and the current move does not capture a piece although
 * capture is possible.
 */
define('GAMES_CHESS_ERROR_MOVE_MUST_CAPTURE', 34);
/**
 * When piece placement is attempted, but no pieces exist to be placed
 */
define('GAMES_CHESS_ERROR_NOPIECES_TOPLACE', 35);
/**
 * When piece placement is attempted, but there is a piece on the desired square already
 */
define('GAMES_CHESS_ERROR_PIECEINTHEWAY', 36);
/**
 * When a pawn placement on the first or back rank is attempted
 */
define('GAMES_CHESS_ERROR_CANT_PLACE_18', 37);
/**
 * ABSTRACT parent class - use {@link Games_Chess_Standard} for a typical
 * chess game
 *
 * This class contains a few public methods that are the only thing most
 * users of the package will ever need.  Protected methods are available
 * for usage by child classes, and it is expected that all child classes
 * will implement certain protected methods used by the utility methods in
 * this class.
 *
 * Public API methods used are:
 *
 * Game-related methods
 *
 * - {@link resetGame()}: in order to start a new game (pass a FEN for a starting
 *   position)
 * - {@link blankBoard()}: in order to start with an empty chessboard
 * - {@link addPiece()}: Use to add pieces one at a time to the board
 * - {@link moveSAN()}: Use to move pieces based on their SAN (Qa3, exd5, etc.)
 * - {@link moveSquare()}: Use to move pieces based on their square (a2 -> a3
 *   for Qa3, e4 -> d5 for exd5, etc.)
 *
 * Game state methods:
 *
 * - {@link inCheck()}: Use to determine the presence of check
 * - {@link inCheckMate()}: Use to determine a won game
 * - {@link inStaleMate()}: Use to determine presence of stalemate draw
 * - {@link in50MoveDraw()}: Use to determine presence of 50-move rule draw
 * - {@link inRepetitionDraw()}: Use to determine presence of a draw by repetition
 * - {@link inStaleMate()}: Use to determine presence of stalemate draw
 * - {@link inDraw()}: Use to determine if any forced draw condition exists
 *
 * Game data methods:
 *
 * - {@link renderFen()}: Use to retrieve a FEN representation of the
 *   current chessboard position, in order to transfer to another chess program
 * - {@link toArray()}: Use to retrieve a literal representation of the
 *   current chessboard position, in order to display as HTML or some other
 *   format for the user
 * - {@link getMoveList()}: Use to retrieve the list of SAN moves for this game
 * @package Games_Chess
 */
class Games_Chess {
    /**
     * Used for transactions
     * @var array
     * @access private
     */
    var $_saveState = array();
    /**
     * @var array
     * @access private
     */
    var $_board;
    /**
     * @var string
     * @access private
     */
    var $_move = 'W';
    /**
     * @var integer
     * @access private
     */
    var $_moveNumber = 1;
    /**
     * Half-moves since last pawn move or capture
     * @var integer
     * @access private
     */
    var $_halfMoves = 1;
    /**
     * Square that an en passant can happen, or "-"
     * @var string
     * @access private
     */
    var $_enPassantSquare = '-';
    /**
     * Moves in SAN format for easy write-out to a PGN file
     *
     * The format is:
     * <pre>
     * array(
     *  movenumber => array(White move, Black move),
     *  movenumber => array(White move, Black move),
     * )
     * </pre>
     * @var array
     * @access private
     */
    var $_moves = array();
    /**
     * Moves in SAN format for easy write-out to a PGN file, with check/checkmate annotations appended
     *
     * The format is:
     * <pre>
     * array(
     *  movenumber => array(White move, Black move),
     *  movenumber => array(White move, Black move),
     * )
     * </pre>
     * @var array
     * @access private
     */
    var $_movesWithCheck = array();
    /**
     * Store every position from the game, used to determine draw by repetition
     *
     * If the exact same position is encountered three times, then it is a draw
     * @var array
     * @access private
     */
    var $_allFENs = array();
    /**#@+
     * Castling rights
     * @var boolean
     * @access private
     */
    var $_WCastleQ = true;
    var $_WCastleK = false;
    var $_BCastleQ = true;
    var $_BCastleK = false;
    /**#@-*/
    /**
     * Contents of the last move returned from {@link _parseMove()}, used to
     * process en passant.
     * @var false|array
     * @access private
     */
    var $_lastMove = false;

    function &factory($type = 'Standard')
    {
        if (!class_exists("Games_Chess_$type")) {
            @include_once 'Games/Chess/' . ucfirst(strtolower($type)) . '.php';
        }
        if (class_exists("Games_Chess_$type")) {
            $type = "Games_Chess_$type";
            $a = new $type;
            return $a;
        } else {
            $a = false;
            return $a;
        }
    }

    /**
     * Create a blank chessboard with no pieces on it
     */
    function blankBoard()
    {
        $this->_board = array();
        for ($j = 8; $j >= 1; $j--) {
            for ($i = ord('a'); $i <= ord('h'); $i++) {
                $this->_board[chr($i) . $j] = chr($i) . $j;
            }
        }
    }

    /**
     * Create a new game with the starting position, or from the position
     * specified by $fen
     *
     * @param false|string
     * @return PEAR_Error|true returns any errors thrown by {@link _parseFen()}
     */
    function resetGame($fen = false)
    {
        $this->_saveState = array();
        if (!$fen) {
            $this->_setupStartingPosition();
        } else {
            return $this->_parseFen($fen);
        }
        return true;
    }
    
    /**
     * Make a move from a Standard Algebraic Notation (SAN) format
     *
     * SAN is just a normal chess move like Na4, instead of the English Notation,
     * like NR4
     * @param string
     * @return true|PEAR_Error
     */
    function moveSAN($move)
    {
        if (!is_array($this->_board)) {
            $this->resetGame();
        }
        if (!$this->isError($parsedMove = $this->_parseMove($move))) {
            if (!$this->isError($err = $this->_validMove($parsedMove))) {
                list($key, $parsedMove) = each($parsedMove);
                $this->_moves[$this->_moveNumber][($this->_move == 'W') ? 0 : 1] = $move;
                $oldMoveNumber = $this->_moveNumber;
                $this->_moveNumber += ($this->_move == 'W') ? 0 : 1;
                $this->_halfMoves++;
                if ($key == GAMES_CHESS_CASTLE) {
                    $a = ($parsedMove == 'Q') ? 'K' : 'Q';
                    // clear castling rights
                    $this->{'_' . $this->_move . 'Castle' . $parsedMove} = false;
                    $this->{'_' . $this->_move . 'Castle' . $a} = false;
                    $row = ($this->_move == 'W') ? 1 : 8;
                    switch ($parsedMove) {
                        case 'K' :
                            $this->_moveAlgebraic("e$row", "g$row");
                            $this->_moveAlgebraic("h$row", "f$row");
                        break;
                        case 'Q' :
                            $this->_moveAlgebraic("e$row", "c$row");
                            $this->_moveAlgebraic("a$row", "d$row");
                        break;
                    }
                    $this->_enPassantSquare = '-';
                } else {
                    $movedfrom = $this->_getSquareFromParsedMove($parsedMove);
                    $promote = isset($parsedMove['promote']) ?
                        $parsedMove['promote'] : '';
                    $this->_moveAlgebraic($movedfrom, $parsedMove['square'], $promote);
                    if ($parsedMove['takes']) {
                        $this->_halfMoves = 1;
                    }
                    if ($parsedMove['piece'] == 'P') {
                        $this->_halfMoves = 1;
                        $this->_enPassantSquare = '-';
                        if (in_array($movedfrom{1} - $parsedMove['square']{1},
                              array(2, -2))) {
                            $direction = ($this->_move == 'W' ? 1 : -1);
                            $this->_enPassantSquare = $parsedMove['square']{0} .
                                ($parsedMove['square']{1} - $direction);
                        }
                    } else {
                        $this->_enPassantSquare = '-';
                    }
                    if ($parsedMove['piece'] == 'K') {
                        $this->{'_' . $this->_move . 'CastleQ'} = false;
                        $this->{'_' . $this->_move . 'CastleK'} = false;
                    }
                    if ($parsedMove['piece'] == 'R') {
                        if ($movedfrom{0} == 'a') {
                        $this->{'_' . $this->_move . 'CastleQ'} = false;
                        }
                        if ($movedfrom{0} == 'h') {
                        $this->{'_' . $this->_move . 'CastleK'} = false;
                        }
                    }
                }
                $moveWithCheck = $move;
                if ($this->inCheckMate(($this->_move == 'W') ? 'B' : 'W')) {
                    $moveWithCheck .= '#';
                } elseif ($this->inCheck(($this->_move == 'W') ? 'B' : 'W')) {
                    $moveWithCheck .= '+';
                }
                $this->_movesWithCheck[$oldMoveNumber][($this->_move == 'W') ? 0 : 1] = $moveWithCheck;
                $this->_move = ($this->_move == 'W' ? 'B' : 'W');
                
                // increment the position counter for this position
                $x = $this->renderFen(false);
                if (!isset($this->_allFENs[$x])) {
                    $this->_allFENs[$x] = 0;
                }
                $this->_allFENs[$x]++;
                return true;
            } else {
                return $err;
            }
        } else {
            return $parsedMove;
        }
    }
    
    /**
     * Move a piece from one square to another, and mark the old square as empty
     *
     * @param string [a-h][1-8] square to move from
     * @param string [a-h][1-8] square to move to
     * @param string piece to promote to, if this is a promotion move
     * @return true|PEAR_Error
     */
    function moveSquare($from, $to, $promote = '')
    {
        $move = $this->_convertSquareToSAN($from, $to, $promote);
        if ($this->isError($move)) {
            return $move;
        } else {
            return $this->moveSAN($move);
        }
    }
    
    /**
     * Get the list of moves in Standard Algebraic Notation
     *
     * Can be used to populate a PGN file.
     * @param boolean If true, then moves that check will be postfixed with "+" and checkmate with "#"
     *                as in Nf3+ or Qxg7#
     * @return array
     */
    function getMoveList($withChecks = false)
    {
        if ($withChecks) {
            return $this->_movesWithCheck;
        }
        return $this->_moves;
    }
    
    /**
     * @return W|B|D|false winner of game, or draw, or false if still going
     */
    function gameOver()
    {
        $opposite = $this->_move == 'W' ? 'B' : 'W';
        if ($this->inCheckmate()) {
            return $opposite;
        }
        if ($this->inDraw()) {
            return 'D';
        }
        return false;
    }
    
    /**
     * Determine whether a side is in checkmate
     * @param W|B color of side to check, defaults to the current side
     * @return boolean
     * @throws GAMES_CHESS_ERROR_INVALID_COLOR
     */
    function inCheckMate($color = null)
    {
        if (is_null($color)) {
            $color = $this->_move;
        }
        $color = strtoupper($color);
        if (!in_array($color, array('W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_COLOR,
                array('color' => $color));
        }
        if (!($checking = $this->inCheck($color))) {
            return false;
        }
        $moves = $this->getPossibleKingMoves($king = $this->_getKing($color), $color);
        foreach ($moves as $escape) {
            $this->startTransaction();
            $this->_move = $color;
            if (!class_exists('PEAR')) {
                require_once 'PEAR.php';
            }
            PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
            $this->moveSquare($king, $escape);
            PEAR::popErrorHandling();
            $this->_move = $color;
            $stillchecked = $this->inCheck($color);
            $this->rollbackTransaction();
            if (!$stillchecked) {
                return false;
            }
        }
        // if we're in double check, and the king can't move, that's checkmate
        if (is_array($checking) && count($checking) > 1) {
            return true;
        }
        $squares = $this->_getPathToKing($checking, $king);
        if ($this->_interposeOrCapture($squares, $color)) {
            return false;
        }
        return true;
    }
    
    /**
     * Determine whether a side is in stalemate
     * @param W|B color of the side to look at, defaults to the current side
     * @return boolean
     * @throws GAMES_CHESS_ERROR_INVALID_COLOR
     */
    function inStaleMate($color = null)
    {
        if (is_null($color)) {
            $color = $this->_move;
        }
        $color = strtoupper($color);
        if (!in_array($color, array('W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_COLOR,
                array('color' => $color));
        }
        if ($this->inCheck($color)) {
            return false;
        }
        $moves = $this->_getPossibleChecks($color);
        foreach($moves as $name => $canmove) {
            if (count($canmove)) {
                $a = $this->_getPiece($name);
                foreach($canmove as $move) {
                    $this->startTransaction();
                    $this->_move = $color;
                    if (!class_exists('PEAR')) {
                        require_once 'PEAR.php';
                    }
                    PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
                    $err = $this->moveSquare($a, $move);
                    PEAR::popErrorHandling();
                    $this->rollbackTransaction();
                    if (!is_object($err)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
    
    /**
     * Determines the presence of a forced draw
     * @param W|B
     * @return boolean
     */
    function inDraw($color = null)
    {
        return $this->inStaleMate($color) ||
               $this->inRepetitionDraw() ||
               $this->in50MoveDraw() ||
               $this->inBasicDraw();
    }
    
    /**
     * Determine whether draw by repetition has happened
     *
     * From FIDE rules:
     * <pre>
     * 10.10
     *
     * The game is drawn, upon a claim by the player having the move, when the
     * same position, for the third time: 
     * (a) is about to appear, if he first writes the move on his
     *     scoresheet and declares to the arbiter his intention of making
     *     this move; or 
     * (b) has just appeared, the same player having the move each time. 
     *
     * The position is considered the same if pieces of the same kind and
     * colour occupy the same squares, and if all the possible moves of
     * all the pieces are the same, including the rights to castle [at
     * some future time] or to capture a pawn "en passant". 
     * </pre>
     *
     * This class determines draw by comparing FENs rendered after every move
     * @return boolean
     */
    function inRepetitionDraw()
    {
        $fen = $this->renderFen(false);
        if (isset($this->_allFENs[$fen]) && $this->_allFENs[$fen] == 3) {
            return true;
        }
        return false;
    }
    
    /**
     * Determine whether any pawn move or capture has occurred in the past 50 moves
     * @return boolean
     */
    function in50MoveDraw()
    {
        return $this->_halfMoves >= 50;
    }
    
    /**
     * Determine the presence of a basic draw as defined by FIDE rules
     *
     * The rule states:
     * <pre>
     * 10.4
     *
     * The game is drawn when one of the following endings arises: 
     * (a) king against king; 
     * (b) king against king with only bishop or knight; 
     * (c) king and bishop against king and bishop, with both bishops
     *     on diagonals of the same colour. 
     * </pre>
     * @return boolean
     */
    function inBasicDraw()
    {
        $pieces = $this->_getPieceTypes();
        $blackpieces = array_keys($pieces['B']);
        $whitepieces = array_keys($pieces['W']);
        if (count($blackpieces) > 2 || count($whitepieces) > 2) {
            return false;
        }
        if (count($blackpieces) == 1) {
            if (count($whitepieces) == 1) {
                return true;
            }
            if ($whitepieces[0] == 'K') {
                if (in_array($whitepieces[1], array('N', 'B'))) {
                    return true;
                } else {
                    return false;
                }
            } else {
                if (in_array($whitepieces[0], array('N', 'B'))) {
                    return true;
                } else {
                    return false;
                }
            }
        }

        if (count($whitepieces) == 1) {
            if (count($blackpieces) == 1) {
                return true;
            }
            if ($blackpieces[0] == 'K') {
                if (in_array($blackpieces[1], array('N', 'B'))) {
                    return true;
                } else {
                    return false;
                }
            } else {
                if (in_array($blackpieces[0], array('N', 'B'))) {
                    return true;
                } else {
                    return false;
                }
            }
        }
        $wpindex = ($whitepieces[0] == 'K') ? 1 : 0;
        $bpindex = ($blackpieces[0] == 'K') ? 1 : 0;
        if ($whitepieces[$wpindex] == 'B' && $blackpieces[$bpindex] == 'B') {
            // bishops of same color?
            if ($pieces['B']['B'][0] == $pieces['W']['B'][0]) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * render the FEN notation for the current board
     * @param boolean private parameter, used to determine whether to include
     *                move number/ply count - this is used to keep track of
     *                positions for draw detection
     * @return string
     */
    function renderFen($include_moves = true)
    {
        $fen = $this->_renderFen() . ' ';
        
        // render who's to move
        $fen .= strtolower($this->_move) . ' ';
        
        // render castling rights
        if (!$this->_WCastleQ && !$this->_WCastleK && !$this->_BCastleQ
              && !$this->_BCastleK) {
            $fen .= '- ';
        } else {
            if ($this->_WCastleK) {
                $fen .= 'K';
            }
            if ($this->_WCastleQ) {
                $fen .= 'Q';
            }
            if ($this->_BCastleK) {
                $fen .= 'k';
            }
            if ($this->_BCastleQ) {
                $fen .= 'q';
            }
            $fen .= ' ';
        }
        
        // render en passant square
        $fen .= $this->_enPassantSquare;

        if (!$include_moves) {
            return $fen;
        }

        // render half moves since last pawn move or capture
        $fen .=  ' ' . $this->_halfMoves . ' ';
        
        // render move number
        $fen .= $this->_moveNumber;
        return $fen;
    }
    
    /**
     * Add a piece to the chessboard
     *
     * Must be overridden in child classes
     * @abstract
     * @param W|B Color of piece
     * @param P|N|K|Q|R|B Piece type
     * @param string algebraic location of piece
     */
    function addPiece($color, $type, $square)
    {
        trigger_error("Error: do not use abstract Games_Chess class", E_USER_ERROR);
    }
    
    /**
     * Generate a representation of the chess board and pieces for use as a
     * direct translation to a visual chess board
     *
     * Must be overridden in child classes
     * @return array
     * @abstract
     */
    function toArray()
    {
        trigger_error("Error: do not use abstract Games_Chess class", E_USER_ERROR);
    }
    
    /**
     * Determine whether moving a piece from one square to another requires
     * a pawn promotion
     * @param string [a-h][1-8] location of the piece to move
     * @param string [a-h][1-8] place to move the piece to
     * @return boolean true if the move represented by moving from $from to $to
     *                 is a pawn promotion move
     */
    function isPromoteMove($from, $to)
    {
        $test = $this->_convertSquareToSAN($from, $to);
        if ($this->isError($test)) {
            return false;
        }
        if (strpos($test, '=Q') !== false) {
            return true;
        }
        return false;
    }
    
    /**
     * @return W|B return the color of the side to move (white or black)
     */
    function toMove()
    {
        return $this->_move;
    }
    
    /**
     * Determine legality of kingside castling
     * @return boolean
     */
    function canCastleKingside()
    {
        return $this->{'_' . $this->_move . 'CastleK'};
    }
    
    
    /**
     * Determine legality of queenside castling
     * @return boolean
     */
    function canCastleQueenside()
    {
        return $this->{'_' . $this->_move . 'CastleQ'};
    }
    
    /**
     * Move a piece from one square to another, and mark the old square as empty
     *
     * NO validation is performed, use {@link moveSquare()} for validation.
     *
     * @param string [a-h][1-8] square to move from
     * @param string [a-h][1-8] square to move to
     * @param string piece to promote to, if this is a promotion move
     * @access protected
     */
    function _moveAlgebraic($from, $to, $promote = '')
    {
        if ($to == $this->_enPassantSquare && $this->isPawn($this->_board[$from])) {
            $rank = ($to{1} == '3') ? '4' : '5';
            // this piece was just taken
            $this->_takePiece($to{0} . $rank);
            $this->_board[$to{0} . $rank] = $to{0} . $rank;
        }
        if ($this->_board[$to] != $to) {
            // this piece was just taken
            $this->_takePiece($to);
        }
        // mark the piece as moved
        $this->_movePiece($from, $to, $promote);
        $this->_board[$to] = $this->_board[$from];
        $this->_board[$from] = $from;
    }
    
    /**
     * Parse out the segments of a move (minus any annotations)
     * @param string
     * @return array
     * @access protected
     */
    function _parseMove($move)
    {
        if ($move == 'O-O') {
            return array(GAMES_CHESS_CASTLE => 'K');
        }
        if ($move == 'O-O-O') {
            return array(GAMES_CHESS_CASTLE => 'Q');
        }
        // pawn moves
        if (preg_match('/^P?(([a-h])([1-8])?(x))?([a-h][1-8])(=?([QRNB]))?$/', $move, $match)) {
            if ($match[2]) {
                $takesfrom = $match[2]{0};
            } else {
                $takesfrom = '';
            }
            $res = array(
                'takesfrom' => $takesfrom,
                'takes' => $match[4],
                'disambiguate' => '',
                'square' => $match[5],
                'promote' => '',
                'piece' => 'P',
            );
            if (isset($match[7])) {
                $res['promote'] = $match[7];
            }
            return array(GAMES_CHESS_PAWNMOVE => $res);
        // piece moves
        } elseif (preg_match('/^(K)(x)?([a-h][1-8])$/', $move, $match)) {
            $res = array(
                'takesfrom' => false,
                'piece' => $match[1],
                'disambiguate' => '',
                'takes' => $match[2],
                'square' => $match[3],
            );
            return array(GAMES_CHESS_PIECEMOVE => $res);
        } elseif (preg_match('/^([QRBN])([a-h]|[1-8]|[a-h][1-8])?(x)?([a-h][1-8])$/', $move, $match)) {
            $res = array(
                'takesfrom' => false,
                'piece' => $match[1],
                'disambiguate' => $match[2],
                'takes' => $match[3],
                'square' => $match[4],
            );
            return array(GAMES_CHESS_PIECEMOVE => $res);
        } elseif (preg_match('/^([QRBN])@([a-h][1-8])$/', $move, $match)) {
            $res = array(
                'piece' => $match[1],
                'square' => $match[2],
            );
            return array(GAMES_CHESS_PIECEPLACEMENT => $res);
        // error
        } elseif (preg_match('/^([P])@([a-h][2-7])$/', $move, $match)) {
            $res = array(
                'piece' => $match[1],
                'square' => $match[2],
            );
            return array(GAMES_CHESS_PIECEPLACEMENT => $res);
        // error
        } elseif (preg_match('/^([P])@([a-h][18])$/', $move, $match)) {
            return $this->raiseError(GAMES_CHESS_ERROR_CANT_PLACE_18, array('san' => $move));
        // error
        } else {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SAN,
                array('pgn' => $move));
        }
    }
    
    
    /**
     * Set up the board with the starting position
     *
     * Must be overridden in child classes
     * @abstract
     * @access protected
     */
    function _setupStartingPosition()
    {
        trigger_error("Error: do not use abstract Games_Chess class", E_USER_ERROR);
    }
    
    /**
     * Parse a Forsyth-Edwards Notation (FEN) chessboard position string, and
     * set up the chessboard with this position
     * @param string
     * @access private
     */
    function _parseFen($fen)
    {
        $splitfen = explode(' ', $fen);
        if (count($splitfen) != 6) {
            return $this->raiseError(GAMES_CHESS_ERROR_FEN_COUNT,
                array('fen' => $fen, 'sections' => count($splitfen)));
        }

        foreach($splitfen as $index => $test) {
            if ($test == '') {
                return $this->raiseError(GAMES_CHESS_ERROR_EMPTY_FEN,
                    array('fen' => $fen, 'section' => $index));
            }
        }

        $this->blankBoard();
        $loc = 'a8';
        $idx = 0;
        $FEN = $splitfen[0];

        // parse position section
        while ($idx < strlen($FEN)) {
            $c = $FEN{$idx};
            switch ($c) {
                case "K" :
                case "Q" :
                case "R" :
                case "B" :
                case "N" :
                case "P" :
                    if (!class_exists('PEAR')) {
                        require_once 'PEAR.php';
                    }
                    PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
                    $err = $this->addPiece('W', $c, $loc);
                    PEAR::popErrorHandling();
                    if ($this->isError($err)) {
                        if ($err->getCode() == GAMES_CHESS_ERROR_MULTIPIECE) {
                            return $this->raiseError(GAMES_CHESS_ERROR_FEN_MULTIPIECE,
                            array('fen' => $fen, 'color' => 'W', 'piece' => $c));
                        } else {
                            return $err;
                        }
                    }
                break;
                case "k" :
                case "q" :
                case "r" :
                case "b" :
                case "n" :
                case "p" :
                    if (!class_exists('PEAR')) {
                        require_once 'PEAR.php';
                    }
                    PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
                    $err = $this->addPiece('B', strtoupper($c), $loc);
                    PEAR::popErrorHandling();
                    if ($this->isError($err)) {
                        if ($err->getCode() == GAMES_CHESS_ERROR_MULTIPIECE) {
                            return $this->raiseError(GAMES_CHESS_ERROR_FEN_MULTIPIECE,
                            array('fen' => $fen, 'color' => 'B', 'piece' => $c));
                        } else {
                            return $err;
                        }
                    }
                break;

                case "1" :
                case "2" :
                case "3" :
                case "4" :
                case "5" :
                case "6" :
                case "7" :
                case "8" :
                    $loc{0} = chr(ord($loc{0}) + ($c - 1));
                break;
                case "/" :
                    $loc{1} = $loc{1} - 1;
                    $loc{0} = 'a';
                    $idx++;
                    continue 2;
                break;
                default :
                    return $this->raiseError(GAMES_CHESS_ERROR_FEN_INVALIDPIECE,
                        array('fen' => $fen, 'fenchar' => $c));
                break;
            }
            $idx++;
            $loc{0} = chr(ord($loc{0}) + 1);
            if (ord($loc{0}) > ord('h')) {
                if (strlen($FEN) > $idx && $FEN{$idx} != '/') {
                    return $this->raiseError(GAMES_CHESS_ERROR_FEN_TOOMUCH,
                        array('fen' => $fen));
                }
            }
        }
        if ($loc != 'i1') {
            return $this->raiseError(GAMES_CHESS_ERROR_FEN_TOOLITTLE,
                array('fen' => $fen));
        }

        // parse who's to move
        if (!in_array($splitfen[1], array('w', 'b', 'W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_FEN_TOMOVEWRONG,
                array('fen' => $fen, 'tomove' => $splitfen[1]));
        }
        $this->_move = strtoupper($splitfen[1]);

        // parse castling rights
        if (strlen($splitfen[2]) > 4) {
            return $this->raiseError(GAMES_CHESS_ERROR_FEN_CASTLETOOLONG,
                array('fen' => $fen, 'castle' => $splitfen[2]));
        }
        $this->_WCastleQ = false;
        $this->_WCastleK = false;
        $this->_BCastleQ = false;
        $this->_BCastleK = false;
        if ($splitfen[2] != '-') {
            for ($i = 0; $i < 4; $i++) {
                if ($i >= strlen($splitfen[2])) {
                    continue;
                }
                switch ($splitfen[2]{$i}) {
                    case 'K' :
                        $this->_WCastleK = true;
                    break;
                    case 'Q' :
                        $this->_WCastleQ = true;
                    break;
                    case 'k' :
                        $this->_BCastleK = true;
                    break;
                    case 'q' :
                        $this->_BCastleQ = true;
                    break;
                    default:
                        return $this->raiseError(GAMES_CHESS_ERROR_FEN_CASTLEWRONG,
                            array('fen' => $fen, 'castle' => $splitfen[2]{$i}));
                    break;
                }
            }
        }

        // parse en passant square
        $this->_enPassantSquare = '-';
        if ($splitfen[3] != '-') {
            if (!preg_match('/^[a-h][36]$/', $splitfen[3])) {
                return $this->raiseError(GAMES_CHESS_ERROR_FEN_INVALID_EP,
                    array('fen' => $fen, 'enpassant' => $splitfen[3]));
            }
            $this->_enPassantSquare = $splitfen[3];
        }

        // parse half moves since last pawn move or capture
        if (!is_numeric($splitfen[4])) {
            return $this->raiseError(GAMES_CHESS_ERROR_FEN_INVALID_PLY,
                array('fen' => $fen, 'ply' => $splitfen[4]));
        }
        $this->_halfMoves = $splitfen[4];

        // parse move number
        if (!is_numeric($splitfen[5])) {
            return $this->raiseError(GAMES_CHESS_ERROR_FEN_INVALID_MOVENUMBER,
                array('fen' => $fen, 'movenumber' => $splitfen[5]));
        }
        $this->_moveNumber = $splitfen[5];
        return true;
    }
    
    /**
     * Validate a move
     * @param array parsed move array from {@link _parsedMove()}
     * @return true|PEAR_Error
     * @throws GAMES_CHESS_ERROR_IN_CHECK
     * @throws GAMES_CHESS_ERROR_CANT_CK
     * @throws GAMES_CHESS_ERROR_CK_PIECES_IN_WAY
     * @throws GAMES_CHESS_ERROR_CANT_CQ
     * @throws GAMES_CHESS_ERROR_CQ_PIECES_IN_WAY
     * @throws GAMES_CHESS_ERROR_CASTLE_WOULD_CHECK
     * @throws GAMES_CHESS_ERROR_CANT_CAPTURE_OWN
     * @throws GAMES_CHESS_ERROR_STILL_IN_CHECK
     * @throws GAMES_CHESS_ERROR_MOVE_WOULD_CHECK
     * @access protected
     */
    function _validMove($move)
    {
        list($type, $info) = each($move);
        $this->startTransaction();
        $valid = false;
        switch ($type) {
            case GAMES_CHESS_CASTLE :
                if ($this->inCheck($this->_move)) {
                    $this->rollbackTransaction();
                    return $this->raiseError(GAMES_CHESS_ERROR_IN_CHECK);
                }
                if ($info == 'K') {
                    if ($this->_move == 'W') {
                        if (!$this->_WCastleK) {
                            $this->rollbackTransaction();
                            return $this->raiseError(GAMES_CHESS_ERROR_CANT_CK);
                        }
                        if ($this->_board['f1'] != 'f1' || $this->_board['g1'] != 'g1') {
                            $this->rollbackTransaction();
                            return $this->raiseError(GAMES_CHESS_ERROR_CK_PIECES_IN_WAY);
                        }
                        $kingsquares = array('f1', 'g1');
                        $on = 'e1';
                    } else {
                        if (!$this->_BCastleK) {
                            $this->rollbackTransaction();
                            return $this->raiseError(GAMES_CHESS_ERROR_CANT_CK);
                        }
                        if ($this->_board['f8'] != 'f8' || $this->_board['g8'] != 'g8') {
                            $this->rollbackTransaction();
                            return $this->raiseError(GAMES_CHESS_ERROR_CK_PIECES_IN_WAY);
                        }
                        $kingsquares = array('f8', 'g8');
                        $on = 'e8';
                    }
                } else {
                    if ($this->_move == 'W') {
                        if (!$this->_WCastleQ) {
                            $this->rollbackTransaction();
                            return $this->raiseError(GAMES_CHESS_ERROR_CANT_CQ);
                        }
                        if ($this->_board['d1'] != 'd1' ||
                              $this->_board['c1'] != 'c1' ||
                              $this->_board['b1'] != 'b1') {
                            $this->rollbackTransaction();
                            return $this->raiseError(GAMES_CHESS_ERROR_CQ_PIECES_IN_WAY);
                        }
                        $kingsquares = array('d1', 'c1');
                        $on = 'e1';
                    } else {
                        if (!$this->_BCastleQ) {
                            $this->rollbackTransaction();
                            return $this->raiseError(GAMES_CHESS_ERROR_CANT_CQ);
                        }
                        if ($this->_board['d8'] != 'd8' ||
                              $this->_board['c8'] != 'c8' ||
                              $this->_board['b8'] != 'b8') {
                            $this->rollbackTransaction();
                            return $this->raiseError(GAMES_CHESS_ERROR_CQ_PIECES_IN_WAY);
                        }
                        $kingsquares = array('d8', 'c8');
                        $on = 'e8';
                    }
                }
                
                // check every square the king could move to and make sure
                // we wouldn't be in check
                foreach ($kingsquares as $square) {
                    $this->_moveAlgebraic($on, $square);
                    if ($this->inCheck($this->_move)) {
                        $this->rollbackTransaction();
                        return $this->raiseError(GAMES_CHESS_ERROR_CASTLE_WOULD_CHECK);
                    }
                    $on = $square;
                }
                $valid = true;
            break;
            case GAMES_CHESS_PIECEMOVE :
            case GAMES_CHESS_PAWNMOVE :
                if (!$this->isError($piecesq = $this->_getSquareFromParsedMove($info))) {
                    $wasinCheck = $this->inCheck($this->_move);
                    $piece = $this->_board[$info['square']];
                    if ($info['takes'] && $this->_board[$info['square']] ==
                          $info['square']) {
                        if (!($info['square'] == $this->_enPassantSquare &&
                              $info['piece'] == 'P')) {
                            return $this->raiseError(GAMES_CHESS_ERROR_NO_PIECE,
                                array('square' => $info['square']));
                        }
                    }
                    $this->_moveAlgebraic($piecesq, $info['square']);
                    $valid = !$this->inCheck($this->_move);
                    if ($wasinCheck && !$valid) {
                        $this->rollbackTransaction();
                        return $this->raiseError(GAMES_CHESS_ERROR_STILL_IN_CHECK);
                    } elseif (!$valid) {
                        $this->rollbackTransaction();
                        return $this->raiseError(GAMES_CHESS_ERROR_MOVE_WOULD_CHECK);
                    }
                } else {
                    $this->rollbackTransaction();
                    return $piecesq;
                }
            break;
        }
        $this->rollbackTransaction();
        return $valid;
    }
    
    /**
     * Convert a starting and ending algebraic square into SAN
     * @access protected
     * @param string [a-h][1-8] square piece is on
     * @param string [a-h][1-8] square piece moves to
     * @param string Q|R|B|N
     * @return string|PEAR_Error
     * @throws GAMES_CHESS_ERROR_INVALID_PROMOTE
     * @throws GAMES_CHESS_ERROR_INVALID_SQUARE
     * @throws GAMES_CHESS_ERROR_NO_PIECE
     * @throws GAMES_CHESS_ERROR_WRONG_COLOR
     * @throws GAMES_CHESS_ERROR_CANT_MOVE_THAT_WAY
     */
    function _convertSquareToSAN($from, $to, $promote = '')
    {
        if ($promote == '') {
            $promote = 'Q';
        }
        $promote = strtoupper($promote);
        if (!in_array($promote, array('Q', 'B', 'N', 'R'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_PROMOTE,
                array('piece' => $promote));
        }
        $SAN = '';
        if (!preg_match('/^[a-h][1-8]$/', $from)) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SQUARE,
                array('square' => $from));
        }
        if (!preg_match('/^[a-h][1-8]$/', $to)) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SQUARE,
                array('square' => $to));
        }
        $piece = $this->_squareToPiece($from);
        if (!$piece) {
            return $this->raiseError(GAMES_CHESS_ERROR_NO_PIECE,
                array('square' => $from));
        }
        if ($piece['color'] != $this->_move) {
            return $this->raiseError(GAMES_CHESS_ERROR_WRONG_COLOR,
                array('square' => $from));
        }
        $moves = $this->getPossibleMoves($piece['piece'], $from, $piece['color']);
        if (!in_array($to, $moves)) {
            return $this->raiseError(GAMES_CHESS_ERROR_CANT_MOVE_THAT_WAY,
                array('from' => $from, 'to' => $to));
        }
        if ($piece['piece'] == 'K' && !in_array($to, $this->_getKingSquares($from))) {
            // this is a castling attempt
            if ($to{0} == 'g') {
                return 'O-O';
            } else {
                return 'O-O-O';
            }
        }
        $others = array();
        if ($piece['piece'] != 'K' && $piece['piece'] != 'P') {
            $others = $this->_getAllPieceSquares($piece['piece'],
                                                 $piece['color'], $from);
        }
        $disambiguate = '';
        $ambiguous = array();
        if (count($others)) {
            foreach ($others as $square) {
                if (in_array($to, $this->getPossibleMoves($piece['piece'], $square,
                                                          $piece['color']))) {
                    // other pieces can move to this square - need to disambiguate
                    $ambiguous[] = $square;
                }
            }
        }
        if (count($ambiguous) == 1) {
            if ($ambiguous[0]{0} != $from{0}) {
                $disambiguate = $from{0};
            } elseif ($ambiguous[0]{1} != $from{1}) {
                $disambiguate = $from{1};
            } else {
                $disambiguate = $from;
            }
        } elseif (count($ambiguous)) {
            $disambiguate = $from;
        }
        if ($piece['piece'] == 'P') {
            if ($from{0} != $to{0}) {
                $SAN = $from{0};
            }
        } else {
            $SAN = $piece['piece'];
        }
        $SAN .= $disambiguate;
        if ($this->_board[$to] != $to) {
            $SAN .= 'x';
        } else {
            if ($piece['piece'] == 'P' && $to == $this->_enPassantSquare) {
                $SAN .= 'x';
            }
        }
        $SAN .= $to;
        if ($piece['piece'] == 'P' && ($to{1} == '1' || $to{1} == '8')) {
            $SAN .= '=' . $promote;
        }
        return $SAN;
    }
    
    /**
     * Get a list of all possible theoretical squares a piece of this nature
     * and color could move to with the current board and game setup.
     *
     * This method will return all valid moves without determining the presence
     * of check
     * @param K|P|Q|R|B|N Piece name
     * @param string [a-h][1-8] algebraic location of the piece
     * @param B|W color of the piece
     * @param boolean Whether to return shortcut king moves for castling
     * @return array|PEAR_Error
     * @throws GAMES_CHESS_ERROR_INVALID_COLOR
     * @throws GAMES_CHESS_ERROR_INVALID_SQUARE
     * @throws GAMES_CHESS_ERROR_INVALID_PIECE
     */
    function getPossibleMoves($piece, $square, $color = null, $returnCastleMoves = true)
    {
        if (is_null($color)) {
            $color = $this->_move;
        }
        $color = strtoupper($color);
        if (!in_array($color, array('W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_COLOR,
                array('color' => $color));
        }
        if (!preg_match('/^[a-h][1-8]$/', $square)) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SQUARE,
                array('square' => $square));
        }
        $piece = strtoupper($piece);
        if (!in_array($piece, array('K', 'Q', 'B', 'N', 'R', 'P'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_PIECE,
                array('piece' => $piece));
        }
        switch ($piece) {
            case 'K' :
                return $this->getPossibleKingMoves($square, $color, $returnCastleMoves);
            break;
            case 'Q' :
                return $this->getPossibleQueenMoves($square, $color);
            break;
            case 'B' :
                return $this->getPossibleBishopMoves($square, $color);
            break;
            case 'N' :
                return $this->getPossibleKnightMoves($square, $color);
            break;
            case 'R' :
                return $this->getPossibleRookMoves($square, $color);
            break;
            case 'P' :
                return $this->getPossiblePawnMoves($square, $color);
            break;
        }
    }
    
    /**
     * Get the set of squares that are diagonals from this square on an empty board.
     *
     * WARNING: assumes valid input
     * @param string [a-h][1-8]
     * @param boolean if true, simply returns an array of all squares
     * @return array Format:
     *
     * <pre>
     * array(
     *   'NE' => array(square, square),
     *   'NW' => array(square, square),
     *   'SE' => array(square, square),
     *   'SW' => array(square, square)
     * )
     * </pre>
     *
     * Think of the diagonal directions as on a map.  squares are listed with
     * closer squares first
     */
    function _getDiagonals($square, $returnFlatArray = false)
    {
        $nw = ($square{0} != 'a') && ($square{1} != '8');
        $ne = ($square{0} != 'h') && ($square{1} != '8');
        $sw = ($square{0} != 'a') && ($square{1} != '1');
        $se = ($square{0} != 'h') && ($square{1} != '1');
        if ($nw) {
            $nw = array();
            $i = $square;
            while(ord($i{0}) > ord('a') && ord($i{1}) < ord('8')) {
                $i{0} = chr(ord($i{0}) - 1);
                $i{1} = chr(ord($i{1}) + 1);
                $nw[] = $i;
            }
        }
        if ($ne) {
            $ne = array();
            $i = $square;
            while(ord($i{0}) < ord('h') && ord($i{1}) < ord('8')) {
                $i{0} = chr(ord($i{0}) + 1);
                $i{1} = chr(ord($i{1}) + 1);
                $ne[] = $i;
            }
        }
        if ($sw) {
            $sw = array();
            $i = $square;
            while(ord($i{0}) > ord('a') && ord($i{1}) > ord('1')) {
                $i{0} = chr(ord($i{0}) - 1);
                $i{1} = chr(ord($i{1}) - 1);
                $sw[] = $i;
            }
        }
        if ($se) {
            $se = array();
            $i = $square;
            while(ord($i{0}) < ord('h') && ord($i{1}) > ord('1')) {
                $i{0} = chr(ord($i{0}) + 1);
                $i{1} = chr(ord($i{1}) - 1);
                $se[] = $i;
            }
        }
        if ($returnFlatArray) {
            if (!$nw) {
                $nw = array();
            }
            if (!$sw) {
                $sw = array();
            }
            if (!$ne) {
                $ne = array();
            }
            if (!$se) {
                $se = array();
            }
            return array_merge($ne, array_merge($nw, array_merge($se, $sw)));
        }
        return array('NE' => $ne, 'NW' => $nw, 'SE' => $se, 'SW' => $sw);
    }
    
    /**
     * Get the set of squares that are diagonals from this square on an empty board.
     *
     * WARNING: assumes valid input
     * @param string [a-h][1-8]
     * @param boolean if true, simply returns an array of all squares
     * @return array Format:
     *
     * <pre>
     * array(
     *   'N' => array(square, square),
     *   'E' => array(square, square),
     *   'S' => array(square, square),
     *   'W' => array(square, square)
     * )
     * </pre>
     *
     * Think of the horizontal directions as on a map.  squares are listed with
     * closer squares first
     * @access protected
     */
    function _getRookSquares($square, $returnFlatArray = false)
    {
        $n = ($square{1} != '8');
        $e = ($square{0} != 'h');
        $s = ($square{1} != '1');
        $w = ($square{0} != 'a');
        if ($n) {
            $n = array();
            $i = $square;
            while(ord($i{1}) < ord('8')) {
                $i{1} = chr(ord($i{1}) + 1);
                $n[] = $i;
            }
        }
        if ($e) {
            $e = array();
            $i = $square;
            while(ord($i{0}) < ord('h')) {
                $i{0} = chr(ord($i{0}) + 1);
                $e[] = $i;
            }
        }
        if ($s) {
            $s = array();
            $i = $square;
            while(ord($i{1}) > ord('1')) {
                $i{1} = chr(ord($i{1}) - 1);
                $s[] = $i;
            }
        }
        if ($w) {
            $w = array();
            $i = $square;
            while(ord($i{0}) > ord('a')) {
                $i{0} = chr(ord($i{0}) - 1);
                $w[] = $i;
            }
        }
        if ($returnFlatArray) {
            if (!$n) {
                $n = array();
            }
            if (!$s) {
                $s = array();
            }
            if (!$e) {
                $e = array();
            }
            if (!$w) {
                $w = array();
            }
            return array_merge($n, array_merge($s, array_merge($e, $w)));
        }
        return array('N' => $n, 'E' => $e, 'S' => $s, 'W' => $w);
    }
    
    /**
     * Get all the squares a queen could go to on a blank board
     *
     * WARNING: assumes valid input
     * @return array combines contents of {@link _getRookSquares()} and
     *               {@link _getDiagonals()}
     * @param string [a-h][1-8]
     * @param boolean if true, simply returns an array of all squares
     * @access protected
     */
    function _getQueenSquares($square, $returnFlatArray = false)
    {
        return array_merge($this->_getRookSquares($square, $returnFlatArray),
                           $this->_getDiagonals($square, $returnFlatArray));
    }
    
    /**
     * Get all the squares a knight could move to on an empty board
     *
     * WARNING: assumes valid input
     * @param string [a-h][1-8]
     * @param boolean if true, simply returns an array of all squares
     * @return array Returns an array of all the squares organized by compass
     *               point, that a knight can go to.  These squares may be indexed
     *               by any of WNW, NNW, NNE, ENE, ESE, SSE, SSW or WSW, unless
     *               $returnFlatArray is true, in which case an array of squares
     *               is returned
     * @access protected
     */
    function _getKnightSquares($square, $returnFlatArray = false)
    {
        $squares = array();
        // west-northwest square
        if (ord($square{0}) > ord('b') && $square{1} < 8) {
            $squares['WNW'] = chr(ord($square{0}) - 2) . ($square{1} + 1);
        }
        // north-northwest square
        if (ord($square{0}) > ord('a') && $square{1} < 7) {
            $squares['NNW'] = chr(ord($square{0}) - 1) . ($square{1} + 2);
        }
        // north-northeast square
        if (ord($square{0}) < ord('h') && $square{1} < 7) {
            $squares['NNE'] = chr(ord($square{0}) + 1) . ($square{1} + 2);
        }
        // east-northeast square
        if (ord($square{0}) < ord('g') && $square{1} < 8) {
            $squares['ENE'] = chr(ord($square{0}) + 2) . ($square{1} + 1);
        }
        // east-southeast square
        if (ord($square{0}) < ord('g') && $square{1} > 1) {
            $squares['ESE'] = chr(ord($square{0}) + 2) . ($square{1} - 1);
        }
        // south-southeast square
        if (ord($square{0}) < ord('h') && $square{1} > 2) {
            $squares['SSE'] = chr(ord($square{0}) + 1) . ($square{1} - 2);
        }
        // south-southwest square
        if (ord($square{0}) > ord('a') && $square{1} > 2) {
            $squares['SSW'] = chr(ord($square{0}) - 1) . ($square{1} - 2);
        }
        // west-southwest square
        if (ord($square{0}) > ord('b') && $square{1} > 1) {
            $squares['WSW'] = chr(ord($square{0}) - 2) . ($square{1} - 1);
        }
        if ($returnFlatArray) {
            return array_values($squares);
        }
        return $squares;
    }
    
    /**
     * Get a list of all the squares a king could castle to on an empty board
     *
     * WARNING: assumes valid input
     * @param string [a-h][1-8]
     * @return array
     * @access protected
     * @since 0.7alpha
     */
    function _getCastleSquares($square)
    {
        $ret = array();
        if ($this->_move == 'W') {
            if ($square == 'e1' && $this->_WCastleK) {
                $ret[] = 'g1';
            }
            if ($square == 'e1' && $this->_WCastleQ) {
                $ret[] = 'c1';
            }

        } else {
            if ($square == 'e8' && $this->_BCastleK) {
                $ret[] = 'g8';
            }
            if ($square == 'e8' && $this->_BCastleQ) {
                $ret[] = 'c8';
            }
        }
        return $ret;
    }
    
    /**
     * Get a list of all the squares a king could move to on an empty board
     *
     * WARNING: assumes valid input
     * @param string [a-h][1-8]
     * @return array
     * @access protected
     */
    function _getKingSquares($square)
    {
        $squares = array();
        if (ord($square{0}) - ord('a')) {
            $squares[] = chr(ord($square{0}) - 1) . $square{1};
            if ($square{1} < 8) {
                $squares[] = chr(ord($square{0}) - 1) . ($square{1} + 1);
            }
            if ($square{1} > 1) {
                $squares[] = chr(ord($square{0}) - 1) . ($square{1} - 1);
            }
        }
        if (ord($square{0}) - ord('h')) {
            $squares[] = chr(ord($square{0}) + 1) . $square{1};
            if ($square{1} < 8) {
                $squares[] = chr(ord($square{0}) + 1) . ($square{1} + 1);
            }
            if ($square{1} > 1) {
                $squares[] = chr(ord($square{0}) + 1) . ($square{1} - 1);
            }
        }
        if ($square{1} > 1) {
            $squares[] = $square{0} . ($square{1} - 1);
        }
        if ($square{1} < 8) {
            $squares[] = $square{0} . ($square{1} + 1);
        }
        return $squares;
    }
    
    /**
     * Get the location of all pieces on the board of a certain color
     *
     * Default is the color that is about to move
     * @param W|B
     * @return array|PEAR_Error
     * @throws GAMES_CHESS_ERROR_INVALID_COLOR
     */
    function getPieceLocations($color = null)
    {
        if (is_null($color)) {
            $color = $this->_move;
        }
        $color = strtoupper($color);
        if (!in_array($color, array('W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_COLOR,
                array('color' => $color));
        }
        return $this->_getAllPieceLocations($color);
    }

    /**
     * Get the location of every piece on the board of color $color
     * @param W|B color of pieces to check
     * @return array
     * @abstract
     * @access protected
     */
    function _getAllPieceLocations($color)
    {
        trigger_error('Error: do not use abstract Games_Chess class', E_USER_ERROR);
    }
    
    /**
     * Get all legal Knight moves (checking of the king is not taken into account)
     * @param string [a-h][1-8] Location of piece
     * @param W|B color of piece, or null to use current piece to move
     * @return array
     */
    function getPossibleKnightMoves($square, $color = null)
    {
        if (is_null($color)) {
            $color = $this->_move;
        }
        $color = strtoupper($color);
        if (!in_array($color, array('W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_COLOR,
                array('color' => $color));
        }
        if (!preg_match('/^[a-h][1-8]$/', $square)) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SQUARE,
                array('square' => $square));
        }
        $allmoves = $this->_getKnightSquares($square);
        $mypieces = $this->getPieceLocations($color);
        return array_values(array_diff($allmoves, $mypieces));
    }
    
    /**
     * Get all legal Bishop moves (checking of the king is not taken into account)
     * @param string [a-h][1-8] Location of piece
     * @param W|B color of piece, or null to use current piece to move
     * @return array
     */
    function getPossibleBishopMoves($square, $color = null)
    {
        if (is_null($color)) {
            $color = $this->_move;
        }
        $color = strtoupper($color);
        if (!in_array($color, array('W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_COLOR,
                array('color' => $color));
        }
        if (!preg_match('/^[a-h][1-8]$/', $square)) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SQUARE,
                array('square' => $square));
        }
        $allmoves = $this->_getDiagonals($square);
        $mypieces = $this->getPieceLocations($color);
        foreach($mypieces as $loc) {
            // go through the diagonals, and remove squares behind our own pieces
            // and also remove the piece's square
            // as bishops cannot pass through any pieces.
            if (is_array($allmoves['NW']) && in_array($loc, $allmoves['NW'])) {
                $pos = array_search($loc, $allmoves['NW']);
                $allmoves['NW'] = array_slice($allmoves['NW'], 0, $pos);
            }
            if (is_array($allmoves['NE']) && in_array($loc, $allmoves['NE'])) {
                $pos = array_search($loc, $allmoves['NE']);
                $allmoves['NE'] = array_slice($allmoves['NE'], 0, $pos);
            }
            if (is_array($allmoves['SE']) && in_array($loc, $allmoves['SE'])) {
                $pos = array_search($loc, $allmoves['SE']);
                $allmoves['SE'] = array_slice($allmoves['SE'], 0, $pos);
            }
            if (is_array($allmoves['SW']) && in_array($loc, $allmoves['SW'])) {
                $pos = array_search($loc, $allmoves['SW']);
                $allmoves['SW'] = array_slice($allmoves['SW'], 0, $pos);
            }
        }
        $enemypieces = $this->getPieceLocations($color == 'W' ? 'B' : 'W');
        foreach($enemypieces as $loc) {
            // go through the diagonals, and remove squares behind enemy pieces
            // and include the piece's square, since we can capture it
            // but bishops cannot pass through any pieces.
            if (is_array($allmoves['NW']) && in_array($loc, $allmoves['NW'])) {
                $pos = array_search($loc, $allmoves['NW']);
                $allmoves['NW'] = array_slice($allmoves['NW'], 0, $pos + 1);
            }
            if (is_array($allmoves['NE']) && in_array($loc, $allmoves['NE'])) {
                $pos = array_search($loc, $allmoves['NE']);
                $allmoves['NE'] = array_slice($allmoves['NE'], 0, $pos + 1);
            }
            if (is_array($allmoves['SE']) && in_array($loc, $allmoves['SE'])) {
                $pos = array_search($loc, $allmoves['SE']);
                $allmoves['SE'] = array_slice($allmoves['SE'], 0, $pos + 1);
            }
            if (is_array($allmoves['SW']) && in_array($loc, $allmoves['SW'])) {
                $pos = array_search($loc, $allmoves['SW']);
                $allmoves['SW'] = array_slice($allmoves['SW'], 0, $pos + 1);
            }
        }
        $newmoves = array();
        foreach($allmoves as $key => $value) {
            if (!$value) {
                continue;
            }
            $newmoves = array_merge($newmoves, $value);
        }
        return array_values(array_diff($newmoves, $mypieces));
    }

    /**
     * Get all legal Rook moves (checking of the king is not taken into account)
     * @param string [a-h][1-8] Location of piece
     * @param W|B color of piece, or null to use current piece to move
     * @return array
     */
    function getPossibleRookMoves($square, $color = null)
    {
        if (is_null($color)) {
            $color = $this->_move;
        }
        $color = strtoupper($color);
        if (!in_array($color, array('W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_COLOR,
                array('color' => $color));
        }
        if (!preg_match('/^[a-h][1-8]$/', $square)) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SQUARE,
                array('square' => $square));
        }
        $allmoves = $this->_getRookSquares($square);
        $mypieces = $this->getPieceLocations($color);
        foreach($mypieces as $loc) {
            // go through the rook squares, and remove squares behind our own pieces
            // and also remove the piece's square
            // as rooks cannot pass through any pieces.
            if (is_array($allmoves['N']) && in_array($loc, $allmoves['N'])) {
                $pos = array_search($loc, $allmoves['N']);
                $allmoves['N'] = array_slice($allmoves['N'], 0, $pos);
            }
            if (is_array($allmoves['E']) && in_array($loc, $allmoves['E'])) {
                $pos = array_search($loc, $allmoves['E']);
                $allmoves['E'] = array_slice($allmoves['E'], 0, $pos);
            }
            if (is_array($allmoves['S']) && in_array($loc, $allmoves['S'])) {
                $pos = array_search($loc, $allmoves['S']);
                $allmoves['S'] = array_slice($allmoves['S'], 0, $pos);
            }
            if (is_array($allmoves['W']) && in_array($loc, $allmoves['W'])) {
                $pos = array_search($loc, $allmoves['W']);
                $allmoves['W'] = array_slice($allmoves['W'], 0, $pos);
            }
        }
        $enemypieces = $this->getPieceLocations($color == 'W' ? 'B' : 'W');
        foreach($enemypieces as $loc) {
            // go through the rook squares, and remove squares behind enemy pieces
            // and include the piece's square, since we can capture it
            // but rooks cannot pass through any pieces.
            if (is_array($allmoves['N']) && in_array($loc, $allmoves['N'])) {
                $pos = array_search($loc, $allmoves['N']);
                $allmoves['N'] = array_slice($allmoves['N'], 0, $pos + 1);
            }
            if (is_array($allmoves['E']) && in_array($loc, $allmoves['E'])) {
                $pos = array_search($loc, $allmoves['E']);
                $allmoves['E'] = array_slice($allmoves['E'], 0, $pos + 1);
            }
            if (is_array($allmoves['S']) && in_array($loc, $allmoves['S'])) {
                $pos = array_search($loc, $allmoves['S']);
                $allmoves['S'] = array_slice($allmoves['S'], 0, $pos + 1);
            }
            if (is_array($allmoves['W']) && in_array($loc, $allmoves['W'])) {
                $pos = array_search($loc, $allmoves['W']);
                $allmoves['W'] = array_slice($allmoves['W'], 0, $pos + 1);
            }
        }
        $newmoves = array();
        foreach($allmoves as $key => $value) {
            if (!$value) {
                continue;
            }
            $newmoves = array_merge($newmoves, $value);
        }
        return array_values(array_diff($newmoves, $mypieces));
    }
    
    /**
     * Get all legal Queen moves (checking of the king is not taken into account)
     * @param string [a-h][1-8] Location of piece
     * @param W|B color of piece, or null to use current piece to move
     * @return array
     */
    function getPossibleQueenMoves($square, $color = null)
    {
        $a = $this->getPossibleRookMoves($square, $color);
        if ($this->isError($a)) {
            return $a;
        }
        $b = $this->getPossibleBishopMoves($square, $color);
        if ($this->isError($b)) {
            return $b;
        }
        return array_merge($a, $b);
    }
    
    /**
     * Get all legal Pawn moves (checking of the king is not taken into account)
     * @param string [a-h][1-8] Location of piece
     * @param W|B color of piece, or null to use current piece to move
     * @return array
     */
    function getPossiblePawnMoves($square, $color = null, $enpassant = null)
    {
        if (is_null($color)) {
            $color = $this->_move;
        }
        $color = strtoupper($color);
        if (!in_array($color, array('W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_COLOR,
                array('color' => $color));
        }
        if (!preg_match('/^[a-h][1-8]$/', $square)) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SQUARE,
                array('square' => $square));
        }
        if (is_null($enpassant)) {
            $enpassant = $this->_enPassantSquare;
        }
        $mypieces = $this->getPieceLocations($color);
        $enemypieces = $this->getPieceLocations($color == 'W' ? 'B' : 'W');
        $allmoves = array();
        if ($color == 'W') {
            $dbl = '2';
            $direction = 1;
            // en passant calculation
            if ($square{1} == '5' && in_array(ord($enpassant{0}) - ord($square{0}),
                                              array(1, -1))) {
                if (in_array(chr(ord($square{0}) - 1) . 5,
                             $enemypieces)) {
                    $allmoves[] = chr(ord($square{0}) - 1) . 6;
                }
                if (in_array(chr(ord($square{0}) + 1) . 5,
                             $enemypieces)) {
                    $allmoves[] = chr(ord($square{0}) + 1) . 6;
                }
            }
        } else {
            $dbl = '7';
            $direction = -1;
            // en passant calculation
            if ($square{1} == '4' && in_array(ord($enpassant{0}) - ord($square{0}),
                                              array(1, -1))) {
                if (in_array(chr(ord($square{0}) - 1) . 4,
                             $enemypieces)) {
                    $allmoves[] = chr(ord($square{0}) - 1) . 3;
                }
                if (in_array(chr(ord($square{0}) + 1) . 4,
                             $enemypieces)) {
                    $allmoves[] = chr(ord($square{0}) + 1) . 3;
                }
            }
        }
        if (!in_array($square{0} . ($square{1} + $direction), $mypieces) &&
            !in_array($square{0} . ($square{1} + $direction), $enemypieces))
        {
            $allmoves[] = $square{0} . ($square{1} + $direction);
        }
        if (count($allmoves) && $square{1} == $dbl) {
            if (!in_array($square{0} . ($square{1} + 2 * $direction), $mypieces) &&
                !in_array($square{0} . ($square{1} + 2 * $direction), $enemypieces))
            {
                $allmoves[] = $square{0} . ($square{1} + 2 * $direction);
            }
        }
        if (in_array(chr(ord($square{0}) - 1) . ($square{1} + $direction),
                     $enemypieces)) {
            $allmoves[] = chr(ord($square{0}) - 1) . ($square{1} + $direction);
        }
        if (in_array(chr(ord($square{0}) + 1) . ($square{1} + $direction),
                     $enemypieces)) {
            $allmoves[] = chr(ord($square{0}) + 1) . ($square{1} + $direction);
        }
        return $allmoves;
    }
    
    /**
     * Get all legal King moves (checking of the king is not taken into account)
     * @param string [a-h][1-8] Location of piece
     * @param W|B color of piece, or null to use current piece to move
     * @return array
     * @since 0.7alpha castling is possible by moving the king to the destination square
     */
    function getPossibleKingMoves($square, $color = null, $returnCastleMoves = true)
    {
        if (is_null($color)) {
            $color = $this->_move;
        }
        $color = strtoupper($color);
        if (!in_array($color, array('W', 'B'))) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_COLOR,
                array('color' => $color));
        }
        if (!preg_match('/^[a-h][1-8]$/', $square)) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SQUARE,
                array('square' => $square));
        }
        $newret = $castleret = array();
        $ret = $this->_getKingSquares($square);
        if ($returnCastleMoves) {
            $castleret = $this->_getCastleSquares($square);
        }
        $mypieces = $this->getPieceLocations($color);
        foreach ($ret as $square) {
            if (!in_array($square, $mypieces)) {
                $newret[] = $square;
            }
        }
        return array_merge($newret, $castleret);
    }
    
    /**
     * Return the color of a square (black or white)
     * @param string [a-h][1-8]
     * @access protected
     * @return B|W
     */
    function _getDiagonalColor($square)
    {
        $map = array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6,
            'g' => 7, 'h' => 8);
        $rank = $map[$square{0}];
        $file = $square{1};
        $color = ($rank + $file) % 2;
        return $color ? 'W' : 'B';
    }
    
    function getDiagonalColor($square)
    {
        if (!preg_match('/^[a-h][1-8]$/', $square)) {
            return $this->raiseError(GAMES_CHESS_ERROR_INVALID_SQUARE,
                array('square' => $square));
        }
        return $this->_getDiagonalColor($square);
    }
    
    /**
     * Get all the squares between an attacker and the king where another
     * piece can interpose, or capture the checking piece
     *
     * @param string algebraic square of the checking piece
     * @param string algebraic square of the king
     */
    function _getPathToKing($checkee, $king)
    {
        if ($this->_isKnight($this->_board[$checkee])) {
            return array($checkee);
        } else {
            $path = array();
            // get all the paths 
            $kingpaths = $this->_getQueenSquares($king);
            foreach ($kingpaths as $subpath) {
                if (!$subpath) {
                    continue;
                }
                if (in_array($checkee, $subpath)) {
                    foreach ($subpath as $square) {
                        $path[] = $square;
                        if ($square == $checkee) {
                            return $path;
                        }
                    }
                }
            }
        }
    }
    
    /**
     * @param integer error code from {@link Chess.php}
     * @param array associative array of additional error message data
     * @uses PEAR::raiseError()
     * @return PEAR_Error
     */
    function raiseError($code, $extra = array())
    {
        require_once 'PEAR.php';
        return PEAR::raiseError($this->getMessage($code, $extra), $code,
            null, null, $extra);
    }
    
    /**
     * Get an error message from the code
     *
     * Future versions of this method will be multi-language
     * @return string
     * @param integer Error code
     * @param array extra information to pass for error message creation
     */
    function getMessage($code, $extra)
    {
        $messages = array(
            GAMES_CHESS_ERROR_INVALID_SAN =>
                '"%pgn%" is not a valid algebraic move',
            GAMES_CHESS_ERROR_FEN_COUNT =>
                'Invalid FEN - "%fen%" has %sections% fields, 6 is required',
            GAMES_CHESS_ERROR_EMPTY_FEN => 
                'Invalid FEN - "%fen%" has an empty field at index %section%',
            GAMES_CHESS_ERROR_FEN_TOOMUCH =>
                'Invalid FEN - "%fen%" has too many pieces for a chessboard',
            GAMES_CHESS_ERROR_FEN_TOMOVEWRONG =>
                'Invalid FEN - "%fen%" has invalid to-move indicator, must be "w" or "b"',
            GAMES_CHESS_ERROR_FEN_CASTLETOOLONG =>
                'Invalid FEN - "%fen%" the castling indicator (KQkq) is too long',
            GAMES_CHESS_ERROR_FEN_CASTLEWRONG =>
                'Invalid FEN - "%fen%" the castling indicator "%castle%" is invalid',
            GAMES_CHESS_ERROR_FEN_INVALID_EP =>
                'Invalid FEN - "%fen%" the en passant square indicator "%enpassant%" is invalid',
            GAMES_CHESS_ERROR_FEN_INVALID_PLY =>
                'Invalid FEN - "%fen%" the half-move ply count "%ply%" is not a number',
            GAMES_CHESS_ERROR_FEN_INVALID_MOVENUMBER =>
                'Invalid FEN - "%fen%" the move number "%movenumber%" is not a number',
            GAMES_CHESS_ERROR_IN_CHECK =>
                'The king is in check and that move does not prevent check',
            GAMES_CHESS_ERROR_CANT_CK =>
                'Can\'t castle kingside, either the king or rook has moved',
            GAMES_CHESS_ERROR_CK_PIECES_IN_WAY =>
                'Can\'t castle kingside, pieces are in the way',
            GAMES_CHESS_ERROR_CANT_CQ =>
                'Can\'t castle queenside, either the king or rook has moved',
            GAMES_CHESS_ERROR_CQ_PIECES_IN_WAY =>
                'Can\'t castle queenside, pieces are in the way',
            GAMES_CHESS_ERROR_CASTLE_WOULD_CHECK =>
                'Can\'t castle, it would put the king in check',
            GAMES_CHESS_ERROR_MOVE_WOULD_CHECK =>
                'That move would put the king in check',
            GAMES_CHESS_ERROR_STILL_IN_CHECK =>
                'The move does not remove the check on the king',
            GAMES_CHESS_ERROR_CANT_CAPTURE_OWN =>
                'Cannot capture your own pieces',
            GAMES_CHESS_ERROR_NO_PIECE =>
                'There is no piece on square %square%',
            GAMES_CHESS_ERROR_WRONG_COLOR =>
                'The piece on %square% is not your piece',
            GAMES_CHESS_ERROR_CANT_MOVE_THAT_WAY =>
                'The piece on %from% cannot move to %to%',
            GAMES_CHESS_ERROR_MULTIPIECE =>
                'Too many %color% %piece%s',
            GAMES_CHESS_ERROR_FEN_MULTIPIECE =>
                'Invalid FEN - "%fen%" Too many %color% %piece%s',
            GAMES_CHESS_ERROR_DUPESQUARE =>
                '%dpiece% already occupies square %square%, cannot be replaced by %piece%',
            GAMES_CHESS_ERROR_FEN_INVALIDPIECE =>
                'Invalid FEN - "%fen%" the character "%fenchar%" is not a valid piece, separator or number',
            GAMES_CHESS_ERROR_FEN_TOOLITTLE =>
                'Invalid FEN - "%fen%" has too few pieces for a chessboard',
            GAMES_CHESS_ERROR_INVALID_COLOR =>
                '"%color%" is not a valid piece color, try W or B',
            GAMES_CHESS_ERROR_INVALID_SQUARE =>
                '"%square%" is not a valid square, must be between a1 and h8',
            GAMES_CHESS_ERROR_INVALID_PIECE =>
                '"%piece%" is not a valid piece, must be P, Q, R, N, K or B',
            GAMES_CHESS_ERROR_INVALID_PROMOTE =>
                '"%piece%" is not a valid promotion piece, must be Q, R, N or B',
            GAMES_CHESS_ERROR_TOO_AMBIGUOUS =>
                '"%san%" does not resolve ambiguity between %piece%s on %squares%',
            GAMES_CHESS_ERROR_NOPIECE_CANDOTHAT =>
                'There are no %color% pieces on the board that can do "%san%"',
            GAMES_CHESS_ERROR_MOVE_MUST_CAPTURE =>
                'Capture is possible, "%san%" does not capture',
            GAMES_CHESS_ERROR_NOPIECES_TOPLACE =>
                'There are no captured %color% %piece%s available to place',
            GAMES_CHESS_ERROR_PIECEINTHEWAY =>
                'There is already a piece on %square%, cannot place another there',
            GAMES_CHESS_ERROR_CANT_PLACE_18 =>
                'Placing a piece on the first or back rank is illegal (%san%)',
        );
        $message = $messages[$code];
        foreach ($extra as $key => $value) {
            if (strpos($key, 'piece') !== false) {
                switch(strtoupper($value)) {
                    case 'R' :
                        $value = 'Rook';
                    break;
                    case 'Q' :
                        $value = 'Queen';
                    break;
                    case 'P' :
                        $value = 'Pawn';
                    break;
                    case 'B' :
                        $value = 'Bishop';
                    break;
                    case 'K' :
                        $value = 'King';
                    break;
                    case 'N' :
                        $value = 'Knight';
                    break;
                }
            }
            if ($key == 'color') {
                switch($value) {
                    case 'W' :
                        $value = 'White';
                    break;
                    case 'B' :
                        $value = 'Black';
                    break;
                }
            }
            $message = str_replace('%'.$key.'%', $value, $message);
        }
        return $message;
    }
    
    /**
     * Determines whether the data returned from a method is a PEAR-related
     * error class
     * @param mixed
     * @return boolean
     */
    function isError($err)
    {
        return is_a($err, 'PEAR_Error');
    }
    
    /**
     * Begin a chess piece transaction
     *
     * Transactions are used to attempt moves that may be revoked later, especially
     * in methods like {@link inCheckMate()}
     */
    function startTransaction()
    {
        $state = get_object_vars($this);
        unset($state['_saveState']);
        if (!is_array($this->_saveState)) {
            $this->_saveState = array();
        }
        array_push($this->_saveState, $state);
    }

    /**
     * Set the state of the chess game
     *
     * WARNING: this resets the state without any validation.
     * @param array
     */
    function setState($state)
    {
        foreach($state as $name => $value) {
            $this->$name = $value;
        }
    }

    /**
     * Get the current state of the chess game
     *
     * Use this in conjunction with setState
     * @param array
     */
    function getState()
    {
        return get_object_vars($this);
    }

    /**
     * Remove any possibility of undo.
     */
    function commitTransaction()
    {
        array_pop($this->_saveState);
    }
    
    /**
     * Undo any changes to state since {@link startTransaction()} was first used
     */
    function rollbackTransaction()
    {
        $vars = array_pop($this->_saveState);
        foreach($vars as $name => $value) {
            $this->$name = $value;
        }
    }
}
?>