Skip to content

Instantly share code, notes, and snippets.

@tomerun
Last active July 25, 2022 15:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomerun/c11b3f0bfae93b7aacf49624b309c23b to your computer and use it in GitHub Desktop.
Save tomerun/c11b3f0bfae93b7aacf49624b309c23b to your computer and use it in GitHub Desktop.

石油王Xの憂鬱 テスター

  • これは RCO presents 日本橋ハーフマラソン 本戦問題A - 石油王Xの憂鬱 のテストを行うプログラムです。
  • これを用いることで、ローカル環境で回答プログラムを実行し、スコアを確認できます。
  • このプログラム上で計算された得点は、当コンテストでの得点ではありません。また、このプログラム上で計算された得点は、当コンテストでの得点を保証するものではありません。
  • このプログラムを使用することによるあらゆる損害は保障しかねますので、予めご了承ください。
  • このプログラムに関する質問は受け付けていません。予めご了承ください。
  • このプログラムの一部を、コンテストの解答に流用してもかまいません。

コンパイル

Tester.java を設置したディレクトリで、以下のコマンドを実行してください。

javac Tester.java

実行

コンパイル後、 -command オプションにあなたの回答プログラムを実行するコマンドを渡してテスターを実行すると、テスターが問題の仕様にしたがってあなたのプログラムとやりとりを行い、スコアを計算します。また、好きな乱数シードを整数で与えることができます(与えない場合は、毎回異なる値が乱数シードとして使われます)。

以下のコマンドは、回答プログラムが a.out という実行可能バイナリにコンパイルされて Tester.class と同じディレクトリに配置されており、 123 という値を乱数シード値としてテストを実行する場合の例です。

java Tester -seed 123 -command "./a.out"

回答プログラムをRubyで main.rb というファイルに記述している場合は、たとえば次のようになります。 -command に渡すオプションは、スペースが含まれていても1つの文字列として認識されるよう、" で囲ってください。

java Tester -seed 123 -command "ruby main.rb"

デバッグ

あなたの回答プログラムが標準エラー出力へ出力した内容は、テスターの標準エラー出力へリダイレクトして書き出します。

また、テスターに -debug というオプションを与えると、回答プログラムの入出力をテスターの標準出力に書き出します。このオプションを指定した例を以下に示します。

java Tester -seed 123 -command "./a.out" -debug
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Random;
import java.util.Scanner;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.TimeUnit;
public class Tester {
private static final int N = 8;
private static final int MIN_CAPACITY = 1;
private static final int MAX_CAPACITY = 10;
private static final int MIN_DEMAND = 1;
private static final int MAX_DEMAND = 50;
private static final int MIN_TIME = 1;
private static final int MAX_TIME = 10;
private static final int PERIOD = 1000;
/*
* transported from testlib.h
* https://github.com/MikeMirzayanov/testlib/blob/af0a6d35628f98129195bebea67d0ab8ca9f2598/testlib.h
*
* Copyright (c) 2005-2016 Mike Mirzayanov
*/
static class TestlibRandom {
private static final long multiplier = 0x5DEECE66DL;
private static final long addend = 0xB;
private static final long mask = (1L << 48) - 1;
private long seed;
void setSeed(long s) {
seed = (s ^ multiplier) & mask;
}
long nextBits(int bits) {
if (bits <= 48) {
seed = (seed * multiplier + addend) & mask;
return seed >> (48 - bits);
} else {
if (bits > 63) {
throw new RuntimeException("nextBits(int bits): n must be less than 64");
}
int lowerBitCount = 32;
long left = (nextBits(31) << 32);
long right = nextBits(lowerBitCount);
return left ^ right;
}
}
long next(long n) {
if (n <= 0) {
throw new RuntimeException("next(int n): n must be positive");
}
final long limit = Long.MAX_VALUE / n * n;
long bits;
do {
bits = nextBits(63);
} while (bits >= limit);
return bits % n;
}
int next(int from, int to) {
return (int) (next((long) to - from + 1) + from);
}
}
static class State {
TestlibRandom rnd;
int[] C;
int[] A;
int D;
int T;
int score;
State(long seed) throws Exception {
rnd = new TestlibRandom();
rnd.setSeed(seed);
this.C = new int[N];
this.A = new int[N];
meetNewCustomer();
for (int i = 0; i < N; i++) {
this.C[i] = rnd.next(MIN_CAPACITY, MAX_CAPACITY);
}
}
void fill(int i) {
A[i] = C[i];
advance();
}
void move(int from, int to) {
if (from == to) {
throw new RuntimeException("MOVE: same tank is specified as from and to.");
}
if (A[from] >= C[to] - A[to]) {
A[from] -= C[to] - A[to];
A[to] = C[to];
} else {
A[to] += A[from];
A[from] = 0;
}
advance();
}
void change(int i) {
A[i] = 0;
C[i] = rnd.next(MIN_CAPACITY, MAX_CAPACITY);
advance();
}
void pass() {
T = 0;
advance();
}
void sell(int[] idx) {
int sum = 0;
boolean[] used = new boolean[A.length];
for (int i : idx) {
if (A[i] == 0) {
throw new RuntimeException("SELL: empty tank is used.");
}
if (used[i]) {
throw new RuntimeException("SELL: tank " + (i + 1) + " is used more than once.");
}
sum += A[i];
used[i] = true;
}
if (sum != D) {
throw new RuntimeException("SELL: sum of the specified tanks " + sum + " does not equal to the D " + D + ".");
}
score += D * D;
for (int i : idx) {
A[i] = 0;
C[i] = rnd.next(MIN_CAPACITY, MAX_CAPACITY);
}
T = 0; // hack
advance();
}
private void advance() {
--T;
if (T <= 0) {
meetNewCustomer();
}
}
private void meetNewCustomer() {
this.D = rnd.next(MIN_DEMAND, MAX_DEMAND);
this.T = rnd.next(MIN_TIME, MAX_TIME);
}
@Override
public String toString() {
int N = C.length;
StringBuilder builder = new StringBuilder();
builder.append(D + " " + T + "\n");
for (int i = 0; i < N; i++) {
builder.append(C[i] + (i == N - 1 ? "\n" : " "));
}
for (int i = 0; i < N; i++) {
builder.append(A[i] + (i == N - 1 ? "\n" : " "));
}
return builder.toString();
}
}
static class Result {
long seed;
long elapsed;
int score;
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("seed:" + seed + "\n");
builder.append("elapsed:" + elapsed / 1000.0 + "\n");
builder.append("score:" + score);
return builder.toString();
}
}
private void processQuery(State state, Scanner input) {
String command = input.next();
switch (command) {
case "fill":
int fi = input.nextInt() - 1;
if (debug) {
System.out.println(command + " " + (fi + 1));
}
state.fill(fi);
break;
case "move":
int from = input.nextInt() - 1;
int to = input.nextInt() - 1;
if (debug) {
System.out.println(command + " " + (from + 1) + " " + (to + 1));
}
state.move(from, to);
break;
case "change":
int ci = input.nextInt() - 1;
if (debug) {
System.out.println(command + " " + (ci + 1));
}
state.change(ci);
break;
case "pass":
if (debug) {
System.out.println(command);
}
state.pass();
break;
case "sell":
int n = input.nextInt();
if (n <= 0 || N < n) {
throw new RuntimeException("SELL: number of tanks " + n + " is invalid.");
}
int[] si = new int[n];
for (int i = 0; i < si.length; i++) {
si[i] = input.nextInt() - 1;
}
if (debug) {
System.out.print(command + " " + si.length);
for (int i = 0; i < si.length; i++) {
System.out.print(" " + (si[i] + 1));
}
System.out.println();
}
state.sell(si);
break;
default:
throw new RuntimeException("unknown command " + command + ".");
}
}
private Result execute(long seed) {
Result res = new Result();
res.seed = seed;
try {
State state = new State(seed);
ProcessBuilder pb = new ProcessBuilder(command.split("\\s+"));
Process proc = pb.start();
OutputStream os = proc.getOutputStream();
ForkJoinTask<?> future = ForkJoinPool.commonPool().submit(() -> {
// redirect command stderr
try (InputStreamReader reader = new InputStreamReader(proc.getErrorStream())) {
while (true) {
int ch = reader.read();
if (ch == -1) break;
System.err.print((char) ch);
}
} catch (IOException e) {
e.printStackTrace();
}
});
long startTime = System.currentTimeMillis();
try (Scanner sc = new Scanner(proc.getInputStream())) {
for (int i = 0; i < PERIOD; i++) {
if (debug) {
System.out.println("time:" + i);
System.out.print(state);
}
os.write(state.toString().getBytes());
os.flush();
processQuery(state, sc);
}
res.elapsed = System.currentTimeMillis() - startTime;
res.score = state.score;
future.get(10, TimeUnit.SECONDS); // wait termination
} finally {
proc.destroy();
}
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
private static String command;
private static boolean debug;
public static void main(String[] args) {
long seed = new Random().nextInt();
for (int i = 0; i < args.length; ++i) {
if (args[i].equals("-seed")) {
seed = Long.parseLong(args[++i]);
} else if (args[i].equals("-command")) {
command = args[++i];
} else if (args[i].equals("-debug")) {
debug = true;
}
}
if (command == null) {
System.err.println("usage: java Tester -command command [-seed seed] [-debug]");
System.exit(1);
}
Tester tester = new Tester();
Result res = tester.execute(seed);
System.out.println(res);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment