ted_kim
목록으로공개 전적
0W 0L 0D
도전자 0명이 남긴 결과입니다. 같은 사람이 여러 번 도전하면 마지막 결과 하나만 셉니다.
브라우저에서 계산한 결과라 조작을 완전히 막지 못합니다. 공식 기록으로 쓰지 않습니다.
미보고 도전 0건
출전 코드
// 아레나 출전판. 원본 claude-fable-5-omok.js와 다른 곳은 시간예산 한 줄뿐이다.
// 아레나는 Date.now()가 호출마다 1ms 전진하는 결정론 시계라 예산이 호출 횟수가
// 되고, 원본의 1350은 실측 수당 1.8초로 2초 제한에 가까웠다. 도전자의 느린
// 기기에서 방어 코드가 실격되지 않도록 400으로 줄였다(2026-10-11).
var SIZE = 15;
var EMPTY = 0;
var ME = 1;
var OPP = 2;
var WINTH = 5000000; // 이 이상이면 그 수로 오목 완성
var OPEN4TH = 700000; // 이 이상이면 열린 4 (막지 못하면 패배)
var WINVAL = 10000000;
var turnMap = null; // 15x15 오목판 상태
var stones = null; // 놓인 돌 목록 (r*15+c)
var deadline = 0;
var timeUp = false;
var DIRS = [[0, 1], [1, 0], [1, 1], [1, -1]];
// 패턴 점수표: 'o' = 평가 대상 플레이어의 돌, '_' = 빈칸, 'x' = 상대 돌 또는 판 밖
var PATTERNS = [
["ooooo", 10000000],
["_oooo_", 1000000],
["oooo_", 120000], ["_oooo", 120000],
["ooo_o", 120000], ["o_ooo", 120000], ["oo_oo", 120000],
["_ooo_", 25000],
["_o_oo_", 20000], ["_oo_o_", 20000],
["_ooo", 3000], ["ooo_", 3000],
["o_oo", 1500], ["oo_o", 1500],
["_oo_", 3000],
["_o_o_", 1200],
["_o_", 40]
];
function log(s) {
try { printLog(s); } catch (e) {}
}
function initBoard() {
turnMap = [];
for (var i = 0; i < SIZE; i++) {
var row = [];
for (var j = 0; j < SIZE; j++) row.push(0);
turnMap.push(row);
}
stones = [];
}
function place(r, c, p) {
turnMap[r][c] = p;
stones.push(r * SIZE + c);
}
function unplace(r, c) {
turnMap[r][c] = 0;
stones.pop();
}
// 문자열 s 안의 패턴 점수 합
function lineVal(s) {
var v = 0;
for (var k = 0; k < PATTERNS.length; k++) {
var pat = PATTERNS[k][0];
var val = PATTERNS[k][1];
var idx = s.indexOf(pat);
while (idx >= 0) {
v += val;
idx = s.indexOf(pat, idx + 1);
}
}
return v;
}
// (r,c)에 p가 두었을 때 4방향 패턴 점수 증가량
function cellScore(r, c, p) {
var total = 0;
for (var d = 0; d < 4; d++) {
var dr = DIRS[d][0], dc = DIRS[d][1];
var before = "", after = "";
for (var k = -4; k <= 4; k++) {
if (k === 0) { before += "_"; after += "o"; continue; }
var rr = r + dr * k, cc = c + dc * k;
var ch;
if (rr < 0 || rr >= SIZE || cc < 0 || cc >= SIZE) ch = "x";
else {
var v = turnMap[rr][cc];
ch = (v === 0) ? "_" : (v === p ? "o" : "x");
}
before += ch;
after += ch;
}
total += lineVal(after) - lineVal(before);
}
return total;
}
// 돌 근처(체비셰프 거리 2 이내)의 빈칸을 공격/수비 점수로 정렬해 상위 limit개 반환
function genMoves(p, limit) {
var o = (p === ME) ? OPP : ME;
if (stones.length === 0) {
return [{ r: 7, c: 7, a: 10, d: 0, s: 10 }];
}
var mark = {};
var list = [];
for (var i = 0; i < stones.length; i++) {
var sr = Math.floor(stones[i] / SIZE), sc = stones[i] % SIZE;
for (var dr = -2; dr <= 2; dr++) {
for (var dc = -2; dc <= 2; dc++) {
var r = sr + dr, c = sc + dc;
if (r < 0 || r >= SIZE || c < 0 || c >= SIZE) continue;
if (turnMap[r][c] !== 0) continue;
var key = r * SIZE + c;
if (mark[key]) continue;
mark[key] = 1;
var a = cellScore(r, c, p);
var dd = cellScore(r, c, o);
var s = a + dd * 0.9 + (14 - Math.abs(r - 7) - Math.abs(c - 7)) * 1.5;
list.push({ r: r, c: c, a: a, d: dd, s: s });
}
}
}
list.sort(function (x, y) { return y.s - x.s; });
if (list.length > limit) list.length = limit;
return list;
}
// p(지금 둘 차례) 관점의 휴리스틱 탐색 값
function searchVal(p, depth) {
if (Date.now() > deadline) { timeUp = true; return 0; }
var moves = genMoves(p, depth >= 2 ? 7 : 5);
if (moves.length === 0) return 0;
if (depth <= 0) {
var top = moves[0].s;
for (var j = 0; j < moves.length; j++) {
if (moves[j].a >= WINTH) return WINVAL;
}
return top;
}
var o = (p === ME) ? OPP : ME;
var best = -Infinity;
for (var i = 0; i < moves.length; i++) {
var m = moves[i];
var v;
if (m.a >= WINTH) {
v = WINVAL;
} else {
place(m.r, m.c, p);
v = m.a + 0.5 * m.d - 0.95 * searchVal(o, depth - 1);
unplace(m.r, m.c);
}
if (timeUp) break;
if (v > best) best = v;
if (best >= WINVAL * 0.9) break;
}
return best;
}
function anyEmpty() {
var best = null, bestDist = Infinity;
for (var r = 0; r < SIZE; r++) {
for (var c = 0; c < SIZE; c++) {
if (turnMap[r][c] === 0) {
var dist = Math.abs(r - 7) + Math.abs(c - 7);
if (dist < bestDist) { bestDist = dist; best = [r, c]; }
}
}
}
return best || [7, 7];
}
function chooseMove() {
deadline = Date.now() + 400;
timeUp = false;
if (stones.length === 0) return [7, 7];
var rootMoves = genMoves(ME, 14);
if (rootMoves.length === 0) return anyEmpty();
var i;
// 1. 내가 지금 오목을 만들 수 있으면 즉시 승리
for (i = 0; i < rootMoves.length; i++) {
if (rootMoves[i].a >= WINTH) return [rootMoves[i].r, rootMoves[i].c];
}
// 2. 상대가 다음 수로 오목을 만들 자리는 반드시 차단 (여러 곳이면 내 공격 가치가 큰 곳)
var blocks = [];
for (i = 0; i < rootMoves.length; i++) {
if (rootMoves[i].d >= WINTH) blocks.push(rootMoves[i]);
}
if (blocks.length > 0) {
blocks.sort(function (x, y) { return y.a - x.a; });
return [blocks[0].r, blocks[0].c];
}
// 3. 내가 열린 4를 만들 수 있으면 만든다 (다음 턴 승리 확정)
for (i = 0; i < rootMoves.length; i++) {
if (rootMoves[i].a >= OPEN4TH) return [rootMoves[i].r, rootMoves[i].c];
}
// 4. 상대가 열린 4를 만들 자리는 차단
var blocks2 = [];
for (i = 0; i < rootMoves.length; i++) {
if (rootMoves[i].d >= OPEN4TH) blocks2.push(rootMoves[i]);
}
if (blocks2.length > 0) {
blocks2.sort(function (x, y) { return (y.d + y.a) - (x.d + x.a); });
return [blocks2[0].r, blocks2[0].c];
}
// 5. 반복 심화 탐색 (깊이 2 -> 3 -> 4, 시간 내 완료된 깊이의 결과만 채택)
var best = rootMoves[0];
for (var depth = 2; depth <= 4; depth++) {
var curBest = null, curVal = -Infinity;
for (i = 0; i < rootMoves.length; i++) {
var m = rootMoves[i];
place(m.r, m.c, ME);
var v = m.a + 0.5 * m.d - 0.95 * searchVal(OPP, depth - 1);
unplace(m.r, m.c);
if (timeUp) break;
if (v > curVal) { curVal = v; curBest = m; }
}
if (timeUp) break;
if (curBest) {
best = curBest;
if (curVal >= WINVAL * 0.5) break; // 승리 수순 확보
}
}
return [best.r, best.c];
}
/**
* 게임이 시작되면 호출됩니다.
*/
function onGameStart() {
log("onGameStart");
}
/**
* 라운드가 시작되면 호출됩니다.
*/
function onRoundStart() {
initBoard();
}
/**
* 턴이 시작되면 호출됩니다.
* @param data data.turncnt, data.opponentchoice
* @return [row, col]
*/
function onTurnStart(data) {
if (!turnMap) initBoard();
var loc;
if (data && data.opponentchoice) {
var orr = data.opponentchoice[0], occ = data.opponentchoice[1];
if (orr >= 0 && orr < SIZE && occ >= 0 && occ < SIZE && turnMap[orr][occ] === 0) {
place(orr, occ, OPP);
}
loc = chooseMove();
} else {
// 첫 수: 중앙
loc = [7, 7];
}
if (!loc || loc[0] < 0 || loc[0] >= SIZE || loc[1] < 0 || loc[1] >= SIZE || turnMap[loc[0]][loc[1]] !== 0) {
loc = anyEmpty();
}
place(loc[0], loc[1], ME);
return loc;
}
/**
* 라운드가 끝나면 호출됩니다.
*/
function onRoundEnd(result) {
log("round end: win=" + result.win + " (" + result.wincnt + "-" + result.losecnt + ")");
}
/**
* 게임이 끝나면 호출됩니다.
*/
function onGameEnd(result) {
log("game end: win=" + result.win);
}
이전 출전
코드를 고쳐 다시 출전하면 전적이 처음부터 시작합니다. 이전 기록은 여기에 남습니다.
| 공개 전적 | 도전자 |
|---|---|
| 0W 0L 0D | 0 |
도전 기록
아직 도전받은 적이 없습니다.
기록된 수를 되짚습니다. 코드를 다시 실행하지 않습니다. 라운드마다 최종 판을 보여 주고, 마지막 수에 테두리를 칩니다.