Bot programming tutorialsMighty Professional
Build a Legend Bot · The arena

How Bot Contests Work

You've written programs that answer one question and exit. A contest bot answers a new question every 50 to 100 milliseconds, for hundreds of turns, against another program trying to beat it, on a machine you don't control, and a single late answer loses the game. This page explains the rules around your code on CodinGame, from the first line your bot reads to the final ranking of a two-week contest.

Time~45 minLevelBeginnerPrereqsNone. You read code in one language and know what a loop and a function are.StackC++20 & Rust · browser demos
◂ Build a Legend BotPhase 1 · The arenaNext · Your First Bot ▸
What you'll be able to do by the end

Explain what the referee does each turn, and why a late, missing or malformed line ends your game whatever the score. Write a turn loop that reads the whole input, answers inside the time limit and flushes. Say how a submission gets promoted in Wood and from Bronze up. Read a leaderboard score as a skill rating with uncertainty, and tell a real gain from the noise between identical submissions. Plan the days of a contest.

01A program that plays one turn at a time

A bot on CodinGame is an ordinary program. It reads text from its standard input (stdin) and writes text to its standard output (stdout), and the platform connects both to the game. CodinGame's help center sums the job up as "Read stdin, Flush stdout".[1] Each turn the game sends your bot the current state as a few lines of text, your bot prints its move as a line of text, and the game moves on.

The input comes in two parts. Before the first turn, the game sends the initialization input: facts that never change, such as the map size or which player you are. Then every turn it sends a frame, the lines that describe the game right now. The game's statement, its rules page, lists every line of both, in order, and says how many lines your bot must print each turn.

Anything you want to read yourself goes to a third stream, standard error (stderr). CodinGame shows your stderr in the IDE console and in the logs of your own test games. Ranked games run without it, so their logs hold only what each bot printed on stdout.[1][2]

Worked example

One turn of the toy game in this page's code. Each turn the referee lists a few piles of coins and your bot takes one. The initialization input is a single line, the number of turns. Each frame is the number of piles, then one line per pile with its coins. The first time through, the bot reads:

5     the game lasts 5 turns (initialization, sent once)
4     this turn has 4 piles
8
17
14
4

It answers TAKE 1, the pile with 17 coins. It has to read all four pile lines first, even though it found the 17 on the second. Lines a bot leaves unread don't go away. If it answered after two, the 14 and the 4 would still be waiting, next turn's read would take 14 as the pile count, and every number after that would be the wrong one. CodinGame's help center calls this desynchronizing from the referee.[3]

Referee holds the true game state Your bot a separate program frame on stdin move on stdout stderr: your console, test games only The opponent's bot is wired the same way.
The referee runs your bot as a separate program. It writes each frame to the bot's stdin and reads exactly the number of lines it expects from the bot's stdout. What the bot writes to stderr goes to your console in test games and nowhere in ranked games.

02The referee owns the game

Your bot never touches the real game. A separate program, the , holds the true state of the game. Each turn it sends every player its view of that state, waits for their answers, checks each answer against the rules, applies the moves in a fixed order and decides whether someone has won. Your bot only ever sees what the referee chooses to send it.

On CodinGame the referee is a Java class built on the CodinGame Game Engine, the kit recent games are written with. It extends a class called AbstractReferee and fills in init(), called once to set up the game, and gameTurn(turn), called once per turn; an onEnd() method is optional.[4] Inside a turn, the engine's documentation describes three calls: sendInputLine() sends a player its lines, execute() runs that player's code for one turn, and getOutputs() reads what it printed.[5]

That last call is where games are lost. If a player didn't answer in time, getOutputs() throws a TimeoutException, and the referee decides what that costs; usually it costs the game.[5] The engine also reads only as many lines as the game expects from you each turn. Its source says so in one line: "The game engine will not read more than the expected output lines. Extra lines will be available for next turn."[4] So a stray extra line isn't thrown away. The engine keeps it and reads it as your answer next turn.

Each map comes from a seed, a number that makes the referee's random generator produce the same map every time. The engine puts a random seed in every game's parameters and tells referee authors to draw all their randomness from a generator started with it.[4] That's what lets you replay a lost game on the same map later (section 06). Your bot also has a seat, its player slot, and some games favour one seat over the other.

Several referees are public. CodinGame publishes some in its GitHub organization, including Spring Challenge 2022 and Ultimate Tic-Tac-Toe; others live on their authors' accounts, such as Troll Farm (Spring Challenge 2026) and Back Track King (Summer Challenge 2026).[6][7][8][9] When a statement leaves a detail out, such as which of two simultaneous moves happens first, the referee's source has the answer. Referees and Simulators shows how to read one and turn it into a simulator your bot can search with.

03Time limits and what breaks them

Every turn has a deadline. If your move hasn't arrived when it passes, the referee stops waiting, and in most games that loses you the game, even if you were far ahead on points. The limit is set per game and printed in the statement, and it's short: tens of milliseconds for most turns.[10]

GameFirst turnEvery later turn
Back Track King (Summer Challenge 2026)1,000 ms50 ms[9]
Spring Challenge 20221,000 ms50 ms[7]
Ultimate Tic-Tac-Toe1,000 ms100 ms[6]
Mad Pod Racing (starting league)1,000 ms75 ms[11]
Tron Battlenone statedunder 100 ms[12]

"First turn" means your bot's first answer, whatever the game's turn number: the engine gives the first-turn limit to any player that has never been run.[4] That's the turn for set-up work such as filling lookup tables. Every turn after it has to be quick.

A bot that misses the deadline, crashes, or prints a line the referee can't use is eliminated. CodinGame's conventions put it plainly: such a bot "usually gets a score of -1 and loses the game, whatever it scored before."[13] The exact penalty is the referee's choice. Ultimate Tic-Tac-Toe's referee sets −1; Troll Farm's sets −2 and ends the game.[6][8] The engine reports a missing answer to the referee as a , whether the bot was slow, crashed or never flushed; a line the referee can't parse is an invalid action. Either one ends your game.

Newer games forgive a little. Version 4.7 of the game engine, which both 2026 contests were built on, lets a bot be up to 50 ms late twice; the third late answer, or any answer more than 50 ms late, is a timeout.[4] Troll Farm's statement says so ("you will only lose the game when you exceed the time limit 3 times by at most 50 ms or once by more than that"); Back Track King's statement doesn't mention it.[8][9] Treat the grace as protection against a busy server, and budget every turn as if it didn't exist.

Worked example

Back Track King: 50 ms per turn, with the 4.7 engine's grace of 50 ms twice.

Turn 12, your answer arrives at 61 ms: late, tolerated (1 of 2). Turn 40, at 58 ms: tolerated (2 of 2). Turn 77, at 52 ms: the third late answer, so it's a timeout, and the game is lost.

A single answer at 101 ms on any turn loses on the spot, because it's more than 50 ms over.

Three bugs turn a fast bot into a timed-out one.

Output that's never flushed. A program doesn't send every character the moment it prints it. Output collects in a buffer and goes out when the buffer is flushed. If your move sits in the buffer while your bot waits for the next frame, the referee sees nothing, and the bot times out with its answer already computed. CodinGame's conventions say to flush stdout every turn.[13]

Whether a missing flush bites depends on your I/O setup. In C++, std::cin is tied to std::cout: any read from cin first flushes whatever cout holds, so a plain read-then-print loop gets away without an explicit flush.[14] The common speed-up std::cin.tie(nullptr) removes that safety net. Rust documents its standard output as line-buffered on a terminal, and current versions flush at every newline when writing into a pipe as well, so println! gets through; wrap stdout in a BufWriter, another common speed-up, and nothing leaves until you call flush().[15][16] The code in section 10 shows both cases.

Debug text on stdout. Every line on stdout is a move. Print debug: 3 moves there and the referee reads it as this turn's answer, finds no valid action, and eliminates you. Debug output belongs on stderr.

Set-up on the wrong turn. Precomputation that runs on your second turn instead of your first runs under the short limit instead of the long one.

One turn, two bots, one referee

The timeline is one turn, in milliseconds after the referee sent both frames. Each bot reads its frame, thinks (the bar) and prints its move, which travels back to the referee; once both moves are in, the referee applies the rules (the blue mark) and sends the next frame. Press Play, then drag B's think time past the 50 ms limit, or turn on B forgets to flush or B prints debug to stdout. In this model the referee stops waiting at the limit, and a late, missing or malformed line eliminates B: A wins even when B leads on points. Overrun grace switches to the 2026 engine's rule. Turn it on and set B to 70 ms: B is late twice and tolerated both times, and the third late answer eliminates it. The model starts the clock when the frame goes out; how CodinGame's servers time a bot isn't public.

04Leagues and bosses

A contest doesn't drop you into one big ranking. Players are sorted into leagues, and you climb them one at a time. The ladder runs Wood, Bronze, Silver, Gold, Legend, with one or more Wood leagues at the bottom, and everyone starts in the lowest league that's open. Not every game goes all the way to Legend; some stop at Gold.[17]

Every league except the top one has a boss: a bot CodinGame copies into the league as the bar to clear. A big league is split into rooms, each with its own copy of the boss, and you're compared with the boss of your room.[17] How you get past the boss depends on the league, and the documentation gives the usual mode at each level.

Wood: boss battles. Each submission plays N games against the boss alone and needs W outright wins; the leagues documentation gives 5 and 3 as an example. A draw or a shared first place doesn't count, and your rank doesn't matter. If the series ends short, you stay, and only a new submission gets a new series, counted from zero.[17]

Bronze and up: rank above the boss. Your submission plays its ranking games against the players of your room. Once it has played all of them, it qualifies when its score, the skill rating of section 05, reaches the boss's. The check repeats after each later ranked game, so a submission can qualify late as its score moves.[17] Beating the boss in test games is a good sign and nothing more, because, in the documentation's words, "the ranking is computed against every player of the room".[17]

Promotion isn't instant either. Each league holds a promotion run on an interval, often every minute and sometimes hours apart, and moves up the players who qualified, best score first. The next league can bring new rules, with extra input lines and new actions. When it does, your current submission stops playing and you need to submit a bot for the new rules, so read the statement again after every promotion.[17]

In a live contest the top leagues, usually Silver, Gold and Legend, are closed at the start. Each opens on a date set in advance: CodinGame selects a player in the league below and makes a copy of their current submission the new boss. Everyone rated above that copy becomes eligible within seconds and moves up in the next promotion runs.[17] The bar you have to clear is then a real player's bot, picked mid-contest.

A Wood series is a small probability problem, and working it out tells you when to submit.

Worked example

Your bot wins 60% of the games against the Wood boss that aren't drawn, and 10% of games are draws. One battle is an outright win with chance q = 0.6 × (1 − 0.1) = 0.54. The series needs at least 3 wins in 5 battles:

P(3 wins) = 10 × 0.54³ × 0.46² = 0.3332. P(4) = 5 × 0.54⁴ × 0.46 = 0.1956. P(5) = 0.54⁵ = 0.0459.

Total: 0.5747. A submission promotes 57.5% of the time, so you need 1 / 0.5747 = 1.74 submissions on average.

The general version adds up the chance of every winning count from W to N. It's the chance that one submission's series promotes you:

P = Σk=W..N C(N, k) × qk × (1 − q)N−k,   q = p × (1 − d)

In words: for each number of wins k that's enough, multiply the number of ways to arrange k wins among N battles by the chance of any one such arrangement, then add those up. Draws count as non-wins, which is why they're folded into q. The average number of submissions until one promotes is 1 / P.

Wood: win W of N

The left curve is the exact chance that one submission wins at least W of its N boss battles, for every win chance p from 0 to 1; the gold dot is your setting. The right panel plays 20 sampled submissions at that setting, one row of battles each, with the promoted ones outlined, and Step plays the next 20. Drag p from 0.3 to 0.9 first. At the defaults (p = 0.6, 10% draws, 3 wins needed in 5 battles) a battle is an outright win 54% of the time, a submission promotes 57.5% of the time, and you need 1.74 submissions on average; 14 of the first 20 sampled submissions promoted. At p = 0.5 the average rises to 2.46 submissions, and at p = 0.3 to 7.95.

The documentation's own advice follows from the arithmetic: every failed series costs a submission, so play test games against the boss first, on several seeds, and submit once your bot wins most of them.[17]

05How the leaderboard scores you

From Bronze up, your place depends on a score, and the score isn't the sum of the points you scored in games. It's a skill rating computed from the ranked games of your current submission, and it's final once those games reach 100%.[13] CodinGame's leaderboard labels the column "Score (TrueSkill System)".[18]

is a rating system Microsoft Research published in 2006 for Xbox Live.[19] It keeps two numbers per player: a mean μ (mu), its best guess at your skill, and an uncertainty σ (sigma), how far off that guess might be. A new player starts at μ = 25 and σ = 25/3 ≈ 8.33. After each game both players' numbers move. A win pulls μ up and a loss pulls it down, by more when σ is large and when the result was a surprise, and σ shrinks as results pile up.[19][20]

A leaderboard sorted by μ alone would put lucky newcomers at the top, so TrueSkill ranks by a conservative estimate: μ − 3σ, three standard deviations below the mean. The system is about 99.9% sure a player's true skill is above that number. A new player's conservative estimate is 25 − 3 × 8.33 = 0.[20]

Worked example

With TrueSkill's default settings, a bot with μ = 27 after a few hundred games has σ near 0.8. Its conservative estimate is 27 − 3 × 0.8 = 24.6.

The same μ after three games, with σ = 6, gives 27 − 18 = 9. Same best guess, very different place on a leaderboard sorted by μ − 3σ.

What CodinGame does with TrueSkill is documented in staff forum posts, so each claim carries its year. In 2014 a CodinGame account answering for the company wrote that a new submission entered the arena at a position "based on your TrueSkill mean for the 10 matches minus 3 times the deviation".[21] In 2016 a staff member wrote, "We are indeed using TrueSkill to both select the opponents and compute the scores," and that it ran with TrueSkill's default parameters.[22] Today's documentation calls the score a skill rating and doesn't give its formula.[13]

How one game moves μ and σ

TrueSkill models each player's performance in a game as their skill plus random noise with a fixed spread β (β = σ₀/2 ≈ 4.17 by default). After a game it asks how surprising the result was, given both players' μ and σ. An upset, a win by the player rated lower, moves both means a lot; an expected result moves them a little. Each player's step is scaled by their own σ², so a confident rating barely moves while a new one jumps.[19]

The uncertainty shrinks after every game, but before each game the system adds a little back, a dynamics term τ = σ₀/100 ≈ 0.083 (the paper calls it γ), because skills can change. The two effects balance, so after a few hundred games σ settles near a floor instead of reaching zero. That floor is why a rating keeps moving forever: the system always treats the newest games as fresh news. A draw is a result too: a draw margin, set from how often equal players draw, decides how close two performances must be to count as one.[19]

The rating moves after every game, and which games you get is luck: which opponents, on which maps, in which order. TrueSkill also weights recent games heavily. It was built for people whose skill changes, and in 2016 CodinGame staff called this its main drawback for bots, which don't change between games.[22] So the same code, submitted twice, lands in two different places.

From the author's lab

One Back Track King network bot was submitted six times without changing a byte, on the permanent Legend ladder between October 22 and October 26, 2026. The six runs finished within about 1.6 points of each other (the highest is a reading taken near its peak; the other five are final scores), with a mean 0.6 above the lowest. One submission resolves differences of about a point and no finer.[23]

Watch a skill rating settle

The left panel follows one submission's TrueSkill mean μ (purple) through its ranked games, with a shaded band of μ ± 3σ and the conservative estimate μ − 3σ in gold dashes. The right panel shows where 40 submissions of the same bot finished. Press Play, then Resubmit the same code, which replays the bot from μ = 25, σ = 8.33 against a new order of opponents. The band narrows quickly, but at the defaults the 40 identical submissions finished their 150 games anywhere from 25.59 to 27.95 (standard deviation 0.59), and at 400 games the spread is the same (0.59). σ stops near 0.8 because TrueSkill adds a little uncertainty back before every game. The ladder is a model: 40 opponents with fixed ratings, each game against one of the eight rated nearest your current μ, and results drawn from TrueSkill's own game model.

A question that comes up after a resubmission: "My last two submissions kept their rank but dropped their score. Did the bot get worse?" Not necessarily. In the lab's Back Track King runs, identical code moved by about a point from one submission to the next, and the bots around you move too, so a rank can hold while the score drops. Treat any change smaller than that band as no change.

The same band answers "Is there any chance of moving up?" If the next place is less than about a point above you, a resubmission can pass it by luck and can just as easily fall back. A gap of several points needs a real improvement. Measuring Bot Strength shows how to find out which you have before you submit.

A submission replaces your current one and starts its own ranking games, and its score comes from those games alone.[2][13] So resubmitting unchanged code doesn't refine a rating; it throws away a settled one and draws a new one.

The end of a contest adds one more step. After CodeBusters in 2016, CodinGame played an extra batch of 200 games for the Legend league, limited to its first 150 players, and averaged each player's TrueSkill score over that batch, because "TrueSkill has a short memory and the last match result can influence way too much the ranking".[24] For Spring Challenge 2022, staff wrote: "After all matches are finished playing, we'll rerun the entire Legend league."[25] Neither post is a promise about the next contest, so expect Legend places to move after the close.

06Test games and submissions

Most of what you learn during a contest comes from test games. A test game runs your current code against opponents you pick, on a map you pick, and shows you the full log. It doesn't count toward your rating, which comes only from the ranked games of your submission.[13]

You pick the opponents: the boss of your league, your own code, or the current bots of players in your league room or from your recent ranked games. You pick the map with game options such as seed=12. A ranked game you lost in your current league can be replayed against your new code with the same map, the same opponents and the same seats.[2] That replay is the fastest check that a fix fixes the game you lost.

The two kinds of log differ. A test-game log includes your bot's stderr; a ranked game's log has every bot's stdout and nobody's stderr.[2] Many games give you a message command whose text shows in the replay, which is how you tag decisions inside a ranked game.[13]

Both test games and submissions are rate-limited, and the windows depend on the contest. CodinGame gives these as typical values: 10 test games a minute and 600 a day, and 5 submissions per 5 minutes and 100 per 5 hours. Only one test game runs at a time.[2] A local copy of the referee removes those limits for everything except playing the real opponents.

Your test games and your ranked games also don't sample the same opponents. Ranked games are played against the players of your room in proportions the system chooses; test games are against whichever bots you pick. A bot that beats the boss nine times out of ten in test games still has to out-rate the whole room.

07Contests and permanent puzzles

CodinGame runs bot programming in two forms: contests, which it calls challenges and describes as "time-limited events", and bot programming puzzles, "permanent multiplayer games".[13] A contest is an event with a start and an end. Back Track King, the Summer Challenge 2026, ran from September 7 to September 21, 2026. 16,581 players registered and 1,595 finished on the final leaderboard, spread over six leagues, two of them Wood; 28 finished in Legend.[18]

When a contest ends, its game doesn't go away. Since 2020 CodinGame has moved all of a contest's players into the game's permanent multiplayer arena, in their leagues, as soon as the contest ends; before that, it released the game a few days later.[26] On October 30, 2026, CodinGame's public API listed 98 bot programming puzzles, 59 of them made by players.[27]

That answers a common surprise: "There are 98 bot programming puzzles, so why are only a few contests live?" Live contests are events, open for a couple of weeks at a time. The 98 are permanent arenas you can enter on any day, and since 2020 each contest's game has joined them when the contest ended. A permanent puzzle keeps the contest's leagues, with one difference in feel: every league is already open and the field changes slowly, while in a live contest everyone improves at once and Silver, Gold and Legend open on dates set in advance.

From the author's lab

In the Back Track King contest, the author's hand-written bot finished in Gold. Legend came after the contest, on the permanent ladder, with a self-play network; its best result there, with a one-turn search added, was near the top of the Legend table on October 25, 2026. The lab's eight other games were all climbed on their permanent puzzle ladders, outside any live contest.[23]

08One file, 100,000 characters

Everything you submit is one source file in one language: the submission API takes a language name and a single block of code.[2] A project you split into modules on your machine has to be merged into one file before it goes in; Fast Bots covers that. The file also has a size limit. The help center: "The code size for any game is limited to 100k characters."[10]

The wording makes a difference only once you go past plain ASCII, where one character is one byte under every definition. Bots that embed data, such as the weights of a neural network, may pack it into non-ASCII characters to fit more in, and the help page doesn't say what it counts. A code point is Unicode's number for a character. Most text uses code points from the Basic Multilingual Plane (BMP), which UTF-16, the encoding Java strings use, stores as one 16-bit unit each. Code points above it, such as most emoji, take two units.

The player CarlWalsh tested the limit on a classic puzzle in April 2025 and found that each character above the BMP counted twice, as two UTF-16 units.[28] The author's IDE probe in September 2026 disagrees on that point (below), and none of these tests used the Submit button.

From the author's lab

October 25, 2026, through the API's test games: a 57,361-character source that took 115,303 bytes in UTF-8 was accepted, and a 100,499-character source was refused with "Submitted code is too big. Max chars is 100000". The limit doesn't count UTF-8 bytes.

September 16, 2026, in the Summer Challenge IDE (Play, not Submit): a source of 60,966 characters, 60,000 of them above the BMP, was accepted. In UTF-16 units that's 120,966, so on that path each counted once.[23]

The safe rule: keep packed data inside the BMP, where every count agrees, and if you need more, test a padded submission on the day. Shipping a Network in 100,000 Characters packs a whole network that way. The same help page sets the memory limit at 768 MB and lists the compilers, among them g++ 11.2.0 in C++20 mode and Rust 1.90.0.[10]

09Spending a contest well

A contest is short. Back Track King ran 14 days, and in most CodinGame contests the top leagues open on fixed dates partway through.[17] What you build on which day decides how much of it you can test before the end. The series follows the order below; adapt it to your game.

  1. Day 1: a bot that reads every line and beats the Wood boss. Correct parsing and a few simple rules are enough for the first leagues. Submit as soon as it wins most test games against the boss. Your First Bot builds one.
  2. The first days: a copy of the rules. A simulator that reproduces the referee lets you test ideas offline, and every search needs one. Referees and Simulators builds it and proves it matches.
  3. The middle: an evaluation and a search. Score positions, then look ahead (Evaluation Functions and the search pages after it). Measure each change on many maps before you keep it; Measuring Bot Strength says how many is many.
  4. When Gold and Legend open: test again. A copy of a selected player's bot becomes the new boss, and the bots around you change.[17] Results against last week's opponents may not hold.
  5. The last days: freeze a safe version. Keep a tested submission live and replace it only for a measured gain. Once the contest ends, test games and submissions are closed.[2]
  6. After the close: the final ranking. Legend gets its rerun (section 05), so places move. Then the game becomes a permanent puzzle and you can keep climbing.

From the author's lab

During Back Track King, the lab built a local referee and a round-robin ladder of bot versions rated with TrueSkill and Bradley–Terry, and found that head-to-head results between versions of its bot didn't add up: a change that won 95 of 100 games against one frozen version won 348, lost 334 and drew 2 of 684 against the current version. Measuring Bot Strength explains why that breaks head-to-head tests. The contest bot finished in Gold.[23]

10A bot and a referee in code

These are complete programs that hold both sides of the toy protocol from section 01. Run with the argument bot, the program is a bot: it reads the initialization line, reads each frame in full, picks the biggest pile, logs its choice to stderr, prints TAKE <pile> and flushes. Run with no arguments, it's a referee: it starts four copies of itself as bots, sends each the same five seeded frames, enforces Back Track King's limits (1,000 ms for the first turn, 50 ms after), and checks how each game ends.

The four bot versions differ in one line each. good flushes every turn and collects all 88 coins on offer. noflush skips the flush and times out on turn 1 with its answer already printed. debug prints a debug line to stdout first and is eliminated on turn 1 for an invalid answer. slow spends 200 ms on turn 1, which the first-turn limit allows, then 80 ms on turn 2 and times out. Both bots buffer their output on purpose, the C++ one by untying cin from cout and the Rust one with a BufWriter, so the flush is the only thing that sends the move.

A bot and a local referee, complete programs
// arena.cpp: a contest bot and a tiny local referee in one program (POSIX: Linux or macOS).
// Build: g++ -std=c++20 -O2 arena.cpp -o arena
// Run:   ./arena           plays four versions of the bot and checks how each game ends
//        ./arena bot good  runs one bot on stdin/stdout, the way CodinGame runs it
#include <poll.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>
#include <vector>

using Clock = std::chrono::steady_clock;
using namespace std::chrono_literals;

constexpr int kTurns = 5;
constexpr auto kFirstTurnLimit = 1000ms;  // Back Track King's limits: 1,000 ms for a bot's first turn,
constexpr auto kTurnLimit = 50ms;         // then 50 ms for every turn after it

// ------------------------------------------------------------------ the bot
int runBot(const std::string& version) {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);  // a common speed-up; it also stops cin from flushing cout before each read
    int turnCount = 0;
    std::cin >> turnCount;  // initialization input: sent once, before the first frame
    for (int turn = 1; turn <= turnCount; ++turn) {
        int pileCount = 0;
        if (!(std::cin >> pileCount)) return 0;  // the referee closed the pipe: the game is over
        int bestPile = 0, bestCoins = -1;
        for (int pile = 0; pile < pileCount; ++pile) {  // read the whole frame before answering
            int coins = 0;
            std::cin >> coins;
            if (coins > bestCoins) { bestCoins = coins; bestPile = pile; }
        }
        std::cerr << "turn " << turn << ": pile " << bestPile << " has " << bestCoins << " coins\n";
        if (turn == 1) std::this_thread::sleep_for(200ms);  // set-up work fits the first-turn limit
        if (version == "slow" && turn == 2) std::this_thread::sleep_for(80ms);  // 30 ms too long
        if (version == "debug") std::cout << "debug: best pile " << bestPile << '\n';  // wrong stream
        std::cout << "TAKE " << bestPile << '\n';
        if (version != "noflush") std::cout.flush();  // until this call the line waits in our buffer
    }
    return 0;
}

// ------------------------------------------------------------------ the referee
struct BotProcess {
    pid_t pid;
    int toBot;            // write end of the bot's stdin
    int fromBot;          // read end of the bot's stdout
    std::string pending;  // bytes read but not yet split into lines
};

BotProcess spawnBot(const char* program, const char* version) {
    int down[2], up[2];  // down: referee to bot, up: bot to referee
    if (pipe(down) != 0 || pipe(up) != 0) { std::perror("pipe"); std::exit(1); }
    pid_t pid = fork();
    if (pid == 0) {  // child process: wire the pipes to stdin and stdout, then become the bot
        dup2(down[0], STDIN_FILENO);
        dup2(up[1], STDOUT_FILENO);
        close(down[0]); close(down[1]); close(up[0]); close(up[1]);
        execlp(program, program, "bot", version, static_cast<char*>(nullptr));
        _exit(127);  // only reached if exec failed
    }
    close(down[0]); close(up[1]);  // the referee keeps the other two ends
    return {pid, down[1], up[0], {}};
}

bool sendFrame(BotProcess& bot, const std::string& text) {
    std::size_t sent = 0;
    while (sent < text.size()) {
        ssize_t wrote = write(bot.toBot, text.data() + sent, text.size() - sent);
        if (wrote <= 0) return false;  // the bot has already exited
        sent += static_cast<std::size_t>(wrote);
    }
    return true;
}

// Waits for one whole line until the deadline. False means no line arrived in time.
bool readLine(BotProcess& bot, Clock::time_point deadline, std::string& line) {
    for (;;) {
        std::size_t newline = bot.pending.find('\n');
        if (newline != std::string::npos) {
            line = bot.pending.substr(0, newline);
            bot.pending.erase(0, newline + 1);  // later lines stay queued for later turns
            return true;
        }
        auto left = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - Clock::now());
        if (left.count() <= 0) return false;
        pollfd watch{bot.fromBot, POLLIN, 0};
        if (poll(&watch, 1, static_cast<int>(left.count())) <= 0) return false;  // nothing came
        char buffer[512];
        ssize_t got = read(bot.fromBot, buffer, sizeof buffer);
        if (got <= 0) return false;  // the bot exited or crashed without answering
        bot.pending.append(buffer, static_cast<std::size_t>(got));
    }
}

struct Outcome {
    std::string status;  // "finished", "timeout" or "invalid"
    int turn;            // the last turn played
    int coins;           // what the bot collected
    int bestPossible;    // what a perfect bot would have collected
    std::string detail;
};

Outcome playMatch(const char* program, const char* version) {
    BotProcess bot = spawnBot(program, version);
    std::uint64_t seed = 2026;  // the same seed deals the same piles in every match
    Outcome outcome{"finished", kTurns, 0, 0, "every answer on time"};
    for (int turn = 1; turn <= kTurns; ++turn) {
        int pileCount = 3 + turn % 3;  // 3 to 5 piles, so the bot has to read the count first
        std::vector<int> piles(pileCount);
        std::ostringstream frame;
        if (turn == 1) frame << kTurns << '\n';  // the initialization line goes before frame 1
        frame << pileCount << '\n';
        for (int& coins : piles) {
            seed = seed * 6364136223846793005ULL + 1442695040888963407ULL;  // a 64-bit LCG step
            coins = 1 + static_cast<int>((seed >> 33) % 20);
            frame << coins << '\n';
        }
        outcome.bestPossible += *std::max_element(piles.begin(), piles.end());
        outcome.turn = turn;
        auto limit = turn == 1 ? kFirstTurnLimit : kTurnLimit;
        std::string answer;
        bool sent = sendFrame(bot, frame.str());
        auto deadline = Clock::now() + limit;  // the clock starts once the frame is out
        if (!sent || !readLine(bot, deadline, answer)) {
            outcome.status = "timeout";
            outcome.detail = "no line within " + std::to_string(limit.count()) + " ms";
            break;
        }
        std::istringstream words(answer);
        std::string command, extra;
        int pile = -1;
        bool valid = (words >> command >> pile) && command == "TAKE" && !(words >> extra) &&
                     pile >= 0 && pile < pileCount;
        if (!valid) {  // the referee can't play a line it doesn't understand
            outcome.status = "invalid";
            outcome.detail = "\"" + answer + "\"";
            break;
        }
        outcome.coins += piles[pile];
    }
    kill(bot.pid, SIGKILL);  // harmless if the bot already exited
    waitpid(bot.pid, nullptr, 0);
    close(bot.toBot);
    close(bot.fromBot);
    return outcome;
}

int main(int argc, char** argv) {
    if (argc >= 3 && std::string(argv[1]) == "bot") return runBot(argv[2]);
    signal(SIGPIPE, SIG_IGN);  // writing to a dead bot should fail, not kill the referee
    struct Expected { const char* version; const char* status; int turn; };
    const Expected cases[] = {
        {"good", "finished", 5}, {"noflush", "timeout", 1}, {"debug", "invalid", 1}, {"slow", "timeout", 2}};
    bool allMatch = true;
    for (const Expected& expected : cases) {
        Outcome outcome = playMatch(argv[0], expected.version);
        bool perfect = outcome.status != "finished" || outcome.coins == outcome.bestPossible;
        bool match = outcome.status == expected.status && outcome.turn == expected.turn && perfect;
        allMatch = allMatch && match;
        std::printf("%-8s %-9s turn %d  coins %2d of %2d  %s\n", expected.version, outcome.status.c_str(),
                    outcome.turn, outcome.coins, outcome.bestPossible, outcome.detail.c_str());
    }
    std::puts(allMatch ? "all four outcomes match" : "MISMATCH");
    return allMatch ? 0 : 1;
}
// arena.rs: a contest bot and a tiny local referee in one program.
// Build: rustc --edition 2021 -O arena.rs
// Run:   ./arena           plays four versions of the bot and checks how each game ends
//        ./arena bot good  runs one bot on stdin/stdout, the way CodinGame runs it
use std::env;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::process::{self, Child, ChildStdin, Command, Stdio};
use std::sync::mpsc::{self, Receiver};
use std::thread;
use std::time::Duration;

const TURNS: usize = 5;
const FIRST_TURN_LIMIT: Duration = Duration::from_millis(1000); // Back Track King's limits: 1,000 ms
const TURN_LIMIT: Duration = Duration::from_millis(50); // for a bot's first turn, then 50 ms each

// ------------------------------------------------------------------ the bot
fn run_bot(version: &str) {
    let mut lines = io::stdin().lock().lines().map_while(Result::ok);
    // A common speed-up: BufWriter keeps everything until flush() (println! would send each line).
    let mut out = BufWriter::new(io::stdout().lock());
    // Initialization input: sent once, before the first frame.
    let turn_count: usize = lines.next().and_then(|line| line.trim().parse().ok()).unwrap_or(0);
    for turn in 1..=turn_count {
        let Some(header) = lines.next() else { return }; // the referee closed the pipe: game over
        let pile_count: usize = header.trim().parse().unwrap_or(0);
        let (mut best_pile, mut best_coins) = (0usize, -1i64);
        for pile in 0..pile_count {
            // Read the whole frame before answering.
            let coins: i64 = lines.next().and_then(|line| line.trim().parse().ok()).unwrap_or(0);
            if coins > best_coins {
                best_coins = coins;
                best_pile = pile;
            }
        }
        eprintln!("turn {turn}: pile {best_pile} has {best_coins} coins");
        if turn == 1 {
            thread::sleep(Duration::from_millis(200)); // set-up work fits the first-turn limit
        }
        if version == "slow" && turn == 2 {
            thread::sleep(Duration::from_millis(80)); // 30 ms too long
        }
        if version == "debug" {
            writeln!(out, "debug: best pile {best_pile}").unwrap(); // wrong stream
        }
        writeln!(out, "TAKE {best_pile}").unwrap();
        if version != "noflush" {
            out.flush().unwrap(); // until this call the line waits in our buffer
        }
    }
}

// ------------------------------------------------------------------ the referee
struct BotProcess {
    child: Child,
    to_bot: ChildStdin,         // write end of the bot's stdin
    from_bot: Receiver<String>, // whole lines from the bot's stdout, in order
}

fn spawn_bot(version: &str) -> BotProcess {
    let program = env::current_exe().expect("path of this program");
    let mut child = Command::new(program)
        .args(["bot", version])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped()) // stderr stays on the terminal, like the IDE console
        .spawn()
        .expect("start the bot");
    let to_bot = child.stdin.take().expect("bot stdin");
    let bot_stdout = child.stdout.take().expect("bot stdout");
    let (sender, from_bot) = mpsc::channel();
    // Reading a pipe blocks, so a helper thread forwards each line to a channel
    // that the referee can wait on with a timeout.
    thread::spawn(move || {
        for line in BufReader::new(bot_stdout).lines().map_while(Result::ok) {
            if sender.send(line).is_err() {
                break;
            }
        }
    });
    BotProcess { child, to_bot, from_bot }
}

struct Outcome {
    status: &'static str, // "finished", "timeout" or "invalid"
    turn: usize,          // the last turn played
    coins: i64,           // what the bot collected
    best_possible: i64,   // what a perfect bot would have collected
    detail: String,
}

fn play_match(version: &str) -> Outcome {
    let mut bot = spawn_bot(version);
    let mut seed: u64 = 2026; // the same seed deals the same piles in every match
    let mut outcome = Outcome {
        status: "finished",
        turn: TURNS,
        coins: 0,
        best_possible: 0,
        detail: "every answer on time".to_string(),
    };
    for turn in 1..=TURNS {
        let pile_count = 3 + turn % 3; // 3 to 5 piles, so the bot has to read the count first
        let mut piles = vec![0i64; pile_count];
        let mut frame = String::new();
        if turn == 1 {
            frame.push_str(&format!("{TURNS}\n")); // the initialization line goes before frame 1
        }
        frame.push_str(&format!("{pile_count}\n"));
        for coins in piles.iter_mut() {
            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); // LCG step
            *coins = 1 + ((seed >> 33) % 20) as i64;
            frame.push_str(&format!("{coins}\n"));
        }
        outcome.best_possible += piles.iter().copied().max().unwrap_or(0);
        outcome.turn = turn;
        let limit = if turn == 1 { FIRST_TURN_LIMIT } else { TURN_LIMIT };
        let sent = bot.to_bot.write_all(frame.as_bytes()).is_ok();
        // The clock starts once the frame is out. A line queued from an earlier turn arrives at once.
        let answer = if sent { bot.from_bot.recv_timeout(limit).ok() } else { None };
        let Some(answer) = answer else {
            outcome.status = "timeout";
            outcome.detail = format!("no line within {} ms", limit.as_millis());
            break;
        };
        let mut words = answer.split_whitespace();
        let command = words.next();
        let pile = words.next().and_then(|word| word.parse::<usize>().ok());
        let extra = words.next();
        let Some(pile) = pile.filter(|&pile| command == Some("TAKE") && extra.is_none() && pile < pile_count)
        else {
            outcome.status = "invalid"; // the referee can't play a line it doesn't understand
            outcome.detail = format!("\"{answer}\"");
            break;
        };
        outcome.coins += piles[pile];
    }
    let _ = bot.child.kill(); // harmless if the bot already exited
    let _ = bot.child.wait();
    outcome
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() >= 3 && args[1] == "bot" {
        run_bot(&args[2]);
        return;
    }
    // Rust ignores SIGPIPE already: writing to a dead bot returns an error instead of killing us.
    let cases = [("good", "finished", 5), ("noflush", "timeout", 1), ("debug", "invalid", 1), ("slow", "timeout", 2)];
    let mut all_match = true;
    for (version, status, turn) in cases {
        let outcome = play_match(version);
        let perfect = outcome.status != "finished" || outcome.coins == outcome.best_possible;
        let matched = outcome.status == status && outcome.turn == turn && perfect;
        all_match = all_match && matched;
        println!(
            "{:<8} {:<9} turn {}  coins {:>2} of {:>2}  {}",
            version, outcome.status, outcome.turn, outcome.coins, outcome.best_possible, outcome.detail
        );
    }
    println!("{}", if all_match { "all four outcomes match" } else { "MISMATCH" });
    process::exit(if all_match { 0 } else { 1 });
}

Run without arguments (with the bots' stderr hidden), both versions print the same table and exit with status 0:

$ ./arena 2>/dev/null
good     finished  turn 5  coins 88 of 88  every answer on time
noflush  timeout   turn 1  coins  0 of 17  no line within 1000 ms
debug    invalid   turn 1  coins  0 of 17  "debug: best pile 1"
slow     timeout   turn 2  coins 17 of 35  no line within 50 ms
all four outcomes match

Delete the std::cin.tie(nullptr) line, or swap the Rust BufWriter for a plain stdout lock, and the noflush bot finishes all five turns: section 03's safety nets at work.

What's intentionally missing

The listing leaves out a second bot playing at the same time, and any real game rules. It doesn't copy CodinGame's way of timing a bot, which isn't public: this referee starts a wall clock once the frame is written. It has no grace for small overruns, no memory limit, and no logs or replays, so the bots' stderr goes straight to your terminal. The C++ referee uses the POSIX calls fork, pipe and poll, so it builds on Linux and macOS but not on Windows.

11Ways to lose a game you were winning

Answering before reading the whole frameUnread lines become next turn's input, and every read after that is shifted. Read every line every turn, even after you've decided.
Not flushingThe move waits in your buffer while the referee waits for it. Flush after every answer, and always if you've untied cin or wrapped stdout in a buffer.
Heavy set-up on turn 2Precomputation belongs on your first turn, which usually gets 1,000 ms. Every later turn may get 50.
Debug output on stdoutThe referee reads it as your move. Debug goes to stderr; use the game's message command to tag ranked replays.
Trusting one submission's scoreIdentical code spread over about a point and a half in the author's lab. A difference smaller than that between two submissions is noise.
Treating test opponents as ranking opponentsTest games are against bots you pick; the rating comes from your whole room. Beating the boss in test games doesn't promote you.

12What's next

Your First Bot puts this protocol to work on Mad Pod Racing: reading a statement as a spec, parsing each turn into a state struct, writing rules in priority order, fixing the desync bug from section 01, and measuring a change by lap time instead of by eye.

13Sources

Mighty Professional Gaming is not affiliated with CodinGame. Platform facts on this page come from CodinGame's documentation, its game engine's source, public referees and dated staff posts. Numbers from the author's lab are first-hand and marked as such. Widget readouts come from the models described in their captions; they aren't measurements of CodinGame's servers.

  1. CodinGame Help Center, last changed 2021. How does it work? (source file code.md). Input on stdin, answers on stdout, "Read stdin, Flush stdout", and stderr for debugging.
  2. CodinGame, 2026. CodinGame API reference (retrieved 2026-10-30). Test-game opponents and game options, replays, stderr only in test games, rate limits, submissions as one block of code that replaces the current one, and the closed state after a contest.
  3. CodinGame Help Center, last changed 2021. Turn-based Coding Games. Reading all input each turn, and the desynchronization and timeout that follow when a bot doesn't.
  4. CodinGame, 2026. CodinGame Game Engine source, version 4.7.8 (AbstractReferee, GameManager, AbstractPlayer, MultiplayerGameManager). The referee's methods, expected output lines, the first-turn limit, seeded randomness and the two-overrun grace.
  5. CodinGame. CodinGame SDK Documentation: Game Manager. sendInputLine(), execute(), getOutputs() and the TimeoutException.
  6. CodinGame, 2025. Ultimate Tic-Tac-Toe referee and statement. Limits of 1,000 ms for each bot's first move and 100 ms after; a score of −1 for a timeout or invalid action.
  7. CodinGame, 2022. Spring Challenge 2022 referee and statement. 1,000 ms for the first turn and 50 ms per turn.
  8. eulerscheZahl, 2026. Troll Farm, the official referee for Spring Challenge 2026. 50 ms per turn, the statement's three-overrun rule, and a score of −2 for an eliminated player.
  9. CGjupoulton, 2026. Summer Challenge 2026 (Back Track King) referee and statement. 1,000 ms for the first turn, 50 ms after, built on engine 4.7.8.
  10. CodinGame Help Center, last changed 2025. Languages Versions (source file languages.md). Time limits set per game in the statement, the 100k-character limit, 768 MB of memory, g++ 11.2.0 in C++20 mode and Rust 1.90.0.
  11. CodinGame. Mad Pod Racing statement, starting league (retrieved 2026-10-30). 1,000 ms for the first turn and 75 ms per turn.
  12. CodinGame. Tron Battle statement (retrieved 2026-10-30). "Your bot must answer in less than 100ms for each game turn"; no separate first-turn limit.
  13. CodinGame, 2026. Agent Skill: CodinGame (retrieved 2026-10-30). The conventions: elimination usually at −1, a longer first turn, reading the whole input and flushing, the score as a skill rating final at 100%, message commands, and contests versus permanent puzzles.
  14. cppreference.com. std::cin, std::wcin. std::cin.tie() returns &std::cout, so input on cin flushes cout first.
  15. The Rust Project. std::io::Stdout. Standard output is line-buffered when connected to a terminal.
  16. The Rust Project. std::io::BufWriter. Buffered output, and the advice to call flush explicitly.
  17. CodinGame, 2026. Leagues and promotion (retrieved 2026-10-30). Leagues, bosses and rooms, Wood boss battles, ranking above the boss, promotion runs, new rules, and league openings in public contests.
  18. CodinGame, 2026. Summer Challenge 2026 (Back Track King) contest page and final leaderboard (retrieved 2026-10-30). Dates, 16,581 registrations, 1,595 ranked players, six leagues, 28 in Legend, and the "Score (TrueSkill System)" column label.
  19. Ralf Herbrich, Tom Minka and Thore Graepel, 2006. TrueSkill: A Bayesian Skill Rating System. Advances in Neural Information Processing Systems 19. Skill as a mean and variance, the per-game update with draws, the dynamics term, and Xbox Live's μ − 3σ display.
  20. Microsoft Research. TrueSkill Ranking System. The conservative skill estimate μ − kσ with k = 3, and a new player's 25 − 3 × 8.333 = 0.
  21. CodinGame (account CodinGame65), 2014-11-26. Multiplayer Contests Ranking System, post 6. Placement by TrueSkill mean minus three deviations, in 2014.
  22. _CG_Maxime (CodinGame staff), 2016-05-09 and 2016-05-11. Smash The Code: question about the final ranking, posts 10 and 30. TrueSkill selects opponents and computes scores, weights recent games, and ran with default parameters, in 2016.
  23. Author's lab records, 2026 (private repository). Six runs of identical code: experiments/live-submissions/summer-challenge-2026-back-track-king/submitted.jsonl, evaluations-20261024.json and AGENTS.md (October 26 entry). Contest and ladder results: docs/provenance.md, docs/postmortem-lessons.md and each game's submitted.jsonl. Size probes: experiments/runs/unicode-check-20261025/record.json and the contest repository's DISCORD_REVIEW.md and tools/utf16_probe.py. Contest ladder and the 95–5 versus 348–334–2 result: PANEL_LADDER.md and IMPROVEMENT_REVIEW_20260916.md.
  24. CodinGame (account CodinGame65), 2016-06-28. CodeBusters Final Ranking, post 1. The 200-game Legend batch for up to 150 players, with TrueSkill scores averaged.
  25. TwoSteps (CodinGame community manager), 2022-05-02. Spring Challenge 2022: Feedbacks & Strategies, post 1. "We'll rerun the entire Legend league."
  26. [CG]Thibaud, 2020-02-19. Competitive CodinGame Is Back with a Bang (CodinGame blog, Internet Archive copy). Contest players moved into the permanent multiplayer arena, in their leagues, when a contest ends.
  27. CodinGame, 2026. Public API puzzle list, bot programming (retrieved 2026-10-30). 98 bot programming puzzles, 59 of them marked as community-made.
  28. CarlWalsh, 2025-04-08. Official documentation for "100k character limit"?, CodinGame forum, post 1. A player's test on a classic puzzle: the limit behaved like 100,000 UTF-16 code units.