| // +----------------------------------------------------------------------+ // // $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 * @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: *
     * array(
     *  movenumber => array(White move, Black move),
     *  movenumber => array(White move, Black move),
     * )
     * 
* @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: *
     * array(
     *  movenumber => array(White move, Black move),
     *  movenumber => array(White move, Black move),
     * )
     * 
* @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: *
     * 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". 
     * 
* * 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: *
     * 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. 
     * 
* @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: * *
     * array(
     *   'NE' => array(square, square),
     *   'NW' => array(square, square),
     *   'SE' => array(square, square),
     *   'SW' => array(square, square)
     * )
     * 
* * 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: * *
     * array(
     *   'N' => array(square, square),
     *   'E' => array(square, square),
     *   'S' => array(square, square),
     *   'W' => array(square, square)
     * )
     * 
* * 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; } } } ?>