Fast Bots
Every search on this site gets stronger with more simulations per turn. A simulator ten times faster is a smarter bot, and the machine your bot runs on may be slower than yours. This page measures what a turn holds, packs Ultimate Tic-Tac-Toe boards into bits, keeps memory allocation out of the search, and turns a multi-file project into the single file CodinGame accepts.
Count your search's iterations per turn with a monotonic clock that starts when the turn's first input line arrives. Budget a turn with a safety margin and spend the long first turn on set-up. Check how CodinGame builds your C++ or Rust, and stop trusting your own machine's numbers. Store a small board as two 9-bit masks and answer questions about it with one table lookup. Keep allocation out of the search, and bundle a multi-file project into one file under the 100,000-character limit.
01Count what fits in a turn
The last three pages built searches that improve with every extra unit of work: alpha-beta reads a ply deeper, MCTS spends more playouts on the moves that hold up, evolution tries more plans. On the Monte Carlo Tree Search page, MCTS with twice the iterations scored .70 against the same search with half as many. Contest winners measure the same thing on their own bots: MSz, who won CodinGame's Summer Challenge 2024, ran his final bot against two earlier versions at half, normal and double its time budget; it scored 49.94%, 54.93% and 57.91% (his figures, each within about ±1 at 99% confidence). He wrote that "the number of iterations strongly affects playing strength".[1]
So the number to watch is how much search fits in one turn: nodes for alpha-beta, iterations for MCTS, plans for evolution. Measure it with a monotonic clock, one that only moves forward even if the computer's date and time get corrected while your bot runs: std::chrono::steady_clock in C++, std::time::Instant in Rust. Start it right after your bot reads the turn's first input line. dwarfie noted on the Ultimate Tic-Tac-Toe forum that "the timer start after first input reading in a turn", and reading the rest of the input comes out of the same turn.[2]
Count iterations as the search runs and print the count to stderr, which shows in your test games. Reading the clock costs time too, so don't read it every iteration. Checking it once every 64 iterations, as the bots on these pages do, keeps the cost negligible, and at 4 µs an iteration it overruns the deadline by at most a quarter of a millisecond.
Worked example
Your MCTS bot prints 21,800 iterations for an 85 ms search: 85,000 µs ÷ 21,800 = 3.9 µs per iteration.
Make each iteration twice as fast and the same 85 ms holds about 43,600. On the MCTS page that doubling scored .70 against the slower search, at much smaller budgets.
02Your machine is not CodinGame's
A count measured on your desktop doesn't transfer. On CodinGame your bot is built by CodinGame's compiler with CodinGame's flags, then run on a CPU you don't choose. The Help Center states the environment, "a Linux environment on a 64bit multi-core architecture", and the versions: g++ 11.2.0 in C++20 mode, Rust 1.90.0, Python 3.11.5. It lists no optimization flags for any language.[3] (Versions as read on 2026-10-30.)
| C++ | Rust | |
|---|---|---|
| Compiler on the Help Center page | g++ 11.2.0, C++20 | rustc 1.90.0 |
| Optimization flags on that page | none (it lists only link libraries) | none |
| What CodinGame staff wrote | 2015 and 2017: no optimization flags | 2018 and 2019: release mode for multiplayer submissions, debug mode in the IDE |
| What players do | #pragma GCC optimize("O3") at the top of the file | nothing extra |
| Checked in 2026 | unknown: run the check below | the IDE's build was optimized (the author's probe, below) |
For C++, the official word is old. In 2015 a CodinGame admin wrote that "there are no optimisation flags at the moment"; in 2016 another explained that optimization would cost the crash line numbers beginners rely on; and in 2017 a third wrote that "every languages on CG are compiled/run without optimization flag (debug purpose > time)".[4] The current Help Center page lists no flags either way. Players add #pragma GCC optimize("O3") as the first line of the file: Thufir_Hawat reported a 4–5× speed-up on an STL-heavy puzzle solution in 2016, and darkhorse64 still recommended it in 2025.[4]
The pragma tells GCC to compile every function defined after it as if it carried an optimize("O3") attribute, which is why it goes above the #include lines: the library code those lines pull in is then defined after the pragma too. GCC's own manual says that attribute "should be used for debugging purposes only" and is "not suitable in production code".[5] Players use it anyway because a single source file has no other way to ask the compiler for optimization; test your bot with it, as you would any build change.
For Rust, staff wrote in 2018 that multiplayer submissions are built in release mode while plays in the IDE stay in debug mode. In 2019 they wrote that the arena ran rustc with -O, which meant opt-level 2 at the time, and then confirmed opt-level 3.[6] The author's 2026 probe found the IDE's build optimized as well.
From the author's lab
On September 17, 2026, during the Summer Challenge, a probe bot from the author's lab ran in the contest's IDE through the Play button. It timed a deliberately naive Rust loop of 22 million multiply-adds (four layers of a 3×3 convolution over a 30×20 grid with 32 channels) and reported its build settings in the game viewer.[7]
CodinGame ran the loop in 11.2 ms. The same loop took 3.4 ms on the author's desktop built with rustc -O, and 107 ms built without optimization. Debug assertions were off and the CPU supported AVX2. So the IDE's Rust build was optimized, and its CPU ran this loop about 3.3 times slower than the desktop.
That is a single measurement of a single loop, taken on September 17; measure again before relying on 3.3.
Don't take any of these answers on trust: make your bot report its own build. In C++, GCC defines __OPTIMIZE__ "in all optimizing compilations".[5] Test it above the pragma, because GCC also defines it once a pragma raises the level: a g++ 16 build at -O0 reports no above the pragma and yes below it. In Rust, cfg!(debug_assertions) is true by default "only if the opt-level is 0".[8] The listing in section 06 prints its language's check first; put the same lines in your bot's first turn and read stderr in a test game.
03Budget the turn
A late answer loses the game whatever the position. Ultimate Tic-Tac-Toe gives each player 1,000 ms for its first move and 100 ms for each move after that.[9] Your clock can't see everything between the referee sending the input and reading your answer, so stop early: jacek's rule of thumb on the forum is "Better give yourself some 5-10ms buffer", and the author's lab bot for the game searches 85 ms of each 100, and 900 ms of the first 1,000 when it moves first.[2][7]
Games built on CodinGame's game engine 4.7 or later forgive two overruns of up to 50 ms per player; the third overrun, or one longer than that, is a timeout.[10] The Spring and Summer Challenges of 2026 run on engine 4.7.8; older games like Ultimate Tic-Tac-Toe don't. Budget as if no game forgave anything.
The first turn is for set-up. Allocate the node pool, build lookup tables, and start the search, keeping the tree for the next turn: Magus allocated 5,000,000 nodes on his first turn and reused that tree on his second.[2]
With a measured speed and a budget, what fits in a turn is arithmetic. The widget below does it for three kinds of search; the formula is the same for all of them.
In words: take the time you allow yourself, multiply by how many simulator steps your machine runs per second, and divide by how much slower CodinGame runs the same code. An MCTS iteration costs one playout's worth of steps; a full-width search to depth d costs b + b² + … + bd steps with b moves per position.
Worked example
Your simulator runs 10 million steps a second; the limit is 100 ms and the margin 15 ms. That gives 10,000,000 × 85 ÷ 1000 = 850,000 steps on your machine, and 850,000 ÷ 3.3 = 257,576 on CodinGame.
A random Ultimate Tic-Tac-Toe playout from move 20 lasts about 39 moves, so MCTS gets 850,000 ÷ 39 = 21,795 iterations on your machine and 6,605 on CodinGame.
With 9 moves per position, a full-width search to depth 5 visits 9 + 81 + 729 + 6,561 + 59,049 = 66,429 positions and depth 6 visits 597,870. Depth 6 fits on your machine and not on CodinGame.
The 39-move playout and the 9 moves per position come from random games played with the page's Ultimate Tic-Tac-Toe rules; your game will have its own. The genetic-algorithm row assumes a plan of six turns, as on Evolution and Beam Search, scored by simulating all six.
04A board in nine bits
A simulator's inner loop asks the same few questions millions of times a turn: which cells are free, did that move complete a line, can the player to move win right now. Stored as nine bytes, each question is a loop over cells. Stored as bits, each is a few machine instructions or one table lookup.
A stores a set of cells as the bits of an integer, one bit per cell. Chess programs built on bitboards keep a 64-bit word per piece type, and the Chess Programming Wiki puts the appeal plainly: bitboards "can operate on all squares in parallel using bitwise instructions".[11] An Ultimate Tic-Tac-Toe small board needs nine bits per player. Number the cells 0 to 8, row by row; X's marks are one 9-bit number with bit k set when X owns cell k, and O's marks are another.
Three operations cover most of what a simulator needs. ~(x | o) & 0x1FF is the set of free cells: OR the two players' marks, flip every bit, keep the low nine. Counting trailing zero bits finds the lowest set bit, the first free cell (std::countr_zero in C++, trailing_zeros in Rust), and m &= m - 1 clears it, so a loop visits only the free cells. A popcount counts them, and most desktop and server CPUs have a single instruction for it. Bit Shifting covers all three, in its sections on masks and counting bits.
Line checks use a table instead. Every possible set of one player's marks is a number from 0 to 511, so build two 512-entry tables once: WIN[mask] is 1 when the mask holds a complete line, and WINNING_CELLS[mask] lists the cells that would complete one. At play time each answer is one lookup. jacek's Legend bot did its small-board win check "with lookup table", Magus advised precomputing "the list of possibles moves for each grids", and Tom Alard writes that "for move generation, the most important thing is to use bitboards".[2][12]
Worked example
X owns cells 0 and 4, O owns 2 and 6. Then x = 0b000010001 = 17 and o = 0b001000100 = 68.
free = ~(17 | 68) & 511 = ~85 & 511 = 0b110101010 = 426: cells 1, 3, 5, 7 and 8. WIN[17] = 0: X has no line yet.
WINNING_CELLS[17] = 0b100000000, cell 8, the end of the 0-4-8 diagonal. AND it with the free cells and it's still cell 8: X wins at once by playing 8.
The same idea covers bigger grids. Janzert, 5th in the 2010 Google AI Challenge's Tron contest, stored the map as "a 64bit integer to represent each column", and bowwowforeach credits bitboards and bit shifts for the speed behind his Summer Challenge 2025 win, "which allowed for a much higher number of search iterations".[13][14] Bitboards are weak at answering "what's on cell k", which takes a test per player, so keep a plain array alongside when your code asks that often.[11]
05Keep allocation out of the hot loop
The hot loop is the code that runs millions of times a turn: selection, the playout, the evaluation. An allocation, asking the allocator for a fresh block of memory, costs time on every call there, and the cost varies from call to call. darkhorse64 on the Ultimate Tic-Tac-Toe forum: "The master rule for performance is 'do not allocate/free anything in your real time loop'. Otherwise, your response time becomes unpredictable."[2]
In practice that means three habits. Allocate the search tree's nodes once, as one pool on the first turn, and hand them out by index. Give move lists a fixed size (an array of 81 for Ultimate Tic-Tac-Toe) instead of a growable vector. And copy small states instead of building new ones: an Ultimate Tic-Tac-Toe state is eighteen 9-bit masks and a few small fields, a few dozen bytes, so copying it at the start of every iteration is cheap. Games with large states often apply a move and undo it afterwards instead, because the copy itself would cost too much. The memory allocators page builds pools and their relatives from scratch.
Worked example
An MCTS node holds its wins as a 4-byte float, its visits as a 4-byte integer, the index of its first child in 4 bytes, its child count in 1 and its move in 1: 14 bytes, padded to 16.
6,000,000 nodes × 16 bytes = 96 MB, an eighth of CodinGame's 768 MB memory limit.[3] Even if every iteration added ten nodes, at 6,600 iterations a turn that pool would hold about 90 turns of search.
From the author's lab
The lab's Legend bot for Ultimate Tic-Tac-Toe uses that pool: 6 million 16-byte nodes. It also keeps a second pool, touched page by page during its long first move when it plays first, so the operating system has mapped that memory before the timed turns begin. When the first pool is half full, the subtree the bot keeps for the next turn is copied into the second.[7]
06Bit tricks and a timing harness in code
These complete programs build the two 512-entry tables at compile time, wrap a small board in the three bit operations, and test everything against plain loops over nine cells. The harness has three parts. It decodes all 39 = 19,683 ways to fill a small board with empty cells, X and O (impossible boards included) and checks that the masks and tables agree with the loops on every one. It times the line check both ways on the same 65,536 boards, repeated 300 times, and prints nanoseconds per board, with the totals printed so the compiler can't skip the work. And it runs a turn-shaped loop for 85 ms, reading the clock only every 64 iterations. The first line it prints is the build check from section 02.
// Ultimate Tic-Tac-Toe small boards as two 9-bit masks, with 512-entry lookup tables.
// main() checks the tables against plain loops on all 19,683 boards, then times both.
// Report the build first: GCC defines __OPTIMIZE__ once a #pragma GCC optimize raises the level,
// so test it before any such pragma to learn what the compiler command line asked for.
#ifdef __OPTIMIZE__
constexpr bool OPTIMIZED_BUILD = true;
#else
constexpr bool OPTIMIZED_BUILD = false;
#endif
#include <array>
#include <bit>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <vector>
// Cell k of a small board is bit k (0 1 2 / 3 4 5 / 6 7 8). The eight lines as masks.
constexpr std::uint16_t FULL_BOARD = 0x1FF;
constexpr std::array<std::uint16_t, 8> LINES = {0x007, 0x038, 0x1C0, 0x049, 0x092, 0x124, 0x111, 0x054};
struct Tables {
std::array<std::uint8_t, 512> hasLine{}; // 1 when the mask holds a complete line
std::array<std::uint16_t, 512> winningCells{}; // cells that would complete a line for that mask
};
// The table generator: every possible mask, checked once, at compile time.
constexpr Tables makeTables() {
Tables tables;
for (int mask = 0; mask < 512; ++mask)
for (std::uint16_t line : LINES)
if ((mask & line) == line) tables.hasLine[mask] = 1;
for (int mask = 0; mask < 512; ++mask)
for (int cell = 0; cell < 9; ++cell)
if (!(mask >> cell & 1) && tables.hasLine[mask | 1 << cell])
tables.winningCells[mask] |= static_cast<std::uint16_t>(1 << cell);
return tables;
}
constexpr Tables TABLES = makeTables();
struct SmallBoard {
std::uint16_t x = 0, o = 0; // one bit per mark, for each player
std::uint16_t freeCells() const { return ~(x | o) & FULL_BOARD; }
bool closed() const { return TABLES.hasLine[x] || TABLES.hasLine[o] || (x | o) == FULL_BOARD; }
// Free cells where `mine` completes a line now: one lookup and one AND.
std::uint16_t winsNow(std::uint16_t mine) const { return TABLES.winningCells[mine] & freeCells(); }
};
// Visits the free cells: lowest set bit first, then clear it, one loop turn per free cell.
int listFree(const SmallBoard& board, std::array<int, 9>& out) {
int count = 0;
for (std::uint16_t free = board.freeCells(); free != 0; free &= free - 1) out[count++] = std::countr_zero(free);
return count;
}
// The same board as nine cells (0 empty, 1 X, 2 O), and the loops the masks replace.
using CellArray = std::array<std::uint8_t, 9>;
constexpr int LINE_CELLS[8][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, {0, 4, 8}, {2, 4, 6}};
bool hasLineLoop(const CellArray& cells, std::uint8_t player) {
for (const auto& line : LINE_CELLS) // up to 8 line tests of 3 reads each
if (cells[line[0]] == player && cells[line[1]] == player && cells[line[2]] == player) return true;
return false;
}
int listFreeLoop(const CellArray& cells, std::array<int, 9>& out) {
int count = 0;
for (int cell = 0; cell < 9; ++cell) if (cells[cell] == 0) out[count++] = cell;
return count;
}
// xorshift64: the same random boards in the C++ and Rust versions.
struct Rng {
std::uint64_t state;
explicit Rng(std::uint64_t seed) : state(seed | 1) {}
std::uint64_t next() { state ^= state << 13; state ^= state >> 7; state ^= state << 17; return state; }
};
using Clock = std::chrono::steady_clock; // monotonic: never jumps when the wall clock is adjusted
void check(bool ok, const char* what) {
if (!ok) { std::fprintf(stderr, "FAILED: %s\n", what); std::exit(1); }
}
int main() {
std::printf("optimized build: %s\n", OPTIMIZED_BUILD ? "yes" : "no");
// 1. Every one of the 3^9 = 19,683 boards: tables and loops must agree.
std::vector<SmallBoard> boards;
std::vector<CellArray> arrays;
for (int code = 0; code < 19683; ++code) {
SmallBoard board;
CellArray cells{};
for (int cell = 0, rest = code; cell < 9; ++cell, rest /= 3) {
cells[cell] = static_cast<std::uint8_t>(rest % 3);
if (rest % 3 == 1) board.x |= static_cast<std::uint16_t>(1 << cell);
if (rest % 3 == 2) board.o |= static_cast<std::uint16_t>(1 << cell);
}
std::array<int, 9> fromMask{}, fromLoop{};
const int freeCount = listFree(board, fromMask);
check(freeCount == listFreeLoop(cells, fromLoop) && fromMask == fromLoop, "free cells");
check((TABLES.hasLine[board.x] == 1) == hasLineLoop(cells, 1), "X line");
check((TABLES.hasLine[board.o] == 1) == hasLineLoop(cells, 2), "O line");
check(board.closed() == (hasLineLoop(cells, 1) || hasLineLoop(cells, 2) || freeCount == 0), "closed");
for (int i = 0; i < freeCount; ++i) { // winsNow agrees with "play there, then look for a line"
CellArray after = cells;
after[fromMask[i]] = 1;
check((board.winsNow(board.x) >> fromMask[i] & 1) == (hasLineLoop(after, 1) ? 1 : 0), "winning cells");
}
boards.push_back(board);
arrays.push_back(cells);
}
std::printf("tables match the loops on all %zu boards\n", boards.size());
// 2. Time the line check both ways on the same random sequence of boards.
constexpr int ROUNDS = 300;
Rng rng(12345);
std::vector<int> order(1 << 16); // small enough to stay in cache, so memory speed doesn't decide
for (int& index : order) index = static_cast<int>(rng.next() % boards.size());
long long tableHits = 0, loopHits = 0; // printed, so the compiler can't skip the work
auto start = Clock::now();
for (int round = 0; round < ROUNDS; ++round)
for (int index : order) tableHits += TABLES.hasLine[boards[index].x] + TABLES.hasLine[boards[index].o];
const double tableSeconds = std::chrono::duration<double>(Clock::now() - start).count();
start = Clock::now();
for (int round = 0; round < ROUNDS; ++round)
for (int index : order) loopHits += hasLineLoop(arrays[index], 1) + hasLineLoop(arrays[index], 2);
const double loopSeconds = std::chrono::duration<double>(Clock::now() - start).count();
check(tableHits == loopHits, "same lines found");
const double checks = double(ROUNDS) * double(order.size());
std::printf("line check: table %.2f ns, loops %.2f ns per board (%lld lines found both ways)\n",
tableSeconds * 1e9 / checks, loopSeconds * 1e9 / checks, tableHits);
// 3. A turn-shaped loop: work until a deadline, reading the clock only every 64 iterations.
const auto deadline = Clock::now() + std::chrono::milliseconds(85);
long long iterations = 0, found = 0;
while ((iterations & 63) != 0 || Clock::now() < deadline) {
const SmallBoard& board = boards[iterations % boards.size()];
found += std::popcount(board.winsNow(board.x));
++iterations;
}
std::printf("in 85 ms: %lld boards scanned for winning cells (%lld found)\n", iterations, found);
return 0;
}
// Ultimate Tic-Tac-Toe small boards as two 9-bit masks, with 512-entry lookup tables.
// main() checks the tables against plain loops on all 19,683 boards, then times both.
use std::time::{Duration, Instant};
// rustc turns debug assertions on only at opt-level 0 (unless told otherwise), so their
// absence means the bot was built with optimization.
const OPTIMIZED_BUILD: bool = !cfg!(debug_assertions);
// Cell k of a small board is bit k (0 1 2 / 3 4 5 / 6 7 8). The eight lines as masks.
const FULL_BOARD: u16 = 0x1FF;
const LINES: [u16; 8] = [0x007, 0x038, 0x1C0, 0x049, 0x092, 0x124, 0x111, 0x054];
struct Tables {
has_line: [u8; 512], // 1 when the mask holds a complete line
winning_cells: [u16; 512], // cells that would complete a line for that mask
}
// The table generator: every possible mask, checked once, at compile time.
const fn make_tables() -> Tables {
let mut tables = Tables { has_line: [0; 512], winning_cells: [0; 512] };
let mut mask = 0;
while mask < 512 {
let mut line = 0;
while line < 8 {
if mask as u16 & LINES[line] == LINES[line] { tables.has_line[mask] = 1; }
line += 1;
}
mask += 1;
}
mask = 0;
while mask < 512 {
let mut cell = 0;
while cell < 9 {
if mask >> cell & 1 == 0 && tables.has_line[mask | 1 << cell] == 1 { tables.winning_cells[mask] |= 1 << cell; }
cell += 1;
}
mask += 1;
}
tables
}
const TABLES: Tables = make_tables();
#[derive(Clone, Copy, Default)]
struct SmallBoard {
x: u16, // one bit per mark, for each player
o: u16,
}
impl SmallBoard {
fn free_cells(&self) -> u16 { !(self.x | self.o) & FULL_BOARD }
fn closed(&self) -> bool { TABLES.has_line[self.x as usize] == 1 || TABLES.has_line[self.o as usize] == 1 || self.x | self.o == FULL_BOARD }
// Free cells where `mine` completes a line now: one lookup and one AND.
fn wins_now(&self, mine: u16) -> u16 { TABLES.winning_cells[mine as usize] & self.free_cells() }
}
// Visits the free cells: lowest set bit first, then clear it, one loop turn per free cell.
fn list_free(board: &SmallBoard, out: &mut [usize; 9]) -> usize {
let mut count = 0;
let mut free = board.free_cells();
while free != 0 { out[count] = free.trailing_zeros() as usize; count += 1; free &= free - 1; }
count
}
// The same board as nine cells (0 empty, 1 X, 2 O), and the loops the masks replace.
type CellArray = [u8; 9];
const LINE_CELLS: [[usize; 3]; 8] = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]];
fn has_line_loop(cells: &CellArray, player: u8) -> bool {
for line in &LINE_CELLS { // up to 8 line tests of 3 reads each
if cells[line[0]] == player && cells[line[1]] == player && cells[line[2]] == player { return true; }
}
false
}
fn list_free_loop(cells: &CellArray, out: &mut [usize; 9]) -> usize {
let mut count = 0;
for cell in 0..9 { if cells[cell] == 0 { out[count] = cell; count += 1; } }
count
}
// xorshift64: the same random boards in the C++ and Rust versions.
struct Rng { state: u64 }
impl Rng {
fn new(seed: u64) -> Rng { Rng { state: seed | 1 } }
fn next(&mut self) -> u64 { self.state ^= self.state << 13; self.state ^= self.state >> 7; self.state ^= self.state << 17; self.state }
}
fn main() {
println!("optimized build: {}", if OPTIMIZED_BUILD { "yes" } else { "no" });
// 1. Every one of the 3^9 = 19,683 boards: tables and loops must agree.
let mut boards = Vec::new();
let mut arrays = Vec::new();
for code in 0..19683 {
let mut board = SmallBoard::default();
let mut cells: CellArray = [0; 9];
let mut rest = code;
for cell in 0..9 {
cells[cell] = (rest % 3) as u8;
if rest % 3 == 1 { board.x |= 1 << cell; }
if rest % 3 == 2 { board.o |= 1 << cell; }
rest /= 3;
}
let (mut from_mask, mut from_loop) = ([0usize; 9], [0usize; 9]);
let free_count = list_free(&board, &mut from_mask);
assert!(free_count == list_free_loop(&cells, &mut from_loop) && from_mask == from_loop);
assert_eq!(TABLES.has_line[board.x as usize] == 1, has_line_loop(&cells, 1));
assert_eq!(TABLES.has_line[board.o as usize] == 1, has_line_loop(&cells, 2));
assert_eq!(board.closed(), has_line_loop(&cells, 1) || has_line_loop(&cells, 2) || free_count == 0);
for &cell in &from_mask[..free_count] { // wins_now agrees with "play there, then look for a line"
let mut after = cells;
after[cell] = 1;
assert_eq!(board.wins_now(board.x) >> cell & 1 == 1, has_line_loop(&after, 1));
}
boards.push(board);
arrays.push(cells);
}
println!("tables match the loops on all {} boards", boards.len());
// 2. Time the line check both ways on the same random sequence of boards.
const ROUNDS: usize = 300;
let mut rng = Rng::new(12345);
// Small enough to stay in cache, so memory speed doesn't decide.
let order: Vec<usize> = (0..1 << 16).map(|_| (rng.next() % boards.len() as u64) as usize).collect();
let (mut table_hits, mut loop_hits) = (0u64, 0u64); // printed, so the compiler can't skip the work
let start = Instant::now(); // monotonic: never jumps when the wall clock is adjusted
for _ in 0..ROUNDS {
for &index in &order { table_hits += (TABLES.has_line[boards[index].x as usize] + TABLES.has_line[boards[index].o as usize]) as u64; }
}
let table_seconds = start.elapsed().as_secs_f64();
let start = Instant::now();
for _ in 0..ROUNDS {
for &index in &order { loop_hits += has_line_loop(&arrays[index], 1) as u64 + has_line_loop(&arrays[index], 2) as u64; }
}
let loop_seconds = start.elapsed().as_secs_f64();
assert_eq!(table_hits, loop_hits);
let checks = (ROUNDS * order.len()) as f64;
println!("line check: table {:.2} ns, loops {:.2} ns per board ({} lines found both ways)",
table_seconds * 1e9 / checks, loop_seconds * 1e9 / checks, table_hits);
// 3. A turn-shaped loop: work until a deadline, reading the clock only every 64 iterations.
let deadline = Instant::now() + Duration::from_millis(85);
let (mut iterations, mut found) = (0usize, 0u32);
while iterations & 63 != 0 || Instant::now() < deadline {
let board = boards[iterations % boards.len()];
found += board.wins_now(board.x).count_ones();
iterations += 1;
}
println!("in 85 ms: {} boards scanned for winning cells ({} found)", iterations, found);
}
Build it optimized and not, and compare the line-check times. Expect the table to win by a wide margin; how wide depends on the compiler, the flags and the CPU. The table won in every build tried on one x86-64 desktop (g++ and clang++ at -O0 and -O2, rustc with and without -O), by factors from about 3 (rustc without optimization) to several tens (the optimized C++ builds). The machine was busy with other work while it ran, so treat those as rough.
The rest of the Ultimate Tic-Tac-Toe state: the masks of won and closed small boards, the board the next player is sent to, and move generation across boards (the shared tables extend to the big board, since won small boards also form a 9-bit mask). Playouts and the search itself, from Monte Carlo Tree Search. SIMD, which can test several boards at once. Reading CodinGame's input: wrap the build check in your bot's first turn to run it there.
07One file, 100,000 characters
CodinGame takes one source file, and the Help Center caps it: "The code size for any game is limited to 100k characters."[3] A bot worth keeping lives in several files, with tests and local tools beside the code that plays. So the project needs a build step that turns it into the one file you submit, and the bundle is what you test before submitting.
- Inline the modules. Inlining pastes each module's text where the file refers to it. In Rust, replace each
mod rules;withmod rules { … }holding the contents ofrules.rs. In C++, paste each local header in place of its#include "…"line, once per header. - Drop test-only code. Remove
#[cfg(test)]modules, and regions you mark yourself as local tools (the sample below marks them with// @bot-begin-skipand// @bot-end-skip). - Strip what you don't need. Comments, blank lines and indentation cost characters and nothing else. A line-based stripper must leave string literals alone: a line inside a string that starts with
//is data. - Check the bundle. Compile the single file with the platform's compiler version and flags, run the tests against it, and play a few local games with it before you submit.
The sample bot in the widget below is laid out the way this list assumes:
bot/ main.rs turn loop: clock starts at the first line, then read, search, print, flush rules.rs Ultimate Tic-Tac-Toe on bitboards, with the 512-entry line table search.rs UCT over a node array allocated once rng.rs xorshift random numbers for the playouts
The sample bot is plain ASCII, so its characters and bytes agree. Packing data into other characters changes that: How Bot Contests Work covers what CodinGame counts as a character, and Shipping a Network in 100,000 Characters packs a network's weights under the limit.
08Pitfalls
A clock started after parsing, or after some set-up, hides that time from your budget, and the bot overruns on exactly the turns where parsing is slow. Read the first line, take the time, then do everything else.
Benchmarking the wrong build. A debug build on your machine makes every idea look slow; an optimized one makes CodinGame look slow if the platform builds yours differently. Compare like with like, and print the build check in a test game.
Treating one ratio as a constant. The 3.3 above is one loop on one day, and other code or another day can give another ratio. Measure in a test game when it counts.
Reading the clock every iteration. The clock call isn't free, and in a loop of a few microseconds it can take a real share. Every 64 iterations keeps the overrun far below a millisecond at these speeds.
Allocating in the playout. A vector of legal moves built and freed per playout move is an allocation in the hottest code you have. Use fixed arrays and preallocated pools.
The pragma below the includes. #pragma GCC optimize covers only functions defined after it, so library code from headers above it keeps the command line's level.
A bundle that behaves differently. Test-only code your bot secretly depends on, or a comment stripper that eats part of a string, gives a submission that differs from the project you tested. Run the tests on the bundle itself.
09What's next
Every speed-up and every setting on this page is a change to a bot, and the only way to know whether a change helped is to measure it in games. Measuring Bot Strength puts error bars on a win rate, works out how many games a decision needs, and shows the traps that made the author's lab believe in improvements that weren't there.
10Sources
Widget readouts are computed in the page from the sliders and the sample bot's text; they are not measurements of CodinGame's servers. Lab numbers come from the author's records, named in their source note.
- MSz, 2024. Summer Challenge 2024 feedback and strategies, post #37. The winner's final bot measured at half, normal and double its time budget against two earlier versions, and "the number of iterations strongly affects playing strength".
- CodinGame forum, 2018–2024. Ultimate Tic Tac Toe [Puzzle discussion], thread 22616. Players' advice quoted here: Magus #36 (2018: nodes allocated on the first turn, the tree kept between turns) and #73 (2019: one integer for a 3×3 grid, precomputed move lists), dwarfie #132 (2022: when the turn's timer starts), jacek #133 (2022: a 5–10 ms buffer) and #150 (2023: lookup-table win checks), darkhorse64 #176 (2023: no allocation in the real-time loop).
- CodinGame Help Center. Languages Versions, read on 2026-10-30, and its source file (last changed 2025-10-08). The 64-bit Linux environment, g++ 11.2.0 in C++20 mode, Rust 1.90.0, Python 3.11.5, the 768 MB memory limit and the 100k-character limit on code; no optimization flags listed for any language.
- CodinGame forum, 2015–2025: staff posts on C++ optimization by _CG_Nick (2015, thread 506 #2), _CG_Maxime (2016, thread 1670 #10 and #31) and _CG_SaiksyApo (2017, thread 2538 #5), and players' use of the pragma: Thufir_Hawat (2016, thread 1670 #24) and darkhorse64 (2025, thread 201110 #9).
- GCC manual: Function Specific Option Pragmas (#pragma GCC optimize applies to functions defined after it), Common Attributes (the optimize attribute "should be used for debugging purposes only") and Common Predefined Macros (__OPTIMIZE__ "is defined in all optimizing compilations").
- CodinGame forum, 2018–2019. Rust Release mode compilation, thread 33552: TwoSteps #4, #8 and #21 (release mode for multiplayer submissions, debug mode in the IDE) and _CG_XorMode #23 and #30 (rustc with -O, then opt-level 3).
- Author's lab records, 2026 (private repository):
CodingGameSummer2026/POLICY_NET_PLAN.md("The probe, ready now" and CodinGame's reading of September 17),CodingGameSummer2026/CLAUDE.mdandtools/utf16_probe.py(the probe bot);codingame-rl-lab/games/tictactoe/README.md(the Ultimate Tic-Tac-Toe bot's clock, budgets and node pool),games/tictactoe/bots/mcts/andexperiments/live-submissions/tic-tac-toe/submitted.jsonl(the submitted bot's long first move only when it moves first, and its node layout). - The rustc book. Codegen Options: opt-level, and debug assertions enabled by default "only if the opt-level is 0".
- CodinGame, 2025. game-ultimate-tictactoe, the official referee and statement (commit 30656ef). 1,000 ms for each player's first move, 100 ms per move after that.
- CodinGame. codingame-game-engine, AbstractPlayer.java and GameManager.java at commit e78a9a4 (the code of engine 4.7.8). Two overruns of up to 50 ms forgiven per player; the third, or a larger one, is a timeout.
- Chess Programming Wiki. Bitboards. A set of squares as the bits of one word, operated on all at once with bitwise instructions.
- Tom Alard, 2026. Fighting for #1 in the Ultimate Tic-Tac-Toe Arena, blog post of September 20, 2026, written while 2nd on the leaderboard. CodinGame's constraints as he describes them, and bitboards for move generation.
- Brian Haskin (Janzert), 2010. Janzert's Tron Bots, Information Page. His bots for the 2010 Google AI Challenge, where he finished 5th (final rankings, Internet Archive copy): alpha-beta over a map stored as one 64-bit integer per column.
- bowwowforeach, 2025. Summer Challenge 2025 post-mortem, post #13. The winner's bitboards and bit shifts for the game's slowest computation, "which allowed for a much higher number of search iterations".