게시판오목 Next.js 웹과 VLM 착수 API 연결하기예제 소스

예제 소스

오목 Next.js 웹과 VLM 착수 API 연결하기 · 파일 59개

ai.ts

import {
  BLACK,
  EMPTY,
  WHITE,
  canPlace,
  idx,
  inBounds,
  opponent,
  type Point,
  type Stone,
  wouldWinIfPlaced,
} from "./board";

const DIRS: Array<[number, number]> = [
  [0, 1],
  [1, 0],
  [1, 1],
  [1, -1],
];

function linePotential(
  board: Stone[],
  r: number,
  c: number,
  dr: number,
  dc: number,
  color: Stone,
  size: number,
): number {
  let streak = 0;
  let blockedNear = false;
  const rr0 = r - dr;
  const cc0 = c - dc;
  if (inBounds(rr0, cc0, size)) {
    const oc = board[idx(rr0, cc0, size)];
    if (oc !== EMPTY && oc !== color) blockedNear = true;
  }

  let rr = r;
  let cc = c;
  for (let k = 0; k < 5 && inBounds(rr, cc, size); k++, rr += dr, cc += dc) {
    const v = board[idx(rr, cc, size)];
    if (v === color) streak++;
    else if (v === EMPTY) {
      /* open */
    } else break;
  }
  let score = streak * streak * 120;
  if (streak >= 4) score += 50000;
  if (blockedNear && streak >= 4) score = Math.floor(score / 8);
  return score;
}

function scorePlacement(board: Stone[], r: number, c: number, who: Stone, size: number): number {
  let s = 0;
  for (const [dr, dc] of DIRS) s += linePotential(board, r, c, dr, dc, who, size);
  return s;
}

function formsWallAttachedRun(
  board: Stone[],
  row: number,
  col: number,
  stone: Stone,
  runLen: number,
  size: number,
): boolean {
  const cellAtH = (r: number, c: number): Stone => {
    if (r === row && c === col) return stone;
    return board[idx(r, c, size)];
  };

  for (const [dr, dc] of DIRS) {
    let sr = row;
    let sc = col;
    while (true) {
      const nr = sr - dr;
      const nc = sc - dc;
      if (!inBounds(nr, nc, size) || cellAtH(nr, nc) !== stone) break;
      sr = nr;
      sc = nc;
    }
    let len = 0;
    let er = sr;
    let ec = sc;
    while (inBounds(er, ec, size) && cellAtH(er, ec) === stone) {
      len++;
      er += dr;
      ec += dc;
    }
    if (len !== runLen) continue;
    const e1r = sr - dr;
    const e1c = sc - dc;
    const e2r = sr + len * dr;
    const e2c = sc + len * dc;
    const wall1 = !inBounds(e1r, e1c, size);
    const wall2 = !inBounds(e2r, e2c, size);
    if (wall1 || wall2) return true;
  }
  return false;
}

/** 로컬 AI (Qt AiPlayer::suggestMove 포팅). 반환 (row, col). */
export function suggestMove(board: Stone[], aiColor: Stone, size: number): Point {
  const opp = opponent(aiColor);
  const mid = Math.floor(size / 2);

  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      if (canPlace(board, r, c, aiColor, size) !== "none") continue;
      if (wouldWinIfPlaced(board, r, c, aiColor, size)) return { r, c };
    }
  }

  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      if (canPlace(board, r, c, aiColor, size) !== "none") continue;
      if (wouldWinIfPlaced(board, r, c, opp, size)) return { r, c };
    }
  }

  let bestScore = Number.MIN_SAFE_INTEGER;
  const candidates: Point[] = [];
  const wallPenalty = 200000;

  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      if (canPlace(board, r, c, aiColor, size) !== "none") continue;
      let s =
        scorePlacement(board, r, c, aiColor, size) * 12 +
        scorePlacement(board, r, c, opp, size) * 15 -
        Math.abs(r - mid) -
        Math.abs(c - mid);
      if (
        formsWallAttachedRun(board, r, c, aiColor, 3, size) ||
        formsWallAttachedRun(board, r, c, aiColor, 4, size)
      ) {
        s -= wallPenalty;
      }
      if (s > bestScore) {
        bestScore = s;
        candidates.length = 0;
        candidates.push({ r, c });
      } else if (s === bestScore) {
        candidates.push({ r, c });
      }
    }
  }

  if (candidates.length) return candidates[0];

  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      if (canPlace(board, r, c, aiColor, size) === "none") return { r, c };
    }
  }
  return { r: mid, c: mid };
}

export { BLACK, WHITE };