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