Interactors

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.

1. What an interactor is

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 checker

stream

holds

a failed read is

it.input

the test

a jury error

it.jury

the answer file, when the test has one (it.has_jury())

a jury error

it.contestant

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.


2. The skeleton

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.


3. Sending, and why there is no flush rule

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.


4. Never trust the solution's stream

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.


5. Query limits

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.


6. Ending, scoring, and the checker

call in the interactor

exit

the checker awards

eo::accept("…", args)

0

the test's full cost

eo::score(f, "…", args)

0

f × cost — PARTIALLY_CORRECT below 1, ACCEPTED at 1

eo::wrong("…", args)

1

nothing: the checker does not run

eo::jury_error("…", args)

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).


7. Time limits and throughput

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.


8. Determinism

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.


9. Solutions that run in phases

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).


10. Review checklist

BLOCKER breaks judging · MAJOR hides defects · MINOR style.

Protocol

  • [ ] 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

Trusting the solution

  • [ ] 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

Scoring

  • [ ] 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

Determinism and limits

  • [ ] 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


11. How to test an interactor

An interactor needs a live partner, and the platform wires the pipes, so the real tests aresubmissions:

  1. Submit the model solution. It must score full marks.

  2. 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

  3. 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.