♣️

競技プログラミングの作問支援ツール kuroe を開発しました

に公開

kuroe

kuroe は Rust で実装された軽量かつステートレスな競技プログラミング作問支援ツールです。

kuroe は,既存の作問支援ツールの Rime よりもラフに使えるツールを目指して開発されました。
Rime の置き換えを目指しているわけではなく,あくまでもユースケースの切り出しです。

  • 想定ユースケース
    • 思いついた問題を試しに作ってみる
    • yukicoder 用に一問だけ作成する
  • 非想定ユースケース
    • CI による自動テスト
    • チームによる作問

機能

  • ジェネレータコードを用いたテストケースの生成(generate)
  • 検証器コードを用いたテストケースの検証(validate)
  • 想定解コードを用いたテストケースの解答生成(solve)
  • ジャッジ(judge)

詳細は README.md をご覧ください。

インストール

cargo でインストールできます。

cargo install kuroe

ユースケース:A+B problem

2つの整数 A, B を入力として,その和 A+B を出力する問題を例に kuroe で作問作業をしてみます。

ディレクトリの作成

ディレクトリを作成して,testlib.h をダウンロードします。

mkdir aplusb
cd aplusb
wget https://raw.githubusercontent.com/MikeMirzayanov/testlib/refs/heads/master/testlib.h # testlib.h

テストケースの作成

testlib.h を用いたジェネレータを実装します。

generator.cpp
#include "testlib.h"
#include <iostream>

int main(int argc, char *argv[]) {
  registerGen(argc, argv, 1);

  std::cout << rnd.next(1, 100) << " ";
  std::cout << rnd.next(1, 100) << std::endl;
  return 0;
}

kuroe によってテストケースを 3 個生成します。

$ kuroe generate generator.cpp -n 3
[Generate] ████████████████████    3/3
+--------+--------------------------------------+-----------------+
| status | generated_case                       | from            |
+--------+--------------------------------------+-----------------+
| OK     | "./testcases/input/generator_000.in" | "generator.cpp" |
+--------+--------------------------------------+-----------------+
| OK     | "./testcases/input/generator_001.in" | "generator.cpp" |
+--------+--------------------------------------+-----------------+
| OK     | "./testcases/input/generator_002.in" | "generator.cpp" |
+--------+--------------------------------------+-----------------+

テストケースの検証

testlib.h を用いた検証器を実装します。

validator.cpp
#include "testlib.h"

int main(int argc, char *argv[]) {
  registerValidation(argc, argv);

  inf.readInt(1, 100); // A
  inf.readSpace();
  inf.readInt(1, 100); // B
  inf.readEoln();
  inf.readEof();
  return 0;
}

kuroe によってテストケースを検証します。

$ kuroe validate validator.cpp -q
["validator.cpp"] ████████████████████    3/3
+--------+--------------------------------------+
| status | target                               |
+--------+--------------------------------------+
| OK     | "./testcases/input/generator_000.in" |
+--------+--------------------------------------+
| OK     | "./testcases/input/generator_001.in" |
+--------+--------------------------------------+
| OK     | "./testcases/input/generator_002.in" |
+--------+--------------------------------------+

想定解の作成

correct.cpp
#include <iostream>

int main() {
    int a, b;
    std::cin >> a >> b;
    std::cout << a + b << std::endl;
    return 0;
}

kuroe によってテストケースの解答を作成します。

$ kuroe solve correct.cpp 
[Solve] ████████████████████    3/3
+--------+--------------------------------------+----------------------------------------+
| status | input                                | generated_answer                       |
+--------+--------------------------------------+----------------------------------------+
| OK     | "./testcases/input/generator_000.in" | "./testcases/answer/generator_000.ans" |
+--------+--------------------------------------+----------------------------------------+
| OK     | "./testcases/input/generator_001.in" | "./testcases/answer/generator_001.ans" |
+--------+--------------------------------------+----------------------------------------+
| OK     | "./testcases/input/generator_002.in" | "./testcases/answer/generator_002.ans" |
+--------+--------------------------------------+----------------------------------------+

ジャッジ

AC/WA/TLE を出すコードを作成してみます。

idiot.cpp
#include <chrono>
#include <iostream>
#include <thread>

int main() {
  int a, b;
  std::cin >> a >> b;

  const int sum = a + b;
  if (sum == 107) {
    std::cout << -1 << std::endl;
  } else if (sum < 100) {
    std::cout << sum << std::endl;
  } else {
    std::this_thread::sleep_for(std::chrono::seconds(10));
  }
  return 0;
}

kuroe によってジャッジします。

$ kuroe judge idiot.cpp 
[SOLVE "idiot.cpp"] ████████████████████    3/3
[JUDGE "idiot.cpp"] ████████████████████    3/3
+--------+----------------------------------------+----------------------------------------------+
| status | input_and_answer                       | info                                         |
+--------+----------------------------------------+----------------------------------------------+
| WA     | "./testcases/input/generator_000.in"   | "./testcases/output/idiot/generator_000.out" |
|        | "./testcases/answer/generator_000.ans" |                                              |
+--------+----------------------------------------+----------------------------------------------+
| AC     | "./testcases/input/generator_001.in"   | time = 9.0866ms                              |
|        | "./testcases/answer/generator_001.ans" |                                              |
+--------+----------------------------------------+----------------------------------------------+
| TLE    | "./testcases/input/generator_002.in"   |                                              |
|        | "./testcases/answer/generator_002.ans" |                                              |
+--------+----------------------------------------+----------------------------------------------+

kuroe の特徴

kuroe には以下のような特徴があります。

ステートレス

kuroe では,設定ファイル等を利用しないステートレスなコマンドを実現しています。
設定ファイル等が存在しないため,対象のコード等をコマンドライン引数で指定するだけで動作します。
一方で Rime における設定ファイルのような詳細なジャッジ設定はできません(オプションによって一部対応)。

カスタム言語

kuroe は,デフォルトで C(gcc), C++(g++), Python, Txt(.in or .txt) に対応しています。
この他の言語を使用したい場合や,コンパイラやオプション等を変更したい場合には,カスタム言語を使用することが可能です。

例:g++ のオプションを変更する

-l, --language オプションによってカスタム言語を指定できます。
オプションには,「対象拡張子」「コンパイルコマンド(0 行以上)」「実行コマンド(1 行)」の三種類を , で区切り指定します。

kuroe judge idiot.cpp -l "(cpp|cc)","g++ -O3 -std=c++20 %(target)","./a.out"

blog

kuroe は以下のような動機で開発しました。

  • 手軽に動かせる cli 作問ツールが欲しい
  • github の見た目のために public なプロジェクトを作りたい
  • Rust でなにか開発したい

開発にあたっては terry-u16 さんの pahcer をかなり参考にしました。ありがとうございます。

Rust での開発は初めてでしたが,rust-analyzer が手元だとかなり重いことを除けば,かなり快適な開発経験でした。型に苦しめられるというイメージを持っていましたが,特に苦しむことなく型の恩恵を受けることができたと感じています。

kuroe は,比較的短期間で集中的に開発を進めました。
本記事の公開以降は,デバッグや新機能開発等を暇のできたときに進める感じになると思います。

Q&A

複数のジェネレータ・検証器・ソルバでジャッジする

複数のコードや,ディレクトリを指定可能です。
-r オプションにより再帰的な探索にも対応しています。

$ kuroe generate ./generators/ # ./generators 直下のコードで生成
$ kuroe generate -r ./generators/ # ./generators 以下の全コードで生成(再帰的に探索)
$ kuroe validate ./validators/
$ kuroe judge solver.cpp solver.py solver.c

ジェネレータごとに生成個数を変更する

ファイル名による生成個数の指定が可能です。
例えば generator.5.cpp とすれば常に 5 個生成されます。
オプションで個数が指定された場合は,ファイル名の指定が優先されます。

kuroe generate generator.5.cpp -n 3 # 5 個生成される

TL を指定してジャッジする

--tl, --timelimit オプションにより実行時間制限を指定可能です(秒)。

kuroe judge idiot.cpp --tl 20 # TL 20 秒

TLE 時にジャッジを打ち切る

-p, --policy オプションにより可能です。

kuroe judge idiot.cpp -p tle-break

テストケースを yukicoder へアップロードする

kuroe はデフォルトで ./testcases/input に入力を生成します。これをアップロードしてください。

ただし,yukicoder では入力ケースと解答ケースのファイル名を一致させる必要があるため,kuroe で作成した解答を直接利用することはできません。
yukicoder 側の機能で解答を作成するか,手元で拡張子を変更してください。

出力先を変更する

-o, --outdir オプションにより変更可能です。

kuroe generate generator.cpp -o ./other

テストケースを指定する

solve, validate, judge コマンドでは,テストケースディレクトリは ./testcases をデフォルトにしています。
テストケースは -t オプションにより変更可能です。

kuroe solve correct.cpp -t ./other -o ./other

ジャッジの仕様

指定された testcase のうち,.in と .ans が揃っているケースを valid なケースとみなし,ジャッジ用テストケースに含めます。

複数のカスタム言語

現状不可能です。
将来的に実装される可能性があります。

並列実行

未対応です。
将来的に実装される可能性があります。

バグの報告や改善の提案

Github の Issue にお願いします。
可能な範囲で対応します。

kuroe?

a tool for creating KyoUpRO problEm

Discussion