mirror of
https://github.com/sigmasternchen/php-chess
synced 2025-03-15 07:58:54 +00:00
feat: Minimax Depth First engine + a few basic heuristics
This commit is contained in:
parent
ff8f74fb15
commit
f881678653
7 changed files with 163 additions and 2 deletions
|
@ -2,6 +2,10 @@
|
|||
|
||||
require_once '../src/core.php';
|
||||
|
||||
use Engine\GameOutcome;
|
||||
use Engine\MinimaxDF;
|
||||
use Engine\PieceValues;
|
||||
use Engine\WeightedHeuristics;
|
||||
use Game\Game;
|
||||
use Game\Move;
|
||||
|
||||
|
@ -12,7 +16,10 @@ if (isset($_SESSION["game"])) {
|
|||
$engine = $_SESSION["engine"];
|
||||
} else {
|
||||
$game = Game::fromStartPosition();
|
||||
$engine = new \Engine\Random();
|
||||
$engine = new MinimaxDF(1, new WeightedHeuristics([
|
||||
[new GameOutcome(), 1.0],
|
||||
[new PieceValues(), 1.0]
|
||||
]));
|
||||
|
||||
$_SESSION["game"] = $game;
|
||||
$_SESSION["engine"] = $engine;
|
||||
|
|
15
src/Engine/GameOutcome.php
Normal file
15
src/Engine/GameOutcome.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Engine;
|
||||
|
||||
use Game\Game;
|
||||
use Game\GameState;
|
||||
|
||||
class GameOutcome implements Heuristic {
|
||||
public function ratePosition(Game $game): float {
|
||||
return match ($game->getGameState()) {
|
||||
GameState::CHECKMATE => -INF,
|
||||
default => 0
|
||||
};
|
||||
}
|
||||
}
|
9
src/Engine/Heuristic.php
Normal file
9
src/Engine/Heuristic.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Engine;
|
||||
|
||||
use Game\Game;
|
||||
|
||||
interface Heuristic {
|
||||
public function ratePosition(Game $game): float;
|
||||
}
|
75
src/Engine/MinimaxDF.php
Normal file
75
src/Engine/MinimaxDF.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Engine;
|
||||
|
||||
use Game\Game;
|
||||
use Game\GameState;
|
||||
use Game\Move;
|
||||
use Game\Side;
|
||||
|
||||
class MinimaxDF implements Engine {
|
||||
private int $depth;
|
||||
private Heuristic $heuristic;
|
||||
|
||||
public function __construct(int $depth, Heuristic $heuristic) {
|
||||
$this->depth = $depth;
|
||||
$this->heuristic = $heuristic;
|
||||
}
|
||||
|
||||
private function findMaxMoveRating(array $moves): array {
|
||||
$max = $moves[0];
|
||||
|
||||
foreach ($moves as $move) {
|
||||
if ($move[1] > $max[1]) {
|
||||
$max = $move;
|
||||
}
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
private function invertRating(array $move): array {
|
||||
return [$move[0], -$move[1]];
|
||||
}
|
||||
|
||||
private function maxWithHeuristic(Game $game): array {
|
||||
$moves = array_map(fn($move) => [
|
||||
$move,
|
||||
-$this->heuristic->ratePosition($game->apply($move))
|
||||
], $game->getLegalMoves());
|
||||
|
||||
return $this->findMaxMoveRating($moves);
|
||||
}
|
||||
|
||||
private function maxWithoutHeuristic(Game $game, int $remaindingDepth): array {
|
||||
$moves = array_map(function ($move) use ($game, $remaindingDepth) {
|
||||
$future = $game->apply($move);
|
||||
|
||||
if ($future->getGameState() == GameState::CHECKMATE) {
|
||||
return [$move, INF];
|
||||
}
|
||||
|
||||
$opponentMove = $this->max($future, $remaindingDepth - 1);
|
||||
|
||||
return $this->invertRating([$move, $opponentMove[1]]);
|
||||
}, $game->getLegalMoves());
|
||||
|
||||
foreach ($moves as $move) {
|
||||
error_log($remaindingDepth . ": " . $move[0]->getLong() . ": " . $move[1]);
|
||||
}
|
||||
|
||||
return $this->findMaxMoveRating($moves);
|
||||
}
|
||||
|
||||
private function max(Game $game, int $remaindingDepth): array {
|
||||
if ($remaindingDepth <= 0) {
|
||||
return $this->maxWithHeuristic($game);
|
||||
} else {
|
||||
return $this->maxWithoutHeuristic($game, $remaindingDepth);
|
||||
}
|
||||
}
|
||||
|
||||
public function nextMove(Game $game): Move {
|
||||
return $this->max($game, $this->depth)[0];
|
||||
}
|
||||
}
|
30
src/Engine/PieceValues.php
Normal file
30
src/Engine/PieceValues.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Engine;
|
||||
|
||||
use Game\Game;
|
||||
use Game\Piece;
|
||||
use Game\PieceType;
|
||||
|
||||
class PieceValues implements Heuristic {
|
||||
private function valueOfPiece(Piece $piece): float {
|
||||
return match($piece->getType()) {
|
||||
PieceType::PAWN => 1,
|
||||
PieceType::BISHOP => 3,
|
||||
PieceType::KNIGHT => 3,
|
||||
PieceType::ROOK => 5,
|
||||
PieceType::QUEEN => 9,
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
|
||||
public function ratePosition(Game $game): float {
|
||||
$ownPieces = $game->getPieces($game->getCurrentSide());
|
||||
$opponentPieces = $game->getPieces($game->getCurrentSide()->getNext());
|
||||
|
||||
$ownValue = array_sum(array_map([$this, "valueOfPiece"], $ownPieces));
|
||||
$opponentValue = array_sum(array_map([$this, "valueOfPiece"], $opponentPieces));
|
||||
|
||||
return $ownValue - $opponentValue;
|
||||
}
|
||||
}
|
25
src/Engine/WeightedHeuristics.php
Normal file
25
src/Engine/WeightedHeuristics.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Engine;
|
||||
|
||||
use Game\Game;
|
||||
|
||||
class WeightedHeuristics implements Heuristic {
|
||||
private array $heuristics;
|
||||
private array $weights;
|
||||
|
||||
public function __construct(array $heuristicsWithWeights) {
|
||||
$this->heuristics = array_map(fn($heuristicWithWeight) => $heuristicWithWeight[0], $heuristicsWithWeights);
|
||||
$this->weights = array_map(fn($heuristicWithWeight) => $heuristicWithWeight[1], $heuristicsWithWeights);
|
||||
}
|
||||
|
||||
public function ratePosition(Game $game): float {
|
||||
return array_sum(
|
||||
array_map(
|
||||
fn($heuristic, $weight) => $weight * $heuristic->ratePosition($game),
|
||||
$this->heuristics,
|
||||
$this->weights,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue