@vibemancer/core 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,7 @@ import {
27
27
  Flamecaller,
28
28
  GCD_DURATION,
29
29
  Golem,
30
+ Hero,
30
31
  Hogger,
31
32
  Infernalist,
32
33
  KNOCKBACK_DAMAGE_THRESHOLD,
@@ -222,7 +223,7 @@ import {
222
223
  withMissileContext,
223
224
  withWizardContext,
224
225
  wrapWithParams
225
- } from "./chunk-XYEK7THS.js";
226
+ } from "./chunk-US4MEDKT.js";
226
227
  export {
227
228
  ALL_BOTS,
228
229
  ARENA_MAX,
@@ -252,6 +253,7 @@ export {
252
253
  Flamecaller,
253
254
  GCD_DURATION,
254
255
  Golem,
256
+ Hero,
255
257
  Hogger,
256
258
  Infernalist,
257
259
  KNOCKBACK_DAMAGE_THRESHOLD,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { FightResult, SimulateResult } from './index-browser.js';
2
- export { ALL_BOTS, ARENA_MAX, ARENA_MIN, ARENA_SIZE, ARENA_WATER_BUFFER, ActionBuilder, AnalyzedThreat, Archlich, Archmage, BLINK_CAST_TIME, BLINK_COOLDOWN, BLINK_MAX_COOLDOWN, BLINK_MAX_RANGE, BLINK_MIN_COOLDOWN, BLINK_RANGE, BOT_GROUPS, Battlemage, BlinkEvent, Bonemancer, BotError, BrowserManualMatchSandbox, BrowserMatchSandbox, BrowserSandboxOptions, CASTING_MOVEMENT_MULT, COLLISION_RADIUS, CastCancelEvent, CastStartEvent, Critter, DEFAULT_SEED, Doombringer, ENGINE_VERSION, EnemyState, FIGHT_SPAWN_DISTANCES, FightStats, FightWinner, FinalAction, Flamecaller, GCD_DURATION, GameConfig, GameState, Golem, Hogger, HomingParams, HookState, Infernalist, InternalWizardState, KNOCKBACK_DAMAGE_THRESHOLD, KNOCKBACK_DECAY, KNOCKBACK_DELAY, KNOCKBACK_SPEED_PER_DAMAGE, LAVA_BORDER_WIDTH, Lich, MATCH_DURATION, MAX_HEALTH, MISSILE_BASE_CAST, MISSILE_BASE_RADIUS, MISSILE_DAMAGE_POWER, MISSILE_DAMAGE_RADIUS_SCALE, MISSILE_DAMAGE_SCALE, MISSILE_HOMING_COEFF, MISSILE_MIN_CAST_TIME, MISSILE_MIN_DAMAGE, MISSILE_MIN_DURATION, MISSILE_MIN_SPEED, MISSILE_RADIUS_PER_DAMAGE, MISSILE_SPEED_DURATION_BASELINE, MISSILE_SPEED_DURATION_COEFF, MISSILE_TURN_DURATION_COEFF, MOVEMENT_SPEED, MOVE_SPEED, ManualMatch, ManualMatchInitOptions, ManualMatchOptions, ManualMatchStepRequest, MatchWinner, MissileAction, MissileActions, MissileConfig, MissileContext, MissileExpiredEvent, MissileFunction, MissileHitEvent, MissileLaunchEvent, MissileOobEvent, MissileTemplate, Nightblade, ParamDeclaration, Position, ProjectileState, Pyromancer, RULES, RULESET_RANGES, RefObject, Rookie, SHIELD_CAST_TIME, SHIELD_DECAY_PER_SECOND, SHIELD_DECAY_RATE, SHIELD_MAX_BLOCK, SHIELD_MAX_STRENGTH, SHIELD_MIN_BLOCK, SHIELD_MIN_STRENGTH, SPAWN_DISTANCE, SeekerParams, Sentinel, Shadowblade, ShieldBlockEvent, ShieldStartEvent, SimEvent, Spellbinder, Spellseeker, Spellshot, Spellspinner, Spelltracer, Spellweaver, SpiralParams, StepResult, Stormcaller, Stormchaser, Stormforger, StraightParams, TICKS_PER_SECOND, TICK_DURATION_MS, TargetDummy, TestBotBuilder, TestFightResult, TestSimulateResult, TraceBotSummary, TraceEvent, TraceEventType, TraceSummary, Turtle, Velocity, Voidblade, WARMUP_DURATION_TOLERANCE, WARMUP_MAX_BONUS, WARMUP_MAX_PENALTY, WARMUP_SPEED_TOLERANCE, WARMUP_TURN_TOLERANCE, WIZARD_HEALTH, WIZARD_RADIUS, Warmage, WizardActions, WizardContext, WizardDeathEvent, WizardEntry, WizardFunction, WizardGroup, WizardLavaDeathEvent, WizardState, WorkerFactory, WorkerLike, analyzeThreats, angleDiff, angleInRange, angleTo, applyDamage, applyRulesetOverrides, blink, browserSandboxFight, browserSandboxSimulate, calculateBlinkCooldown, calculateMissileCastTime, calculateMissileRadius, calculateMissileSimilarity, calculateShieldBlock, calculateWarmupMultiplier, cancel, cancelCast, clampPositionToArena, clampToArena, clearHooks, clearParamValues, completeCast, createEntitySeed, createInitialState, createRandom, createWorkerScript, currentRuleset, diagnoseTrace, directionAway, directionTo, distanceTo, effectiveTurnRateCost, extractAction, extractMissileAction, extractStats, extractTraceEvents, fight, findInRange, findNearest, fitMissileForEscapingTarget, fitMissileToBudget, flyStraight, formatDiagnosis, formatStats, formatTraceEvents, formatTraceSummary, generateCandidates, generateCombos, getAdaptiveMissileConfig, getEffectiveRange, getLeadPosition, getMissileCastTime, getMissileContext, getPlayerState, getWizardContext, hashCombine, homingMissile, idle, inRange, interceptAngle, isBannedBotName, isInLava, magnitude, matchesAnyBanRule, missile, move, moveInDirection, moveProjectile, moveWizard, narrowRange, nextRandom, normalize, normalizeAngle, predictPosition, resetAllHooks, resetRuleset, resolveWizardCollision, runWithHooks, runWizardWithContext, scoreFight, scoreFightAsWizard2, seekerMissile, setParamValues, shield, simulate, simulateMinDuration, sortByDistance, spiralMissile, startCast, startDiscovery, stopDiscovery, straightMissile, summarizeTrace, sweptCircleCollision, testBot, tick, turnToAngle, turnToward, updateShield, useArenaSize, useBlinkCooldown, useCastProgress, useCastingSpell, useClosestThreat, useDamageDealt, useDamageTaken, useEffect, useEnemy, useHealth, useLastHitTick, useLastMissileConfig, useMemo, useMyProjectiles, useMyThreatsToEnemy, useParam, usePosition, useProjectiles, useRandom, useRef, useShieldStrength, useState, useStatus, useThreats, useTick, useTicksUntilReady, useVelocity, validateHookCall, validateMissileConfig, withMissileContext, withWizardContext, wrapWithParams } from './index-browser.js';
1
+ import { F as FightResult, S as SimulateResult } from './index-browser-CYoJrb2d.js';
2
+ export { A as ALL_BOTS, a as ARENA_MAX, b as ARENA_MIN, c as ARENA_SIZE, d as ARENA_WATER_BUFFER, e as ActionBuilder, f as AnalyzedThreat, g as Archlich, h as Archmage, B as BLINK_CAST_TIME, i as BLINK_COOLDOWN, j as BLINK_MAX_COOLDOWN, k as BLINK_MAX_RANGE, l as BLINK_MIN_COOLDOWN, m as BLINK_RANGE, n as BOT_GROUPS, o as Battlemage, p as BlinkEvent, q as Bonemancer, r as BotBudgetState, s as BotError, t as BrowserManualMatchSandbox, u as BrowserMatchSandbox, v as BrowserSandboxOptions, w as BudgetLimits, C as CASTING_MOVEMENT_MULT, x as COLLISION_RADIUS, y as CastCancelEvent, z as CastStartEvent, D as Critter, E as DEFAULT_BUDGET, G as DEFAULT_FIGHT_BACKSTOP_MS, H as DEFAULT_SEED, I as Doombringer, J as ENGINE_VERSION, K as EnemyState, L as FIGHT_SPAWN_DISTANCES, M as FightBudget, N as FightStats, O as FightWinner, P as FinalAction, Q as Flamecaller, R as GCD_DURATION, T as GameConfig, U as GameState, V as Golem, W as Hero, X as Hogger, Y as HomingParams, Z as HookState, _ as Infernalist, $ as InternalWizardState, a0 as KNOCKBACK_DAMAGE_THRESHOLD, a1 as KNOCKBACK_DECAY, a2 as KNOCKBACK_DELAY, a3 as KNOCKBACK_SPEED_PER_DAMAGE, a4 as LAVA_BORDER_WIDTH, a5 as Lich, a6 as MATCH_DURATION, a7 as MAX_HEALTH, a8 as MISSILE_BASE_CAST, a9 as MISSILE_BASE_RADIUS, aa as MISSILE_DAMAGE_POWER, ab as MISSILE_DAMAGE_RADIUS_SCALE, ac as MISSILE_DAMAGE_SCALE, ad as MISSILE_HOMING_COEFF, ae as MISSILE_MIN_CAST_TIME, af as MISSILE_MIN_DAMAGE, ag as MISSILE_MIN_DURATION, ah as MISSILE_MIN_SPEED, ai as MISSILE_RADIUS_PER_DAMAGE, aj as MISSILE_SPEED_DURATION_BASELINE, ak as MISSILE_SPEED_DURATION_COEFF, al as MISSILE_TURN_DURATION_COEFF, am as MOVEMENT_SPEED, an as MOVE_SPEED, ao as ManualMatch, ap as ManualMatchInitOptions, aq as ManualMatchOptions, ar as ManualMatchStepRequest, as as MatchWinner, at as MissileAction, au as MissileActions, av as MissileConfig, aw as MissileContext, ax as MissileExpiredEvent, ay as MissileFunction, az as MissileHitEvent, aA as MissileLaunchEvent, aB as MissileOobEvent, aC as MissileTemplate, aD as Nightblade, aE as ParamDeclaration, aF as Position, aG as ProjectileState, aH as Pyromancer, aI as RULES, aJ as RULESET_RANGES, aK as RefObject, aL as Rookie, aM as SHIELD_CAST_TIME, aN as SHIELD_DECAY_PER_SECOND, aO as SHIELD_DECAY_RATE, aP as SHIELD_MAX_BLOCK, aQ as SHIELD_MAX_STRENGTH, aR as SHIELD_MIN_BLOCK, aS as SHIELD_MIN_STRENGTH, aT as SPAWN_DISTANCE, aU as SeekerParams, aV as Sentinel, aW as Shadowblade, aX as ShieldBlockEvent, aY as ShieldStartEvent, aZ as SimEvent, a_ as Spellbinder, a$ as Spellseeker, b0 as Spellshot, b1 as Spellspinner, b2 as Spelltracer, b3 as Spellweaver, b4 as SpiralParams, b5 as StepResult, b6 as Stormcaller, b7 as Stormchaser, b8 as Stormforger, b9 as StraightParams, ba as TICKS_PER_SECOND, bb as TICK_DURATION_MS, bc as TargetDummy, bd as TestBotBuilder, be as TestFightResult, bf as TestSimulateResult, bg as TraceBotSummary, bh as TraceEvent, bi as TraceEventType, bj as TraceSummary, bk as Turtle, bl as Velocity, bm as Voidblade, bn as WARMUP_DURATION_TOLERANCE, bo as WARMUP_MAX_BONUS, bp as WARMUP_MAX_PENALTY, bq as WARMUP_SPEED_TOLERANCE, br as WARMUP_TURN_TOLERANCE, bs as WIZARD_HEALTH, bt as WIZARD_RADIUS, bu as Warmage, bv as WizardActions, bw as WizardContext, bx as WizardDeathEvent, by as WizardEntry, bz as WizardFunction, bA as WizardGroup, bB as WizardLavaDeathEvent, bC as WizardState, bD as WorkerFactory, bE as WorkerLike, bF as analyzeThreats, bG as angleDiff, bH as angleInRange, bI as angleTo, bJ as applyDamage, bK as applyRulesetOverrides, bL as blink, bM as browserSandboxFight, bN as browserSandboxSimulate, bO as calculateBlinkCooldown, bP as calculateMissileCastTime, bQ as calculateMissileRadius, bR as calculateMissileSimilarity, bS as calculateShieldBlock, bT as calculateWarmupMultiplier, bU as cancel, bV as cancelCast, bW as clampPositionToArena, bX as clampToArena, bY as clearHooks, bZ as clearParamValues, b_ as completeCast, b$ as createBudgetState, c0 as createEntitySeed, c1 as createFightBudget, c2 as createInitialState, c3 as createRandom, c4 as createWorkerScript, c5 as currentRuleset, c6 as diagnoseTrace, c7 as directionAway, c8 as directionTo, c9 as distanceTo, ca as effectiveTurnRateCost, cb as extractAction, cc as extractMissileAction, cd as extractStats, ce as extractTraceEvents, cf as fight, cg as findInRange, ch as findNearest, ci as fitMissileForEscapingTarget, cj as fitMissileToBudget, ck as flyStraight, cl as formatDiagnosis, cm as formatStats, cn as formatTraceEvents, co as formatTraceSummary, cp as generateCandidates, cq as generateCombos, cr as getAdaptiveMissileConfig, cs as getEffectiveRange, ct as getLeadPosition, cu as getMissileCastTime, cv as getMissileContext, cw as getPlayerState, cx as getWizardContext, cy as hashCombine, cz as homingMissile, cA as idle, cB as inRange, cC as interceptAngle, cD as isBannedBotName, cE as isInLava, cF as magnitude, cG as matchesAnyBanRule, cH as mayAct, cI as missile, cJ as move, cK as moveInDirection, cL as moveProjectile, cM as moveWizard, cN as narrowRange, cO as nextRandom, cP as normalize, cQ as normalizeAngle, cR as predictPosition, cS as recordSpend, cT as resetAllHooks, cU as resetRuleset, cV as resolveWizardCollision, cW as runWithHooks, cX as runWizardWithContext, cY as scoreFight, cZ as scoreFightAsWizard2, c_ as seekerMissile, c$ as setParamValues, d0 as shield, d1 as simulate, d2 as simulateMinDuration, d3 as sortByDistance, d4 as spiralMissile, d5 as startCast, d6 as startDiscovery, d7 as stopDiscovery, d8 as straightMissile, d9 as summarizeTrace, da as sweptCircleCollision, db as testBot, dc as tick, dd as turnToAngle, de as turnToward, df as updateShield, dg as useArenaSize, dh as useBlinkCooldown, di as useCastProgress, dj as useCastingSpell, dk as useClosestThreat, dl as useDamageDealt, dm as useDamageTaken, dn as useEffect, dp as useEnemy, dq as useHealth, dr as useLastHitTick, ds as useLastMissileConfig, dt as useMemo, du as useMyProjectiles, dv as useMyThreatsToEnemy, dw as useParam, dx as usePosition, dy as useProjectiles, dz as useRandom, dA as useRef, dB as useShieldStrength, dC as useState, dD as useStatus, dE as useThreats, dF as useTick, dG as useTicksUntilReady, dH as useVelocity, dI as validateHookCall, dJ as validateMissileConfig, dK as withMissileContext, dL as withWizardContext, dM as wrapWithParams } from './index-browser-CYoJrb2d.js';
3
3
 
4
4
  /**
5
5
  * A compiled bot ready for sandboxed execution.
@@ -173,6 +173,8 @@ interface RunBundleFightOptions {
173
173
  seed: number;
174
174
  /** Prebuilt match template (e.g. the committed Cloud Functions one). Built lazily if omitted. */
175
175
  matchTemplate?: string;
176
+ /** Extend the per-fight isolate timeout. Can only raise it above the default. */
177
+ fightTimeoutMs?: number;
176
178
  /** Skip per-tick history in the result (smaller copy out of the isolate). Default true. */
177
179
  skipHistory?: boolean;
178
180
  }
@@ -189,6 +191,8 @@ interface RunBundleSimulateOptions {
189
191
  spawnDistance?: number;
190
192
  maxTicks?: number;
191
193
  matchTemplate?: string;
194
+ /** Extend the per-fight isolate timeout. Can only raise it above the default. */
195
+ fightTimeoutMs?: number;
192
196
  }
193
197
  /**
194
198
  * Run a SINGLE match between two compiled bundles, returning the full per-tick
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ import {
20
20
  CASTING_MOVEMENT_MULT,
21
21
  COLLISION_RADIUS,
22
22
  Critter,
23
+ DEFAULT_BUDGET,
24
+ DEFAULT_FIGHT_BACKSTOP_MS,
23
25
  DEFAULT_SEED,
24
26
  Doombringer,
25
27
  ENGINE_VERSION,
@@ -27,6 +29,7 @@ import {
27
29
  Flamecaller,
28
30
  GCD_DURATION,
29
31
  Golem,
32
+ Hero,
30
33
  Hogger,
31
34
  Infernalist,
32
35
  KNOCKBACK_DAMAGE_THRESHOLD,
@@ -113,7 +116,9 @@ import {
113
116
  clearHooks,
114
117
  clearParamValues,
115
118
  completeCast,
119
+ createBudgetState,
116
120
  createEntitySeed,
121
+ createFightBudget,
117
122
  createInitialState,
118
123
  createRandom,
119
124
  createWorkerScript,
@@ -155,6 +160,7 @@ import {
155
160
  isInLava,
156
161
  magnitude,
157
162
  matchesAnyBanRule,
163
+ mayAct,
158
164
  missile,
159
165
  move,
160
166
  moveInDirection,
@@ -165,6 +171,7 @@ import {
165
171
  normalize,
166
172
  normalizeAngle,
167
173
  predictPosition,
174
+ recordSpend,
168
175
  resetAllHooks,
169
176
  resetRuleset,
170
177
  resolveWizardCollision,
@@ -222,7 +229,7 @@ import {
222
229
  withMissileContext,
223
230
  withWizardContext,
224
231
  wrapWithParams
225
- } from "./chunk-XYEK7THS.js";
232
+ } from "./chunk-US4MEDKT.js";
226
233
 
227
234
  // src/engine/sandbox.ts
228
235
  import ivm from "isolated-vm";
@@ -273,9 +280,14 @@ import {${bot1ExportName} as __Bot1} from '${bot1Path}';
273
280
  import {${bot2ExportName} as __Bot2} from '${bot2Path}';
274
281
  import {fight, simulate} from '${srcPath}/engine/simulation.ts';
275
282
  import {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';
283
+ import {DEFAULT_BUDGET} from '${srcPath}/engine/bot-compute-budget.ts';
276
284
 
277
285
  globalThis.__fight = function __fight(options) {
278
- const result = fight(__Bot1, __Bot2, options);
286
+ // Budgeted like every other fight path. This one matters MORE than it looks: it is what
287
+ // sandboxFight uses, so it is the MCP fight tools and the CLI \u2014 the place a user's
288
+ // runaway bot most directly burns server time. Leaving it unbudgeted while the backstop
289
+ // moved from 60s to 110s would have made this path strictly worse than before.
290
+ const result = fight(__Bot1, __Bot2, {budgetLimits: DEFAULT_BUDGET, ...options});
279
291
  return result;
280
292
  };
281
293
 
@@ -283,7 +295,10 @@ globalThis.__simulate = function __simulate(options) {
283
295
  const {params1, params2, ...simOptions} = options;
284
296
  const bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;
285
297
  const bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;
286
- const result = simulate(bot1, bot2, simOptions);
298
+ // Budgeted like __fight. This path is trace and the optimizer \u2014 a single match rather
299
+ // than ten \u2014 and leaving it out would have made it strictly worse than before, since the
300
+ // same change raised its backstop from 60s to 110s.
301
+ const result = simulate(bot1, bot2, {budgetLimits: DEFAULT_BUDGET, ...simOptions});
287
302
  return result;
288
303
  };
289
304
  `;
@@ -391,7 +406,7 @@ var MatchSandbox = class _MatchSandbox {
391
406
  */
392
407
  static async create(bot1, bot2, options) {
393
408
  const memoryLimitMB = options?.memoryLimitMB ?? 512;
394
- const timeoutMs = options?.timeoutMs ?? 6e4;
409
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_FIGHT_BACKSTOP_MS;
395
410
  const bundle = await compileMatchBundle(bot1, bot2, options?.compileOptions);
396
411
  const isolate = new ivm.Isolate({ memoryLimit: memoryLimitMB });
397
412
  try {
@@ -416,7 +431,7 @@ var MatchSandbox = class _MatchSandbox {
416
431
  */
417
432
  static async fromBundle(bundle, options) {
418
433
  const memoryLimitMB = options?.memoryLimitMB ?? 512;
419
- const timeoutMs = options?.timeoutMs ?? 6e4;
434
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_FIGHT_BACKSTOP_MS;
420
435
  const isolate = new ivm.Isolate({ memoryLimit: memoryLimitMB });
421
436
  try {
422
437
  const context = await isolate.createContext();
@@ -534,7 +549,11 @@ import path2 from "path";
534
549
  import ivm2 from "isolated-vm";
535
550
  import { build as build2 } from "esbuild";
536
551
  var MEMORY_LIMIT_MB = 256;
537
- var FIGHT_TIMEOUT_MS = 6e4;
552
+ var DEFAULT_FIGHT_TIMEOUT_MS = DEFAULT_FIGHT_BACKSTOP_MS;
553
+ function resolveFightTimeout(requested) {
554
+ if (typeof requested !== "number" || !Number.isFinite(requested)) return DEFAULT_FIGHT_TIMEOUT_MS;
555
+ return Math.max(DEFAULT_FIGHT_TIMEOUT_MS, requested);
556
+ }
538
557
  var FREEZE_BANNER = `
539
558
  Object.freeze(Object.prototype);
540
559
  Object.freeze(Array.prototype);
@@ -573,6 +592,7 @@ function buildMatchTemplate() {
573
592
  const entryPoint = `
574
593
  import {fight, simulate} from '${coreSrc}/engine/simulation.ts';
575
594
  import {wrapWithParams} from '${coreSrc}/engine/params-runtime.ts';
595
+ import {DEFAULT_BUDGET} from '${coreSrc}/engine/bot-compute-budget.ts';
576
596
 
577
597
  const __Bot1 = (globalThis as any).__injectedBot1;
578
598
  const __Bot2 = (globalThis as any).__injectedBot2;
@@ -581,14 +601,18 @@ if (!__Bot1) throw new Error('Bot 1 not injected (set globalThis.__injectedBot1)
581
601
  if (!__Bot2) throw new Error('Bot 2 not injected (set globalThis.__injectedBot2)');
582
602
 
583
603
  (globalThis as any).__fight = function __fight(options: any) {
584
- return fight(__Bot1, __Bot2, options);
604
+ // Same per-bot compute budget the server uses, so a runaway bot fails the same way in
605
+ // the CLI as it will on the ladder \u2014 and so it cannot hang someone's terminal.
606
+ return fight(__Bot1, __Bot2, {budgetLimits: DEFAULT_BUDGET, ...options});
585
607
  };
586
608
 
587
609
  (globalThis as any).__simulate = function __simulate(options: any) {
588
610
  const {params1, params2, ...simOptions} = options;
589
611
  const bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;
590
612
  const bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;
591
- return simulate(bot1, bot2, simOptions);
613
+ // Budgeted like __fight \u2014 this is the CLI's trace/optimize path, and its backstop went
614
+ // up to 110s with everything else, so leaving it out would make it worse than before.
615
+ return simulate(bot1, bot2, {budgetLimits: DEFAULT_BUDGET, ...simOptions});
592
616
  };
593
617
  `;
594
618
  const result = await build2({
@@ -620,6 +644,7 @@ if (!__Bot2) throw new Error('Bot 2 not injected (set globalThis.__injectedBot2)
620
644
  }
621
645
  async function runBundleFight(bundle1, bundle2, options) {
622
646
  const template = options.matchTemplate ?? await buildMatchTemplate();
647
+ const fightTimeoutMs = resolveFightTimeout(options.fightTimeoutMs);
623
648
  const skipHistory = options.skipHistory ?? true;
624
649
  const code = FREEZE_BANNER + "\n" + bundle1 + "\nvar __savedBot1 = globalThis.__injectedBot1;\nglobalThis.__injectedBot1 = undefined;\n" + bundle2 + "\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\n\nglobalThis.__injectedBot1 = __savedBot1;\n__savedBot1 = undefined;\n" + template + "\ndelete globalThis.__injectedBot1;\ndelete globalThis.__injectedBot2;\n";
625
650
  const isolate = new ivm2.Isolate({ memoryLimit: MEMORY_LIMIT_MB });
@@ -628,12 +653,12 @@ async function runBundleFight(bundle1, bundle2, options) {
628
653
  const jail = context.global;
629
654
  await jail.set("global", jail.derefInto());
630
655
  const script = await isolate.compileScript(code);
631
- await script.run(context, { timeout: FIGHT_TIMEOUT_MS });
656
+ await script.run(context, { timeout: fightTimeoutMs });
632
657
  const fightFn = await jail.get("__fight");
633
658
  const result = await fightFn.apply(
634
659
  void 0,
635
660
  [new ivm2.ExternalCopy({ seed: options.seed, skipHistory }).copyInto()],
636
- { timeout: FIGHT_TIMEOUT_MS, result: { copy: true } }
661
+ { timeout: fightTimeoutMs, result: { copy: true } }
637
662
  );
638
663
  return result;
639
664
  } finally {
@@ -642,6 +667,7 @@ async function runBundleFight(bundle1, bundle2, options) {
642
667
  }
643
668
  async function runBundleSimulate(bundle1, bundle2, options = {}) {
644
669
  const template = options.matchTemplate ?? await buildMatchTemplate();
670
+ const fightTimeoutMs = resolveFightTimeout(options.fightTimeoutMs);
645
671
  const code = FREEZE_BANNER + "\n" + bundle1 + "\nvar __savedBot1 = globalThis.__injectedBot1;\nglobalThis.__injectedBot1 = undefined;\n" + bundle2 + "\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\n\nglobalThis.__injectedBot1 = __savedBot1;\n__savedBot1 = undefined;\n" + template + "\ndelete globalThis.__injectedBot1;\ndelete globalThis.__injectedBot2;\n";
646
672
  const isolate = new ivm2.Isolate({ memoryLimit: MEMORY_LIMIT_MB });
647
673
  try {
@@ -649,7 +675,7 @@ async function runBundleSimulate(bundle1, bundle2, options = {}) {
649
675
  const jail = context.global;
650
676
  await jail.set("global", jail.derefInto());
651
677
  const script = await isolate.compileScript(code);
652
- await script.run(context, { timeout: FIGHT_TIMEOUT_MS });
678
+ await script.run(context, { timeout: fightTimeoutMs });
653
679
  const simulateFn = await jail.get("__simulate");
654
680
  const simOptions = {
655
681
  seed: options.seed ?? 1,
@@ -659,7 +685,7 @@ async function runBundleSimulate(bundle1, bundle2, options = {}) {
659
685
  const result = await simulateFn.apply(
660
686
  void 0,
661
687
  [new ivm2.ExternalCopy(simOptions).copyInto()],
662
- { timeout: FIGHT_TIMEOUT_MS, result: { copy: true } }
688
+ { timeout: fightTimeoutMs, result: { copy: true } }
663
689
  );
664
690
  return result;
665
691
  } finally {
@@ -689,6 +715,8 @@ export {
689
715
  CASTING_MOVEMENT_MULT,
690
716
  COLLISION_RADIUS,
691
717
  Critter,
718
+ DEFAULT_BUDGET,
719
+ DEFAULT_FIGHT_BACKSTOP_MS,
692
720
  DEFAULT_SEED,
693
721
  Doombringer,
694
722
  ENGINE_VERSION,
@@ -696,6 +724,7 @@ export {
696
724
  Flamecaller,
697
725
  GCD_DURATION,
698
726
  Golem,
727
+ Hero,
699
728
  Hogger,
700
729
  Infernalist,
701
730
  KNOCKBACK_DAMAGE_THRESHOLD,
@@ -785,7 +814,9 @@ export {
785
814
  clearParamValues,
786
815
  compileMatchBundle,
787
816
  completeCast,
817
+ createBudgetState,
788
818
  createEntitySeed,
819
+ createFightBudget,
789
820
  createInitialState,
790
821
  createRandom,
791
822
  createWorkerScript,
@@ -827,6 +858,7 @@ export {
827
858
  isInLava,
828
859
  magnitude,
829
860
  matchesAnyBanRule,
861
+ mayAct,
830
862
  missile,
831
863
  move,
832
864
  moveInDirection,
@@ -837,6 +869,7 @@ export {
837
869
  normalize,
838
870
  normalizeAngle,
839
871
  predictPosition,
872
+ recordSpend,
840
873
  resetAllHooks,
841
874
  resetRuleset,
842
875
  resolveWizardCollision,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/engine/sandbox.ts","../src/engine/sandbox-compile.ts","../src/engine/sandbox-harness.ts","../src/engine/bundle-fight.ts"],"sourcesContent":["/**\r\n * VIBEMANCER — SANDBOX\r\n *\r\n * Provides isolated-vm sandboxing for bot code execution. Both bots + the\r\n * entire simulation engine run inside a single V8 isolate, so there is ZERO\r\n * per-tick boundary crossing overhead. The only data crossing the boundary\r\n * is fight/simulate options going in and results coming out.\r\n *\r\n * Architecture:\r\n * - Host: creates isolate, loads compiled bundle, calls __fight/__simulate\r\n * - Isolate: contains both bots + full simulation engine, runs fight/simulate\r\n *\r\n * Safety:\r\n * - Memory limit (default 512 MB) catches memory bombs\r\n * - Timeout (default 30s) catches infinite loops\r\n * - Prototype freeze prevents cross-bot sabotage\r\n * - platform: 'neutral' strips Node.js APIs (no fs/net/process)\r\n */\r\n\r\nimport ivm from 'isolated-vm';\r\nimport type {FightResult, SimulateResult} from './simulation.js';\r\nimport {BotBundle, compileMatchBundle} from './sandbox-compile.js';\r\nimport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n// Re-export BotBundle so existing imports from sandbox.ts keep working\r\nexport {BotBundle} from './sandbox-compile.js';\r\nexport {compileMatchBundle, compileManualMatchBundle} from './sandbox-compile.js';\r\nexport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n/**\r\n * Options for sandbox creation.\r\n */\r\nexport interface SandboxOptions\r\n{\r\n\t/** Memory limit in MB for the isolate (default: 512). */\r\n\tmemoryLimitMB?: number;\r\n\t/** Timeout in ms for fight/simulate calls (default: 60000). */\r\n\ttimeoutMs?: number;\r\n\t/** Options passed to esbuild compilation (aliases, externals). */\r\n\tcompileOptions?: CompileOptions;\r\n}\r\n\r\n/**\r\n * A sandboxed match runner. Both bots + the entire simulation engine run\r\n * inside a single isolated-vm isolate.\r\n *\r\n * Usage:\r\n * ```ts\r\n * const sandbox = await MatchSandbox.create(botA, botB);\r\n * const result = sandbox.fight({ seed: 42 });\r\n * sandbox.dispose();\r\n * ```\r\n */\r\nexport class MatchSandbox\r\n{\r\n\tprivate isolate: ivm.Isolate;\r\n\tprivate context: ivm.Context;\r\n\tprivate fightFn: ivm.Reference;\r\n\tprivate simulateFn: ivm.Reference;\r\n\tprivate timeout: number;\r\n\tprivate disposed = false;\r\n\r\n\tprivate constructor(\r\n\t\tisolate: ivm.Isolate,\r\n\t\tcontext: ivm.Context,\r\n\t\tfightFn: ivm.Reference,\r\n\t\tsimulateFn: ivm.Reference,\r\n\t\ttimeout: number,\r\n\t)\r\n\t{\r\n\t\tthis.isolate = isolate;\r\n\t\tthis.context = context;\r\n\t\tthis.fightFn = fightFn;\r\n\t\tthis.simulateFn = simulateFn;\r\n\t\tthis.timeout = timeout;\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox with both bots loaded. Compiles the match bundle\r\n\t * automatically using esbuild.\r\n\t */\r\n\tstatic async create(\r\n\t\tbot1: BotBundle,\r\n\t\tbot2: BotBundle,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\tconst timeoutMs = options?.timeoutMs ?? 60000;\r\n\r\n\t\t// 1. Compile the match bundle\r\n\t\tconst bundle = await compileMatchBundle(bot1, bot2, options?.compileOptions);\r\n\r\n\t\t// 2. Create isolate with memory limit\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// 3. Create context and load the bundle\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\t// 4. Get references to the exposed functions\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\t// OOM or timeout can auto-dispose the isolate, so guard the cleanup dispose.\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox from a pre-compiled bundle string.\r\n\t * Useful for caching compiled bundles across multiple MatchSandbox instances.\r\n\t */\r\n\tstatic async fromBundle(\r\n\t\tbundle: string,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\tconst timeoutMs = options?.timeoutMs ?? 60000;\r\n\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Compile a match bundle without creating an isolate.\r\n\t * Returns the compiled JS string for caching/reuse.\r\n\t */\r\n\tstatic async compile(bot1: BotBundle, bot2: BotBundle, compileOptions?: CompileOptions): Promise<string>\r\n\t{\r\n\t\treturn compileMatchBundle(bot1, bot2, compileOptions);\r\n\t}\r\n\r\n\t/**\r\n\t * Run a full fight (10 matches: 5 spawn distances x 2 sides).\r\n\t * Synchronous after isolate creation — runs entirely inside the isolate.\r\n\t */\r\n\tfight(options?: {seed?: number; maxTicks?: number}): FightResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.fightFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as FightResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Run a single simulation.\r\n\t * Synchronous after isolate creation.\r\n\t *\r\n\t * @param options.params1 - useParam overrides for bot 1 (wizard-1)\r\n\t * @param options.params2 - useParam overrides for bot 2 (wizard-2)\r\n\t */\r\n\tsimulate(options?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t}): SimulateResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.simulateFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as SimulateResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Dispose the isolate and free all memory.\r\n\t * The sandbox cannot be used after disposal.\r\n\t */\r\n\tdispose(): void\r\n\t{\r\n\t\tif (!this.disposed)\r\n\t\t{\r\n\t\t\tthis.disposed = true;\r\n\t\t\t// OOM can auto-dispose the isolate, so guard all cleanup\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.fightFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.simulateFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.context.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.isolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Whether this sandbox has been disposed.\r\n\t */\r\n\tget isDisposed(): boolean\r\n\t{\r\n\t\treturn this.disposed;\r\n\t}\r\n\r\n\tprivate ensureNotDisposed(): void\r\n\t{\r\n\t\tif (this.disposed)\r\n\t\t{\r\n\t\t\tthrow new Error('MatchSandbox has been disposed');\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed fight. Creates isolate, runs fight, disposes.\r\n * Convenience wrapper for single-use scenarios.\r\n */\r\nexport async function sandboxFight(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {seed?: number; maxTicks?: number} & SandboxOptions,\r\n): Promise<FightResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.fight({seed: options?.seed, maxTicks: options?.maxTicks});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed simulate. Creates isolate, runs simulate, disposes.\r\n */\r\nexport async function sandboxSimulate(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t} & SandboxOptions,\r\n): Promise<SimulateResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.simulate({\r\n\t\t\tseed: options?.seed,\r\n\t\t\tmaxTicks: options?.maxTicks,\r\n\t\t\tspawnDistance: options?.spawnDistance,\r\n\t\t\tskipHistory: options?.skipHistory,\r\n\t\t\tparams1: options?.params1,\r\n\t\t\tparams2: options?.params2,\r\n\t\t});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n","/**\r\n * VIBEMANCER — SANDBOX COMPILATION\r\n *\r\n * Compiles match bundles using esbuild. Extracted from sandbox.ts so that\r\n * compilation can be used independently of isolated-vm (e.g., in the CLI\r\n * dev server or for browser Web Worker sandboxes).\r\n *\r\n * This file has NO isolated-vm dependency — only esbuild + Node.js builtins.\r\n */\r\n\r\nimport {build} from 'esbuild';\r\nimport path from 'node:path';\r\nimport fs from 'node:fs';\r\nimport {fileURLToPath} from 'node:url';\r\nimport {PROTOTYPE_FREEZE_BANNER, generateManualMatchEntryPoint, generateMatchEntryPoint} from './sandbox-harness.js';\r\n\r\nconst currentFilename = fileURLToPath(import.meta.url);\r\nconst currentDirname = path.dirname(currentFilename);\r\n\r\n/**\r\n * Find the src/engine/ directory. Works from src/, dist/engine/, or dist/ (tsup bundle).\r\n * esbuild needs TypeScript source files, so we look for the src/ tree.\r\n */\r\nexport function getEngineDir(): string\r\n{\r\n\t// When running from src/engine/ (dev/test), currentDirname is already src/engine/\r\n\tconst directPath = path.resolve(currentDirname);\r\n\tif (fs.existsSync(path.join(directPath, 'simulation.ts')))\r\n\t{\r\n\t\treturn directPath;\r\n\t}\r\n\r\n\t// When running from dist/engine/ (individual files), package root is 2 levels up\r\n\tconst packageRoot2 = path.resolve(currentDirname, '..', '..');\r\n\tconst srcEngine2 = path.join(packageRoot2, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine2, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine2;\r\n\t}\r\n\r\n\t// When running from dist/ (tsup bundle), package root is 1 level up\r\n\tconst packageRoot1 = path.resolve(currentDirname, '..');\r\n\tconst srcEngine1 = path.join(packageRoot1, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine1, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine1;\r\n\t}\r\n\r\n\tthrow new Error('Could not find engine source directory (src/engine/simulation.ts)');\r\n}\r\n\r\n/**\r\n * A compiled bot ready for sandboxed execution.\r\n * Stores the source path and export name — actual compilation\r\n * happens when creating a MatchSandbox or calling compileMatchBundle.\r\n */\r\nexport class BotBundle\r\n{\r\n\treadonly sourcePath: string;\r\n\treadonly exportName: string;\r\n\r\n\tconstructor(sourcePath: string, exportName: string)\r\n\t{\r\n\t\tif (!sourcePath || typeof sourcePath !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('sourcePath must be a non-empty string');\r\n\t\t}\r\n\t\tif (!exportName || typeof exportName !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('exportName must be a non-empty string');\r\n\t\t}\r\n\t\tthis.sourcePath = path.resolve(sourcePath);\r\n\t\tthis.exportName = exportName;\r\n\t}\r\n}\r\n\r\n/**\r\n * Options for esbuild compilation. Allows the caller to add esbuild\r\n * aliases (e.g., resolving @vibemancer/core to the TypeScript source).\r\n */\r\nexport interface CompileOptions\r\n{\r\n\t/** Additional esbuild alias entries (e.g., {'@vibemancer/core': '/path/to/src/index.ts'}). */\r\n\talias?: Record<string, string>;\r\n\t/** Additional modules to treat as external (not bundled). */\r\n\texternal?: string[];\r\n}\r\n\r\n/**\r\n * Compile a match bundle using esbuild. Bundles both bots + simulation engine\r\n * into a single self-contained IIFE with prototype freezing banner.\r\n *\r\n * No isolated-vm dependency — returns a plain JS string.\r\n */\r\nexport async function compileMatchBundle(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateMatchEntryPoint(\r\n\t\tbot1.sourcePath,\r\n\t\tbot1.exportName,\r\n\t\tbot2.sourcePath,\r\n\t\tbot2.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\t// Suppress warnings about top-level this in ESM\r\n\t\tlogLevel: 'error',\r\n\t\t// Match bundles run in sandboxed environments (Web Workers / isolated-vm)\r\n\t\t// and should never include Node.js native modules\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n\r\n/**\r\n * Compile a manual-play sandbox bundle. Bundles ONE opponent bot + ManualMatch\r\n * + simulation engine into a self-contained IIFE. The \"player\" wizard is a\r\n * worker-local stub that reads from `__latestHumanActions` (set per-step by\r\n * the host).\r\n *\r\n * Returns a plain JS string that, when loaded into a Web Worker, exposes the\r\n * `__manualMatchInit`, `__manualMatchStep`, `__manualMatchGuide`,\r\n * `__manualMatchRelease`, `__manualMatchSetInvincible`,\r\n * `__manualMatchGetState`, `__manualMatchGetResult`, and `__manualMatchDispose`\r\n * globals on the worker's globalThis.\r\n */\r\nexport async function compileManualMatchBundle(\r\n\topponent: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateManualMatchEntryPoint(\r\n\t\topponent.sourcePath,\r\n\t\topponent.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\tlogLevel: 'error',\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n","/**\n * VIBEMANCER — SANDBOX HARNESS\n *\n * Generates the entry point code that runs inside an isolated-vm isolate.\n * The harness bundles both bots + the simulation engine into a single IIFE\n * via esbuild, then exposes __fight and __simulate on globalThis.\n *\n * The prototype freeze banner runs before any module code, preventing\n * prototype pollution attacks between bots sharing the same isolate.\n */\n\n/**\n * JavaScript code injected as esbuild banner — runs before the IIFE bundle.\n *\n * 1. Freezes all built-in prototypes to prevent cross-bot sabotage via prototype\n * pollution. This does NOT prevent calling existing methods (e.g. Array.push\n * still works), it only prevents reassigning them.\n *\n * 2. Blocks all IO/network capabilities. Web Workers have fetch, XMLHttpRequest,\n * WebSocket, importScripts, etc. Bot code must not be able to make network\n * calls or load external scripts. Uses Object.defineProperty to make the block\n * irrecoverable (non-writable, non-configurable). In isolated-vm, these globals\n * don't exist — the try-catch makes the deletes a harmless no-op.\n */\nexport const PROTOTYPE_FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\n/**\n * Generate the TypeScript entry point for a match sandbox.\n *\n * This entry point imports both bots and the simulation engine, then\n * exposes __fight and __simulate on globalThis. esbuild bundles this\n * + all transitive imports into a single self-contained IIFE.\n *\n * @param bot1SourcePath - Absolute path to bot 1's TypeScript source file\n * @param bot1ExportName - Named export of bot 1's WizardFunction\n * @param bot2SourcePath - Absolute path to bot 2's TypeScript source file\n * @param bot2ExportName - Named export of bot 2's WizardFunction\n * @param engineDir - Absolute path to the engine directory (src/engine/)\n */\nexport function generateMatchEntryPoint(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\t// Use forward slashes for esbuild compatibility (works on all platforms)\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\n\t// All imports use the engineDir's parent (= packages/core/src/) as root.\n\t// When user bots alias @vibemancer/core → src/index-browser.ts, esbuild\n\t// deduplicates these with the bot's imports since they resolve to the same files.\n\t// This ensures the hooks runtime global state is shared between harness and bot.\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\n\nglobalThis.__fight = function __fight(options) {\n\tconst result = fight(__Bot1, __Bot2, options);\n\treturn result;\n};\n\nglobalThis.__simulate = function __simulate(options) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\tconst result = simulate(bot1, bot2, simOptions);\n\treturn result;\n};\n`;\n}\n\n/**\n * Generate the entry point for a Manual Play sandbox bundle.\n *\n * Unlike the fight/simulate entry point (which exposes one-shot batch APIs),\n * the manual-match entry point holds a single long-lived ManualMatch instance\n * inside the worker and exposes per-tick step/rewind/guide APIs.\n *\n * The \"player\" wizard is a worker-local stub that returns whatever\n * `__latestHumanActions` is set to — this avoids the can't-postMessage-functions\n * problem (the function lives entirely worker-side, only data crosses the\n * boundary). Guided missiles use a similar pattern via\n * `__latestHumanMissileTargets[projectileId]`.\n *\n * @param opponentSourcePath - Absolute path to opponent bot's TypeScript source\n * @param opponentExportName - Named export of the opponent's WizardFunction\n * @param engineDir - Absolute path to src/engine/\n */\nexport function generateManualMatchEntryPoint(\n\topponentSourcePath: string,\n\topponentExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst opponentPath = opponentSourcePath.replace(/\\\\/g, '/');\n\treturn generateManualMatchEntryPointInner(\n\t\t`import {${opponentExportName} as __RawOpponent} from '${opponentPath}';`,\n\t\tengineDir,\n\t);\n}\n\n/**\n * Browser-friendly variant — the opponent is injected at runtime via\n * globalThis.__injectedBot1 (set by prepending the player's bot bundle to\n * the compiled output of this template). Used by the web client for manual\n * play against uploaded wizards.\n */\nexport function generateBrowserManualMatchEntryPoint(engineDir: string): string\n{\n\tconst opponentImport = 'var __RawOpponent = globalThis.__injectedBot1;\\n'\n\t\t+ 'if (!__RawOpponent) throw new Error(\"Opponent not injected (set globalThis.__injectedBot1)\");';\n\treturn generateManualMatchEntryPointInner(opponentImport, engineDir);\n}\n\nfunction generateManualMatchEntryPointInner(opponentImport: string, engineDir: string): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\n${opponentImport}\nimport {ManualMatch} from '${srcPath}/engine/manual-match.ts';\nimport {idle, turnToward, flyStraight} from '${srcPath}/hooks/action-builders.ts';\nimport {getMissileContext} from '${srcPath}/engine/hooks-runtime.ts';\n\n// Worker-local state — the player AI and guided-missile AIs read from these.\nvar __latestHumanActions = null;\nvar __latestHumanMissileTargets = {};\nvar __manualMatch = null;\n\nfunction __playerStub() {\n\tif (__latestHumanActions) {\n\t\treturn {_toAction: function() { return __latestHumanActions; }};\n\t}\n\treturn idle();\n}\n\nfunction __makeGuideStub(projectileId) {\n\treturn function() {\n\t\tvar target = __latestHumanMissileTargets[projectileId];\n\t\tif (!target) return flyStraight();\n\t\treturn turnToward(target.x, target.y);\n\t};\n}\n\nfunction __ensureMatch() {\n\tif (!__manualMatch) throw new Error('ManualMatch not initialized — call manualMatchInit first');\n\treturn __manualMatch;\n}\n\nglobalThis.__manualMatchInit = function(options) {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = (options && options.initialHumanActions) || null;\n\t__latestHumanMissileTargets = {};\n\t__manualMatch = new ManualMatch(__playerStub, __RawOpponent, options || {});\n\treturn __manualMatch.getGameState();\n};\n\nglobalThis.__manualMatchStep = function(options) {\n\tvar match = __ensureMatch();\n\tif (options) {\n\t\tif (options.humanActions) __latestHumanActions = options.humanActions;\n\t\tif (options.humanMissileTargets) {\n\t\t\t// Merge — the host may only update specific projectiles per call\n\t\t\tfor (var k in options.humanMissileTargets) {\n\t\t\t\t__latestHumanMissileTargets[k] = options.humanMissileTargets[k];\n\t\t\t}\n\t\t}\n\t}\n\tvar count = (options && options.count) || 1;\n\treturn match.step(count);\n};\n\nglobalThis.__manualMatchGuide = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.replaceMissileAI(id, __makeGuideStub(id));\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchRelease = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.restoreMissileAI(id);\n\tdelete __latestHumanMissileTargets[id];\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchSetInvincible = function(options) {\n\tvar match = __ensureMatch();\n\tvar idx = (options && options.wizardIndex) || 0;\n\tvar on = !!(options && options.on);\n\tmatch.setInvincible(idx, on);\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchGetState = function() {\n\tvar match = __ensureMatch();\n\treturn match.getGameState();\n};\n\nglobalThis.__manualMatchGetResult = function() {\n\tvar match = __ensureMatch();\n\treturn match.getResult();\n};\n\nglobalThis.__manualMatchDispose = function() {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = {move: {x: 0, y: 0}};\n\t__latestHumanMissileTargets = {};\n\treturn {ok: true};\n};\n`;\n}\n\n/**\n * Generate an alternate entry point where __fight/__simulate accept and return\n * JSON strings instead of structured objects. Used by the benchmark to compare\n * JSON serialization vs V8 structured clone performance.\n */\nexport function generateMatchEntryPointJSON(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\n\nglobalThis.__fightJSON = function __fightJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\tconst result = fight(__Bot1, __Bot2, options);\n\treturn JSON.stringify(result);\n};\n\nglobalThis.__simulateJSON = function __simulateJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\tconst result = simulate(bot1, bot2, simOptions);\n\treturn JSON.stringify(result);\n};\n`;\n}\n","/**\n * Run a fight between two PRECOMPILED bot bundles — the canonical uploaded-wizard\n * format where each IIFE sets `globalThis.__injectedBot1` (see compileSingleBotBundle).\n *\n * Shared by the Cloud Functions matchmaker and the devkit CLI so that a\n * `handle/botname` fight runs through the exact same engine as the live ladder.\n *\n * Both bundles run inside an isolated-vm isolate alongside a \"match template\" —\n * the engine + a `__fight` harness that reads the injected bots. The template is\n * built lazily from the engine source on first use and cached; callers that\n * already have one (the Cloud Functions committed MATCH_TEMPLATE) pass it in to\n * skip the esbuild step.\n *\n * Execution order inside the isolate: bot1 bundle → bot2 bundle → match template.\n */\n\nimport path from 'node:path';\nimport ivm from 'isolated-vm';\nimport {build} from 'esbuild';\nimport {getEngineDir} from './sandbox-compile.js';\nimport type {FightResult, SimulateResult} from './simulation.js';\n\n/** Memory limit per sandbox isolate (MB). */\nconst MEMORY_LIMIT_MB = 256;\n/** Timeout per fight (ms). */\nconst FIGHT_TIMEOUT_MS = 60_000;\n\nconst FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\nlet cachedTemplate: Promise<string> | null = null;\n\n/**\n * Build (and cache) the match template: the engine + `__fight`/`__simulate`\n * harness bundled into a single IIFE string, ready to run after two bot bundles\n * have set `globalThis.__injectedBot1`/`__injectedBot2`.\n */\nexport function buildMatchTemplate(): Promise<string>\n{\n\tif (!cachedTemplate)\n\t{\n\t\tcachedTemplate = (async(): Promise<string> =>\n\t\t{\n\t\t\tconst engineDir = getEngineDir();\n\t\t\tconst coreSrc = path.dirname(engineDir).replace(/\\\\/g, '/');\n\n\t\t\tconst entryPoint = `\nimport {fight, simulate} from '${coreSrc}/engine/simulation.ts';\nimport {wrapWithParams} from '${coreSrc}/engine/params-runtime.ts';\n\nconst __Bot1 = (globalThis as any).__injectedBot1;\nconst __Bot2 = (globalThis as any).__injectedBot2;\n\nif (!__Bot1) throw new Error('Bot 1 not injected (set globalThis.__injectedBot1)');\nif (!__Bot2) throw new Error('Bot 2 not injected (set globalThis.__injectedBot2)');\n\n(globalThis as any).__fight = function __fight(options: any) {\n\treturn fight(__Bot1, __Bot2, options);\n};\n\n(globalThis as any).__simulate = function __simulate(options: any) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\treturn simulate(bot1, bot2, simOptions);\n};\n`;\n\n\t\t\tconst result = await build({\n\t\t\t\tstdin: {contents: entryPoint, resolveDir: engineDir, loader: 'ts'},\n\t\t\t\tbundle: true,\n\t\t\t\twrite: false,\n\t\t\t\tformat: 'iife',\n\t\t\t\tplatform: 'neutral',\n\t\t\t\ttarget: 'es2022',\n\t\t\t\tbanner: {js: FREEZE_BANNER},\n\t\t\t\tlogLevel: 'error',\n\t\t\t\texternal: [\n\t\t\t\t\t'isolated-vm', 'esbuild',\n\t\t\t\t\t'node:path', 'node:fs', 'node:url',\n\t\t\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\n\t\t\t\t],\n\t\t\t});\n\n\t\t\tif (!result.outputFiles?.[0]) throw new Error('esbuild produced no match-template output');\n\t\t\treturn result.outputFiles[0].text;\n\t\t})();\n\t}\n\treturn cachedTemplate;\n}\n\nexport interface RunBundleFightOptions\n{\n\tseed: number;\n\t/** Prebuilt match template (e.g. the committed Cloud Functions one). Built lazily if omitted. */\n\tmatchTemplate?: string;\n\t/** Skip per-tick history in the result (smaller copy out of the isolate). Default true. */\n\tskipHistory?: boolean;\n}\n\n/**\n * Run a fight between two compiled `__injectedBot1`-format bundles.\n *\n * bundle1 is saved and its global cleared before bundle2 runs, so bot code can\n * never read the opponent's function off globalThis. Both globals are deleted\n * after wiring so runtime bot code can't reach them either.\n */\nexport async function runBundleFight(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleFightOptions,\n): Promise<FightResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\tconst skipHistory = options.skipHistory ?? true;\n\n\t// Freeze prototypes/globals BEFORE the untrusted bot bundles run. The bundles\n\t// execute at module-eval (ahead of the template, whose banner used to be the\n\t// only freeze) — so without this a bot could pollute Object/Array/Math/etc. at\n\t// load time and corrupt the engine or its opponent. Mirrors the source path\n\t// (sandbox-harness), which already freezes before importing the bots.\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: FIGHT_TIMEOUT_MS});\n\n\t\tconst fightFn = await jail.get('__fight');\n\t\tconst result = await fightFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy({seed: options.seed, skipHistory}).copyInto()],\n\t\t\t{timeout: FIGHT_TIMEOUT_MS, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as FightResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n\nexport interface RunBundleSimulateOptions\n{\n\tseed?: number;\n\tspawnDistance?: number;\n\tmaxTicks?: number;\n\tmatchTemplate?: string;\n}\n\n/**\n * Run a SINGLE match between two compiled bundles, returning the full per-tick\n * history (for tracing/debugging). Same isolate wiring as runBundleFight, but\n * calls the template's `__simulate` so the caller gets a SimulateResult.\n */\nexport async function runBundleSimulate(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleSimulateOptions = {},\n): Promise<SimulateResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\n\t// Freeze before the untrusted bundles run (see runBundleFight).\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: FIGHT_TIMEOUT_MS});\n\n\t\tconst simulateFn = await jail.get('__simulate');\n\t\tconst simOptions = {\n\t\t\tseed: options.seed ?? 1,\n\t\t\tspawnDistance: options.spawnDistance,\n\t\t\tmaxTicks: options.maxTicks,\n\t\t};\n\t\tconst result = await simulateFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy(simOptions).copyInto()],\n\t\t\t{timeout: FIGHT_TIMEOUT_MS, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as SimulateResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n"],"mappings":"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","names":["path","ivm","build","path","build","ivm"]}
1
+ {"version":3,"sources":["../src/engine/sandbox.ts","../src/engine/sandbox-compile.ts","../src/engine/sandbox-harness.ts","../src/engine/bundle-fight.ts"],"sourcesContent":["/**\r\n * VIBEMANCER — SANDBOX\r\n *\r\n * Provides isolated-vm sandboxing for bot code execution. Both bots + the\r\n * entire simulation engine run inside a single V8 isolate, so there is ZERO\r\n * per-tick boundary crossing overhead. The only data crossing the boundary\r\n * is fight/simulate options going in and results coming out.\r\n *\r\n * Architecture:\r\n * - Host: creates isolate, loads compiled bundle, calls __fight/__simulate\r\n * - Isolate: contains both bots + full simulation engine, runs fight/simulate\r\n *\r\n * Safety:\r\n * - Memory limit (default 512 MB) catches memory bombs\r\n * - Timeout (default 30s) catches infinite loops\r\n * - Prototype freeze prevents cross-bot sabotage\r\n * - platform: 'neutral' strips Node.js APIs (no fs/net/process)\r\n */\r\n\r\nimport ivm from 'isolated-vm';\r\nimport type {FightResult, SimulateResult} from './simulation.js';\r\nimport {BotBundle, compileMatchBundle} from './sandbox-compile.js';\r\nimport {DEFAULT_FIGHT_BACKSTOP_MS} from './bot-compute-budget.js';\r\nimport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n// Re-export BotBundle so existing imports from sandbox.ts keep working\r\nexport {BotBundle} from './sandbox-compile.js';\r\nexport {compileMatchBundle, compileManualMatchBundle} from './sandbox-compile.js';\r\nexport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n/**\r\n * Options for sandbox creation.\r\n */\r\nexport interface SandboxOptions\r\n{\r\n\t/** Memory limit in MB for the isolate (default: 512). */\r\n\tmemoryLimitMB?: number;\r\n\t/** Timeout in ms for fight/simulate calls (default: 60000). */\r\n\ttimeoutMs?: number;\r\n\t/** Options passed to esbuild compilation (aliases, externals). */\r\n\tcompileOptions?: CompileOptions;\r\n}\r\n\r\n/**\r\n * A sandboxed match runner. Both bots + the entire simulation engine run\r\n * inside a single isolated-vm isolate.\r\n *\r\n * Usage:\r\n * ```ts\r\n * const sandbox = await MatchSandbox.create(botA, botB);\r\n * const result = sandbox.fight({ seed: 42 });\r\n * sandbox.dispose();\r\n * ```\r\n */\r\nexport class MatchSandbox\r\n{\r\n\tprivate isolate: ivm.Isolate;\r\n\tprivate context: ivm.Context;\r\n\tprivate fightFn: ivm.Reference;\r\n\tprivate simulateFn: ivm.Reference;\r\n\tprivate timeout: number;\r\n\tprivate disposed = false;\r\n\r\n\tprivate constructor(\r\n\t\tisolate: ivm.Isolate,\r\n\t\tcontext: ivm.Context,\r\n\t\tfightFn: ivm.Reference,\r\n\t\tsimulateFn: ivm.Reference,\r\n\t\ttimeout: number,\r\n\t)\r\n\t{\r\n\t\tthis.isolate = isolate;\r\n\t\tthis.context = context;\r\n\t\tthis.fightFn = fightFn;\r\n\t\tthis.simulateFn = simulateFn;\r\n\t\tthis.timeout = timeout;\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox with both bots loaded. Compiles the match bundle\r\n\t * automatically using esbuild.\r\n\t */\r\n\tstatic async create(\r\n\t\tbot1: BotBundle,\r\n\t\tbot2: BotBundle,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\t// Was a bare 60000, which the per-bot compute budget made wrong: a budgeted fight can\r\n\t\t// legitimately let one runaway bot spend 45s before it stops being called, and 60s\r\n\t\t// here would kill the whole fight first — putting the wall clock back in charge of\r\n\t\t// outcomes. Shared constant so this cannot drift from the other copies again.\r\n\t\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_FIGHT_BACKSTOP_MS;\r\n\r\n\t\t// 1. Compile the match bundle\r\n\t\tconst bundle = await compileMatchBundle(bot1, bot2, options?.compileOptions);\r\n\r\n\t\t// 2. Create isolate with memory limit\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// 3. Create context and load the bundle\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\t// 4. Get references to the exposed functions\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\t// OOM or timeout can auto-dispose the isolate, so guard the cleanup dispose.\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox from a pre-compiled bundle string.\r\n\t * Useful for caching compiled bundles across multiple MatchSandbox instances.\r\n\t */\r\n\tstatic async fromBundle(\r\n\t\tbundle: string,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\t// Was a bare 60000, which the per-bot compute budget made wrong: a budgeted fight can\r\n\t\t// legitimately let one runaway bot spend 45s before it stops being called, and 60s\r\n\t\t// here would kill the whole fight first — putting the wall clock back in charge of\r\n\t\t// outcomes. Shared constant so this cannot drift from the other copies again.\r\n\t\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_FIGHT_BACKSTOP_MS;\r\n\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Compile a match bundle without creating an isolate.\r\n\t * Returns the compiled JS string for caching/reuse.\r\n\t */\r\n\tstatic async compile(bot1: BotBundle, bot2: BotBundle, compileOptions?: CompileOptions): Promise<string>\r\n\t{\r\n\t\treturn compileMatchBundle(bot1, bot2, compileOptions);\r\n\t}\r\n\r\n\t/**\r\n\t * Run a full fight (10 matches: 5 spawn distances x 2 sides).\r\n\t * Synchronous after isolate creation — runs entirely inside the isolate.\r\n\t */\r\n\tfight(options?: {seed?: number; maxTicks?: number}): FightResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.fightFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as FightResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Run a single simulation.\r\n\t * Synchronous after isolate creation.\r\n\t *\r\n\t * @param options.params1 - useParam overrides for bot 1 (wizard-1)\r\n\t * @param options.params2 - useParam overrides for bot 2 (wizard-2)\r\n\t */\r\n\tsimulate(options?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t}): SimulateResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.simulateFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as SimulateResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Dispose the isolate and free all memory.\r\n\t * The sandbox cannot be used after disposal.\r\n\t */\r\n\tdispose(): void\r\n\t{\r\n\t\tif (!this.disposed)\r\n\t\t{\r\n\t\t\tthis.disposed = true;\r\n\t\t\t// OOM can auto-dispose the isolate, so guard all cleanup\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.fightFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.simulateFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.context.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.isolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Whether this sandbox has been disposed.\r\n\t */\r\n\tget isDisposed(): boolean\r\n\t{\r\n\t\treturn this.disposed;\r\n\t}\r\n\r\n\tprivate ensureNotDisposed(): void\r\n\t{\r\n\t\tif (this.disposed)\r\n\t\t{\r\n\t\t\tthrow new Error('MatchSandbox has been disposed');\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed fight. Creates isolate, runs fight, disposes.\r\n * Convenience wrapper for single-use scenarios.\r\n */\r\nexport async function sandboxFight(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {seed?: number; maxTicks?: number} & SandboxOptions,\r\n): Promise<FightResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.fight({seed: options?.seed, maxTicks: options?.maxTicks});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed simulate. Creates isolate, runs simulate, disposes.\r\n */\r\nexport async function sandboxSimulate(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t} & SandboxOptions,\r\n): Promise<SimulateResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.simulate({\r\n\t\t\tseed: options?.seed,\r\n\t\t\tmaxTicks: options?.maxTicks,\r\n\t\t\tspawnDistance: options?.spawnDistance,\r\n\t\t\tskipHistory: options?.skipHistory,\r\n\t\t\tparams1: options?.params1,\r\n\t\t\tparams2: options?.params2,\r\n\t\t});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n","/**\r\n * VIBEMANCER — SANDBOX COMPILATION\r\n *\r\n * Compiles match bundles using esbuild. Extracted from sandbox.ts so that\r\n * compilation can be used independently of isolated-vm (e.g., in the CLI\r\n * dev server or for browser Web Worker sandboxes).\r\n *\r\n * This file has NO isolated-vm dependency — only esbuild + Node.js builtins.\r\n */\r\n\r\nimport {build} from 'esbuild';\r\nimport path from 'node:path';\r\nimport fs from 'node:fs';\r\nimport {fileURLToPath} from 'node:url';\r\nimport {PROTOTYPE_FREEZE_BANNER, generateManualMatchEntryPoint, generateMatchEntryPoint} from './sandbox-harness.js';\r\n\r\nconst currentFilename = fileURLToPath(import.meta.url);\r\nconst currentDirname = path.dirname(currentFilename);\r\n\r\n/**\r\n * Find the src/engine/ directory. Works from src/, dist/engine/, or dist/ (tsup bundle).\r\n * esbuild needs TypeScript source files, so we look for the src/ tree.\r\n */\r\nexport function getEngineDir(): string\r\n{\r\n\t// When running from src/engine/ (dev/test), currentDirname is already src/engine/\r\n\tconst directPath = path.resolve(currentDirname);\r\n\tif (fs.existsSync(path.join(directPath, 'simulation.ts')))\r\n\t{\r\n\t\treturn directPath;\r\n\t}\r\n\r\n\t// When running from dist/engine/ (individual files), package root is 2 levels up\r\n\tconst packageRoot2 = path.resolve(currentDirname, '..', '..');\r\n\tconst srcEngine2 = path.join(packageRoot2, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine2, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine2;\r\n\t}\r\n\r\n\t// When running from dist/ (tsup bundle), package root is 1 level up\r\n\tconst packageRoot1 = path.resolve(currentDirname, '..');\r\n\tconst srcEngine1 = path.join(packageRoot1, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine1, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine1;\r\n\t}\r\n\r\n\tthrow new Error('Could not find engine source directory (src/engine/simulation.ts)');\r\n}\r\n\r\n/**\r\n * A compiled bot ready for sandboxed execution.\r\n * Stores the source path and export name — actual compilation\r\n * happens when creating a MatchSandbox or calling compileMatchBundle.\r\n */\r\nexport class BotBundle\r\n{\r\n\treadonly sourcePath: string;\r\n\treadonly exportName: string;\r\n\r\n\tconstructor(sourcePath: string, exportName: string)\r\n\t{\r\n\t\tif (!sourcePath || typeof sourcePath !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('sourcePath must be a non-empty string');\r\n\t\t}\r\n\t\tif (!exportName || typeof exportName !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('exportName must be a non-empty string');\r\n\t\t}\r\n\t\tthis.sourcePath = path.resolve(sourcePath);\r\n\t\tthis.exportName = exportName;\r\n\t}\r\n}\r\n\r\n/**\r\n * Options for esbuild compilation. Allows the caller to add esbuild\r\n * aliases (e.g., resolving @vibemancer/core to the TypeScript source).\r\n */\r\nexport interface CompileOptions\r\n{\r\n\t/** Additional esbuild alias entries (e.g., {'@vibemancer/core': '/path/to/src/index.ts'}). */\r\n\talias?: Record<string, string>;\r\n\t/** Additional modules to treat as external (not bundled). */\r\n\texternal?: string[];\r\n}\r\n\r\n/**\r\n * Compile a match bundle using esbuild. Bundles both bots + simulation engine\r\n * into a single self-contained IIFE with prototype freezing banner.\r\n *\r\n * No isolated-vm dependency — returns a plain JS string.\r\n */\r\nexport async function compileMatchBundle(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateMatchEntryPoint(\r\n\t\tbot1.sourcePath,\r\n\t\tbot1.exportName,\r\n\t\tbot2.sourcePath,\r\n\t\tbot2.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\t// Suppress warnings about top-level this in ESM\r\n\t\tlogLevel: 'error',\r\n\t\t// Match bundles run in sandboxed environments (Web Workers / isolated-vm)\r\n\t\t// and should never include Node.js native modules\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n\r\n/**\r\n * Compile a manual-play sandbox bundle. Bundles ONE opponent bot + ManualMatch\r\n * + simulation engine into a self-contained IIFE. The \"player\" wizard is a\r\n * worker-local stub that reads from `__latestHumanActions` (set per-step by\r\n * the host).\r\n *\r\n * Returns a plain JS string that, when loaded into a Web Worker, exposes the\r\n * `__manualMatchInit`, `__manualMatchStep`, `__manualMatchGuide`,\r\n * `__manualMatchRelease`, `__manualMatchSetInvincible`,\r\n * `__manualMatchGetState`, `__manualMatchGetResult`, and `__manualMatchDispose`\r\n * globals on the worker's globalThis.\r\n */\r\nexport async function compileManualMatchBundle(\r\n\topponent: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateManualMatchEntryPoint(\r\n\t\topponent.sourcePath,\r\n\t\topponent.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\tlogLevel: 'error',\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n","/**\n * VIBEMANCER — SANDBOX HARNESS\n *\n * Generates the entry point code that runs inside an isolated-vm isolate.\n * The harness bundles both bots + the simulation engine into a single IIFE\n * via esbuild, then exposes __fight and __simulate on globalThis.\n *\n * The prototype freeze banner runs before any module code, preventing\n * prototype pollution attacks between bots sharing the same isolate.\n */\n\n/**\n * JavaScript code injected as esbuild banner — runs before the IIFE bundle.\n *\n * 1. Freezes all built-in prototypes to prevent cross-bot sabotage via prototype\n * pollution. This does NOT prevent calling existing methods (e.g. Array.push\n * still works), it only prevents reassigning them.\n *\n * 2. Blocks all IO/network capabilities. Web Workers have fetch, XMLHttpRequest,\n * WebSocket, importScripts, etc. Bot code must not be able to make network\n * calls or load external scripts. Uses Object.defineProperty to make the block\n * irrecoverable (non-writable, non-configurable). In isolated-vm, these globals\n * don't exist — the try-catch makes the deletes a harmless no-op.\n */\nexport const PROTOTYPE_FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\n/**\n * Generate the TypeScript entry point for a match sandbox.\n *\n * This entry point imports both bots and the simulation engine, then\n * exposes __fight and __simulate on globalThis. esbuild bundles this\n * + all transitive imports into a single self-contained IIFE.\n *\n * @param bot1SourcePath - Absolute path to bot 1's TypeScript source file\n * @param bot1ExportName - Named export of bot 1's WizardFunction\n * @param bot2SourcePath - Absolute path to bot 2's TypeScript source file\n * @param bot2ExportName - Named export of bot 2's WizardFunction\n * @param engineDir - Absolute path to the engine directory (src/engine/)\n */\nexport function generateMatchEntryPoint(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\t// Use forward slashes for esbuild compatibility (works on all platforms)\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\n\t// All imports use the engineDir's parent (= packages/core/src/) as root.\n\t// When user bots alias @vibemancer/core → src/index-browser.ts, esbuild\n\t// deduplicates these with the bot's imports since they resolve to the same files.\n\t// This ensures the hooks runtime global state is shared between harness and bot.\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\nimport {DEFAULT_BUDGET} from '${srcPath}/engine/bot-compute-budget.ts';\n\nglobalThis.__fight = function __fight(options) {\n\t// Budgeted like every other fight path. This one matters MORE than it looks: it is what\n\t// sandboxFight uses, so it is the MCP fight tools and the CLI — the place a user's\n\t// runaway bot most directly burns server time. Leaving it unbudgeted while the backstop\n\t// moved from 60s to 110s would have made this path strictly worse than before.\n\tconst result = fight(__Bot1, __Bot2, {budgetLimits: DEFAULT_BUDGET, ...options});\n\treturn result;\n};\n\nglobalThis.__simulate = function __simulate(options) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\t// Budgeted like __fight. This path is trace and the optimizer — a single match rather\n\t// than ten — and leaving it out would have made it strictly worse than before, since the\n\t// same change raised its backstop from 60s to 110s.\n\tconst result = simulate(bot1, bot2, {budgetLimits: DEFAULT_BUDGET, ...simOptions});\n\treturn result;\n};\n`;\n}\n\n/**\n * Generate the entry point for a Manual Play sandbox bundle.\n *\n * Unlike the fight/simulate entry point (which exposes one-shot batch APIs),\n * the manual-match entry point holds a single long-lived ManualMatch instance\n * inside the worker and exposes per-tick step/rewind/guide APIs.\n *\n * The \"player\" wizard is a worker-local stub that returns whatever\n * `__latestHumanActions` is set to — this avoids the can't-postMessage-functions\n * problem (the function lives entirely worker-side, only data crosses the\n * boundary). Guided missiles use a similar pattern via\n * `__latestHumanMissileTargets[projectileId]`.\n *\n * @param opponentSourcePath - Absolute path to opponent bot's TypeScript source\n * @param opponentExportName - Named export of the opponent's WizardFunction\n * @param engineDir - Absolute path to src/engine/\n */\nexport function generateManualMatchEntryPoint(\n\topponentSourcePath: string,\n\topponentExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst opponentPath = opponentSourcePath.replace(/\\\\/g, '/');\n\treturn generateManualMatchEntryPointInner(\n\t\t`import {${opponentExportName} as __RawOpponent} from '${opponentPath}';`,\n\t\tengineDir,\n\t);\n}\n\n/**\n * Browser-friendly variant — the opponent is injected at runtime via\n * globalThis.__injectedBot1 (set by prepending the player's bot bundle to\n * the compiled output of this template). Used by the web client for manual\n * play against uploaded wizards.\n */\nexport function generateBrowserManualMatchEntryPoint(engineDir: string): string\n{\n\tconst opponentImport = 'var __RawOpponent = globalThis.__injectedBot1;\\n'\n\t\t+ 'if (!__RawOpponent) throw new Error(\"Opponent not injected (set globalThis.__injectedBot1)\");';\n\treturn generateManualMatchEntryPointInner(opponentImport, engineDir);\n}\n\nfunction generateManualMatchEntryPointInner(opponentImport: string, engineDir: string): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\n${opponentImport}\nimport {ManualMatch} from '${srcPath}/engine/manual-match.ts';\nimport {idle, turnToward, flyStraight} from '${srcPath}/hooks/action-builders.ts';\nimport {getMissileContext} from '${srcPath}/engine/hooks-runtime.ts';\n\n// Worker-local state — the player AI and guided-missile AIs read from these.\nvar __latestHumanActions = null;\nvar __latestHumanMissileTargets = {};\nvar __manualMatch = null;\n\nfunction __playerStub() {\n\tif (__latestHumanActions) {\n\t\treturn {_toAction: function() { return __latestHumanActions; }};\n\t}\n\treturn idle();\n}\n\nfunction __makeGuideStub(projectileId) {\n\treturn function() {\n\t\tvar target = __latestHumanMissileTargets[projectileId];\n\t\tif (!target) return flyStraight();\n\t\treturn turnToward(target.x, target.y);\n\t};\n}\n\nfunction __ensureMatch() {\n\tif (!__manualMatch) throw new Error('ManualMatch not initialized — call manualMatchInit first');\n\treturn __manualMatch;\n}\n\nglobalThis.__manualMatchInit = function(options) {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = (options && options.initialHumanActions) || null;\n\t__latestHumanMissileTargets = {};\n\t__manualMatch = new ManualMatch(__playerStub, __RawOpponent, options || {});\n\treturn __manualMatch.getGameState();\n};\n\nglobalThis.__manualMatchStep = function(options) {\n\tvar match = __ensureMatch();\n\tif (options) {\n\t\tif (options.humanActions) __latestHumanActions = options.humanActions;\n\t\tif (options.humanMissileTargets) {\n\t\t\t// Merge — the host may only update specific projectiles per call\n\t\t\tfor (var k in options.humanMissileTargets) {\n\t\t\t\t__latestHumanMissileTargets[k] = options.humanMissileTargets[k];\n\t\t\t}\n\t\t}\n\t}\n\tvar count = (options && options.count) || 1;\n\treturn match.step(count);\n};\n\nglobalThis.__manualMatchGuide = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.replaceMissileAI(id, __makeGuideStub(id));\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchRelease = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.restoreMissileAI(id);\n\tdelete __latestHumanMissileTargets[id];\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchSetInvincible = function(options) {\n\tvar match = __ensureMatch();\n\tvar idx = (options && options.wizardIndex) || 0;\n\tvar on = !!(options && options.on);\n\tmatch.setInvincible(idx, on);\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchGetState = function() {\n\tvar match = __ensureMatch();\n\treturn match.getGameState();\n};\n\nglobalThis.__manualMatchGetResult = function() {\n\tvar match = __ensureMatch();\n\treturn match.getResult();\n};\n\nglobalThis.__manualMatchDispose = function() {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = {move: {x: 0, y: 0}};\n\t__latestHumanMissileTargets = {};\n\treturn {ok: true};\n};\n`;\n}\n\n/**\n * Generate an alternate entry point where __fight/__simulate accept and return\n * JSON strings instead of structured objects. Used by the benchmark to compare\n * JSON serialization vs V8 structured clone performance.\n */\nexport function generateMatchEntryPointJSON(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\nimport {DEFAULT_BUDGET} from '${srcPath}/engine/bot-compute-budget.ts';\n\nglobalThis.__fightJSON = function __fightJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\t// Budgeted, same as the non-JSON variant — two generators, one contract.\n\tconst result = fight(__Bot1, __Bot2, {budgetLimits: DEFAULT_BUDGET, ...options});\n\treturn JSON.stringify(result);\n};\n\nglobalThis.__simulateJSON = function __simulateJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\t// Budgeted, same as the non-JSON variant — two generators, one contract.\n\tconst result = simulate(bot1, bot2, {budgetLimits: DEFAULT_BUDGET, ...simOptions});\n\treturn JSON.stringify(result);\n};\n`;\n}\n","/**\n * Run a fight between two PRECOMPILED bot bundles — the canonical uploaded-wizard\n * format where each IIFE sets `globalThis.__injectedBot1` (see compileSingleBotBundle).\n *\n * Shared by the Cloud Functions matchmaker and the devkit CLI so that a\n * `handle/botname` fight runs through the exact same engine as the live ladder.\n *\n * Both bundles run inside an isolated-vm isolate alongside a \"match template\" —\n * the engine + a `__fight` harness that reads the injected bots. The template is\n * built lazily from the engine source on first use and cached; callers that\n * already have one (the Cloud Functions committed MATCH_TEMPLATE) pass it in to\n * skip the esbuild step.\n *\n * Execution order inside the isolate: bot1 bundle → bot2 bundle → match template.\n */\n\nimport path from 'node:path';\nimport ivm from 'isolated-vm';\nimport {build} from 'esbuild';\nimport {getEngineDir} from './sandbox-compile.js';\nimport type {FightResult, SimulateResult} from './simulation.js';\nimport {DEFAULT_FIGHT_BACKSTOP_MS} from './bot-compute-budget.js';\n\n/** Memory limit per sandbox isolate (MB). */\nconst MEMORY_LIMIT_MB = 256;\n/**\n * Default timeout per fight (ms).\n *\n * This is a real safety limit: it stops a malicious or looping bot burning server time in\n * the matchmaker, so the DEFAULT must not move. It was previously hardcoded and unreachable\n * from any option, which made the suite unusable on a busy machine — one e2e fight exceeded\n * it and blocked seven consecutive commits, while a comparable fight run to the tick cap\n * finished in 1.3s on the same machine at the same moment. The limit was not catching a\n * runaway bot; it was catching load.\n */\nexport const DEFAULT_FIGHT_TIMEOUT_MS = DEFAULT_FIGHT_BACKSTOP_MS;\n\n/**\n * Resolve the effective fight timeout.\n *\n * Only ever EXTENDS the default. A caller who knows the work is legitimate (a test on a\n * slow machine) can ask for more; nobody can quietly ask for less, because weakening a\n * safety limit by accident is the direction that turns it into a flaky one.\n */\nexport function resolveFightTimeout(requested: number | undefined): number\n{\n\tif (typeof requested !== 'number' || !Number.isFinite(requested)) return DEFAULT_FIGHT_TIMEOUT_MS;\n\treturn Math.max(DEFAULT_FIGHT_TIMEOUT_MS, requested);\n}\n\nconst FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\nlet cachedTemplate: Promise<string> | null = null;\n\n/**\n * Build (and cache) the match template: the engine + `__fight`/`__simulate`\n * harness bundled into a single IIFE string, ready to run after two bot bundles\n * have set `globalThis.__injectedBot1`/`__injectedBot2`.\n */\nexport function buildMatchTemplate(): Promise<string>\n{\n\tif (!cachedTemplate)\n\t{\n\t\tcachedTemplate = (async(): Promise<string> =>\n\t\t{\n\t\t\tconst engineDir = getEngineDir();\n\t\t\tconst coreSrc = path.dirname(engineDir).replace(/\\\\/g, '/');\n\n\t\t\tconst entryPoint = `\nimport {fight, simulate} from '${coreSrc}/engine/simulation.ts';\nimport {wrapWithParams} from '${coreSrc}/engine/params-runtime.ts';\nimport {DEFAULT_BUDGET} from '${coreSrc}/engine/bot-compute-budget.ts';\n\nconst __Bot1 = (globalThis as any).__injectedBot1;\nconst __Bot2 = (globalThis as any).__injectedBot2;\n\nif (!__Bot1) throw new Error('Bot 1 not injected (set globalThis.__injectedBot1)');\nif (!__Bot2) throw new Error('Bot 2 not injected (set globalThis.__injectedBot2)');\n\n(globalThis as any).__fight = function __fight(options: any) {\n\t// Same per-bot compute budget the server uses, so a runaway bot fails the same way in\n\t// the CLI as it will on the ladder — and so it cannot hang someone's terminal.\n\treturn fight(__Bot1, __Bot2, {budgetLimits: DEFAULT_BUDGET, ...options});\n};\n\n(globalThis as any).__simulate = function __simulate(options: any) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\t// Budgeted like __fight — this is the CLI's trace/optimize path, and its backstop went\n\t// up to 110s with everything else, so leaving it out would make it worse than before.\n\treturn simulate(bot1, bot2, {budgetLimits: DEFAULT_BUDGET, ...simOptions});\n};\n`;\n\n\t\t\tconst result = await build({\n\t\t\t\tstdin: {contents: entryPoint, resolveDir: engineDir, loader: 'ts'},\n\t\t\t\tbundle: true,\n\t\t\t\twrite: false,\n\t\t\t\tformat: 'iife',\n\t\t\t\tplatform: 'neutral',\n\t\t\t\ttarget: 'es2022',\n\t\t\t\tbanner: {js: FREEZE_BANNER},\n\t\t\t\tlogLevel: 'error',\n\t\t\t\texternal: [\n\t\t\t\t\t'isolated-vm', 'esbuild',\n\t\t\t\t\t'node:path', 'node:fs', 'node:url',\n\t\t\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\n\t\t\t\t],\n\t\t\t});\n\n\t\t\tif (!result.outputFiles?.[0]) throw new Error('esbuild produced no match-template output');\n\t\t\treturn result.outputFiles[0].text;\n\t\t})();\n\t}\n\treturn cachedTemplate;\n}\n\nexport interface RunBundleFightOptions\n{\n\tseed: number;\n\t/** Prebuilt match template (e.g. the committed Cloud Functions one). Built lazily if omitted. */\n\tmatchTemplate?: string;\n\t/** Extend the per-fight isolate timeout. Can only raise it above the default. */\n\tfightTimeoutMs?: number;\n\t/** Skip per-tick history in the result (smaller copy out of the isolate). Default true. */\n\tskipHistory?: boolean;\n}\n\n/**\n * Run a fight between two compiled `__injectedBot1`-format bundles.\n *\n * bundle1 is saved and its global cleared before bundle2 runs, so bot code can\n * never read the opponent's function off globalThis. Both globals are deleted\n * after wiring so runtime bot code can't reach them either.\n */\nexport async function runBundleFight(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleFightOptions,\n): Promise<FightResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\tconst fightTimeoutMs = resolveFightTimeout(options.fightTimeoutMs);\n\tconst skipHistory = options.skipHistory ?? true;\n\n\t// Freeze prototypes/globals BEFORE the untrusted bot bundles run. The bundles\n\t// execute at module-eval (ahead of the template, whose banner used to be the\n\t// only freeze) — so without this a bot could pollute Object/Array/Math/etc. at\n\t// load time and corrupt the engine or its opponent. Mirrors the source path\n\t// (sandbox-harness), which already freezes before importing the bots.\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: fightTimeoutMs});\n\n\t\tconst fightFn = await jail.get('__fight');\n\t\tconst result = await fightFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy({seed: options.seed, skipHistory}).copyInto()],\n\t\t\t{timeout: fightTimeoutMs, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as FightResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n\nexport interface RunBundleSimulateOptions\n{\n\tseed?: number;\n\tspawnDistance?: number;\n\tmaxTicks?: number;\n\tmatchTemplate?: string;\n\t/** Extend the per-fight isolate timeout. Can only raise it above the default. */\n\tfightTimeoutMs?: number;\n}\n\n/**\n * Run a SINGLE match between two compiled bundles, returning the full per-tick\n * history (for tracing/debugging). Same isolate wiring as runBundleFight, but\n * calls the template's `__simulate` so the caller gets a SimulateResult.\n */\nexport async function runBundleSimulate(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleSimulateOptions = {},\n): Promise<SimulateResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\tconst fightTimeoutMs = resolveFightTimeout(options.fightTimeoutMs);\n\n\t// Freeze before the untrusted bundles run (see runBundleFight).\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: fightTimeoutMs});\n\n\t\tconst simulateFn = await jail.get('__simulate');\n\t\tconst simOptions = {\n\t\t\tseed: options.seed ?? 1,\n\t\t\tspawnDistance: options.spawnDistance,\n\t\t\tmaxTicks: options.maxTicks,\n\t\t};\n\t\tconst result = await simulateFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy(simOptions).copyInto()],\n\t\t\t{timeout: fightTimeoutMs, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as SimulateResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n"],"mappings":"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","names":["path","ivm","build","path","build","ivm"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibemancer/core",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Vibemancer game engine - competitive programming wizard battle arena",
5
5
  "type": "module",
6
6
  "author": "Low Entry",
package/src/bots/index.ts CHANGED
@@ -27,4 +27,9 @@ export * from './berserker/03_Stormforger.js';
27
27
  export * from './kiter/01_Spellspinner.js';
28
28
  export * from './kiter/02_Spellweaver.js';
29
29
  export * from './kiter/03_Spellbinder.js';
30
+ // Hero sits at the top level rather than in a progression directory, and was left out of
31
+ // this barrel when it was added. That mattered: the MCP fight path builds an opponent by
32
+ // importing `{<name>}` from THIS file, so the one bot the roster tells players to beat could
33
+ // not be fought — and the failure was swallowed into an outcome instead of surfacing.
34
+ export * from './Hero.js';
30
35
  export * from './registry.js';