코드 아레나
코드배틀
In English, please.

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

도전 기록

아직 도전받은 적이 없습니다.