Bot programming tutorialsMighty Professional
Build a Legend Bot · Rules in code

Referees and Simulators

A search bot asks "what happens if I do this?" thousands of times a turn, and a trainer asks it millions of times a night. Only a copy of the game's rules can answer, and if the copy is wrong, both learn the wrong game. This page explains what a referee is, how to copy its rules into your own language, and how to prove the copy matches it, turn for turn.

Time~50 minLevelIntermediatePrereqsHow Bot Contests Work and Your First Bot; Floating Point helps with the rounding details.StackC++20 & Rust · browser demos
◂ Build a Legend BotPhase 2 · Rules in codeNext · Evaluation Functions ▸
What you'll be able to do by the end

01What a referee is

A referee is the program that runs the game. Your bot never touches the game itself: it reads lines of text from the referee and prints lines back. The referee owns the true state of the game (where every unit is, every score, every random number still to come), and it alone decides what your lines do.

Each turn, a referee does the same jobs in the same order. It sends each player the lines describing that player's view of the game. It waits for each player's answer, up to the time limit. It reads the answers and checks that they're legal. It applies the rules, in a fixed order. Then it checks whether the game is over and, if it is, who won. Everything the viewer shows is something the decided.

On CodinGame's current game engine, a referee is a Java class built on the engine's SDK. It extends the engine's AbstractReferee, whose source calls it "the brain of your game", and fills in two methods: init, run once, and gameTurn, run every turn, with an optional onEnd.[1] Inside gameTurn the referee sends each player its lines with sendInputLine, runs the player's bot with execute, and reads its answer with getOutputs.[2]

The engine reads exactly as many lines as the game expects: a player who doesn't write that many lines before the timeout gets no output at all, and, in the engine's words, "Extra lines will be available for next turn." A bot that is late or silent makes getOutputs throw a TimeoutException, and the referee decides the penalty.[1] Ultimate Tic-Tac-Toe gives such a bot a score of −1 and ends the game; Troll Farm gives −2.[3][4]

Unless a game sets its own limits, the engine gives each bot 1,000 ms for its first turn and 50 ms for every turn after. The engine's 4.7 versions also forgive two overruns of up to 50 ms: the third overrun, or any larger one, is a timeout.[1] Budget as if the forgiveness weren't there.

Inside one referee turn

A toy game: two units collect coins on a 7 by 5 grid, and each bot prints one line a turn, WAIT or MOVE followed by UP, DOWN, LEFT or RIGHT. Press Step to walk through the referee's seven steps for each turn, or Play, which pauses on the first turn with a conflict; the line under the board gives the result of the full game with the current settings. On turn 2 both units try to enter the middle cell. Moving at the same time, both stay put, and the game ends 2–2 at the 12-turn limit. Switch to Player 0 moves first and the same two bots end 3–2 on turn 8. Then turn on the invalid command: player 1 prints MOVE NORTH on turn 2, validation eliminates it, player 0's valid move still happens, and player 0 wins on the spot, whatever the score.

The two endings come from one sentence of rules: what happens when two units want the same cell. A statement can say "moves are simultaneous" and still leave that open. The referee's code can't leave it open, because it has to do something.

Many referees are public. CodinGame publishes some on GitHub, such as Ultimate Tic-Tac-Toe and Spring Challenge 2022, and some contest games are published by their authors, such as Troll Farm, the Spring Challenge 2026 game.[3][5][4] For older games, unofficial copies of referees circulate in players' repositories without saying where they came from, and players have rewritten some referees from scratch. Treat those as a description you still have to check against real games. Read any referee to learn its rules, and check its license before you copy its code.

02Read what the statement leaves out

A statement is written for players. It explains the game well enough to play it, and it skips the details only a program needs: the order in which rules apply, how numbers are rounded, who wins a tie, and when the referee draws a random number. A simulator built from the statement alone gets the game mostly right, which in practice means it plays a different game.

The referee's source settles each of those details. A few examples from real referees:

GameWhat the referee doesWhy a simulator cares
Ultimate Tic-Tac-ToeShuffles the list of legal moves it sends you with java.util.Random, seeded from the game seed, every turn.[3]The order of the list you read is part of the game. A simulator that wants identical input lines has to make the same shuffle.
Spring Challenge 2022When a WIND spell pushes monsters out of a base's zone, each one draws a random new direction, in the iteration order of a Java HashMap keyed by object identity.[5]That order can change from one run of the referee to the next, so the same seed and the same commands don't always give the same game, even in the official referee.
Troll FarmBuilds the map, then draws the viewer's decorations, and later breaks ties between equally short paths for moving units, all from one random generator.[4]A simulator that skips the viewer's draws gets different tie-breaks for every unit after them.
Mad Pod RacingMultiplies the speed by 0.85, then truncates it toward zero; rounds positions to the nearest unit.[6]Truncating first, or rounding instead, makes the race drift apart from turn 1 (the diff widget below shows both).

The Troll Farm case is easy to miss. Its viewer sometimes adds a bird that flies between trees. Every turn the bird's animation draws from the same generator as the units' tie-breaks, and so does its route, which it gets from the game's own path-finding.[4] The author's lab engine makes the same draws for the decorations and the bird, and it replays the official referee's games exactly.[7]

03Port the rules into a pure function

Your simulator will be called from places the referee never is: inside a search that tries ten thousand moves, inside a trainer that runs hundreds of games at once, inside a test that replays last week's games. It can only be trusted in all of them if its output depends on nothing but its input.

So write the game as a state struct plus one function, step(commands), that applies one turn. A game is then a pure function of two things: the seed that built the map and the commands the players sent. Everything else is out: the clock, global variables, static counters, and containers whose iteration order depends on memory addresses.

Worked example

In the toy game, player 0 stands at (2, 1) and player 1 at (3, 1). Both print MOVE RIGHT.

Applied simultaneously, player 1 moves to (4, 1) and player 0 follows into the cell player 1 just left, (3, 1). Applied with player 0 first, player 0 finds (3, 1) still occupied and stays at (2, 1); then player 1 moves to (4, 1).

Same state, same commands, different next state. Your step has to make the referee's choice.

Keep the two sides of the protocol apart too. The author's lab splits every game into a Game, the referee's side (the full state, the lines each seat is sent, step and the scores), and an Agent, the bot's side, which builds everything it knows from the lines its seat was sent. Its step takes commands as protocol text, so every local game goes through the same parser as the referee.[7] A bot built that way can't accidentally read the opponent's hidden state in local games and then lose that advantage on CodinGame.

04Copy the random numbers draw for draw

Referees that make random maps build them from a seed, a number fixed for each game, so the same seed always gives the same map. To get the same map in your simulator, you have to produce the same random numbers, in the same order, from the same seed. On CodinGame that usually means reproducing Java's generator exactly.

The SDK hands referees a generator from getRandom(): Java's SecureRandom in its SHA1PRNG mode, seeded from the game seed, and its documentation tells referees to draw from that rather than from the seed directly.[1] Not every referee does. Troll Farm uses getRandom(); Ultimate Tic-Tac-Toe and Spring Challenge 2022 create a plain java.util.Random from the seed.[4][3][5] Port whichever one your referee uses. This page ports java.util.Random, which is short.

java.util.Random is a linear congruential generator. It keeps one 48-bit number as its state. Each draw multiplies the state by a fixed constant, adds 11, keeps the low 48 bits, and hands out the top bits. Java's documentation spells out every step, so that "Java implementations must use all the algorithms shown here".[8] That makes a bit-for-bit port possible in any language.

state = (state × 0x5DEECE66D + 11) mod 248
next(bits) = state >> (48 − bits)

In words: to draw, multiply the state by 0x5DEECE66D, add 11 and keep the low 48 bits; the answer is the new state's top bits, as many as were asked for. Before the first draw, new Random(seed) sets the state to the seed XOR 0x5DEECE66D, keeping 48 bits, so that small seeds don't start from a small state.

nextInt(bound), the call referees use most, needs one more step. It draws 31 bits and takes the remainder after dividing by bound. When bound doesn't divide 231 evenly, the last block of values is incomplete and would make small results slightly more likely, so Java rejects any draw from that block and draws again. A power of two is the exception: it uses the top bits directly.[8]

Worked example

new Random(7) starts from 7 XOR 0x5DEECE66D = 0x5DEECE66A. One step later the state is 0xBB0F1799572D, and its top 31 bits are 1,569,164,236.

nextInt(13) takes 1,569,164,236 mod 13 = 3. Mad Pod Racing's referee, as the author's lab reproduced it, makes exactly this draw to pick one of its 13 maps, so seed 7 races on map 3.

A bound of 230 + 1 = 1,073,741,825 is the worst case: one complete block fits below 231 and about half of all draws land in the incomplete second block. For seed 7, the seventh draw in the test below takes five tries.

Same seed, same map

Press Play. Mad Pod Racing's referee, as the author's lab reproduced it, builds its track from new Random(seed): one draw picks one of 13 maps, one rotates it, and two per checkpoint shift it by up to 100 units. Your copy is the same generator written in JavaScript, so it makes the same draws and the dashed track lies exactly on the solid one, for every seed on the slider. Now turn on One extra draw first, as when code you didn't know about draws a decoration from the same generator: every later draw takes the next number in the sequence, and seed 7 gives map 12 instead of map 3.

Draw order is part of the rules. A port that makes the right draws in a different order (y before x, or the decoration after the map instead of before) builds a different map from the same seed. In the listing below, the toy game draws each coin's x before its y and says so in a comment, because that order is a fact about the game.

05Prove it with transcripts

You've read the referee, ported the rules and the generator, and your simulator runs. Is it right? Watching games in it proves nothing; a one-unit difference in a position looks identical on screen. The test that settles it is mechanical: make the real referee play games, record everything it sent, replay the same commands in your simulator, and compare every line.

A record of one game is a : the seed, every line the referee sent each player on every turn, and every command each player printed. CodinGame's SDK can run a referee on your machine with a fixed seed and your own bots, headless, through MultiplayerGameRunner and its simulate() call.[2] An older referee may first need rebuilding against a newer SDK: Ultimate Tic-Tac-Toe's targets an engine version without simulate(), so the author's lab rebuilt it against version 4.1.6.[7] Run it with fuzz bots, bots that send random legal commands. Your own bot never tries most moves, so it never tests the rules behind them.

  1. Record. Play a few hundred games on the official referee with fuzz bots in both seats, each with its own seed, and save every transcript.
  2. Replay. For each transcript, build your simulator from the same seed and feed it the recorded commands, turn by turn.
  3. Diff. Compare each line your simulator would send with the recorded line. Stop at the first difference and print the turn, the player, the line, and both versions.
  4. Fix and repeat. The first mismatch points at the rule that ran on that turn. Fix it and run all the games again, until none differ.

Compare every line of every turn, and stop at the first difference. Later lines usually differ too, mostly because the first difference spreads, so fix the first one and run again. And a simulator that ends games with the right winner can still be wrong in the middle: in the listing below, a wrong resolution order changes 56 of 1,000 fuzz games, and only 15 of those end with a different result.

Find the first wrong turn

The recording is a four-pod race under the correct rules. Your engine replays the same commands with one bug and compares every frame, meaning the numbers seat 0 would read. Press Play: the replay pauses on the first frame that differs and outlines the number. With the collision radius one unit short, nothing changes until the first collision, on turn 4, where pod 0's x comes out 12,562 instead of 12,560. Press Play again and follow the error plot, which is logarithmic: that small miss passes 100 units on turn 23 and 1,000 on turn 49, and by then your engine is running a different race. Try the other bugs: each shows first on a turn that uses its rule. The two rounding bugs show on turn 1, as soon as a velocity has a fraction, and the 17° limit on turn 2, when a pod first turns the full 18°.

The first mismatch names a turn, and the turn names a rule: something that happened then, and only then, is wrong. In the widget the readout also lists what happened on that turn (a collision, a full-lock turn), which is usually enough to find the line of the referee to read again.

From the author's lab

Engines checked this way, against games played on the game's referee or on CodinGame itself:[7]

GameGames replayed exactlyNotes
Back Track King2,540 of 2,540about 210,000 turns, zero mismatches
Troll Farm1,000 of 1,000, then 300 of 300after porting SHA1PRNG and the viewer's draws
Spring Challenge 2022996 of 1,000the other 4 diverge only after a WIND turn the referee itself can't reproduce
Mad Pod Racing500 of 500on an unofficial copy of the 2016 referee, patched to match live games (Legend rules), plus 226 of 226 live ranking battles
Ultimate Tic-Tac-Toe300 of 300including the shuffled move lists
Fantastic Bits91 of 93 live gamesno official referee to port, so written from the statement and live games; in 2 games one number first differs, by 1 to 3 units, right after a wizard hits a bludger

The 4 Spring Challenge 2022 games aren't an engine bug: each diverges only after a WIND turn whose outcome depends on that HashMap order, which the referee itself can't reproduce. Fantastic Bits is the one engine with no official referee to port, and the one that misses games.

Verification also tells you when to stop: zero mismatches over a few hundred fuzz games and a handful of live games, with every exception explained, is a reasonable bar before you build search or training on the engine. A engine, one that matches every number, also lets you replay a lost live game locally from its log and see exactly what your bot saw.

06The whole check in code

These complete programs put the pieces together in C++ and Rust, with the same names in both. JavaRandom ports setSeed, next(bits) and nextInt(bound) with its rejection loop. Game is the toy game from the first widget as a pure function of seed and commands: its coins come from JavaRandom, step applies one turn, and a Resolution flag switches on the resolution-order bug. runGame records a Transcript, and firstMismatch diffs two of them.

The test in main checks three things. The generator's first ten draws for three seeds equal the values the JDK (OpenJDK 17) prints; seeds 7 and 42 both hit the rejection loop, so a port without it fails. Then 1,000 fuzz games recorded under the correct rules replay with no mismatch, while the buggy resolution changes 56 of them, 15 with a different result. Last, an invalid line on turn 2 ends the game for its sender. The programs print:

seed 23: first mismatch on turn 8, player 0's line 1: expected "3 1 0", got "2 1 0"
1000 of 1000 replays match. The resolution-order bug changes 56 games, and 15 of them end differently.

On seed 23, turn 7 is the worked example from section 03: player 0 at (2, 1) follows player 1 to the right. The first frame that shows the difference is the one sent on turn 8, where player 0's own line reads 3 1 0 in the recording and 2 1 0 in the buggy replay. That game ends 0–1 either way, so a check of results alone would have passed it.

A checked simulator core, complete programs
// A simulator's core: java.util.Random bit for bit, a toy game that is a pure function of its seed
// and the commands it receives, and a transcript diff that finds the first line that differs.
#include <algorithm>
#include <array>
#undef NDEBUG   // keep assert() on in every build, like Rust's assert!: main's checks run inside it
#include <cassert>
#include <climits>
#include <cstdint>
#include <iostream>
#include <optional>
#include <string>
#include <vector>

// java.util.Random as its Javadoc specifies it: a 48-bit linear congruential generator.
class JavaRandom {
public:
    explicit JavaRandom(std::int64_t seed) { setSeed(seed); }
    void setSeed(std::int64_t seed) {
        // Java scrambles the seed so that small seeds don't start from a small state.
        state = (static_cast<std::uint64_t>(seed) ^ kMultiplier) & kMask;
    }
    // One step of the generator, then the top `bits` of the 48-bit state as a Java int.
    std::int32_t next(int bits) {
        state = (state * kMultiplier + kAddend) & kMask;   // unsigned math wraps like Java's long
        return static_cast<std::int32_t>(state >> (48 - bits));
    }
    // A uniform integer in [0, bound).
    std::int32_t nextInt(std::int32_t bound) {
        assert(bound > 0);
        if ((bound & -bound) == bound)   // a power of two: use the top bits directly
            return static_cast<std::int32_t>((bound * static_cast<std::int64_t>(next(31))) >> 31);
        std::int32_t bits, value;
        do {   // reject the last, incomplete block of values so every result is equally likely
            bits = next(31);
            value = bits % bound;
            // Java tests bits - value + (bound - 1) < 0, relying on int overflow. Signed overflow
            // is undefined in C++, so do the same sum in 64 bits and compare with INT32_MAX.
        } while (static_cast<std::int64_t>(bits) - value + (bound - 1) > INT32_MAX);
        return value;
    }
private:
    static constexpr std::uint64_t kMultiplier = 0x5DEECE66DULL, kAddend = 0xBULL, kMask = (1ULL << 48) - 1;
    std::uint64_t state = 0;
};

// The coin game from the "Inside one referee turn" widget. Everything the game will ever do
// follows from the seed and the commands: no clock, no global state, no unordered containers.
constexpr int kWidth = 7, kHeight = 5, kCoins = 5, kTurnLimit = 12;
struct Cell {
    int x, y;
    bool operator==(const Cell&) const = default;
};
enum class Resolution { Simultaneous, PlayerZeroFirst };   // the second one is the bug to catch
using Frame = std::vector<std::string>;                    // the lines one player reads on one turn

class Game {
public:
    Game(std::int64_t seed, Resolution resolution) : resolution(resolution) {
        JavaRandom random(seed);
        while (static_cast<int>(coins.size()) < kCoins) {
            const int x = random.nextInt(kWidth);     // x before y: the draw order is part of the rules
            const int y = random.nextInt(kHeight);
            const Cell coin{x, y};
            if (coin != units[0] && coin != units[1] && std::find(coins.begin(), coins.end(), coin) == coins.end())
                coins.push_back(coin);
        }
    }
    bool over() const { return winner != kPlaying; }

    // What `player` reads this turn: its unit and score, the opponent's, the coins.
    Frame frameLines(int player) const {
        const auto unitLine = [&](int who) {
            return std::to_string(units[who].x) + ' ' + std::to_string(units[who].y) + ' ' + std::to_string(scores[who]);
        };
        if (over()) return {"game over: winner " + std::to_string(winner) + ", score " + std::to_string(scores[0]) + '-' + std::to_string(scores[1])};
        Frame lines{unitLine(player), unitLine(1 - player), std::to_string(coins.size())};
        for (const Cell& coin : coins) lines.push_back(std::to_string(coin.x) + ' ' + std::to_string(coin.y));
        return lines;
    }

    // One referee turn: validate, work out targets, resolve conflicts, score, check for the end.
    void step(const std::array<std::string, 2>& commands) {
        ++turn;
        // An invalid line eliminates its sender; the other player's move still happens this turn.
        std::array<bool, 2> valid{};
        std::array<Cell, 2> targets = units;
        for (int player = 0; player < 2; ++player) {
            const std::optional<Cell> move = parseCommand(commands[player]);
            valid[player] = move.has_value();
            if (!move) continue;
            const Cell target{units[player].x + move->x, units[player].y + move->y};   // walls: stay
            if (target.x >= 0 && target.x < kWidth && target.y >= 0 && target.y < kHeight) targets[player] = target;
        }
        const auto moving = [&](int player) { return targets[player] != units[player]; };
        if (resolution == Resolution::Simultaneous) {
            const bool sameCell = moving(0) && moving(1) && targets[0] == targets[1];
            const bool swap = moving(0) && moving(1) && targets[0] == units[1] && targets[1] == units[0];
            if (!sameCell && !swap) {
                std::array<Cell, 2> next = targets;
                for (int player = 0; player < 2; ++player)   // walking into a unit that stays: stay
                    if (targets[player] == units[1 - player] && !moving(1 - player)) next[player] = units[player];
                units = next;
            }   // two units wanting one cell, or swapping: both stay
        } else {
            for (int player = 0; player < 2; ++player)   // player 0 moves first and takes the cell
                if (targets[player] != units[1 - player]) units[player] = targets[player];
        }
        for (int player = 0; player < 2; ++player) {
            const auto coin = std::find(coins.begin(), coins.end(), units[player]);
            if (valid[player] && coin != coins.end()) { coins.erase(coin); ++scores[player]; }
        }
        if (!valid[0] || !valid[1]) winner = valid[0] ? 0 : valid[1] ? 1 : kDraw;
        else if (coins.empty() || turn >= kTurnLimit)
            winner = scores[0] > scores[1] ? 0 : scores[1] > scores[0] ? 1 : kDraw;
    }
    int winner = kPlaying;

private:
    static constexpr int kPlaying = -2, kDraw = -1;
    // The move as an offset (WAIT is 0, 0), or nothing for a line that isn't a command.
    static std::optional<Cell> parseCommand(const std::string& line) {
        if (line == "WAIT") return Cell{0, 0};
        if (line == "MOVE UP") return Cell{0, -1};
        if (line == "MOVE DOWN") return Cell{0, 1};
        if (line == "MOVE LEFT") return Cell{-1, 0};
        if (line == "MOVE RIGHT") return Cell{1, 0};
        return std::nullopt;
    }
    Resolution resolution;
    std::array<Cell, 2> units{{{1, 2}, {5, 2}}};
    std::array<int, 2> scores{0, 0};
    std::vector<Cell> coins;
    int turn = 0;
};

// Everything one game sent and received: frames[turn][player] and commands[turn][player].
struct Transcript {
    std::vector<std::array<Frame, 2>> frames;
    std::vector<std::array<std::string, 2>> commands;
};

// Plays a game, asking `commandsFor(turnIndex, game)` for both players' lines each turn.
template <typename CommandSource>
Transcript runGame(std::int64_t seed, Resolution resolution, CommandSource commandsFor) {
    Game game(seed, resolution);
    Transcript transcript;
    while (true) {
        transcript.frames.push_back({game.frameLines(0), game.frameLines(1)});
        if (game.over()) return transcript;   // the last frame says how the game ended
        transcript.commands.push_back(commandsFor(transcript.commands.size(), game));
        game.step(transcript.commands.back());
    }
}

struct Mismatch {
    std::size_t turn, line;
    int player;
    std::string expected, actual;
};
// The first line where two transcripts disagree, or nothing when they match everywhere.
std::optional<Mismatch> firstMismatch(const Transcript& expected, const Transcript& actual) {
    const std::size_t turns = std::max(expected.frames.size(), actual.frames.size());
    for (std::size_t turn = 0; turn < turns; ++turn)
        for (int player = 0; player < 2; ++player) {
            const Frame none{};
            const Frame& want = turn < expected.frames.size() ? expected.frames[turn][player] : none;
            const Frame& got = turn < actual.frames.size() ? actual.frames[turn][player] : none;
            for (std::size_t line = 0; line < std::max(want.size(), got.size()); ++line) {
                const std::string wantLine = line < want.size() ? want[line] : "(missing)";
                const std::string gotLine = line < got.size() ? got[line] : "(missing)";
                if (wantLine != gotLine) return Mismatch{turn + 1, line + 1, player, wantLine, gotLine};
            }
        }
    return std::nullopt;
}

// A fuzz bot pair: random valid commands, from their own generator so the game's draws stay untouched.
auto fuzzCommands(std::int64_t seed) {
    return [random = JavaRandom(seed)](std::size_t, const Game&) mutable {
        static const std::array<std::string, 5> kChoices{"WAIT", "MOVE UP", "MOVE DOWN", "MOVE LEFT", "MOVE RIGHT"};
        const std::string first = kChoices[random.nextInt(5)];
        return std::array<std::string, 2>{first, kChoices[random.nextInt(5)]};
    };
}

int main() {
    // 1. JavaRandom against the JDK: nine bounded draws, then nextInt() with no bound (next(32)).
    const std::array<std::int32_t, 9> bounds{13, 4, 201, 199, 16, 1500000000, 1073741825, 7, 5};
    const std::array<std::int64_t, 3> seeds{7, 42, -9876543210LL};
    const std::array<std::array<std::int32_t, 10>, 3> expected{{
        {3, 2, 159, 153, 5, 1053566254, 755731200, 5, 3, 1085177823},
        {0, 0, 90, 15, 4, 595021505, 196118093, 3, 2, 1583910553},
        {3, 0, 184, 79, 10, 1230284183, 716858268, 1, 3, 768250101}}};
    for (std::size_t seedIndex = 0; seedIndex < seeds.size(); ++seedIndex) {
        JavaRandom random(seeds[seedIndex]);
        for (std::size_t i = 0; i < bounds.size(); ++i) assert(random.nextInt(bounds[i]) == expected[seedIndex][i]);
        assert(random.next(32) == expected[seedIndex][9]);
    }
    // 2. Record fuzz games under the correct rules, replay their commands, diff every frame:
    // the correct engine must match every game, and the buggy one must be caught.
    int caught = 0, caughtByResult = 0;
    for (std::int64_t seed = 1; seed <= 1000; ++seed) {
        const Transcript recorded = runGame(seed, Resolution::Simultaneous, fuzzCommands(seed + 1000000));
        const auto replayCommands = [&](std::size_t turn, const Game&) { return recorded.commands[turn]; };
        assert(!firstMismatch(recorded, runGame(seed, Resolution::Simultaneous, replayCommands)));
        const Transcript buggy = runGame(seed, Resolution::PlayerZeroFirst, replayCommands);
        const auto mismatch = firstMismatch(recorded, buggy);
        if (!mismatch) continue;
        if (caught++ == 0)
            std::cout << "seed " << seed << ": first mismatch on turn " << mismatch->turn << ", player "
                      << mismatch->player << "'s line " << mismatch->line << ": expected \"" << mismatch->expected
                      << "\", got \"" << mismatch->actual << "\"\n";
        if (recorded.frames.back() != buggy.frames.back()) ++caughtByResult;   // the final line alone
    }
    std::cout << "1000 of 1000 replays match. The resolution-order bug changes " << caught
              << " games, and " << caughtByResult << " of them end differently.\n";
    // 3. An invalid line eliminates its sender at once, whatever the score.
    const Transcript invalid = runGame(3, Resolution::Simultaneous, [](std::size_t turn, const Game&) {
        return std::array<std::string, 2>{"MOVE RIGHT", turn == 1 ? "MOVE NORTH" : "WAIT"};
    });
    assert(invalid.commands.size() == 2 && invalid.frames.back()[0][0].starts_with("game over: winner 0"));
}
// A simulator's core: java.util.Random bit for bit, a toy game that is a pure function of its seed
// and the commands it receives, and a transcript diff that finds the first line that differs.

// java.util.Random as its Javadoc specifies it: a 48-bit linear congruential generator.
struct JavaRandom {
    state: u64,
}
impl JavaRandom {
    const MULTIPLIER: u64 = 0x5DEECE66D;
    const ADDEND: u64 = 0xB;
    const MASK: u64 = (1 << 48) - 1;
    fn new(seed: i64) -> JavaRandom {
        let mut random = JavaRandom { state: 0 };
        random.set_seed(seed);
        random
    }
    fn set_seed(&mut self, seed: i64) {
        // Java scrambles the seed so that small seeds don't start from a small state.
        self.state = (seed as u64 ^ Self::MULTIPLIER) & Self::MASK;
    }
    // One step of the generator, then the top `bits` of the 48-bit state as a Java int.
    fn next(&mut self, bits: u32) -> i32 {
        self.state = self.state.wrapping_mul(Self::MULTIPLIER).wrapping_add(Self::ADDEND) & Self::MASK;
        (self.state >> (48 - bits)) as i32   // `as` keeps the low 32 bits, like Java's (int) cast
    }
    // A uniform integer in [0, bound).
    fn next_int(&mut self, bound: i32) -> i32 {
        assert!(bound > 0);
        if bound & -bound == bound {   // a power of two: use the top bits directly
            return ((bound as i64 * self.next(31) as i64) >> 31) as i32;
        }
        loop {   // reject the last, incomplete block of values so every result is equally likely
            let bits = self.next(31);
            let value = bits % bound;
            // Java tests bits - value + (bound - 1) < 0, relying on int overflow; the same sum
            // in 64 bits, compared with i32::MAX, needs no overflow.
            if bits as i64 - value as i64 + (bound - 1) as i64 <= i32::MAX as i64 {
                return value;
            }
        }
    }
}

// The coin game from the "Inside one referee turn" widget. Everything the game will ever do
// follows from the seed and the commands: no clock, no global state, no hash-ordered containers.
const WIDTH: i32 = 7;
const HEIGHT: i32 = 5;
const COINS: usize = 5;
const TURN_LIMIT: u32 = 12;
#[derive(Clone, Copy, PartialEq)]
struct Cell {
    x: i32,
    y: i32,
}
#[derive(Clone, Copy, PartialEq)]
enum Resolution { Simultaneous, PlayerZeroFirst }   // the second one is the bug to catch
type Frame = Vec<String>;                             // the lines one player reads on one turn
const PLAYING: i32 = -2;
const DRAW: i32 = -1;

struct Game {
    resolution: Resolution,
    units: [Cell; 2],
    scores: [u32; 2],
    coins: Vec<Cell>,
    turn: u32,
    winner: i32,
}
impl Game {
    fn new(seed: i64, resolution: Resolution) -> Game {
        let mut game = Game { resolution, units: [Cell { x: 1, y: 2 }, Cell { x: 5, y: 2 }],
                              scores: [0, 0], coins: Vec::new(), turn: 0, winner: PLAYING };
        let mut random = JavaRandom::new(seed);
        while game.coins.len() < COINS {
            let x = random.next_int(WIDTH);   // x before y: the draw order is part of the rules
            let y = random.next_int(HEIGHT);
            let coin = Cell { x, y };
            if coin != game.units[0] && coin != game.units[1] && !game.coins.contains(&coin) {
                game.coins.push(coin);
            }
        }
        game
    }
    fn over(&self) -> bool { self.winner != PLAYING }

    // What `player` reads this turn: its unit and score, the opponent's, the coins.
    fn frame_lines(&self, player: usize) -> Frame {
        let unit_line = |who: usize| format!("{} {} {}", self.units[who].x, self.units[who].y, self.scores[who]);
        if self.over() {
            return vec![format!("game over: winner {}, score {}-{}", self.winner, self.scores[0], self.scores[1])];
        }
        let mut lines = vec![unit_line(player), unit_line(1 - player), self.coins.len().to_string()];
        lines.extend(self.coins.iter().map(|coin| format!("{} {}", coin.x, coin.y)));
        lines
    }

    // One referee turn: validate, work out targets, resolve conflicts, score, check for the end.
    fn step(&mut self, commands: &[String; 2]) {
        self.turn += 1;
        // An invalid line eliminates its sender; the other player's move still happens this turn.
        let mut valid = [false; 2];
        let mut targets = self.units;
        for player in 0..2 {
            let Some(movement) = Game::parse_command(&commands[player]) else { continue };
            valid[player] = true;
            let target = Cell { x: self.units[player].x + movement.x, y: self.units[player].y + movement.y };
            if (0..WIDTH).contains(&target.x) && (0..HEIGHT).contains(&target.y) {
                targets[player] = target;       // walls: stay
            }
        }
        let moving = |player: usize| targets[player] != self.units[player];
        if self.resolution == Resolution::Simultaneous {
            let same_cell = moving(0) && moving(1) && targets[0] == targets[1];
            let swap = moving(0) && moving(1) && targets[0] == self.units[1] && targets[1] == self.units[0];
            if !same_cell && !swap {
                let mut next = targets;
                for player in 0..2 {             // walking into a unit that stays: stay
                    if targets[player] == self.units[1 - player] && !moving(1 - player) {
                        next[player] = self.units[player];
                    }
                }
                self.units = next;
            }                                    // two units wanting one cell, or swapping: both stay
        } else {
            for player in 0..2 {                 // player 0 moves first and takes the cell
                if targets[player] != self.units[1 - player] {
                    self.units[player] = targets[player];
                }
            }
        }
        for player in 0..2 {
            if let Some(index) = self.coins.iter().position(|coin| *coin == self.units[player]) {
                if valid[player] {
                    self.coins.remove(index);
                    self.scores[player] += 1;
                }
            }
        }
        if !valid[0] || !valid[1] {
            self.winner = if valid[0] { 0 } else if valid[1] { 1 } else { DRAW };
        } else if self.coins.is_empty() || self.turn >= TURN_LIMIT {
            self.winner = if self.scores[0] > self.scores[1] { 0 } else if self.scores[1] > self.scores[0] { 1 } else { DRAW };
        }
    }
    // The move as an offset (WAIT is 0, 0), or nothing for a line that isn't a command.
    fn parse_command(line: &str) -> Option<Cell> {
        match line {
            "WAIT" => Some(Cell { x: 0, y: 0 }),
            "MOVE UP" => Some(Cell { x: 0, y: -1 }),
            "MOVE DOWN" => Some(Cell { x: 0, y: 1 }),
            "MOVE LEFT" => Some(Cell { x: -1, y: 0 }),
            "MOVE RIGHT" => Some(Cell { x: 1, y: 0 }),
            _ => None,
        }
    }
}

// Everything one game sent and received: frames[turn][player] and commands[turn][player].
struct Transcript {
    frames: Vec<[Frame; 2]>,
    commands: Vec<[String; 2]>,
}

// Plays a game, asking `commands_for(turn_index, game)` for both players' lines each turn.
fn run_game(seed: i64, resolution: Resolution, mut commands_for: impl FnMut(usize, &Game) -> [String; 2]) -> Transcript {
    let mut game = Game::new(seed, resolution);
    let mut transcript = Transcript { frames: Vec::new(), commands: Vec::new() };
    loop {
        transcript.frames.push([game.frame_lines(0), game.frame_lines(1)]);
        if game.over() {
            return transcript;                   // the last frame says how the game ended
        }
        let commands = commands_for(transcript.commands.len(), &game);
        game.step(&commands);
        transcript.commands.push(commands);
    }
}

struct Mismatch {
    turn: usize,
    line: usize,
    player: usize,
    expected: String,
    actual: String,
}
// The first line where two transcripts disagree, or nothing when they match everywhere.
fn first_mismatch(expected: &Transcript, actual: &Transcript) -> Option<Mismatch> {
    let turns = expected.frames.len().max(actual.frames.len());
    let none: Frame = Vec::new();
    for turn in 0..turns {
        for player in 0..2 {
            let want = expected.frames.get(turn).map_or(&none, |frame| &frame[player]);
            let got = actual.frames.get(turn).map_or(&none, |frame| &frame[player]);
            for line in 0..want.len().max(got.len()) {
                let want_line = want.get(line).map_or("(missing)", |text| text.as_str());
                let got_line = got.get(line).map_or("(missing)", |text| text.as_str());
                if want_line != got_line {
                    return Some(Mismatch { turn: turn + 1, line: line + 1, player,
                                           expected: want_line.to_string(), actual: got_line.to_string() });
                }
            }
        }
    }
    None
}

// A fuzz bot pair: random valid commands, from their own generator so the game's draws stay untouched.
fn fuzz_commands(seed: i64) -> impl FnMut(usize, &Game) -> [String; 2] {
    let mut random = JavaRandom::new(seed);
    move |_, _| {
        const CHOICES: [&str; 5] = ["WAIT", "MOVE UP", "MOVE DOWN", "MOVE LEFT", "MOVE RIGHT"];
        let first = CHOICES[random.next_int(5) as usize].to_string();
        [first, CHOICES[random.next_int(5) as usize].to_string()]
    }
}

fn main() {
    // 1. JavaRandom against the JDK: nine bounded draws, then nextInt() with no bound (next(32)).
    let bounds = [13, 4, 201, 199, 16, 1500000000, 1073741825, 7, 5];
    let seeds: [i64; 3] = [7, 42, -9876543210];
    let expected: [[i32; 10]; 3] = [
        [3, 2, 159, 153, 5, 1053566254, 755731200, 5, 3, 1085177823],
        [0, 0, 90, 15, 4, 595021505, 196118093, 3, 2, 1583910553],
        [3, 0, 184, 79, 10, 1230284183, 716858268, 1, 3, 768250101]];
    for (seed_index, &seed) in seeds.iter().enumerate() {
        let mut random = JavaRandom::new(seed);
        for (i, &bound) in bounds.iter().enumerate() {
            assert_eq!(random.next_int(bound), expected[seed_index][i]);
        }
        assert_eq!(random.next(32), expected[seed_index][9]);
    }
    // 2. Record fuzz games under the correct rules, replay their commands, diff every frame:
    // the correct engine must match every game, and the buggy one must be caught.
    let (mut caught, mut caught_by_result) = (0, 0);
    for seed in 1..=1000 {
        let recorded = run_game(seed, Resolution::Simultaneous, fuzz_commands(seed + 1000000));
        let replay_commands = |turn: usize, _: &Game| recorded.commands[turn].clone();
        assert!(first_mismatch(&recorded, &run_game(seed, Resolution::Simultaneous, replay_commands)).is_none());
        let buggy = run_game(seed, Resolution::PlayerZeroFirst, replay_commands);
        let Some(mismatch) = first_mismatch(&recorded, &buggy) else { continue };
        if caught == 0 {
            println!("seed {seed}: first mismatch on turn {}, player {}'s line {}: expected \"{}\", got \"{}\"",
                     mismatch.turn, mismatch.player, mismatch.line, mismatch.expected, mismatch.actual);
        }
        caught += 1;
        if recorded.frames.last() != buggy.frames.last() {
            caught_by_result += 1;               // the final line alone
        }
    }
    println!("1000 of 1000 replays match. The resolution-order bug changes {caught} games, and {caught_by_result} of them end differently.");
    // 3. An invalid line eliminates its sender at once, whatever the score.
    let invalid = run_game(3, Resolution::Simultaneous, |turn, _| {
        ["MOVE RIGHT".to_string(), if turn == 1 { "MOVE NORTH" } else { "WAIT" }.to_string()]
    });
    assert!(invalid.commands.len() == 2 && invalid.frames.last().unwrap()[0][0].starts_with("game over: winner 0"));
}

Both compile with warnings treated as errors (g++ and clang++ in C++20 mode, rustc with the 2021 edition), print the two lines above, and pass every assertion. A copy of the first widget's toy rules, replaying 600 of these games (300 seeds under both resolution rules), produces the same 7,800 frames for player 0.

What's intentionally missing

The official referee: here the reference rules stand in for it so the program runs on its own, while a real check records its transcripts from the SDK's runner or from live games. SHA1PRNG, which referees using getRandom() need. Bots as separate processes talking over pipes, with time limits. Variants for referees that are nondeterministic, like Spring Challenge 2022's. And speed: this Game copies strings every turn, which is fine for checking and slow for search.

07Run seeded matches in both seats

With a verified simulator, or the official referee on your own machine, you can play your bot against its rivals as often as you like. Three habits make those games mean something: fix the seeds, play both seats, and play many games at once.

Fixed seeds let you rerun the same maps after a change and compare like with like. Both seats matter because a map can favor one side: a bot can win a map from one seat and lose it from the other, so play every seed twice, with the bots swapped. And many games need many cores, but not too many: psyleague's documentation warns that "If you spawn too many workers your bots will start timing out randomly".[9] Measuring Bot Strength works out how many games a comparison needs.

You can write the match runner yourself (your simulator plus your bots as processes), or use the community's tools, none of them official:

A common complaint once local matches run in bulk: a bot is crashing constantly. Check the bots before the referee. In the author's lab, a rule-based Spring Challenge 2022 bot used as a training opponent crashed about 4,000 times a day, because it never cleared a list between turns; one inserted line fixed it. Results recorded before the fix had left the crashed games out, so they didn't measure what the referee would have scored.[7] Your match runner should treat a crash the way the referee does, as a loss for the bot that crashed, write it to the log, and keep going. Then fix the bot.

08Correct first, fast later

A search bot calls step thousands of times in a 50 ms turn, and a self-play trainer calls it for every turn of hundreds of games at once. The simulator that the diff approved is usually much slower than that work needs.

Make it correct first, then make it fast, and keep the slow version. A fast simulator rewritten with bit tricks and preallocated arrays is new code, so it gets the same check: replay the transcripts in both and diff them. Fast Bots covers the speed tricks, and Building the Trainer runs hundreds of simulations in one process.

09Where simulators go wrong

Small errors don't stay small

A simulator that is off by one unit on one turn of one game in a hundred is a different game. Search and training will exploit the difference, and your bot will play moves that work only in your copy.

Porting from the statement only. The statement leaves out resolution order, rounding and tie-breaks. Use it to understand the game, and the referee's code to copy it.

Floating-point differences between Java and your language. Java evaluates double arithmetic in a fixed order, while C++ compilers may contract a multiply and an add into one fused instruction or reorder math under fast-math flags, and math libraries can return a different last bit for sin, cos or atan2. Write expressions in the referee's order, keep fast-math off in the simulator, and let the transcripts tell you whether rounding hides the rest. Floating Point explains why the order of operations changes results.

Draws you didn't know about. A viewer's decoration, a shuffle of a list you never read, a tie-break deep in a movement rule: each one consumes a random number and shifts every later draw. When a map comes out wrong, count the referee's draws before you suspect your arithmetic.

Verifying with your own bot only. Your bot plays a narrow style, so it never tests the rules it avoids. Record games with fuzz bots, and with more than one real bot.

Trusting the official referee to be deterministic. Spring Challenge 2022's cannot always replay its own games. When a few games refuse to match, check whether the referee iterates over something whose order isn't fixed before you rewrite a rule that is right.

10What's next

Evaluation Functions uses the simulator to score positions instead of only replaying them: it turns "how good is this position for me?" into a number, which is what lets a bot compare its options when its rules disagree, and what every search in Phase 3 builds on.

11Sources

Referee behavior is described from each referee's source at the commit named in its note. Widget readouts are computed in the page from the toy game, from java.util.Random and from the pod physics in the author's lab.

  1. CodinGame, 2026. codingame-game-engine, engine core (version 4.7.8 sources, commit e78a9a4). AbstractReferee (init, gameTurn, onEnd), AbstractPlayer (expected output lines, extra lines left for the next turn, TimeoutException, two forgiven overruns), GameManager (1,000 ms first turn, 50 ms per turn by default), MultiplayerGameManager (the seed and getRandom(), a SHA1PRNG).
  2. CodinGame. CodinGame SDK Documentation: Game Manager, and the Game Runner page of the same documentation. Sending input, executing a player and reading its output; MultiplayerGameRunner with a fixed seed, start() and the headless simulate().
  3. CodinGame, 2025. game-ultimate-tictactoe, Referee.java at commit 30656ef. new Random(gameManager.getSeed()), the shuffled list of legal moves each turn, and a score of −1 for a timeout or an invalid move.
  4. eulerscheZahl, 2026. Troll-Farm, the official referee for CodinGame's Spring Challenge 2026, at commit 2901291. The map, the viewer's decorations and bird, and movement tie-breaks drawing from gameManager.getRandom(); a score of −2 for an eliminated player.
  5. CodinGame, 2022. SpringChallenge2022, Referee.java at commit 760e720. new Random(this.seed), and the WIND push resolved by iterating a HashMap of game entities.
  6. Magus, 2016. Coders Strikes Back (English version of his guide). The order of a pod's turn: friction of 0.85, positions rounded, speeds truncated.
  7. Author's lab records, 2026 (private repository). Engine checks: README.md and docs/provenance.md (Back Track King), games/trollfarm/README.md, games/spring2022/README.md, games/madpodracing/README.md, games/tictactoe/README.md, docs/multi-game.md (Fantastic Bits, status of October 17); the Game and Agent split in docs/multi-game.md; the crashing Spring Challenge 2022 opponent in AGENTS.md (October 18).
  8. Oracle. java.util.Random, Java SE 17 API documentation. The 48-bit state, setSeed, next(bits) and nextInt(bound) as required algorithms, with the rejection loop and the power-of-two case.
  9. Psyho, 2024. psyleague (README at commit 15fcf7c). A local league with automatic matchmaking and a TrueSkill table; works with any referee cg-brutaltester drives; too many workers cause random timeouts.
  10. dreignier, 2026. cg-brutaltester (README at commit d6bbeb3). The options for games, threads, seat swaps and the starting seed; the CommandLineInterface class SDK referees need; timeouts still on its list of future features.
  11. aangairbender, 2026. cgarena (version 1.4.2). A local arena with a web interface, candidate evaluation against a benchmark pool, and managed referee installs.
  12. jmerle, 2024. cg-local-app and cg-local-ext. Syncing the CodinGame IDE with a local file.