@vibemancer/core 0.1.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.
Files changed (78) hide show
  1. package/README.md +28 -0
  2. package/dist/chunk-L7Z7OFXD.js +9140 -0
  3. package/dist/chunk-L7Z7OFXD.js.map +1 -0
  4. package/dist/index-browser.d.ts +2602 -0
  5. package/dist/index-browser.js +407 -0
  6. package/dist/index-browser.js.map +1 -0
  7. package/dist/index.d.ts +150 -0
  8. package/dist/index.js +750 -0
  9. package/dist/index.js.map +1 -0
  10. package/package.json +79 -0
  11. package/src/bots/berserker/01_Stormchaser.ts +457 -0
  12. package/src/bots/berserker/02_Stormcaller.ts +417 -0
  13. package/src/bots/berserker/03_Stormforger.ts +481 -0
  14. package/src/bots/caster/01_Flamecaller.ts +286 -0
  15. package/src/bots/caster/02_Pyromancer.ts +350 -0
  16. package/src/bots/caster/03_Infernalist.ts +492 -0
  17. package/src/bots/defensive/01_Turtle.ts +151 -0
  18. package/src/bots/defensive/02_Sentinel.ts +134 -0
  19. package/src/bots/defensive/03_Golem.ts +357 -0
  20. package/src/bots/duelist/01_Battlemage.ts +433 -0
  21. package/src/bots/duelist/02_Warmage.ts +438 -0
  22. package/src/bots/duelist/03_Archmage.ts +588 -0
  23. package/src/bots/homing/01_Bonemancer.ts +67 -0
  24. package/src/bots/homing/02_Lich.ts +356 -0
  25. package/src/bots/homing/03_Archlich.ts +220 -0
  26. package/src/bots/index.ts +30 -0
  27. package/src/bots/kiter/01_Spellspinner.ts +398 -0
  28. package/src/bots/kiter/02_Spellweaver.ts +378 -0
  29. package/src/bots/kiter/03_Spellbinder.ts +448 -0
  30. package/src/bots/melee/01_Shadowblade.ts +270 -0
  31. package/src/bots/melee/02_Nightblade.ts +437 -0
  32. package/src/bots/melee/03_Voidblade.ts +582 -0
  33. package/src/bots/registry.ts +207 -0
  34. package/src/bots/shared.ts +472 -0
  35. package/src/bots/sniper/01_Spellshot.ts +385 -0
  36. package/src/bots/sniper/02_Spelltracer.ts +441 -0
  37. package/src/bots/sniper/03_Spellseeker.ts +546 -0
  38. package/src/bots/standalone/Critter.ts +89 -0
  39. package/src/bots/standalone/Doombringer.ts +91 -0
  40. package/src/bots/standalone/Hogger.ts +228 -0
  41. package/src/bots/standalone/Rookie.ts +50 -0
  42. package/src/bots/standalone/TargetDummy.ts +21 -0
  43. package/src/bots/test/cheater.ts +405 -0
  44. package/src/bots/test/crasher.ts +81 -0
  45. package/src/engine/hooks-runtime.ts +394 -0
  46. package/src/engine/manual-match.ts +289 -0
  47. package/src/engine/missile-templates.ts +155 -0
  48. package/src/engine/optimizer.ts +220 -0
  49. package/src/engine/params-runtime.ts +189 -0
  50. package/src/engine/physics.ts +143 -0
  51. package/src/engine/sandbox-browser.ts +671 -0
  52. package/src/engine/sandbox-compile.ts +197 -0
  53. package/src/engine/sandbox-harness.ts +367 -0
  54. package/src/engine/sandbox.ts +332 -0
  55. package/src/engine/simulation.ts +828 -0
  56. package/src/engine/spells.ts +128 -0
  57. package/src/engine-version.ts +11 -0
  58. package/src/hooks/action-builders.ts +210 -0
  59. package/src/hooks/bot-wrapper.ts +84 -0
  60. package/src/hooks/index.ts +75 -0
  61. package/src/hooks/state-hooks.ts +354 -0
  62. package/src/hooks/threat-analysis.ts +365 -0
  63. package/src/hooks/types.ts +142 -0
  64. package/src/index-browser.ts +30 -0
  65. package/src/index.ts +24 -0
  66. package/src/rules.ts +254 -0
  67. package/src/stats.ts +262 -0
  68. package/src/testing.ts +207 -0
  69. package/src/trace.ts +430 -0
  70. package/src/types.ts +193 -0
  71. package/src/utils/angles.ts +47 -0
  72. package/src/utils/combat.ts +279 -0
  73. package/src/utils/distance.ts +21 -0
  74. package/src/utils/index.ts +7 -0
  75. package/src/utils/movement.ts +108 -0
  76. package/src/utils/random.ts +65 -0
  77. package/src/utils/spatial.ts +63 -0
  78. package/src/utils/targeting.ts +45 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * VIBEMANCER — SANDBOX COMPILATION
3
+ *
4
+ * Compiles match bundles using esbuild. Extracted from sandbox.ts so that
5
+ * compilation can be used independently of isolated-vm (e.g., in the CLI
6
+ * dev server or for browser Web Worker sandboxes).
7
+ *
8
+ * This file has NO isolated-vm dependency — only esbuild + Node.js builtins.
9
+ */
10
+
11
+ import {build} from 'esbuild';
12
+ import path from 'node:path';
13
+ import fs from 'node:fs';
14
+ import {fileURLToPath} from 'node:url';
15
+ import {PROTOTYPE_FREEZE_BANNER, generateManualMatchEntryPoint, generateMatchEntryPoint} from './sandbox-harness.js';
16
+
17
+ const currentFilename = fileURLToPath(import.meta.url);
18
+ const currentDirname = path.dirname(currentFilename);
19
+
20
+ /**
21
+ * Find the src/engine/ directory. Works from src/, dist/engine/, or dist/ (tsup bundle).
22
+ * esbuild needs TypeScript source files, so we look for the src/ tree.
23
+ */
24
+ export function getEngineDir(): string
25
+ {
26
+ // When running from src/engine/ (dev/test), currentDirname is already src/engine/
27
+ const directPath = path.resolve(currentDirname);
28
+ if (fs.existsSync(path.join(directPath, 'simulation.ts')))
29
+ {
30
+ return directPath;
31
+ }
32
+
33
+ // When running from dist/engine/ (individual files), package root is 2 levels up
34
+ const packageRoot2 = path.resolve(currentDirname, '..', '..');
35
+ const srcEngine2 = path.join(packageRoot2, 'src', 'engine');
36
+ if (fs.existsSync(path.join(srcEngine2, 'simulation.ts')))
37
+ {
38
+ return srcEngine2;
39
+ }
40
+
41
+ // When running from dist/ (tsup bundle), package root is 1 level up
42
+ const packageRoot1 = path.resolve(currentDirname, '..');
43
+ const srcEngine1 = path.join(packageRoot1, 'src', 'engine');
44
+ if (fs.existsSync(path.join(srcEngine1, 'simulation.ts')))
45
+ {
46
+ return srcEngine1;
47
+ }
48
+
49
+ throw new Error('Could not find engine source directory (src/engine/simulation.ts)');
50
+ }
51
+
52
+ /**
53
+ * A compiled bot ready for sandboxed execution.
54
+ * Stores the source path and export name — actual compilation
55
+ * happens when creating a MatchSandbox or calling compileMatchBundle.
56
+ */
57
+ export class BotBundle
58
+ {
59
+ readonly sourcePath: string;
60
+ readonly exportName: string;
61
+
62
+ constructor(sourcePath: string, exportName: string)
63
+ {
64
+ if (!sourcePath || typeof sourcePath !== 'string')
65
+ {
66
+ throw new Error('sourcePath must be a non-empty string');
67
+ }
68
+ if (!exportName || typeof exportName !== 'string')
69
+ {
70
+ throw new Error('exportName must be a non-empty string');
71
+ }
72
+ this.sourcePath = path.resolve(sourcePath);
73
+ this.exportName = exportName;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Options for esbuild compilation. Allows the caller to add esbuild
79
+ * aliases (e.g., resolving @vibemancer/core to the TypeScript source).
80
+ */
81
+ export interface CompileOptions
82
+ {
83
+ /** Additional esbuild alias entries (e.g., {'@vibemancer/core': '/path/to/src/index.ts'}). */
84
+ alias?: Record<string, string>;
85
+ /** Additional modules to treat as external (not bundled). */
86
+ external?: string[];
87
+ }
88
+
89
+ /**
90
+ * Compile a match bundle using esbuild. Bundles both bots + simulation engine
91
+ * into a single self-contained IIFE with prototype freezing banner.
92
+ *
93
+ * No isolated-vm dependency — returns a plain JS string.
94
+ */
95
+ export async function compileMatchBundle(
96
+ bot1: BotBundle,
97
+ bot2: BotBundle,
98
+ options?: CompileOptions,
99
+ ): Promise<string>
100
+ {
101
+ const engineDir = getEngineDir();
102
+ const entryPoint = generateMatchEntryPoint(
103
+ bot1.sourcePath,
104
+ bot1.exportName,
105
+ bot2.sourcePath,
106
+ bot2.exportName,
107
+ engineDir,
108
+ );
109
+
110
+ const result = await build({
111
+ stdin: {
112
+ contents: entryPoint,
113
+ resolveDir: engineDir,
114
+ loader: 'ts',
115
+ },
116
+ bundle: true,
117
+ write: false,
118
+ format: 'iife',
119
+ platform: 'neutral',
120
+ target: 'es2022',
121
+ banner: {
122
+ js: PROTOTYPE_FREEZE_BANNER,
123
+ },
124
+ // Suppress warnings about top-level this in ESM
125
+ logLevel: 'error',
126
+ // Match bundles run in sandboxed environments (Web Workers / isolated-vm)
127
+ // and should never include Node.js native modules
128
+ external: [
129
+ 'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',
130
+ 'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',
131
+ ...(options?.external ?? []),
132
+ ],
133
+ ...(options?.alias ? {alias: options.alias} : {}),
134
+ });
135
+
136
+ if (!result.outputFiles?.[0])
137
+ {
138
+ throw new Error('esbuild produced no output');
139
+ }
140
+
141
+ return result.outputFiles[0].text;
142
+ }
143
+
144
+ /**
145
+ * Compile a manual-play sandbox bundle. Bundles ONE opponent bot + ManualMatch
146
+ * + simulation engine into a self-contained IIFE. The "player" wizard is a
147
+ * worker-local stub that reads from `__latestHumanActions` (set per-step by
148
+ * the host).
149
+ *
150
+ * Returns a plain JS string that, when loaded into a Web Worker, exposes the
151
+ * `__manualMatchInit`, `__manualMatchStep`, `__manualMatchHijack`,
152
+ * `__manualMatchRelease`, `__manualMatchSetInvincible`,
153
+ * `__manualMatchGetState`, `__manualMatchGetResult`, and `__manualMatchDispose`
154
+ * globals on the worker's globalThis.
155
+ */
156
+ export async function compileManualMatchBundle(
157
+ opponent: BotBundle,
158
+ options?: CompileOptions,
159
+ ): Promise<string>
160
+ {
161
+ const engineDir = getEngineDir();
162
+ const entryPoint = generateManualMatchEntryPoint(
163
+ opponent.sourcePath,
164
+ opponent.exportName,
165
+ engineDir,
166
+ );
167
+
168
+ const result = await build({
169
+ stdin: {
170
+ contents: entryPoint,
171
+ resolveDir: engineDir,
172
+ loader: 'ts',
173
+ },
174
+ bundle: true,
175
+ write: false,
176
+ format: 'iife',
177
+ platform: 'neutral',
178
+ target: 'es2022',
179
+ banner: {
180
+ js: PROTOTYPE_FREEZE_BANNER,
181
+ },
182
+ logLevel: 'error',
183
+ external: [
184
+ 'isolated-vm', 'esbuild', 'node:path', 'node:fs', 'node:url',
185
+ 'node:worker_threads', 'node:crypto', 'node:os', 'node:child_process',
186
+ ...(options?.external ?? []),
187
+ ],
188
+ ...(options?.alias ? {alias: options.alias} : {}),
189
+ });
190
+
191
+ if (!result.outputFiles?.[0])
192
+ {
193
+ throw new Error('esbuild produced no output');
194
+ }
195
+
196
+ return result.outputFiles[0].text;
197
+ }
@@ -0,0 +1,367 @@
1
+ /**
2
+ * VIBEMANCER — SANDBOX HARNESS
3
+ *
4
+ * Generates the entry point code that runs inside an isolated-vm isolate.
5
+ * The harness bundles both bots + the simulation engine into a single IIFE
6
+ * via esbuild, then exposes __fight and __simulate on globalThis.
7
+ *
8
+ * The prototype freeze banner runs before any module code, preventing
9
+ * prototype pollution attacks between bots sharing the same isolate.
10
+ */
11
+
12
+ /**
13
+ * JavaScript code injected as esbuild banner — runs before the IIFE bundle.
14
+ *
15
+ * 1. Freezes all built-in prototypes to prevent cross-bot sabotage via prototype
16
+ * pollution. This does NOT prevent calling existing methods (e.g. Array.push
17
+ * still works), it only prevents reassigning them.
18
+ *
19
+ * 2. Blocks all IO/network capabilities. Web Workers have fetch, XMLHttpRequest,
20
+ * WebSocket, importScripts, etc. Bot code must not be able to make network
21
+ * calls or load external scripts. Uses Object.defineProperty to make the block
22
+ * irrecoverable (non-writable, non-configurable). In isolated-vm, these globals
23
+ * don't exist — the try-catch makes the deletes a harmless no-op.
24
+ */
25
+ export const PROTOTYPE_FREEZE_BANNER = `
26
+ Object.freeze(Object.prototype);
27
+ Object.freeze(Array.prototype);
28
+ Object.freeze(Function.prototype);
29
+ Object.freeze(String.prototype);
30
+ Object.freeze(Number.prototype);
31
+ Object.freeze(Boolean.prototype);
32
+ Object.freeze(RegExp.prototype);
33
+ Object.freeze(Date.prototype);
34
+ Object.freeze(Error.prototype);
35
+ Object.freeze(Map.prototype);
36
+ Object.freeze(Set.prototype);
37
+ Object.freeze(Math);
38
+ Object.freeze(JSON);
39
+ (function() {
40
+ var g = typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : {};
41
+ var blocked = [
42
+ 'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
43
+ 'importScripts', 'Worker', 'SharedWorker',
44
+ 'Request', 'Response', 'Headers',
45
+ 'navigator', 'BroadcastChannel',
46
+ 'indexedDB', 'caches'
47
+ ];
48
+ for (var i = 0; i < blocked.length; i++) {
49
+ try { Object.defineProperty(g, blocked[i], {value: undefined, writable: false, configurable: false}); }
50
+ catch(e) {}
51
+ }
52
+ })();
53
+ `;
54
+
55
+ /**
56
+ * Generate the TypeScript entry point for a match sandbox.
57
+ *
58
+ * This entry point imports both bots and the simulation engine, then
59
+ * exposes __fight and __simulate on globalThis. esbuild bundles this
60
+ * + all transitive imports into a single self-contained IIFE.
61
+ *
62
+ * @param bot1SourcePath - Absolute path to bot 1's TypeScript source file
63
+ * @param bot1ExportName - Named export of bot 1's WizardFunction
64
+ * @param bot2SourcePath - Absolute path to bot 2's TypeScript source file
65
+ * @param bot2ExportName - Named export of bot 2's WizardFunction
66
+ * @param engineDir - Absolute path to the engine directory (src/engine/)
67
+ */
68
+ export function generateMatchEntryPoint(
69
+ bot1SourcePath: string,
70
+ bot1ExportName: string,
71
+ bot2SourcePath: string,
72
+ bot2ExportName: string,
73
+ engineDir: string,
74
+ ): string
75
+ {
76
+ // Use forward slashes for esbuild compatibility (works on all platforms)
77
+ const enginePath = engineDir.replace(/\\/g, '/');
78
+ const bot1Path = bot1SourcePath.replace(/\\/g, '/');
79
+ const bot2Path = bot2SourcePath.replace(/\\/g, '/');
80
+
81
+ // All imports use the engineDir's parent (= packages/core/src/) as root.
82
+ // When user bots alias @vibemancer/core → src/index-browser.ts, esbuild
83
+ // deduplicates these with the bot's imports since they resolve to the same files.
84
+ // This ensures the hooks runtime global state is shared between harness and bot.
85
+ const srcPath = enginePath.replace(/\/engine\/?$/, '');
86
+
87
+ return `
88
+ import {${bot1ExportName} as __RawBot1} from '${bot1Path}';
89
+ import {${bot2ExportName} as __RawBot2} from '${bot2Path}';
90
+ import {fight, simulate} from '${srcPath}/engine/simulation.ts';
91
+ import {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';
92
+ import {wrapNewBot} from '${srcPath}/hooks/bot-wrapper.ts';
93
+
94
+ // Auto-detect new-style bots (hooks API) and wrap them for the simulation engine.
95
+ // New-style: uses hooks, returns FinalAction (has _toAction method).
96
+ // Old-style: accepts ({state, config, random}), returns WizardActions directly.
97
+ // We detect by doing a dry-run call — if the result has _toAction, it's new-style.
98
+ //
99
+ // Detection is deferred until the first fight/simulate call so that malicious
100
+ // bots (infinite loops, memory bombs) time out during fight(), not during
101
+ // sandbox creation. __resolveBots memoizes the wrapped functions.
102
+ function __wrap(bot) {
103
+ if (bot.length > 0) return bot; // Old-style: has parameters
104
+ // 0-param function could be either style. Try calling it to check return type.
105
+ try {
106
+ var result = bot();
107
+ if (result && typeof result._toAction === 'function') {
108
+ return wrapNewBot(bot);
109
+ }
110
+ } catch(e) {
111
+ // If it throws (e.g. hooks not initialized), it must be new-style
112
+ return wrapNewBot(bot);
113
+ }
114
+ return bot;
115
+ }
116
+
117
+ var __Bot1 = null;
118
+ var __Bot2 = null;
119
+ function __resolveBots() {
120
+ if (__Bot1 === null) __Bot1 = __wrap(__RawBot1);
121
+ if (__Bot2 === null) __Bot2 = __wrap(__RawBot2);
122
+ }
123
+
124
+ globalThis.__fight = function __fight(options) {
125
+ __resolveBots();
126
+ const result = fight(__Bot1, __Bot2, options);
127
+ return result;
128
+ };
129
+
130
+ globalThis.__simulate = function __simulate(options) {
131
+ __resolveBots();
132
+ const {params1, params2, ...simOptions} = options;
133
+ const bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;
134
+ const bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;
135
+ const result = simulate(bot1, bot2, simOptions);
136
+ return result;
137
+ };
138
+ `;
139
+ }
140
+
141
+ /**
142
+ * Generate the entry point for a Manual Play sandbox bundle.
143
+ *
144
+ * Unlike the fight/simulate entry point (which exposes one-shot batch APIs),
145
+ * the manual-match entry point holds a single long-lived ManualMatch instance
146
+ * inside the worker and exposes per-tick step/rewind/hijack APIs.
147
+ *
148
+ * The "player" wizard is a worker-local stub that returns whatever
149
+ * `__latestHumanActions` is set to — this avoids the can't-postMessage-functions
150
+ * problem (the function lives entirely worker-side, only data crosses the
151
+ * boundary). Hijacked missiles use a similar pattern via
152
+ * `__latestHumanMissileTargets[projectileId]`.
153
+ *
154
+ * @param opponentSourcePath - Absolute path to opponent bot's TypeScript source
155
+ * @param opponentExportName - Named export of the opponent's WizardFunction
156
+ * @param engineDir - Absolute path to src/engine/
157
+ */
158
+ export function generateManualMatchEntryPoint(
159
+ opponentSourcePath: string,
160
+ opponentExportName: string,
161
+ engineDir: string,
162
+ ): string
163
+ {
164
+ const opponentPath = opponentSourcePath.replace(/\\/g, '/');
165
+ return generateManualMatchEntryPointInner(
166
+ `import {${opponentExportName} as __RawOpponent} from '${opponentPath}';`,
167
+ engineDir,
168
+ );
169
+ }
170
+
171
+ /**
172
+ * Browser-friendly variant — the opponent is injected at runtime via
173
+ * globalThis.__injectedBot1 (set by prepending the player's bot bundle to
174
+ * the compiled output of this template). Used by the web client for manual
175
+ * play against uploaded wizards.
176
+ */
177
+ export function generateBrowserManualMatchEntryPoint(engineDir: string): string
178
+ {
179
+ const opponentImport = 'var __RawOpponent = globalThis.__injectedBot1;\n'
180
+ + 'if (!__RawOpponent) throw new Error("Opponent not injected (set globalThis.__injectedBot1)");';
181
+ return generateManualMatchEntryPointInner(opponentImport, engineDir);
182
+ }
183
+
184
+ function generateManualMatchEntryPointInner(opponentImport: string, engineDir: string): string
185
+ {
186
+ const enginePath = engineDir.replace(/\\/g, '/');
187
+ const srcPath = enginePath.replace(/\/engine\/?$/, '');
188
+
189
+ return `
190
+ ${opponentImport}
191
+ import {ManualMatch} from '${srcPath}/engine/manual-match.ts';
192
+ import {wrapNewBot} from '${srcPath}/hooks/bot-wrapper.ts';
193
+
194
+ // Worker-local state — the player AI and hijacked-missile AIs read from these.
195
+ var __latestHumanActions = {move: {x: 0, y: 0}};
196
+ var __latestHumanMissileTargets = {};
197
+ var __manualMatch = null;
198
+
199
+ function __wrap(bot) {
200
+ if (bot.length > 0) return bot;
201
+ try {
202
+ var result = bot();
203
+ if (result && typeof result._toAction === 'function') {
204
+ return wrapNewBot(bot);
205
+ }
206
+ } catch(e) {
207
+ return wrapNewBot(bot);
208
+ }
209
+ return bot;
210
+ }
211
+
212
+ function __playerStub() {
213
+ return __latestHumanActions;
214
+ }
215
+
216
+ function __makeHijackStub(projectileId) {
217
+ return function(props) {
218
+ var target = __latestHumanMissileTargets[projectileId];
219
+ if (!target) return {};
220
+ return {turnToward: target};
221
+ };
222
+ }
223
+
224
+ function __ensureMatch() {
225
+ if (!__manualMatch) throw new Error('ManualMatch not initialized — call manualMatchInit first');
226
+ return __manualMatch;
227
+ }
228
+
229
+ globalThis.__manualMatchInit = function(options) {
230
+ if (__manualMatch) {
231
+ __manualMatch.dispose();
232
+ __manualMatch = null;
233
+ }
234
+ __latestHumanActions = (options && options.initialHumanActions) || {move: {x: 0, y: 0}};
235
+ __latestHumanMissileTargets = {};
236
+ var opponent = __wrap(__RawOpponent);
237
+ __manualMatch = new ManualMatch(__playerStub, opponent, options || {});
238
+ return __manualMatch.getGameState();
239
+ };
240
+
241
+ globalThis.__manualMatchStep = function(options) {
242
+ var match = __ensureMatch();
243
+ if (options) {
244
+ if (options.humanActions) __latestHumanActions = options.humanActions;
245
+ if (options.humanMissileTargets) {
246
+ // Merge — the host may only update specific projectiles per call
247
+ for (var k in options.humanMissileTargets) {
248
+ __latestHumanMissileTargets[k] = options.humanMissileTargets[k];
249
+ }
250
+ }
251
+ }
252
+ var count = (options && options.count) || 1;
253
+ return match.step(count);
254
+ };
255
+
256
+ globalThis.__manualMatchHijack = function(options) {
257
+ var match = __ensureMatch();
258
+ var id = options && options.projectileId;
259
+ if (!id) return {ok: false, reason: 'missing projectileId'};
260
+ match.replaceMissileAI(id, __makeHijackStub(id));
261
+ return {ok: true};
262
+ };
263
+
264
+ globalThis.__manualMatchRelease = function(options) {
265
+ var match = __ensureMatch();
266
+ var id = options && options.projectileId;
267
+ if (!id) return {ok: false, reason: 'missing projectileId'};
268
+ match.restoreMissileAI(id);
269
+ delete __latestHumanMissileTargets[id];
270
+ return {ok: true};
271
+ };
272
+
273
+ globalThis.__manualMatchSetInvincible = function(options) {
274
+ var match = __ensureMatch();
275
+ var idx = (options && options.wizardIndex) || 0;
276
+ var on = !!(options && options.on);
277
+ match.setInvincible(idx, on);
278
+ return {ok: true};
279
+ };
280
+
281
+ globalThis.__manualMatchGetState = function() {
282
+ var match = __ensureMatch();
283
+ return match.getGameState();
284
+ };
285
+
286
+ globalThis.__manualMatchGetResult = function() {
287
+ var match = __ensureMatch();
288
+ return match.getResult();
289
+ };
290
+
291
+ globalThis.__manualMatchDispose = function() {
292
+ if (__manualMatch) {
293
+ __manualMatch.dispose();
294
+ __manualMatch = null;
295
+ }
296
+ __latestHumanActions = {move: {x: 0, y: 0}};
297
+ __latestHumanMissileTargets = {};
298
+ return {ok: true};
299
+ };
300
+ `;
301
+ }
302
+
303
+ /**
304
+ * Generate an alternate entry point where __fight/__simulate accept and return
305
+ * JSON strings instead of structured objects. Used by the benchmark to compare
306
+ * JSON serialization vs V8 structured clone performance.
307
+ */
308
+ export function generateMatchEntryPointJSON(
309
+ bot1SourcePath: string,
310
+ bot1ExportName: string,
311
+ bot2SourcePath: string,
312
+ bot2ExportName: string,
313
+ engineDir: string,
314
+ ): string
315
+ {
316
+ const enginePath = engineDir.replace(/\\/g, '/');
317
+ const bot1Path = bot1SourcePath.replace(/\\/g, '/');
318
+ const bot2Path = bot2SourcePath.replace(/\\/g, '/');
319
+ const srcPath = enginePath.replace(/\/engine\/?$/, '');
320
+
321
+ return `
322
+ import {${bot1ExportName} as __RawBot1} from '${bot1Path}';
323
+ import {${bot2ExportName} as __RawBot2} from '${bot2Path}';
324
+ import {fight, simulate} from '${srcPath}/engine/simulation.ts';
325
+ import {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';
326
+ import {wrapNewBot} from '${srcPath}/hooks/bot-wrapper.ts';
327
+
328
+ function __wrap(bot) {
329
+ if (bot.length > 0) return bot;
330
+ try {
331
+ var result = bot();
332
+ if (result && typeof result._toAction === 'function') {
333
+ return wrapNewBot(bot);
334
+ }
335
+ } catch(e) {
336
+ return wrapNewBot(bot);
337
+ }
338
+ return bot;
339
+ }
340
+
341
+ // Detection is deferred until the first fight/simulate call so that malicious
342
+ // bots time out during fight(), not during sandbox creation.
343
+ var __Bot1 = null;
344
+ var __Bot2 = null;
345
+ function __resolveBots() {
346
+ if (__Bot1 === null) __Bot1 = __wrap(__RawBot1);
347
+ if (__Bot2 === null) __Bot2 = __wrap(__RawBot2);
348
+ }
349
+
350
+ globalThis.__fightJSON = function __fightJSON(optionsJSON) {
351
+ __resolveBots();
352
+ const options = JSON.parse(optionsJSON);
353
+ const result = fight(__Bot1, __Bot2, options);
354
+ return JSON.stringify(result);
355
+ };
356
+
357
+ globalThis.__simulateJSON = function __simulateJSON(optionsJSON) {
358
+ __resolveBots();
359
+ const options = JSON.parse(optionsJSON);
360
+ const {params1, params2, ...simOptions} = options;
361
+ const bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;
362
+ const bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;
363
+ const result = simulate(bot1, bot2, simOptions);
364
+ return JSON.stringify(result);
365
+ };
366
+ `;
367
+ }