mirror of
https://github.com/sigmasternchen/php-chess
synced 2025-03-14 23:58:53 +00:00
feat: Add game system function for fetching legal moves in position
This commit is contained in:
parent
2848208132
commit
ba9fc502c3
7 changed files with 461 additions and 1 deletions
|
@ -21,6 +21,22 @@ class Game {
|
|||
return array_filter($this->pieces, fn($p) => $p->getSide() == $side);
|
||||
}
|
||||
|
||||
private function &findPiece(Piece $needle): Piece {
|
||||
foreach ($this->pieces as &$piece) {
|
||||
if ($piece->equals($needle)) {
|
||||
return $piece;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException("piece not found: " . $piece);
|
||||
}
|
||||
|
||||
private function removePiece(Piece $needle): void {
|
||||
$this->pieces = array_values(
|
||||
array_filter($this->pieces, fn($p) => !($p->equals($needle)))
|
||||
);
|
||||
}
|
||||
|
||||
private function getKing(Side $side): King {
|
||||
if ($side == Side::WHITE) {
|
||||
return $this->whiteKing;
|
||||
|
@ -42,6 +58,24 @@ class Game {
|
|||
return $this->getOccupied($this->pieces);
|
||||
}
|
||||
|
||||
private function getCaptureable(array $pieces, bool $forPawn): FieldBitMap {
|
||||
$captureableMap = FieldBitMap::empty();
|
||||
foreach ($pieces as $piece) {
|
||||
$captureableMap = $captureableMap->union($piece->getCaptureableMap($forPawn));
|
||||
}
|
||||
|
||||
return $captureableMap;
|
||||
}
|
||||
|
||||
private function getThreatened(array $pieces, FieldBitMap $occupied): FieldBitMap {
|
||||
$threatenedMap = FieldBitMap::empty();
|
||||
foreach ($pieces as $piece) {
|
||||
$threatenedMap = $threatenedMap->union($piece->getCaptureMap($occupied));
|
||||
}
|
||||
|
||||
return $threatenedMap;
|
||||
}
|
||||
|
||||
private function isInCheck(Side $side, FieldBitMap $allOccupied): bool {
|
||||
$opponentPieces = $this->getPieces($side->getNext());
|
||||
$king = $this->getKing($side);
|
||||
|
@ -65,6 +99,131 @@ class Game {
|
|||
return $this->isInCheck($this->current->getNext(), $allOccupied);
|
||||
}
|
||||
|
||||
|
||||
public function getLegalMoves(): array {
|
||||
$ownPieces = $this->getPieces($this->current);
|
||||
$opponentPieces = $this->getPieces($this->current->getNext());
|
||||
|
||||
$occupied = $this->getOccupied($ownPieces);
|
||||
$threatened = $this->getThreatened($opponentPieces, $occupied);
|
||||
|
||||
return $this->getLegalMovesCached(
|
||||
$ownPieces,
|
||||
$opponentPieces,
|
||||
$occupied,
|
||||
$this->getCaptureable($opponentPieces, false),
|
||||
$this->getCaptureable($opponentPieces, true),
|
||||
$threatened,
|
||||
);
|
||||
}
|
||||
|
||||
private function generatePromotionMoves(Move $candidate): array {
|
||||
$candidates = [];
|
||||
|
||||
foreach (PieceType::getPromotionPieces() as $type) {
|
||||
$candidate->promoteTo = $type;
|
||||
$candidates[] = clone $candidate;
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
private function findCapturedPiece(Piece $piece, array $opponentPieces, Position $target): ?Piece {
|
||||
foreach ($opponentPieces as $capture) {
|
||||
if ($capture->getCaptureableMap($piece instanceof Pawn)->has($target)) {
|
||||
return $capture;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isCapture(Position $target, FieldBitMap $captureableForPawn): bool {
|
||||
return $captureableForPawn->has($target);
|
||||
}
|
||||
|
||||
private function getCandidateMovesForPiece(
|
||||
Piece $piece,
|
||||
array $opponentPieces,
|
||||
FieldBitMap $occupied,
|
||||
FieldBitMap $capturableForNonPawn,
|
||||
FieldBitMap $captureableForPawn,
|
||||
FieldBitMap $threatened,
|
||||
): array {
|
||||
$candidates = [];
|
||||
|
||||
$candidateMap = $piece->getMoveCandidateMap(
|
||||
$occupied,
|
||||
($piece instanceof Pawn) ? $captureableForPawn : $capturableForNonPawn,
|
||||
$threatened
|
||||
);
|
||||
|
||||
foreach ($candidateMap->getPositions() as $target) {
|
||||
$candidate = new Move($piece, $target);
|
||||
if ($this->isCapture($target, $captureableForPawn)) {
|
||||
$candidate->captures = $this->findCapturedPiece($piece, $opponentPieces, $target);
|
||||
}
|
||||
if ($piece->canPromote($target)) {
|
||||
$candidates = array_merge($candidates, $this->generatePromotionMoves($candidate));
|
||||
} else {
|
||||
$candidates[] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
private function isMoveLegal(Move $move) {
|
||||
$futureGame = $this->apply($move);
|
||||
return $futureGame->getGameState() != GameState::ILLEGAL;
|
||||
}
|
||||
|
||||
private function getLegalMovesCached(
|
||||
array &$ownPieces,
|
||||
array &$opponentPieces,
|
||||
FieldBitMap $occupied,
|
||||
FieldBitMap $capturableNonPawn,
|
||||
FieldBitMap $captureablePawn,
|
||||
FieldBitMap $threatened
|
||||
): array {
|
||||
$candidates = [];
|
||||
|
||||
foreach ($ownPieces as $piece) {
|
||||
$candidates = array_merge($candidates, $this->getCandidateMovesForPiece(
|
||||
$piece,
|
||||
$opponentPieces,
|
||||
$occupied,
|
||||
$capturableNonPawn,
|
||||
$captureablePawn,
|
||||
$threatened,
|
||||
));
|
||||
}
|
||||
|
||||
return array_values(array_filter($candidates, [$this, "isMoveLegal"]));
|
||||
}
|
||||
|
||||
public function apply(Move $move): Game {
|
||||
$game = clone $this;
|
||||
|
||||
if ($move->captures) {
|
||||
$game->removePiece($move->captures);
|
||||
}
|
||||
if ($move->promoteTo) {
|
||||
$game->removePiece($move->piece);
|
||||
|
||||
$promoted = $move->piece->promote($move->promoteTo);
|
||||
$promoted->move($move->target);
|
||||
$game->pieces[] = $promoted;
|
||||
} else {
|
||||
$piece = $game->findPiece($move->piece);
|
||||
$piece->move($move->target);
|
||||
}
|
||||
|
||||
$game->current = $game->current->getNext();
|
||||
|
||||
return $game;
|
||||
}
|
||||
|
||||
public function getGameState(): GameState {
|
||||
$allOccupied = $this->getAllOccupied();
|
||||
|
||||
|
@ -81,6 +240,10 @@ class Game {
|
|||
return GameState::DEFAULT;
|
||||
}
|
||||
|
||||
public function __clone(): void {
|
||||
$this->pieces = array_map(fn($p) => clone $p, $this->pieces);
|
||||
}
|
||||
|
||||
public function visualize(): string {
|
||||
|
||||
}
|
||||
|
|
33
src/Game/Move.php
Normal file
33
src/Game/Move.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace Game;
|
||||
|
||||
class Move {
|
||||
public Piece $piece;
|
||||
public Position $target;
|
||||
public ?Piece $captures = null;
|
||||
public ?PieceType $promoteTo = null;
|
||||
|
||||
public function __construct(Piece $piece, Position $target, ?Piece $captures = null, ?PieceType $promoteTo = null) {
|
||||
$this->piece = $piece;
|
||||
$this->target = $target;
|
||||
$this->captures = $captures;
|
||||
$this->promoteTo = $promoteTo;
|
||||
}
|
||||
|
||||
public function equals(Move $move): bool {
|
||||
return $this->piece->equals($move->piece) &&
|
||||
$this->target->equals($move->target) &&
|
||||
(
|
||||
($this->captures != null && $move->captures != null && $this->captures->equals($move->captures)) ||
|
||||
($this->captures == null && $move->captures == null)
|
||||
) &&
|
||||
$this->promoteTo == $move->promoteTo;
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return $this->piece . " " .
|
||||
$this->piece->getShort() . ($this->captures ? "x" : "") . $this->target .
|
||||
($this->promoteTo ? $this->promoteTo->getShort() : "");
|
||||
}
|
||||
}
|
|
@ -68,4 +68,8 @@ class Pawn extends Piece {
|
|||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function canPromote(Position $position): bool {
|
||||
return ($this->side == Side::WHITE) ? ($position->rank == 7) : ($position->rank == 0);
|
||||
}
|
||||
}
|
|
@ -9,9 +9,10 @@ abstract class Piece {
|
|||
protected bool $wasMovedLast = false;
|
||||
protected ?Position $oldPosition = null;
|
||||
|
||||
public function __construct(Position $position, Side $side) {
|
||||
public function __construct(Position $position, Side $side, bool $hasMoved = false) {
|
||||
$this->position = $position;
|
||||
$this->side = $side;
|
||||
$this->hasMoved = $hasMoved;
|
||||
}
|
||||
|
||||
public function tick() {
|
||||
|
@ -46,4 +47,44 @@ abstract class Piece {
|
|||
public function __toString() {
|
||||
return $this->getShort() . $this->getPosition();
|
||||
}
|
||||
|
||||
private static function getClassForType(PieceType $type): string {
|
||||
switch ($type) {
|
||||
case PieceType::PAWN:
|
||||
return Pawn::class;
|
||||
case PieceType::BISHOP:
|
||||
return Bishop::class;
|
||||
case PieceType::KNIGHT:
|
||||
return Knight::class;
|
||||
case PieceType::ROOK:
|
||||
return Rook::class;
|
||||
case PieceType::QUEEN:
|
||||
return Queen::class;
|
||||
case PieceType::KING:
|
||||
return King::class;
|
||||
}
|
||||
|
||||
throw new \RuntimeException("unknown piecetype " . $type);
|
||||
}
|
||||
|
||||
public static function ofType(PieceType $type, Position $position, Side $side): Piece {
|
||||
return new (self::getClassForType($type))($position, $side);
|
||||
}
|
||||
|
||||
public function promote(PieceType $type): Piece {
|
||||
$result = self::ofType($type, $this->position, $this->side);
|
||||
$result->hasMoved = $this->hasMoved;
|
||||
$result->wasMovedLast = $this->wasMovedLast;
|
||||
$result->oldPosition = $this->oldPosition;
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function equals(Piece $piece): bool {
|
||||
return get_class($this) == get_class($piece) &&
|
||||
$this->position->equals($piece->position);
|
||||
}
|
||||
|
||||
public function canPromote(Position $position): bool {
|
||||
return false;
|
||||
}
|
||||
}
|
27
src/Game/PieceType.php
Normal file
27
src/Game/PieceType.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Game;
|
||||
|
||||
enum PieceType {
|
||||
case PAWN;
|
||||
case BISHOP;
|
||||
case KNIGHT;
|
||||
case ROOK;
|
||||
case QUEEN;
|
||||
case KING;
|
||||
|
||||
public static function getPromotionPieces() {
|
||||
return [PieceType::BISHOP, PieceType::KNIGHT, PieceType::ROOK, PieceType::QUEEN];
|
||||
}
|
||||
|
||||
public function getShort(): string {
|
||||
return match ($this) {
|
||||
self::PAWN => "",
|
||||
self::BISHOP => "B",
|
||||
self::KNIGHT => "N",
|
||||
self::ROOK => "R",
|
||||
self::QUEEN => "Q",
|
||||
self::KING => "K",
|
||||
};
|
||||
}
|
||||
}
|
|
@ -20,4 +20,8 @@ class Position {
|
|||
public function __toString(): string {
|
||||
return ["a", "b", "c", "d", "e", "f", "g", "h"][$this->file] . ($this->rank + 1);
|
||||
}
|
||||
|
||||
public function equals(Position $position): bool {
|
||||
return $this->file == $position->file && $this->rank == $position->rank;
|
||||
}
|
||||
}
|
|
@ -6,6 +6,26 @@ use PHPUnit\Framework\TestCase;
|
|||
|
||||
final class GameTest extends TestCase {
|
||||
|
||||
protected function assertContainsEqualsOnce(object $needle, array $haystack) {
|
||||
if (!method_exists($needle, "equals")) {
|
||||
$this->assertFalse("equals() missing on needle");
|
||||
}
|
||||
|
||||
$result = false;
|
||||
|
||||
foreach ($haystack as $item) {
|
||||
if ($needle->equals($item)) {
|
||||
if ($result) {
|
||||
$this->assertFalse("element duplication");
|
||||
} else {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($result, "no such element");
|
||||
}
|
||||
|
||||
public function testGameState_illegal_white() {
|
||||
$subject = new Game(
|
||||
[
|
||||
|
@ -57,4 +77,172 @@ final class GameTest extends TestCase {
|
|||
|
||||
$this->assertEquals(GameState::CHECK, $subject->getGameState());
|
||||
}
|
||||
|
||||
public function testLegalMoves_pawnPinnedBecauseOfCheckKingRestrictedByQueenAndPawn() {
|
||||
$subject = new Game(
|
||||
[
|
||||
new King(new Position(7, 6), Side::BLACK),
|
||||
new Queen(new Position(1, 6), Side::BLACK),
|
||||
new Pawn(new Position(2, 6), Side::WHITE),
|
||||
new King(new Position(3, 6), Side::WHITE),
|
||||
],
|
||||
Side::WHITE
|
||||
);
|
||||
|
||||
$legalMoves = $subject->getLegalMoves();
|
||||
|
||||
$this->assertCount(5, $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new King(new Position(3, 6), Side::WHITE),
|
||||
new Position(3, 7),
|
||||
null, null,
|
||||
), $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new King(new Position(3, 6), Side::WHITE),
|
||||
new Position(4, 7),
|
||||
null, null,
|
||||
), $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new King(new Position(3, 6), Side::WHITE),
|
||||
new Position(4, 6),
|
||||
null, null,
|
||||
), $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new King(new Position(3, 6), Side::WHITE),
|
||||
new Position(4, 5),
|
||||
null, null,
|
||||
), $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new King(new Position(3, 6), Side::WHITE),
|
||||
new Position(3, 5),
|
||||
null, null,
|
||||
), $legalMoves);
|
||||
}
|
||||
|
||||
public function testLegalMoves_kingIsBlockedPawnCanPromote() {
|
||||
$subject = new Game(
|
||||
[
|
||||
new King(new Position(0, 0), Side::BLACK),
|
||||
new King(new Position(3, 6), Side::WHITE),
|
||||
new Queen(new Position(1, 2), Side::WHITE),
|
||||
new Pawn(new Position(7, 1), Side::BLACK, true),
|
||||
],
|
||||
Side::BLACK
|
||||
);
|
||||
|
||||
$legalMoves = $subject->getLegalMoves();
|
||||
|
||||
$this->assertCount(4, $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new Pawn(new Position(7, 1), Side::BLACK),
|
||||
new Position(7, 0),
|
||||
null, PieceType::BISHOP,
|
||||
), $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new Pawn(new Position(7, 1), Side::BLACK),
|
||||
new Position(7, 0),
|
||||
null, PieceType::KNIGHT,
|
||||
), $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new Pawn(new Position(7, 1), Side::BLACK),
|
||||
new Position(7, 0),
|
||||
null, PieceType::ROOK,
|
||||
), $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new Pawn(new Position(7, 1), Side::BLACK),
|
||||
new Position(7, 0),
|
||||
null, PieceType::QUEEN,
|
||||
), $legalMoves);
|
||||
}
|
||||
|
||||
public function testLegalMoves_kingIsBlockedInitialPawnMove() {
|
||||
$subject = new Game(
|
||||
[
|
||||
new King(new Position(0, 0), Side::BLACK),
|
||||
new King(new Position(3, 6), Side::WHITE),
|
||||
new Queen(new Position(1, 2), Side::WHITE),
|
||||
new Pawn(new Position(1, 6), Side::BLACK),
|
||||
],
|
||||
Side::BLACK
|
||||
);
|
||||
|
||||
$legalMoves = $subject->getLegalMoves();
|
||||
|
||||
$this->assertCount(2, $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new Pawn(new Position(1, 6), Side::BLACK),
|
||||
new Position(1, 5),
|
||||
null, null,
|
||||
), $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new Pawn(new Position(1, 6), Side::BLACK),
|
||||
new Position(1, 4),
|
||||
null, null,
|
||||
), $legalMoves);
|
||||
}
|
||||
|
||||
public function testLegalMoves_kingIsBlockedEnPassant() {
|
||||
$opponentPawn = new Pawn(new Position(3, 1), Side::WHITE);
|
||||
$opponentPawn->move(new Position(3, 3));
|
||||
|
||||
$subject = new Game(
|
||||
[
|
||||
new King(new Position(0, 0), Side::BLACK),
|
||||
new King(new Position(7, 6), Side::WHITE),
|
||||
new Queen(new Position(1, 2), Side::WHITE),
|
||||
$opponentPawn,
|
||||
new Pawn(new Position(4, 3), Side::BLACK, true),
|
||||
],
|
||||
Side::BLACK
|
||||
);
|
||||
|
||||
$legalMoves = $subject->getLegalMoves();
|
||||
|
||||
$this->assertCount(2, $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new Pawn(new Position(4, 3), Side::BLACK),
|
||||
new Position(4, 2),
|
||||
null, null,
|
||||
), $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new Pawn(new Position(4, 3), Side::BLACK),
|
||||
new Position(3, 2),
|
||||
$opponentPawn, null,
|
||||
), $legalMoves);
|
||||
}
|
||||
|
||||
public function testLegalMoves_kingIsInCheckAttackerCanBeTaken() {
|
||||
$subject = new Game(
|
||||
[
|
||||
new King(new Position(0, 0), Side::BLACK),
|
||||
new King(new Position(3, 6), Side::WHITE),
|
||||
new Queen(new Position(1, 1), Side::WHITE),
|
||||
new Queen(new Position(5, 1), Side::BLACK),
|
||||
],
|
||||
Side::BLACK
|
||||
);
|
||||
|
||||
$legalMoves = $subject->getLegalMoves();
|
||||
|
||||
$this->assertCount(2, $legalMoves);
|
||||
|
||||
$this->assertContainsEqualsOnce(new Move(
|
||||
new King(new Position(0, 0), Side::BLACK),
|
||||
new Position(1, 1),
|
||||
new Queen(new Position(1, 1), Side::WHITE), null,
|
||||
), $legalMoves);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue