Interactor
Before reading this chapter, please make sure you have read the Checker chapter and are familiar with the basic usage of cplib::var::Var
. This chapter will not repeat the introduction to this part.
The interactor is used to interact with the standard input/output of the participant's program in "stdio"-interactive problems.
In CPLib, interactor performs both "interacting with the participant's program" and "checking whether the answer is correct", instead of having two separate programs like Testlib or Polygon's design.
Interactor usually needs to deal with two input streams:
- Test input file (
inf
): Contains information about the test. Note: In interactive problems, this file is not equivalent to the standard input of the participant's program. - User input stream (
from_user
): Connects to the standard output of the participant's program, used to read the output from participant's program.
And one output stream:
- User output stream (
to_user
): Used to send data to the standard input of the participant's program.
The input stream is wrapped using cplib::var::Reader
, and the output stream is wrapped using std::ostream
.
The interaction process of CPLib interactor is roughly as follows:
- Interactor reads the test point information from the test point input file (
inf
) and extracts the part that can be known to the participant's program, then sends it to the standard input of the participant's program through the user output stream (to_user
) according to the given format. - The participant's program sends several operations to interactor, which interactor reads and parses through the user input stream (
from_user
). The return value of the operation is sent to the standard input of the participant's program through the user output stream (to_user
). In some problems, an error code should also be sent to indicate whether the participant's program should continue to interact or exit the program. - Use the
quit*
function series to exit the program and return the result of this interaction.
The related code for this section can be found on the GitHub repository (opens in a new tab).
Let's start with a simple problem to explain how to write and use the interactor.
Problem Description: Given an integer (), the interaction library will select an integer in , and you should write a program to guess it. You can perform two operations:
Q x
: ask whether the integer is greater than, less than, or equal to . The interaction library returns a character indicating the answer:>
if ,=
if ,<
if . This operation cannot be called more than times;A x
: report that the value of is , this operation has no return value, and the program must exit immediately after executing this operation.
Implementation
First, start with an empty working directory, copy cplib.hpp
into the working directory, and then create a file named intr.cpp
with the following code:
#include "cplib.hpp"
using namespace cplib;
CPLIB_REGISTER_INTERACTOR(intr);
struct Input {
int n, m;
static Input read(var::Reader& in) {
auto [n, m] = in(var::i32("n"), var::i32("A"));
return {n, m};
}
};
struct Query {
int x;
static Query read(var::Reader& in, const Input& input) {
auto x = in.read(var::i32("x", 1, input.n));
return {x};
}
};
struct Answer {
int x;
static Answer read(var::Reader& in, const Input& input) {
auto x = in.read(var::i32("x", 1, input.n));
return {x};
}
};
struct Operate : std::variant<Query, Answer> {
static Operate read(var::Reader& in, const Input& input) {
auto op = in.read(var::String("type", Pattern("[QA]")));
if (op == "Q") {
return {in.read(var::ExtVar<Query>("Q", input))};
} else {
return {in.read(var::ExtVar<Answer>("A", input))};
}
}
};
void interactor_main() {
auto input = intr.inf.read(var::ExtVar<Input>("input"));
intr.to_user << input.n << "\n";
int use_cnt = 0;
while (true) {
auto op = intr.from_user.read(var::ExtVar<Operate>("operate", input));
if (op.index() == 0) {
const auto& Q = std::get<0>(op);
if (use_cnt >= 50) intr.quit_wa("Too many queries");
if (Q.x > input.m)
intr.to_user << ">\n";
else if (Q.x == input.m)
intr.to_user << "=\n";
else
intr.to_user << "<\n";
++use_cnt;
} else {
const auto& A = std::get<1>(op);
if (A.x == input.m)
intr.quit_ac();
else
intr.quit_wa(format("Expected %d, got %d", input.m, A.x));
}
}
}
As you can see, the general structure of interactor is similar to that of checker, and it will not be explained in detail in this chapter.
For convenience in code writing, the to_user
stream adopts a design without a buffer area, and all data output to this stream will be sent to the standard input of the contestant program immediately. Therefore, the syntax for flushing the buffer area of the to_user
stream, such as intr.to_user.flush()
, is not used in the code.
Usage
Switch to the working directory and compile intr.cpp
into an executable file. Store the test point input file in data/0.in
. Then run the executable file in the terminal to simulate the interaction between the contestant program and the interactor through standard input/output.
For data with a large amount of interaction, manually simulating interaction will be very time-consuming. At this time, we can write a script to automatically connect the input/output of the contestant program and interactor to test the program.
This tutorial uses Python 3 scripts for demonstration. Create a file run.py
in the working directory, and then enter the following code:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import subprocess
import threading
BIN_INTR = "./intr"
BIN_STD = "./std"
class SubprocessThread(threading.Thread):
def __init__(self, args, stdin_pipe, stdout_pipe, stderr_pipe):
threading.Thread.__init__(self)
self.p = subprocess.Popen(args, stdin=stdin_pipe, stdout=stdout_pipe, stderr=stderr_pipe)
def run(self):
self.p.wait()
def main():
inf = sys.argv[1]
t_sol = SubprocessThread([BIN_STD], subprocess.PIPE, subprocess.PIPE, subprocess.DEVNULL)
t_judge = SubprocessThread([BIN_INTR, inf], t_sol.p.stdout, t_sol.p.stdin, sys.stderr)
t_sol.start()
t_judge.start()
t_sol.join()
t_judge.join()
if __name__ == "__main__":
main()
The values of the constants BIN_INTR
and BIN_STD
in the code are the commands required to execute interactor and the contestant program, usually the relative path of the executable file obtained by compiling interactor and the contestant program. For example, on Windows, it should be .\intr.exe
and .\std.exe
.
Then execute the command python3 run.py data/0.in
in the terminal (on Windows, you may need to replace the slash /
in the path with a backslash \
) to get the output result of interactor.
Suggestions
In summary, when writing more complex interactors, we recommend the following suggestions:
- Use
std::variant
to describe different operations of the contestant. - Use
intr.from_user
andintr.to_user
to communicate with the contestant program in any case. Do not usestd::cin
/std::cout
or C-style input functions to read fromstdin
/stdout
for input and output. These functions usually do not have the error handling mechanism required by interactor, and using these functions will also destroy the internal state ofintr.from_user
andintr.to_user
, leading to unpredictable behavior. - And all the suggestions mentioned in the chapter Checker.