Skip to content

Commit d56afd3

Browse files
authored
Merge pull request #93 from garak/board-predicates
♻️ move board-only predicates to Board
2 parents 0b311ed + 50bd1ed commit d56afd3

6 files changed

Lines changed: 221 additions & 193 deletions

File tree

‎Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ install: ## install vendors
1919
docker compose exec php composer install
2020

2121
stan: ## static analysis via phpstan
22-
docker compose exec php php vendor/bin/phpstan analyse -v
22+
docker compose exec php php vendor/bin/phpstan analyse -v --memory-limit=-1
2323

2424
start: ## start docker image
2525
docker compose up -d

‎docs/board.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ foreach ($board as $offset => $piece) {
1616
}
1717
```
1818

19-
The only method that can be directly used is `$board->reverse()`, useful to toggle sides (e.g. to see
20-
the board from the black point of view, and then from the white again).
19+
The following methods can be used directly:
20+
21+
* `reverse()`: toggle sides (e.g. to see the board from the black point of view, and then from the white again)
22+
* `isAttacked(string $color, int $square): bool`: check if a square is attacked by any piece of the given color
23+
(e.g. `$chess->board->isAttacked(Piece::WHITE, Board::SQUARES['e4'])`)
24+
* `hasInsufficientMaterial(): bool`: check if material is insufficient to continue the game.
25+
Same as `Chess::insufficientMaterial()`
2126

2227
The following static methods can be used if you need to deal
2328
with [SAN](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)):

‎src/Board.php‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,113 @@ public function isReversed(): bool
142142
return $this->reversed;
143143
}
144144

145+
public function isAttacked(string $color, int $square): bool
146+
{
147+
for ($i = self::SQUARES['a8']; $i <= self::SQUARES['h1']; ++$i) {
148+
if (($i & 0x88) > 0) {
149+
$i += 7;
150+
continue;
151+
}
152+
$piece = $this->squares[$i] ?? null;
153+
// check empty square and color
154+
if (null === $piece || $piece->getColor() !== $color) {
155+
continue;
156+
}
157+
158+
$difference = $i - $square;
159+
$index = $difference + 119;
160+
161+
if ((self::ATTACKS[$index] & (1 << Piece::SHIFTS[$piece->getType()])) > 0) {
162+
if ($piece->isPawn()) {
163+
if ($difference > 0) {
164+
if ($piece->getColor() === Piece::WHITE) {
165+
return true;
166+
}
167+
} elseif ($piece->getColor() === Piece::BLACK) {
168+
return true;
169+
}
170+
continue;
171+
}
172+
173+
if ($piece->isKnight() || $piece->isKing()) {
174+
return true;
175+
}
176+
177+
$offset = self::RAYS[$index];
178+
$j = $i + $offset;
179+
$blocked = false;
180+
while ($j !== $square) {
181+
if (($this->squares[$j] ?? null) !== null) {
182+
$blocked = true;
183+
break;
184+
}
185+
$j += $offset;
186+
}
187+
188+
if (!$blocked) {
189+
return true;
190+
}
191+
}
192+
}
193+
194+
return false;
195+
}
196+
197+
public function hasInsufficientMaterial(): bool
198+
{
199+
$pieces = [
200+
Piece::PAWN => 0,
201+
Piece::KNIGHT => 0,
202+
Piece::BISHOP => 0,
203+
Piece::ROOK => 0,
204+
Piece::QUEEN => 0,
205+
Piece::KING => 0,
206+
];
207+
$bishops = [];
208+
$numPieces = 0;
209+
$sqColor = 0;
210+
211+
for ($i = self::SQUARES['a8']; $i <= self::SQUARES['h1']; ++$i) {
212+
$sqColor = ($sqColor + 1) % 2;
213+
if (($i & 0x88) > 0) {
214+
$i += 7;
215+
continue;
216+
}
217+
218+
$piece = $this->squares[$i] ?? null;
219+
if ($piece !== null) {
220+
++$pieces[$piece->getType()];
221+
if ($piece->isBishop()) {
222+
$bishops[] = $sqColor;
223+
}
224+
++$numPieces;
225+
}
226+
}
227+
228+
// k vs k
229+
if ($numPieces === 2) {
230+
return true;
231+
}
232+
233+
// k vs kn / k vs kb
234+
if ($numPieces === 3 && ($pieces[Piece::BISHOP] === 1 || $pieces[Piece::KNIGHT] === 1)) {
235+
return true;
236+
}
237+
238+
// k(b){0,} vs k(b){0,} , because maybe you are a programmer we talk in regex (preg) :-p
239+
if ($numPieces === $pieces[Piece::BISHOP] + 2) {
240+
$sum = 0;
241+
foreach ($bishops as $bishop) {
242+
$sum += $bishop;
243+
}
244+
if ($sum === 0 || $sum === \count($bishops)) {
245+
return true;
246+
}
247+
}
248+
249+
return false;
250+
}
251+
145252
public static function rank(int $i): int
146253
{
147254
return $i >> 4;

‎src/Chess.php‎

Lines changed: 9 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -526,9 +526,9 @@ protected function generateMoves(?int $square = null, bool $legal = true): array
526526
if (
527527
$this->board[$castlingFrom + 1] === null &&
528528
$this->board[$castlingTo] === null &&
529-
!$this->attacked($them, $this->kings[$us]) &&
530-
!$this->attacked($them, $castlingFrom + 1) &&
531-
!$this->attacked($them, $castlingTo)
529+
!$this->board->isAttacked($them, $this->kings[$us]) &&
530+
!$this->board->isAttacked($them, $castlingFrom + 1) &&
531+
!$this->board->isAttacked($them, $castlingTo)
532532
) {
533533
self::addMove($us, $this->board, $moves, $this->kings[$us], $castlingTo, Move::BITS['KSIDE_CASTLE']);
534534
}
@@ -543,9 +543,9 @@ protected function generateMoves(?int $square = null, bool $legal = true): array
543543
$this->board[$castlingFrom - 1] === null &&
544544
$this->board[$castlingFrom - 2] === null && // $castlingTo
545545
$this->board[$castlingFrom - 3] === null && // col "b", next to rock
546-
!$this->attacked($them, $this->kings[$us]) &&
547-
!$this->attacked($them, $castlingFrom - 1) &&
548-
!$this->attacked($them, $castlingTo)
546+
!$this->board->isAttacked($them, $this->kings[$us]) &&
547+
!$this->board->isAttacked($them, $castlingFrom - 1) &&
548+
!$this->board->isAttacked($them, $castlingTo)
549549
) {
550550
self::addMove($us, $this->board, $moves, $this->kings[$us], $castlingTo, Move::BITS['QSIDE_CASTLE']);
551551
}
@@ -638,59 +638,12 @@ public function moves(?int $square = null): array
638638

639639
protected function attacked(string $color, int $square): bool
640640
{
641-
for ($i = Board::SQUARES['a8']; $i <= Board::SQUARES['h1']; ++$i) {
642-
if (($i & 0x88) > 0) {
643-
$i += 7;
644-
continue;
645-
}
646-
$piece = $this->board[$i];
647-
// check empty square and color
648-
if (null === $piece || $piece->getColor() !== $color) {
649-
continue;
650-
}
651-
652-
$difference = $i - $square;
653-
$index = $difference + 119;
654-
655-
if ((Board::ATTACKS[$index] & (1 << Piece::SHIFTS[$piece->getType()])) > 0) {
656-
if ($piece->isPawn()) {
657-
if ($difference > 0) {
658-
if ($piece->getColor() === Piece::WHITE) {
659-
return true;
660-
}
661-
} elseif ($piece->getColor() === Piece::BLACK) {
662-
return true;
663-
}
664-
continue;
665-
}
666-
667-
if ($piece->isKnight() || $piece->isKing()) {
668-
return true;
669-
}
670-
671-
$offset = Board::RAYS[$index];
672-
$j = $i + $offset;
673-
$blocked = false;
674-
while ($j !== $square) {
675-
if ($this->board[$j] !== null) {
676-
$blocked = true;
677-
break;
678-
}
679-
$j += $offset;
680-
}
681-
682-
if (!$blocked) {
683-
return true;
684-
}
685-
}
686-
}
687-
688-
return false;
641+
return $this->board->isAttacked($color, $square);
689642
}
690643

691644
protected function kingAttacked(string $color): bool
692645
{
693-
return null !== $this->kings[$color] && $this->attacked(self::swapColor($color), $this->kings[$color]);
646+
return null !== $this->kings[$color] && $this->board->isAttacked(self::swapColor($color), $this->kings[$color]);
694647
}
695648

696649
public function inCheck(): bool
@@ -710,57 +663,7 @@ public function inStalemate(): bool
710663

711664
public function insufficientMaterial(): bool
712665
{
713-
$pieces = [
714-
Piece::PAWN => 0,
715-
Piece::KNIGHT => 0,
716-
Piece::BISHOP => 0,
717-
Piece::ROOK => 0,
718-
Piece::QUEEN => 0,
719-
Piece::KING => 0,
720-
];
721-
$bishops = [];
722-
$numPieces = 0;
723-
$sqColor = 0;
724-
725-
for ($i = Board::SQUARES['a8']; $i <= Board::SQUARES['h1']; ++$i) {
726-
$sqColor = ($sqColor + 1) % 2;
727-
if (($i & 0x88) > 0) {
728-
$i += 7;
729-
continue;
730-
}
731-
732-
$piece = $this->board[$i];
733-
if ($piece !== null) {
734-
++$pieces[$piece->getType()];
735-
if ($piece->isBishop()) {
736-
$bishops[] = $sqColor;
737-
}
738-
++$numPieces;
739-
}
740-
}
741-
742-
// k vs k
743-
if ($numPieces === 2) {
744-
return true;
745-
}
746-
747-
// k vs kn / k vs kb
748-
if ($numPieces === 3 && ($pieces[Piece::BISHOP] === 1 || $pieces[Piece::KNIGHT] === 1)) {
749-
return true;
750-
}
751-
752-
// k(b){0,} vs k(b){0,} , because maybe you are a programmer we talk in regex (preg) :-p
753-
if ($numPieces === $pieces[Piece::BISHOP] + 2) {
754-
$sum = 0;
755-
foreach ($bishops as $bishop) {
756-
$sum += $bishop;
757-
}
758-
if ($sum === 0 || $sum === \count($bishops)) {
759-
return true;
760-
}
761-
}
762-
763-
return false;
666+
return $this->board->hasInsufficientMaterial();
764667
}
765668

766669
/* TODO: while this function is fine for casual use, a better

0 commit comments

Comments
 (0)