mirror of
https://github.com/sigmasternchen/php-chess
synced 2025-03-14 23:58:53 +00:00
feat: Add three-fold-repetition check
This commit is contained in:
parent
26c44a0ff4
commit
b15e5fc922
3 changed files with 250 additions and 1 deletions
|
@ -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
114
src/Game/GameHistory.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
[
|
||||
|
|
Loading…
Reference in a new issue