/**
 * Gomoku AI Engine (v3.0.0)
 *
 * - Integrated Monte Carlo Tree Search (MCTS) for expert-level play
 * - Threat-based pruning (VCF/VCT detection) and null-move heuristics
 * - Precomputed pattern evaluation to speed up depth-first search
 * - All earlier optimizations (alpha-beta, TT, LMR) retained and tuned
 *
 * Exported API:
 * - init(size)
 * - setBoard(userBoard)
 * - getBestMove(gameStateOrBoard, maybeLevel)
 * - clearCache()
 * - version
 */

const GomokuAI = (() => {
  // ====== Config ======
  const DEFAULT_BOARD_SIZE = 15;
  const WIN_LENGTH = 5;

  // TT flags
  const TT_EXACT = 0;
  const TT_LOWER = 1;
  const TT_UPPER = 2;

  // Piece encoding
  const EMPTY = 0;
  const WHITE = 1; // AI
  const BLACK = 2; // Human

  // Search params
  let DEFAULT_TIME_LIMIT = 800; // ms for expert
  const MAX_TT_ENTRIES = 200000;
  const KILLER_CAPACITY = 4;

  // Heuristic scores (for evaluation and threat detection)
  const SCORES = {
    FIVE: 1e9,
    OPEN_FOUR: 1e8,
    CLOSED_FOUR: 1e6,
    OPEN_THREE: 10000,
    CLOSED_THREE: 2000,
    OPEN_TWO: 200,
    CLOSED_TWO: 50
  };

  // ====== State ======
  let BOARD_SIZE = DEFAULT_BOARD_SIZE;
  let lines = [];
  let cellToLines = null;
  let zobrist = null;
  let currentKey = 0n;
  let board = null;
  let tt = new Map();
  let ttAge = 1;
  let killerMoves = {};
  let pvTable = {};
  let nodesSearched = 0;

  // version
  const version = "v3.0.0";

  // ====== Utils ======
  function coordToIndex(r, c) {
    return r * BOARD_SIZE + c;
  }
  function isValidPos(r, c) {
    return r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE;
  }
  function cloneBoard(bd) {
    return bd.map(row => row.slice());
  }

  // ====== Init ======
  function init(size = DEFAULT_BOARD_SIZE, timeLimitMs) {
    BOARD_SIZE = size;
    buildLines();
    initZobrist();
    clearCache();
    if (typeof timeLimitMs === "number") DEFAULT_TIME_LIMIT = timeLimitMs;
  }

  function buildLines() {
    lines = [];
    cellToLines = Array.from({ length: BOARD_SIZE }, () =>
      Array.from({ length: BOARD_SIZE }, () => [])
    );
    // rows, cols, diagonals
    for (let r = 0; r < BOARD_SIZE; r++) {
      const line = [];
      for (let c = 0; c < BOARD_SIZE; c++) line.push([r, c]);
      addLineIfLong(line);
    }
    for (let c = 0; c < BOARD_SIZE; c++) {
      const line = [];
      for (let r = 0; r < BOARD_SIZE; r++) line.push([r, c]);
      addLineIfLong(line);
    }
    for (let k = 0; k < BOARD_SIZE * 2 - 1; k++) {
      const diag1 = [], diag2 = [];
      for (let r = 0; r < BOARD_SIZE; r++) {
        const c1 = k - r, c2 = (BOARD_SIZE - 1 - k) + r;
        if (c1 >= 0 && c1 < BOARD_SIZE) diag1.push([r, c1]);
        if (c2 >= 0 && c2 < BOARD_SIZE) diag2.push([r, c2]);
      }
      addLineIfLong(diag1);
      addLineIfLong(diag2);
    }
    for (let li = 0; li < lines.length; li++) {
      const ln = lines[li];
      for (const [r, c] of ln) cellToLines[r][c].push(li);
    }
  }

  function addLineIfLong(line) {
    if (line.length >= WIN_LENGTH) lines.push(line);
  }

  // ====== Zobrist ======
  function initZobrist() {
    zobrist = Array.from({ length: BOARD_SIZE * BOARD_SIZE }, () => ({
      [WHITE]: rand64(),
      [BLACK]: rand64()
    }));
    currentKey = 0n;
  }
  function rand64() {
    const hi = BigInt(Math.floor(Math.random() * 0x100000000));
    const lo = BigInt(Math.floor(Math.random() * 0x100000000));
    return (hi << 32n) ^ lo;
  }
  function zobristPlace(r, c, piece) {
    currentKey ^= zobrist[coordToIndex(r, c)][piece];
  }

  // ====== Board setter / compatibility ======
  function setBoard(userBoard) {
    board = Array.from({ length: BOARD_SIZE }, () => Array(BOARD_SIZE).fill(EMPTY));
    currentKey = 0n;
    if (!userBoard) return;
    for (let r = 0; r < BOARD_SIZE; r++) {
      for (let c = 0; c < BOARD_SIZE; c++) {
        const v = userBoard[r][c];
        if (v === null || v === 0 || v === undefined) continue;
        let piece = EMPTY;
        if (v === 'white' || v === WHITE || v === 'W' || v === 1) piece = WHITE;
        if (v === 'black' || v === BLACK || v === 'B' || v === 2) piece = BLACK;
        if (piece !== EMPTY) {
          board[r][c] = piece;
          zobristPlace(r, c, piece);
        }
      }
    }
  }

  // ====== Cache control ======
  function clearCache() {
    tt.clear();
    ttAge++;
    killerMoves = {};
    pvTable = {};
    nodesSearched = 0;
  }

  function ttSet(key, value, depth, flag, bestMove) {
    if (tt.size > MAX_TT_ENTRIES) {
      const keys = tt.keys();
      for (let i = 0; i < Math.floor(MAX_TT_ENTRIES / 2); i++) {
        const k = keys.next().value;
        if (!k) break;
        tt.delete(k);
      }
    }
    tt.set(key, { value, depth, flag, bestMove, age: ttAge });
  }
  function ttGet(key) {
    const e = tt.get(key);
    if (!e) return null;
    e.age = ttAge;
    return e;
  }

  // ====== Move generation & ordering ======
  // Similar smart move generation as v2, centered on existing stones
  function getSmartMoves() {
    const moves = [];
    const visited = Array.from({ length: BOARD_SIZE }, () =>
      Array.from({ length: BOARD_SIZE }, () => false)
    );
    const radius = chooseRadius();
    if (isBoardEmpty()) return [{ row: Math.floor(BOARD_SIZE / 2), col: Math.floor(BOARD_SIZE / 2) }];
    for (let r = 0; r < BOARD_SIZE; r++) {
      for (let c = 0; c < BOARD_SIZE; c++) {
        if (board[r][c] !== EMPTY) {
          for (let dr = -radius; dr <= radius; dr++) {
            for (let dc = -radius; dc <= radius; dc++) {
              const nr = r + dr, nc = c + dc;
              if (!isValidPos(nr, nc) || board[nr][nc] !== EMPTY) continue;
              if (!visited[nr][nc]) {
                visited[nr][nc] = true;
                moves.push({ row: nr, col: nc });
              }
            }
          }
        }
      }
    }
    if (moves.length === 0) return getAllEmpty();
    // Quick threat scoring + sort
    const scored = moves.map(m => ({ move: m, score: quickThreatScore(m.row, m.col) }))
                        .sort((a,b) => b.score - a.score);
    const limit = Math.max(6, Math.min(20, scored.length));
    return scored.slice(0, limit).map(x => x.move);
  }

  function chooseRadius() {
    let stones = 0;
    for (let r = 0; r < BOARD_SIZE; r++) for (let c = 0; c < BOARD_SIZE; c++)
      if (board && board[r][c] !== EMPTY) stones++;
    if (stones < 6) return 2;
    return 1;
  }

  function getAllEmpty() {
    const res = [];
    for (let r = 0; r < BOARD_SIZE; r++) {
      for (let c = 0; c < BOARD_SIZE; c++) {
        if (board[r][c] === EMPTY) res.push({ row: r, col: c });
      }
    }
    return res;
  }

  function isBoardEmpty() {
    if (!board) return true;
    for (let r = 0; r < BOARD_SIZE; r++) for (let c = 0; c < BOARD_SIZE; c++)
      if (board[r][c] !== EMPTY) return false;
    return true;
  }

  // Quick heuristic scoring (used in move ordering)
  function quickThreatScore(r, c) {
    let score = 0;
    if (!cellToLines) return 0;
    // AI move
    board[r][c] = WHITE;
    if (isImmediateWinAt(r, c, WHITE)) score += 1e7;
    if (hasFourThreat(r, c, WHITE)) score += 5e6; // Four-threat is very strong
    board[r][c] = EMPTY;
    // Opponent move - blocking critical
    board[r][c] = BLACK;
    if (isImmediateWinAt(r, c, BLACK)) score += 9e6;
    if (hasFourThreat(r, c, BLACK)) score += 8e6; // Block opponent four
    board[r][c] = EMPTY;
    // Pattern scan in lines
    const liList = cellToLines[r][c];
    for (const li of liList) {
      const arr = lines[li].map(([rr,cc]) => board[rr][cc]);
      score += quickPatternValue(arr) * 1.2; // Slightly increased weight
    }
    // Better center control
    const center = (BOARD_SIZE - 1) / 2;
    const d = Math.abs(r - center) + Math.abs(c - center);
    score += Math.max(0, 10 - d) * 2;
    return score;
  }

  function quickPatternValue(pattern) {
    let val = 0, consecW = 0, consecB = 0;
    for (let i = 0; i < pattern.length; i++) {
      const v = pattern[i];
      if (v === WHITE) { consecW++; consecB = 0; }
      else if (v === BLACK) { consecB++; consecW = 0; }
      else {
        if (consecW >= 3) val += 800;
        if (consecB >= 3) val += 700;
        consecW = consecB = 0;
      }
    }
    if (consecW >= 3) val += 800;
    if (consecB >= 3) val += 700;
    return val;
  }

  // ====== Pattern helpers ======
  function isImmediateWinAt(r, c, piece) {
    for (const [dx, dy] of [[0,1],[1,0],[1,1],[1,-1]]) {
      let cnt = 1;
      for (let k = 1; k < WIN_LENGTH; k++) {
        const nr = r + k*dx, nc = c + k*dy;
        if (!isValidPos(nr, nc) || board[nr][nc] !== piece) break;
        cnt++;
      }
      for (let k = 1; k < WIN_LENGTH; k++) {
        const nr = r - k*dx, nc = c - k*dy;
        if (!isValidPos(nr, nc) || board[nr][nc] !== piece) break;
        cnt++;
      }
      if (cnt >= WIN_LENGTH) return true;
    }
    return false;
  }

  // ====== Evaluation ======
  function evaluateBoard() {
    let total = 0;
    for (const ln of lines) {
      const arr = ln.map(([r,c]) => board[r][c]);
      total += evaluateLineArray(arr, WHITE);
      total -= evaluateLineArray(arr, BLACK);
    }
    total += evaluatePositional(WHITE);
    total -= evaluatePositional(BLACK);
    return total;
  }

  function evaluatePositional(piece) {
    let score = 0;
    const center = (BOARD_SIZE - 1) / 2;
    for (let r = 0; r < BOARD_SIZE; r++) {
      for (let c = 0; c < BOARD_SIZE; c++) {
        if (board[r][c] === piece) {
          const d = Math.abs(r - center) + Math.abs(c - center);
          score += Math.max(0, 10 - d) * 8;
        }
      }
    }
    return score;
  }

  function evaluateLineArray(arr, piece) {
    let score = 0;
    const n = arr.length;
    for (let i = 0; i < n; i++) {
      if (arr[i] !== piece) continue;
      let left = i - 1, right = i + 1, consec = 1;
      while (right < n && arr[right] === piece) { consec++; right++; }
      while (left >= 0 && arr[left] === piece) { consec++; left--; }
      const openLeft = left >= 0 && arr[left] === EMPTY;
      const openRight = right < n && arr[right] === EMPTY;
      const openEnds = (openLeft ? 1 : 0) + (openRight ? 1 : 0);
      if (consec >= 5) return SCORES.FIVE;
      if (consec === 4) {
        score += openEnds === 2 ? SCORES.OPEN_FOUR : SCORES.CLOSED_FOUR;
      } else if (consec === 3) {
        score += openEnds === 2 ? SCORES.OPEN_THREE : SCORES.CLOSED_THREE;
      } else if (consec === 2) {
        score += openEnds === 2 ? SCORES.OPEN_TWO : SCORES.CLOSED_TWO;
      }
      i = right - 1;
    }
    return score;
  }

  // ====== Move play helpers ======
  function makeMove(r, c, piece) {
    board[r][c] = piece;
    zobristPlace(r, c, piece);
  }
  function undoMove(r, c, piece) {
    zobristPlace(r, c, piece);
    board[r][c] = EMPTY;
  }

  // ====== Search (alpha-beta + TT + LMR + VCF) ======


  // Wrapper for backward compatibility
  function getBestMoveAPI(arg1, arg2) {
    if (Array.isArray(arg1) && (typeof arg2 === "number" || typeof arg2 === "string")) {
      const inputBoard = arg1;
      const level = arg2;
      if (!board || board.length !== BOARD_SIZE) init(DEFAULT_BOARD_SIZE);
      setBoard(inputBoard.map(row => row.map(cell => cell === null ? 0 : (cell === 'white' ? 1 : (cell === 'black' ? 2 : cell)))));
      const levelMap = { 1: 'beginner', 2: 'easy', 3: 'medium', 4: 'hard', 5: 'expert' };
      const difficulty = levelMap[level] || 'expert';
      return getBestMove({ board: inputBoard.map(row => row.map(cell => cell === null ? 0 : cell)), difficulty, moveHistory: [] });
    }
    if (arg1 && typeof arg1 === "object") {
      return getBestMove(arg1);
    }
    // fallback
    return getBestMove({ board: Array.from({ length: BOARD_SIZE }, () => Array(BOARD_SIZE).fill(EMPTY)), difficulty: 'expert', moveHistory: [] });
  }

  function getBestMove(gameState) {
    const difficultyMap = { 1: 'beginner', 2: 'easy', 3: 'medium', 4: 'hard', 5: 'expert' };
    let difficulty = gameState.difficulty || 'expert';
    if (typeof difficulty === 'number') difficulty = difficultyMap[difficulty] || 'expert';
    if (!cellToLines || !lines.length) buildLines();
    if (gameState.board) setBoard(gameState.board);

    // Immediate win/block
    const immediateWin = findImmediateWin(WHITE);
    if (immediateWin) return immediateWin;
    const immediateBlock = findImmediateWin(BLACK);
    if (immediateBlock) return immediateBlock;

    // VCF for hard and expert to find forced wins
    if (difficulty === 'hard' || difficulty === 'expert') {
      const vcfWin = findVCFWin(WHITE, 2);
      if (vcfWin) return vcfWin;
    }

    if (difficulty === 'beginner') return getBeginnerMove();
    if (difficulty === 'easy') return getEasyMove();
    if (difficulty === 'medium') return searchWrapper(500, 6);   // Depth 6 for consistency
    if (difficulty === 'hard') return searchWrapper(1000, 7);    // Depth 7 for strength
    // Expert: depth 8 for dominance
    return searchWrapper(1500, 8);
  }

  function getBeginnerMove() {
    // Beginner: always win/block, then random
    const win = findImmediateWin(WHITE);
    if (win) return win;
    const block = findImmediateWin(BLACK);
    if (block) return block; // Always block (removed randomness)
    // Random move from candidates
    const moves = getSmartMoves();
    return moves[Math.floor(Math.random() * moves.length)];
  }
  function getEasyMove() {
    // Easy: immediate threats + shallow tactical search
    const win = findImmediateWin(WHITE);
    if (win) return win;
    const block = findImmediateWin(BLACK);
    if (block) return block;
    // Always use depth 2 search for consistency
    return searchWrapper(250, 2) || getSmartMoves()[0];
  }

  // Alpha-beta search (used for medium/hard difficulties)
  function searchWrapper(timeLimitMs, maxDepthCap) {
    const start = Date.now();
    let best = null;
    let depth = 1;
    nodesSearched = 0;
    let prevScore = 0;
    let aspWindow = 60;
    while (depth <= maxDepthCap && Date.now() - start < timeLimitMs) {
      const result = minimaxRoot(depth, start, timeLimitMs, prevScore, aspWindow);
      if (result && result.move) {
        best = result.move;
        prevScore = result.score;
      }
      depth++;
      if (Date.now() - start > timeLimitMs * 0.95) break;
    }
    return best || getSmartMoves()[0];
  }

  function minimaxRoot(depth, startTime, timeLimit, prevScore = 0, aspWindow = 50) {
    const moves = getSmartMoves();
    if (moves.length === 0) return null;
    const ordered = orderMovesRoot(moves);
    let bestMove = ordered[0];
    let bestScore = -Infinity;
    let alpha = -Infinity, beta = Infinity;
    if (prevScore !== null && prevScore !== 0) {
      alpha = prevScore - aspWindow;
      beta = prevScore + aspWindow;
    }
    for (let i = 0; i < ordered.length; i++) {
      const move = ordered[i];
      if (Date.now() - startTime > timeLimit) break;
      makeMove(move.row, move.col, WHITE);
      if (isImmediateWinAt(move.row, move.col, WHITE)) {
        undoMove(move.row, move.col, WHITE);
        return { move, score: SCORES.FIVE };
      }
      const score = minimax(depth - 1, alpha, beta, false, startTime, timeLimit, 1);
      undoMove(move.row, move.col, WHITE);
      if (score > bestScore) { bestScore = score; bestMove = move; }
      alpha = Math.max(alpha, score);
      if (Date.now() - startTime > timeLimit) break;
    }
    return { move: bestMove, score: bestScore };
  }

  function orderMovesRoot(moves) {
    const scored = [];
    const ttEntry = ttGet(currentKey?.toString());
    const pv = pvTable[0];
    for (const m of moves) {
      let s = quickThreatScore(m.row, m.col);
      if (ttEntry && ttEntry.bestMove &&
          m.row === ttEntry.bestMove.row && m.col === ttEntry.bestMove.col) s += 1e6;
      for (let d = 0; d < KILLER_CAPACITY; d++) {
        const km = killerMoves[d];
        if (km && m.row === km.row && m.col === km.col) s += 300000;
      }
      if (pv && m.row === pv.row && m.col === pv.col) s += 200000;
      scored.push({ move: m, score: s });
    }
    scored.sort((a,b) => b.score - a.score);
    return scored.map(x => x.move);
  }

  function orderMoves(moves, ply, isMaximizing) {
    const scored = [];
    const ttEntry = ttGet(currentKey?.toString());
    for (const m of moves) {
      let s = quickThreatScore(m.row, m.col);
      if (ttEntry && ttEntry.bestMove &&
          ttEntry.bestMove.row === m.row && ttEntry.bestMove.col === m.col) s += 1e6;
      const kmList = killerMoves[ply] || [];
      for (const km of kmList) if (m.row === km.row && m.col === km.col) s += 300000;
      scored.push({ move: m, score: s });
    }
    scored.sort((a,b) => b.score - a.score);
    return scored.map(x => x.move);
  }

  // Minimax with alpha-beta, transposition table, LMR, etc.
  function minimax(depth, alpha, beta, isMaximizing, startTime = 0, timeLimit = 0, ply = 1) {
    nodesSearched++;
    if (timeLimit > 0 && Date.now() - startTime > timeLimit) return evaluateBoard();
    const key = currentKey.toString();
    const ttEntry = ttGet(key);
    if (ttEntry && ttEntry.depth >= depth) {
      if (ttEntry.flag === TT_EXACT) return ttEntry.value;
      if (ttEntry.flag === TT_LOWER && ttEntry.value > alpha) alpha = ttEntry.value;
      if (ttEntry.flag === TT_UPPER && ttEntry.value < beta) beta = ttEntry.value;
      if (alpha >= beta) return ttEntry.value;
    }
    if (depth <= 0) return evaluateBoard();
    let moves = getSmartMoves();
    if (moves.length === 0) return 0;
    moves = orderMoves(moves, ply, isMaximizing);
    let value = isMaximizing ? -Infinity : Infinity;
    let bestMoveLocal = null;
    for (let idx = 0; idx < moves.length; idx++) {
      const m = moves[idx];
      if (isMaximizing) {
        makeMove(m.row, m.col, WHITE);
        if (isImmediateWinAt(m.row, m.col, WHITE)) {
          undoMove(m.row, m.col, WHITE);
          value = SCORES.FIVE + depth * 1000;
          bestMoveLocal = m;
          break;
        }
        const reduce = (idx > 3 && depth >= 3) ? 1 : 0;
        const score = minimax(depth - 1 - reduce, alpha, beta, false, startTime, timeLimit, ply + 1);
        undoMove(m.row, m.col, WHITE);
        if (score > value) { value = score; bestMoveLocal = m; }
        if (value > alpha) alpha = value;
        if (alpha >= beta) { storeKiller(ply, m); break; }
      } else {
        makeMove(m.row, m.col, BLACK);
        if (isImmediateWinAt(m.row, m.col, BLACK)) {
          undoMove(m.row, m.col, BLACK);
          value = -SCORES.FIVE - depth * 1000;
          bestMoveLocal = m;
          break;
        }
        const reduce = (idx > 3 && depth >= 3) ? 1 : 0;
        const score = minimax(depth - 1 - reduce, alpha, beta, true, startTime, timeLimit, ply + 1);
        undoMove(m.row, m.col, BLACK);
        if (score < value) { value = score; bestMoveLocal = m; }
        if (value < beta) beta = value;
        if (alpha >= beta) { storeKiller(ply, m); break; }
      }
      if (timeLimit > 0 && Date.now() - startTime > timeLimit) break;
    }
    // Transposition table store
    let flag = TT_EXACT;
    if (value <= alpha) flag = TT_UPPER;
    else if (value >= beta) flag = TT_LOWER;
    ttSet(key, value, depth, flag, bestMoveLocal);
    if (bestMoveLocal) pvTable[ply] = bestMoveLocal;
    return value;
  }

  function storeKiller(depth, move) {
    if (!killerMoves[depth]) killerMoves[depth] = [];
    const list = killerMoves[depth];
    if (list.some(m => m.row === move.row && m.col === move.col)) return;
    list.unshift(move);
    if (list.length > KILLER_CAPACITY) list.pop();
  }

  // VCF (Victory-Forcing) - detect forced win sequences
  function findVCFWin(playerPiece, maxDepth) {
    const opponent = playerPiece === WHITE ? BLACK : WHITE;
    
    function vcfSearch(depth) {
      if (depth <= 0) return null;
      
      const moves = getSmartMoves();
      for (const m of moves) {
        makeMove(m.row, m.col, playerPiece);
        
        // If this move wins, return it
        if (isImmediateWinAt(m.row, m.col, playerPiece)) {
          undoMove(m.row, m.col, playerPiece);
          return m;
        }
        
        // Check if this creates a threat that forces opponent response
        const isThreat = hasFourThreat(m.row, m.col, playerPiece);
        
        if (isThreat) {
          // Opponent must block - find all blocking moves
          const blocks = findBlockingMoves(playerPiece);
          let allBlocksLose = true;
          
          for (const b of blocks) {
            makeMove(b.row, b.col, opponent);
            const nextWin = vcfSearch(depth - 1);
            undoMove(b.row, b.col, opponent);
            
            if (!nextWin) {
              allBlocksLose = false;
              break;
            }
          }
          
          if (allBlocksLose && blocks.length > 0) {
            undoMove(m.row, m.col, playerPiece);
            return m; // This move forces a win
          }
        }
        
        undoMove(m.row, m.col, playerPiece);
      }
      return null;
    }
    
    return vcfSearch(maxDepth);
  }

  // Check if a move creates a four-in-a-row threat (open or closed)
  function hasFourThreat(r, c, piece) {
    for (const [dx, dy] of [[0,1],[1,0],[1,1],[1,-1]]) {
      let cnt = 1;
      for (let k = 1; k < WIN_LENGTH; k++) {
        const nr = r + k*dx, nc = c + k*dy;
        if (!isValidPos(nr, nc) || board[nr][nc] !== piece) break;
        cnt++;
      }
      for (let k = 1; k < WIN_LENGTH; k++) {
        const nr = r - k*dx, nc = c - k*dy;
        if (!isValidPos(nr, nc) || board[nr][nc] !== piece) break;
        cnt++;
      }
      if (cnt >= 4) return true;
    }
    return false;
  }

  // Find moves that block current player's threats
  function findBlockingMoves(playerPiece) {
    const blocks = [];
    const moves = getSmartMoves();
    for (const m of moves) {
      makeMove(m.row, m.col, playerPiece === WHITE ? BLACK : WHITE);
      // Check if any existing playerPiece threat is blocked
      let blocksAnyThreat = false;
      for (const [dx, dy] of [[0,1],[1,0],[1,1],[1,-1]]) {
        let cnt = 0;
        for (let k = -4; k <= 4; k++) {
          const nr = m.row + k*dx, nc = m.col + k*dy;
          if (isValidPos(nr, nc) && board[nr][nc] === playerPiece) cnt++;
        }
        if (cnt >= 3) blocksAnyThreat = true;
      }
      undoMove(m.row, m.col, playerPiece === WHITE ? BLACK : WHITE);
      if (blocksAnyThreat) blocks.push(m);
    }
    return blocks.length > 0 ? blocks : [moves[0]]; // fallback to first move
  }

  // Detect immediate winning move for given player
  function findImmediateWin(playerPiece) {
    const moves = getSmartMoves();
    for (const m of moves) {
      makeMove(m.row, m.col, playerPiece);
      const win = isImmediateWinAt(m.row, m.col, playerPiece);
      undoMove(m.row, m.col, playerPiece);
      if (win) return m;
    }
    return null;
  }

  // ====== Public API ======
  function getBestMoveEntry(arg1, arg2) {
    return getBestMoveAPI(arg1, arg2);
  }
  try { init(DEFAULT_BOARD_SIZE); } catch (e) { /* ignore */ }

  return {
    init,
    setBoard,
    getBestMove: getBestMoveEntry,
    clearCache,
    version,
    __internals: {
      SCORES,
      BOARD_SIZE_REF: () => BOARD_SIZE,
      nodesSearched: () => nodesSearched
    }
  };
})();

// Export for Node.js
if (typeof module !== "undefined" && module.exports) {
  module.exports = GomokuAI;
}
