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