/** * @param MoveInterface $move * * @return string */ public static function toNotation(MoveInterface $move) { if (null === $move->getPiece()) { return ''; } $notation = ''; $piece = $move->getPiece(); $toLabel = strtolower($move->getToLabel()); $isPawn = $piece->getType() === PieceInterface::TYPE_PAWN; $pieceLetter = PieceDecorator::toLetter($piece); if (null !== ($capture = $move->getCapture())) { if ($isPawn) { $column = BoardHelper::getColumnFromPosition($move->getFrom()); $pieceLetter = BoardHelper::columnNumberToLetter($column); } $notation .= sprintf('%sx%s', $pieceLetter, $toLabel); if ($isPawn) { if ($move->getType() === MoveInterface::TYPE_CAPTURE_EN_PASSANT) { $notation .= 'e.p.'; } } return $notation; } if ($isPawn) { return $toLabel; } return sprintf('%s%s', $pieceLetter, $toLabel); }
/** * @param string $asciiBoard * @param array $expectedPossibleMovesByColor * * @dataProvider getBoardsAndPossibleMoves() */ public function testPossibleMoves($asciiBoard, array $expectedPossibleMovesByColor) { if (empty($expectedPossibleMovesByColor)) { $this->markTestIncomplete('No possible moves provided by testing data'); } $game = $this->createGameFromAsciiBoard($asciiBoard); foreach ($expectedPossibleMovesByColor as $colorToMove => $expectedPossibleMoves) { $game->setCurrentColor($colorToMove); $board = $game->getBoard(); $actualMovesByColor = $this->getSimplifiedActualMoves($colorToMove, $board); foreach ($expectedPossibleMoves as $x => $expectedMove) { $found = false; $expectedFrom = $expectedMove[0]; $expectedTo = $expectedMove[1]; foreach ($actualMovesByColor as $y => $actualMove) { $actualFrom = $actualMove[0]; $actualTo = $actualMove[1]; if ($expectedFrom == $actualFrom && $expectedTo == $actualTo) { unset($expectedPossibleMoves[$x]); unset($actualMovesByColor[$y]); $found = true; } } if (null === ($fromPiece = $board->getSquare($expectedFrom)->getPiece())) { throw new \RuntimeException(sprintf('No piece found to move from %s to %s: %s', BoardHelper::positionToLabel($expectedFrom), BoardHelper::positionToLabel($expectedTo), BoardDecorator::toAscii($board))); } $this->assertTrue($found, sprintf('Expected move for %s (%s from %s to %s) is missing from actual moves', $colorToMove === Color::WHITE ? 'white' : 'black', $fromPiece->getTypeLabel(), BoardHelper::positionToLabel($expectedFrom), BoardHelper::positionToLabel($expectedTo))); } foreach ($actualMovesByColor as $actualMove) { $actualFrom = $actualMove[0]; $actualTo = $actualMove[1]; $this->assertTrue(false, sprintf('Actual move for %s (%s from %s to %s) is missing from expected moves', $colorToMove === Color::WHITE ? 'white' : 'black', $board->getSquare($actualFrom)->getPiece()->getTypeLabel(), BoardHelper::positionToLabel($actualFrom), BoardHelper::positionToLabel($actualTo))); } } }
/** * @param BoardWalker $walker * @param MoveInterface $lastMove */ private function walkEnPassant(BoardWalker $walker, MoveInterface $lastMove = null) { $lastEnemyPiece = $lastMove ? $walker->getBoard()->getSquare($lastMove->getTo())->getPiece() : null; if ($lastMove === null || $lastEnemyPiece === null || $lastEnemyPiece->getType() !== PieceInterface::TYPE_PAWN) { return; } $leftToPosition = $walker->peek(BoardWalker::DIRECTION_LEFT, 1, null, true, false); $rightToPosition = $walker->peek(BoardWalker::DIRECTION_RIGHT, 1, null, true, false); if ($leftToPosition !== null) { if (abs(BoardHelper::getRowFromPosition($lastMove->getFrom()) - BoardHelper::getRowFromPosition($leftToPosition)) !== 2) { return; } if ($lastMove->getTo() === $leftToPosition) { // en passant left $walker->forwardLeft(1, false)->restart(); } } if ($rightToPosition !== null) { if (abs(BoardHelper::getRowFromPosition($lastMove->getFrom()) - BoardHelper::getRowFromPosition($rightToPosition)) !== 2) { return; } if ($lastMove->getTo() === $rightToPosition) { // en passant left $walker->forwardRight(1, false)->restart(); } } }
/** * @return Board */ public static function createEmpty() { $squares = []; foreach (BoardHelper::getAllPositions() as $position) { $squares[] = SquareFactory::create($position); } return new Board($squares); }
/** * {@inheritdoc} */ public function configureWalker(BoardWalker $walker, GameInterface $game) { $walker->omnidirectional(1); $kingPosition = BoardHelper::getKingPosition($game->getBoard(), $game->getCurrentColor()); $kingColumn = BoardHelper::getColumnFromPosition($kingPosition); $kingRow = BoardHelper::getRowFromPosition($kingPosition); $board = $game->getBoard(); $king = $board->getSquare($kingPosition)->getPiece(); $track = $kingPosition === SquareInterface::POSITION_E1; // check for possible castle move if (empty($game->getMovesByPiece($king->getId()))) { // see if one of the rooks did not make any moves $rookCriteria = ['piece_type' => PieceInterface::TYPE_ROOK, 'piece_color' => $game->getCurrentColor()]; foreach ($board->getPiecesBy($rookCriteria) as $rookPosition => $rook) { if (empty($game->getMovesByPiece($rook->getId()))) { // see if there are no pieces between the king and this rook $rookColumn = BoardHelper::getColumnFromPosition($rookPosition); $columnDiff = abs($kingColumn - $rookColumn) - 1; $track = $track === true && $columnDiff === 3; if ($columnDiff > 1) { $skip = false; for ($x = 1; $x <= $columnDiff; $x++) { if ($rookColumn > $kingColumn) { $cursor = $rookColumn - $x . $kingRow; } else { $cursor = $rookColumn + $x . $kingRow; } if (null !== ($piece = $board->getSquare($cursor)->getPiece())) { $skip = true; break; } } if ($skip) { continue; } if ($kingColumn > $rookColumn) { if ($game->getCurrentColor() === Color::WHITE) { $walker->jump(BoardWalker::DIRECTION_LEFT, 2); } else { $walker->jump(BoardWalker::DIRECTION_RIGHT, 2); } } else { if ($game->getCurrentColor() === Color::WHITE) { $walker->jump(BoardWalker::DIRECTION_RIGHT, 2); } else { $walker->jump(BoardWalker::DIRECTION_LEFT, 2); } } } } } } }
/** * {@inheritdoc} */ public function getToLabel() { return BoardHelper::positionToLabel($this->getTo()); }
/** * {@inheritdoc} */ public function getIndexedSquares($indexedBy = 'row') { $squares = []; switch ($indexedBy) { case 'row': $squares = []; foreach ($this->getSquares() as $square) { $row = BoardHelper::getRowFromPosition($square->getPosition()); if (!array_key_exists($row, $squares)) { $squares[$row] = []; } $squares[$row][$square->getPosition()] = $square; } break; case 'column': $squares = []; foreach ($this->getSquares() as $square) { $column = BoardHelper::getColumnFromPosition($square->getPosition()); if (!array_key_exists($column, $squares)) { $squares[$column] = []; } $squares[$column][$square->getPosition()] = $square; } break; } ksort($squares); return $squares; }
/** * @param int $direction The direction to step into * @param int $jumps The number of jumps to make in this direction * @param int|null $startingPosition * @param bool $ignoreCapture * * @return int|null The expected position, or null if the move is not possible * * @throws \InvalidArgumentException */ public function peek($direction, $jumps = 1, $startingPosition = null, $ignoreCapture = false, $requireAttack = false) { $newPosition = null; if ($startingPosition !== null) { $column = BoardHelper::getColumnFromPosition($startingPosition); $row = BoardHelper::getRowFromPosition($startingPosition); } else { $column = $this->getColumn(); $row = $this->getRow(); } switch ($direction) { case self::DIRECTION_FORWARD: if ($this->color === Color::WHITE) { $newRow = $this->formatNumber($row + $jumps); } else { $newRow = $this->formatNumber($row - $jumps); } if ($newRow !== null) { $newPosition = intval($column . $newRow); } break; case self::DIRECTION_FORWARDRIGHT: if ($this->color === Color::WHITE) { $newColumn = $this->formatNumber($column + $jumps); $newRow = $this->formatNumber($row + $jumps); } else { $newColumn = $this->formatNumber($column - $jumps); $newRow = $this->formatNumber($row - $jumps); } if ($newColumn !== null && $newRow !== null) { $newPosition = intval($newColumn . $newRow); } break; case self::DIRECTION_RIGHT: if ($this->color === Color::WHITE) { $newColumn = $this->formatNumber($column + $jumps); } else { $newColumn = $this->formatNumber($column - $jumps); } if ($newColumn !== null) { $newPosition = intval($newColumn . $row); } break; case self::DIRECTION_BACKWARDRIGHT: if ($this->color === Color::WHITE) { $newColumn = $this->formatNumber($column + $jumps); $newRow = $this->formatNumber($row - $jumps); } else { $newColumn = $this->formatNumber($column - $jumps); $newRow = $this->formatNumber($row + $jumps); } if ($newColumn !== null && $newRow !== null) { $newPosition = intval($newColumn . $newRow); } break; case self::DIRECTION_BACKWARD: if ($this->color === Color::WHITE) { $newRow = $this->formatNumber($row - $jumps); } else { $newRow = $this->formatNumber($row + $jumps); } if ($newRow !== null) { $newPosition = intval($column . $newRow); } break; case self::DIRECTION_BACKWARDLEFT: if ($this->color === Color::WHITE) { $newColumn = $this->formatNumber($column - $jumps); $newRow = $this->formatNumber($row - $jumps); } else { $newColumn = $this->formatNumber($column + $jumps); $newRow = $this->formatNumber($row + $jumps); } if ($newColumn !== null && $newRow !== null) { $newPosition = intval($newColumn . $newRow); } break; case self::DIRECTION_LEFT: if ($this->color === Color::WHITE) { $newColumn = $this->formatNumber($column - $jumps); } else { $newColumn = $this->formatNumber($column + $jumps); } if ($newColumn !== null) { $newPosition = intval($newColumn . $row); } break; case self::DIRECTION_FORWARDLEFT: if ($this->color === Color::WHITE) { $newColumn = $this->formatNumber($column - $jumps); $newRow = $this->formatNumber($row + $jumps); } else { $newColumn = $this->formatNumber($column + $jumps); $newRow = $this->formatNumber($row - $jumps); } if ($newColumn !== null && $newRow !== null) { $newPosition = intval($newColumn . $newRow); } break; default: throw new \InvalidArgumentException(sprintf('Unknown direction to calculate: %s', $direction)); } if ($ignoreCapture !== true && $newPosition !== null) { $toPiece = $this->board->getSquare($newPosition)->getPiece(); if (null !== $toPiece) { if ($toPiece->getColor() === $this->getColor()) { // own piece, can't capture return null; } } elseif ($requireAttack === true) { return null; } } return $newPosition; }
/** * @param MoveInterface $move * @param GameInterface $game * * @return int|null */ private static function determineEnPassantType(MoveInterface $move, GameInterface $game) { if (null !== ($lastMove = $game->getLastMove())) { if ($lastMove->getPiece()->getType() === PieceInterface::TYPE_PAWN) { if (abs(BoardHelper::getColumnFromPosition($lastMove->getTo()) - BoardHelper::getColumnFromPosition($move->getFrom())) === 1 && abs(BoardHelper::getRowFromPosition($lastMove->getTo()) - BoardHelper::getRowFromPosition($lastMove->getFrom())) === 2 && BoardHelper::getRowFromPosition($move->getFrom()) === BoardHelper::getRowFromPosition($lastMove->getTo())) { $toPiece = $game->getBoard()->getSquare($move->getTo())->getPiece(); $rowDiff = abs(BoardHelper::getRowFromPosition($move->getFrom()) - BoardHelper::getRowFromPosition($move->getTo())); $columnDiff = abs(BoardHelper::getColumnFromPosition($move->getFrom()) - BoardHelper::getColumnFromPosition($move->getTo())); if ($rowDiff === 1 && $columnDiff === 1) { if ($toPiece === null) { // en passant capture return MoveInterface::TYPE_CAPTURE_EN_PASSANT; } else { return MoveInterface::TYPE_CAPTURE; } } } } } return null; }
/** * @param string $notation * @param GameInterface $game * @param array $parsed * * @return int * * @throws InvalidNotationException */ private function parseFrom($notation, GameInterface $game, array $parsed) { $enPassant = false; $capture = false; switch ($parsed['piece_type']) { case PieceInterface::TYPE_PAWN: if (substr($notation, -4) === 'e.p.') { $enPassant = true; } elseif (substr($notation, 1, 1) === 'x') { // calculate 'from' for capture by pawn (diagonally) $capture = true; } else { // calculate 'from' for forward move by pawn } break; default: // non-pawn calculation, depends on type break; } $finalMove = null; $criteria = ['piece_color' => $parsed['color'], 'piece_type' => $parsed['piece_type']]; if (strlen($notation) > 3 && in_array(substr($notation, 1, 1), ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])) { $criteria['column'] = BoardHelper::columnLetterToNumber(substr($notation, 1, 1)); } $possiblePieceMoves = $this->moveCalculator->possibleMovesTo($parsed['to'], $game->getBoard(), $criteria); foreach ($possiblePieceMoves as $move) { MoveHelper::enrich($move, $game); if ($enPassant === true && $move->getType() !== MoveInterface::TYPE_CAPTURE_EN_PASSANT) { continue; } elseif ($capture === true && $move->getType() !== MoveInterface::TYPE_CAPTURE) { continue; } if ($finalMove !== null) { throw new InvalidNotationException(sprintf('Multiple moves found starting from %s and ending on %s, you should make your notation more specific (criteria: %s)', $finalMove->getFromLabel(), $finalMove->getToLabel(), json_encode($criteria))); } $finalMove = $move; } if ($finalMove === null) { throw new InvalidNotationException(sprintf('There are no moves to make to this position: %s (criteria: %s)', $parsed['to'], json_encode($criteria, true))); } return $finalMove->getFrom(); }
/** * {@inheritdoc} */ public function __construct($position, PieceInterface $piece = null) { $this->position = $position; $this->color = BoardHelper::getSquareColorFromPosition($position); $this->piece = $piece; }
/** * @param Move[] $moves * @param int $ownColor * @param BoardInterface $board * * @return Move[] */ private function filterOwnKingChecks(array $moves, $ownColor, BoardInterface $board) { $opposingColor = $ownColor === Color::WHITE ? Color::BLACK : Color::WHITE; $safeMoves = []; foreach ($moves as $move) { $futureBoard = $this->mockMove($move, $board); $checksKing = false; $ownKingPosition = BoardHelper::getKingPosition($futureBoard, $ownColor); foreach ($board->getSquaresBy(['piece_color' => $opposingColor]) as $square) { foreach ($this->possibleMovesFrom($square->getPosition(), $futureBoard, false, true) as $miscMove) { if ($miscMove->getTo() == $ownKingPosition) { $checksKing = true; break 2; } } } if ($checksKing === false) { $safeMoves[] = $move; } } return $safeMoves; }