Which library. A new interactor is written with eolymp.h, as on this page. If the problemalready has one, read it first with get_interactor: one that includes testlib.h or callsregisterInteraction is changed in testlib — followWrite an interactor with testlib.h — and so is a newone the requester explicitly asked to be written with testlib. The interactor and thechecker are a pair: an eolymp.h interactor needs the eolymp.h checker in §6, and a testlibinteractor keeps its testlib checker. The rule, and why, is ineolymp.h reference.
A program that talks to the contestant's process over a pipe, in real time. It reads thetest, answers the solution's requests, enforces the rules, and decides how well the solutiondid.
its stdout ──────────────► its stdin
solution interactor ◄── the test input
its stdin ◄────────────── its stdout ──► a summary for the checkerstream | holds | a failed read is |
|---|---|---|
| the test | a jury error |
| the answer file, when the test has one ( | a jury error |
| what the solution prints, live | a wrong answer |
The problem's type must be INTERACTIVE, and the interactor is set withupdate_interactor.
The judge looks at the solution first: if it crashed or exceeded a limit, that is theverdict whatever the interactor did. Otherwise the interactor's exit code decides:
exit | result |
|---|---|
0 | the checker runs and grades the summary |
1 | WRONG_ANSWER; the checker does not run |
anything else | the interaction failed — a system error, and the submission lands in FAILURE |
An interactor cannot hand out points through its exit code. It writes a summary, and thechecker turns that into points (§6). The library writes the summary for you.
Guess a hidden number between 1 and n in at most 20 questions: the solution prints ? xand learns <, > or =; it answers with ! x.
#include <eolymp.h>
int main(int argc, char** argv) {
eo::interactor it(argc, argv);
int n = it.input.read_int(1, 1000000, "n");
int secret = it.input.read_int(1, n, "secret");
eo::budget queries(it, 20, "queries");
it.send(n);
for (;;) {
std::string command = it.contestant.read_choice({"?", "!"}, "command");
int x = it.contestant.read_int(1, n, "x");
if (command == "!") {
if (x != secret) eo::wrong("answered {}, the number was {}", x, secret);
eo::accept("{} queries", queries.used());
}
queries.spend();
it.send(x < secret ? "<" : x > secret ? ">" : "=");
}
}And its checker, the same one line for every problem whose interactor uses eolymp.h:
#include <eolymp.h>
int main(int argc, char** argv) { eo::checker(argc, argv).from_interactor(); }Runtime: both are stored as cpp:20-gnu14 with no files entry — the header is in thejudge's runtime. The checker's type is PROGRAM.
it.send(values…) writes one line to the solution: the values separated by single spaces,then a line break. A container is written as its elements.
Output is flushed automatically, right before the interactor waits for the solution. Theclassic deadlock — a reply sitting in a buffer while both programs wait for each other — cannothappen, and several replies between two reads cost one system call. it.flush() exists but israrely needed.
Never print with std::cout or printf. stdout is the pipe to the solution: anythingprinted there is data the solution reads. Log with eo::log("…", args), which goes to theinteractor's log.
A solution that stopped reading makes a write fail; that is a wrong answer, "thesolution stopped reading". SIGPIPE is ignored, so the interactor is never killed by it.
A reply sent immediately before the verdict — "correct" after the final answer — isdelivered, written without waiting so a solution that has already exited cannot hang theinteractor. The safe protocol is still one where the solution does not have to readanything after sending its final answer.
it.contestant is the solution's live output — arbitrary bytes from a process that may bebuggy, adversarial, or already dead. A read waits until the solution has written enough;if the solution ends first, the read is a wrong answer, "the solution ended the dialogueearly". A solution that goes silent without ending is stopped by its own time limit.
Bound everything it sends. An interactor that reads a count and loops that many timeshangs on 2³¹−1; one that uses a value as an index crashes on a negative — both a systemfailure where a wrong answer belonged. A bounded read turns them into a wrong answer on thespot: the solution, line 1, x: 5000 is above 100.
Read commands with read_choice, not a string compared by hand — garbage then gets averdict instead of falling through an if/else chain.
A failed read is already the verdict. You do not need a check after it.
There is no readEof trap. Reading past the final answer is not required, and thelibrary does not fail a solution for a trailing line break.
eo::budget queries(it, 20, "queries"); queries.spend();
spend() counts one question (spend(k) counts k) and ends the run withwrong answer more than 20 queries when the budget is exceeded. Spend before replying, sothe question over the limit never gets a truthful answer. queries.used() andqueries.left() read it back; several budgets can coexist.
The statement saying "at most 20 queries" is not enforcement — this is. It is the check mostoften lost when a CMS function-call grader, which enforced the budget in-process, is ported.
Decide whether exceeding the limit is a wrong answer or a scored zero. For a problemscored by the number of queries, it is usually a score (§6), not eo::wrong.
If the problem promises an adaptive interactor, it must be genuinely adaptive: consistentwith every answer still compatible with the queries so far, not secretly committed to onevalue.
call in the interactor | exit | the checker awards |
|---|---|---|
| 0 | the test's full cost |
| 0 |
|
| 1 | nothing: the checker does not run |
| 3 | nothing: the interaction failed |
eo::score takes a fraction of the test, exactly as in a checker — eo::ratio(a, b) for"a out of b", eo::round_to(d) for a statement that rounds:
if (asked <= 10) eo::accept("{} queries", asked);
eo::score(eo::ratio(10, asked), "{} queries", asked);The stock checker — eo::checker(argc, argv).from_interactor() — reads the summary and awardsexactly that. Every eolymp.h interactor needs it, pass/fail ones included: it is whatreads the summary the interactor writes.
When the score depends on something only the checker knows, such as the subtask, record anumber in the interactor with it.value("quality", q) and map it in the checker:
#include <eolymp.h>
int main(int argc, char** argv) {
eo::checker c(argc, argv);
c.from_interactor([&](eo::summary const& said) {
return c.group() == 3 ? said.value("quality") : said.fraction();
});
}said.fraction() is what the interactor scored, said.value(name) a recorded number,said.has(name) whether it was recorded.
The testset decides whether a partial score pays, as for any checker: a scored subtask isWORST with the subtask's value on every test; ALL pays nothing for a partial score, and apartial score on a test worth 0 is recorded ACCEPTED(Design a testset).
Set a wall timeLimit, and set it generously. The interactor's own time is the solution'swall limit plus one second, and every round trip costs wall time. Make cpuLimit the reallimit for the solution — the pipe's system calls are billed to the solution's CPU time.
Eolymp runs the interactor as a separate process, so every query is a pipe round trip:roughly 150,000 round trips a second. Olympiads that link their grader into thecontestant's binary have no such cost and size their tests accordingly — CEOI 2026 "TreasureHunt" ships about 22 million queries in its largest file. Before porting such a problem,compute:
units × queries-per-unit ÷ 150,000 > time limit ?
If it does, the lever that does not change the problem is fewer units per test, taken asan even spread rather than a prefix. The interactor's log ends with the round-trip count(10 round trips, 23 bytes sent), which is the number to read.
The hidden state comes from the test. An interactor that needs randomness — to shuffle, or toadapt — draws from it.rng(), seeded from the test, so the same solution gets the samedialogue and the same verdict on every rejudge: uniform(low, high), chance(p),pick(container), shuffle(values), perm(n, 1). No rand(), no std::mt19937, noclock. An adaptive interactor's choices are a deterministic function of the test and thequery history.
Some tasks run the solution several times per test with nothing remembered in between —an encoder and a decoder, Alice and Bob. On Eolymp that is run_count in the testing config:with run_count set to k the solution runs k times, each a fresh process in a cleandirectory, and run i+1's input is run i's interactor output — the only channel between theruns, and only the jury writes to it.
#include <eolymp.h>
int main(int argc, char** argv) {
eo::interactor it(argc, argv);
eo::phases ph(it, 2);
long long x = it.input.read_long(0, 1000000000, "x");
if (ph.number() == 1) {
it.send("alice", x);
std::string code = it.contestant.read_token(1, 60, eo::charset("01"), "code");
ph.handoff([&](eo::writer& w) { w.line(code); });
}
std::string code = ph.previous().read_token(1, 60, eo::charset("01"), "code");
it.send("bob", code);
long long said = it.contestant.read_long(eo::any, "x");
if (said != x) eo::wrong("Bob answered {}, the number was {}", said, x);
if (code.size() <= 30) eo::accept("{} characters", code.size());
eo::score(eo::ratio(30, static_cast<long long>(code.size())), "{} characters", code.size());
}ph.number() is the current phase, from 1. ph.handoff(write) writes what the next phasereceives and ends this run, so the rest of main only runs in the last phase.
ph.previous() reads what the previous phase wrote; it.input is the original test inevery phase.
Set run_count to k and interactive_followup to true, and send the problem type inthe same update. A run_count that does not match the phases is reported as a jury errornaming the fix, rather than scoring noise.
An early phase that must end the test with a partial score calls ph.finish(fraction).
BLOCKER breaks judging · MAJOR hides defects · MINOR style.
[ ] BLOCKER eo::interactor it(argc, argv), #include <eolymp.h> first, runtime cpp:20-gnu14
[ ] BLOCKER the checker is eo::checker(argc, argv).from_interactor() (or a mapping over it), type PROGRAM
[ ] BLOCKER nothing printed with cout / printf; replies go through it.send
[ ] BLOCKER the query limit is an eo::budget, spent before the reply
[ ] MAJOR commands read with read_choice, so garbage gets a verdict
[ ] BLOCKER every read from it.contestant producing a count or index is bounded
[ ] BLOCKER no container sized or array indexed from an unchecked value
[ ] MAJOR a reply cannot leak information beyond the stated protocol
[ ] MAJOR a scored problem ends with eo::score(fraction), not points
[ ] BLOCKER scored testsets are WORST with the value on every test
[ ] MAJOR full marks, partial and zero all reachable and verified on the judge
[ ] BLOCKER no randomness except it.rng(), no clock
[ ] MINOR the interactor's log read back, and its warnings understood (the codes)
[ ] MAJOR an "adaptive" interactor is genuinely adaptive
[ ] MAJOR a generous wall timeLimit; total round trips ÷ 150,000 comfortably under it
An interactor needs a live partner, and the platform wires the pipes, so the real tests aresubmissions:
Submit the model solution. It must score full marks.
Submit deliberately broken solutions and confirm each verdict:
one that exceeds the query limit → more than … queries, not a time limit
one that answers wrongly → a wrong answer
one that prints garbage → a wrong answer naming the value, not a hang
one that exits immediately → "ended the dialogue early", not a hang
Time the largest test with the model solution and compare against §7.
The hang cases are the ones worth the effort: a checker bug produces a wrong verdict, but aninteractor bug produces a hung judge, and hangs are found only by trying them.