diff --git a/README.md b/README.md index fc06ea3..3b04412 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,61 @@ -Chess -===== +Шахматы +======= -`chess.php` is a CLI program that displays chess games. -It receives players' moves as arguments and prints the board state -with figure positions after those moves. +`chess.php` — консольный проигрыватель шахматных партий. +Программа получает ходы игроков в качестве аргументов и выводит +состояние доски с фигурами после этих ходов. -Example: +Например: -`php chess.php e2-e4 e7-e5` +```bash +php chess.php e2-e4 e7-e5 +``` +``` + 8 ♜♞♝♛♚♝♞♜ + 7 ♟♟♟♟-♟♟♟ + 6 -------- + 5 ----♟--- + 4 ----♙--- + 3 -------- + 2 ♙♙♙♙-♙♙♙ + 1 ♖♘♗♕♔♗♘♖ + abcdefgh +``` -Example of the program execution -Validation: +В текущем виде `chess.php` никак не проверяет правильность ходов. -Currently `chess.php` does nothing to verify chess rules in those moves. -And you don't need to implement each rule for the game. +## Задача 1 -Add only the validations required by the tasks below. +Задача 1: дописать программу таким образом, чтобы она выкидывала исключение +при нарушении очерёдности хода (например, два раза подряд ход белых). -## Task 1 +Чтобы проверить корректность решения, запустите тесты: -Task 1: modify the program so that it throws an exception -when player move order is wrong (e.g. if whites make two moves in a row). +```bash +./vendor/bin/phpunit --group=rotation +``` -To verify your solution, run tests: +## Задача 2 - $ ./vendor/bin/phpunit --group=rotation +Задача 2: дописать программу таким образом, чтобы она выкидывала исключение +при нарушении правил хода пешкой (pawn). -## Task 2 +Чтобы проверить корректность решения, запустите тесты: -Task 2: modify the program so that it throws an exception -when a pawn makes an illegal move. +```bash +./vendor/bin/phpunit --group=pawn +``` -To verify your solution, run tests: +В тестах проверяются только ходы пешками, для других фигур валидацию ходов делать не нужно. - $ ./vendor/bin/phpunit --group=pawn +### Как ходит пешка -Tests only move pawns, other figures are not moved, -so you are safe to ignore non-pawn specifics in your solution. +* Пешка может ходить вперёд (по вертикали) на одну клетку; +* Если пешка ещё ни разу не ходила, она может пойти вперёд на две клетки; +* Пешка не может перепрыгивать через другие фигуры; +* Пешка может бить фигуры противника только по диагонали вперёд на одну клетку; +* Также существует взятие на проходе, но им можно пренебречь :) -### Chess pawn rules - - * A pawn can move forward (vertically) by 1 square; - * The first time a pawn moves, it may move forward by 2 squares instead of 1; - * Pawns cannot jump over other figures; - * Pawns can capture enemy figures only by diagonally moving 1 square forward; - * En passant capture also exists, but don't bother about it :) +## Комментарии к выполнению +Класс Figure изменил на абстрактный класс и добавил абстрактный метод. Сделал так потому что если надо будет добавить правила для других фигур то это решение выглядит лучше чем добавления интерфейса каждой фигуре. Во всех фигурах кроме пешки метод для описания правил пустой. Логика работы метода следующая, если ход соответствует одной из проверок то метод просто завершает работу, если нет то метод выбрасывает исключение. В начале поставил базовую проверку (возможно избыточна) для проверки того что пешка не пошла более чем на 2 клетки. diff --git a/src/Bishop.php b/src/Bishop.php index fe68f85..b2f221a 100644 --- a/src/Bishop.php +++ b/src/Bishop.php @@ -1,7 +1,14 @@ isBlack ? '♝' : '♗'; } + + public function checkMove(Path $path) + { + // TODO: Implement checkMove() method. + } } diff --git a/src/Board.php b/src/Board.php index 2b4bc6a..a0744c4 100644 --- a/src/Board.php +++ b/src/Board.php @@ -1,9 +1,13 @@ figures['a'][1] = new Rook(false); $this->figures['b'][1] = new Knight(false); $this->figures['c'][1] = new Bishop(false); @@ -41,23 +45,49 @@ public function __construct() { $this->figures['h'][8] = new Rook(true); } - public function move($move) { + public function move($move) + { if (!preg_match('/^([a-h])(\d)-([a-h])(\d)$/', $move, $match)) { throw new \Exception("Incorrect move"); } $xFrom = $match[1]; $yFrom = $match[2]; - $xTo = $match[3]; - $yTo = $match[4]; + $xTo = $match[3]; + $yTo = $match[4]; + + if (!isset($this->figures[$xFrom][$yFrom])) { + return; + } + + /** @var Figure $movingFigure */ + $movingFigure = $this->figures[$xFrom][$yFrom]; + + $isBlackLastMoveFigure = isset($this->lastMoveFigure) ? $this->lastMoveFigure->isBlack() : true; - if (isset($this->figures[$xFrom][$yFrom])) { - $this->figures[$xTo][$yTo] = $this->figures[$xFrom][$yFrom]; + if ($isBlackLastMoveFigure === $movingFigure->isBlack()) { + throw new \Exception("Incorrect color"); } + + $movingFigure->checkMove( + new Path( + figures: $this->figures, + xFrom: $xFrom, + yFrom: $yFrom, + xTo: $xTo, + yTo: $yTo + ) + ); + + $this->lastMoveFigure = $movingFigure; + + $this->figures[$xTo][$yTo] = $movingFigure; + unset($this->figures[$xFrom][$yFrom]); } - public function dump() { + public function dump() + { for ($y = 8; $y >= 1; $y--) { echo "$y "; for ($x = 'a'; $x <= 'h'; $x++) { diff --git a/src/DirectionMovement.php b/src/DirectionMovement.php new file mode 100644 index 0000000..5582920 --- /dev/null +++ b/src/DirectionMovement.php @@ -0,0 +1,18 @@ +isBlack = $isBlack; } /** @noinspection PhpToStringReturnInspection */ - public function __toString() { + public function __toString() + { throw new \Exception("Not implemented"); } + + public function isBlack() + { + return $this->isBlack; + } + + abstract public function checkMove(Path $path); } diff --git a/src/King.php b/src/King.php index 3f223d6..817265e 100644 --- a/src/King.php +++ b/src/King.php @@ -1,7 +1,14 @@ isBlack ? '♚' : '♔'; } + + public function checkMove(Path $path) + { + // TODO: Implement checkMove() method. + } } diff --git a/src/Knight.php b/src/Knight.php index 0815830..6e51696 100644 --- a/src/Knight.php +++ b/src/Knight.php @@ -1,7 +1,14 @@ isBlack ? '♞' : '♘'; } + + public function checkMove(Path $path) + { + // TODO: Implement checkMove() method. + } } diff --git a/src/Path.php b/src/Path.php new file mode 100644 index 0000000..4c0ef4c --- /dev/null +++ b/src/Path.php @@ -0,0 +1,189 @@ + 1, + 'b' => 2, + 'c' => 3, + 'd' => 4, + 'e' => 5, + 'f' => 6, + 'g' => 7, + 'h' => 8, + ]; + + private bool $thereIsFigureOnPathMovement; + + private TypeMoving $typeMoving; + + private DirectionMovement $directionMovement; + + private int $numberOfCellsPassed = 0; + + private int $xIntFrom; + + private int $xIntTo; + + private int $xPath; + + private int $yPath; + + private bool $isThereAreFiguresOnWay = false; + + public function __construct( + private array $figures, + private string $xFrom, + private int $yFrom, + private string $xTo, + private int $yTo + ) { + $this->xIntFrom = static::HORIZONTAL[$this->xFrom]; + $this->xIntTo = static::HORIZONTAL[$this->xTo]; + + $this->xPath = abs($this->xIntFrom - $this->xIntTo); + $this->yPath = abs($this->yFrom - $this->yTo); + + $this->setTypeMoving(); + $this->setNumberOfCellsPassed(); + $this->setDirectionMovement(); + $this->setThereAreFiguresOnWay(); + } + + public function isThereAreFiguresOnWay(): bool + { + return $this->isThereAreFiguresOnWay; + } + + public function getNumberOfCellsPassed(): int + { + return $this->numberOfCellsPassed; + } + + public function getDirectionMovement(): DirectionMovement + { + return $this->directionMovement; + } + + private function setTypeMoving() + { + if ($this->xFrom === $this->xTo || $this->yFrom === $this->yTo) { + $this->typeMoving = TypeMoving::Direct; + } elseif ($this->xPath === $this->yPath) { + $this->typeMoving = TypeMoving::Diagonal; + } else { + $this->typeMoving = TypeMoving::Indefinite; + } + } + + public function getXFrom(): string + { + return $this->xFrom; + } + + public function getXTo(): string + { + return $this->xTo; + } + + public function getYFrom(): int + { + return $this->yFrom; + } + + public function getYTo(): int + { + return $this->yTo; + } + + private function setNumberOfCellsPassed() + { + if ($this->typeMoving === TypeMoving::Direct) { + $this->numberOfCellsPassed = max($this->xPath, $this->yPath); + } + if ($this->typeMoving === TypeMoving::Diagonal) { + $this->numberOfCellsPassed = $this->xPath; + } + } + + private function setDirectionMovement() + { + if ($this->typeMoving === TypeMoving::Direct) { + if ($this->xIntFrom > $this->xIntTo) { + $this->directionMovement = DirectionMovement::Left; + } elseif ($this->xIntFrom < $this->xIntTo) { + $this->directionMovement = DirectionMovement::Right; + } elseif ($this->yFrom < $this->yTo) { + $this->directionMovement = DirectionMovement::Up; + } elseif ($this->yFrom > $this->yTo) { + $this->directionMovement = DirectionMovement::Down; + } else { + $this->directionMovement = DirectionMovement::Indefinite; + } + } elseif ($this->typeMoving === TypeMoving::Diagonal) { + if ($this->xIntFrom < $this->xIntTo && $this->yFrom < $this->yTo) { + $this->directionMovement = DirectionMovement::UpRight; + } elseif ($this->xIntFrom > $this->xIntTo && $this->yFrom > $this->yTo) { + $this->directionMovement = DirectionMovement::DownRight; + } elseif ($this->xIntFrom < $this->xIntTo && $this->yFrom > $this->yTo) { + $this->directionMovement = DirectionMovement::DownLeft; + } elseif ($this->xIntFrom > $this->xIntTo && $this->yFrom < $this->yTo) { + $this->directionMovement = DirectionMovement::UpLeft; + } else { + $this->directionMovement = DirectionMovement::Indefinite; + } + } + } + + private function setThereAreFiguresOnWay() + { + if ($this->typeMoving === TypeMoving::Direct) { + if ($this->directionMovement === DirectionMovement::Up) { + for ($i = $this->yFrom + 1; $i <= $this->yTo; $i++) { + if (isset($this->figures[$this->xFrom][$i])) { + $this->isThereAreFiguresOnWay = true; + } + } + }elseif ($this->directionMovement === DirectionMovement::Down) { + for ($i = $this->yFrom - 1; $i >= $this->yTo; $i--) { + if (isset($this->figures[$this->xFrom][$i])) { + $this->isThereAreFiguresOnWay = true; + } + } + } + }elseif ($this->typeMoving === TypeMoving::Diagonal) { + $horizontalKey = array_flip(static::HORIZONTAL); + if ($this->directionMovement === DirectionMovement::UpLeft) { + for ($i = 1; $i <= $this->numberOfCellsPassed; $i++) { + $xValue = $horizontalKey[$this->xIntFrom - $i]; + if (isset($this->figures[$xValue][$this->yFrom + $i])) { + $this->isThereAreFiguresOnWay = true; + } + } + }elseif ($this->directionMovement === DirectionMovement::UpRight) { + for ($i = 1; $i <= $this->numberOfCellsPassed; $i++) { + $xValue = $horizontalKey[$this->xIntFrom + $i]; + if (isset($this->figures[$xValue][$this->yFrom + $i])) { + $this->isThereAreFiguresOnWay = true; + } + } + }elseif ($this->directionMovement === DirectionMovement::DownLeft) { + for ($i = 1; $i <= $this->numberOfCellsPassed; $i++) { + $xValue = $horizontalKey[$this->xIntFrom - $i]; + if (isset($this->figures[$xValue][$this->yFrom - $i])) { + $this->isThereAreFiguresOnWay = true; + } + } + }elseif ($this->directionMovement === DirectionMovement::DownRight) { + for ($i = 1; $i <= $this->numberOfCellsPassed; $i++) { + $xValue = $horizontalKey[$this->xIntFrom + $i]; + if (isset($this->figures[$xValue][$this->yFrom - $i])) { + $this->isThereAreFiguresOnWay = true; + } + } + } + } + } +} diff --git a/src/Pawn.php b/src/Pawn.php index 5c1a178..f545b00 100644 --- a/src/Pawn.php +++ b/src/Pawn.php @@ -1,7 +1,67 @@ isBlack ? '♟' : '♙'; } + + public function checkMove(Path $path) + { + if ($path->getNumberOfCellsPassed() > 2) { + throw new \Exception("A pawn moves a max of 2 squares"); + } + + $isBlackAndDown = $path->getDirectionMovement() === DirectionMovement::Down && $this->isBlack; + $isWhiteAndUp = $path->getDirectionMovement() === DirectionMovement::Up && !$this->isBlack; + $isDirectionSameAsColor = $isBlackAndDown || $isWhiteAndUp; + + //Пешка может ходить вперёд(по вертикали) на одну клетку; + if ($isDirectionSameAsColor + && $path->getNumberOfCellsPassed() === 1 + && !$path->isThereAreFiguresOnWay() + ) { + $this->notMoving = false; + return; + } + + //Если пешка ещё ни разу не ходила, она может пойти вперёд на две клетки; + if ($isDirectionSameAsColor + && $this->notMoving + && $path->getNumberOfCellsPassed() === 2 + && !$path->isThereAreFiguresOnWay() + ) { + $this->notMoving = false; + return; + } + + //Пешка может бить фигуры противника только по диагонали вперёд на одну клетку; + + $isBlackAndDownLeft = $path->getDirectionMovement() === DirectionMovement::DownLeft && $this->isBlack; + $isBlackAndDownRight = $path->getDirectionMovement() === DirectionMovement::DownRight && $this->isBlack; + + $isWhiteAndUpLeft = $path->getDirectionMovement() === DirectionMovement::UpLeft && !$this->isBlack; + $isWhiteAndUpRight = $path->getDirectionMovement() === DirectionMovement::UpRight && !$this->isBlack; + + $isDirectionSameAsColorForPunch = $isBlackAndDownLeft + || $isBlackAndDownRight + || $isWhiteAndUpLeft + || $isWhiteAndUpRight; + + if ( + $isDirectionSameAsColorForPunch + && $path->getNumberOfCellsPassed() === 1 + && $path->isThereAreFiguresOnWay() + ) { + $this->notMoving = false; + return; + } + + throw new \Exception( + "The move {$path->getXFrom()}{$path->getYFrom()}-{$path->getXTo()}{$path->getYTo()} with {$this->__toString()} is impossible" + ); + } } diff --git a/src/Queen.php b/src/Queen.php index 2e51be8..b9ec7cf 100644 --- a/src/Queen.php +++ b/src/Queen.php @@ -4,4 +4,9 @@ class Queen extends Figure { public function __toString() { return $this->isBlack ? '♛' : '♕'; } + + public function checkMove(Path $path) + { + // TODO: Implement checkMove() method. + } } diff --git a/src/Rook.php b/src/Rook.php index da39547..b36e30a 100644 --- a/src/Rook.php +++ b/src/Rook.php @@ -1,7 +1,14 @@ isBlack ? '♜' : '♖'; } + + public function checkMove(Path $path) + { + // TODO: Implement checkMove() method. + } } diff --git a/src/TypeMoving.php b/src/TypeMoving.php new file mode 100644 index 0000000..7725c73 --- /dev/null +++ b/src/TypeMoving.php @@ -0,0 +1,10 @@ +