From 29a05bd0921c3e9b350d763727b4a9a30c33ac86 Mon Sep 17 00:00:00 2001 From: overflowerror Date: Tue, 9 Jan 2024 22:31:41 +0100 Subject: [PATCH] feat: Add short algebraic notation for moves --- src/Game/Game.php | 4 +- src/Game/Move.php | 126 ++++++++++++++++++++++++++++++++++++---- src/Game/Position.php | 10 +++- tests/Game/MoveTest.php | 102 ++++++++++++++++++++++++++++++++ 4 files changed, 229 insertions(+), 13 deletions(-) create mode 100644 tests/Game/MoveTest.php diff --git a/src/Game/Game.php b/src/Game/Game.php index 150ffd8..bb52522 100644 --- a/src/Game/Game.php +++ b/src/Game/Game.php @@ -63,7 +63,7 @@ class Game { } } - private function getOccupied(array $pieces): FieldBitMap { + public function getOccupied(array $pieces): FieldBitMap { $occupiedMap = FieldBitMap::empty(); foreach ($pieces as $piece) { $occupiedMap->add($piece->getPosition()); @@ -76,7 +76,7 @@ class Game { return $this->getOccupied($this->pieces); } - private function getCaptureable(array $pieces, bool $forPawn): FieldBitMap { + public function getCaptureable(array $pieces, bool $forPawn): FieldBitMap { $captureableMap = FieldBitMap::empty(); foreach ($pieces as $piece) { $captureableMap = $captureableMap->union($piece->getCaptureableMap($forPawn)); diff --git a/src/Game/Move.php b/src/Game/Move.php index fd4d384..40960a3 100644 --- a/src/Game/Move.php +++ b/src/Game/Move.php @@ -37,20 +37,126 @@ class Move { ); } - public function getLong(): string { - if ($this->castleWith) { - if (abs($this->piece->getPosition()->file - $this->castleWith->getPosition()->file) > 3) { - return "O-O-O"; - } else { - return "O-O"; - } + private function getCheckMarker(?Game $game = null): string { + if (!$game) { + return ""; } else { - return $this->piece . " " . - $this->piece->getType()->getShort() . ($this->captures ? "x" : "") . $this->target . - ($this->promoteTo ? $this->promoteTo->getShort() : ""); + $game = $game->apply($this); + $state = $game->getGameState(); + + if ($state == GameState::CHECK) { + return "+"; + } else if ($state == GameState::CHECKMATE) { + return "++"; + } else { + return ""; + } } } + private function isCastles(): bool { + return !!$this->castleWith; + } + + private function isLongCastles(): bool { + return abs($this->piece->getPosition()->file - $this->castleWith->getPosition()->file) > 3; + } + + private function getCastlesMarker(): string { + if ($this->isLongCastles()) { + return "O-O-O"; + } else { + return "O-O"; + } + } + + private function getCapturesMarker(): string { + return $this->captures ? "x" : ""; + } + + private function getPromotionMarker(): string { + return $this->promoteTo ? $this->promoteTo->getShort() : ""; + } + + private function getMinimalSourcePositionMarker(Game $game): string { + $pieceClass = get_class($this->piece); + $piecePosition = $this->piece->getPosition(); + + $needsFile = false; + $needsRank = false; + $needsAny = false; + + foreach ($game->getLegalMoves() as $move) { + if (!is_a($move->piece, $pieceClass)) { + continue; + } + if (!$this->target->equals($move->target)) { + continue; + } + if ($this->piece->equals($move->piece)) { + continue; + } + + $otherPosition = $move->piece->getPosition(); + + $wouldNeedRank = $piecePosition->file == $otherPosition->file; + $wouldNeedFile = $piecePosition->rank == $otherPosition->rank; + + if ($wouldNeedRank) { + $needsRank = true; + $needsAny = false; + } + if ($wouldNeedFile) { + $needsFile = true; + $needsAny = false; + } + if (!$needsRank && !$needsFile) { + $needsAny = true; + } + } + + $result = ""; + if ($needsFile || $needsAny) { + $result .= $piecePosition->getFileString(); + } + if ($needsRank) { + $result .= $piecePosition->getRankString(); + } + + return $result; + } + + public function getLong(?Game $game = null): string { + if ($this->isCastles()) { + $result = $this->getCastlesMarker(); + } else { + $result = $this->piece; + $result .= $this->getCapturesMarker(); + $result .= $this->target; + $result .= $this->getPromotionMarker(); + } + + $result .= $this->getCheckMarker($game); + + return $result; + } + + public function getShort(Game $game): string { + if ($this->isCastles()) { + $result = $this->getCastlesMarker(); + } else { + $result = $this->piece->getType()->getShort(); + $result .= $this->getMinimalSourcePositionMarker($game); + $result .= $this->getCapturesMarker(); + $result .= $this->target; + $result .= $this->getPromotionMarker(); + } + + $result .= $this->getCheckMarker($game); + + return $result; + } + public function __toString(): string { return $this->getLong(); } diff --git a/src/Game/Position.php b/src/Game/Position.php index 64a1f6b..791f66e 100644 --- a/src/Game/Position.php +++ b/src/Game/Position.php @@ -21,8 +21,16 @@ class Position { return ($this->rank % 2) ^ ($this->file % 2) ? Side::WHITE : Side::BLACK; } + public function getFileString(): string { + return ["a", "b", "c", "d", "e", "f", "g", "h"][$this->file]; + } + + public function getRankString(): string { + return strval($this->rank + 1); + } + public function __toString(): string { - return ["a", "b", "c", "d", "e", "f", "g", "h"][$this->file] . ($this->rank + 1); + return $this->getFileString() . $this->getRankString(); } public function equals(Position $position): bool { diff --git a/tests/Game/MoveTest.php b/tests/Game/MoveTest.php new file mode 100644 index 0000000..9e7ba7b --- /dev/null +++ b/tests/Game/MoveTest.php @@ -0,0 +1,102 @@ +assertEquals("Nxd4", $subject->getShort($game)); + } + + public function testShort_simpleCollisionAndCheck() { + $game = new Game([ + new King(new Position(0,0), Side::WHITE), + new King(new Position(2, 5), Side::BLACK), + new Pawn(new Position(3, 3), Side::BLACK, true), + new Knight(new Position(2, 1), Side::WHITE), + new Knight(new Position(4, 5), Side::WHITE), + ], Side::WHITE); + + $subject = new Move( + new Knight(new Position(2, 1), Side::WHITE), + new Position(3, 3), + new Pawn(new Position(3, 3), Side::BLACK), + ); + + $this->assertEquals("Ncxd4+", $subject->getShort($game)); + } + + public function testShort_fileCollision() { + $game = new Game([ + new King(new Position(0,0), Side::WHITE), + new King(new Position(7, 7), Side::BLACK), + new Pawn(new Position(3, 3), Side::BLACK, true), + new Knight(new Position(2, 1), Side::WHITE), + new Knight(new Position(2, 5), Side::WHITE), + ], Side::WHITE); + + $subject = new Move( + new Knight(new Position(2, 1), Side::WHITE), + new Position(3, 3), + new Pawn(new Position(3, 3), Side::BLACK), + ); + + $this->assertEquals("N2xd4", $subject->getShort($game)); + } + + public function testShort_rankCollision() { + $game = new Game([ + new King(new Position(0,0), Side::WHITE), + new King(new Position(0, 0), Side::BLACK), + new Pawn(new Position(3, 3), Side::BLACK, true), + new Knight(new Position(2, 1), Side::WHITE), + new Knight(new Position(4, 1), Side::WHITE), + ], Side::WHITE); + + $subject = new Move( + new Knight(new Position(2, 1), Side::WHITE), + new Position(3, 3), + new Pawn(new Position(3, 3), Side::BLACK), + ); + + $this->assertEquals("Ncxd4", $subject->getShort($game)); + } + + public function testShort_fullCollisionAndCheckmate() { + $game = new Game([ + new King(new Position(0,0), Side::WHITE), + new King(new Position(4, 5), Side::BLACK), + new Pawn(new Position(3, 3), Side::BLACK, true), + new Knight(new Position(2, 1), Side::WHITE), + new Knight(new Position(2, 5), Side::WHITE), + new Knight(new Position(4, 1), Side::WHITE), + new Queen(new Position(6, 6), Side::WHITE), + new Rook(new Position(3, 7), Side::WHITE), + ], Side::WHITE); + + $subject = new Move( + new Knight(new Position(2, 1), Side::WHITE), + new Position(3, 3), + new Pawn(new Position(3, 3), Side::BLACK), + ); + + $this->assertEquals("Nc2xd4++", $subject->getShort($game)); + } + +} \ No newline at end of file