@vibemancer/core 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-W7HWJFQ4.js +10599 -0
  3. package/dist/chunk-W7HWJFQ4.js.map +1 -0
  4. package/dist/index-browser.d.ts +1300 -1051
  5. package/dist/index-browser.js +55 -17
  6. package/dist/index.d.ts +53 -3
  7. package/dist/index.js +200 -54
  8. package/dist/index.js.map +1 -1
  9. package/package.json +21 -15
  10. package/src/bots/Hero.ts +867 -0
  11. package/src/bots/berserker/01_Stormchaser.ts +162 -120
  12. package/src/bots/berserker/02_Stormcaller.ts +160 -116
  13. package/src/bots/berserker/03_Stormforger.ts +162 -129
  14. package/src/bots/caster/01_Flamecaller.ts +126 -84
  15. package/src/bots/caster/02_Pyromancer.ts +148 -101
  16. package/src/bots/caster/03_Infernalist.ts +180 -160
  17. package/src/bots/defensive/01_Turtle.ts +48 -44
  18. package/src/bots/defensive/02_Sentinel.ts +57 -53
  19. package/src/bots/defensive/03_Golem.ts +85 -97
  20. package/src/bots/duelist/01_Battlemage.ts +157 -122
  21. package/src/bots/duelist/02_Warmage.ts +166 -121
  22. package/src/bots/duelist/03_Archmage.ts +198 -178
  23. package/src/bots/homing/01_Bonemancer.ts +20 -32
  24. package/src/bots/homing/02_Lich.ts +145 -93
  25. package/src/bots/homing/03_Archlich.ts +129 -60
  26. package/src/bots/kiter/01_Spellspinner.ts +145 -107
  27. package/src/bots/kiter/02_Spellweaver.ts +153 -105
  28. package/src/bots/kiter/03_Spellbinder.ts +168 -123
  29. package/src/bots/melee/01_Shadowblade.ts +131 -75
  30. package/src/bots/melee/02_Nightblade.ts +174 -130
  31. package/src/bots/melee/03_Voidblade.ts +266 -168
  32. package/src/bots/registry.ts +44 -37
  33. package/src/bots/shared.ts +185 -25
  34. package/src/bots/sniper/01_Spellshot.ts +151 -98
  35. package/src/bots/sniper/02_Spelltracer.ts +173 -128
  36. package/src/bots/sniper/03_Spellseeker.ts +177 -152
  37. package/src/bots/standalone/Critter.ts +46 -52
  38. package/src/bots/standalone/Doombringer.ts +36 -40
  39. package/src/bots/standalone/Hogger.ts +120 -89
  40. package/src/bots/standalone/Rookie.ts +21 -23
  41. package/src/bots/standalone/TargetDummy.ts +5 -6
  42. package/src/bots/test/cheater.ts +91 -85
  43. package/src/bots/test/crasher.ts +26 -28
  44. package/src/engine/bundle-fight.ts +242 -0
  45. package/src/engine/hooks-runtime.ts +58 -18
  46. package/src/engine/manual-match.ts +33 -25
  47. package/src/engine/missile-templates.ts +25 -43
  48. package/src/engine/optimizer.ts +7 -7
  49. package/src/engine/params-runtime.ts +3 -3
  50. package/src/engine/physics.ts +33 -23
  51. package/src/engine/sandbox-browser.ts +8 -7
  52. package/src/engine/sandbox-compile.ts +1 -1
  53. package/src/engine/sandbox-harness.ts +299 -367
  54. package/src/engine/sandbox.ts +3 -3
  55. package/src/engine/simulation.ts +291 -76
  56. package/src/engine/spells.ts +9 -12
  57. package/src/engine-version.ts +1 -1
  58. package/src/hooks/action-builders.ts +76 -21
  59. package/src/hooks/index.ts +17 -9
  60. package/src/hooks/state-hooks.ts +61 -25
  61. package/src/hooks/threat-analysis.ts +44 -20
  62. package/src/hooks/types.ts +62 -5
  63. package/src/index.ts +2 -0
  64. package/src/rules.ts +209 -63
  65. package/src/stats.ts +2 -2
  66. package/src/testing.ts +6 -30
  67. package/src/trace.ts +63 -3
  68. package/src/types.ts +127 -14
  69. package/src/utils/angles.ts +1 -0
  70. package/src/utils/combat.ts +98 -6
  71. package/src/utils/spatial.ts +3 -3
  72. package/dist/chunk-74FFJCFF.js +0 -9140
  73. package/dist/chunk-74FFJCFF.js.map +0 -1
  74. package/src/hooks/bot-wrapper.ts +0 -84
@@ -1,367 +1,299 @@
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
- }
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 __Bot1} from '${bot1Path}';
89
+ import {${bot2ExportName} as __Bot2} from '${bot2Path}';
90
+ import {fight, simulate} from '${srcPath}/engine/simulation.ts';
91
+ import {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';
92
+
93
+ globalThis.__fight = function __fight(options) {
94
+ const result = fight(__Bot1, __Bot2, options);
95
+ return result;
96
+ };
97
+
98
+ globalThis.__simulate = function __simulate(options) {
99
+ const {params1, params2, ...simOptions} = options;
100
+ const bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;
101
+ const bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;
102
+ const result = simulate(bot1, bot2, simOptions);
103
+ return result;
104
+ };
105
+ `;
106
+ }
107
+
108
+ /**
109
+ * Generate the entry point for a Manual Play sandbox bundle.
110
+ *
111
+ * Unlike the fight/simulate entry point (which exposes one-shot batch APIs),
112
+ * the manual-match entry point holds a single long-lived ManualMatch instance
113
+ * inside the worker and exposes per-tick step/rewind/guide APIs.
114
+ *
115
+ * The "player" wizard is a worker-local stub that returns whatever
116
+ * `__latestHumanActions` is set to — this avoids the can't-postMessage-functions
117
+ * problem (the function lives entirely worker-side, only data crosses the
118
+ * boundary). Guided missiles use a similar pattern via
119
+ * `__latestHumanMissileTargets[projectileId]`.
120
+ *
121
+ * @param opponentSourcePath - Absolute path to opponent bot's TypeScript source
122
+ * @param opponentExportName - Named export of the opponent's WizardFunction
123
+ * @param engineDir - Absolute path to src/engine/
124
+ */
125
+ export function generateManualMatchEntryPoint(
126
+ opponentSourcePath: string,
127
+ opponentExportName: string,
128
+ engineDir: string,
129
+ ): string
130
+ {
131
+ const opponentPath = opponentSourcePath.replace(/\\/g, '/');
132
+ return generateManualMatchEntryPointInner(
133
+ `import {${opponentExportName} as __RawOpponent} from '${opponentPath}';`,
134
+ engineDir,
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Browser-friendly variant — the opponent is injected at runtime via
140
+ * globalThis.__injectedBot1 (set by prepending the player's bot bundle to
141
+ * the compiled output of this template). Used by the web client for manual
142
+ * play against uploaded wizards.
143
+ */
144
+ export function generateBrowserManualMatchEntryPoint(engineDir: string): string
145
+ {
146
+ const opponentImport = 'var __RawOpponent = globalThis.__injectedBot1;\n'
147
+ + 'if (!__RawOpponent) throw new Error("Opponent not injected (set globalThis.__injectedBot1)");';
148
+ return generateManualMatchEntryPointInner(opponentImport, engineDir);
149
+ }
150
+
151
+ function generateManualMatchEntryPointInner(opponentImport: string, engineDir: string): string
152
+ {
153
+ const enginePath = engineDir.replace(/\\/g, '/');
154
+ const srcPath = enginePath.replace(/\/engine\/?$/, '');
155
+
156
+ return `
157
+ ${opponentImport}
158
+ import {ManualMatch} from '${srcPath}/engine/manual-match.ts';
159
+ import {idle, turnToward, flyStraight} from '${srcPath}/hooks/action-builders.ts';
160
+ import {getMissileContext} from '${srcPath}/engine/hooks-runtime.ts';
161
+
162
+ // Worker-local state — the player AI and guided-missile AIs read from these.
163
+ var __latestHumanActions = null;
164
+ var __latestHumanMissileTargets = {};
165
+ var __manualMatch = null;
166
+
167
+ function __playerStub() {
168
+ if (__latestHumanActions) {
169
+ return {_toAction: function() { return __latestHumanActions; }};
170
+ }
171
+ return idle();
172
+ }
173
+
174
+ function __makeGuideStub(projectileId) {
175
+ return function() {
176
+ var target = __latestHumanMissileTargets[projectileId];
177
+ if (!target) return flyStraight();
178
+ return turnToward(target.x, target.y);
179
+ };
180
+ }
181
+
182
+ function __ensureMatch() {
183
+ if (!__manualMatch) throw new Error('ManualMatch not initialized — call manualMatchInit first');
184
+ return __manualMatch;
185
+ }
186
+
187
+ globalThis.__manualMatchInit = function(options) {
188
+ if (__manualMatch) {
189
+ __manualMatch.dispose();
190
+ __manualMatch = null;
191
+ }
192
+ __latestHumanActions = (options && options.initialHumanActions) || null;
193
+ __latestHumanMissileTargets = {};
194
+ __manualMatch = new ManualMatch(__playerStub, __RawOpponent, options || {});
195
+ return __manualMatch.getGameState();
196
+ };
197
+
198
+ globalThis.__manualMatchStep = function(options) {
199
+ var match = __ensureMatch();
200
+ if (options) {
201
+ if (options.humanActions) __latestHumanActions = options.humanActions;
202
+ if (options.humanMissileTargets) {
203
+ // Merge the host may only update specific projectiles per call
204
+ for (var k in options.humanMissileTargets) {
205
+ __latestHumanMissileTargets[k] = options.humanMissileTargets[k];
206
+ }
207
+ }
208
+ }
209
+ var count = (options && options.count) || 1;
210
+ return match.step(count);
211
+ };
212
+
213
+ globalThis.__manualMatchGuide = function(options) {
214
+ var match = __ensureMatch();
215
+ var id = options && options.projectileId;
216
+ if (!id) return {ok: false, reason: 'missing projectileId'};
217
+ match.replaceMissileAI(id, __makeGuideStub(id));
218
+ return {ok: true};
219
+ };
220
+
221
+ globalThis.__manualMatchRelease = function(options) {
222
+ var match = __ensureMatch();
223
+ var id = options && options.projectileId;
224
+ if (!id) return {ok: false, reason: 'missing projectileId'};
225
+ match.restoreMissileAI(id);
226
+ delete __latestHumanMissileTargets[id];
227
+ return {ok: true};
228
+ };
229
+
230
+ globalThis.__manualMatchSetInvincible = function(options) {
231
+ var match = __ensureMatch();
232
+ var idx = (options && options.wizardIndex) || 0;
233
+ var on = !!(options && options.on);
234
+ match.setInvincible(idx, on);
235
+ return {ok: true};
236
+ };
237
+
238
+ globalThis.__manualMatchGetState = function() {
239
+ var match = __ensureMatch();
240
+ return match.getGameState();
241
+ };
242
+
243
+ globalThis.__manualMatchGetResult = function() {
244
+ var match = __ensureMatch();
245
+ return match.getResult();
246
+ };
247
+
248
+ globalThis.__manualMatchDispose = function() {
249
+ if (__manualMatch) {
250
+ __manualMatch.dispose();
251
+ __manualMatch = null;
252
+ }
253
+ __latestHumanActions = {move: {x: 0, y: 0}};
254
+ __latestHumanMissileTargets = {};
255
+ return {ok: true};
256
+ };
257
+ `;
258
+ }
259
+
260
+ /**
261
+ * Generate an alternate entry point where __fight/__simulate accept and return
262
+ * JSON strings instead of structured objects. Used by the benchmark to compare
263
+ * JSON serialization vs V8 structured clone performance.
264
+ */
265
+ export function generateMatchEntryPointJSON(
266
+ bot1SourcePath: string,
267
+ bot1ExportName: string,
268
+ bot2SourcePath: string,
269
+ bot2ExportName: string,
270
+ engineDir: string,
271
+ ): string
272
+ {
273
+ const enginePath = engineDir.replace(/\\/g, '/');
274
+ const bot1Path = bot1SourcePath.replace(/\\/g, '/');
275
+ const bot2Path = bot2SourcePath.replace(/\\/g, '/');
276
+ const srcPath = enginePath.replace(/\/engine\/?$/, '');
277
+
278
+ return `
279
+ import {${bot1ExportName} as __Bot1} from '${bot1Path}';
280
+ import {${bot2ExportName} as __Bot2} from '${bot2Path}';
281
+ import {fight, simulate} from '${srcPath}/engine/simulation.ts';
282
+ import {wrapWithParams} from '${srcPath}/engine/params-runtime.ts';
283
+
284
+ globalThis.__fightJSON = function __fightJSON(optionsJSON) {
285
+ const options = JSON.parse(optionsJSON);
286
+ const result = fight(__Bot1, __Bot2, options);
287
+ return JSON.stringify(result);
288
+ };
289
+
290
+ globalThis.__simulateJSON = function __simulateJSON(optionsJSON) {
291
+ const options = JSON.parse(optionsJSON);
292
+ const {params1, params2, ...simOptions} = options;
293
+ const bot1 = params1 ? wrapWithParams(__Bot1, params1) : __Bot1;
294
+ const bot2 = params2 ? wrapWithParams(__Bot2, params2) : __Bot2;
295
+ const result = simulate(bot1, bot2, simOptions);
296
+ return JSON.stringify(result);
297
+ };
298
+ `;
299
+ }