From f8816786535fd12350e7af62550b09b78765e9a7 Mon Sep 17 00:00:00 2001 From: sigmasternchen Date: Thu, 31 Oct 2024 18:43:42 +0100 Subject: [PATCH] feat: Minimax Depth First engine + a few basic heuristics --- html/index.php | 9 +++- src/Engine/GameOutcome.php | 15 +++++++ src/Engine/Heuristic.php | 9 ++++ src/Engine/MinimaxDF.php | 75 +++++++++++++++++++++++++++++++ src/Engine/PieceValues.php | 30 +++++++++++++ src/Engine/Random.php | 2 +- src/Engine/WeightedHeuristics.php | 25 +++++++++++ 7 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 src/Engine/GameOutcome.php create mode 100644 src/Engine/Heuristic.php create mode 100644 src/Engine/MinimaxDF.php create mode 100644 src/Engine/PieceValues.php create mode 100644 src/Engine/WeightedHeuristics.php diff --git a/html/index.php b/html/index.php index 2c0c09d..3ab3363 100644 --- a/html/index.php +++ b/html/index.php @@ -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; diff --git a/src/Engine/GameOutcome.php b/src/Engine/GameOutcome.php new file mode 100644 index 0000000..fc9b651 --- /dev/null +++ b/src/Engine/GameOutcome.php @@ -0,0 +1,15 @@ +getGameState()) { + GameState::CHECKMATE => -INF, + default => 0 + }; + } +} \ No newline at end of file diff --git a/src/Engine/Heuristic.php b/src/Engine/Heuristic.php new file mode 100644 index 0000000..5d7e06c --- /dev/null +++ b/src/Engine/Heuristic.php @@ -0,0 +1,9 @@ +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]; + } +} \ No newline at end of file diff --git a/src/Engine/PieceValues.php b/src/Engine/PieceValues.php new file mode 100644 index 0000000..2a2bba4 --- /dev/null +++ b/src/Engine/PieceValues.php @@ -0,0 +1,30 @@ +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; + } +} \ No newline at end of file diff --git a/src/Engine/Random.php b/src/Engine/Random.php index ac83d73..56717aa 100644 --- a/src/Engine/Random.php +++ b/src/Engine/Random.php @@ -10,4 +10,4 @@ class Random implements Engine { $legalMoves = $game->getLegalMoves(); return $legalMoves[array_rand($legalMoves, 1)]; } -} \ No newline at end of file +} diff --git a/src/Engine/WeightedHeuristics.php b/src/Engine/WeightedHeuristics.php new file mode 100644 index 0000000..81f6815 --- /dev/null +++ b/src/Engine/WeightedHeuristics.php @@ -0,0 +1,25 @@ +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, + ) + ); + } +} \ No newline at end of file