game.php

21.27 KB
27/05/2025 10:24
PHP
<?php
// api/game.php
class GoGame
{
    /**
     * @var mixed
     */
    private $board;
    /**
     * @var mixed
     */
    private $size;
    /**
     * @var mixed
     */
    private $currentPlayer;
    /**
     * @var mixed
     */
    private $blackCaptures;
    /**
     * @var mixed
     */
    private $whiteCaptures;

    /**
     * @param $size
     */
    public function __construct($size = 19)
    {
        $this->size = $size;
        $this->board = array_fill(0, $size, array_fill(0, $size, 0));
        $this->currentPlayer = 1; // 1 = ดำ, -1 = ขาว
        $this->blackCaptures = 0;
        $this->whiteCaptures = 0;
    }

    /**
     * @param $x
     * @param $y
     * @param $player
     */
    public function makeMove($x, $y, $player)
    {
        if (!$this->isValidMove($x, $y)) {
            return false;
        }

        $this->board[$x][$y] = $player;
        $this->checkCaptures($x, $y, $player);
        $this->currentPlayer = -$player;

        return true;
    }

    /**
     * @param $x
     * @param $y
     * @return mixed
     */
    private function isValidMove($x, $y)
    {
        return $x >= 0 && $x < $this->size &&
        $y >= 0 && $y < $this->size &&
        $this->board[$x][$y] === 0;
    }

    /**
     * @param $x
     * @param $y
     * @param $player
     */
    private function checkCaptures($x, $y, $player)
    {
        $opponent = -$player;
        $directions = [[0, 1], [1, 0], [0, -1], [-1, 0]];

        foreach ($directions as $dir) {
            $nx = $x + $dir[0];
            $ny = $y + $dir[1];

            if ($this->isInBounds($nx, $ny) &&
                $this->board[$nx][$ny] === $opponent) {

                $group = $this->getGroup($nx, $ny);
                if ($this->hasNoLiberties($group)) {
                    $this->captureGroup($group, $player);
                }
            }
        }
    }

    /**
     * @param $x
     * @param $y
     * @return mixed
     */
    private function getGroup($x, $y)
    {
        $player = $this->board[$x][$y];
        $group = [];
        $visited = [];
        $this->dfsGroup($x, $y, $player, $group, $visited);
        return $group;
    }

    /**
     * @param $x
     * @param $y
     * @param $player
     * @param $group
     * @param $visited
     * @return null
     */
    private function dfsGroup($x, $y, $player, &$group, &$visited)
    {
        $key = "$x,$y";
        if (isset($visited[$key]) || !$this->isInBounds($x, $y) ||
            $this->board[$x][$y] !== $player) {
            return;
        }

        $visited[$key] = true;
        $group[] = [$x, $y];

        $directions = [[0, 1], [1, 0], [0, -1], [-1, 0]];
        foreach ($directions as $dir) {
            $this->dfsGroup($x + $dir[0], $y + $dir[1], $player, $group, $visited);
        }
    }

    /**
     * @param $group
     */
    private function hasNoLiberties($group)
    {
        foreach ($group as $stone) {
            $directions = [[0, 1], [1, 0], [0, -1], [-1, 0]];
            foreach ($directions as $dir) {
                $nx = $stone[0] + $dir[0];
                $ny = $stone[1] + $dir[1];

                if ($this->isInBounds($nx, $ny) && $this->board[$nx][$ny] === 0) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * @param $group
     * @param $capturer
     */
    private function captureGroup($group, $capturer)
    {
        foreach ($group as $stone) {
            $this->board[$stone[0]][$stone[1]] = 0;
        }

        if ($capturer === 1) {
            $this->blackCaptures += count($group);
        } else {
            $this->whiteCaptures += count($group);
        }
    }

    /**
     * @param $x
     * @param $y
     * @return mixed
     */
    private function isInBounds($x, $y)
    {
        return $x >= 0 && $x < $this->size && $y >= 0 && $y < $this->size;
    }

    public function getGameState()
    {
        return [
            'board' => $this->board,
            'currentPlayer' => $this->currentPlayer,
            'blackCaptures' => $this->blackCaptures,
            'whiteCaptures' => $this->whiteCaptures
        ];
    }

    /**
     * @param $gameState
     */
    public function setGameState($gameState)
    {
        $this->board = $gameState['board'];
        $this->currentPlayer = $gameState['currentPlayer'];
        $this->blackCaptures = $gameState['blackCaptures'];
        $this->whiteCaptures = $gameState['whiteCaptures'];
        $this->size = count($this->board);
    }

    /**
     * @return mixed
     */
    public function isGameOver()
    {
        // ตรวจสอบว่าเกมจบแล้วหรือไม่
        // สำหรับความง่าย เราจะใช้เงื่อนไขง่ายๆ
        $emptyCells = 0;
        for ($x = 0; $x < $this->size; $x++) {
            for ($y = 0; $y < $this->size; $y++) {
                if ($this->board[$x][$y] === 0) {
                    $emptyCells++;
                }
            }
        }

        // เกมจบเมื่อมีช่องว่างน้อยกว่า 10% ของกระดาน
        return $emptyCells < ($this->size * $this->size * 0.1);
    }

    public function getScore()
    {
        // คำนวณคะแนนแบบง่าย (นับจำนวนหินและดินแดน)
        $blackScore = $this->blackCaptures;
        $whiteScore = $this->whiteCaptures;

        // นับหินบนกระดาน
        for ($x = 0; $x < $this->size; $x++) {
            for ($y = 0; $y < $this->size; $y++) {
                if ($this->board[$x][$y] === 1) {
                    $blackScore++;
                } elseif ($this->board[$x][$y] === -1) {
                    $whiteScore++;
                }
            }
        }

        return ['black' => $blackScore, 'white' => $whiteScore];
    }

    // ตรวจสอบว่าการเดินนั้นเป็น suicide move หรือไม่
    /**
     * @param $x
     * @param $y
     * @param $player
     */
    private function isSuicideMove($x, $y, $player)
    {
        // สำหรับความง่าย เราจะข้าม suicide rule
        return false;
    }

    // ตรวจสอบ Ko rule (ห้ามกลับไปสู่สถานะเดิม)
    /**
     * @param $x
     * @param $y
     * @param $player
     */
    private function violatesKoRule($x, $y, $player)
    {
        // สำหรับความง่าย เราจะข้าม Ko rule
        return false;
    }
}

// AI Engine ด้วย MCTS
class MCTSAIEngine
{
    /**
     * @var mixed
     */
    private $game;
    /**
     * @var mixed
     */
    private $simulations;
    /**
     * @var mixed
     */
    private $explorationConstant;

    /**
     * @param $game
     * @param $simulations
     */
    public function __construct($game, $simulations = 1000)
    {
        $this->game = $game;
        $this->simulations = $simulations;
        $this->explorationConstant = sqrt(2);
    }

    /**
     * @return mixed
     */
    public function getBestMove()
    {
        $rootNode = new MCTSNode($this->game->getGameState());

        for ($i = 0; $i < $this->simulations; $i++) {
            $node = $this->select($rootNode);
            $expandedNode = $this->expand($node);
            $result = $this->simulate($expandedNode);
            $this->backpropagate($expandedNode, $result);
        }

        return $this->getBestChild($rootNode)->getMove();
    }

    /**
     * @param $node
     * @return mixed
     */
    private function select($node)
    {
        while (!$node->isLeaf() && $node->isFullyExpanded()) {
            $node = $this->getBestUCBChild($node);
        }
        return $node;
    }

    /**
     * @param $node
     * @return mixed
     */
    private function expand($node)
    {
        if ($node->isTerminal()) {
            return $node;
        }

        $possibleMoves = $this->getPossibleMoves($node->getGameState());
        foreach ($possibleMoves as $move) {
            if (!$node->hasChild($move)) {
                return $node->addChild($move, $this->makeMove($node->getGameState(), $move));
            }
        }

        return $node;
    }

    /**
     * @param $node
     * @return mixed
     */
    private function simulate($node)
    {
        $gameState = $node->getGameState();
        $currentPlayer = $gameState['currentPlayer'];

        // เล่นแบบสุ่มจนจบเกม
        $moves = 0;
        while ($moves < 100) {
            // จำกัดจำนวน move เพื่อไม่ให้เกมยาวเกินไป
            $possibleMoves = $this->getPossibleMoves($gameState);
            if (empty($possibleMoves)) {
                break;
            }

            $randomMove = $possibleMoves[array_rand($possibleMoves)];
            $gameState = $this->makeMove($gameState, $randomMove);
            $currentPlayer = -$currentPlayer;
            $moves++;
        }

        return $this->evaluateGameState($gameState);
    }

    /**
     * @param $node
     * @param $result
     */
    private function backpropagate($node, $result)
    {
        while ($node !== null) {
            $node->updateStats($result);
            $node = $node->getParent();
        }
    }

    /**
     * @param $node
     * @return mixed
     */
    private function getBestUCBChild($node)
    {
        $bestScore = -INF;
        $bestChild = null;

        foreach ($node->getChildren() as $child) {
            $ucbScore = $this->calculateUCB($child, $node->getVisits());
            if ($ucbScore > $bestScore) {
                $bestScore = $ucbScore;
                $bestChild = $child;
            }
        }

        return $bestChild;
    }

    /**
     * @param $node
     * @param $parentVisits
     * @return mixed
     */
    private function calculateUCB($node, $parentVisits)
    {
        if ($node->getVisits() === 0) {
            return INF;
        }

        $exploitation = $node->getWins() / $node->getVisits();
        $exploration = $this->explorationConstant *
        sqrt(log($parentVisits) / $node->getVisits());

        return $exploitation + $exploration;
    }

    /**
     * @param $node
     * @return mixed
     */
    private function getBestChild($node)
    {
        $bestWinRate = -1;
        $bestChild = null;

        foreach ($node->getChildren() as $child) {
            $winRate = $child->getVisits() > 0 ?
            $child->getWins() / $child->getVisits() : 0;

            if ($winRate > $bestWinRate) {
                $bestWinRate = $winRate;
                $bestChild = $child;
            }
        }

        return $bestChild;
    }

    /**
     * @param $gameState
     * @param $move
     * @return mixed
     */
    private function makeMove($gameState, $move)
    {
        $tempGame = new GoGame(count($gameState['board']));
        $tempGame->setGameState($gameState);
        $tempGame->makeMove($move['x'], $move['y'], $gameState['currentPlayer']);
        return $tempGame->getGameState();
    }

    /**
     * @param $gameState
     * @return int
     */
    private function evaluateGameState($gameState)
    {
        $game = new GoGame(count($gameState['board']));
        $game->setGameState($gameState);

        if ($game->isGameOver()) {
            $scores = $game->getScore();
            if ($scores['black'] > $scores['white']) {
                return -1; // ดำชนะ (ไม่ดีสำหรับ AI ที่เป็นขาว)
            } elseif ($scores['white'] > $scores['black']) {
                return 1; // ขาวชนะ (ดีสำหรับ AI)
            } else {
                return 0; // เสมอ
            }
        }

        // ประเมินจากจำนวนหินและดินแดนที่ควบคุม
        $blackStones = 0;
        $whiteStones = 0;
        $blackInfluence = 0;
        $whiteInfluence = 0;

        for ($x = 0; $x < count($gameState['board']); $x++) {
            for ($y = 0; $y < count($gameState['board']); $y++) {
                $cell = $gameState['board'][$x][$y];
                if ($cell === 1) {
                    $blackStones++;
                    $blackInfluence += $this->calculateInfluence($gameState['board'], $x, $y, 1);
                } elseif ($cell === -1) {
                    $whiteStones++;
                    $whiteInfluence += $this->calculateInfluence($gameState['board'], $x, $y, -1);
                }
            }
        }

        $blackTotal = $blackStones + $gameState['blackCaptures'] + $blackInfluence * 0.1;
        $whiteTotal = $whiteStones + $gameState['whiteCaptures'] + $whiteInfluence * 0.1;

        // ส่งคืนค่าระหว่าง -1 ถึง 1
        $diff = $whiteTotal - $blackTotal;
        return tanh($diff / 10); // ใช้ tanh เพื่อให้ค่าอยู่ในช่วง -1 ถึง 1
    }

    /**
     * @param $board
     * @param $x
     * @param $y
     * @param $player
     * @return mixed
     */
    private function calculateInfluence($board, $x, $y, $player)
    {
        $influence = 0;
        $directions = [[0, 1], [1, 0], [0, -1], [-1, 0], [1, 1], [-1, -1], [1, -1], [-1, 1]];

        foreach ($directions as $dir) {
            for ($dist = 1; $dist <= 3; $dist++) {
                $nx = $x + $dir[0] * $dist;
                $ny = $y + $dir[1] * $dist;

                if ($nx >= 0 && $nx < count($board) &&
                    $ny >= 0 && $ny < count($board[0])) {

                    if ($board[$nx][$ny] === 0) {
                        $influence += (4 - $dist); // ยิ่งใกล้ยิ่งมีอิทธิพลมาก
                    } elseif ($board[$nx][$ny] === $player) {
                        $influence += (4 - $dist) * 0.5; // หินเดียวกันให้อิทธิพลน้อยลง
                    } else {
                        break; // หินฝ่ายตรงข้ามขวางทาง
                    }
                }
            }
        }

        return $influence;
    }

    // ปรับปรุงการเลือก move ให้ชาญฉลาดขึ้น
    /**
     * @param $gameState
     * @return mixed
     */
    private function getPossibleMoves($gameState)
    {
        $moves = [];
        $board = $gameState['board'];
        $size = count($board);

        // เพิ่ม move ทั้งหมดที่เป็นไปได้
        for ($x = 0; $x < $size; $x++) {
            for ($y = 0; $y < $size; $y++) {
                if ($board[$x][$y] === 0) {
                    $move = ['x' => $x, 'y' => $y];
                    $move['priority'] = $this->evaluateMoveQuickly($gameState, $x, $y);
                    $moves[] = $move;
                }
            }
        }

        // เรียงลำดับ move ตามความสำคัญ
        usort($moves, function ($a, $b) {
            return $b['priority'] - $a['priority'];
        });

        // เลือกเฉพาะ move ที่ดีที่สุด 20-30 ตัว เพื่อลดเวลาคำนวณ
        return array_slice($moves, 0, min(25, count($moves)));
    }

    /**
     * @param $gameState
     * @param $x
     * @param $y
     * @return mixed
     */
    private function evaluateMoveQuickly($gameState, $x, $y)
    {
        $priority = 0;
        $board = $gameState['board'];
        $size = count($board);
        $player = $gameState['currentPlayer'];

        // ตรวจสอบรอบๆ
        $directions = [[0, 1], [1, 0], [0, -1], [-1, 0]];
        $friendlyNeighbors = 0;
        $enemyNeighbors = 0;
        $emptyNeighbors = 0;

        foreach ($directions as $dir) {
            $nx = $x + $dir[0];
            $ny = $y + $dir[1];

            if ($nx >= 0 && $nx < $size && $ny >= 0 && $ny < $size) {
                $cell = $board[$nx][$ny];
                if ($cell === $player) {
                    $friendlyNeighbors++;
                } elseif ($cell === -$player) {
                    $enemyNeighbors++;
                } else {
                    $emptyNeighbors++;
                }
            }
        }

        // ให้คะแนนสูงกับ move ที่:
        // 1. อยู่ใกล้หินเพื่อน (เพื่อสร้างกลุ่ม)
        $priority += $friendlyNeighbors * 3;

        // 2. อยู่ใกล้หินศัตรู (เพื่อโจมตี)
        $priority += $enemyNeighbors * 2;

        // 3. อยู่ใกล้กึ่งกลางกระดาน
        $centerX = $size / 2;
        $centerY = $size / 2;
        $distanceFromCenter = abs($x - $centerX) + abs($y - $centerY);
        $priority += (20 - $distanceFromCenter);

        // 4. หลีกเลี่ยงขอบกระดาน (ยกเว้นเป็นการป้องกัน)
        if ($x === 0 || $x === $size - 1 || $y === 0 || $y === $size - 1) {
            $priority -= 5;
            if ($enemyNeighbors > 0) {
                $priority += 3; // ยกเว้นถ้าเป็นการป้องกัน
            }
        }

        return $priority;
    }
}

class MCTSNode
{
    /**
     * @var mixed
     */
    private $gameState;
    /**
     * @var mixed
     */
    private $move;
    /**
     * @var mixed
     */
    private $parent;
    /**
     * @var mixed
     */
    private $children;
    /**
     * @var mixed
     */
    private $visits;
    /**
     * @var mixed
     */
    private $wins;

    /**
     * @param $gameState
     * @param $move
     * @param null $parent
     */
    public function __construct($gameState, $move = null, $parent = null)
    {
        $this->gameState = $gameState;
        $this->move = $move;
        $this->parent = $parent;
        $this->children = [];
        $this->visits = 0;
        $this->wins = 0;
    }

    /**
     * @param $move
     * @param $gameState
     * @return mixed
     */
    public function addChild($move, $gameState)
    {
        $child = new MCTSNode($gameState, $move, $this);
        $this->children[] = $child;
        return $child;
    }

    /**
     * @param $result
     */
    public function updateStats($result)
    {
        $this->visits++;
        $this->wins += $result;
    }

    public function isLeaf()
    {
        return empty($this->children);
    }

    public function isTerminal()
    {
        // ตรวจสอบว่าเกมจบแล้วหรือไม่
        return false; // ในเกมจริงจะต้องตรวจสอบเงื่อนไขการจบเกม
    }

    public function isFullyExpanded()
    {
        // ตรวจสอบว่าได้สร้าง child node ครบทุก possible move แล้วหรือไม่
        return true; // สำหรับตัวอย่างนี้
    }

    /**
     * @param $move
     */
    public function hasChild($move)
    {
        foreach ($this->children as $child) {
            if ($child->getMove()['x'] === $move['x'] &&
                $child->getMove()['y'] === $move['y']) {
                return true;
            }
        }
        return false;
    }

    // Getters
    /**
     * @return mixed
     */
    public function getGameState()
    {return $this->gameState;}
    /**
     * @return mixed
     */
    public function getMove()
    {return $this->move;}
    /**
     * @return mixed
     */
    public function getParent()
    {return $this->parent;}
    /**
     * @return mixed
     */
    public function getChildren()
    {return $this->children;}
    /**
     * @return mixed
     */
    public function getVisits()
    {return $this->visits;}
    /**
     * @return mixed
     */
    public function getWins()
    {return $this->wins;}
}

// API Endpoints
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $input = json_decode(file_get_contents('php://input'), true);

    switch ($input['action']) {
        case 'new_game':
            $game = new GoGame($input['size'] ?? 19);
            echo json_encode($game->getGameState());
            break;

        case 'make_move':
            $game = new GoGame($input['size'] ?? 19);
            $game->setGameState($input['gameState']);

            if ($game->makeMove($input['x'], $input['y'], $input['player'])) {
                echo json_encode(['success' => true, 'gameState' => $game->getGameState()]);
            } else {
                echo json_encode(['success' => false, 'message' => 'Invalid move']);
            }
            break;

        case 'ai_move':
            $game = new GoGame($input['size'] ?? 19);
            $game->setGameState($input['gameState']);

            $ai = new MCTSAIEngine($game, $input['difficulty'] ?? 1000);
            $bestMove = $ai->getBestMove();

            if ($game->makeMove($bestMove['x'], $bestMove['y'], $input['gameState']['currentPlayer'])) {
                echo json_encode([
                    'success' => true,
                    'move' => $bestMove,
                    'gameState' => $game->getGameState()
                ]);
            } else {
                echo json_encode(['success' => false, 'message' => 'AI cannot make move']);
            }
            break;
    }
}