setFen($fen); } } public function newGame($fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1') { $this->validMoves = null; $this->setFen($fen); } /** * Set new fen position * Example: * $parser = new FenParser0x88(); * $parser->setFen('8/7P/8/8/1k15/8/P7/K7 w - - 0 1'); * * @param string $fen */ public function setFen($fen) { $this->cache = array( 'board' => array(), 'white' => array(), 'black' => array(), 'whiteSliding' => array(), 'blackSliding' => array(), 'king' => array('white' => null, 'black' => null) ); if ($this->fen) { $this->previousFen = $this->fen; } $this->fen = $fen; $this->updateFenArray(); $this->parseFen(); } public function getLongNotation() { return $this->longNotation; } /** * @param array $move * @param bool $shortNotation * @return string */ public function getLongNotationForAMove($move, $shortNotation) { if (strstr($shortNotation, 'O-')) { return $shortNotation; } $fromSquare = $move['from']; $toSquare = $move['to']; $type = $this->cache['board'][Board0x88Config::$mapping[$move['from']]]; $type = Board0x88Config::$typeMapping[$type]; $separator = strstr($shortNotation, 'x') >= 0 ? 'x' : '-'; $ret = $type . $fromSquare . $separator . $toSquare; if (isset($move['promoteTo'])) { $ret .= '=' . $move['promoteTo']; } return $ret; } private function updateFenArray() { $fenParts = explode(" ", $this->fen); $castleCode = 0; for ($i = 0, $count = strlen($fenParts[2]); $i < $count; $i++) { $castleCode += Board0x88Config::$castle[substr($fenParts[2], $i, 1)]; } $this->fenParts = array( 'pieces' => $fenParts[0], 'color' => $fenParts[1], 'castle' => $fenParts[2], 'castleCode' => $castleCode, 'enPassant' => $fenParts[3], 'halfMoves' => $fenParts[4], 'fullMoves' => $fenParts[5] ); } private function parseFen() { $pos = 0; $this->cache['board'] = Board0x88Config::$defaultBoard; $squares = Board0x88Config::$fenSquares; for ($i = 0, $len = strlen($this->fenParts['pieces']); $i < $len; $i++) { $token = $this->fenParts['pieces'][$i]; if (isset(Board0x88Config::$fenPieces[$token])) { $index = Board0x88Config::$mapping[$squares[$pos]]; $type = Board0x88Config::$pieces[$token]; $piece = array( 't' => $type, 's' => $index ); // Board array $this->cache['board'][$index] = $type; // White and black array $this->cache[Board0x88Config::$colorMapping[$token]][] = $piece; // King array if (Board0x88Config::$typeMapping[$type] == 'king') { $this->cache['king' . ($piece['t'] & 0x8 ? 'black' : 'white')] = $piece; } $pos++; } else if ($i < $len - 1 && isset(Board0x88Config::$numbers[$token])) { $pos += intval($token); } } } /** * Returns piece on given square * @param string $square * @return array|null */ public function getPieceOnSquareBoardCoordinate($square) { return $this->getPieceOnSquare(Board0x88Config::$mapping[$square]); } /** * Returns piece on a square. * Example: * $fenBishopOnB3CheckingKingOnG7 = '6k1/6pp/8/8/8/1B6/8/6K1 b - - 0 1'; * $parser = new FenParser0x88($fenBishopOnB3CheckingKingOnG7); * $bishop = $parser->getPieceOnSquare(Board0x88Config::$mapping['b3']); * var_dump($bishop). * * Returns an array * { * "square" : "b3", * "s" : 33, * "t" : 5, * "type" : "bishop", * "color": "white", * "sliding" : 4 * } * * sliding is greater than 0 for bishop, rook and queen. * @param int $square * @return array|null */ public function getPieceOnSquare($square) { $piece = $this->cache['board'][$square]; if (isset($piece)) { return array( 'square' => Board0x88Config::$numberToSquareMapping[$square], 's' => $square, 't' => $piece, 'type' => Board0x88Config::$typeMapping[$piece], 'color' => $piece & 0x8 ? 'black' : 'white', 'sliding' => $piece & 0x4 ); } return null; } public function isValid($move, $fen) { $this->setFen($fen); if (!isset($move['from'])) { $fromAndTo = $this->getFromAndToByNotation($move['m']); $move['from'] = $fromAndTo['from']; $move['to'] = $fromAndTo['to']; } $from = Board0x88Config::$mapping[$move['from']]; $to = Board0x88Config::$mapping[$move['to']]; $obj = $this->getValidMovesAndResult(); $moves = $obj['moves']; if (isset($moves[$from]) && in_array($to, $moves[$from])) { return true; } return false; } /** * Return square of white king, example: "g1" * Example: * $parser = new FenParser0x88('6k1/6pp/8/8/8/1B6/8/6K1 b - - 0 1'); * $whiteKing = $parser->getWhiteKingSquare(); // returns g1 * @return string */ public function getWhiteKingSquare() { return $this->getKingSquareBoardCoordinates("white"); } /** * Returns square of black king, example "g8" * Example: * $parser = new FenParser0x88('6k1/6pp/8/8/8/1B6/8/6K1 b - - 0 1'); * $whiteKing = $parser->getBlackKingSquare(); // returns g8 * @return string */ public function getBlackKingSquare() { return $this->getKingSquareBoardCoordinates("black"); } public function getKingSquareBoardCoordinates($color) { $king = $this->getKing($color); return Board0x88Config::$numberToSquareMapping[$king["s"]]; } /** * Returns king square in numeric format. * Example: * $fenBishopOnB3CheckingKingOnG7 = '6k1/6pp/8/8/8/1B6/8/6K1 b - - 0 1'; * $parser = new FenParser0x88($fenBishopOnB3CheckingKingOnG7); * $king = $parser->getKing("black"); * * returns array("t" : 11, "s" : 128). * * where "t" is type, and "s" is square. Square can be converted to board coordinates using * Board0x88Config::$numberToSquareMapping[$array["s"]] * @param string $color * @return array */ public function getKing($color) { return $this->cache['king' . $color]; } /** * Returns pieces in given color, * example: * * @param $color * @return array */ public function getPiecesOfAColor($color) { return $this->cache[$color]; } /** * Returns en passant square or null * @return string|null */ function getEnPassantSquare() { return ($this->fenParts['enPassant'] != '-') ? $this->fenParts['enPassant'] : null; } private function setEnPassantSquare($square) { $this->fenParts['enPassant'] = $square; } /** * Returns array of sliding pieces(i.e. bishop, rook and queens) * @param string $color * @return array */ function getSlidingPieces($color) { return $this->cache[$color . 'Sliding']; } /** * Returns count half moves made(1. e4 e5 2 Nf3 Nf6 counts as 4 half moves) * @return int */ function getHalfMoves() { return $this->fenParts['halfMoves']; } /** * Returns count full moves made(1. e4 e5 2 Nf3 Nf6 counts as 2 full moves); * @return int */ function getFullMoves() { return $this->fenParts['fullMoves']; } /** * Returns true if white can castle king side * Example: * $fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; * $parser = new FenParser0x88($fen); * $whiteCanCastle = $parser->canWhiteCastleKingSide(); * @return bool */ public function canWhiteCastleKingSide() { return $this->canCastleKingSide("white"); } /** * Returns true if black can castle king side * Example: * $fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; * $parser = new FenParser0x88($fen); * $whiteCanCastle = $parser->canBlackCastleKingSide(); * @return bool */ public function canBlackCastleKingSide() { return $this->canCastleKingSide("black"); } /** * Return boolean true if king side castling is possible * Example: * $fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; * $parser = new FenParser0x88($fen); * $whiteCanCastle = $parser->canCastleKingSide("white"); * $blackCanCastle = $parser->canCastleKingSide("black"); * @param string $color * @return bool */ public function canCastleKingSide($color) { $code = $color === 'white' ? Board0x88Config::$castle['K'] : Board0x88Config::$castle['k']; return ($this->fenParts['castleCode'] & $code) ? true : false; } /** * Return color to move, "white" or "black" * @return string */ function getColor() { return Board0x88Config::$colorAbbreviations[$this->fenParts['color']]; } function switchColor() { $this->fenParts['color'] = $this->fenParts['color'] == 'w' ? 'b' : 'w'; } private function getColorCode() { return $this->fenParts['color']; } /** * Returns true if white can castle queen side(from current fen) * @return bool */ public function canWhiteCastleQueenSide() { return $this->canCastleQueenSide("white"); } /** * Returns true if black can castle queen side (from current fen) * @return bool */ public function canBlackCastleQueenSide() { return $this->canCastleQueenSide("black"); } /** * Returns true if queen side castle for given color is possible(based on fen only, i.e. no checks or obstructions is checked). * @param string $color * @return bool */ function canCastleQueenSide($color) { $code = $color === 'white' ? Board0x88Config::$castle['Q'] : Board0x88Config::$castle['q']; return ($this->fenParts['castleCode'] & $code) ? true : false; } /** * Returns true if two squares are on the same rank. * @param int $square1 * @param int $square2 * @return bool */ function isOnSameRank($square1, $square2) { return ($square1 & 240) === ($square2 & 240); } /** * Returns true if two squares are on the same file * @param int $square1 * @param int $square2 * @return bool */ function isOnSameFile($square1, $square2) { return ($square1 & 15) === ($square2 & 15); } /** * Returns array of valid moves for given color in real board coordinates. * @param string|null $color * @return array * * Example: * $parser = new FenParser0x88('6k1/6p1/4n3/8/8/8/B7/6K1 b - - 0 1'); * $validBlackMoves = $parser->getValidMovesBoardCoordinates("black"); * * returns {"g8":["f7","h7","f8","h8"],"g7":["g6","g5"],"e6":[]} * * where the array key(example "g8") is from square and ["f7","h7","f8","h8"] are valid square for * the piece on "g8" * */ public function getValidMovesBoardCoordinates($color = null) { $movesAndResult = $this->getValidMovesAndResult($color); $moves = $movesAndResult["moves"]; $ret = array(); foreach ($moves as $from => $toSquares) { $fromSquare = Board0x88Config::$numberToSquareMapping[$from]; $squares = array(); foreach ($toSquares as $square) { $squares[] = Board0x88Config::$numberToSquareMapping[$square]; } $ret[$fromSquare] = $squares; } return $ret; } /** * Returns result(0 = undecided, 0.5 = draw, 1 = white wins, -1 = black wins) * @return int */ public function getResult() { $movesAndResult = $this->getValidMovesAndResult(); return $movesAndResult["result"]; } /** * Returns valid moves in 0x88 numeric format and result * @param null $color * @return array|null */ function getValidMovesAndResult($color = null) { if (!$color) { $color = $this->getColor(); } $ret = array(); $enPassantSquare = $this->getEnPassantSquare(); if ($enPassantSquare) { $enPassantSquare = Board0x88Config::$mapping[$enPassantSquare]; } $kingSideCastle = $this->canCastleKingSide($color); $queenSideCastle = $this->canCastleQueenSide($color); $oppositeColor = $color === 'white' ? 'black' : 'white'; $WHITE = $color === 'white' ? true : false; $protectiveMoves = $this->getCaptureAndProtectiveMoves($oppositeColor); $checks = $this->getCountChecks($color, $protectiveMoves); $validSquares = null; $pinned = array(); if ($checks === 2) { $pieces = array($this->getKing($color)); } else { $pieces = $this->cache[$color]; $pinned = $this->getPinned($color); if ($checks === 1) { $validSquares = $this->getValidSquaresOnCheck($color); } } $totalCountMoves = 0; for ($i = 0, $count = count($pieces); $i < $count; $i++) { $piece = $pieces[$i]; $paths = array(); switch ($piece['t']) { // pawns case 0x01: if (!isset($pinned[$piece['s']]) || ($pinned[$piece['s']] && $this->isOnSameFile($piece['s'], $pinned[$piece['s']]['by']))) { if (!$this->cache['board'][$piece['s'] + 16]) { $paths[] = $piece['s'] + 16; if ($piece['s'] < 32) { if (!$this->cache['board'][$piece['s'] + 32]) { $paths[] = $piece['s'] + 32; } } } } if (!isset($pinned[$piece['s']]) || ($pinned[$piece['s']] && $pinned[$piece['s']]['by'] === $piece['s'] + 15)) { if ($enPassantSquare == $piece['s'] + 15 || $this->cache['board'][$piece['s'] + 15] & 0x8) { $paths[] = $piece['s'] + 15; } } if (isset($this->cache['board'][$piece['s'] + 17]) && (!isset($pinned[$piece['s']]) || ($pinned[$piece['s']] && $pinned[$piece['s']]['by'] === $piece['s'] + 17))) { if ($enPassantSquare == $piece['s'] + 17 || ($this->cache['board'][$piece['s'] + 17]) && $this->cache['board'][$piece['s'] + 17] & 0x8) { $paths[] = $piece['s'] + 17; } } break; case 0x09: if (!isset($pinned[$piece['s']]) || ($pinned[$piece['s']] && $this->isOnSameFile($piece['s'], $pinned[$piece['s']]['by']))) { if (!$this->cache['board'][$piece['s'] - 16]) { $paths[] = $piece['s'] - 16; if ($piece['s'] > 87) { if (!$this->cache['board'][$piece['s'] - 32]) { $paths[] = $piece['s'] - 32; } } } } if (!isset($pinned[$piece['s']]) || ($pinned[$piece['s']] && $pinned[$piece['s']]['by'] === $piece['s'] - 15)) { if ($enPassantSquare == $piece['s'] - 15 || ($this->cache['board'][$piece['s'] - 15]) && !($this->cache['board'][$piece['s'] - 15] & 0x8)) { $paths[] = $piece['s'] - 15; } } if ($piece['s'] - 17 >= 0) { if (!isset($pinned[$piece['s']]) || ($pinned[$piece['s']] && $pinned[$piece['s']]['by'] === $piece['s'] - 17)) { if ($enPassantSquare == $piece['s'] - 17 || ($this->cache['board'][$piece['s'] - 17]) && !($this->cache['board'][$piece['s'] - 17] & 0x8)) { $paths[] = $piece['s'] - 17; } } } break; // Sliding pieces case 0x05: case 0x07: case 0x06: case 0x0D: case 0x0E: case 0x0F: $directions = Board0x88Config::$movePatterns[$piece['t']]; if (isset($pinned[$piece['s']])) { if (array_search($pinned[$piece['s']]['direction'], $directions) !== FALSE) { $directions = array($pinned[$piece['s']]['direction'], $pinned[$piece['s']]['direction'] * -1); } else { $directions = array(); } } for ($a = 0, $len = count($directions); $a < $len; $a++) { $square = $piece['s'] + $directions[$a]; while (($square & 0x88) === 0) { if ($this->cache['board'][$square]) { if (($WHITE && $this->cache['board'][$square] & 0x8) || (!$WHITE && !($this->cache['board'][$square] & 0x8))) { $paths[] = $square; } break; } $paths[] = $square; $square += $directions[$a]; } } break; // Knight case 0x02: case 0x0A: if (isset($pinned[$piece['s']])) { break; } $directions = Board0x88Config::$movePatterns[$piece['t']]; for ($a = 0, $lenD = count($directions); $a < $lenD; $a++) { $square = $piece['s'] + $directions[$a]; if (($square & 0x88) === 0) { if ($this->cache['board'][$square]) { if (($WHITE && $this->cache['board'][$square] & 0x8) || (!$WHITE && !($this->cache['board'][$square] & 0x8))) { $paths[] = $square; } } else { $paths[] = $square; } } } break; // White king // Black king case 0X03: case 0X0B: $directions = Board0x88Config::$movePatterns[$piece['t']]; for ($a = 0, $lenD = count($directions); $a < $lenD; $a++) { $square = $piece['s'] + $directions[$a]; if (($square & 0x88) === 0) { if (!strstr($protectiveMoves, Board0x88Config::$keySquares[$square])) { #if ($protectiveMoves.indexOf(Board0x88Config::$keySquares[$square]) == -1) { if ($this->cache['board'][$square]) { if (($WHITE && $this->cache['board'][$square] & 0x8) || (!$WHITE && !($this->cache['board'][$square] & 0x8))) { $paths[] = $square; } } else { $paths[] = $square; } } } } if ($kingSideCastle && !($this->cache['board'][$piece['s'] + 1]) && !($this->cache['board'][$piece['s'] + 2]) && ($this->cache['board'][$piece['s'] + 3]) && !strstr($protectiveMoves, Board0x88Config::$keySquares[$piece['s']]) && ($piece['s'] < 118 && !strstr($protectiveMoves, Board0x88Config::$keySquares[$piece['s'] + 1])) && ($piece['s'] < 117 && !strstr($protectiveMoves, Board0x88Config::$keySquares[$piece['s'] + 2])) ) { $paths[] = $piece['s'] + 2; } if ($queenSideCastle && $piece['s'] - 2 != -1 && !($this->cache['board'][$piece['s'] - 1]) && !($this->cache['board'][$piece['s'] - 2]) && !($this->cache['board'][$piece['s'] - 3]) && ($this->cache['board'][$piece['s'] - 4]) && !strstr($protectiveMoves, Board0x88Config::$keySquares[$piece['s']]) && !strstr($protectiveMoves, Board0x88Config::$keySquares[$piece['s'] - 1]) && !strstr($protectiveMoves, Board0x88Config::$keySquares[$piece['s'] - 2]) ) { $paths[] = $piece['s'] - 2; } break; } if ($validSquares && $piece['t'] != 0x03 && $piece['t'] != 0x0B) { $paths = $this->excludeInvalidSquares($paths, $validSquares); } $ret[$piece['s']] = $paths; $totalCountMoves += count($paths); } $result = 0; if ($checks && !$totalCountMoves) { $result = $color === 'black' ? 1 : -1; } else if (!$checks && !$totalCountMoves) { $result = .5; } $this->validMoves = array('moves' => $ret, 'result' => $result, 'check' => $checks); return $this->validMoves; } private function validMoves(){ $validMovesAndResult = $this->getValidMovesAndResult(); return $validMovesAndResult["moves"]; } private function excludeInvalidSquares($squares, $validSquares) { $ret = array(); for ($i = 0, $len = count($squares); $i < $len; $i++) { if (in_array($squares[$i], $validSquares)) { $ret[] = $squares[$i]; } } return $ret; } /** * Returns comma-separated string of moves(since it's faster to work with than arrays). * @param string $color * @return string */ function getCaptureAndProtectiveMoves($color) { $ret = array(); $pieces = $this->cache[$color]; $oppositeKing = $this->getKing($color === 'white' ? 'black' : 'white'); $oppositeKingSquare = $oppositeKing['s']; for ($i = 0, $len = count($pieces); $i < $len; $i++) { $piece = $pieces[$i]; switch ($piece['t']) { // pawns case 0x01: if ((($piece['s'] + 15) & 0x88) === 0) $ret[] = $piece['s'] + 15; if ((($piece['s'] + 17) & 0x88) === 0) $ret[] = $piece['s'] + 17; break; case 0x09: if ((($piece['s'] - 15) & 0x88) === 0) $ret[] = $piece['s'] - 15; if ((($piece['s'] - 17) & 0x88) === 0) $ret[] = $piece['s'] - 17; break; // Sliding pieces case 0x05: case 0x07: case 0x06: case 0x0D: case 0x0E: case 0x0F: $directions = Board0x88Config::$movePatterns[$piece['t']]; for ($a = 0, $lenA = count($directions); $a < $lenA; $a++) { $square = $piece['s'] + $directions[$a]; while (($square & 0x88) === 0) { if ($this->cache['board'][$square] && $square !== $oppositeKingSquare) { $ret[] = $square; break; } $ret[] = $square; $square += $directions[$a]; } } break; // knight case 0x02: case 0x0A: // White knight $directions = Board0x88Config::$movePatterns[$piece['t']]; for ($a = 0, $lenA = count($directions); $a < $lenA; $a++) { $square = $piece['s'] + $directions[$a]; if (($square & 0x88) === 0) { $ret[] = $square; } } break; // king case 0X03: case 0X0B: $directions = Board0x88Config::$movePatterns[$piece['t']]; for ($a = 0, $lenD = count($directions); $a < $lenD; $a++) { $square = $piece['s'] + $directions[$a]; if (($square & 0x88) === 0) { $ret[] = $square; } } break; } } return ',' . implode(",", $ret) . ','; } function getSlidingPiecesAttackingKing($color) { $ret = array(); $king = $this->cache['king' . ($color === 'white' ? 'black' : 'white')]; $pieces = $this->cache[$color]; for ($i = 0, $len = count($pieces); $i < $len; $i++) { $piece = $pieces[$i]; if ($piece['t'] & 0x4) { $numericDistance = $king['s'] - $piece['s']; $boardDistance = ($king['s'] - $piece['s']) / $this->getDistance($king['s'], $piece['s']); switch ($piece['t']) { // Bishop case 0x05: case 0x0D: if ($numericDistance % 15 === 0 || $numericDistance % 17 === 0) { $ret[] = (array('s' => $piece['s'], 'p' => $boardDistance)); } break; // Rook case 0x06: case 0x0E: if ($numericDistance % 16 === 0) { $ret[] = array('s' => $piece['s'], 'p' => $boardDistance); } else if (($piece['s'] & 240) == ($king['s'] & 240)) { $ret[] = array('s' => $piece['s'], 'p' => $numericDistance > 0 ? 1 : -1); } break; // Queen case 0x07: case 0x0F: if ($numericDistance % 15 === 0 || $numericDistance % 17 === 0 || $numericDistance % 16 === 0) { $ret[] = array('s' => $piece['s'], 'p' => $boardDistance); } else if (($piece['s'] & 240) == ($king['s'] & 240)) { $ret[] = (array('s' => $piece['s'], 'p' => $numericDistance > 0 ? 1 : -1)); } break; } } } return $ret; } /** * Returns array of pinned pieces in standard chess coordinate system. * Example: * $parser = new FenParser0x88('5k2/8/8/8/8/8/r5PK/8 w - - 0 1'); // pawn on g2 pinned by rook on a2 * $pinned = $parser->getPinnedBoardCoordinates('white'); // find pinned white pieces * var_dump($pinned); * * returns * array(1) { * [0]=> * array(3) { * ["square"]=> * string(2) "g2" * ["pinnedBy"]=> * string(2) "a2" * ["direction"]=> * int(1) * } * } * * @param string $color * @return array */ public function getPinnedBoardCoordinates($color) { $pinned = $this->getPinned($color); $ret = array(); foreach ($pinned as $square => $by) { $ret[] = array( "square" => Board0x88Config::$numberToSquareMapping[$square], "pinnedBy" => Board0x88Config::$numberToSquareMapping[$by["by"]], "direction" => $by["direction"] ); } return $ret; } /** * Return numeric squares(0x88) of pinned pieces * @param $color * @return array|null */ function getPinned($color) { $ret = array(); $pieces = $this->getSlidingPiecesAttackingKing(($color === 'white' ? 'black' : 'white')); $WHITE = $color === 'white' ? true : false; $king = $this->cache['king' . $color]; $i = 0; $countPieces = count($pieces); while ($i < $countPieces) { $piece = $pieces[$i]; $square = $piece['s'] + $piece['p']; $countOpposite = 0; $squares = array($piece['s']); $pinning = ''; while ($square !== $king['s'] && $countOpposite < 2) { $squares[] = $square; if ($this->cache['board'][$square]) { $countOpposite++; if ((!$WHITE && $this->cache['board'][$square] & 0x8) || ($WHITE && !($this->cache['board'][$square] & 0x8))) { $pinning = $square; } else { break; } } $square += $piece['p']; } if ($countOpposite === 1) { $ret[$pinning] = array('by' => $piece['s'], 'direction' => $piece['p']); } $i++; } if (count($ret) === 0) { return null; } return $ret; } public function getBoardCache() { return $this->cache['board']; } /** * returns getValidSquaresOnCheck($color) in chess coordinates system(example: array("g2","g3","g4","g5","g6",g7","g8") * $color is either "white" or "black" * @param string $color * @return array */ public function getValidSquaresOnCheckBoardCoordinates($color) { $squares = $this->getValidSquaresOnCheck($color); $ret = array(); foreach ($squares as $square) { $ret[] = Board0x88Config::$numberToSquareMapping[$square]; } return $ret; } /** * Return valid squares for other pieces than king to move to when in check, i.e. squares * which avoids the check. * Example: if white king on g1 is checked by rook on g8, then valid squares for other pieces * are the squares g2,g3,g4,g5,g6,g7,g8. * Squares are returned in numeric format * @method getValidSquaresOnCheck * @param $color * @return array|null */ function getValidSquaresOnCheck($color) { $king = $this->cache['king' . $color]; $pieces = $this->cache[$color === 'white' ? 'black' : 'white']; $enPassantSquare = $this->getEnPassantSquare(); if ($enPassantSquare) { $enPassantSquare = Board0x88Config::$mapping[$enPassantSquare]; } for ($i = 0, $len = count($pieces); $i < $len; $i++) { $piece = $pieces[$i]; switch ($piece['t']) { case 0x01: if ($king['s'] === $piece['s'] + 15 || $king['s'] === $piece['s'] + 17) { if($enPassantSquare === $piece['s'] - 16){ return array($piece['s'], $enPassantSquare); } return array($piece['s']); } break; case 0x09: if ($king['s'] === $piece['s'] - 15 || $king['s'] === $piece['s'] - 17) { if($enPassantSquare === $piece['s'] + 16){ return array($piece['s'], $enPassantSquare); } return array($piece['s']); } break; // knight case 0x02: case 0x0A: if ($this->getDistance($piece['s'], $king['s']) === 2) { $directions = Board0x88Config::$movePatterns[$piece['t']]; for ($a = 0, $lenD = count($directions); $a < $lenD; $a++) { $square = $piece['s'] + $directions[$a]; if ($square === $king['s']) { return array($piece['s']); } } } break; // Bishop case 0x05: case 0x0D: $checks = $this->getBishopCheckPath($piece, $king); if (isset($checks) && is_array($checks)) { return $checks; } break; // Rook case 0x06: case 0x0E: $checks = $this->getRookCheckPath($piece, $king); if (isset($checks) && is_array($checks)) { return $checks; } break; case 0x07: case 0x0F: $checks = $this->getRookCheckPath($piece, $king); if (isset($checks) && is_array($checks)) { return $checks; } $checks = $this->getBishopCheckPath($piece, $king); if (isset($checks) && is_array($checks)) { return $checks; } break; } } return null; } public function getBishopCheckPath($piece, $king) { if (($king['s'] - $piece['s']) % 15 === 0 || ($king['s'] - $piece['s']) % 17 === 0) { $direction = ($king['s'] - $piece['s']) / $this->getDistance($piece['s'], $king['s']); $square = $piece['s'] + $direction; $pieceFound = false; $squares = array($piece['s']); while ($square !== $king['s'] && !$pieceFound) { $squares[] = $square; if (isset($this->cache['board'][$square]) && $this->cache['board'][$square]) { $pieceFound = true; } $square += $direction; } if (!$pieceFound) { return $squares; } } return null; } function getRookCheckPath($piece, $king) { $direction = null; if ($this->isOnSameFile($piece['s'], $king['s'])) { $direction = ($king['s'] - $piece['s']) / $this->getDistance($piece['s'], $king['s']); } else if ($this->isOnSameRank($piece['s'], $king['s'])) { $direction = $king['s'] > $piece['s'] ? 1 : -1; } if ($direction) { $square = $piece['s'] + $direction; $pieceFound = false; $squares = array($piece['s']); while ($square !== $king['s'] && !$pieceFound) { $squares[] = $square; if ($this->cache['board'][$square]) { $pieceFound = true; } $square += $direction; } if (!$pieceFound) { return $squares; } } return null; } /** * @param $kingColor * @param $moves * @return int */ function getCountChecks($kingColor, $moves) { $king = $this->cache['king' . $kingColor]; $index = strpos($moves, Board0x88Config::$keySquares[$king['s']]); if ($index > 0) { if (strpos($moves, Board0x88Config::$keySquares[$king['s']], $index + 1) > 0) { return 2; } return 1; } return 0; } function getDistance($sq1, $sq2) { return Board0x88Config::$distances[$sq2 - $sq1 + ($sq2 | 7) - ($sq1 | 7) + 240]; } function getPiecesInvolvedInMove($move) { $ret = array( array('from' => $move['from'], 'to' => $move['to']) ); $move = array( 'from' => Board0x88Config::$mapping[$move['from']], 'to' => Board0x88Config::$mapping[$move['to']], 'promoteTo' => isset($move['promoteTo']) ? $move['promoteTo'] : null ); $color = ($this->cache['board'][$move['from']] & 0x8) ? 'black' : 'white'; if ($this->isEnPassantMove($move)) { if ($color == 'black') { $square = $move['to'] + 16; } else { $square = $move['to'] - 16; } $ret[] = array('capture' => Board0x88Config::$numberToSquareMapping[$square]); } if ($this->isCastleMove($move)) { if (($move['from'] & 15) < ($move['to'] & 15)) { $ret[] = (array( 'from' => 'h' . ($color == 'white' ? 1 : 8), 'to' => 'f' . ($color == 'white' ? 1 : 8) )); } else { $ret[] = (array( 'from' => 'a' . ($color == 'white' ? 1 : 8), 'to' => 'd' . ($color == 'white' ? 1 : 8) )); } } if ($move['promoteTo']) { $ret[] = (array( 'promoteTo' => $move['promoteTo'], 'square' => Board0x88Config::$numberToSquareMapping[$move['to']] )); } return $ret; } function isEnPassantMove($move) { if (($this->cache['board'][$move['from']] === 0x01 || $this->cache['board'][$move['from']] == 0x09)) { if ( !$this->cache['board'][$move['to']] && (($move['from'] - $move['to']) % 17 === 0 || ($move['from'] - $move['to']) % 15 === 0) ) { return true; } } return false; } function isCastleMove($move) { if (($this->cache['board'][$move['from']] === 0x03 || $this->cache['board'][$move['from']] == 0x0B)) { if ($this->getDistance($move['from'], $move['to']) === 2) { return true; } } return false; } function makeMoveByNotation($notation) { $this->makeMove($this->getFromAndToByNotation($notation)); } function makeMove($move) { $this->updateBoardData($move); $this->fen = null; } private function getFromAndToByLongNotation($notation) { $notation = preg_replace('/[^a-h0-8]/si', '', $notation); return array( 'from' => substr($notation, 0, 2), 'to' => substr($notation, 2, 2) ); } public function getExtendedMoveInfo($move) { $move = $this->getParsed($move); return $move; } public function getParsed($move) { if (is_string($move)) $move = array('m' => $move); $move["m"] = preg_replace("/([a-h])([a-h])([0-8])/s", "$1x$2$3", $move["m"]); if (isset($move['m'])) { if ($move['m'] == '--') { $this->fen = null; $this->switchColor(); return array( 'm' => $move['m'], 'fen' => $this->getFen() ); } if (is_string($move['m']) && preg_match('/^[a-h][0-8][a-h][0-8]$/', $move['m'])) { $fromAndTo = $this->getFromAndToByLongNotation($move['m']); } else { $fromAndTo = $this->getFromAndToByNotation($move['m']); } } else { $fromAndTo = $move; } $this->makeMove($fromAndTo); $newProperties = array( 'from' => $fromAndTo['from'], 'to' => $fromAndTo['to'], 'fen' => $this->getFen() ); return array_merge($move, $newProperties); } function getFromAndToByNotation($notation) { $notation = str_replace(".", "", $notation); $ret = array('promoteTo' => $this->getPromoteByNotation($notation)); $color = $this->getColor(); $offset = 0; /* a1 */ if ($color === 'black') { $offset = 112; /* a8 */ } $foundPieces = array(); $fromRank = $this->getFromRankByNotation($notation); $fromFile = $this->getFromFileByNotation($notation); if (strlen($notation) === 2) { $square = Board0x88Config::$mapping[$notation]; $ret['to'] = Board0x88Config::$mapping[$notation]; $direction = $color === 'white' ? -16 : 16; if ($this->cache['board'][$square + $direction]) { $foundPieces[] = $square + $direction; } else { $foundPieces[] = $square + ($direction * 2); } } else { $notation = preg_replace("/=[QRBN]/", "", $notation); $notation = preg_replace("/[\+#!\?]/s", "", $notation); $notation = preg_replace("/^(.*?)[QRBN]$/s", "$1", $notation); $pieceType = $this->getPieceTypeByNotation($notation, $color); $capture = strpos($notation, "x") > 0; $ret['to'] = $this->getToSquareByNotation($notation); switch ($pieceType) { case 0x01: // pawn case 0x09: if ($color === 'black') { $offsets = $capture ? array(15, 17) : array(16); if ($ret['to'] >= 64) { $offsets[] = 32; } } else { $offsets = $capture ? array(-15, -17) : array(-16); if ($ret['to'] < 64) { $offsets[] = -32; } } for ($i = 0, $lenO = count($offsets); $i < $lenO; $i++) { $sq = $ret['to'] + $offsets[$i]; if ($this->cache['board'][$sq] && $this->cache['board'][$sq] === $pieceType) { $foundPieces[] = ($sq); } } break; case 0x03: // king case 0x0B: if (!strncmp($notation, 'O-O-O', 5)) { $foundPieces[] = ($offset + 4); $ret['to'] = $offset + 2; } else if (!strncmp($notation, 'O-O', 3)) { $foundPieces[] = ($offset + 4); $ret['to'] = $offset + 6; } else { $k = $this->getKing($color); $foundPieces[] = $k['s']; } break; case 0x02: // knight case 0x0A: $pattern = Board0x88Config::$movePatterns[$pieceType]; for ($i = 0, $len = count($pattern); $i < $len; $i++) { $sq = $ret['to'] + $pattern[$i]; if (!($sq & 0x88)) { if ($this->cache['board'][$sq] && $this->cache['board'][$sq] === $pieceType) { $foundPieces[] = ($sq); } } } break; // Sliding pieces default: $patterns = Board0x88Config::$movePatterns[$pieceType]; for ($i = 0, $len = count($patterns); $i < $len; $i++) { $sq = $ret['to'] + $patterns[$i]; while (!($sq & 0x88)) { if ($this->cache['board'][$sq] && $this->cache['board'][$sq] === $pieceType) { $foundPieces[] = ($sq); } if ($this->cache['board'][$sq]) { break; } $sq += $patterns[$i]; } } break; } } if (count($foundPieces) === 1) { $ret['from'] = $foundPieces[0]; } else { if ($fromRank !== null && $fromRank >= 0) { for ($i = 0, $len = count($foundPieces); $i < $len; $i++) { if ($this->isOnSameRank($foundPieces[$i], $fromRank)) { $ret['from'] = $foundPieces[$i]; break; } } } else if ($fromFile !== null && $fromFile >= 0) { for ($i = 0, $len = count($foundPieces); $i < $len; $i++) { if ($this->isOnSameFile($foundPieces[$i], $fromFile)) { $ret['from'] = $foundPieces[$i]; break; } } } if (!isset($ret['from'])) { $config = $this->getValidMovesAndResult(); $moves = $config['moves']; foreach ($foundPieces as $piece) { if (in_array($ret['to'], $moves[$piece])) { $ret['from'] = $piece; break; } } } } // TODO some pgn files may not have correct notations for all moves. Example Nd7 which may be from b2 or f6. // this may cause problems later on in the game. Figure out a way to handle this. #if (count($foundPieces) === 2){ #$ret['from'] = $foundPieces[1]; #throw new Exception("Unable to decide which move to take for notation: ". $notation); #} if (!isset($ret['from'])) { $msg = "Fen: " . $this->fen . "\ncolor: " . $color . "\nnotation: " . $notation . "\nRank:" . $fromRank . "\nFile:" . $fromFile . "\n" . count($foundPieces) . ", " . implode(",", $foundPieces); throw new FenParser0x88Exception($msg); } $ret['from'] = Board0x88Config::$numberToSquareMapping[$ret['from']]; $ret['to'] = Board0x88Config::$numberToSquareMapping[$ret['to']]; return $ret; } public function hasThreeFoldRepetition($fens = array()) { if (!count($fens)) return false; $shortenedFens = array(); foreach ($fens as $fen) { $fen = array_slice(explode(" ", $fen), 0, 3); $fen = implode(" ", $fen); $shortenedFens[] = $fen; } $lastFen = $shortenedFens[count($shortenedFens) - 1]; $count = array_count_values($shortenedFens); return $count[$lastFen] >= 2; } public function getPromoteByNotation($notation) { if (strstr($notation, '=')) { $piece = preg_replace("/^.*?=([QRBN]).*$/", '$1', $notation); return Board0x88Config::$pieceAbbr[$piece]; } if (preg_match("/[a-h][18][NBRQ]/", $notation)) { $notation = preg_replace("/[^a-h18NBRQ]/s", "", $notation); return Board0x88Config::$pieceAbbr[substr($notation, strlen($notation) - 1, 1)]; } return ''; } function getFromRankByNotation($notation) { $notation = preg_replace("/^.+([0-9]).+[0-9].*$/s", '$1', $notation); if (strlen($notation) > 1) { return null; } return ($notation - 1) * 16; } function getFromFileByNotation($notation) { $notation = preg_replace("/^.*([a-h]).*[a-h].*$/s", '$1', $notation); if (strlen($notation) > 1) { return null; } return Board0x88Config::$files[$notation]; } /** * @param $notation * @return int */ function getToSquareByNotation($notation) { $notation = preg_replace("/.*([a-h][1-8]).*/s", '$1', $notation); if (isset(Board0x88Config::$mapping[$notation])) { return Board0x88Config::$mapping[$notation]; } return ''; } function getPieceTypeByNotation($notation, $color = null) { if (!strncmp($notation, 'O-O', 3)) { $pieceType = 'K'; } else { $token = substr($notation, 0, 1); $pieceType = preg_match("/[NRBQK]/", $token) ? $token : 'P'; } $pieceType = Board0x88Config::$pieces[$pieceType]; if ($color === 'black') { $pieceType += 8; } return $pieceType; } function moveByLongNotation($notation) { $fromAndTo = $this->getFromAndToByLongNotation($notation); $this->move($fromAndTo); } /** * Make a move on the board * Example: * * $parser = new FenParser0x88(); * $parser->newGame(); * $parser->move("Nf3"); * $notation = $parser->getNotation(); * * * * @param mixed $move * @throws Exception * * $move can be a string like Nf3, g1f3 or an array with from and to squares, like array("from" => "g1", "to"=>"f3") */ public function move($move) { if (is_string($move) && strlen($move) == 4) { $move = $this->getFromAndToByLongNotation($move); } else if (is_string($move)) { $move = $this->getFromAndToByNotation($move); } if(!$this->canMoveFromTo($move["from"], $move["to"])){ throw new FenParser0x88Exception("Invalid move " . $this->getColor() . " - " . json_encode($move)); } $this->fen = null; $this->validMoves = null; $this->piecesInvolved = $this->getPiecesInvolvedInMove($move); $this->notation = $this->getNotationForAMove($move); $this->updateBoardData($move); $config = $this->getValidMovesAndResult(); if ($config['result'] === 1 || $config['result'] === -1) { $this->notation .= '#'; } else { if ($config['check'] > 0) { $this->notation .= '+'; } } } private function canMoveFromTo($from, $to){ $validMoves = $this->validMoves(); $from = Board0x88Config::$mapping[$from]; $to = Board0x88Config::$mapping[$to]; if(empty($validMoves[$from]) || !in_array($to, $validMoves[$from])){ return false; } return true; } function setNewColor() { $this->fenParts['color'] = ($this->fenParts['color'] == 'w') ? 'b' : 'w'; } private function setCastle($castle) { if (!$castle) { $castle = '-'; } $this->fenParts['castle'] = $castle; $castleCode = 0; for ($i = 0, $count = strlen($castle); $i < $count; $i++) { $castleCode += Board0x88Config::$castle[substr($castle, $i, 1)]; } $this->fenParts['castleCode'] = $castleCode; } function getCastle() { return $this->fenParts['castle']; } function getCastleCode() { return $this->fenParts['castleCode']; } private function updateBoardData($move) { $move = array( 'from' => Board0x88Config::$mapping[$move['from']], 'to' => Board0x88Config::$mapping[$move['to']], 'promoteTo' => isset($move['promoteTo']) ? $move['promoteTo'] : '' ); $movedPiece = $this->cache['board'][$move['from']]; $color = ($movedPiece & 0x8) ? 'black' : 'white'; $enPassant = '-'; if ($this->cache['board'][$move['to']]) { $incrementHalfMoves = false; } else { $incrementHalfMoves = true; } if (($this->cache['board'][$move['from']] === 0x01 || $this->cache['board'][$move['from']] == 0x09)) { $incrementHalfMoves = false; if ($this->isEnPassantMove($move)) { if ($color == 'black') { $this->cache['board'][$move['to'] + 16] = null; } else { $this->cache['board'][$move['to'] - 16] = null; } } if (($move['from'] & 15) == ($move['to'] & 15) && $this->getDistance($move['from'], $move['to']) == 2) { if ($color === 'white') { $enPassant = Board0x88Config::$numberToSquareMapping[$move['from'] + 16]; } else { $enPassant = Board0x88Config::$numberToSquareMapping[$move['from'] - 16]; } } } $this->setEnPassantSquare($enPassant); if ($this->isCastleMove(array('from' => $move['from'], 'to' => $move['to']))) { $castle = $this->getCastle(); if ($color == 'white') { $castleNotation = '/[KQ]/s'; $pieceType = 0x06; $offset = 0; } else { $castleNotation = '/[kq]/s'; $pieceType = 0x0E; $offset = 112; } if ($move['from'] < $move['to']) { $this->cache['board'][7 + $offset] = null; $this->cache['board'][5 + $offset] = $pieceType; } else { $this->cache['board'][0 + $offset] = null; $this->cache['board'][3 + $offset] = $pieceType; } $castle = preg_replace($castleNotation, '', $castle); $this->setCastle($castle); } else { $this->updateCastleForMove($movedPiece, $move['from']); } if ($color === 'black') { $this->incrementFullMoves(); } if ($incrementHalfMoves) { $this->incrementHalfMoves(); } else { $this->resetHalfMoves(); } $this->cache['board'][$move['to']] = $this->cache['board'][$move['from']]; $this->cache['board'][$move['from']] = null; if ($move['promoteTo']) { $this->cache['board'][$move['to']] = Board0x88Config::$typeToNumberMapping[$move['promoteTo']]; if ($color === 'black') { $this->cache['board'][$move['to']] += 8; } } $this->setNewColor(); $this->updatePieces(); } private function updateCastleForMove($movedPiece, $from) { switch ($movedPiece) { case 0x03: $this->setCastle(preg_replace("/[KQ]/s", "", $this->getCastle())); break; case 0x0B: $this->setCastle(preg_replace("/[kq]/s", "", $this->getCastle())); break; case 0x06: if ($from === 0) { $this->setCastle(preg_replace("/[Q]/s", "", $this->getCastle())); } if ($from === 7) { $this->setCastle(preg_replace("/[K]/s", "", $this->getCastle())); } break; case 0x0E: if ($from === 112) { $this->setCastle(preg_replace("/[q]/s", "", $this->getCastle())); } if ($from === 119) { $this->setCastle(preg_replace("/[k]/s", "", $this->getCastle())); } break; } } private function updatePieces() { $this->cache['white'] = array(); $this->cache['black'] = array(); $piece = null; for ($i = 0; $i < 120; $i++) { if ($i & 0x88) { $i += 8; } if ($piece = $this->cache['board'][$i]) { $color = $piece & 0x8 ? 'black' : 'white'; $obj = array( 't' => $piece, 's' => $i ); $this->cache[$color][] = $obj; if ($piece == 0x03 || $piece == 0x0B) { $this->cache['king' . $color] = $obj; } } } } private function incrementFullMoves() { $this->fenParts['fullMoves']++; } private function incrementHalfMoves() { $this->fenParts['halfMoves']++; } private function resetHalfMoves() { $this->fenParts['halfMoves'] = 0; } function getPiecesInvolvedInLastMove() { return $this->piecesInvolved; } function getNotation() { return $this->notation; } /** * Returns FEN for current position * @return string */ public function getFen() { if (!$this->fen) { $this->fen = $this->getNewFen(); } return $this->fen; } private function getNotationForAMove($move) { $move['from'] = Board0x88Config::$mapping[$move['from']]; $move['to'] = Board0x88Config::$mapping[$move['to']]; $type = $this->cache['board'][$move['from']]; $ret = Board0x88Config::$notationMapping[$this->cache['board'][$move['from']]]; switch ($type) { case 0x01: case 0x09: if ($this->isEnPassantMove($move) || $this->cache['board'][$move['to']]) { $ret .= Board0x88Config::$fileMapping[$move['from'] & 15] . 'x'; } $ret .= Board0x88Config::$fileMapping[$move['to'] & 15] . '' . Board0x88Config::$rankMapping[$move['to'] & 240]; if (isset($move['promoteTo']) && $move['promoteTo']) { $numType = Board0x88Config::$typeToNumberMapping[$move['promoteTo']]; $ret .= '=' . Board0x88Config::$notationMapping[$numType]; } break; case 0x02: case 0x05: case 0x06: case 0x07: case 0x0A: case 0x0D: case 0x0E: case 0x0F: $config = $this->getValidMovesAndResult(); $configMoves = $config['moves']; foreach ($configMoves as $square => $moves) { if ($square != $move['from'] && $this->cache['board'][$square] === $type) { if (array_search($move['to'], $moves) !== FALSE) { if (($square & 15) != ($move['from'] & 15)) { $ret .= Board0x88Config::$fileMapping[$move['from'] & 15]; } else if (($square & 240) != ($move['from'] & 240)) { $ret .= Board0x88Config::$rankMapping[$move['from'] & 240]; } } } } if ($this->cache['board'][$move['to']]) { $ret .= 'x'; } $ret .= Board0x88Config::$fileMapping[$move['to'] & 15]; $ret .= Board0x88Config::$rankMapping[$move['to'] & 240]; break; case 0x03: case 0x0B: if ($this->isCastleMove($move)) { if ($move['to'] > $move['from']) { $ret = 'O-O'; } else { $ret = 'O-O-O'; } } else { if ($this->cache['board'][$move['to']]) { $ret .= 'x'; } $ret .= Board0x88Config::$fileMapping[$move['to'] & 15] . '' . Board0x88Config::$rankMapping[$move['to'] & 240]; } break; } return $ret; } private function getNewFen() { $board = $this->cache['board']; $fen = ''; $emptyCounter = 0; for ($rank = 7; $rank >= 0; $rank--) { for ($file = 0; $file < 8; $file++) { $index = ($rank * 8) + $file; if ($board[Board0x88Config::$numericMapping[$index]]) { if ($emptyCounter) { $fen .= $emptyCounter; } $fen .= Board0x88Config::$pieceMapping[$board[Board0x88Config::$numericMapping[$index]]]; $emptyCounter = 0; } else { $emptyCounter++; } } if ($rank) { if ($emptyCounter) { $fen .= $emptyCounter; } $fen .= '/'; $emptyCounter = 0; } } if ($emptyCounter) { $fen .= $emptyCounter; } return $fen . " " . $this->getColorCode() . " " . $this->getCastle() . " " . $this->fenParts['enPassant'] . " " . $this->getHalfMoves() . " " . $this->getFullMoves(); } } class FenParser0x88Exception extends Exception{ }