From 2848208132af035a89e35583981fab2e61a30c47 Mon Sep 17 00:00:00 2001 From: overflowerror Date: Wed, 3 Jan 2024 14:02:52 +0100 Subject: [PATCH] feat: Add game system to recognize illegal and check game states --- src/Game/FieldBitMap.php | 6 +++ src/Game/Game.php | 87 ++++++++++++++++++++++++++++++++++++++++ src/Game/GameState.php | 14 +++++++ src/Game/King.php | 4 ++ src/Game/Piece.php | 12 +++++- src/Game/Side.php | 8 ++++ tests/Game/GameTest.php | 60 +++++++++++++++++++++++++++ 7 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/Game/Game.php create mode 100644 src/Game/GameState.php create mode 100644 tests/Game/GameTest.php diff --git a/src/Game/FieldBitMap.php b/src/Game/FieldBitMap.php index 014ac13..39eeaf7 100644 --- a/src/Game/FieldBitMap.php +++ b/src/Game/FieldBitMap.php @@ -29,6 +29,12 @@ class FieldBitMap { } } + public function remove(Position $position): void { + if ($position->isValid()) { + $this->map &= ~(1 << $this->getBitForPosition($position)); + } + } + public function intersect(FieldBitMap $map): FieldBitMap { return new FieldBitMap($this->map & $map->map); } diff --git a/src/Game/Game.php b/src/Game/Game.php new file mode 100644 index 0000000..3d6a6e7 --- /dev/null +++ b/src/Game/Game.php @@ -0,0 +1,87 @@ +pieces = $pieces; + $this->current = $current; + + $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)); + } + + private function getPieces(Side $side): array { + return array_filter($this->pieces, fn($p) => $p->getSide() == $side); + } + + private function getKing(Side $side): King { + if ($side == Side::WHITE) { + return $this->whiteKing; + } else { + return $this->blackKing; + } + } + + private function getOccupied(array $pieces): FieldBitMap { + $occupiedMap = FieldBitMap::empty(); + foreach ($pieces as $piece) { + $occupiedMap->add($piece->getPosition()); + } + + return $occupiedMap; + } + + private function getAllOccupied(): FieldBitMap { + return $this->getOccupied($this->pieces); + } + + private function isInCheck(Side $side, FieldBitMap $allOccupied): bool { + $opponentPieces = $this->getPieces($side->getNext()); + $king = $this->getKing($side); + + $occupied = clone $allOccupied; + $occupied->remove($king->getPosition()); + + $captureMap = FieldBitMap::empty(); + foreach ($opponentPieces as $piece) { + $captureMap = $captureMap->union($piece->getCaptureMap($occupied)); + } + + return $king->isInCheck($captureMap); + } + + private function isCheck(FieldBitMap $allOccupied): bool { + return $this->isInCheck($this->current, $allOccupied); + } + + private function isIllegal(FieldBitMap $allOccupied): bool { + return $this->isInCheck($this->current->getNext(), $allOccupied); + } + + public function getGameState(): GameState { + $allOccupied = $this->getAllOccupied(); + + if ($this->isIllegal($allOccupied)) { + return GameState::ILLEGAL; + } + + if ($this->isCheck($allOccupied)) { + // TODO: check for checkmate + + return GameState::CHECK; + } + + return GameState::DEFAULT; + } + + public function visualize(): string { + + } +} \ No newline at end of file diff --git a/src/Game/GameState.php b/src/Game/GameState.php new file mode 100644 index 0000000..2f7e0f3 --- /dev/null +++ b/src/Game/GameState.php @@ -0,0 +1,14 @@ +has($this->position); + } } \ No newline at end of file diff --git a/src/Game/Piece.php b/src/Game/Piece.php index d410d70..ff7e08a 100644 --- a/src/Game/Piece.php +++ b/src/Game/Piece.php @@ -25,6 +25,14 @@ abstract class Piece { $this->wasMovedLast = true; } + public function getSide(): Side { + return $this->side; + } + + public function getPosition(): Position { + return $this->position; + } + abstract public function getName(): string; abstract public function getShort(): string; abstract public function getMoveCandidateMap(FieldBitMap $occupied, FieldBitMap $captureable, FieldBitMap $threatened): FieldBitMap; @@ -35,5 +43,7 @@ abstract class Piece { return new FieldBitMap([$this->position]); } - + public function __toString() { + return $this->getShort() . $this->getPosition(); + } } \ No newline at end of file diff --git a/src/Game/Side.php b/src/Game/Side.php index e5cee8d..7b7afd7 100644 --- a/src/Game/Side.php +++ b/src/Game/Side.php @@ -5,4 +5,12 @@ namespace Game; enum Side { case WHITE; case BLACK; + + public function getNext(): Side { + if ($this == Side::WHITE) { + return Side::BLACK; + } else { + return Side::WHITE; + } + } } \ No newline at end of file diff --git a/tests/Game/GameTest.php b/tests/Game/GameTest.php new file mode 100644 index 0000000..69c7e7f --- /dev/null +++ b/tests/Game/GameTest.php @@ -0,0 +1,60 @@ +assertEquals(GameState::ILLEGAL, $subject->getGameState()); + } + + public function testGameState_illegal_black() { + $subject = new Game( + [ + new King(new Position(0, 0), Side::WHITE), + new Queen(new Position(7, 7), Side::BLACK), + new King(new Position(7, 6), Side::BLACK), + ], + Side::BLACK + ); + + $this->assertEquals(GameState::ILLEGAL, $subject->getGameState()); + } + + public function testGameState_check_white() { + $subject = new Game( + [ + new King(new Position(5, 4), Side::WHITE), + new Rook(new Position(2, 4), Side::BLACK), + new King(new Position(7, 6), Side::BLACK), + ], + Side::WHITE + ); + + $this->assertEquals(GameState::CHECK, $subject->getGameState()); + } + + public function testGameState_check_black() { + $subject = new Game( + [ + new King(new Position(5, 4), Side::BLACK), + new Pawn(new Position(4, 3), Side::WHITE), + new King(new Position(7, 6), Side::WHITE), + ], + Side::BLACK + ); + + $this->assertEquals(GameState::CHECK, $subject->getGameState()); + } +} \ No newline at end of file