feat: Minimax Depth First engine + a few basic heuristics

This commit is contained in:
sigmasternchen 2024-10-31 18:43:42 +01:00
parent ff8f74fb15
commit f881678653
7 changed files with 163 additions and 2 deletions

View file

@ -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;

View 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
View 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
View 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];
}
}

View 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;
}
}

View 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,
)
);
}
}