Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement DeflectionEval #579

4 changes: 4 additions & 0 deletions docs/heuristics.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Listed below are the chess heuristics implemented in PHP Chess.
| Center | [Chess\Eval\CenterEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/CenterEvalTest.php) |
| Connectivity | [Chess\Eval\ConnectivityEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/ConnectivityEvalTest.php) |
| Defense | [Chess\Eval\DefenseEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/DefenseEvalTest.php) |
| Deflection | [Chess\Eval\DeflectionEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/DeflectionEvalTest.php) |
| Diagonal opposition | [Chess\Eval\DiagonalOppositionEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/DiagonalOppositionEvalTest.php) |
| Direct opposition | [Chess\Eval\DirectOppositionEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/DirectOppositionEvalTest.php) |
| Discovered check | [Chess\Eval\DiscoveredCheckEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/DiscoveredCheckEvalTest.php) |
Expand All @@ -35,6 +36,7 @@ Listed below are the chess heuristics implemented in PHP Chess.
| King safety | [Chess\Eval\KingSafetyEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/KingSafetyEvalTest.php) |
| Knight outpost | [Chess\Eval\KnightOutpostEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/KnightOutpostEvalTest.php) |
| Material | [Chess\Eval\MaterialEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/MaterialEvalTest.php) |
| Overloading | [Chess\Eval\OverloadingEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/OverloadingEvalTest.php) |
| Passed pawn | [Chess\Eval\PassedPawnEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/PassedPawnEvalTest.php) |
| Pressure | [Chess\Eval\PressureEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/PressureEvalTest.php) |
| Protection | [Chess\Eval\ProtectionEval](https://github.com/chesslablab/php-chess/blob/main/tests/unit/Eval/ProtectionEvalTest.php) |
Expand Down Expand Up @@ -98,6 +100,7 @@ Array
[26] => Direct opposition
[27] => Attack
[28] => Overloading
[29] => Deflection
)

[balance] => Array
Expand Down Expand Up @@ -131,6 +134,7 @@ Array
[26] => 0
[27] => 0
[28] => 0
[29] => 0
)

)
Expand Down
227 changes: 227 additions & 0 deletions src/Eval/DeflectionEval.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<?php

namespace Chess\Eval;

use Chess\Tutor\PiecePhrase;
use Chess\Variant\AbstractBoard;
use Chess\Variant\AbstractPiece;
use Chess\Variant\Classical\PGN\AN\Piece;

class DeflectionEval extends AbstractEval implements ElaborateEvalInterface
{
use ElaborateEvalTrait;

const NAME = 'Deflection';

protected bool $deflectionExists = false;
protected bool $checkExists = false;
protected bool $mateExists = false;

public function __construct(AbstractBoard $board)
{
$this->board = $board;
$checkingPieces = $this->board->piece($this->board->turn, Piece::K)->attacking();

foreach ($this->board->pieces($this->board->turn) as $piece) {
$legalMoveSquares = $this->board->legal($piece->sq);
$defendedSquares = $this->board->pieceBySq($piece->sq)->defendedSqs();
// considering deflection due to pieces checking the king or attacking the piece
$attackingPieces = $checkingPieces + $piece->attacking();

// for optimisation, sorting legal moves to check cases of capturing attacking pieces first, as they would majorly lead to deflection cases
$this->sortLegalMoves($legalMoveSquares, $attackingPieces);

if (!empty($legalMoveSquares) && !empty($attackingPieces)) {
$legalMovesCount = count($legalMoveSquares);
$primaryDeflectionPhrase = $this->primaryPhrase($piece, $attackingPieces, $legalMovesCount);

foreach ($legalMoveSquares as $square) {
$clone = $this->board->clone();
$clone->playLan($clone->turn, $piece->sq . $square);

// checks for exposed pieces and protected pawns to ensure deflection
$exposedPieceList = $this->checkExposedPieceAdvantage($clone, $defendedSquares, $square);
$protectedPawnsList = $this->checkAdvancedPawnAdvantage($clone, $piece);

if (!empty($exposedPieceList) || !empty($protectedPawnsList)) {
$this->deflectionExists = true;
$this->elaborateAdvantage($primaryDeflectionPhrase, $exposedPieceList, $protectedPawnsList, $legalMovesCount);
break;
}
}
}
if ($this->deflectionExists) {
break;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case deflection exists, the optimal result for the first case for that piece and move found will be returned to keep things simple, if we rather want to evaluate for all the pieces, we can extend onto it, which would require a few updates, but for simplicity and optimization, I limited it to a single case, looks fine to me.

}
}

/**
* sorts the legal moves array, so that it arranges the moves for capturing the attacking pieces on top
*/
private function sortLegalMoves(&$legalMoveSquares, $attackingPieces)
Comment on lines +69 to +72
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorting the legal moves to arrange the moves for capturing the attacking pieces on top. As during testing, I realized, that in most cases, deflection usually happens for these moves, also are more practical.

{
usort($legalMoveSquares, function ($a, $b) use ($attackingPieces) {
$aInAttacking = array_key_exists($a, $attackingPieces);
$bInAttacking = array_key_exists($b, $attackingPieces);

if ($aInAttacking && !$bInAttacking) {
return -1;
} elseif (!$aInAttacking && $bInAttacking) {
return 1;
} else {
return 0;
}
});
}

private function primaryPhrase(AbstractPiece $piece, array $attackingPieces, int $legalMovesCount): string
{
$piecePhrase = PiecePhrase::create($piece);
$basePhrase = $piecePhrase . " is deflected";
$attackedByPhrase = $this->attackedByPhrase($attackingPieces);

return ($legalMovesCount > 1 ? "If " : "") . $basePhrase . $attackedByPhrase . ", ";
}

private function attackedByPhrase(array $attackingPieces): string
{
$basePhrase = " due to ";
if (count($attackingPieces) === 1) {
return $basePhrase . PiecePhrase::create($attackingPieces[0]);
} elseif (count($attackingPieces) > 1) {
$squares = array_map(fn($attacking) => PiecePhrase::create($attacking), $attackingPieces);
$lastSquare = array_pop($squares);
return $basePhrase . implode(', ', $squares) . ' and ' . $lastSquare;
}

return "";
}

/**
* checks for exposed pieces due to deflection, also checks for possibility of check or mate to the king
*/
private function checkExposedPieceAdvantage(AbstractBoard $clone, array $defendedSquares, string $square): array
{
$piecePhrase = [];

$updatedDefendedSquares = $clone->pieceBySq($square)->defendedSqs();
$undefendedSquares = array_diff($defendedSquares, $updatedDefendedSquares);

if (!empty($undefendedSquares)) {
foreach ($undefendedSquares as $undefendedSquare) {
$attackers = $clone->pieceBySq($undefendedSquare)->attacking();
if (!empty($attackers)) {
$piecePhrase[] = PiecePhrase::create($clone->pieceBySq($undefendedSquare));

foreach ($attackers as $attacker) {
$clone->playLan($clone->turn, $attacker->sq . $undefendedSquare);
if ($clone->isMate()) {
$this->mateExists = true;
} else if ($clone->isCheck()) {
$this->checkExists = true;
}
$clone->undo();
}
}
}
}

return $piecePhrase;
}

/**
* checks for the advanced pawns that become protected due to deflection
*/
private function checkAdvancedPawnAdvantage(AbstractBoard $clone, AbstractPiece $piece): array
{
$pawnsList = [];

$advancedPawnEval = new FarAdvancedPawnEval($clone);
$advancedPawns = $advancedPawnEval->getResult()[$clone->turn];

if (!empty($advancedPawns)) {
foreach ($advancedPawns as $pawn) {
$attackersDiff = array_udiff_assoc(
$this->board->pieceBySq($pawn)->attacking(),
$clone->pieceBySq($pawn)->attacking(),
function ($obj1, $obj2) {
return strcmp($obj1->sq, $obj2->sq);
}
);

foreach ($attackersDiff as $attacker) {
if ($attacker->sq === $piece->sq) {
$pawnsList[] = PiecePhrase::create($this->board->pieceBySq($pawn));
}
}
}
}

return $pawnsList;
}

private function elaborateAdvantage(String $primaryDeflectionPhrase, array $exposedPieceList, array $protectedPawnsList, int $legalMovesCount)
{
if (!$this->deflectionExists) {
return;
}

$exposedPiecePhrase = $this->elaborateExposedPieceAdvantage($exposedPieceList);
$protectedPawnPhrase = $this->elaborateProtectedPawnAdvantage($protectedPawnsList);
$elaborationPhrase = $primaryDeflectionPhrase . $exposedPiecePhrase . $protectedPawnPhrase;

// in case of only moves, also presenting more details
if ($legalMovesCount == 1) {
$elaborationPhrase = str_replace("may well", "will", $elaborationPhrase);
if ($this->mateExists) {
$elaborationPhrase .= "; threatning checkmate";
}
if ($this->checkExists) {
$elaborationPhrase .= "; threatning a check";
}
}

$elaborationPhrase .= ".";

$this->elaboration[] = $elaborationPhrase;
}

private function elaborateAdvantageText(array $itemList, string $singularMessage, string $pluralPrefix): string
{
$rephrase = "";
$count = count($itemList);
if ($count === 1) {
$item = mb_strtolower($itemList[0]);
$rephrase = $item . ' ' . $singularMessage;
} elseif ($count > 1) {
$rephrase .= $pluralPrefix;
foreach ($itemList as $item) {
$rephrase .= $item . ', ';
}
$rephrase = substr_replace(trim($rephrase), '', -1);
}

return $rephrase;
}

private function elaborateExposedPieceAdvantage(array $exposedPieceList): string
{
return $this->elaborateAdvantageText(
$exposedPieceList,
'may well be exposed to attack',
'these pieces may well be exposed to attack: ',
''
);
}

private function elaborateProtectedPawnAdvantage(array $protectedPawnsList): string
{
return $this->elaborateAdvantageText(
$protectedPawnsList,
'is not attacked by it and may well be advanced for promotion',
'these pawns are not attacked by it and may well be advanced for promotion: ',
''
);
}
}
16 changes: 16 additions & 0 deletions src/Function/CompleteFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Chess\Eval\CenterEval;
use Chess\Eval\ConnectivityEval;
use Chess\Eval\DefenseEval;
use Chess\Eval\DeflectionEval;
use Chess\Eval\DiagonalOppositionEval;
use Chess\Eval\DirectOppositionEval;
use Chess\Eval\DiscoveredCheckEval;
Expand Down Expand Up @@ -65,5 +66,20 @@ class CompleteFunction extends AbstractFunction
DirectOppositionEval::class,
AttackEval::class,
OverloadingEval::class,
DeflectionEval::class,
];

public function getEval(): array
{
return $this->eval;
}
Comment on lines +72 to +75
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to mention, as I have made updates to this file long ago, I think after latest rebase with conflict resolution, there are now some extra methods, let me know, if they are to be removed.


public function names(): array
{
foreach ($this->eval as $val) {
$names[] = (new \ReflectionClass($val))->getConstant('NAME');
}

return $names;
}
}
Loading
Loading