@vibemancer/core 1.0.5 → 1.0.7

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.
@@ -608,7 +608,44 @@ declare function currentRuleset(): Record<string, number>;
608
608
  * Used to gate spectator replays: a recorded match can only be re-simulated when
609
609
  * the runtime engine version matches the version that produced the match.
610
610
  */
611
- declare const ENGINE_VERSION = 4396905295552957;
611
+ declare const ENGINE_VERSION = 4204441759696527;
612
+ /**
613
+ * Historical versions that denote the SAME engine as `ENGINE_VERSION`.
614
+ *
615
+ * A content hash answers "is this the same source?", not "is this the same behaviour?" —
616
+ * and those come apart when the HASHING is corrected rather than the engine. Spectate
617
+ * consults this through `canReplayMatch` so a stored match is not thrown away over a
618
+ * number that moved for a reason no player could act on.
619
+ *
620
+ * Generated from `scripts/engine-version-equivalents.json`, which holds the reason for
621
+ * each entry and the rule for adding one. A wrong entry fails OPEN; read it first.
622
+ */
623
+ declare const EQUIVALENT_ENGINE_VERSIONS: readonly number[];
624
+
625
+ /**
626
+ * Whether a stored match can still be replayed by the running engine.
627
+ *
628
+ * Spectate re-simulates a recorded match from its seed, so it may only do that when the
629
+ * engine is the one that produced it. The check used to be `match.engineVersion ===
630
+ * ENGINE_VERSION`, which is right in spirit and slightly too strict in fact.
631
+ *
632
+ * ENGINE_VERSION is a CONTENT HASH, so it answers "is this the same source?" rather than
633
+ * "is this the same behaviour?". Those come apart when the hashing itself is corrected. On
634
+ * 2026-09-09 the comment stripper was fixed — it had been hashing most comments as if they
635
+ * were code — and the value moved from 1688330189011034 to 3884810833238487 without
636
+ * packages/core/src changing by a byte. Under an exact comparison that would have stranded
637
+ * 26,948 of the 42,896 stored matches, 63% of them, for no reason a player could act on.
638
+ *
639
+ * So the gate consults a short list of historical values known to describe the same engine.
640
+ * See scripts/engine-version-equivalents.json for what may go in it, and for why a wrong
641
+ * entry is the dangerous kind of wrong: it fails OPEN.
642
+ */
643
+ /**
644
+ * @param matchEngineVersion the `engineVersion` field as read from a stored match.
645
+ * Deliberately typed to admit the junk a database can hand back: a document written
646
+ * before the field existed returns undefined, and a missing version is never a match.
647
+ */
648
+ declare function canReplayMatch(matchEngineVersion: number | null | undefined): boolean;
612
649
 
613
650
  /**
614
651
  * VIBEMANCER — BOT COMPUTE BUDGET
@@ -1289,8 +1326,20 @@ declare function directionTo(from: Position, to: Position): Position;
1289
1326
  */
1290
1327
  declare function directionAway(from: Position, to: Position): Position;
1291
1328
  /**
1292
- * Clamp a position to valid arena bounds.
1293
- * Note: For wizard-specific clamping with radius, use clampToArena from physics.ts
1329
+ * Clamp a position into the arena rectangle — [0, ARENA_SIZE] on both axes.
1330
+ *
1331
+ * READ THIS BEFORE USING IT FOR SAFETY. It does NOT keep a wizard alive. The arena includes
1332
+ * the 30-unit lava border, so clamping to it can hand you back a coordinate that is deep
1333
+ * inside the lava — for example (0, 0), which is lethal. It used to be documented as
1334
+ * clamping to "valid arena bounds", which reads like exactly the safety helper it is not:
1335
+ * a player sanitising a blink target with this will blink into the fire.
1336
+ *
1337
+ * To stay ALIVE you want the safe box for a wizard's CENTRE, which is
1338
+ * [ARENA_MIN + WIZARD_RADIUS, ARENA_MAX - WIZARD_RADIUS] — currently [35, 825], because
1339
+ * `isInLava` tests your edge rather than your centre.
1340
+ *
1341
+ * For the engine's own wizard clamp (arena bounds minus the radius, still not lava-safe),
1342
+ * see clampToArena in physics.ts.
1294
1343
  */
1295
1344
  declare function clampPositionToArena(position: Position): Position;
1296
1345
  /**
@@ -1413,7 +1462,13 @@ declare function useHealth(): number;
1413
1462
  * Clamped to [5, 855] — the ARENA bounds minus the wizard radius, NOT the playfield. That
1414
1463
  * distinction is the difference between living and dying: the playfield is [30, 830] and
1415
1464
  * everything outside it is lava, so you can walk straight out of the safe area and be
1416
- * killed. Nothing stops you. Check against ARENA_MIN/ARENA_MAX, not against these numbers.
1465
+ * killed. Nothing stops you.
1466
+ *
1467
+ * And the number you actually need is neither of those. Lava is tested against your EDGE,
1468
+ * not your centre (`isInLava` checks `x + WIZARD_RADIUS > ARENA_MAX`), and this returns your
1469
+ * CENTRE — so with a radius of 5 you die once your centre passes 825. The safe range to
1470
+ * steer by is [35, 825]. Aiming for 828 because "the playfield is [30, 830]" is fatal, which
1471
+ * is exactly what a real fight trace showed: LAVA_DEATH at (826, 430).
1417
1472
  *
1418
1473
  * (This previously quoted the playfield-sized bounds as the clamp, which told the reader
1419
1474
  * they were safely fenced in. They are not — see docs-match-engine.test.ts.)
@@ -3074,4 +3129,4 @@ declare function diagnoseTrace(events: TraceEvent[], summary: TraceSummary): str
3074
3129
  /** Format diagnostic tips as a human-readable string. */
3075
3130
  declare function formatDiagnosis(tips: string[]): string;
3076
3131
 
3077
- export { type InternalWizardState as $, ALL_BOTS as A, BLINK_CAST_TIME as B, CASTING_MOVEMENT_MULT as C, Critter as D, DEFAULT_BUDGET as E, type FightResult as F, DEFAULT_FIGHT_BACKSTOP_MS as G, DEFAULT_SEED as H, Doombringer as I, ENGINE_VERSION as J, type EnemyState as K, FIGHT_SPAWN_DISTANCES as L, type FightBudget as M, type FightStats as N, type FightWinner as O, type FinalAction as P, Flamecaller as Q, GCD_DURATION as R, type SimulateResult as S, type GameConfig as T, type GameState as U, Golem as V, Hero as W, Hogger as X, type HomingParams as Y, type HookState as Z, Infernalist as _, ARENA_MAX as a, Spellseeker as a$, KNOCKBACK_DAMAGE_THRESHOLD as a0, KNOCKBACK_DECAY as a1, KNOCKBACK_DELAY as a2, KNOCKBACK_SPEED_PER_DAMAGE as a3, LAVA_BORDER_WIDTH as a4, Lich as a5, MATCH_DURATION as a6, MAX_HEALTH as a7, MISSILE_BASE_CAST as a8, MISSILE_BASE_RADIUS as a9, type MissileLaunchEvent as aA, type MissileOobEvent as aB, type MissileTemplate as aC, Nightblade as aD, type ParamDeclaration as aE, type Position as aF, type ProjectileState as aG, Pyromancer as aH, RULES as aI, RULESET_RANGES as aJ, type RefObject as aK, Rookie as aL, SHIELD_CAST_TIME as aM, SHIELD_DECAY_PER_SECOND as aN, SHIELD_DECAY_RATE as aO, SHIELD_MAX_BLOCK as aP, SHIELD_MAX_STRENGTH as aQ, SHIELD_MIN_BLOCK as aR, SHIELD_MIN_STRENGTH as aS, SPAWN_DISTANCE as aT, type SeekerParams as aU, Sentinel as aV, Shadowblade as aW, type ShieldBlockEvent as aX, type ShieldStartEvent as aY, type SimEvent as aZ, Spellbinder as a_, MISSILE_DAMAGE_POWER as aa, MISSILE_DAMAGE_RADIUS_SCALE as ab, MISSILE_DAMAGE_SCALE as ac, MISSILE_HOMING_COEFF as ad, MISSILE_MIN_CAST_TIME as ae, MISSILE_MIN_DAMAGE as af, MISSILE_MIN_DURATION as ag, MISSILE_MIN_SPEED as ah, MISSILE_RADIUS_PER_DAMAGE as ai, MISSILE_SPEED_DURATION_BASELINE as aj, MISSILE_SPEED_DURATION_COEFF as ak, MISSILE_TURN_DURATION_COEFF as al, MOVEMENT_SPEED as am, MOVE_SPEED as an, ManualMatch as ao, type ManualMatchInitOptions as ap, type ManualMatchOptions as aq, type ManualMatchStepRequest as ar, type MatchWinner as as, type MissileAction as at, type MissileActions as au, type MissileConfig as av, type MissileContext as aw, type MissileExpiredEvent as ax, type MissileFunction$1 as ay, type MissileHitEvent as az, ARENA_MIN as b, createBudgetState as b$, Spellshot as b0, Spellspinner as b1, Spelltracer as b2, Spellweaver as b3, type SpiralParams as b4, type StepResult as b5, Stormcaller as b6, Stormchaser as b7, Stormforger as b8, type StraightParams as b9, type WizardGroup as bA, type WizardLavaDeathEvent as bB, type WizardState as bC, type WorkerFactory as bD, type WorkerLike as bE, analyzeThreats as bF, angleDiff as bG, angleInRange as bH, angleTo as bI, applyDamage as bJ, applyRulesetOverrides as bK, blink as bL, browserSandboxFight as bM, browserSandboxSimulate as bN, calculateBlinkCooldown as bO, calculateMissileCastTime as bP, calculateMissileRadius as bQ, calculateMissileSimilarity as bR, calculateShieldBlock as bS, calculateWarmupMultiplier as bT, cancel as bU, cancelCast as bV, clampPositionToArena as bW, clampToArena as bX, clearHooks as bY, clearParamValues as bZ, completeCast as b_, TICKS_PER_SECOND as ba, TICK_DURATION_MS as bb, TargetDummy as bc, type TestBotBuilder as bd, type TestFightResult as be, type TestSimulateResult as bf, type TraceBotSummary as bg, type TraceEvent as bh, type TraceEventType as bi, type TraceSummary as bj, Turtle as bk, type Velocity as bl, Voidblade as bm, WARMUP_DURATION_TOLERANCE as bn, WARMUP_MAX_BONUS as bo, WARMUP_MAX_PENALTY as bp, WARMUP_SPEED_TOLERANCE as bq, WARMUP_TURN_TOLERANCE as br, WIZARD_HEALTH as bs, WIZARD_RADIUS as bt, Warmage as bu, type WizardActions as bv, type WizardContext as bw, type WizardDeathEvent as bx, type WizardEntry as by, type WizardFunction as bz, ARENA_SIZE as c, setParamValues as c$, createEntitySeed as c0, createFightBudget as c1, createInitialState as c2, createRandom as c3, createWorkerScript as c4, currentRuleset as c5, diagnoseTrace as c6, directionAway as c7, directionTo as c8, distanceTo as c9, idle as cA, inRange as cB, interceptAngle as cC, isBannedBotName as cD, isInLava as cE, magnitude as cF, matchesAnyBanRule as cG, mayAct as cH, missile as cI, move as cJ, moveInDirection as cK, moveProjectile as cL, moveWizard as cM, narrowRange as cN, nextRandom as cO, normalize as cP, normalizeAngle as cQ, predictPosition as cR, recordSpend as cS, resetAllHooks as cT, resetRuleset as cU, resolveWizardCollision as cV, runWithHooks as cW, runWizardWithContext as cX, scoreFight as cY, scoreFightAsWizard2 as cZ, seekerMissile as c_, effectiveTurnRateCost as ca, extractAction as cb, extractMissileAction as cc, extractStats as cd, extractTraceEvents as ce, fight as cf, findInRange as cg, findNearest as ch, fitMissileForEscapingTarget as ci, fitMissileToBudget as cj, flyStraight as ck, formatDiagnosis as cl, formatStats as cm, formatTraceEvents as cn, formatTraceSummary as co, generateCandidates as cp, generateCombos as cq, getAdaptiveMissileConfig as cr, getEffectiveRange as cs, getLeadPosition as ct, getMissileCastTime as cu, getMissileContext as cv, getPlayerState as cw, getWizardContext as cx, hashCombine as cy, homingMissile as cz, ARENA_WATER_BUFFER as d, shield as d0, simulate as d1, simulateMinDuration as d2, sortByDistance as d3, spiralMissile as d4, startCast as d5, startDiscovery as d6, stopDiscovery as d7, straightMissile as d8, summarizeTrace as d9, useRef as dA, useShieldStrength as dB, useState as dC, useStatus as dD, useThreats as dE, useTick as dF, useTicksUntilReady as dG, useVelocity as dH, validateHookCall as dI, validateMissileConfig as dJ, withMissileContext as dK, withWizardContext as dL, wrapWithParams as dM, sweptCircleCollision as da, testBot as db, tick as dc, turnToAngle as dd, turnToward as de, updateShield as df, useArenaSize as dg, useBlinkCooldown as dh, useCastProgress as di, useCastingSpell as dj, useClosestThreat as dk, useDamageDealt as dl, useDamageTaken as dm, useEffect as dn, useEnemy as dp, useHealth as dq, useLastHitTick as dr, useLastMissileConfig as ds, useMemo as dt, useMyProjectiles as du, useMyThreatsToEnemy as dv, useParam as dw, usePosition as dx, useProjectiles as dy, useRandom as dz, type ActionBuilder as e, type AnalyzedThreat as f, Archlich as g, Archmage as h, BLINK_COOLDOWN as i, BLINK_MAX_COOLDOWN as j, BLINK_MAX_RANGE as k, BLINK_MIN_COOLDOWN as l, BLINK_RANGE as m, BOT_GROUPS as n, Battlemage as o, type BlinkEvent as p, Bonemancer as q, type BotBudgetState as r, type BotError as s, BrowserManualMatchSandbox as t, BrowserMatchSandbox as u, type BrowserSandboxOptions as v, type BudgetLimits as w, COLLISION_RADIUS as x, type CastCancelEvent as y, type CastStartEvent as z };
3132
+ export { Infernalist as $, ALL_BOTS as A, BLINK_CAST_TIME as B, CASTING_MOVEMENT_MULT as C, Critter as D, DEFAULT_BUDGET as E, type FightResult as F, DEFAULT_FIGHT_BACKSTOP_MS as G, DEFAULT_SEED as H, Doombringer as I, ENGINE_VERSION as J, EQUIVALENT_ENGINE_VERSIONS as K, type EnemyState as L, FIGHT_SPAWN_DISTANCES as M, type FightBudget as N, type FightStats as O, type FightWinner as P, type FinalAction as Q, Flamecaller as R, type SimulateResult as S, GCD_DURATION as T, type GameConfig as U, type GameState as V, Golem as W, Hero as X, Hogger as Y, type HomingParams as Z, type HookState as _, ARENA_MAX as a, Spellbinder as a$, type InternalWizardState as a0, KNOCKBACK_DAMAGE_THRESHOLD as a1, KNOCKBACK_DECAY as a2, KNOCKBACK_DELAY as a3, KNOCKBACK_SPEED_PER_DAMAGE as a4, LAVA_BORDER_WIDTH as a5, Lich as a6, MATCH_DURATION as a7, MAX_HEALTH as a8, MISSILE_BASE_CAST as a9, type MissileHitEvent as aA, type MissileLaunchEvent as aB, type MissileOobEvent as aC, type MissileTemplate as aD, Nightblade as aE, type ParamDeclaration as aF, type Position as aG, type ProjectileState as aH, Pyromancer as aI, RULES as aJ, RULESET_RANGES as aK, type RefObject as aL, Rookie as aM, SHIELD_CAST_TIME as aN, SHIELD_DECAY_PER_SECOND as aO, SHIELD_DECAY_RATE as aP, SHIELD_MAX_BLOCK as aQ, SHIELD_MAX_STRENGTH as aR, SHIELD_MIN_BLOCK as aS, SHIELD_MIN_STRENGTH as aT, SPAWN_DISTANCE as aU, type SeekerParams as aV, Sentinel as aW, Shadowblade as aX, type ShieldBlockEvent as aY, type ShieldStartEvent as aZ, type SimEvent as a_, MISSILE_BASE_RADIUS as aa, MISSILE_DAMAGE_POWER as ab, MISSILE_DAMAGE_RADIUS_SCALE as ac, MISSILE_DAMAGE_SCALE as ad, MISSILE_HOMING_COEFF as ae, MISSILE_MIN_CAST_TIME as af, MISSILE_MIN_DAMAGE as ag, MISSILE_MIN_DURATION as ah, MISSILE_MIN_SPEED as ai, MISSILE_RADIUS_PER_DAMAGE as aj, MISSILE_SPEED_DURATION_BASELINE as ak, MISSILE_SPEED_DURATION_COEFF as al, MISSILE_TURN_DURATION_COEFF as am, MOVEMENT_SPEED as an, MOVE_SPEED as ao, ManualMatch as ap, type ManualMatchInitOptions as aq, type ManualMatchOptions as ar, type ManualMatchStepRequest as as, type MatchWinner as at, type MissileAction as au, type MissileActions as av, type MissileConfig as aw, type MissileContext as ax, type MissileExpiredEvent as ay, type MissileFunction$1 as az, ARENA_MIN as b, clearParamValues as b$, Spellseeker as b0, Spellshot as b1, Spellspinner as b2, Spelltracer as b3, Spellweaver as b4, type SpiralParams as b5, type StepResult as b6, Stormcaller as b7, Stormchaser as b8, Stormforger as b9, type WizardFunction as bA, type WizardGroup as bB, type WizardLavaDeathEvent as bC, type WizardState as bD, type WorkerFactory as bE, type WorkerLike as bF, analyzeThreats as bG, angleDiff as bH, angleInRange as bI, angleTo as bJ, applyDamage as bK, applyRulesetOverrides as bL, blink as bM, browserSandboxFight as bN, browserSandboxSimulate as bO, calculateBlinkCooldown as bP, calculateMissileCastTime as bQ, calculateMissileRadius as bR, calculateMissileSimilarity as bS, calculateShieldBlock as bT, calculateWarmupMultiplier as bU, canReplayMatch as bV, cancel as bW, cancelCast as bX, clampPositionToArena as bY, clampToArena as bZ, clearHooks as b_, type StraightParams as ba, TICKS_PER_SECOND as bb, TICK_DURATION_MS as bc, TargetDummy as bd, type TestBotBuilder as be, type TestFightResult as bf, type TestSimulateResult as bg, type TraceBotSummary as bh, type TraceEvent as bi, type TraceEventType as bj, type TraceSummary as bk, Turtle as bl, type Velocity as bm, Voidblade as bn, WARMUP_DURATION_TOLERANCE as bo, WARMUP_MAX_BONUS as bp, WARMUP_MAX_PENALTY as bq, WARMUP_SPEED_TOLERANCE as br, WARMUP_TURN_TOLERANCE as bs, WIZARD_HEALTH as bt, WIZARD_RADIUS as bu, Warmage as bv, type WizardActions as bw, type WizardContext as bx, type WizardDeathEvent as by, type WizardEntry as bz, ARENA_SIZE as c, scoreFightAsWizard2 as c$, completeCast as c0, createBudgetState as c1, createEntitySeed as c2, createFightBudget as c3, createInitialState as c4, createRandom as c5, createWorkerScript as c6, currentRuleset as c7, diagnoseTrace as c8, directionAway as c9, hashCombine as cA, homingMissile as cB, idle as cC, inRange as cD, interceptAngle as cE, isBannedBotName as cF, isInLava as cG, magnitude as cH, matchesAnyBanRule as cI, mayAct as cJ, missile as cK, move as cL, moveInDirection as cM, moveProjectile as cN, moveWizard as cO, narrowRange as cP, nextRandom as cQ, normalize as cR, normalizeAngle as cS, predictPosition as cT, recordSpend as cU, resetAllHooks as cV, resetRuleset as cW, resolveWizardCollision as cX, runWithHooks as cY, runWizardWithContext as cZ, scoreFight as c_, directionTo as ca, distanceTo as cb, effectiveTurnRateCost as cc, extractAction as cd, extractMissileAction as ce, extractStats as cf, extractTraceEvents as cg, fight as ch, findInRange as ci, findNearest as cj, fitMissileForEscapingTarget as ck, fitMissileToBudget as cl, flyStraight as cm, formatDiagnosis as cn, formatStats as co, formatTraceEvents as cp, formatTraceSummary as cq, generateCandidates as cr, generateCombos as cs, getAdaptiveMissileConfig as ct, getEffectiveRange as cu, getLeadPosition as cv, getMissileCastTime as cw, getMissileContext as cx, getPlayerState as cy, getWizardContext as cz, ARENA_WATER_BUFFER as d, seekerMissile as d0, setParamValues as d1, shield as d2, simulate as d3, simulateMinDuration as d4, sortByDistance as d5, spiralMissile as d6, startCast as d7, startDiscovery as d8, stopDiscovery as d9, useProjectiles as dA, useRandom as dB, useRef as dC, useShieldStrength as dD, useState as dE, useStatus as dF, useThreats as dG, useTick as dH, useTicksUntilReady as dI, useVelocity as dJ, validateHookCall as dK, validateMissileConfig as dL, withMissileContext as dM, withWizardContext as dN, wrapWithParams as dO, straightMissile as da, summarizeTrace as db, sweptCircleCollision as dc, testBot as dd, tick as de, turnToAngle as df, turnToward as dg, updateShield as dh, useArenaSize as di, useBlinkCooldown as dj, useCastProgress as dk, useCastingSpell as dl, useClosestThreat as dm, useDamageDealt as dn, useDamageTaken as dp, useEffect as dq, useEnemy as dr, useHealth as ds, useLastHitTick as dt, useLastMissileConfig as du, useMemo as dv, useMyProjectiles as dw, useMyThreatsToEnemy as dx, useParam as dy, usePosition as dz, type ActionBuilder as e, type AnalyzedThreat as f, Archlich as g, Archmage as h, BLINK_COOLDOWN as i, BLINK_MAX_COOLDOWN as j, BLINK_MAX_RANGE as k, BLINK_MIN_COOLDOWN as l, BLINK_RANGE as m, BOT_GROUPS as n, Battlemage as o, type BlinkEvent as p, Bonemancer as q, type BotBudgetState as r, type BotError as s, BrowserManualMatchSandbox as t, BrowserMatchSandbox as u, type BrowserSandboxOptions as v, type BudgetLimits as w, COLLISION_RADIUS as x, type CastCancelEvent as y, type CastStartEvent as z };
@@ -1 +1 @@
1
- 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, s as BotError, t as BrowserManualMatchSandbox, u as BrowserMatchSandbox, v as BrowserSandboxOptions, C as CASTING_MOVEMENT_MULT, x as COLLISION_RADIUS, y as CastCancelEvent, z as CastStartEvent, D as Critter, H as DEFAULT_SEED, I as Doombringer, J as ENGINE_VERSION, K as EnemyState, L as FIGHT_SPAWN_DISTANCES, F as FightResult, 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, S as SimulateResult, 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, c0 as createEntitySeed, 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, 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, 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';
1
+ 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, s as BotError, t as BrowserManualMatchSandbox, u as BrowserMatchSandbox, v as BrowserSandboxOptions, C as CASTING_MOVEMENT_MULT, x as COLLISION_RADIUS, y as CastCancelEvent, z as CastStartEvent, D as Critter, H as DEFAULT_SEED, I as Doombringer, J as ENGINE_VERSION, K as EQUIVALENT_ENGINE_VERSIONS, L as EnemyState, M as FIGHT_SPAWN_DISTANCES, F as FightResult, O as FightStats, P as FightWinner, Q as FinalAction, R as Flamecaller, T as GCD_DURATION, U as GameConfig, V as GameState, W as Golem, X as Hero, Y as Hogger, Z as HomingParams, _ as HookState, $ as Infernalist, a0 as InternalWizardState, a1 as KNOCKBACK_DAMAGE_THRESHOLD, a2 as KNOCKBACK_DECAY, a3 as KNOCKBACK_DELAY, a4 as KNOCKBACK_SPEED_PER_DAMAGE, a5 as LAVA_BORDER_WIDTH, a6 as Lich, a7 as MATCH_DURATION, a8 as MAX_HEALTH, a9 as MISSILE_BASE_CAST, aa as MISSILE_BASE_RADIUS, ab as MISSILE_DAMAGE_POWER, ac as MISSILE_DAMAGE_RADIUS_SCALE, ad as MISSILE_DAMAGE_SCALE, ae as MISSILE_HOMING_COEFF, af as MISSILE_MIN_CAST_TIME, ag as MISSILE_MIN_DAMAGE, ah as MISSILE_MIN_DURATION, ai as MISSILE_MIN_SPEED, aj as MISSILE_RADIUS_PER_DAMAGE, ak as MISSILE_SPEED_DURATION_BASELINE, al as MISSILE_SPEED_DURATION_COEFF, am as MISSILE_TURN_DURATION_COEFF, an as MOVEMENT_SPEED, ao as MOVE_SPEED, ap as ManualMatch, aq as ManualMatchInitOptions, ar as ManualMatchOptions, as as ManualMatchStepRequest, at as MatchWinner, au as MissileAction, av as MissileActions, aw as MissileConfig, ax as MissileContext, ay as MissileExpiredEvent, az as MissileFunction, aA as MissileHitEvent, aB as MissileLaunchEvent, aC as MissileOobEvent, aD as MissileTemplate, aE as Nightblade, aF as ParamDeclaration, aG as Position, aH as ProjectileState, aI as Pyromancer, aJ as RULES, aK as RULESET_RANGES, aL as RefObject, aM as Rookie, aN as SHIELD_CAST_TIME, aO as SHIELD_DECAY_PER_SECOND, aP as SHIELD_DECAY_RATE, aQ as SHIELD_MAX_BLOCK, aR as SHIELD_MAX_STRENGTH, aS as SHIELD_MIN_BLOCK, aT as SHIELD_MIN_STRENGTH, aU as SPAWN_DISTANCE, aV as SeekerParams, aW as Sentinel, aX as Shadowblade, aY as ShieldBlockEvent, aZ as ShieldStartEvent, a_ as SimEvent, S as SimulateResult, a$ as Spellbinder, b0 as Spellseeker, b1 as Spellshot, b2 as Spellspinner, b3 as Spelltracer, b4 as Spellweaver, b5 as SpiralParams, b6 as StepResult, b7 as Stormcaller, b8 as Stormchaser, b9 as Stormforger, ba as StraightParams, bb as TICKS_PER_SECOND, bc as TICK_DURATION_MS, bd as TargetDummy, be as TestBotBuilder, bf as TestFightResult, bg as TestSimulateResult, bh as TraceBotSummary, bi as TraceEvent, bj as TraceEventType, bk as TraceSummary, bl as Turtle, bm as Velocity, bn as Voidblade, bo as WARMUP_DURATION_TOLERANCE, bp as WARMUP_MAX_BONUS, bq as WARMUP_MAX_PENALTY, br as WARMUP_SPEED_TOLERANCE, bs as WARMUP_TURN_TOLERANCE, bt as WIZARD_HEALTH, bu as WIZARD_RADIUS, bv as Warmage, bw as WizardActions, bx as WizardContext, by as WizardDeathEvent, bz as WizardEntry, bA as WizardFunction, bB as WizardGroup, bC as WizardLavaDeathEvent, bD as WizardState, bE as WorkerFactory, bF as WorkerLike, bG as analyzeThreats, bH as angleDiff, bI as angleInRange, bJ as angleTo, bK as applyDamage, bL as applyRulesetOverrides, bM as blink, bN as browserSandboxFight, bO as browserSandboxSimulate, bP as calculateBlinkCooldown, bQ as calculateMissileCastTime, bR as calculateMissileRadius, bS as calculateMissileSimilarity, bT as calculateShieldBlock, bU as calculateWarmupMultiplier, bV as canReplayMatch, bW as cancel, bX as cancelCast, bY as clampPositionToArena, bZ as clampToArena, b_ as clearHooks, b$ as clearParamValues, c0 as completeCast, c2 as createEntitySeed, c4 as createInitialState, c5 as createRandom, c6 as createWorkerScript, c7 as currentRuleset, c8 as diagnoseTrace, c9 as directionAway, ca as directionTo, cb as distanceTo, cc as effectiveTurnRateCost, cd as extractAction, ce as extractMissileAction, cf as extractStats, cg as extractTraceEvents, ch as fight, ci as findInRange, cj as findNearest, ck as fitMissileForEscapingTarget, cl as fitMissileToBudget, cm as flyStraight, cn as formatDiagnosis, co as formatStats, cp as formatTraceEvents, cq as formatTraceSummary, cr as generateCandidates, cs as generateCombos, ct as getAdaptiveMissileConfig, cu as getEffectiveRange, cv as getLeadPosition, cw as getMissileCastTime, cx as getMissileContext, cy as getPlayerState, cz as getWizardContext, cA as hashCombine, cB as homingMissile, cC as idle, cD as inRange, cE as interceptAngle, cF as isBannedBotName, cG as isInLava, cH as magnitude, cI as matchesAnyBanRule, cK as missile, cL as move, cM as moveInDirection, cN as moveProjectile, cO as moveWizard, cP as narrowRange, cQ as nextRandom, cR as normalize, cS as normalizeAngle, cT as predictPosition, cV as resetAllHooks, cW as resetRuleset, cX as resolveWizardCollision, cY as runWithHooks, cZ as runWizardWithContext, c_ as scoreFight, c$ as scoreFightAsWizard2, d0 as seekerMissile, d1 as setParamValues, d2 as shield, d3 as simulate, d4 as simulateMinDuration, d5 as sortByDistance, d6 as spiralMissile, d7 as startCast, d8 as startDiscovery, d9 as stopDiscovery, da as straightMissile, db as summarizeTrace, dc as sweptCircleCollision, dd as testBot, de as tick, df as turnToAngle, dg as turnToward, dh as updateShield, di as useArenaSize, dj as useBlinkCooldown, dk as useCastProgress, dl as useCastingSpell, dm as useClosestThreat, dn as useDamageDealt, dp as useDamageTaken, dq as useEffect, dr as useEnemy, ds as useHealth, dt as useLastHitTick, du as useLastMissileConfig, dv as useMemo, dw as useMyProjectiles, dx as useMyThreatsToEnemy, dy as useParam, dz as usePosition, dA as useProjectiles, dB as useRandom, dC as useRef, dD as useShieldStrength, dE as useState, dF as useStatus, dG as useThreats, dH as useTick, dI as useTicksUntilReady, dJ as useVelocity, dK as validateHookCall, dL as validateMissileConfig, dM as withMissileContext, dN as withWizardContext, dO as wrapWithParams } from './index-browser-iiIpqpDl.js';
@@ -23,6 +23,7 @@ import {
23
23
  DEFAULT_SEED,
24
24
  Doombringer,
25
25
  ENGINE_VERSION,
26
+ EQUIVALENT_ENGINE_VERSIONS,
26
27
  FIGHT_SPAWN_DISTANCES,
27
28
  Flamecaller,
28
29
  GCD_DURATION,
@@ -107,6 +108,7 @@ import {
107
108
  calculateMissileSimilarity,
108
109
  calculateShieldBlock,
109
110
  calculateWarmupMultiplier,
111
+ canReplayMatch,
110
112
  cancel,
111
113
  cancelCast,
112
114
  clampPositionToArena,
@@ -223,7 +225,7 @@ import {
223
225
  withMissileContext,
224
226
  withWizardContext,
225
227
  wrapWithParams
226
- } from "./chunk-NEXWZU5I.js";
228
+ } from "./chunk-NR326XEY.js";
227
229
  export {
228
230
  ALL_BOTS,
229
231
  ARENA_MAX,
@@ -249,6 +251,7 @@ export {
249
251
  DEFAULT_SEED,
250
252
  Doombringer,
251
253
  ENGINE_VERSION,
254
+ EQUIVALENT_ENGINE_VERSIONS,
252
255
  FIGHT_SPAWN_DISTANCES,
253
256
  Flamecaller,
254
257
  GCD_DURATION,
@@ -333,6 +336,7 @@ export {
333
336
  calculateMissileSimilarity,
334
337
  calculateShieldBlock,
335
338
  calculateWarmupMultiplier,
339
+ canReplayMatch,
336
340
  cancel,
337
341
  cancelCast,
338
342
  clampPositionToArena,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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';
1
+ import { F as FightResult, S as SimulateResult } from './index-browser-iiIpqpDl.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 EQUIVALENT_ENGINE_VERSIONS, L as EnemyState, M as FIGHT_SPAWN_DISTANCES, N as FightBudget, O as FightStats, P as FightWinner, Q as FinalAction, R as Flamecaller, T as GCD_DURATION, U as GameConfig, V as GameState, W as Golem, X as Hero, Y as Hogger, Z as HomingParams, _ as HookState, $ as Infernalist, a0 as InternalWizardState, a1 as KNOCKBACK_DAMAGE_THRESHOLD, a2 as KNOCKBACK_DECAY, a3 as KNOCKBACK_DELAY, a4 as KNOCKBACK_SPEED_PER_DAMAGE, a5 as LAVA_BORDER_WIDTH, a6 as Lich, a7 as MATCH_DURATION, a8 as MAX_HEALTH, a9 as MISSILE_BASE_CAST, aa as MISSILE_BASE_RADIUS, ab as MISSILE_DAMAGE_POWER, ac as MISSILE_DAMAGE_RADIUS_SCALE, ad as MISSILE_DAMAGE_SCALE, ae as MISSILE_HOMING_COEFF, af as MISSILE_MIN_CAST_TIME, ag as MISSILE_MIN_DAMAGE, ah as MISSILE_MIN_DURATION, ai as MISSILE_MIN_SPEED, aj as MISSILE_RADIUS_PER_DAMAGE, ak as MISSILE_SPEED_DURATION_BASELINE, al as MISSILE_SPEED_DURATION_COEFF, am as MISSILE_TURN_DURATION_COEFF, an as MOVEMENT_SPEED, ao as MOVE_SPEED, ap as ManualMatch, aq as ManualMatchInitOptions, ar as ManualMatchOptions, as as ManualMatchStepRequest, at as MatchWinner, au as MissileAction, av as MissileActions, aw as MissileConfig, ax as MissileContext, ay as MissileExpiredEvent, az as MissileFunction, aA as MissileHitEvent, aB as MissileLaunchEvent, aC as MissileOobEvent, aD as MissileTemplate, aE as Nightblade, aF as ParamDeclaration, aG as Position, aH as ProjectileState, aI as Pyromancer, aJ as RULES, aK as RULESET_RANGES, aL as RefObject, aM as Rookie, aN as SHIELD_CAST_TIME, aO as SHIELD_DECAY_PER_SECOND, aP as SHIELD_DECAY_RATE, aQ as SHIELD_MAX_BLOCK, aR as SHIELD_MAX_STRENGTH, aS as SHIELD_MIN_BLOCK, aT as SHIELD_MIN_STRENGTH, aU as SPAWN_DISTANCE, aV as SeekerParams, aW as Sentinel, aX as Shadowblade, aY as ShieldBlockEvent, aZ as ShieldStartEvent, a_ as SimEvent, a$ as Spellbinder, b0 as Spellseeker, b1 as Spellshot, b2 as Spellspinner, b3 as Spelltracer, b4 as Spellweaver, b5 as SpiralParams, b6 as StepResult, b7 as Stormcaller, b8 as Stormchaser, b9 as Stormforger, ba as StraightParams, bb as TICKS_PER_SECOND, bc as TICK_DURATION_MS, bd as TargetDummy, be as TestBotBuilder, bf as TestFightResult, bg as TestSimulateResult, bh as TraceBotSummary, bi as TraceEvent, bj as TraceEventType, bk as TraceSummary, bl as Turtle, bm as Velocity, bn as Voidblade, bo as WARMUP_DURATION_TOLERANCE, bp as WARMUP_MAX_BONUS, bq as WARMUP_MAX_PENALTY, br as WARMUP_SPEED_TOLERANCE, bs as WARMUP_TURN_TOLERANCE, bt as WIZARD_HEALTH, bu as WIZARD_RADIUS, bv as Warmage, bw as WizardActions, bx as WizardContext, by as WizardDeathEvent, bz as WizardEntry, bA as WizardFunction, bB as WizardGroup, bC as WizardLavaDeathEvent, bD as WizardState, bE as WorkerFactory, bF as WorkerLike, bG as analyzeThreats, bH as angleDiff, bI as angleInRange, bJ as angleTo, bK as applyDamage, bL as applyRulesetOverrides, bM as blink, bN as browserSandboxFight, bO as browserSandboxSimulate, bP as calculateBlinkCooldown, bQ as calculateMissileCastTime, bR as calculateMissileRadius, bS as calculateMissileSimilarity, bT as calculateShieldBlock, bU as calculateWarmupMultiplier, bV as canReplayMatch, bW as cancel, bX as cancelCast, bY as clampPositionToArena, bZ as clampToArena, b_ as clearHooks, b$ as clearParamValues, c0 as completeCast, c1 as createBudgetState, c2 as createEntitySeed, c3 as createFightBudget, c4 as createInitialState, c5 as createRandom, c6 as createWorkerScript, c7 as currentRuleset, c8 as diagnoseTrace, c9 as directionAway, ca as directionTo, cb as distanceTo, cc as effectiveTurnRateCost, cd as extractAction, ce as extractMissileAction, cf as extractStats, cg as extractTraceEvents, ch as fight, ci as findInRange, cj as findNearest, ck as fitMissileForEscapingTarget, cl as fitMissileToBudget, cm as flyStraight, cn as formatDiagnosis, co as formatStats, cp as formatTraceEvents, cq as formatTraceSummary, cr as generateCandidates, cs as generateCombos, ct as getAdaptiveMissileConfig, cu as getEffectiveRange, cv as getLeadPosition, cw as getMissileCastTime, cx as getMissileContext, cy as getPlayerState, cz as getWizardContext, cA as hashCombine, cB as homingMissile, cC as idle, cD as inRange, cE as interceptAngle, cF as isBannedBotName, cG as isInLava, cH as magnitude, cI as matchesAnyBanRule, cJ as mayAct, cK as missile, cL as move, cM as moveInDirection, cN as moveProjectile, cO as moveWizard, cP as narrowRange, cQ as nextRandom, cR as normalize, cS as normalizeAngle, cT as predictPosition, cU as recordSpend, cV as resetAllHooks, cW as resetRuleset, cX as resolveWizardCollision, cY as runWithHooks, cZ as runWizardWithContext, c_ as scoreFight, c$ as scoreFightAsWizard2, d0 as seekerMissile, d1 as setParamValues, d2 as shield, d3 as simulate, d4 as simulateMinDuration, d5 as sortByDistance, d6 as spiralMissile, d7 as startCast, d8 as startDiscovery, d9 as stopDiscovery, da as straightMissile, db as summarizeTrace, dc as sweptCircleCollision, dd as testBot, de as tick, df as turnToAngle, dg as turnToward, dh as updateShield, di as useArenaSize, dj as useBlinkCooldown, dk as useCastProgress, dl as useCastingSpell, dm as useClosestThreat, dn as useDamageDealt, dp as useDamageTaken, dq as useEffect, dr as useEnemy, ds as useHealth, dt as useLastHitTick, du as useLastMissileConfig, dv as useMemo, dw as useMyProjectiles, dx as useMyThreatsToEnemy, dy as useParam, dz as usePosition, dA as useProjectiles, dB as useRandom, dC as useRef, dD as useShieldStrength, dE as useState, dF as useStatus, dG as useThreats, dH as useTick, dI as useTicksUntilReady, dJ as useVelocity, dK as validateHookCall, dL as validateMissileConfig, dM as withMissileContext, dN as withWizardContext, dO as wrapWithParams } from './index-browser-iiIpqpDl.js';
3
3
 
4
4
  /**
5
5
  * A compiled bot ready for sandboxed execution.
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  DEFAULT_SEED,
26
26
  Doombringer,
27
27
  ENGINE_VERSION,
28
+ EQUIVALENT_ENGINE_VERSIONS,
28
29
  FIGHT_SPAWN_DISTANCES,
29
30
  Flamecaller,
30
31
  GCD_DURATION,
@@ -109,6 +110,7 @@ import {
109
110
  calculateMissileSimilarity,
110
111
  calculateShieldBlock,
111
112
  calculateWarmupMultiplier,
113
+ canReplayMatch,
112
114
  cancel,
113
115
  cancelCast,
114
116
  clampPositionToArena,
@@ -229,7 +231,7 @@ import {
229
231
  withMissileContext,
230
232
  withWizardContext,
231
233
  wrapWithParams
232
- } from "./chunk-NEXWZU5I.js";
234
+ } from "./chunk-NR326XEY.js";
233
235
 
234
236
  // src/engine/sandbox.ts
235
237
  import ivm from "isolated-vm";
@@ -720,6 +722,7 @@ export {
720
722
  DEFAULT_SEED,
721
723
  Doombringer,
722
724
  ENGINE_VERSION,
725
+ EQUIVALENT_ENGINE_VERSIONS,
723
726
  FIGHT_SPAWN_DISTANCES,
724
727
  Flamecaller,
725
728
  GCD_DURATION,
@@ -806,6 +809,7 @@ export {
806
809
  calculateMissileSimilarity,
807
810
  calculateShieldBlock,
808
811
  calculateWarmupMultiplier,
812
+ canReplayMatch,
809
813
  cancel,
810
814
  cancelCast,
811
815
  clampPositionToArena,
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 {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"]}
1
+ {"version":3,"sources":["../src/engine/sandbox.ts","../src/engine/sandbox-compile.ts","../src/engine/sandbox-harness.ts","../src/engine/bundle-fight.ts"],"sourcesContent":["/**\r\n * VIBEMANCER — SANDBOX\r\n *\r\n * Provides isolated-vm sandboxing for bot code execution. Both bots + the\r\n * entire simulation engine run inside a single V8 isolate, so there is ZERO\r\n * per-tick boundary crossing overhead. The only data crossing the boundary\r\n * is fight/simulate options going in and results coming out.\r\n *\r\n * Architecture:\r\n * - Host: creates isolate, loads compiled bundle, calls __fight/__simulate\r\n * - Isolate: contains both bots + full simulation engine, runs fight/simulate\r\n *\r\n * Safety:\r\n * - Memory limit (default 512 MB) catches memory bombs\r\n * - Timeout (default 30s) catches infinite loops\r\n * - Prototype freeze prevents cross-bot sabotage\r\n * - platform: 'neutral' strips Node.js APIs (no fs/net/process)\r\n */\r\n\r\nimport ivm from 'isolated-vm';\r\nimport type {FightResult, SimulateResult} from './simulation.js';\r\nimport {BotBundle, compileMatchBundle} from './sandbox-compile.js';\r\nimport {DEFAULT_FIGHT_BACKSTOP_MS} from './bot-compute-budget.js';\r\nimport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n// Re-export BotBundle so existing imports from sandbox.ts keep working\r\nexport {BotBundle} from './sandbox-compile.js';\r\nexport {compileMatchBundle, compileManualMatchBundle} from './sandbox-compile.js';\r\nexport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n/**\r\n * Options for sandbox creation.\r\n */\r\nexport interface SandboxOptions\r\n{\r\n\t/** Memory limit in MB for the isolate (default: 512). */\r\n\tmemoryLimitMB?: number;\r\n\t/** Timeout in ms for fight/simulate calls (default: 60000). */\r\n\ttimeoutMs?: number;\r\n\t/** Options passed to esbuild compilation (aliases, externals). */\r\n\tcompileOptions?: CompileOptions;\r\n}\r\n\r\n/**\r\n * A sandboxed match runner. Both bots + the entire simulation engine run\r\n * inside a single isolated-vm isolate.\r\n *\r\n * Usage:\r\n * ```ts\r\n * const sandbox = await MatchSandbox.create(botA, botB);\r\n * const result = sandbox.fight({ seed: 42 });\r\n * sandbox.dispose();\r\n * ```\r\n */\r\nexport class MatchSandbox\r\n{\r\n\tprivate isolate: ivm.Isolate;\r\n\tprivate context: ivm.Context;\r\n\tprivate fightFn: ivm.Reference;\r\n\tprivate simulateFn: ivm.Reference;\r\n\tprivate timeout: number;\r\n\tprivate disposed = false;\r\n\r\n\tprivate constructor(\r\n\t\tisolate: ivm.Isolate,\r\n\t\tcontext: ivm.Context,\r\n\t\tfightFn: ivm.Reference,\r\n\t\tsimulateFn: ivm.Reference,\r\n\t\ttimeout: number,\r\n\t)\r\n\t{\r\n\t\tthis.isolate = isolate;\r\n\t\tthis.context = context;\r\n\t\tthis.fightFn = fightFn;\r\n\t\tthis.simulateFn = simulateFn;\r\n\t\tthis.timeout = timeout;\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox with both bots loaded. Compiles the match bundle\r\n\t * automatically using esbuild.\r\n\t */\r\n\tstatic async create(\r\n\t\tbot1: BotBundle,\r\n\t\tbot2: BotBundle,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\t// Was a bare 60000, which the per-bot compute budget made wrong: a budgeted fight can\r\n\t\t// legitimately let one runaway bot spend 45s before it stops being called, and 60s\r\n\t\t// here would kill the whole fight first — putting the wall clock back in charge of\r\n\t\t// outcomes. Shared constant so this cannot drift from the other copies again.\r\n\t\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_FIGHT_BACKSTOP_MS;\r\n\r\n\t\t// 1. Compile the match bundle\r\n\t\tconst bundle = await compileMatchBundle(bot1, bot2, options?.compileOptions);\r\n\r\n\t\t// 2. Create isolate with memory limit\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// 3. Create context and load the bundle\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\t// 4. Get references to the exposed functions\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\t// OOM or timeout can auto-dispose the isolate, so guard the cleanup dispose.\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox from a pre-compiled bundle string.\r\n\t * Useful for caching compiled bundles across multiple MatchSandbox instances.\r\n\t */\r\n\tstatic async fromBundle(\r\n\t\tbundle: string,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\t// Was a bare 60000, which the per-bot compute budget made wrong: a budgeted fight can\r\n\t\t// legitimately let one runaway bot spend 45s before it stops being called, and 60s\r\n\t\t// here would kill the whole fight first — putting the wall clock back in charge of\r\n\t\t// outcomes. Shared constant so this cannot drift from the other copies again.\r\n\t\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_FIGHT_BACKSTOP_MS;\r\n\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Compile a match bundle without creating an isolate.\r\n\t * Returns the compiled JS string for caching/reuse.\r\n\t */\r\n\tstatic async compile(bot1: BotBundle, bot2: BotBundle, compileOptions?: CompileOptions): Promise<string>\r\n\t{\r\n\t\treturn compileMatchBundle(bot1, bot2, compileOptions);\r\n\t}\r\n\r\n\t/**\r\n\t * Run a full fight (10 matches: 5 spawn distances x 2 sides).\r\n\t * Synchronous after isolate creation — runs entirely inside the isolate.\r\n\t */\r\n\tfight(options?: {seed?: number; maxTicks?: number}): FightResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.fightFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as FightResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Run a single simulation.\r\n\t * Synchronous after isolate creation.\r\n\t *\r\n\t * @param options.params1 - useParam overrides for bot 1 (wizard-1)\r\n\t * @param options.params2 - useParam overrides for bot 2 (wizard-2)\r\n\t */\r\n\tsimulate(options?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t}): SimulateResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.simulateFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as SimulateResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Dispose the isolate and free all memory.\r\n\t * The sandbox cannot be used after disposal.\r\n\t */\r\n\tdispose(): void\r\n\t{\r\n\t\tif (!this.disposed)\r\n\t\t{\r\n\t\t\tthis.disposed = true;\r\n\t\t\t// OOM can auto-dispose the isolate, so guard all cleanup\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.fightFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.simulateFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.context.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.isolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Whether this sandbox has been disposed.\r\n\t */\r\n\tget isDisposed(): boolean\r\n\t{\r\n\t\treturn this.disposed;\r\n\t}\r\n\r\n\tprivate ensureNotDisposed(): void\r\n\t{\r\n\t\tif (this.disposed)\r\n\t\t{\r\n\t\t\tthrow new Error('MatchSandbox has been disposed');\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed fight. Creates isolate, runs fight, disposes.\r\n * Convenience wrapper for single-use scenarios.\r\n */\r\nexport async function sandboxFight(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {seed?: number; maxTicks?: number} & SandboxOptions,\r\n): Promise<FightResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.fight({seed: options?.seed, maxTicks: options?.maxTicks});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed simulate. Creates isolate, runs simulate, disposes.\r\n */\r\nexport async function sandboxSimulate(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t} & SandboxOptions,\r\n): Promise<SimulateResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.simulate({\r\n\t\t\tseed: options?.seed,\r\n\t\t\tmaxTicks: options?.maxTicks,\r\n\t\t\tspawnDistance: options?.spawnDistance,\r\n\t\t\tskipHistory: options?.skipHistory,\r\n\t\t\tparams1: options?.params1,\r\n\t\t\tparams2: options?.params2,\r\n\t\t});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n","/**\r\n * VIBEMANCER — SANDBOX COMPILATION\r\n *\r\n * Compiles match bundles using esbuild. Extracted from sandbox.ts so that\r\n * compilation can be used independently of isolated-vm (e.g., in the CLI\r\n * dev server or for browser Web Worker sandboxes).\r\n *\r\n * This file has NO isolated-vm dependency — only esbuild + Node.js builtins.\r\n */\r\n\r\nimport {build} from 'esbuild';\r\nimport path from 'node:path';\r\nimport fs from 'node:fs';\r\nimport {fileURLToPath} from 'node:url';\r\nimport {PROTOTYPE_FREEZE_BANNER, generateManualMatchEntryPoint, generateMatchEntryPoint} from './sandbox-harness.js';\r\n\r\nconst currentFilename = fileURLToPath(import.meta.url);\r\nconst currentDirname = path.dirname(currentFilename);\r\n\r\n/**\r\n * Find the src/engine/ directory. Works from src/, dist/engine/, or dist/ (tsup bundle).\r\n * esbuild needs TypeScript source files, so we look for the src/ tree.\r\n */\r\nexport function getEngineDir(): string\r\n{\r\n\t// When running from src/engine/ (dev/test), currentDirname is already src/engine/\r\n\tconst directPath = path.resolve(currentDirname);\r\n\tif (fs.existsSync(path.join(directPath, 'simulation.ts')))\r\n\t{\r\n\t\treturn directPath;\r\n\t}\r\n\r\n\t// When running from dist/engine/ (individual files), package root is 2 levels up\r\n\tconst packageRoot2 = path.resolve(currentDirname, '..', '..');\r\n\tconst srcEngine2 = path.join(packageRoot2, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine2, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine2;\r\n\t}\r\n\r\n\t// When running from dist/ (tsup bundle), package root is 1 level up\r\n\tconst packageRoot1 = path.resolve(currentDirname, '..');\r\n\tconst srcEngine1 = path.join(packageRoot1, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine1, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine1;\r\n\t}\r\n\r\n\tthrow new Error('Could not find engine source directory (src/engine/simulation.ts)');\r\n}\r\n\r\n/**\r\n * A compiled bot ready for sandboxed execution.\r\n * Stores the source path and export name — actual compilation\r\n * happens when creating a MatchSandbox or calling compileMatchBundle.\r\n */\r\nexport class BotBundle\r\n{\r\n\treadonly sourcePath: string;\r\n\treadonly exportName: string;\r\n\r\n\tconstructor(sourcePath: string, exportName: string)\r\n\t{\r\n\t\tif (!sourcePath || typeof sourcePath !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('sourcePath must be a non-empty string');\r\n\t\t}\r\n\t\tif (!exportName || typeof exportName !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('exportName must be a non-empty string');\r\n\t\t}\r\n\t\tthis.sourcePath = path.resolve(sourcePath);\r\n\t\tthis.exportName = exportName;\r\n\t}\r\n}\r\n\r\n/**\r\n * Options for esbuild compilation. Allows the caller to add esbuild\r\n * aliases (e.g., resolving @vibemancer/core to the TypeScript source).\r\n */\r\nexport interface CompileOptions\r\n{\r\n\t/** Additional esbuild alias entries (e.g., {'@vibemancer/core': '/path/to/src/index.ts'}). */\r\n\talias?: Record<string, string>;\r\n\t/** Additional modules to treat as external (not bundled). */\r\n\texternal?: string[];\r\n}\r\n\r\n/**\r\n * Compile a match bundle using esbuild. Bundles both bots + simulation engine\r\n * into a single self-contained IIFE with prototype freezing banner.\r\n *\r\n * No isolated-vm dependency — returns a plain JS string.\r\n */\r\nexport async function compileMatchBundle(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateMatchEntryPoint(\r\n\t\tbot1.sourcePath,\r\n\t\tbot1.exportName,\r\n\t\tbot2.sourcePath,\r\n\t\tbot2.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\t// Suppress warnings about top-level this in ESM\r\n\t\tlogLevel: 'error',\r\n\t\t// Match bundles run in sandboxed environments (Web Workers / isolated-vm)\r\n\t\t// and should never include Node.js native modules\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n\r\n/**\r\n * Compile a manual-play sandbox bundle. Bundles ONE opponent bot + ManualMatch\r\n * + simulation engine into a self-contained IIFE. The \"player\" wizard is a\r\n * worker-local stub that reads from `__latestHumanActions` (set per-step by\r\n * the host).\r\n *\r\n * Returns a plain JS string that, when loaded into a Web Worker, exposes the\r\n * `__manualMatchInit`, `__manualMatchStep`, `__manualMatchGuide`,\r\n * `__manualMatchRelease`, `__manualMatchSetInvincible`,\r\n * `__manualMatchGetState`, `__manualMatchGetResult`, and `__manualMatchDispose`\r\n * globals on the worker's globalThis.\r\n */\r\nexport async function compileManualMatchBundle(\r\n\topponent: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateManualMatchEntryPoint(\r\n\t\topponent.sourcePath,\r\n\t\topponent.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\tlogLevel: 'error',\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n","/**\n * VIBEMANCER — SANDBOX HARNESS\n *\n * Generates the entry point code that runs inside an isolated-vm isolate.\n * The harness bundles both bots + the simulation engine into a single IIFE\n * via esbuild, then exposes __fight and __simulate on globalThis.\n *\n * The prototype freeze banner runs before any module code, preventing\n * prototype pollution attacks between bots sharing the same isolate.\n */\n\n/**\n * JavaScript code injected as esbuild banner — runs before the IIFE bundle.\n *\n * 1. Freezes all built-in prototypes to prevent cross-bot sabotage via prototype\n * pollution. This does NOT prevent calling existing methods (e.g. Array.push\n * still works), it only prevents reassigning them.\n *\n * 2. Blocks all IO/network capabilities. Web Workers have fetch, XMLHttpRequest,\n * WebSocket, importScripts, etc. Bot code must not be able to make network\n * calls or load external scripts. Uses Object.defineProperty to make the block\n * irrecoverable (non-writable, non-configurable). In isolated-vm, these globals\n * don't exist — the try-catch makes the deletes a harmless no-op.\n */\nexport const PROTOTYPE_FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\n/**\n * Generate the TypeScript entry point for a match sandbox.\n *\n * This entry point imports both bots and the simulation engine, then\n * exposes __fight and __simulate on globalThis. esbuild bundles this\n * + all transitive imports into a single self-contained IIFE.\n *\n * @param bot1SourcePath - Absolute path to bot 1's TypeScript source file\n * @param bot1ExportName - Named export of bot 1's WizardFunction\n * @param bot2SourcePath - Absolute path to bot 2's TypeScript source file\n * @param bot2ExportName - Named export of bot 2's WizardFunction\n * @param engineDir - Absolute path to the engine directory (src/engine/)\n */\nexport function generateMatchEntryPoint(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\t// Use forward slashes for esbuild compatibility (works on all platforms)\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\n\t// All imports use the engineDir's parent (= packages/core/src/) as root.\n\t// When user bots alias @vibemancer/core → src/index-browser.ts, esbuild\n\t// deduplicates these with the bot's imports since they resolve to the same files.\n\t// This ensures the hooks runtime global state is shared between harness and bot.\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\nimport {DEFAULT_BUDGET} from '${srcPath}/engine/bot-compute-budget.ts';\n\nglobalThis.__fight = function __fight(options) {\n\t// Budgeted like every other fight path. This one matters MORE than it looks: it is what\n\t// sandboxFight uses, so it is the MCP fight tools and the CLI — the place a user's\n\t// runaway bot most directly burns server time. Leaving it unbudgeted while the backstop\n\t// moved from 60s to 110s would have made this path strictly worse than before.\n\tconst result = fight(__Bot1, __Bot2, {budgetLimits: DEFAULT_BUDGET, ...options});\n\treturn result;\n};\n\nglobalThis.__simulate = function __simulate(options) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\t// Budgeted like __fight. This path is trace and the optimizer — a single match rather\n\t// than ten — and leaving it out would have made it strictly worse than before, since the\n\t// same change raised its backstop from 60s to 110s.\n\tconst result = simulate(bot1, bot2, {budgetLimits: DEFAULT_BUDGET, ...simOptions});\n\treturn result;\n};\n`;\n}\n\n/**\n * Generate the entry point for a Manual Play sandbox bundle.\n *\n * Unlike the fight/simulate entry point (which exposes one-shot batch APIs),\n * the manual-match entry point holds a single long-lived ManualMatch instance\n * inside the worker and exposes per-tick step/rewind/guide APIs.\n *\n * The \"player\" wizard is a worker-local stub that returns whatever\n * `__latestHumanActions` is set to — this avoids the can't-postMessage-functions\n * problem (the function lives entirely worker-side, only data crosses the\n * boundary). Guided missiles use a similar pattern via\n * `__latestHumanMissileTargets[projectileId]`.\n *\n * @param opponentSourcePath - Absolute path to opponent bot's TypeScript source\n * @param opponentExportName - Named export of the opponent's WizardFunction\n * @param engineDir - Absolute path to src/engine/\n */\nexport function generateManualMatchEntryPoint(\n\topponentSourcePath: string,\n\topponentExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst opponentPath = opponentSourcePath.replace(/\\\\/g, '/');\n\treturn generateManualMatchEntryPointInner(\n\t\t`import {${opponentExportName} as __RawOpponent} from '${opponentPath}';`,\n\t\tengineDir,\n\t);\n}\n\n/**\n * Browser-friendly variant — the opponent is injected at runtime via\n * globalThis.__injectedBot1 (set by prepending the player's bot bundle to\n * the compiled output of this template). Used by the web client for manual\n * play against uploaded wizards.\n */\nexport function generateBrowserManualMatchEntryPoint(engineDir: string): string\n{\n\tconst opponentImport = 'var __RawOpponent = globalThis.__injectedBot1;\\n'\n\t\t+ 'if (!__RawOpponent) throw new Error(\"Opponent not injected (set globalThis.__injectedBot1)\");';\n\treturn generateManualMatchEntryPointInner(opponentImport, engineDir);\n}\n\nfunction generateManualMatchEntryPointInner(opponentImport: string, engineDir: string): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\n${opponentImport}\nimport {ManualMatch} from '${srcPath}/engine/manual-match.ts';\nimport {idle, turnToward, flyStraight} from '${srcPath}/hooks/action-builders.ts';\nimport {getMissileContext} from '${srcPath}/engine/hooks-runtime.ts';\n\n// Worker-local state — the player AI and guided-missile AIs read from these.\nvar __latestHumanActions = null;\nvar __latestHumanMissileTargets = {};\nvar __manualMatch = null;\n\nfunction __playerStub() {\n\tif (__latestHumanActions) {\n\t\treturn {_toAction: function() { return __latestHumanActions; }};\n\t}\n\treturn idle();\n}\n\nfunction __makeGuideStub(projectileId) {\n\treturn function() {\n\t\tvar target = __latestHumanMissileTargets[projectileId];\n\t\tif (!target) return flyStraight();\n\t\treturn turnToward(target.x, target.y);\n\t};\n}\n\nfunction __ensureMatch() {\n\tif (!__manualMatch) throw new Error('ManualMatch not initialized — call manualMatchInit first');\n\treturn __manualMatch;\n}\n\nglobalThis.__manualMatchInit = function(options) {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = (options && options.initialHumanActions) || null;\n\t__latestHumanMissileTargets = {};\n\t__manualMatch = new ManualMatch(__playerStub, __RawOpponent, options || {});\n\treturn __manualMatch.getGameState();\n};\n\nglobalThis.__manualMatchStep = function(options) {\n\tvar match = __ensureMatch();\n\tif (options) {\n\t\tif (options.humanActions) __latestHumanActions = options.humanActions;\n\t\tif (options.humanMissileTargets) {\n\t\t\t// Merge — the host may only update specific projectiles per call\n\t\t\tfor (var k in options.humanMissileTargets) {\n\t\t\t\t__latestHumanMissileTargets[k] = options.humanMissileTargets[k];\n\t\t\t}\n\t\t}\n\t}\n\tvar count = (options && options.count) || 1;\n\treturn match.step(count);\n};\n\nglobalThis.__manualMatchGuide = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.replaceMissileAI(id, __makeGuideStub(id));\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchRelease = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.restoreMissileAI(id);\n\tdelete __latestHumanMissileTargets[id];\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchSetInvincible = function(options) {\n\tvar match = __ensureMatch();\n\tvar idx = (options && options.wizardIndex) || 0;\n\tvar on = !!(options && options.on);\n\tmatch.setInvincible(idx, on);\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchGetState = function() {\n\tvar match = __ensureMatch();\n\treturn match.getGameState();\n};\n\nglobalThis.__manualMatchGetResult = function() {\n\tvar match = __ensureMatch();\n\treturn match.getResult();\n};\n\nglobalThis.__manualMatchDispose = function() {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = {move: {x: 0, y: 0}};\n\t__latestHumanMissileTargets = {};\n\treturn {ok: true};\n};\n`;\n}\n\n/**\n * Generate an alternate entry point where __fight/__simulate accept and return\n * JSON strings instead of structured objects. Used by the benchmark to compare\n * JSON serialization vs V8 structured clone performance.\n */\nexport function generateMatchEntryPointJSON(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\nimport {DEFAULT_BUDGET} from '${srcPath}/engine/bot-compute-budget.ts';\n\nglobalThis.__fightJSON = function __fightJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\t// Budgeted, same as the non-JSON variant — two generators, one contract.\n\tconst result = fight(__Bot1, __Bot2, {budgetLimits: DEFAULT_BUDGET, ...options});\n\treturn JSON.stringify(result);\n};\n\nglobalThis.__simulateJSON = function __simulateJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\t// Budgeted, same as the non-JSON variant — two generators, one contract.\n\tconst result = simulate(bot1, bot2, {budgetLimits: DEFAULT_BUDGET, ...simOptions});\n\treturn JSON.stringify(result);\n};\n`;\n}\n","/**\n * Run a fight between two PRECOMPILED bot bundles — the canonical uploaded-wizard\n * format where each IIFE sets `globalThis.__injectedBot1` (see compileSingleBotBundle).\n *\n * Shared by the Cloud Functions matchmaker and the devkit CLI so that a\n * `handle/botname` fight runs through the exact same engine as the live ladder.\n *\n * Both bundles run inside an isolated-vm isolate alongside a \"match template\" —\n * the engine + a `__fight` harness that reads the injected bots. The template is\n * built lazily from the engine source on first use and cached; callers that\n * already have one (the Cloud Functions committed MATCH_TEMPLATE) pass it in to\n * skip the esbuild step.\n *\n * Execution order inside the isolate: bot1 bundle → bot2 bundle → match template.\n */\n\nimport path from 'node:path';\nimport ivm from 'isolated-vm';\nimport {build} from 'esbuild';\nimport {getEngineDir} from './sandbox-compile.js';\nimport type {FightResult, SimulateResult} from './simulation.js';\nimport {DEFAULT_FIGHT_BACKSTOP_MS} from './bot-compute-budget.js';\n\n/** Memory limit per sandbox isolate (MB). */\nconst MEMORY_LIMIT_MB = 256;\n/**\n * Default timeout per fight (ms).\n *\n * This is a real safety limit: it stops a malicious or looping bot burning server time in\n * the matchmaker, so the DEFAULT must not move. It was previously hardcoded and unreachable\n * from any option, which made the suite unusable on a busy machine — one e2e fight exceeded\n * it and blocked seven consecutive commits, while a comparable fight run to the tick cap\n * finished in 1.3s on the same machine at the same moment. The limit was not catching a\n * runaway bot; it was catching load.\n */\nexport const DEFAULT_FIGHT_TIMEOUT_MS = DEFAULT_FIGHT_BACKSTOP_MS;\n\n/**\n * Resolve the effective fight timeout.\n *\n * Only ever EXTENDS the default. A caller who knows the work is legitimate (a test on a\n * slow machine) can ask for more; nobody can quietly ask for less, because weakening a\n * safety limit by accident is the direction that turns it into a flaky one.\n */\nexport function resolveFightTimeout(requested: number | undefined): number\n{\n\tif (typeof requested !== 'number' || !Number.isFinite(requested)) return DEFAULT_FIGHT_TIMEOUT_MS;\n\treturn Math.max(DEFAULT_FIGHT_TIMEOUT_MS, requested);\n}\n\nconst FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\nlet cachedTemplate: Promise<string> | null = null;\n\n/**\n * Build (and cache) the match template: the engine + `__fight`/`__simulate`\n * harness bundled into a single IIFE string, ready to run after two bot bundles\n * have set `globalThis.__injectedBot1`/`__injectedBot2`.\n */\nexport function buildMatchTemplate(): Promise<string>\n{\n\tif (!cachedTemplate)\n\t{\n\t\tcachedTemplate = (async(): Promise<string> =>\n\t\t{\n\t\t\tconst engineDir = getEngineDir();\n\t\t\tconst coreSrc = path.dirname(engineDir).replace(/\\\\/g, '/');\n\n\t\t\tconst entryPoint = `\nimport {fight, simulate} from '${coreSrc}/engine/simulation.ts';\nimport {wrapWithParams} from '${coreSrc}/engine/params-runtime.ts';\nimport {DEFAULT_BUDGET} from '${coreSrc}/engine/bot-compute-budget.ts';\n\nconst __Bot1 = (globalThis as any).__injectedBot1;\nconst __Bot2 = (globalThis as any).__injectedBot2;\n\nif (!__Bot1) throw new Error('Bot 1 not injected (set globalThis.__injectedBot1)');\nif (!__Bot2) throw new Error('Bot 2 not injected (set globalThis.__injectedBot2)');\n\n(globalThis as any).__fight = function __fight(options: any) {\n\t// Same per-bot compute budget the server uses, so a runaway bot fails the same way in\n\t// the CLI as it will on the ladder — and so it cannot hang someone's terminal.\n\treturn fight(__Bot1, __Bot2, {budgetLimits: DEFAULT_BUDGET, ...options});\n};\n\n(globalThis as any).__simulate = function __simulate(options: any) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\t// Budgeted like __fight — this is the CLI's trace/optimize path, and its backstop went\n\t// up to 110s with everything else, so leaving it out would make it worse than before.\n\treturn simulate(bot1, bot2, {budgetLimits: DEFAULT_BUDGET, ...simOptions});\n};\n`;\n\n\t\t\tconst result = await build({\n\t\t\t\tstdin: {contents: entryPoint, resolveDir: engineDir, loader: 'ts'},\n\t\t\t\tbundle: true,\n\t\t\t\twrite: false,\n\t\t\t\tformat: 'iife',\n\t\t\t\tplatform: 'neutral',\n\t\t\t\ttarget: 'es2022',\n\t\t\t\tbanner: {js: FREEZE_BANNER},\n\t\t\t\tlogLevel: 'error',\n\t\t\t\texternal: [\n\t\t\t\t\t'isolated-vm', 'esbuild',\n\t\t\t\t\t'node:path', 'node:fs', 'node:url',\n\t\t\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\n\t\t\t\t],\n\t\t\t});\n\n\t\t\tif (!result.outputFiles?.[0]) throw new Error('esbuild produced no match-template output');\n\t\t\treturn result.outputFiles[0].text;\n\t\t})();\n\t}\n\treturn cachedTemplate;\n}\n\nexport interface RunBundleFightOptions\n{\n\tseed: number;\n\t/** Prebuilt match template (e.g. the committed Cloud Functions one). Built lazily if omitted. */\n\tmatchTemplate?: string;\n\t/** Extend the per-fight isolate timeout. Can only raise it above the default. */\n\tfightTimeoutMs?: number;\n\t/** Skip per-tick history in the result (smaller copy out of the isolate). Default true. */\n\tskipHistory?: boolean;\n}\n\n/**\n * Run a fight between two compiled `__injectedBot1`-format bundles.\n *\n * bundle1 is saved and its global cleared before bundle2 runs, so bot code can\n * never read the opponent's function off globalThis. Both globals are deleted\n * after wiring so runtime bot code can't reach them either.\n */\nexport async function runBundleFight(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleFightOptions,\n): Promise<FightResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\tconst fightTimeoutMs = resolveFightTimeout(options.fightTimeoutMs);\n\tconst skipHistory = options.skipHistory ?? true;\n\n\t// Freeze prototypes/globals BEFORE the untrusted bot bundles run. The bundles\n\t// execute at module-eval (ahead of the template, whose banner used to be the\n\t// only freeze) — so without this a bot could pollute Object/Array/Math/etc. at\n\t// load time and corrupt the engine or its opponent. Mirrors the source path\n\t// (sandbox-harness), which already freezes before importing the bots.\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: fightTimeoutMs});\n\n\t\tconst fightFn = await jail.get('__fight');\n\t\tconst result = await fightFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy({seed: options.seed, skipHistory}).copyInto()],\n\t\t\t{timeout: fightTimeoutMs, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as FightResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n\nexport interface RunBundleSimulateOptions\n{\n\tseed?: number;\n\tspawnDistance?: number;\n\tmaxTicks?: number;\n\tmatchTemplate?: string;\n\t/** Extend the per-fight isolate timeout. Can only raise it above the default. */\n\tfightTimeoutMs?: number;\n}\n\n/**\n * Run a SINGLE match between two compiled bundles, returning the full per-tick\n * history (for tracing/debugging). Same isolate wiring as runBundleFight, but\n * calls the template's `__simulate` so the caller gets a SimulateResult.\n */\nexport async function runBundleSimulate(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleSimulateOptions = {},\n): Promise<SimulateResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\tconst fightTimeoutMs = resolveFightTimeout(options.fightTimeoutMs);\n\n\t// Freeze before the untrusted bundles run (see runBundleFight).\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: fightTimeoutMs});\n\n\t\tconst simulateFn = await jail.get('__simulate');\n\t\tconst simOptions = {\n\t\t\tseed: options.seed ?? 1,\n\t\t\tspawnDistance: options.spawnDistance,\n\t\t\tmaxTicks: options.maxTicks,\n\t\t};\n\t\tconst result = await simulateFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy(simOptions).copyInto()],\n\t\t\t{timeout: fightTimeoutMs, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as SimulateResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n"],"mappings":"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","names":["path","ivm","build","path","build","ivm"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibemancer/core",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Vibemancer game engine - competitive programming wizard battle arena",
5
5
  "type": "module",
6
6
  "author": "Low Entry",
@@ -1102,7 +1102,10 @@ export function simulate(
1102
1102
  history.push(getPlayerState(0, wizards, projectiles, currentTick, result.events));
1103
1103
  }
1104
1104
 
1105
- // Track death and add grace period (0.5s = 50 ticks at 100 tps)
1105
+ // Track death and add grace period (2s = 200 ticks at 100 tps). The window is what
1106
+ // makes a Draw reachable: a second death inside it is still recorded. Pinned as
1107
+ // behaviour by tests/death-grace-period.test.ts, because the previous version of this
1108
+ // sentence said 0.5s and 50 ticks while the code below used 200.
1106
1109
  if (!deathTick && (wizards[0]!.health <= 0 || wizards[1]!.health <= 0))
1107
1110
  {
1108
1111
  deathTick = currentTick;
@@ -9,4 +9,17 @@
9
9
  * Used to gate spectator replays: a recorded match can only be re-simulated when
10
10
  * the runtime engine version matches the version that produced the match.
11
11
  */
12
- export const ENGINE_VERSION = 4396905295552957;
12
+ export const ENGINE_VERSION = 4204441759696527;
13
+
14
+ /**
15
+ * Historical versions that denote the SAME engine as `ENGINE_VERSION`.
16
+ *
17
+ * A content hash answers "is this the same source?", not "is this the same behaviour?" —
18
+ * and those come apart when the HASHING is corrected rather than the engine. Spectate
19
+ * consults this through `canReplayMatch` so a stored match is not thrown away over a
20
+ * number that moved for a reason no player could act on.
21
+ *
22
+ * Generated from `scripts/engine-version-equivalents.json`, which holds the reason for
23
+ * each entry and the rule for adding one. A wrong entry fails OPEN; read it first.
24
+ */
25
+ export const EQUIVALENT_ENGINE_VERSIONS: readonly number[] = [1688330189011034];
@@ -47,7 +47,13 @@ export function useHealth(): number
47
47
  * Clamped to [5, 855] — the ARENA bounds minus the wizard radius, NOT the playfield. That
48
48
  * distinction is the difference between living and dying: the playfield is [30, 830] and
49
49
  * everything outside it is lava, so you can walk straight out of the safe area and be
50
- * killed. Nothing stops you. Check against ARENA_MIN/ARENA_MAX, not against these numbers.
50
+ * killed. Nothing stops you.
51
+ *
52
+ * And the number you actually need is neither of those. Lava is tested against your EDGE,
53
+ * not your centre (`isInLava` checks `x + WIZARD_RADIUS > ARENA_MAX`), and this returns your
54
+ * CENTRE — so with a radius of 5 you die once your centre passes 825. The safe range to
55
+ * steer by is [35, 825]. Aiming for 828 because "the playfield is [30, 830]" is fatal, which
56
+ * is exactly what a real fight trace showed: LAVA_DEATH at (826, 430).
51
57
  *
52
58
  * (This previously quoted the playfield-sized bounds as the clamp, which told the reader
53
59
  * they were safely fenced in. They are not — see docs-match-engine.test.ts.)
@@ -9,6 +9,7 @@
9
9
  export * from './types.js';
10
10
  export * from './rules.js';
11
11
  export * from './engine-version.js';
12
+ export * from './replay-compat.js';
12
13
  export * from './engine/simulation.js';
13
14
  export * from './engine/hooks-runtime.js';
14
15
  export * from './engine/physics.js';