예제 소스
오목 좌표 LoRA: 15×15와 19×19 격자를 정확히 읽히기 · 파일 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 };