1/**2 * Real validation runtime: QuickJS compile (stage 'syntax') + dynamic smoke3 * test (stage 'smoke') running the candidate script in the actual engine for4 * SMOKE_TICKS against an idle opponent, under the real gas/memory limits.5 *6 * The QuickJS wasm module is created lazily on first use (never at global7 * scope) and reused across requests in the same isolate.8 */9import { LIMITS } from '@tribes/protocol';10import type { CompileResult, Order, ScriptHost, ValidationReport } from '@tribes/protocol';11import { createWorld } from '@tribes/engine';12import { createScriptHost } from '@tribes/script-host';13import { WorkerdReleaseSyncVariant } from '@cf-wasm/quickjs';14import type { QuickJSSyncVariant } from 'quickjs-emscripten';15import { baseStages, createValidator } from './validate';16import type { ValidationStage } from './validate';1718const SMOKE_TICKS = 50;19const SMOKE_SEED = '424242';2021let hostPromise: Promise<ScriptHost> | null = null;22export function getHost(): Promise<ScriptHost> {23 hostPromise ??= createScriptHost(WorkerdReleaseSyncVariant as unknown as QuickJSSyncVariant);24 return hostPromise;25}2627async function compile(source: string): Promise<CompileResult> {28 const host = await getHost();29 return host.compile(source);30}3132/**33 * Rejects scripts that are systematically broken: no global tick function34 * (first-tick error mentioning it) or an error/overrun on every smoke tick.35 * Occasional runtime errors are legal in real sims (they just waste the tick),36 * so partial failures pass with the detail recorded.37 */38const smokeStage: ValidationStage = {39 name: 'smoke',40 async run(source: string): Promise<string | null> {41 const host = await getHost();42 const world = createWorld({43 seed: SMOKE_SEED,44 tribeCount: 2,45 mapSize: LIMITS.MAP_SIZE,46 maxTicks: SMOKE_TICKS + 1,47 });48 const vm = host.createTribeVm(source, {49 gasPerTick: LIMITS.GAS_PER_TICK,50 memoryLimitBytes: LIMITS.VM_MEMORY_LIMIT,51 rngSeed: `${SMOKE_SEED}:0`,52 });53 try {54 let failures = 0;55 let firstError: string | null = null;56 for (let t = 0; t < SMOKE_TICKS; t++) {57 const outcome = vm.runTick(world.viewFor(0));58 if (outcome.error !== null || outcome.overBudget) {59 failures++;60 firstError ??= outcome.error ?? 'over gas budget';61 if (t === 0 && outcome.error?.includes('must define a global function tick')) {62 return outcome.error;63 }64 }65 const orders = new Map<number, Order[]>([66 [0, outcome.orders],67 [1, []],68 ]);69 world.step(orders);70 }71 if (failures === SMOKE_TICKS) {72 return `script failed on all ${SMOKE_TICKS} smoke-test ticks; first failure: ${firstError}`;73 }74 return null;75 } finally {76 vm.dispose();77 }78 },79};8081export function createRuntimeValidator(): (source: string) => Promise<ValidationReport> {82 return createValidator([...baseStages(compile), smokeStage]);83}