@vibemancer/core 1.0.3 → 1.0.5
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.
- package/dist/{chunk-E7R4JFAN.js → chunk-NEXWZU5I.js} +85 -20
- package/dist/chunk-NEXWZU5I.js.map +1 -0
- package/dist/index-browser-AFWi8jxB.d.ts +3077 -0
- package/dist/index-browser.d.ts +1 -2878
- package/dist/index-browser.js +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +35 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bots/index.ts +5 -0
- package/src/engine/bot-compute-budget.ts +173 -0
- package/src/engine/bundle-fight.ts +9 -3
- package/src/engine/sandbox-browser.ts +7 -1
- package/src/engine/sandbox-harness.ts +15 -4
- package/src/engine/sandbox.ts +11 -2
- package/src/engine/simulation.ts +113 -17
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +3 -2
- package/src/hooks/state-hooks.ts +16 -5
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +2 -0
- package/dist/chunk-E7R4JFAN.js.map +0 -1
package/dist/index-browser.js
CHANGED
|
@@ -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-
|
|
226
|
+
} from "./chunk-NEXWZU5I.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-AFWi8jxB.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-AFWi8jxB.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A compiled bot ready for sandboxed execution.
|
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-
|
|
232
|
+
} from "./chunk-NEXWZU5I.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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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 ??
|
|
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,7 @@ 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 DEFAULT_FIGHT_TIMEOUT_MS =
|
|
552
|
+
var DEFAULT_FIGHT_TIMEOUT_MS = DEFAULT_FIGHT_BACKSTOP_MS;
|
|
538
553
|
function resolveFightTimeout(requested) {
|
|
539
554
|
if (typeof requested !== "number" || !Number.isFinite(requested)) return DEFAULT_FIGHT_TIMEOUT_MS;
|
|
540
555
|
return Math.max(DEFAULT_FIGHT_TIMEOUT_MS, requested);
|
|
@@ -577,6 +592,7 @@ function buildMatchTemplate() {
|
|
|
577
592
|
const entryPoint = `
|
|
578
593
|
import {fight, simulate} from '${coreSrc}/engine/simulation.ts';
|
|
579
594
|
import {wrapWithParams} from '${coreSrc}/engine/params-runtime.ts';
|
|
595
|
+
import {DEFAULT_BUDGET} from '${coreSrc}/engine/bot-compute-budget.ts';
|
|
580
596
|
|
|
581
597
|
const __Bot1 = (globalThis as any).__injectedBot1;
|
|
582
598
|
const __Bot2 = (globalThis as any).__injectedBot2;
|
|
@@ -585,14 +601,18 @@ if (!__Bot1) throw new Error('Bot 1 not injected (set globalThis.__injectedBot1)
|
|
|
585
601
|
if (!__Bot2) throw new Error('Bot 2 not injected (set globalThis.__injectedBot2)');
|
|
586
602
|
|
|
587
603
|
(globalThis as any).__fight = function __fight(options: any) {
|
|
588
|
-
|
|
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});
|
|
589
607
|
};
|
|
590
608
|
|
|
591
609
|
(globalThis as any).__simulate = function __simulate(options: any) {
|
|
592
610
|
const {params1, params2, ...simOptions} = options;
|
|
593
611
|
const bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;
|
|
594
612
|
const bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;
|
|
595
|
-
|
|
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});
|
|
596
616
|
};
|
|
597
617
|
`;
|
|
598
618
|
const result = await build2({
|
|
@@ -695,6 +715,8 @@ export {
|
|
|
695
715
|
CASTING_MOVEMENT_MULT,
|
|
696
716
|
COLLISION_RADIUS,
|
|
697
717
|
Critter,
|
|
718
|
+
DEFAULT_BUDGET,
|
|
719
|
+
DEFAULT_FIGHT_BACKSTOP_MS,
|
|
698
720
|
DEFAULT_SEED,
|
|
699
721
|
Doombringer,
|
|
700
722
|
ENGINE_VERSION,
|
|
@@ -702,6 +724,7 @@ export {
|
|
|
702
724
|
Flamecaller,
|
|
703
725
|
GCD_DURATION,
|
|
704
726
|
Golem,
|
|
727
|
+
Hero,
|
|
705
728
|
Hogger,
|
|
706
729
|
Infernalist,
|
|
707
730
|
KNOCKBACK_DAMAGE_THRESHOLD,
|
|
@@ -791,7 +814,9 @@ export {
|
|
|
791
814
|
clearParamValues,
|
|
792
815
|
compileMatchBundle,
|
|
793
816
|
completeCast,
|
|
817
|
+
createBudgetState,
|
|
794
818
|
createEntitySeed,
|
|
819
|
+
createFightBudget,
|
|
795
820
|
createInitialState,
|
|
796
821
|
createRandom,
|
|
797
822
|
createWorkerScript,
|
|
@@ -833,6 +858,7 @@ export {
|
|
|
833
858
|
isInLava,
|
|
834
859
|
magnitude,
|
|
835
860
|
matchesAnyBanRule,
|
|
861
|
+
mayAct,
|
|
836
862
|
missile,
|
|
837
863
|
move,
|
|
838
864
|
moveInDirection,
|
|
@@ -843,6 +869,7 @@ export {
|
|
|
843
869
|
normalize,
|
|
844
870
|
normalizeAngle,
|
|
845
871
|
predictPosition,
|
|
872
|
+
recordSpend,
|
|
846
873
|
resetAllHooks,
|
|
847
874
|
resetRuleset,
|
|
848
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/**\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 = 60_000;\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';\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/** 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"]}
|
|
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
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';
|