feat: Add three-fold-repetition check

This commit is contained in:
overflowerror 2024-01-07 15:03:23 +01:00
parent 26c44a0ff4
commit b15e5fc922
3 changed files with 250 additions and 1 deletions

View file

@ -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)) {

114
src/Game/GameHistory.php Normal file
View file

@ -0,0 +1,114 @@
<?php
namespace Game;
if (gettype(2147483648) == "double") {
die("need 64 bit integers");
}
class GameHistory {
private array $counts = [];
private function hasCastlingRights(array &$rooks, King $king): bool {
foreach ($rooks as $rook) {
if ($king->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;
}
}
}

View file

@ -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(
[