// 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(); } }