Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development. cameron.tngl.io/misaligned
Something went wrong. Try again.
3.9 kB · 84 lines
TypeScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485// Bot runs share a small queue, including the survey's three overlapping batches.import { spawn } from 'node:child_process';import { availableParallelism } from 'node:os';import { fileURLToPath } from 'node:url';import type { Outcome } from './play.ts';
export interface Played { seed: number; outcome: Outcome; line: string; t: number; marks: Record<string, number>; stats: Record<string, number> }const WORKER = fileURLToPath(new URL('./play-worker.ts', import.meta.url));
export function workerLimit(value: string | undefined, cores: number): number { if (value !== undefined && !/^[1-9]\d*$/.test(value)) throw new Error('MISALIGNED_WORKERS must be a positive integer'); return Math.max(1, Math.min(cores, value === undefined ? Math.min(4, Math.max(1, Math.floor(cores / 2))) : Number(value)));}
type Group = { start: () => void; pending: () => boolean; signal: AbortSignal };
/** Round-robin between callers, so long survey games need not wait behind all the short ones. */export class SimulationQueue { private groups: Group[] = []; private active = 0; private readonly limit: number; constructor(limit: number) { if (!Number.isInteger(limit) || limit < 1) throw new Error('Simulation concurrency must be a positive integer'); this.limit = limit; } run<T>(tasks: ((signal: AbortSignal) => Promise<T>)[]): Promise<T[]> { if (!tasks.length) return Promise.resolve([]); return new Promise((resolve, reject) => { const controller = new AbortController(), results: T[] = []; let next = 0, left = tasks.length; const group: Group = { signal: controller.signal, pending: () => next < tasks.length, start: () => { const index = next++; this.active++; Promise.resolve().then(() => tasks[index](controller.signal)).then(value => { if (controller.signal.aborted) return; results[index] = value; if (--left === 0) resolve(results); }, error => { controller.abort(); reject(error); }).finally(() => { this.active--; this.drain(); }); }, }; this.groups.push(group); // Coalesce synchronous callers before choosing the first four games. queueMicrotask(() => this.drain()); }); } private drain(): void { while (this.active < this.limit && this.groups.length) { const group = this.groups.shift()!; if (group.signal.aborted) continue; group.start(); if (group.pending()) this.groups.push(group); } }}
const queue = new SimulationQueue(workerLimit(process.env.MISALIGNED_WORKERS, availableParallelism()));
/** Play one game for each seed and give the results back in the order of the seeds. */export function playMany(careful: boolean, seeds: number[], schemes = false, marathon = 0): Promise<Played[]> { return queue.run(seeds.map(seed => signal => new Promise<Played>((done, fail) => { const child = spawn(process.execPath, [WORKER, JSON.stringify({ careful, seeds: [seed], schemes, marathon })], { stdio: ['ignore', 'pipe', 'inherit'], signal }); let out = ''; let spawnError: Error | undefined; child.stdout.on('data', d => { out += d; }); child.on('error', error => { spawnError = error; }); child.on('close', (code, killed) => { if (spawnError) { fail(spawnError); return; } if (code !== 0) { fail(new Error(`a bot game crashed (${killed ?? `exit ${code}`})`)); return; } try { const lines = out.trim().split('\n').filter(Boolean); if (lines.length !== 1) throw new Error('A bot worker returned an unexpected number of games'); const result = JSON.parse(lines[0]) as Played; if (result.seed !== seed) throw new Error('A bot worker returned another seed'); done(result); } catch (error) { fail(error); } }); })));}