Game Rules
Read the strategy guideOpponents
TURN
ROUND
GAME
Game Log
Code Arena
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.