@vibemancer/core 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-NPRXDUQZ.js → chunk-LNFGLNTJ.js} +8 -6
- package/dist/chunk-LNFGLNTJ.js.map +1 -0
- package/dist/index-browser.d.ts +3 -2
- package/dist/index-browser.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/manual-match.ts +3 -2
- package/src/engine/simulation.ts +3 -2
- package/src/engine-version.ts +1 -1
- package/src/rules.ts +6 -0
- package/dist/chunk-NPRXDUQZ.js.map +0 -1
package/dist/index-browser.d.ts
CHANGED
|
@@ -565,6 +565,7 @@ declare function calculateMissileSimilarity(prev: MissileConfig | undefined, cur
|
|
|
565
565
|
*/
|
|
566
566
|
declare function calculateWarmupMultiplier(prev: MissileConfig | undefined, current: MissileConfig): number;
|
|
567
567
|
declare const MATCH_DURATION = 30000;
|
|
568
|
+
declare const DEFAULT_SEED = 42;
|
|
568
569
|
declare const MAX_HEALTH: number;
|
|
569
570
|
declare const COLLISION_RADIUS = 5;
|
|
570
571
|
declare const MOVE_SPEED: number;
|
|
@@ -605,7 +606,7 @@ declare function currentRuleset(): Record<string, number>;
|
|
|
605
606
|
* Used to gate spectator replays: a recorded match can only be re-simulated when
|
|
606
607
|
* the runtime engine version matches the version that produced the match.
|
|
607
608
|
*/
|
|
608
|
-
declare const ENGINE_VERSION =
|
|
609
|
+
declare const ENGINE_VERSION = 1782526029280;
|
|
609
610
|
|
|
610
611
|
interface InternalWizardState extends WizardState {
|
|
611
612
|
missileConfig?: MissileConfig;
|
|
@@ -2876,4 +2877,4 @@ declare function diagnoseTrace(events: TraceEvent[], summary: TraceSummary): str
|
|
|
2876
2877
|
/** Format diagnostic tips as a human-readable string. */
|
|
2877
2878
|
declare function formatDiagnosis(tips: string[]): string;
|
|
2878
2879
|
|
|
2879
|
-
export { ALL_BOTS, ARENA_MAX, ARENA_MIN, ARENA_SIZE, ARENA_WATER_BUFFER, type ActionBuilder, type AnalyzedThreat, Archlich, Archmage, BLINK_CAST_TIME, BLINK_COOLDOWN, BLINK_MAX_COOLDOWN, BLINK_MAX_RANGE, BLINK_MIN_COOLDOWN, BLINK_RANGE, BOT_GROUPS, Battlemage, type BlinkEvent, Bonemancer, type BotError, BrowserManualMatchSandbox, BrowserMatchSandbox, type BrowserSandboxOptions, CASTING_MOVEMENT_MULT, COLLISION_RADIUS, type CastCancelEvent, type CastStartEvent, Critter, Doombringer, ENGINE_VERSION, type EnemyState, FIGHT_SPAWN_DISTANCES, type FightResult, type FightStats, type FightWinner, type FinalAction, Flamecaller, GCD_DURATION, type GameConfig, type GameState, Golem, Hogger, type HomingParams, type HookState, Infernalist, type InternalWizardState, KNOCKBACK_DAMAGE_THRESHOLD, KNOCKBACK_DECAY, KNOCKBACK_DELAY, KNOCKBACK_SPEED_PER_DAMAGE, LAVA_BORDER_WIDTH, Lich, MATCH_DURATION, MAX_HEALTH, MISSILE_BASE_CAST, MISSILE_BASE_RADIUS, MISSILE_DAMAGE_POWER, MISSILE_DAMAGE_RADIUS_SCALE, MISSILE_DAMAGE_SCALE, MISSILE_HOMING_COEFF, MISSILE_MIN_CAST_TIME, MISSILE_MIN_DAMAGE, MISSILE_MIN_DURATION, MISSILE_MIN_SPEED, MISSILE_RADIUS_PER_DAMAGE, MISSILE_SPEED_DURATION_BASELINE, MISSILE_SPEED_DURATION_COEFF, MISSILE_TURN_DURATION_COEFF, MOVEMENT_SPEED, MOVE_SPEED, ManualMatch, type ManualMatchInitOptions, type ManualMatchOptions, type ManualMatchStepRequest, type MatchWinner, type MissileAction, type MissileActions, type MissileConfig, type MissileContext, type MissileExpiredEvent, type MissileFunction$1 as MissileFunction, type MissileHitEvent, type MissileLaunchEvent, type MissileOobEvent, type MissileTemplate, Nightblade, type ParamDeclaration, type Position, type ProjectileState, Pyromancer, RULES, RULESET_RANGES, type RefObject, Rookie, SHIELD_CAST_TIME, SHIELD_DECAY_PER_SECOND, SHIELD_DECAY_RATE, SHIELD_MAX_BLOCK, SHIELD_MAX_STRENGTH, SHIELD_MIN_BLOCK, SHIELD_MIN_STRENGTH, SPAWN_DISTANCE, type SeekerParams, Sentinel, Shadowblade, type ShieldBlockEvent, type ShieldStartEvent, type SimEvent, type SimulateResult, Spellbinder, Spellseeker, Spellshot, Spellspinner, Spelltracer, Spellweaver, type SpiralParams, type StepResult, Stormcaller, Stormchaser, Stormforger, type StraightParams, TICKS_PER_SECOND, TICK_DURATION_MS, TargetDummy, type TestBotBuilder, type TestFightResult, type TestSimulateResult, type TraceBotSummary, type TraceEvent, type TraceEventType, type TraceSummary, Turtle, type Velocity, Voidblade, WARMUP_DURATION_TOLERANCE, WARMUP_MAX_BONUS, WARMUP_MAX_PENALTY, WARMUP_SPEED_TOLERANCE, WARMUP_TURN_TOLERANCE, WIZARD_HEALTH, WIZARD_RADIUS, Warmage, type WizardActions, type WizardContext, type WizardDeathEvent, type WizardEntry, type WizardFunction, type WizardGroup, type WizardLavaDeathEvent, type WizardState, type WorkerFactory, type WorkerLike, analyzeThreats, angleDiff, angleInRange, angleTo, applyDamage, applyRulesetOverrides, blink, browserSandboxFight, browserSandboxSimulate, calculateBlinkCooldown, calculateMissileCastTime, calculateMissileRadius, calculateMissileSimilarity, calculateShieldBlock, calculateWarmupMultiplier, cancel, cancelCast, clampPositionToArena, clampToArena, clearHooks, clearParamValues, completeCast, createEntitySeed, createInitialState, createRandom, createWorkerScript, currentRuleset, diagnoseTrace, directionAway, directionTo, distanceTo, effectiveTurnRateCost, extractAction, extractMissileAction, extractStats, extractTraceEvents, fight, findInRange, findNearest, fitMissileForEscapingTarget, fitMissileToBudget, flyStraight, formatDiagnosis, formatStats, formatTraceEvents, formatTraceSummary, generateCandidates, generateCombos, getAdaptiveMissileConfig, getEffectiveRange, getLeadPosition, getMissileCastTime, getMissileContext, getPlayerState, getWizardContext, hashCombine, homingMissile, idle, inRange, interceptAngle, isBannedBotName, isInLava, magnitude, matchesAnyBanRule, missile, move, moveInDirection, moveProjectile, moveWizard, narrowRange, nextRandom, normalize, normalizeAngle, predictPosition, resetAllHooks, resetRuleset, resolveWizardCollision, runWithHooks, runWizardWithContext, scoreFight, scoreFightAsWizard2, seekerMissile, setParamValues, shield, simulate, simulateMinDuration, sortByDistance, spiralMissile, startCast, startDiscovery, stopDiscovery, straightMissile, summarizeTrace, sweptCircleCollision, testBot, tick, turnToAngle, turnToward, updateShield, useArenaSize, useBlinkCooldown, useCastProgress, useCastingSpell, useClosestThreat, useDamageDealt, useDamageTaken, useEffect, useEnemy, useHealth, useLastHitTick, useLastMissileConfig, useMemo, useMyProjectiles, useMyThreatsToEnemy, useParam, usePosition, useProjectiles, useRandom, useRef, useShieldStrength, useState, useStatus, useThreats, useTick, useTicksUntilReady, useVelocity, validateHookCall, validateMissileConfig, withMissileContext, withWizardContext, wrapWithParams };
|
|
2880
|
+
export { ALL_BOTS, ARENA_MAX, ARENA_MIN, ARENA_SIZE, ARENA_WATER_BUFFER, type ActionBuilder, type AnalyzedThreat, Archlich, Archmage, BLINK_CAST_TIME, BLINK_COOLDOWN, BLINK_MAX_COOLDOWN, BLINK_MAX_RANGE, BLINK_MIN_COOLDOWN, BLINK_RANGE, BOT_GROUPS, Battlemage, type BlinkEvent, Bonemancer, type BotError, BrowserManualMatchSandbox, BrowserMatchSandbox, type BrowserSandboxOptions, CASTING_MOVEMENT_MULT, COLLISION_RADIUS, type CastCancelEvent, type CastStartEvent, Critter, DEFAULT_SEED, Doombringer, ENGINE_VERSION, type EnemyState, FIGHT_SPAWN_DISTANCES, type FightResult, type FightStats, type FightWinner, type FinalAction, Flamecaller, GCD_DURATION, type GameConfig, type GameState, Golem, Hogger, type HomingParams, type HookState, Infernalist, type InternalWizardState, KNOCKBACK_DAMAGE_THRESHOLD, KNOCKBACK_DECAY, KNOCKBACK_DELAY, KNOCKBACK_SPEED_PER_DAMAGE, LAVA_BORDER_WIDTH, Lich, MATCH_DURATION, MAX_HEALTH, MISSILE_BASE_CAST, MISSILE_BASE_RADIUS, MISSILE_DAMAGE_POWER, MISSILE_DAMAGE_RADIUS_SCALE, MISSILE_DAMAGE_SCALE, MISSILE_HOMING_COEFF, MISSILE_MIN_CAST_TIME, MISSILE_MIN_DAMAGE, MISSILE_MIN_DURATION, MISSILE_MIN_SPEED, MISSILE_RADIUS_PER_DAMAGE, MISSILE_SPEED_DURATION_BASELINE, MISSILE_SPEED_DURATION_COEFF, MISSILE_TURN_DURATION_COEFF, MOVEMENT_SPEED, MOVE_SPEED, ManualMatch, type ManualMatchInitOptions, type ManualMatchOptions, type ManualMatchStepRequest, type MatchWinner, type MissileAction, type MissileActions, type MissileConfig, type MissileContext, type MissileExpiredEvent, type MissileFunction$1 as MissileFunction, type MissileHitEvent, type MissileLaunchEvent, type MissileOobEvent, type MissileTemplate, Nightblade, type ParamDeclaration, type Position, type ProjectileState, Pyromancer, RULES, RULESET_RANGES, type RefObject, Rookie, SHIELD_CAST_TIME, SHIELD_DECAY_PER_SECOND, SHIELD_DECAY_RATE, SHIELD_MAX_BLOCK, SHIELD_MAX_STRENGTH, SHIELD_MIN_BLOCK, SHIELD_MIN_STRENGTH, SPAWN_DISTANCE, type SeekerParams, Sentinel, Shadowblade, type ShieldBlockEvent, type ShieldStartEvent, type SimEvent, type SimulateResult, Spellbinder, Spellseeker, Spellshot, Spellspinner, Spelltracer, Spellweaver, type SpiralParams, type StepResult, Stormcaller, Stormchaser, Stormforger, type StraightParams, TICKS_PER_SECOND, TICK_DURATION_MS, TargetDummy, type TestBotBuilder, type TestFightResult, type TestSimulateResult, type TraceBotSummary, type TraceEvent, type TraceEventType, type TraceSummary, Turtle, type Velocity, Voidblade, WARMUP_DURATION_TOLERANCE, WARMUP_MAX_BONUS, WARMUP_MAX_PENALTY, WARMUP_SPEED_TOLERANCE, WARMUP_TURN_TOLERANCE, WIZARD_HEALTH, WIZARD_RADIUS, Warmage, type WizardActions, type WizardContext, type WizardDeathEvent, type WizardEntry, type WizardFunction, type WizardGroup, type WizardLavaDeathEvent, type WizardState, type WorkerFactory, type WorkerLike, analyzeThreats, angleDiff, angleInRange, angleTo, applyDamage, applyRulesetOverrides, blink, browserSandboxFight, browserSandboxSimulate, calculateBlinkCooldown, calculateMissileCastTime, calculateMissileRadius, calculateMissileSimilarity, calculateShieldBlock, calculateWarmupMultiplier, cancel, cancelCast, clampPositionToArena, clampToArena, clearHooks, clearParamValues, completeCast, createEntitySeed, createInitialState, createRandom, createWorkerScript, currentRuleset, diagnoseTrace, directionAway, directionTo, distanceTo, effectiveTurnRateCost, extractAction, extractMissileAction, extractStats, extractTraceEvents, fight, findInRange, findNearest, fitMissileForEscapingTarget, fitMissileToBudget, flyStraight, formatDiagnosis, formatStats, formatTraceEvents, formatTraceSummary, generateCandidates, generateCombos, getAdaptiveMissileConfig, getEffectiveRange, getLeadPosition, getMissileCastTime, getMissileContext, getPlayerState, getWizardContext, hashCombine, homingMissile, idle, inRange, interceptAngle, isBannedBotName, isInLava, magnitude, matchesAnyBanRule, missile, move, moveInDirection, moveProjectile, moveWizard, narrowRange, nextRandom, normalize, normalizeAngle, predictPosition, resetAllHooks, resetRuleset, resolveWizardCollision, runWithHooks, runWizardWithContext, scoreFight, scoreFightAsWizard2, seekerMissile, setParamValues, shield, simulate, simulateMinDuration, sortByDistance, spiralMissile, startCast, startDiscovery, stopDiscovery, straightMissile, summarizeTrace, sweptCircleCollision, testBot, tick, turnToAngle, turnToward, updateShield, useArenaSize, useBlinkCooldown, useCastProgress, useCastingSpell, useClosestThreat, useDamageDealt, useDamageTaken, useEffect, useEnemy, useHealth, useLastHitTick, useLastMissileConfig, useMemo, useMyProjectiles, useMyThreatsToEnemy, useParam, usePosition, useProjectiles, useRandom, useRef, useShieldStrength, useState, useStatus, useThreats, useTick, useTicksUntilReady, useVelocity, validateHookCall, validateMissileConfig, withMissileContext, withWizardContext, wrapWithParams };
|
package/dist/index-browser.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
CASTING_MOVEMENT_MULT,
|
|
21
21
|
COLLISION_RADIUS,
|
|
22
22
|
Critter,
|
|
23
|
+
DEFAULT_SEED,
|
|
23
24
|
Doombringer,
|
|
24
25
|
ENGINE_VERSION,
|
|
25
26
|
FIGHT_SPAWN_DISTANCES,
|
|
@@ -221,7 +222,7 @@ import {
|
|
|
221
222
|
withMissileContext,
|
|
222
223
|
withWizardContext,
|
|
223
224
|
wrapWithParams
|
|
224
|
-
} from "./chunk-
|
|
225
|
+
} from "./chunk-LNFGLNTJ.js";
|
|
225
226
|
export {
|
|
226
227
|
ALL_BOTS,
|
|
227
228
|
ARENA_MAX,
|
|
@@ -244,6 +245,7 @@ export {
|
|
|
244
245
|
CASTING_MOVEMENT_MULT,
|
|
245
246
|
COLLISION_RADIUS,
|
|
246
247
|
Critter,
|
|
248
|
+
DEFAULT_SEED,
|
|
247
249
|
Doombringer,
|
|
248
250
|
ENGINE_VERSION,
|
|
249
251
|
FIGHT_SPAWN_DISTANCES,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FightResult, SimulateResult } from './index-browser.js';
|
|
2
|
-
export { ALL_BOTS, ARENA_MAX, ARENA_MIN, ARENA_SIZE, ARENA_WATER_BUFFER, ActionBuilder, AnalyzedThreat, Archlich, Archmage, BLINK_CAST_TIME, BLINK_COOLDOWN, BLINK_MAX_COOLDOWN, BLINK_MAX_RANGE, BLINK_MIN_COOLDOWN, BLINK_RANGE, BOT_GROUPS, Battlemage, BlinkEvent, Bonemancer, BotError, BrowserManualMatchSandbox, BrowserMatchSandbox, BrowserSandboxOptions, CASTING_MOVEMENT_MULT, COLLISION_RADIUS, CastCancelEvent, CastStartEvent, Critter, Doombringer, ENGINE_VERSION, EnemyState, FIGHT_SPAWN_DISTANCES, FightStats, FightWinner, FinalAction, Flamecaller, GCD_DURATION, GameConfig, GameState, Golem, Hogger, HomingParams, HookState, Infernalist, InternalWizardState, KNOCKBACK_DAMAGE_THRESHOLD, KNOCKBACK_DECAY, KNOCKBACK_DELAY, KNOCKBACK_SPEED_PER_DAMAGE, LAVA_BORDER_WIDTH, Lich, MATCH_DURATION, MAX_HEALTH, MISSILE_BASE_CAST, MISSILE_BASE_RADIUS, MISSILE_DAMAGE_POWER, MISSILE_DAMAGE_RADIUS_SCALE, MISSILE_DAMAGE_SCALE, MISSILE_HOMING_COEFF, MISSILE_MIN_CAST_TIME, MISSILE_MIN_DAMAGE, MISSILE_MIN_DURATION, MISSILE_MIN_SPEED, MISSILE_RADIUS_PER_DAMAGE, MISSILE_SPEED_DURATION_BASELINE, MISSILE_SPEED_DURATION_COEFF, MISSILE_TURN_DURATION_COEFF, MOVEMENT_SPEED, MOVE_SPEED, ManualMatch, ManualMatchInitOptions, ManualMatchOptions, ManualMatchStepRequest, MatchWinner, MissileAction, MissileActions, MissileConfig, MissileContext, MissileExpiredEvent, MissileFunction, MissileHitEvent, MissileLaunchEvent, MissileOobEvent, MissileTemplate, Nightblade, ParamDeclaration, Position, ProjectileState, Pyromancer, RULES, RULESET_RANGES, RefObject, Rookie, SHIELD_CAST_TIME, SHIELD_DECAY_PER_SECOND, SHIELD_DECAY_RATE, SHIELD_MAX_BLOCK, SHIELD_MAX_STRENGTH, SHIELD_MIN_BLOCK, SHIELD_MIN_STRENGTH, SPAWN_DISTANCE, SeekerParams, Sentinel, Shadowblade, ShieldBlockEvent, ShieldStartEvent, SimEvent, Spellbinder, Spellseeker, Spellshot, Spellspinner, Spelltracer, Spellweaver, SpiralParams, StepResult, Stormcaller, Stormchaser, Stormforger, StraightParams, TICKS_PER_SECOND, TICK_DURATION_MS, TargetDummy, TestBotBuilder, TestFightResult, TestSimulateResult, TraceBotSummary, TraceEvent, TraceEventType, TraceSummary, Turtle, Velocity, Voidblade, WARMUP_DURATION_TOLERANCE, WARMUP_MAX_BONUS, WARMUP_MAX_PENALTY, WARMUP_SPEED_TOLERANCE, WARMUP_TURN_TOLERANCE, WIZARD_HEALTH, WIZARD_RADIUS, Warmage, WizardActions, WizardContext, WizardDeathEvent, WizardEntry, WizardFunction, WizardGroup, WizardLavaDeathEvent, WizardState, WorkerFactory, WorkerLike, analyzeThreats, angleDiff, angleInRange, angleTo, applyDamage, applyRulesetOverrides, blink, browserSandboxFight, browserSandboxSimulate, calculateBlinkCooldown, calculateMissileCastTime, calculateMissileRadius, calculateMissileSimilarity, calculateShieldBlock, calculateWarmupMultiplier, cancel, cancelCast, clampPositionToArena, clampToArena, clearHooks, clearParamValues, completeCast, createEntitySeed, createInitialState, createRandom, createWorkerScript, currentRuleset, diagnoseTrace, directionAway, directionTo, distanceTo, effectiveTurnRateCost, extractAction, extractMissileAction, extractStats, extractTraceEvents, fight, findInRange, findNearest, fitMissileForEscapingTarget, fitMissileToBudget, flyStraight, formatDiagnosis, formatStats, formatTraceEvents, formatTraceSummary, generateCandidates, generateCombos, getAdaptiveMissileConfig, getEffectiveRange, getLeadPosition, getMissileCastTime, getMissileContext, getPlayerState, getWizardContext, hashCombine, homingMissile, idle, inRange, interceptAngle, isBannedBotName, isInLava, magnitude, matchesAnyBanRule, missile, move, moveInDirection, moveProjectile, moveWizard, narrowRange, nextRandom, normalize, normalizeAngle, predictPosition, resetAllHooks, resetRuleset, resolveWizardCollision, runWithHooks, runWizardWithContext, scoreFight, scoreFightAsWizard2, seekerMissile, setParamValues, shield, simulate, simulateMinDuration, sortByDistance, spiralMissile, startCast, startDiscovery, stopDiscovery, straightMissile, summarizeTrace, sweptCircleCollision, testBot, tick, turnToAngle, turnToward, updateShield, useArenaSize, useBlinkCooldown, useCastProgress, useCastingSpell, useClosestThreat, useDamageDealt, useDamageTaken, useEffect, useEnemy, useHealth, useLastHitTick, useLastMissileConfig, useMemo, useMyProjectiles, useMyThreatsToEnemy, useParam, usePosition, useProjectiles, useRandom, useRef, useShieldStrength, useState, useStatus, useThreats, useTick, useTicksUntilReady, useVelocity, validateHookCall, validateMissileConfig, withMissileContext, withWizardContext, wrapWithParams } from './index-browser.js';
|
|
2
|
+
export { ALL_BOTS, ARENA_MAX, ARENA_MIN, ARENA_SIZE, ARENA_WATER_BUFFER, ActionBuilder, AnalyzedThreat, Archlich, Archmage, BLINK_CAST_TIME, BLINK_COOLDOWN, BLINK_MAX_COOLDOWN, BLINK_MAX_RANGE, BLINK_MIN_COOLDOWN, BLINK_RANGE, BOT_GROUPS, Battlemage, BlinkEvent, Bonemancer, BotError, BrowserManualMatchSandbox, BrowserMatchSandbox, BrowserSandboxOptions, CASTING_MOVEMENT_MULT, COLLISION_RADIUS, CastCancelEvent, CastStartEvent, Critter, DEFAULT_SEED, Doombringer, ENGINE_VERSION, EnemyState, FIGHT_SPAWN_DISTANCES, FightStats, FightWinner, FinalAction, Flamecaller, GCD_DURATION, GameConfig, GameState, Golem, Hogger, HomingParams, HookState, Infernalist, InternalWizardState, KNOCKBACK_DAMAGE_THRESHOLD, KNOCKBACK_DECAY, KNOCKBACK_DELAY, KNOCKBACK_SPEED_PER_DAMAGE, LAVA_BORDER_WIDTH, Lich, MATCH_DURATION, MAX_HEALTH, MISSILE_BASE_CAST, MISSILE_BASE_RADIUS, MISSILE_DAMAGE_POWER, MISSILE_DAMAGE_RADIUS_SCALE, MISSILE_DAMAGE_SCALE, MISSILE_HOMING_COEFF, MISSILE_MIN_CAST_TIME, MISSILE_MIN_DAMAGE, MISSILE_MIN_DURATION, MISSILE_MIN_SPEED, MISSILE_RADIUS_PER_DAMAGE, MISSILE_SPEED_DURATION_BASELINE, MISSILE_SPEED_DURATION_COEFF, MISSILE_TURN_DURATION_COEFF, MOVEMENT_SPEED, MOVE_SPEED, ManualMatch, ManualMatchInitOptions, ManualMatchOptions, ManualMatchStepRequest, MatchWinner, MissileAction, MissileActions, MissileConfig, MissileContext, MissileExpiredEvent, MissileFunction, MissileHitEvent, MissileLaunchEvent, MissileOobEvent, MissileTemplate, Nightblade, ParamDeclaration, Position, ProjectileState, Pyromancer, RULES, RULESET_RANGES, RefObject, Rookie, SHIELD_CAST_TIME, SHIELD_DECAY_PER_SECOND, SHIELD_DECAY_RATE, SHIELD_MAX_BLOCK, SHIELD_MAX_STRENGTH, SHIELD_MIN_BLOCK, SHIELD_MIN_STRENGTH, SPAWN_DISTANCE, SeekerParams, Sentinel, Shadowblade, ShieldBlockEvent, ShieldStartEvent, SimEvent, Spellbinder, Spellseeker, Spellshot, Spellspinner, Spelltracer, Spellweaver, SpiralParams, StepResult, Stormcaller, Stormchaser, Stormforger, StraightParams, TICKS_PER_SECOND, TICK_DURATION_MS, TargetDummy, TestBotBuilder, TestFightResult, TestSimulateResult, TraceBotSummary, TraceEvent, TraceEventType, TraceSummary, Turtle, Velocity, Voidblade, WARMUP_DURATION_TOLERANCE, WARMUP_MAX_BONUS, WARMUP_MAX_PENALTY, WARMUP_SPEED_TOLERANCE, WARMUP_TURN_TOLERANCE, WIZARD_HEALTH, WIZARD_RADIUS, Warmage, WizardActions, WizardContext, WizardDeathEvent, WizardEntry, WizardFunction, WizardGroup, WizardLavaDeathEvent, WizardState, WorkerFactory, WorkerLike, analyzeThreats, angleDiff, angleInRange, angleTo, applyDamage, applyRulesetOverrides, blink, browserSandboxFight, browserSandboxSimulate, calculateBlinkCooldown, calculateMissileCastTime, calculateMissileRadius, calculateMissileSimilarity, calculateShieldBlock, calculateWarmupMultiplier, cancel, cancelCast, clampPositionToArena, clampToArena, clearHooks, clearParamValues, completeCast, createEntitySeed, createInitialState, createRandom, createWorkerScript, currentRuleset, diagnoseTrace, directionAway, directionTo, distanceTo, effectiveTurnRateCost, extractAction, extractMissileAction, extractStats, extractTraceEvents, fight, findInRange, findNearest, fitMissileForEscapingTarget, fitMissileToBudget, flyStraight, formatDiagnosis, formatStats, formatTraceEvents, formatTraceSummary, generateCandidates, generateCombos, getAdaptiveMissileConfig, getEffectiveRange, getLeadPosition, getMissileCastTime, getMissileContext, getPlayerState, getWizardContext, hashCombine, homingMissile, idle, inRange, interceptAngle, isBannedBotName, isInLava, magnitude, matchesAnyBanRule, missile, move, moveInDirection, moveProjectile, moveWizard, narrowRange, nextRandom, normalize, normalizeAngle, predictPosition, resetAllHooks, resetRuleset, resolveWizardCollision, runWithHooks, runWizardWithContext, scoreFight, scoreFightAsWizard2, seekerMissile, setParamValues, shield, simulate, simulateMinDuration, sortByDistance, spiralMissile, startCast, startDiscovery, stopDiscovery, straightMissile, summarizeTrace, sweptCircleCollision, testBot, tick, turnToAngle, turnToward, updateShield, useArenaSize, useBlinkCooldown, useCastProgress, useCastingSpell, useClosestThreat, useDamageDealt, useDamageTaken, useEffect, useEnemy, useHealth, useLastHitTick, useLastMissileConfig, useMemo, useMyProjectiles, useMyThreatsToEnemy, useParam, usePosition, useProjectiles, useRandom, useRef, useShieldStrength, useState, useStatus, useThreats, useTick, useTicksUntilReady, useVelocity, validateHookCall, validateMissileConfig, withMissileContext, withWizardContext, wrapWithParams } from './index-browser.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A compiled bot ready for sandboxed execution.
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
CASTING_MOVEMENT_MULT,
|
|
21
21
|
COLLISION_RADIUS,
|
|
22
22
|
Critter,
|
|
23
|
+
DEFAULT_SEED,
|
|
23
24
|
Doombringer,
|
|
24
25
|
ENGINE_VERSION,
|
|
25
26
|
FIGHT_SPAWN_DISTANCES,
|
|
@@ -221,7 +222,7 @@ import {
|
|
|
221
222
|
withMissileContext,
|
|
222
223
|
withWizardContext,
|
|
223
224
|
wrapWithParams
|
|
224
|
-
} from "./chunk-
|
|
225
|
+
} from "./chunk-LNFGLNTJ.js";
|
|
225
226
|
|
|
226
227
|
// src/engine/sandbox.ts
|
|
227
228
|
import ivm from "isolated-vm";
|
|
@@ -688,6 +689,7 @@ export {
|
|
|
688
689
|
CASTING_MOVEMENT_MULT,
|
|
689
690
|
COLLISION_RADIUS,
|
|
690
691
|
Critter,
|
|
692
|
+
DEFAULT_SEED,
|
|
691
693
|
Doombringer,
|
|
692
694
|
ENGINE_VERSION,
|
|
693
695
|
FIGHT_SPAWN_DISTANCES,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/engine/sandbox.ts","../src/engine/sandbox-compile.ts","../src/engine/sandbox-harness.ts","../src/engine/bundle-fight.ts"],"sourcesContent":["/**\r\n * VIBEMANCER — SANDBOX\r\n *\r\n * Provides isolated-vm sandboxing for bot code execution. Both bots + the\r\n * entire simulation engine run inside a single V8 isolate, so there is ZERO\r\n * per-tick boundary crossing overhead. The only data crossing the boundary\r\n * is fight/simulate options going in and results coming out.\r\n *\r\n * Architecture:\r\n * - Host: creates isolate, loads compiled bundle, calls __fight/__simulate\r\n * - Isolate: contains both bots + full simulation engine, runs fight/simulate\r\n *\r\n * Safety:\r\n * - Memory limit (default 512 MB) catches memory bombs\r\n * - Timeout (default 30s) catches infinite loops\r\n * - Prototype freeze prevents cross-bot sabotage\r\n * - platform: 'neutral' strips Node.js APIs (no fs/net/process)\r\n */\r\n\r\nimport ivm from 'isolated-vm';\r\nimport type {FightResult, SimulateResult} from './simulation.js';\r\nimport {BotBundle, compileMatchBundle} from './sandbox-compile.js';\r\nimport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n// Re-export BotBundle so existing imports from sandbox.ts keep working\r\nexport {BotBundle} from './sandbox-compile.js';\r\nexport {compileMatchBundle, compileManualMatchBundle} from './sandbox-compile.js';\r\nexport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n/**\r\n * Options for sandbox creation.\r\n */\r\nexport interface SandboxOptions\r\n{\r\n\t/** Memory limit in MB for the isolate (default: 512). */\r\n\tmemoryLimitMB?: number;\r\n\t/** Timeout in ms for fight/simulate calls (default: 60000). */\r\n\ttimeoutMs?: number;\r\n\t/** Options passed to esbuild compilation (aliases, externals). */\r\n\tcompileOptions?: CompileOptions;\r\n}\r\n\r\n/**\r\n * A sandboxed match runner. Both bots + the entire simulation engine run\r\n * inside a single isolated-vm isolate.\r\n *\r\n * Usage:\r\n * ```ts\r\n * const sandbox = await MatchSandbox.create(botA, botB);\r\n * const result = sandbox.fight({ seed: 42 });\r\n * sandbox.dispose();\r\n * ```\r\n */\r\nexport class MatchSandbox\r\n{\r\n\tprivate isolate: ivm.Isolate;\r\n\tprivate context: ivm.Context;\r\n\tprivate fightFn: ivm.Reference;\r\n\tprivate simulateFn: ivm.Reference;\r\n\tprivate timeout: number;\r\n\tprivate disposed = false;\r\n\r\n\tprivate constructor(\r\n\t\tisolate: ivm.Isolate,\r\n\t\tcontext: ivm.Context,\r\n\t\tfightFn: ivm.Reference,\r\n\t\tsimulateFn: ivm.Reference,\r\n\t\ttimeout: number,\r\n\t)\r\n\t{\r\n\t\tthis.isolate = isolate;\r\n\t\tthis.context = context;\r\n\t\tthis.fightFn = fightFn;\r\n\t\tthis.simulateFn = simulateFn;\r\n\t\tthis.timeout = timeout;\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox with both bots loaded. Compiles the match bundle\r\n\t * automatically using esbuild.\r\n\t */\r\n\tstatic async create(\r\n\t\tbot1: BotBundle,\r\n\t\tbot2: BotBundle,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\tconst timeoutMs = options?.timeoutMs ?? 60000;\r\n\r\n\t\t// 1. Compile the match bundle\r\n\t\tconst bundle = await compileMatchBundle(bot1, bot2, options?.compileOptions);\r\n\r\n\t\t// 2. Create isolate with memory limit\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// 3. Create context and load the bundle\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\t// 4. Get references to the exposed functions\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\t// OOM or timeout can auto-dispose the isolate, so guard the cleanup dispose.\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox from a pre-compiled bundle string.\r\n\t * Useful for caching compiled bundles across multiple MatchSandbox instances.\r\n\t */\r\n\tstatic async fromBundle(\r\n\t\tbundle: string,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\tconst timeoutMs = options?.timeoutMs ?? 60000;\r\n\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Compile a match bundle without creating an isolate.\r\n\t * Returns the compiled JS string for caching/reuse.\r\n\t */\r\n\tstatic async compile(bot1: BotBundle, bot2: BotBundle, compileOptions?: CompileOptions): Promise<string>\r\n\t{\r\n\t\treturn compileMatchBundle(bot1, bot2, compileOptions);\r\n\t}\r\n\r\n\t/**\r\n\t * Run a full fight (10 matches: 5 spawn distances x 2 sides).\r\n\t * Synchronous after isolate creation — runs entirely inside the isolate.\r\n\t */\r\n\tfight(options?: {seed?: number; maxTicks?: number}): FightResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.fightFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as FightResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Run a single simulation.\r\n\t * Synchronous after isolate creation.\r\n\t *\r\n\t * @param options.params1 - useParam overrides for bot 1 (wizard-1)\r\n\t * @param options.params2 - useParam overrides for bot 2 (wizard-2)\r\n\t */\r\n\tsimulate(options?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t}): SimulateResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.simulateFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as SimulateResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Dispose the isolate and free all memory.\r\n\t * The sandbox cannot be used after disposal.\r\n\t */\r\n\tdispose(): void\r\n\t{\r\n\t\tif (!this.disposed)\r\n\t\t{\r\n\t\t\tthis.disposed = true;\r\n\t\t\t// OOM can auto-dispose the isolate, so guard all cleanup\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.fightFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.simulateFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.context.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.isolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Whether this sandbox has been disposed.\r\n\t */\r\n\tget isDisposed(): boolean\r\n\t{\r\n\t\treturn this.disposed;\r\n\t}\r\n\r\n\tprivate ensureNotDisposed(): void\r\n\t{\r\n\t\tif (this.disposed)\r\n\t\t{\r\n\t\t\tthrow new Error('MatchSandbox has been disposed');\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed fight. Creates isolate, runs fight, disposes.\r\n * Convenience wrapper for single-use scenarios.\r\n */\r\nexport async function sandboxFight(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {seed?: number; maxTicks?: number} & SandboxOptions,\r\n): Promise<FightResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.fight({seed: options?.seed, maxTicks: options?.maxTicks});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed simulate. Creates isolate, runs simulate, disposes.\r\n */\r\nexport async function sandboxSimulate(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t} & SandboxOptions,\r\n): Promise<SimulateResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.simulate({\r\n\t\t\tseed: options?.seed,\r\n\t\t\tmaxTicks: options?.maxTicks,\r\n\t\t\tspawnDistance: options?.spawnDistance,\r\n\t\t\tskipHistory: options?.skipHistory,\r\n\t\t\tparams1: options?.params1,\r\n\t\t\tparams2: options?.params2,\r\n\t\t});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n","/**\r\n * VIBEMANCER — SANDBOX COMPILATION\r\n *\r\n * Compiles match bundles using esbuild. Extracted from sandbox.ts so that\r\n * compilation can be used independently of isolated-vm (e.g., in the CLI\r\n * dev server or for browser Web Worker sandboxes).\r\n *\r\n * This file has NO isolated-vm dependency — only esbuild + Node.js builtins.\r\n */\r\n\r\nimport {build} from 'esbuild';\r\nimport path from 'node:path';\r\nimport fs from 'node:fs';\r\nimport {fileURLToPath} from 'node:url';\r\nimport {PROTOTYPE_FREEZE_BANNER, generateManualMatchEntryPoint, generateMatchEntryPoint} from './sandbox-harness.js';\r\n\r\nconst currentFilename = fileURLToPath(import.meta.url);\r\nconst currentDirname = path.dirname(currentFilename);\r\n\r\n/**\r\n * Find the src/engine/ directory. Works from src/, dist/engine/, or dist/ (tsup bundle).\r\n * esbuild needs TypeScript source files, so we look for the src/ tree.\r\n */\r\nexport function getEngineDir(): string\r\n{\r\n\t// When running from src/engine/ (dev/test), currentDirname is already src/engine/\r\n\tconst directPath = path.resolve(currentDirname);\r\n\tif (fs.existsSync(path.join(directPath, 'simulation.ts')))\r\n\t{\r\n\t\treturn directPath;\r\n\t}\r\n\r\n\t// When running from dist/engine/ (individual files), package root is 2 levels up\r\n\tconst packageRoot2 = path.resolve(currentDirname, '..', '..');\r\n\tconst srcEngine2 = path.join(packageRoot2, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine2, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine2;\r\n\t}\r\n\r\n\t// When running from dist/ (tsup bundle), package root is 1 level up\r\n\tconst packageRoot1 = path.resolve(currentDirname, '..');\r\n\tconst srcEngine1 = path.join(packageRoot1, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine1, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine1;\r\n\t}\r\n\r\n\tthrow new Error('Could not find engine source directory (src/engine/simulation.ts)');\r\n}\r\n\r\n/**\r\n * A compiled bot ready for sandboxed execution.\r\n * Stores the source path and export name — actual compilation\r\n * happens when creating a MatchSandbox or calling compileMatchBundle.\r\n */\r\nexport class BotBundle\r\n{\r\n\treadonly sourcePath: string;\r\n\treadonly exportName: string;\r\n\r\n\tconstructor(sourcePath: string, exportName: string)\r\n\t{\r\n\t\tif (!sourcePath || typeof sourcePath !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('sourcePath must be a non-empty string');\r\n\t\t}\r\n\t\tif (!exportName || typeof exportName !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('exportName must be a non-empty string');\r\n\t\t}\r\n\t\tthis.sourcePath = path.resolve(sourcePath);\r\n\t\tthis.exportName = exportName;\r\n\t}\r\n}\r\n\r\n/**\r\n * Options for esbuild compilation. Allows the caller to add esbuild\r\n * aliases (e.g., resolving @vibemancer/core to the TypeScript source).\r\n */\r\nexport interface CompileOptions\r\n{\r\n\t/** Additional esbuild alias entries (e.g., {'@vibemancer/core': '/path/to/src/index.ts'}). */\r\n\talias?: Record<string, string>;\r\n\t/** Additional modules to treat as external (not bundled). */\r\n\texternal?: string[];\r\n}\r\n\r\n/**\r\n * Compile a match bundle using esbuild. Bundles both bots + simulation engine\r\n * into a single self-contained IIFE with prototype freezing banner.\r\n *\r\n * No isolated-vm dependency — returns a plain JS string.\r\n */\r\nexport async function compileMatchBundle(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateMatchEntryPoint(\r\n\t\tbot1.sourcePath,\r\n\t\tbot1.exportName,\r\n\t\tbot2.sourcePath,\r\n\t\tbot2.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\t// Suppress warnings about top-level this in ESM\r\n\t\tlogLevel: 'error',\r\n\t\t// Match bundles run in sandboxed environments (Web Workers / isolated-vm)\r\n\t\t// and should never include Node.js native modules\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n\r\n/**\r\n * Compile a manual-play sandbox bundle. Bundles ONE opponent bot + ManualMatch\r\n * + simulation engine into a self-contained IIFE. The \"player\" wizard is a\r\n * worker-local stub that reads from `__latestHumanActions` (set per-step by\r\n * the host).\r\n *\r\n * Returns a plain JS string that, when loaded into a Web Worker, exposes the\r\n * `__manualMatchInit`, `__manualMatchStep`, `__manualMatchGuide`,\r\n * `__manualMatchRelease`, `__manualMatchSetInvincible`,\r\n * `__manualMatchGetState`, `__manualMatchGetResult`, and `__manualMatchDispose`\r\n * globals on the worker's globalThis.\r\n */\r\nexport async function compileManualMatchBundle(\r\n\topponent: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateManualMatchEntryPoint(\r\n\t\topponent.sourcePath,\r\n\t\topponent.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\tlogLevel: 'error',\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n","/**\n * VIBEMANCER — SANDBOX HARNESS\n *\n * Generates the entry point code that runs inside an isolated-vm isolate.\n * The harness bundles both bots + the simulation engine into a single IIFE\n * via esbuild, then exposes __fight and __simulate on globalThis.\n *\n * The prototype freeze banner runs before any module code, preventing\n * prototype pollution attacks between bots sharing the same isolate.\n */\n\n/**\n * JavaScript code injected as esbuild banner — runs before the IIFE bundle.\n *\n * 1. Freezes all built-in prototypes to prevent cross-bot sabotage via prototype\n * pollution. This does NOT prevent calling existing methods (e.g. Array.push\n * still works), it only prevents reassigning them.\n *\n * 2. Blocks all IO/network capabilities. Web Workers have fetch, XMLHttpRequest,\n * WebSocket, importScripts, etc. Bot code must not be able to make network\n * calls or load external scripts. Uses Object.defineProperty to make the block\n * irrecoverable (non-writable, non-configurable). In isolated-vm, these globals\n * don't exist — the try-catch makes the deletes a harmless no-op.\n */\nexport const PROTOTYPE_FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\n/**\n * Generate the TypeScript entry point for a match sandbox.\n *\n * This entry point imports both bots and the simulation engine, then\n * exposes __fight and __simulate on globalThis. esbuild bundles this\n * + all transitive imports into a single self-contained IIFE.\n *\n * @param bot1SourcePath - Absolute path to bot 1's TypeScript source file\n * @param bot1ExportName - Named export of bot 1's WizardFunction\n * @param bot2SourcePath - Absolute path to bot 2's TypeScript source file\n * @param bot2ExportName - Named export of bot 2's WizardFunction\n * @param engineDir - Absolute path to the engine directory (src/engine/)\n */\nexport function generateMatchEntryPoint(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\t// Use forward slashes for esbuild compatibility (works on all platforms)\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\n\t// All imports use the engineDir's parent (= packages/core/src/) as root.\n\t// When user bots alias @vibemancer/core → src/index-browser.ts, esbuild\n\t// deduplicates these with the bot's imports since they resolve to the same files.\n\t// This ensures the hooks runtime global state is shared between harness and bot.\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\n\nglobalThis.__fight = function __fight(options) {\n\tconst result = fight(__Bot1, __Bot2, options);\n\treturn result;\n};\n\nglobalThis.__simulate = function __simulate(options) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\tconst result = simulate(bot1, bot2, simOptions);\n\treturn result;\n};\n`;\n}\n\n/**\n * Generate the entry point for a Manual Play sandbox bundle.\n *\n * Unlike the fight/simulate entry point (which exposes one-shot batch APIs),\n * the manual-match entry point holds a single long-lived ManualMatch instance\n * inside the worker and exposes per-tick step/rewind/guide APIs.\n *\n * The \"player\" wizard is a worker-local stub that returns whatever\n * `__latestHumanActions` is set to — this avoids the can't-postMessage-functions\n * problem (the function lives entirely worker-side, only data crosses the\n * boundary). Guided missiles use a similar pattern via\n * `__latestHumanMissileTargets[projectileId]`.\n *\n * @param opponentSourcePath - Absolute path to opponent bot's TypeScript source\n * @param opponentExportName - Named export of the opponent's WizardFunction\n * @param engineDir - Absolute path to src/engine/\n */\nexport function generateManualMatchEntryPoint(\n\topponentSourcePath: string,\n\topponentExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst opponentPath = opponentSourcePath.replace(/\\\\/g, '/');\n\treturn generateManualMatchEntryPointInner(\n\t\t`import {${opponentExportName} as __RawOpponent} from '${opponentPath}';`,\n\t\tengineDir,\n\t);\n}\n\n/**\n * Browser-friendly variant — the opponent is injected at runtime via\n * globalThis.__injectedBot1 (set by prepending the player's bot bundle to\n * the compiled output of this template). Used by the web client for manual\n * play against uploaded wizards.\n */\nexport function generateBrowserManualMatchEntryPoint(engineDir: string): string\n{\n\tconst opponentImport = 'var __RawOpponent = globalThis.__injectedBot1;\\n'\n\t\t+ 'if (!__RawOpponent) throw new Error(\"Opponent not injected (set globalThis.__injectedBot1)\");';\n\treturn generateManualMatchEntryPointInner(opponentImport, engineDir);\n}\n\nfunction generateManualMatchEntryPointInner(opponentImport: string, engineDir: string): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\n${opponentImport}\nimport {ManualMatch} from '${srcPath}/engine/manual-match.ts';\nimport {idle, turnToward, flyStraight} from '${srcPath}/hooks/action-builders.ts';\nimport {getMissileContext} from '${srcPath}/engine/hooks-runtime.ts';\n\n// Worker-local state — the player AI and guided-missile AIs read from these.\nvar __latestHumanActions = null;\nvar __latestHumanMissileTargets = {};\nvar __manualMatch = null;\n\nfunction __playerStub() {\n\tif (__latestHumanActions) {\n\t\treturn {_toAction: function() { return __latestHumanActions; }};\n\t}\n\treturn idle();\n}\n\nfunction __makeGuideStub(projectileId) {\n\treturn function() {\n\t\tvar target = __latestHumanMissileTargets[projectileId];\n\t\tif (!target) return flyStraight();\n\t\treturn turnToward(target.x, target.y);\n\t};\n}\n\nfunction __ensureMatch() {\n\tif (!__manualMatch) throw new Error('ManualMatch not initialized — call manualMatchInit first');\n\treturn __manualMatch;\n}\n\nglobalThis.__manualMatchInit = function(options) {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = (options && options.initialHumanActions) || null;\n\t__latestHumanMissileTargets = {};\n\t__manualMatch = new ManualMatch(__playerStub, __RawOpponent, options || {});\n\treturn __manualMatch.getGameState();\n};\n\nglobalThis.__manualMatchStep = function(options) {\n\tvar match = __ensureMatch();\n\tif (options) {\n\t\tif (options.humanActions) __latestHumanActions = options.humanActions;\n\t\tif (options.humanMissileTargets) {\n\t\t\t// Merge — the host may only update specific projectiles per call\n\t\t\tfor (var k in options.humanMissileTargets) {\n\t\t\t\t__latestHumanMissileTargets[k] = options.humanMissileTargets[k];\n\t\t\t}\n\t\t}\n\t}\n\tvar count = (options && options.count) || 1;\n\treturn match.step(count);\n};\n\nglobalThis.__manualMatchGuide = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.replaceMissileAI(id, __makeGuideStub(id));\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchRelease = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.restoreMissileAI(id);\n\tdelete __latestHumanMissileTargets[id];\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchSetInvincible = function(options) {\n\tvar match = __ensureMatch();\n\tvar idx = (options && options.wizardIndex) || 0;\n\tvar on = !!(options && options.on);\n\tmatch.setInvincible(idx, on);\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchGetState = function() {\n\tvar match = __ensureMatch();\n\treturn match.getGameState();\n};\n\nglobalThis.__manualMatchGetResult = function() {\n\tvar match = __ensureMatch();\n\treturn match.getResult();\n};\n\nglobalThis.__manualMatchDispose = function() {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = {move: {x: 0, y: 0}};\n\t__latestHumanMissileTargets = {};\n\treturn {ok: true};\n};\n`;\n}\n\n/**\n * Generate an alternate entry point where __fight/__simulate accept and return\n * JSON strings instead of structured objects. Used by the benchmark to compare\n * JSON serialization vs V8 structured clone performance.\n */\nexport function generateMatchEntryPointJSON(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\n\nglobalThis.__fightJSON = function __fightJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\tconst result = fight(__Bot1, __Bot2, options);\n\treturn JSON.stringify(result);\n};\n\nglobalThis.__simulateJSON = function __simulateJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\tconst result = simulate(bot1, bot2, simOptions);\n\treturn JSON.stringify(result);\n};\n`;\n}\n","/**\n * Run a fight between two PRECOMPILED bot bundles — the canonical uploaded-wizard\n * format where each IIFE sets `globalThis.__injectedBot1` (see compileSingleBotBundle).\n *\n * Shared by the Cloud Functions matchmaker and the devkit CLI so that a\n * `handle/botname` fight runs through the exact same engine as the live ladder.\n *\n * Both bundles run inside an isolated-vm isolate alongside a \"match template\" —\n * the engine + a `__fight` harness that reads the injected bots. The template is\n * built lazily from the engine source on first use and cached; callers that\n * already have one (the Cloud Functions committed MATCH_TEMPLATE) pass it in to\n * skip the esbuild step.\n *\n * Execution order inside the isolate: bot1 bundle → bot2 bundle → match template.\n */\n\nimport path from 'node:path';\nimport ivm from 'isolated-vm';\nimport {build} from 'esbuild';\nimport {getEngineDir} from './sandbox-compile.js';\nimport type {FightResult, SimulateResult} from './simulation.js';\n\n/** Memory limit per sandbox isolate (MB). */\nconst MEMORY_LIMIT_MB = 256;\n/** Timeout per fight (ms). */\nconst FIGHT_TIMEOUT_MS = 60_000;\n\nconst FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\nlet cachedTemplate: Promise<string> | null = null;\n\n/**\n * Build (and cache) the match template: the engine + `__fight`/`__simulate`\n * harness bundled into a single IIFE string, ready to run after two bot bundles\n * have set `globalThis.__injectedBot1`/`__injectedBot2`.\n */\nexport function buildMatchTemplate(): Promise<string>\n{\n\tif (!cachedTemplate)\n\t{\n\t\tcachedTemplate = (async(): Promise<string> =>\n\t\t{\n\t\t\tconst engineDir = getEngineDir();\n\t\t\tconst coreSrc = path.dirname(engineDir).replace(/\\\\/g, '/');\n\n\t\t\tconst entryPoint = `\nimport {fight, simulate} from '${coreSrc}/engine/simulation.ts';\nimport {wrapWithParams} from '${coreSrc}/engine/params-runtime.ts';\n\nconst __Bot1 = (globalThis as any).__injectedBot1;\nconst __Bot2 = (globalThis as any).__injectedBot2;\n\nif (!__Bot1) throw new Error('Bot 1 not injected (set globalThis.__injectedBot1)');\nif (!__Bot2) throw new Error('Bot 2 not injected (set globalThis.__injectedBot2)');\n\n(globalThis as any).__fight = function __fight(options: any) {\n\treturn fight(__Bot1, __Bot2, options);\n};\n\n(globalThis as any).__simulate = function __simulate(options: any) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\treturn simulate(bot1, bot2, simOptions);\n};\n`;\n\n\t\t\tconst result = await build({\n\t\t\t\tstdin: {contents: entryPoint, resolveDir: engineDir, loader: 'ts'},\n\t\t\t\tbundle: true,\n\t\t\t\twrite: false,\n\t\t\t\tformat: 'iife',\n\t\t\t\tplatform: 'neutral',\n\t\t\t\ttarget: 'es2022',\n\t\t\t\tbanner: {js: FREEZE_BANNER},\n\t\t\t\tlogLevel: 'error',\n\t\t\t\texternal: [\n\t\t\t\t\t'isolated-vm', 'esbuild',\n\t\t\t\t\t'node:path', 'node:fs', 'node:url',\n\t\t\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\n\t\t\t\t],\n\t\t\t});\n\n\t\t\tif (!result.outputFiles?.[0]) throw new Error('esbuild produced no match-template output');\n\t\t\treturn result.outputFiles[0].text;\n\t\t})();\n\t}\n\treturn cachedTemplate;\n}\n\nexport interface RunBundleFightOptions\n{\n\tseed: number;\n\t/** Prebuilt match template (e.g. the committed Cloud Functions one). Built lazily if omitted. */\n\tmatchTemplate?: string;\n\t/** Skip per-tick history in the result (smaller copy out of the isolate). Default true. */\n\tskipHistory?: boolean;\n}\n\n/**\n * Run a fight between two compiled `__injectedBot1`-format bundles.\n *\n * bundle1 is saved and its global cleared before bundle2 runs, so bot code can\n * never read the opponent's function off globalThis. Both globals are deleted\n * after wiring so runtime bot code can't reach them either.\n */\nexport async function runBundleFight(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleFightOptions,\n): Promise<FightResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\tconst skipHistory = options.skipHistory ?? true;\n\n\t// Freeze prototypes/globals BEFORE the untrusted bot bundles run. The bundles\n\t// execute at module-eval (ahead of the template, whose banner used to be the\n\t// only freeze) — so without this a bot could pollute Object/Array/Math/etc. at\n\t// load time and corrupt the engine or its opponent. Mirrors the source path\n\t// (sandbox-harness), which already freezes before importing the bots.\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: FIGHT_TIMEOUT_MS});\n\n\t\tconst fightFn = await jail.get('__fight');\n\t\tconst result = await fightFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy({seed: options.seed, skipHistory}).copyInto()],\n\t\t\t{timeout: FIGHT_TIMEOUT_MS, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as FightResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n\nexport interface RunBundleSimulateOptions\n{\n\tseed?: number;\n\tspawnDistance?: number;\n\tmaxTicks?: number;\n\tmatchTemplate?: string;\n}\n\n/**\n * Run a SINGLE match between two compiled bundles, returning the full per-tick\n * history (for tracing/debugging). Same isolate wiring as runBundleFight, but\n * calls the template's `__simulate` so the caller gets a SimulateResult.\n */\nexport async function runBundleSimulate(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleSimulateOptions = {},\n): Promise<SimulateResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\n\t// Freeze before the untrusted bundles run (see runBundleFight).\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: FIGHT_TIMEOUT_MS});\n\n\t\tconst simulateFn = await jail.get('__simulate');\n\t\tconst simOptions = {\n\t\t\tseed: options.seed ?? 1,\n\t\t\tspawnDistance: options.spawnDistance,\n\t\t\tmaxTicks: options.maxTicks,\n\t\t};\n\t\tconst result = await simulateFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy(simOptions).copyInto()],\n\t\t\t{timeout: FIGHT_TIMEOUT_MS, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as SimulateResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n"],"mappings":"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","names":["path","ivm","build","path","build","ivm"]}
|
|
1
|
+
{"version":3,"sources":["../src/engine/sandbox.ts","../src/engine/sandbox-compile.ts","../src/engine/sandbox-harness.ts","../src/engine/bundle-fight.ts"],"sourcesContent":["/**\r\n * VIBEMANCER — SANDBOX\r\n *\r\n * Provides isolated-vm sandboxing for bot code execution. Both bots + the\r\n * entire simulation engine run inside a single V8 isolate, so there is ZERO\r\n * per-tick boundary crossing overhead. The only data crossing the boundary\r\n * is fight/simulate options going in and results coming out.\r\n *\r\n * Architecture:\r\n * - Host: creates isolate, loads compiled bundle, calls __fight/__simulate\r\n * - Isolate: contains both bots + full simulation engine, runs fight/simulate\r\n *\r\n * Safety:\r\n * - Memory limit (default 512 MB) catches memory bombs\r\n * - Timeout (default 30s) catches infinite loops\r\n * - Prototype freeze prevents cross-bot sabotage\r\n * - platform: 'neutral' strips Node.js APIs (no fs/net/process)\r\n */\r\n\r\nimport ivm from 'isolated-vm';\r\nimport type {FightResult, SimulateResult} from './simulation.js';\r\nimport {BotBundle, compileMatchBundle} from './sandbox-compile.js';\r\nimport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n// Re-export BotBundle so existing imports from sandbox.ts keep working\r\nexport {BotBundle} from './sandbox-compile.js';\r\nexport {compileMatchBundle, compileManualMatchBundle} from './sandbox-compile.js';\r\nexport type {CompileOptions} from './sandbox-compile.js';\r\n\r\n/**\r\n * Options for sandbox creation.\r\n */\r\nexport interface SandboxOptions\r\n{\r\n\t/** Memory limit in MB for the isolate (default: 512). */\r\n\tmemoryLimitMB?: number;\r\n\t/** Timeout in ms for fight/simulate calls (default: 60000). */\r\n\ttimeoutMs?: number;\r\n\t/** Options passed to esbuild compilation (aliases, externals). */\r\n\tcompileOptions?: CompileOptions;\r\n}\r\n\r\n/**\r\n * A sandboxed match runner. Both bots + the entire simulation engine run\r\n * inside a single isolated-vm isolate.\r\n *\r\n * Usage:\r\n * ```ts\r\n * const sandbox = await MatchSandbox.create(botA, botB);\r\n * const result = sandbox.fight({ seed: 42 });\r\n * sandbox.dispose();\r\n * ```\r\n */\r\nexport class MatchSandbox\r\n{\r\n\tprivate isolate: ivm.Isolate;\r\n\tprivate context: ivm.Context;\r\n\tprivate fightFn: ivm.Reference;\r\n\tprivate simulateFn: ivm.Reference;\r\n\tprivate timeout: number;\r\n\tprivate disposed = false;\r\n\r\n\tprivate constructor(\r\n\t\tisolate: ivm.Isolate,\r\n\t\tcontext: ivm.Context,\r\n\t\tfightFn: ivm.Reference,\r\n\t\tsimulateFn: ivm.Reference,\r\n\t\ttimeout: number,\r\n\t)\r\n\t{\r\n\t\tthis.isolate = isolate;\r\n\t\tthis.context = context;\r\n\t\tthis.fightFn = fightFn;\r\n\t\tthis.simulateFn = simulateFn;\r\n\t\tthis.timeout = timeout;\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox with both bots loaded. Compiles the match bundle\r\n\t * automatically using esbuild.\r\n\t */\r\n\tstatic async create(\r\n\t\tbot1: BotBundle,\r\n\t\tbot2: BotBundle,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\tconst timeoutMs = options?.timeoutMs ?? 60000;\r\n\r\n\t\t// 1. Compile the match bundle\r\n\t\tconst bundle = await compileMatchBundle(bot1, bot2, options?.compileOptions);\r\n\r\n\t\t// 2. Create isolate with memory limit\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// 3. Create context and load the bundle\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\t// 4. Get references to the exposed functions\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\t// OOM or timeout can auto-dispose the isolate, so guard the cleanup dispose.\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Create a sandbox from a pre-compiled bundle string.\r\n\t * Useful for caching compiled bundles across multiple MatchSandbox instances.\r\n\t */\r\n\tstatic async fromBundle(\r\n\t\tbundle: string,\r\n\t\toptions?: SandboxOptions,\r\n\t): Promise<MatchSandbox>\r\n\t{\r\n\t\tconst memoryLimitMB = options?.memoryLimitMB ?? 512;\r\n\t\tconst timeoutMs = options?.timeoutMs ?? 60000;\r\n\r\n\t\tconst isolate = new ivm.Isolate({memoryLimit: memoryLimitMB});\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tconst context = await isolate.createContext();\r\n\t\t\tconst script = await isolate.compileScript(bundle, {filename: 'match-bundle.js'});\r\n\t\t\tawait script.run(context, {timeout: timeoutMs});\r\n\r\n\t\t\tconst global = context.global;\r\n\t\t\tconst fightFn = await global.get('__fight', {reference: true});\r\n\t\t\tconst simulateFn = await global.get('__simulate', {reference: true});\r\n\r\n\t\t\treturn new MatchSandbox(isolate, context, fightFn, simulateFn, timeoutMs);\r\n\t\t}\r\n\t\tcatch(error)\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tisolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// already disposed\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Compile a match bundle without creating an isolate.\r\n\t * Returns the compiled JS string for caching/reuse.\r\n\t */\r\n\tstatic async compile(bot1: BotBundle, bot2: BotBundle, compileOptions?: CompileOptions): Promise<string>\r\n\t{\r\n\t\treturn compileMatchBundle(bot1, bot2, compileOptions);\r\n\t}\r\n\r\n\t/**\r\n\t * Run a full fight (10 matches: 5 spawn distances x 2 sides).\r\n\t * Synchronous after isolate creation — runs entirely inside the isolate.\r\n\t */\r\n\tfight(options?: {seed?: number; maxTicks?: number}): FightResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.fightFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as FightResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Run a single simulation.\r\n\t * Synchronous after isolate creation.\r\n\t *\r\n\t * @param options.params1 - useParam overrides for bot 1 (wizard-1)\r\n\t * @param options.params2 - useParam overrides for bot 2 (wizard-2)\r\n\t */\r\n\tsimulate(options?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t}): SimulateResult\r\n\t{\r\n\t\tthis.ensureNotDisposed();\r\n\r\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\r\n\t\treturn this.simulateFn.applySync(undefined, [options ?? {}], {\r\n\t\t\targuments: {copy: true},\r\n\t\t\tresult: {copy: true},\r\n\t\t\ttimeout: this.timeout,\r\n\t\t}) as SimulateResult;\r\n\t}\r\n\r\n\t/**\r\n\t * Dispose the isolate and free all memory.\r\n\t * The sandbox cannot be used after disposal.\r\n\t */\r\n\tdispose(): void\r\n\t{\r\n\t\tif (!this.disposed)\r\n\t\t{\r\n\t\t\tthis.disposed = true;\r\n\t\t\t// OOM can auto-dispose the isolate, so guard all cleanup\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.fightFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.simulateFn.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.context.release();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tthis.isolate.dispose();\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t// isolate already disposed\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Whether this sandbox has been disposed.\r\n\t */\r\n\tget isDisposed(): boolean\r\n\t{\r\n\t\treturn this.disposed;\r\n\t}\r\n\r\n\tprivate ensureNotDisposed(): void\r\n\t{\r\n\t\tif (this.disposed)\r\n\t\t{\r\n\t\t\tthrow new Error('MatchSandbox has been disposed');\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed fight. Creates isolate, runs fight, disposes.\r\n * Convenience wrapper for single-use scenarios.\r\n */\r\nexport async function sandboxFight(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {seed?: number; maxTicks?: number} & SandboxOptions,\r\n): Promise<FightResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.fight({seed: options?.seed, maxTicks: options?.maxTicks});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n\r\n/**\r\n * One-shot sandboxed simulate. Creates isolate, runs simulate, disposes.\r\n */\r\nexport async function sandboxSimulate(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: {\r\n\t\tseed?: number;\r\n\t\tmaxTicks?: number;\r\n\t\tspawnDistance?: number;\r\n\t\tskipHistory?: boolean;\r\n\t\tparams1?: Record<string, number>;\r\n\t\tparams2?: Record<string, number>;\r\n\t} & SandboxOptions,\r\n): Promise<SimulateResult>\r\n{\r\n\tconst sandbox = await MatchSandbox.create(bot1, bot2, options);\r\n\ttry\r\n\t{\r\n\t\treturn sandbox.simulate({\r\n\t\t\tseed: options?.seed,\r\n\t\t\tmaxTicks: options?.maxTicks,\r\n\t\t\tspawnDistance: options?.spawnDistance,\r\n\t\t\tskipHistory: options?.skipHistory,\r\n\t\t\tparams1: options?.params1,\r\n\t\t\tparams2: options?.params2,\r\n\t\t});\r\n\t}\r\n\tfinally\r\n\t{\r\n\t\tsandbox.dispose();\r\n\t}\r\n}\r\n","/**\r\n * VIBEMANCER — SANDBOX COMPILATION\r\n *\r\n * Compiles match bundles using esbuild. Extracted from sandbox.ts so that\r\n * compilation can be used independently of isolated-vm (e.g., in the CLI\r\n * dev server or for browser Web Worker sandboxes).\r\n *\r\n * This file has NO isolated-vm dependency — only esbuild + Node.js builtins.\r\n */\r\n\r\nimport {build} from 'esbuild';\r\nimport path from 'node:path';\r\nimport fs from 'node:fs';\r\nimport {fileURLToPath} from 'node:url';\r\nimport {PROTOTYPE_FREEZE_BANNER, generateManualMatchEntryPoint, generateMatchEntryPoint} from './sandbox-harness.js';\r\n\r\nconst currentFilename = fileURLToPath(import.meta.url);\r\nconst currentDirname = path.dirname(currentFilename);\r\n\r\n/**\r\n * Find the src/engine/ directory. Works from src/, dist/engine/, or dist/ (tsup bundle).\r\n * esbuild needs TypeScript source files, so we look for the src/ tree.\r\n */\r\nexport function getEngineDir(): string\r\n{\r\n\t// When running from src/engine/ (dev/test), currentDirname is already src/engine/\r\n\tconst directPath = path.resolve(currentDirname);\r\n\tif (fs.existsSync(path.join(directPath, 'simulation.ts')))\r\n\t{\r\n\t\treturn directPath;\r\n\t}\r\n\r\n\t// When running from dist/engine/ (individual files), package root is 2 levels up\r\n\tconst packageRoot2 = path.resolve(currentDirname, '..', '..');\r\n\tconst srcEngine2 = path.join(packageRoot2, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine2, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine2;\r\n\t}\r\n\r\n\t// When running from dist/ (tsup bundle), package root is 1 level up\r\n\tconst packageRoot1 = path.resolve(currentDirname, '..');\r\n\tconst srcEngine1 = path.join(packageRoot1, 'src', 'engine');\r\n\tif (fs.existsSync(path.join(srcEngine1, 'simulation.ts')))\r\n\t{\r\n\t\treturn srcEngine1;\r\n\t}\r\n\r\n\tthrow new Error('Could not find engine source directory (src/engine/simulation.ts)');\r\n}\r\n\r\n/**\r\n * A compiled bot ready for sandboxed execution.\r\n * Stores the source path and export name — actual compilation\r\n * happens when creating a MatchSandbox or calling compileMatchBundle.\r\n */\r\nexport class BotBundle\r\n{\r\n\treadonly sourcePath: string;\r\n\treadonly exportName: string;\r\n\r\n\tconstructor(sourcePath: string, exportName: string)\r\n\t{\r\n\t\tif (!sourcePath || typeof sourcePath !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('sourcePath must be a non-empty string');\r\n\t\t}\r\n\t\tif (!exportName || typeof exportName !== 'string')\r\n\t\t{\r\n\t\t\tthrow new Error('exportName must be a non-empty string');\r\n\t\t}\r\n\t\tthis.sourcePath = path.resolve(sourcePath);\r\n\t\tthis.exportName = exportName;\r\n\t}\r\n}\r\n\r\n/**\r\n * Options for esbuild compilation. Allows the caller to add esbuild\r\n * aliases (e.g., resolving @vibemancer/core to the TypeScript source).\r\n */\r\nexport interface CompileOptions\r\n{\r\n\t/** Additional esbuild alias entries (e.g., {'@vibemancer/core': '/path/to/src/index.ts'}). */\r\n\talias?: Record<string, string>;\r\n\t/** Additional modules to treat as external (not bundled). */\r\n\texternal?: string[];\r\n}\r\n\r\n/**\r\n * Compile a match bundle using esbuild. Bundles both bots + simulation engine\r\n * into a single self-contained IIFE with prototype freezing banner.\r\n *\r\n * No isolated-vm dependency — returns a plain JS string.\r\n */\r\nexport async function compileMatchBundle(\r\n\tbot1: BotBundle,\r\n\tbot2: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateMatchEntryPoint(\r\n\t\tbot1.sourcePath,\r\n\t\tbot1.exportName,\r\n\t\tbot2.sourcePath,\r\n\t\tbot2.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\t// Suppress warnings about top-level this in ESM\r\n\t\tlogLevel: 'error',\r\n\t\t// Match bundles run in sandboxed environments (Web Workers / isolated-vm)\r\n\t\t// and should never include Node.js native modules\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n\r\n/**\r\n * Compile a manual-play sandbox bundle. Bundles ONE opponent bot + ManualMatch\r\n * + simulation engine into a self-contained IIFE. The \"player\" wizard is a\r\n * worker-local stub that reads from `__latestHumanActions` (set per-step by\r\n * the host).\r\n *\r\n * Returns a plain JS string that, when loaded into a Web Worker, exposes the\r\n * `__manualMatchInit`, `__manualMatchStep`, `__manualMatchGuide`,\r\n * `__manualMatchRelease`, `__manualMatchSetInvincible`,\r\n * `__manualMatchGetState`, `__manualMatchGetResult`, and `__manualMatchDispose`\r\n * globals on the worker's globalThis.\r\n */\r\nexport async function compileManualMatchBundle(\r\n\topponent: BotBundle,\r\n\toptions?: CompileOptions,\r\n): Promise<string>\r\n{\r\n\tconst engineDir = getEngineDir();\r\n\tconst entryPoint = generateManualMatchEntryPoint(\r\n\t\topponent.sourcePath,\r\n\t\topponent.exportName,\r\n\t\tengineDir,\r\n\t);\r\n\r\n\tconst result = await build({\r\n\t\tstdin: {\r\n\t\t\tcontents: entryPoint,\r\n\t\t\tresolveDir: engineDir,\r\n\t\t\tloader: 'ts',\r\n\t\t},\r\n\t\tbundle: true,\r\n\t\twrite: false,\r\n\t\tformat: 'iife',\r\n\t\tplatform: 'neutral',\r\n\t\ttarget: 'es2022',\r\n\t\tbanner: {\r\n\t\t\tjs: PROTOTYPE_FREEZE_BANNER,\r\n\t\t},\r\n\t\tlogLevel: 'error',\r\n\t\texternal: [\r\n\t\t\t'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',\r\n\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\r\n\t\t\t...(options?.external ?? []),\r\n\t\t],\r\n\t\t...(options?.alias ? {alias: options.alias} : {}),\r\n\t});\r\n\r\n\tif (!result.outputFiles?.[0])\r\n\t{\r\n\t\tthrow new Error('esbuild produced no output');\r\n\t}\r\n\r\n\treturn result.outputFiles[0].text;\r\n}\r\n","/**\n * VIBEMANCER — SANDBOX HARNESS\n *\n * Generates the entry point code that runs inside an isolated-vm isolate.\n * The harness bundles both bots + the simulation engine into a single IIFE\n * via esbuild, then exposes __fight and __simulate on globalThis.\n *\n * The prototype freeze banner runs before any module code, preventing\n * prototype pollution attacks between bots sharing the same isolate.\n */\n\n/**\n * JavaScript code injected as esbuild banner — runs before the IIFE bundle.\n *\n * 1. Freezes all built-in prototypes to prevent cross-bot sabotage via prototype\n * pollution. This does NOT prevent calling existing methods (e.g. Array.push\n * still works), it only prevents reassigning them.\n *\n * 2. Blocks all IO/network capabilities. Web Workers have fetch, XMLHttpRequest,\n * WebSocket, importScripts, etc. Bot code must not be able to make network\n * calls or load external scripts. Uses Object.defineProperty to make the block\n * irrecoverable (non-writable, non-configurable). In isolated-vm, these globals\n * don't exist — the try-catch makes the deletes a harmless no-op.\n */\nexport const PROTOTYPE_FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\n/**\n * Generate the TypeScript entry point for a match sandbox.\n *\n * This entry point imports both bots and the simulation engine, then\n * exposes __fight and __simulate on globalThis. esbuild bundles this\n * + all transitive imports into a single self-contained IIFE.\n *\n * @param bot1SourcePath - Absolute path to bot 1's TypeScript source file\n * @param bot1ExportName - Named export of bot 1's WizardFunction\n * @param bot2SourcePath - Absolute path to bot 2's TypeScript source file\n * @param bot2ExportName - Named export of bot 2's WizardFunction\n * @param engineDir - Absolute path to the engine directory (src/engine/)\n */\nexport function generateMatchEntryPoint(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\t// Use forward slashes for esbuild compatibility (works on all platforms)\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\n\t// All imports use the engineDir's parent (= packages/core/src/) as root.\n\t// When user bots alias @vibemancer/core → src/index-browser.ts, esbuild\n\t// deduplicates these with the bot's imports since they resolve to the same files.\n\t// This ensures the hooks runtime global state is shared between harness and bot.\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\n\nglobalThis.__fight = function __fight(options) {\n\tconst result = fight(__Bot1, __Bot2, options);\n\treturn result;\n};\n\nglobalThis.__simulate = function __simulate(options) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\tconst result = simulate(bot1, bot2, simOptions);\n\treturn result;\n};\n`;\n}\n\n/**\n * Generate the entry point for a Manual Play sandbox bundle.\n *\n * Unlike the fight/simulate entry point (which exposes one-shot batch APIs),\n * the manual-match entry point holds a single long-lived ManualMatch instance\n * inside the worker and exposes per-tick step/rewind/guide APIs.\n *\n * The \"player\" wizard is a worker-local stub that returns whatever\n * `__latestHumanActions` is set to — this avoids the can't-postMessage-functions\n * problem (the function lives entirely worker-side, only data crosses the\n * boundary). Guided missiles use a similar pattern via\n * `__latestHumanMissileTargets[projectileId]`.\n *\n * @param opponentSourcePath - Absolute path to opponent bot's TypeScript source\n * @param opponentExportName - Named export of the opponent's WizardFunction\n * @param engineDir - Absolute path to src/engine/\n */\nexport function generateManualMatchEntryPoint(\n\topponentSourcePath: string,\n\topponentExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst opponentPath = opponentSourcePath.replace(/\\\\/g, '/');\n\treturn generateManualMatchEntryPointInner(\n\t\t`import {${opponentExportName} as __RawOpponent} from '${opponentPath}';`,\n\t\tengineDir,\n\t);\n}\n\n/**\n * Browser-friendly variant — the opponent is injected at runtime via\n * globalThis.__injectedBot1 (set by prepending the player's bot bundle to\n * the compiled output of this template). Used by the web client for manual\n * play against uploaded wizards.\n */\nexport function generateBrowserManualMatchEntryPoint(engineDir: string): string\n{\n\tconst opponentImport = 'var __RawOpponent = globalThis.__injectedBot1;\\n'\n\t\t+ 'if (!__RawOpponent) throw new Error(\"Opponent not injected (set globalThis.__injectedBot1)\");';\n\treturn generateManualMatchEntryPointInner(opponentImport, engineDir);\n}\n\nfunction generateManualMatchEntryPointInner(opponentImport: string, engineDir: string): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\n${opponentImport}\nimport {ManualMatch} from '${srcPath}/engine/manual-match.ts';\nimport {idle, turnToward, flyStraight} from '${srcPath}/hooks/action-builders.ts';\nimport {getMissileContext} from '${srcPath}/engine/hooks-runtime.ts';\n\n// Worker-local state — the player AI and guided-missile AIs read from these.\nvar __latestHumanActions = null;\nvar __latestHumanMissileTargets = {};\nvar __manualMatch = null;\n\nfunction __playerStub() {\n\tif (__latestHumanActions) {\n\t\treturn {_toAction: function() { return __latestHumanActions; }};\n\t}\n\treturn idle();\n}\n\nfunction __makeGuideStub(projectileId) {\n\treturn function() {\n\t\tvar target = __latestHumanMissileTargets[projectileId];\n\t\tif (!target) return flyStraight();\n\t\treturn turnToward(target.x, target.y);\n\t};\n}\n\nfunction __ensureMatch() {\n\tif (!__manualMatch) throw new Error('ManualMatch not initialized — call manualMatchInit first');\n\treturn __manualMatch;\n}\n\nglobalThis.__manualMatchInit = function(options) {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = (options && options.initialHumanActions) || null;\n\t__latestHumanMissileTargets = {};\n\t__manualMatch = new ManualMatch(__playerStub, __RawOpponent, options || {});\n\treturn __manualMatch.getGameState();\n};\n\nglobalThis.__manualMatchStep = function(options) {\n\tvar match = __ensureMatch();\n\tif (options) {\n\t\tif (options.humanActions) __latestHumanActions = options.humanActions;\n\t\tif (options.humanMissileTargets) {\n\t\t\t// Merge — the host may only update specific projectiles per call\n\t\t\tfor (var k in options.humanMissileTargets) {\n\t\t\t\t__latestHumanMissileTargets[k] = options.humanMissileTargets[k];\n\t\t\t}\n\t\t}\n\t}\n\tvar count = (options && options.count) || 1;\n\treturn match.step(count);\n};\n\nglobalThis.__manualMatchGuide = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.replaceMissileAI(id, __makeGuideStub(id));\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchRelease = function(options) {\n\tvar match = __ensureMatch();\n\tvar id = options && options.projectileId;\n\tif (!id) return {ok: false, reason: 'missing projectileId'};\n\tmatch.restoreMissileAI(id);\n\tdelete __latestHumanMissileTargets[id];\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchSetInvincible = function(options) {\n\tvar match = __ensureMatch();\n\tvar idx = (options && options.wizardIndex) || 0;\n\tvar on = !!(options && options.on);\n\tmatch.setInvincible(idx, on);\n\treturn {ok: true};\n};\n\nglobalThis.__manualMatchGetState = function() {\n\tvar match = __ensureMatch();\n\treturn match.getGameState();\n};\n\nglobalThis.__manualMatchGetResult = function() {\n\tvar match = __ensureMatch();\n\treturn match.getResult();\n};\n\nglobalThis.__manualMatchDispose = function() {\n\tif (__manualMatch) {\n\t\t__manualMatch.dispose();\n\t\t__manualMatch = null;\n\t}\n\t__latestHumanActions = {move: {x: 0, y: 0}};\n\t__latestHumanMissileTargets = {};\n\treturn {ok: true};\n};\n`;\n}\n\n/**\n * Generate an alternate entry point where __fight/__simulate accept and return\n * JSON strings instead of structured objects. Used by the benchmark to compare\n * JSON serialization vs V8 structured clone performance.\n */\nexport function generateMatchEntryPointJSON(\n\tbot1SourcePath: string,\n\tbot1ExportName: string,\n\tbot2SourcePath: string,\n\tbot2ExportName: string,\n\tengineDir: string,\n): string\n{\n\tconst enginePath = engineDir.replace(/\\\\/g, '/');\n\tconst bot1Path = bot1SourcePath.replace(/\\\\/g, '/');\n\tconst bot2Path = bot2SourcePath.replace(/\\\\/g, '/');\n\tconst srcPath = enginePath.replace(/\\/engine\\/?$/, '');\n\n\treturn `\nimport {${bot1ExportName} as __Bot1} from '${bot1Path}';\nimport {${bot2ExportName} as __Bot2} from '${bot2Path}';\nimport {fight, simulate} from '${srcPath}/engine/simulation.ts';\nimport {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';\n\nglobalThis.__fightJSON = function __fightJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\tconst result = fight(__Bot1, __Bot2, options);\n\treturn JSON.stringify(result);\n};\n\nglobalThis.__simulateJSON = function __simulateJSON(optionsJSON) {\n\tconst options = JSON.parse(optionsJSON);\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\tconst result = simulate(bot1, bot2, simOptions);\n\treturn JSON.stringify(result);\n};\n`;\n}\n","/**\n * Run a fight between two PRECOMPILED bot bundles — the canonical uploaded-wizard\n * format where each IIFE sets `globalThis.__injectedBot1` (see compileSingleBotBundle).\n *\n * Shared by the Cloud Functions matchmaker and the devkit CLI so that a\n * `handle/botname` fight runs through the exact same engine as the live ladder.\n *\n * Both bundles run inside an isolated-vm isolate alongside a \"match template\" —\n * the engine + a `__fight` harness that reads the injected bots. The template is\n * built lazily from the engine source on first use and cached; callers that\n * already have one (the Cloud Functions committed MATCH_TEMPLATE) pass it in to\n * skip the esbuild step.\n *\n * Execution order inside the isolate: bot1 bundle → bot2 bundle → match template.\n */\n\nimport path from 'node:path';\nimport ivm from 'isolated-vm';\nimport {build} from 'esbuild';\nimport {getEngineDir} from './sandbox-compile.js';\nimport type {FightResult, SimulateResult} from './simulation.js';\n\n/** Memory limit per sandbox isolate (MB). */\nconst MEMORY_LIMIT_MB = 256;\n/** Timeout per fight (ms). */\nconst FIGHT_TIMEOUT_MS = 60_000;\n\nconst FREEZE_BANNER = `\nObject.freeze(Object.prototype);\nObject.freeze(Array.prototype);\nObject.freeze(Function.prototype);\nObject.freeze(String.prototype);\nObject.freeze(Number.prototype);\nObject.freeze(Boolean.prototype);\nObject.freeze(RegExp.prototype);\nObject.freeze(Date.prototype);\nObject.freeze(Error.prototype);\nObject.freeze(Map.prototype);\nObject.freeze(Set.prototype);\nObject.freeze(Math);\nObject.freeze(JSON);\n(function() {\n\tvar g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};\n\tvar blocked = [\n\t\t'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',\n\t\t'importScripts', 'Worker', 'SharedWorker',\n\t\t'Request', 'Response', 'Headers',\n\t\t'navigator', 'BroadcastChannel',\n\t\t'indexedDB', 'caches'\n\t];\n\tfor (var i = 0; i < blocked.length; i++) {\n\t\ttry { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }\n\t\tcatch(e) {}\n\t}\n})();\n`;\n\nlet cachedTemplate: Promise<string> | null = null;\n\n/**\n * Build (and cache) the match template: the engine + `__fight`/`__simulate`\n * harness bundled into a single IIFE string, ready to run after two bot bundles\n * have set `globalThis.__injectedBot1`/`__injectedBot2`.\n */\nexport function buildMatchTemplate(): Promise<string>\n{\n\tif (!cachedTemplate)\n\t{\n\t\tcachedTemplate = (async(): Promise<string> =>\n\t\t{\n\t\t\tconst engineDir = getEngineDir();\n\t\t\tconst coreSrc = path.dirname(engineDir).replace(/\\\\/g, '/');\n\n\t\t\tconst entryPoint = `\nimport {fight, simulate} from '${coreSrc}/engine/simulation.ts';\nimport {wrapWithParams} from '${coreSrc}/engine/params-runtime.ts';\n\nconst __Bot1 = (globalThis as any).__injectedBot1;\nconst __Bot2 = (globalThis as any).__injectedBot2;\n\nif (!__Bot1) throw new Error('Bot 1 not injected (set globalThis.__injectedBot1)');\nif (!__Bot2) throw new Error('Bot 2 not injected (set globalThis.__injectedBot2)');\n\n(globalThis as any).__fight = function __fight(options: any) {\n\treturn fight(__Bot1, __Bot2, options);\n};\n\n(globalThis as any).__simulate = function __simulate(options: any) {\n\tconst {params1, params2, ...simOptions} = options;\n\tconst bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;\n\tconst bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;\n\treturn simulate(bot1, bot2, simOptions);\n};\n`;\n\n\t\t\tconst result = await build({\n\t\t\t\tstdin: {contents: entryPoint, resolveDir: engineDir, loader: 'ts'},\n\t\t\t\tbundle: true,\n\t\t\t\twrite: false,\n\t\t\t\tformat: 'iife',\n\t\t\t\tplatform: 'neutral',\n\t\t\t\ttarget: 'es2022',\n\t\t\t\tbanner: {js: FREEZE_BANNER},\n\t\t\t\tlogLevel: 'error',\n\t\t\t\texternal: [\n\t\t\t\t\t'isolated-vm', 'esbuild',\n\t\t\t\t\t'node:path', 'node:fs', 'node:url',\n\t\t\t\t\t'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',\n\t\t\t\t],\n\t\t\t});\n\n\t\t\tif (!result.outputFiles?.[0]) throw new Error('esbuild produced no match-template output');\n\t\t\treturn result.outputFiles[0].text;\n\t\t})();\n\t}\n\treturn cachedTemplate;\n}\n\nexport interface RunBundleFightOptions\n{\n\tseed: number;\n\t/** Prebuilt match template (e.g. the committed Cloud Functions one). Built lazily if omitted. */\n\tmatchTemplate?: string;\n\t/** Skip per-tick history in the result (smaller copy out of the isolate). Default true. */\n\tskipHistory?: boolean;\n}\n\n/**\n * Run a fight between two compiled `__injectedBot1`-format bundles.\n *\n * bundle1 is saved and its global cleared before bundle2 runs, so bot code can\n * never read the opponent's function off globalThis. Both globals are deleted\n * after wiring so runtime bot code can't reach them either.\n */\nexport async function runBundleFight(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleFightOptions,\n): Promise<FightResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\tconst skipHistory = options.skipHistory ?? true;\n\n\t// Freeze prototypes/globals BEFORE the untrusted bot bundles run. The bundles\n\t// execute at module-eval (ahead of the template, whose banner used to be the\n\t// only freeze) — so without this a bot could pollute Object/Array/Math/etc. at\n\t// load time and corrupt the engine or its opponent. Mirrors the source path\n\t// (sandbox-harness), which already freezes before importing the bots.\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: FIGHT_TIMEOUT_MS});\n\n\t\tconst fightFn = await jail.get('__fight');\n\t\tconst result = await fightFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy({seed: options.seed, skipHistory}).copyInto()],\n\t\t\t{timeout: FIGHT_TIMEOUT_MS, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as FightResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n\nexport interface RunBundleSimulateOptions\n{\n\tseed?: number;\n\tspawnDistance?: number;\n\tmaxTicks?: number;\n\tmatchTemplate?: string;\n}\n\n/**\n * Run a SINGLE match between two compiled bundles, returning the full per-tick\n * history (for tracing/debugging). Same isolate wiring as runBundleFight, but\n * calls the template's `__simulate` so the caller gets a SimulateResult.\n */\nexport async function runBundleSimulate(\n\tbundle1: string,\n\tbundle2: string,\n\toptions: RunBundleSimulateOptions = {},\n): Promise<SimulateResult>\n{\n\tconst template = options.matchTemplate ?? await buildMatchTemplate();\n\n\t// Freeze before the untrusted bundles run (see runBundleFight).\n\tconst code = FREEZE_BANNER + '\\n' + bundle1\n\t\t+ '\\nvar __savedBot1 = globalThis.__injectedBot1;\\n'\n\t\t+ 'globalThis.__injectedBot1 = undefined;\\n'\n\t\t+ bundle2\n\t\t+ '\\nglobalThis.__injectedBot2 = globalThis.__injectedBot1;\\n'\n\t\t+ '\\nglobalThis.__injectedBot1 = __savedBot1;\\n'\n\t\t+ '__savedBot1 = undefined;\\n'\n\t\t+ template\n\t\t+ '\\ndelete globalThis.__injectedBot1;\\ndelete globalThis.__injectedBot2;\\n';\n\n\tconst isolate = new ivm.Isolate({memoryLimit: MEMORY_LIMIT_MB});\n\n\ttry\n\t{\n\t\tconst context = await isolate.createContext();\n\t\tconst jail = context.global;\n\t\tawait jail.set('global', jail.derefInto());\n\n\t\tconst script = await isolate.compileScript(code);\n\t\tawait script.run(context, {timeout: FIGHT_TIMEOUT_MS});\n\n\t\tconst simulateFn = await jail.get('__simulate');\n\t\tconst simOptions = {\n\t\t\tseed: options.seed ?? 1,\n\t\t\tspawnDistance: options.spawnDistance,\n\t\t\tmaxTicks: options.maxTicks,\n\t\t};\n\t\tconst result = await simulateFn.apply(\n\t\t\tundefined,\n\t\t\t[new ivm.ExternalCopy(simOptions).copyInto()],\n\t\t\t{timeout: FIGHT_TIMEOUT_MS, result: {copy: true}},\n\t\t);\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- isolated-vm returns unknown\n\t\treturn result as SimulateResult;\n\t}\n\tfinally\n\t{\n\t\tif (!isolate.isDisposed) isolate.dispose();\n\t}\n}\n"],"mappings":"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","names":["path","ivm","build","path","build","ivm"]}
|
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
ARENA_MIN,
|
|
20
20
|
ARENA_MAX,
|
|
21
21
|
MATCH_DURATION,
|
|
22
|
+
DEFAULT_SEED,
|
|
22
23
|
SPAWN_DISTANCE,
|
|
23
24
|
TICKS_PER_SECOND,
|
|
24
25
|
} from '../rules.js';
|
|
@@ -124,12 +125,12 @@ export class ManualMatch
|
|
|
124
125
|
this.wizard2AI = wizard2AI;
|
|
125
126
|
|
|
126
127
|
const {
|
|
127
|
-
seed: rawSeed =
|
|
128
|
+
seed: rawSeed = DEFAULT_SEED,
|
|
128
129
|
spawnDistance: rawSpawn,
|
|
129
130
|
maxTicks: rawMaxTicks = MATCH_DURATION,
|
|
130
131
|
} = options;
|
|
131
132
|
|
|
132
|
-
this.seed = Number.isFinite(rawSeed) ? Math.floor(rawSeed) & 0x7FFFFFFF :
|
|
133
|
+
this.seed = Number.isFinite(rawSeed) ? Math.floor(rawSeed) & 0x7FFFFFFF : DEFAULT_SEED;
|
|
133
134
|
this.maxTicks = Number.isFinite(rawMaxTicks) && rawMaxTicks > 0
|
|
134
135
|
? Math.min(Math.floor(rawMaxTicks), 100000)
|
|
135
136
|
: MATCH_DURATION;
|
package/src/engine/simulation.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
calculateBlinkCooldown,
|
|
22
22
|
WIZARD_RADIUS,
|
|
23
23
|
MATCH_DURATION,
|
|
24
|
+
DEFAULT_SEED,
|
|
24
25
|
validateMissileConfig,
|
|
25
26
|
calculateMissileRadius,
|
|
26
27
|
ARENA_WATER_BUFFER,
|
|
@@ -931,10 +932,10 @@ export function simulate(
|
|
|
931
932
|
}
|
|
932
933
|
|
|
933
934
|
// Sanitize options (Lesson #3)
|
|
934
|
-
const {maxTicks: rawMaxTicks = MATCH_DURATION, seed: rawSeed =
|
|
935
|
+
const {maxTicks: rawMaxTicks = MATCH_DURATION, seed: rawSeed = DEFAULT_SEED, spawnDistance: rawSpawnDistance, skipHistory = false} = options;
|
|
935
936
|
|
|
936
937
|
// Sanitize seed - ensure valid 32-bit integer
|
|
937
|
-
const seed = Number.isFinite(rawSeed) ? Math.floor(rawSeed) & 0x7FFFFFFF :
|
|
938
|
+
const seed = Number.isFinite(rawSeed) ? Math.floor(rawSeed) & 0x7FFFFFFF : DEFAULT_SEED;
|
|
938
939
|
|
|
939
940
|
// Sanitize maxTicks - ensure positive integer with reasonable limit
|
|
940
941
|
const maxTicks = Number.isFinite(rawMaxTicks) && rawMaxTicks > 0
|
package/src/engine-version.ts
CHANGED
package/src/rules.ts
CHANGED
|
@@ -284,6 +284,12 @@ export function calculateWarmupMultiplier(prev: MissileConfig | undefined, curre
|
|
|
284
284
|
// === MATCH (structural) ===
|
|
285
285
|
export const MATCH_DURATION = 30000; // ticks (5 minutes)
|
|
286
286
|
|
|
287
|
+
// Deterministic default seed. Fights/simulations are fully reproducible unless a caller
|
|
288
|
+
// EXPLICITLY passes a seed. Only the ranked matchmaker overrides it (a random seed it
|
|
289
|
+
// records, so the spectator UI can replay the exact match) — everything else gets this
|
|
290
|
+
// fixed default, so non-determinism never sneaks in by omission.
|
|
291
|
+
export const DEFAULT_SEED = 42;
|
|
292
|
+
|
|
287
293
|
// ============================================================
|
|
288
294
|
// NEW API CONSTANT ALIASES
|
|
289
295
|
// These are the preferred names for the new hooks-based API
|