Five in a Row (Omok) Game
Code Battle
한글로 보기
Check the rules of the game, fill in the following functions, select the opponent, and click the Start Game button to start the battle.
It selects one of the coordinates from (0,0) to (14,14) on the 15x15 coordinate plate and returns it. If you take five consecutive coordinates in one direction of horizontal / vertical / diagonal, you win.
The player who starts the game first starts 1 round and the player who lost in the previous round starts the next round.
If you win 4 rounds out of a total of 6 rounds, you win the game.
Your color is white, and your opponent's color is black.

My Code (JavaScript)

Opponents

Online battles are played between signed-in members. Sign in to join.
If you or the opponent's screen is not visible, the communication speed will be slower.
TURN
COUNT
0
ROUND
WIN
0
LOSE
0
GAME
WIN
0
LOSE
0

Game Log

Recent logs are displayed at the top.

Code Arena

Enter your saved code and other members challenge it. The record grows while you are away.
Entering and challenging require a signed-in account. The list and the code are readable without signing in.
See the entry list

How to decide where one stone goes

All your omok code does is return one coordinate per turn. You never need to look at all 225 squares, because the next stone lands near the stones already on the board. The real question is how to score those candidates. Below are three ways of scoring and the results each actually produced.

1. Anywhere among the neighbours (the default sample)

This is what the starter code on this screen does. It plays the centre first, then picks at random among the squares next to stones already played. The scaffolding — holding the board, narrowing the candidates — is already there. The only thing missing is a score.

RANDOM roughly even LEVEL 1·2·3 loses every game

It is even against RANDOM, because neither side is thinking. The scaffolding is fine, so all you have to add is the scoring.

2. Count the length of the run

The first score that comes to mind is length. Drop your stone on a candidate square and count the longest run it makes in each of the four directions (horizontal, vertical, both diagonals). Then drop the opponent's stone on the same square and count their longest run too. Now "a square that extends me" and "a square that cuts them" sit on the same scale.

var SIZE = 15;
var EMPTY = 0, ME = 1, OP = 2;
var board = null;
var candidates = null;

function onGameStart() {}

function onRoundStart() {
	var r;

	board = [];
	for (r = 0; r < SIZE; r += 1) {
		board.push([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
	}
	candidates = [];
}

function inside(v) { return v >= 0 && v < SIZE; }

// Place a stone and add its eight neighbours to the candidate list.
// The next stone lands near existing ones, so the whole board is never needed.
function place(r, c, who) {
	var diffs = [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]];
	var at = candidates.indexOf(r * SIZE + c);
	var i, nr, nc;

	board[r][c] = who;
	if (at >= 0) { candidates.splice(at, 1); }
	for (i = 0; i < diffs.length; i += 1) {
		nr = r + diffs[i][0];
		nc = c + diffs[i][1];
		if (inside(nr) && inside(nc) && board[nr][nc] === EMPTY
				&& candidates.indexOf(nr * SIZE + nc) < 0) {
			candidates.push(nr * SIZE + nc);
		}
	}
}

// Longest run in any of the four directions if `who` played (r,c).
function runLength(r, c, who) {
	var dirs = [[0,1],[1,0],[1,1],[1,-1]];
	var best = 1;
	var i, side, len, nr, nc;

	for (i = 0; i < dirs.length; i += 1) {
		len = 1;
		for (side = -1; side <= 1; side += 2) {
			nr = r + dirs[i][0] * side;
			nc = c + dirs[i][1] * side;
			while (inside(nr) && inside(nc) && board[nr][nc] === who) {
				len += 1;
				nr += dirs[i][0] * side;
				nc += dirs[i][1] * side;
			}
		}
		if (len > best) { best = len; }
	}
	return best;
}

function onTurnStart(data) {
	var best = -1, bestScore = -1;
	var i, key, r, c, score, loc;

	if (data && data.opponentchoice) {
		place(data.opponentchoice[0], data.opponentchoice[1], OP);
	}
	if (!candidates.length) {
		place(7, 7, ME);
		return [7, 7];
	}
	for (i = 0; i < candidates.length; i += 1) {
		key = candidates[i];
		r = Math.floor(key / SIZE);
		c = key % SIZE;
		// Just add the value of extending my run to the value of cutting theirs.
		score = runLength(r, c, ME) + runLength(r, c, OP);
		if (score > bestScore) { bestScore = score; best = key; }
	}
	loc = [Math.floor(best / SIZE), best % SIZE];
	place(loc[0], loc[1], ME);
	return loc;
}

function onRoundEnd(result) {}
function onGameEnd(result) {}

RANDOM wins every game LEVEL 1·2·3 loses every game

RANDOM: wins every game. LEVEL 1: loses every game. Attacking only, defending only, and adding the two together all gave the same result. As long as the score is just length, nothing you add gets past it. One thing is missing.

3. An open run and a blocked run are not worth the same

Put two runs of length three side by side and the reason shows up.

· ● ● ● · Open at both ends. Becomes a four next turn, then a win.
○ ● ● ● · Blocked on one side. Only one way left to grow, so it is worth far less.

Both are length three. But the first is on its way to winning and the second is nearly dead. Counting length alone gives them the same score. So count whether the square past the run is empty, and score on (length, open ends). And for the same length, make your own value larger than theirs — a square that wins for you comes before a square that blocks them.

var SIZE = 15;
var EMPTY = 0, ME = 1, OP = 2;
var board = null;
var candidates = null;

function onGameStart() {}

function onRoundStart() {
	var r;

	board = [];
	for (r = 0; r < SIZE; r += 1) {
		board.push([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
	}
	candidates = [];
}

function inside(v) { return v >= 0 && v < SIZE; }

// Place a stone and add its eight neighbours to the candidate list.
// The next stone lands near existing ones, so the whole board is never needed.
function place(r, c, who) {
	var diffs = [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]];
	var at = candidates.indexOf(r * SIZE + c);
	var i, nr, nc;

	board[r][c] = who;
	if (at >= 0) { candidates.splice(at, 1); }
	for (i = 0; i < diffs.length; i += 1) {
		nr = r + diffs[i][0];
		nc = c + diffs[i][1];
		if (inside(nr) && inside(nc) && board[nr][nc] === EMPTY
				&& candidates.indexOf(nr * SIZE + nc) < 0) {
			candidates.push(nr * SIZE + nc);
		}
	}
}

// Lines through (r,c) if `who` played there, reported as length and how many
// ends are open. This is what differs from step 2.
function lines(r, c, who) {
	var dirs = [[0,1],[1,0],[1,1],[1,-1]];
	var out = [];
	var i, side, len, open, nr, nc;

	for (i = 0; i < dirs.length; i += 1) {
		len = 1;
		open = 0;
		for (side = -1; side <= 1; side += 2) {
			nr = r + dirs[i][0] * side;
			nc = c + dirs[i][1] * side;
			while (inside(nr) && inside(nc) && board[nr][nc] === who) {
				len += 1;
				nr += dirs[i][0] * side;
				nc += dirs[i][1] * side;
			}
			// If the square past the run is empty, that end is open.
			if (inside(nr) && inside(nc) && board[nr][nc] === EMPTY) {
				open += 1;
			}
		}
		out.push({len: len, open: open});
	}
	return out;
}

// An open three becomes a four next turn; a blocked three does not. So the same
// length is worth different amounts. Mine outranks theirs: winning beats blocking.
function lineScore(list, mine) {
	var best = 0, sum = 0;
	var i, line, value;

	for (i = 0; i < list.length; i += 1) {
		line = list[i];
		if (line.len >= 5) {
			value = mine ? 1000000 : 100000;
		} else if (line.len === 4) {
			value = line.open >= 2 ? (mine ? 50000 : 9000) : (mine ? 8000 : 4000);
		} else if (line.len === 3) {
			value = line.open >= 2 ? (mine ? 3000 : 2500) : (mine ? 200 : 150);
		} else if (line.len === 2) {
			value = line.open >= 2 ? (mine ? 100 : 80) : (mine ? 10 : 8);
		} else {
			value = line.open >= 2 ? 5 : 1;
		}
		sum += value;
		if (value > best) { best = value; }
	}
	// Add the rest to the most valuable single line: squares that hit two
	// directions at once are the good ones.
	return best + sum;
}

function onTurnStart(data) {
	var best = -1, bestScore = -1;
	var i, key, r, c, score, loc;

	if (data && data.opponentchoice) {
		place(data.opponentchoice[0], data.opponentchoice[1], OP);
	}
	if (!candidates.length) {
		place(7, 7, ME);
		return [7, 7];
	}
	for (i = 0; i < candidates.length; i += 1) {
		key = candidates[i];
		r = Math.floor(key / SIZE);
		c = key % SIZE;
		// Not raw lengths: values that also account for open ends.
		score = lineScore(lines(r, c, ME), true)
			+ lineScore(lines(r, c, OP), false);
		if (score > bestScore) { bestScore = score; best = key; }
	}
	loc = [Math.floor(best / SIZE), best % SIZE];
	place(loc[0], loc[1], ME);
	return loc;
}

function onRoundEnd(result) {}
function onGameEnd(result) {}

RANDOM · LEVEL 1 wins every game LEVEL 2·3 loses every game

Adding that one idea flipped LEVEL 1 to wins every game. Only two functions changed: the one that measures runs and the one that scores them. Everything else is the same as step 2.

4. From here it is your turn

LEVEL 2 and LEVEL 3 are still out of reach for this code. It only looks one move ahead; it never asks what the opponent does after that. And it does not go hunting for shapes that cannot be answered with a single stone, such as a square that creates two open threes at once. Paste the code above into the editor and start there.

How a battle runs

  • Code battle is a battle between your code and opponent's code, and the language is JAVASCRIPT.
  • Fill in the code, select the opponent (RANDOM, LEVEL1, LEVEL2, LEVEL3, Online opponent) and then press the game start button will start the match.
  • 1 game consists of 6 rounds, 1 round will continue until one side wins or all coordinates are occupied.

Omok has five hooks. There is no onTurnEnd; the opponent's move arrives as data.opponentchoice on the next onTurnStart. The code above contains all five, so it runs as-is.

Results come from 60 games (360 rounds) against each opponent, and a test in the repository re-checks them against the real rule engine. Every opponent except LEVEL 3 carries some randomness, so results vary; “wins every game” and “loses every game” mean 95% or more of the games that were not draws. At 20 games the sample-versus-RANDOM rate ranged from 21% to 80%, touching both edges of “roughly even”, so the count went up to 60.