// A bot in its own process (referee/bot.mjs loads
/bot.mjs), spoken to one JSON line at a time, like kit/host.py:
// each message is numbered so a late answer to an earlier ask is dropped; a move later than the limit, or from a bot
// that crashed, is no move (the caller passes for it). A bot sees no job token: SS_* and SLURM_* are not passed on.
import { spawn, type ChildProcess } from 'node:child_process';
import { createInterface } from 'node:readline';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export const REFEREE = path.dirname(fileURLToPath(import.meta.url));
export class Bot {
errors = 0;
dead = false;
private p: ChildProcess;
private n = 0;
private waiting = new Map void>();
ready: Promise;
constructor(readonly dir: string, readonly limitMs = 2000) {
if (!existsSync(path.join(dir, 'bot.mjs'))) throw new Error(`No bot at ${path.join(dir, 'bot.mjs')}`);
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^(SS_|SLURM_)/.test(k)));
this.p = spawn(process.execPath, [path.join(REFEREE, 'bot.mjs'), path.resolve(dir)], {
stdio: ['pipe', 'pipe', process.env.SGS_BOT_STDERR ? 'inherit' : 'ignore'],
env,
});
createInterface({ input: this.p.stdout! }).on('line', (line) => {
try {
const m = JSON.parse(line);
if (m && typeof m === 'object' && 'answer' in m) this.waiting.get(m.n)?.(m.answer);
} catch {
// a line that is not an answer
}
});
this.p.on('exit', () => {
this.dead = true;
for (const r of this.waiting.values()) r(undefined);
});
this.p.stdin!.on('error', () => {});
this.ready = this.ask({ hello: true }, 10000);
}
/** The bot's answer, or undefined when it is late, broken or gone. */
async ask(msg: object, limitMs = this.limitMs): Promise {
if (this.dead) return undefined;
const n = ++this.n;
let timer: NodeJS.Timeout | undefined;
const answer = await new Promise((resolve) => {
this.waiting.set(n, resolve);
timer = setTimeout(() => resolve(undefined), limitMs);
this.p.stdin!.write(`${JSON.stringify({ ...msg, n })}\n`);
});
clearTimeout(timer);
this.waiting.delete(n);
if (answer === undefined || answer === null) {
if (++this.errors >= 20) this.close(); // ponytail: a bot that misses 20 asks is stopped for the rest of the run
return undefined;
}
return answer;
}
close(): void {
this.dead = true;
this.p.kill();
}
}