From b15e5fc92295821b68f66aa7a4a42592e5f9a07a Mon Sep 17 00:00:00 2001 From: overflowerror Date: Sun, 7 Jan 2024 15:03:23 +0100 Subject: [PATCH] feat: Add three-fold-repetition check --- src/Game/Game.php | 14 ++++- src/Game/GameHistory.php | 114 ++++++++++++++++++++++++++++++++++++ tests/Game/GameTest.php | 123 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/Game/GameHistory.php diff --git a/src/Game/Game.php b/src/Game/Game.php index 57709cc..f1a2963 100644 --- a/src/Game/Game.php +++ b/src/Game/Game.php @@ -9,6 +9,8 @@ class Game { private Side $current; + private GameHistory $history; + private ?array $moveCache = null; public function __construct(array $pieces, Side $current) { @@ -17,6 +19,9 @@ class Game { $this->whiteKing = current(array_filter($this->pieces, fn($p) => ($p instanceof King) && $p->getSide() == Side::WHITE)); $this->blackKing = current(array_filter($this->pieces, fn($p) => ($p instanceof King) && $p->getSide() == Side::BLACK)); + + $this->history = new GameHistory(); + $this->history->add($this); } public function getCurrentSide(): Side { @@ -47,7 +52,7 @@ class Game { ); } - private function getKing(Side $side): King { + public function getKing(Side $side): King { if ($side == Side::WHITE) { return $this->whiteKing; } else { @@ -260,6 +265,7 @@ class Game { array_map(fn($p) => clone $p, $this->pieces), $this->current, ); + $game->history = clone $game->history; $game->applyInPlace($move); @@ -302,6 +308,8 @@ class Game { } $this->current = $this->current->getNext(); + + $this->history->add($this); } public function getGameState(bool $onlyIsLegal = false): GameState { @@ -315,6 +323,10 @@ class Game { return GameState::UNKNOWN_VALID; } + if ($this->history->count($this) >= 3) { + return GameState::THREEFOLD_REPETITION; + } + $legalMoves = $this->getLegalMoves(); if ($this->isCheck($allOccupied)) { diff --git a/src/Game/GameHistory.php b/src/Game/GameHistory.php new file mode 100644 index 0000000..b936b69 --- /dev/null +++ b/src/Game/GameHistory.php @@ -0,0 +1,114 @@ +canCastle(FieldBitMap::empty(), FieldBitMap::empty(), FieldBitMap::empty(), $rook)) { + return true; + } + } + + return false; + } + + private function getHashForPieces(array &$pieces): string { + usort($pieces, function ($a, $b) { + $ap = $a->getPosition(); + $bp = $b->getPosition(); + $fileComp = $ap->file <=> $bp->file; + if ($fileComp == 0) { + return $ap->rank <=> $bp->rank; + } else { + return $fileComp; + } + }); + + return join("", array_map(function ($piece) { + $pos = $piece->getPosition(); + $num = $pos->file * 8 + $pos->rank; + if ($num < 26) { + return chr(ord('a') + $num); + } else if ($num < 26 + 26) { + return chr(ord('A') + $num - 26); + } else if ($num < 26 + 26 + 10) { + return chr(ord('0') + $num - 26 - 26); + } else { // 26 + 26 + 10 = 62 -> 2 left + return ["-", "="][$num - 26 - 26 - 10]; + } + }, $pieces)); + } + + private function getHashForSide(array &$pieces, King $king): string { + $pawns = []; + $bishops = []; + $knights = []; + $rooks = []; + $queens = []; + foreach ($pieces as $piece) { + if ($piece instanceof Pawn) { + $pawns[] = $piece; + } else if ($piece instanceof Bishop) { + $bishops[] = $piece; + } else if ($piece instanceof Knight) { + $knights[] = $piece; + } else if ($piece instanceof Rook) { + $rooks[] = $piece; + } else if ($piece instanceof Queen) { + $queens[] = $piece; + } + } + + $kings = [$king]; + + return join( + ",", + array_map( + [$this, "getHashForPieces"], + [$pawns, $bishops, $knights, $rooks, $queens, $kings] + ) + ) . ($this->hasCastlingRights($rooks, $king) ? "." : ""); + } + + private function getHash(array $whitePieces, array $blackPieces, King $whiteKing, King $blackKing) { + return $this->getHashForSide($whitePieces, $whiteKing) . + $this->getHashForSide($blackPieces, $blackKing); + } + + private function getHashForGame(Game $game): string { + return ($game->getCurrentSide() == Side::WHITE ? "*" : "+") . + $this->getHash( + $game->getPieces(Side::WHITE), + $game->getPieces(Side::BLACK), + $game->getKing(Side::WHITE), + $game->getKing(Side::BLACK), + ); + } + + public function count(Game $game): int { + $hash = $this->getHashForGame($game); + + if (array_key_exists($hash, $this->counts)) { + return $this->counts[$hash]; + } else { + return 0; + } + } + + public function add(Game $game): void { + $hash = $this->getHashForGame($game); + + if (array_key_exists($hash, $this->counts)) { + $this->counts[$hash]++; + } else { + $this->counts[$hash] = 1; + } + } +} \ No newline at end of file diff --git a/tests/Game/GameTest.php b/tests/Game/GameTest.php index 992f0b7..6bc2509 100644 --- a/tests/Game/GameTest.php +++ b/tests/Game/GameTest.php @@ -134,6 +134,129 @@ final class GameTest extends TestCase { $this->assertEquals(GameState::STALEMATE, $subject->getGameState()); } + + public function testGameState_threeFoldRepetition_black() { + $subject = new Game( + [ + new King(new Position(1, 1), Side::BLACK, true), + new King(new Position(7, 6), Side::WHITE, true), + ], + Side::BLACK + ); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(1, 1), Side::BLACK), + new Position(1, 2), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(7, 6), Side::WHITE), + new Position(7, 7), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(1, 2), Side::BLACK), + new Position(1, 1), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(7, 7), Side::WHITE), + new Position(7, 6), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(1, 1), Side::BLACK), + new Position(1, 2), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(7, 6), Side::WHITE), + new Position(7, 7), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(1, 2), Side::BLACK), + new Position(1, 1), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(7, 7), Side::WHITE), + new Position(7, 6), + )); + + $this->assertEquals(GameState::THREEFOLD_REPETITION, $subject->getGameState()); + } + + + public function testGameState_noThreeFoldRepetitionWithCastlingRights_black() { + $subject = new Game( + [ + new King(new Position(4, 7), Side::BLACK, false), + new Rook(new Position(0, 7), Side::BLACK, false), + new King(new Position(7, 6), Side::WHITE, true), + ], + Side::BLACK + ); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(4, 7), Side::BLACK), + new Position(3, 7), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(7, 6), Side::WHITE), + new Position(7, 7), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(3, 7), Side::BLACK), + new Position(4, 7), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(7, 7), Side::WHITE), + new Position(7, 6), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(4, 7), Side::BLACK), + new Position(3, 7), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(7, 6), Side::WHITE), + new Position(7, 7), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(3, 7), Side::BLACK), + new Position(4, 7), + )); + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + + $subject->applyInPlace(new Move( + new King(new Position(7, 7), Side::WHITE), + new Position(7, 6), + )); + + $this->assertEquals(GameState::DEFAULT, $subject->getGameState()); + } + public function testLegalMoves_pawnPinnedBecauseOfCheckKingRestrictedByQueenAndPawn() { $subject = new Game( [