@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,671 @@
1
+ /**
2
+ * VIBEMANCER — BROWSER SANDBOX
3
+ *
4
+ * Provides Web Worker-based sandboxing for bot code execution in the browser.
5
+ * Same compiled bundles as the isolated-vm sandbox (MatchSandbox), but runs
6
+ * in a Web Worker instead of a V8 isolate.
7
+ *
8
+ * Architecture:
9
+ * - Host: creates Worker from Blob URL, communicates via postMessage
10
+ * - Worker: loads compiled bundle (sets globalThis.__fight/__simulate),
11
+ * dispatches fight/simulate calls, posts results back
12
+ *
13
+ * Safety:
14
+ * - Timeout via setTimeout + worker.terminate() catches infinite loops
15
+ * - No memory limit (browser manages worker memory; worst case = tab crash)
16
+ * - Prototype freeze prevents cross-bot sabotage (same banner as isolated-vm)
17
+ * - No Node.js APIs available in Web Workers
18
+ *
19
+ * NOTE: This file has ZERO Node.js dependencies. It works in any JS environment.
20
+ */
21
+
22
+ import type {GameState, MissileAIFunction, ProjectileState, WizardActions} from '../types.js';
23
+ import type {BotError, FightResult, MatchWinner, SimulateResult} from './simulation.js';
24
+ import type {StepResult} from './manual-match.js';
25
+
26
+ // ============================================================
27
+ // WORKER PROTOCOL
28
+ // ============================================================
29
+
30
+ /**
31
+ * Minimal Worker interface for dependency injection.
32
+ * Matches the browser Worker API subset we need.
33
+ * For tests, a Node.js worker_threads adapter can implement this.
34
+ */
35
+ export interface WorkerLike
36
+ {
37
+ postMessage(data: unknown): void;
38
+ terminate(): void;
39
+ addEventListener(type: string, listener: (ev: unknown) => void): void;
40
+ removeEventListener(type: string, listener: (ev: unknown) => void): void;
41
+ }
42
+
43
+ /**
44
+ * Factory function that creates a WorkerLike from a JavaScript code string.
45
+ * Default: creates a browser Web Worker via Blob URL.
46
+ * Override in options.createWorker for testing with Node.js worker_threads.
47
+ */
48
+ export type WorkerFactory = (code: string) => {worker: WorkerLike; cleanup?: () => void};
49
+
50
+ /**
51
+ * Options for browser sandbox creation.
52
+ */
53
+ export interface BrowserSandboxOptions
54
+ {
55
+ /** Timeout in ms for fight/simulate calls (default: 30000). */
56
+ timeoutMs?: number;
57
+ /** Custom worker factory for dependency injection (testing). */
58
+ createWorker?: WorkerFactory;
59
+ }
60
+
61
+ // ============================================================
62
+ // WORKER BOOTSTRAP
63
+ // ============================================================
64
+
65
+ /**
66
+ * JavaScript bootstrap code appended to the compiled bundle.
67
+ * Runs inside the worker after the IIFE sets up the relevant globals
68
+ * (e.g., __fight/__simulate for fight bundles, __manualMatchInit/Step/etc
69
+ * for manual-play bundles).
70
+ *
71
+ * The dispatcher is generic: any method name `foo` resolves to
72
+ * `globalThis.__foo`. This allows new bundle types to expose new method
73
+ * names without changing the bootstrap.
74
+ *
75
+ * Environment-adaptive: works in both browser Web Workers and
76
+ * Node.js worker_threads (for testing).
77
+ */
78
+ const WORKER_BOOTSTRAP = `
79
+ ;(function() {
80
+ var _port = null;
81
+ try { _port = require('worker_threads').parentPort; } catch(e) {}
82
+
83
+ function send(data) {
84
+ if (_port) _port.postMessage(data);
85
+ else self.postMessage(data);
86
+ }
87
+
88
+ function listen(fn) {
89
+ if (_port) _port.on('message', fn);
90
+ else self.addEventListener('message', function(e) { fn(e.data); });
91
+ }
92
+
93
+ listen(function(data) {
94
+ if (data && data.method) {
95
+ try {
96
+ var fn = globalThis['__' + data.method];
97
+ if (typeof fn !== 'function') {
98
+ throw new Error('Unknown sandbox method: ' + data.method);
99
+ }
100
+ var result = fn(data.options);
101
+ send({id: data.id, result: result});
102
+ } catch(err) {
103
+ send({id: data.id, error: (err && err.message) || String(err)});
104
+ }
105
+ }
106
+ });
107
+
108
+ send({type: 'ready'});
109
+ })();
110
+ `;
111
+
112
+ /**
113
+ * Create the full worker script from a compiled match bundle.
114
+ * Appends the message-handling bootstrap to the bundle IIFE.
115
+ */
116
+ export function createWorkerScript(bundle: string): string
117
+ {
118
+ return bundle + '\n' + WORKER_BOOTSTRAP;
119
+ }
120
+
121
+ // ============================================================
122
+ // BROWSER MATCH SANDBOX
123
+ // ============================================================
124
+
125
+ interface PendingCall
126
+ {
127
+ resolve: (value: unknown) => void;
128
+ reject: (error: Error) => void;
129
+ timer: ReturnType<typeof setTimeout>;
130
+ }
131
+
132
+ /**
133
+ * Browser-compatible sandboxed match runner using Web Workers.
134
+ *
135
+ * Same compiled bundles as MatchSandbox (isolated-vm), but runs in a
136
+ * Web Worker instead. All fight/simulate calls are async (postMessage-based).
137
+ *
138
+ * Usage:
139
+ * ```ts
140
+ * // Bundle is compiled server-side or at build time (Node.js only)
141
+ * const bundle = await MatchSandbox.compile(bot1, bot2);
142
+ *
143
+ * // Run in browser via Web Worker
144
+ * const sandbox = await BrowserMatchSandbox.fromBundle(bundle);
145
+ * const result = await sandbox.fight({ seed: 42 });
146
+ * sandbox.dispose();
147
+ * ```
148
+ */
149
+ export class BrowserMatchSandbox
150
+ {
151
+ private worker: WorkerLike;
152
+ private workerCleanup: (() => void) | undefined;
153
+ private timeout: number;
154
+ private disposed = false;
155
+ private nextId = 0;
156
+ private pending = new Map<number, PendingCall>();
157
+ private messageHandler: ((ev: unknown) => void) | null = null;
158
+ private errorHandler: ((ev: unknown) => void) | null = null;
159
+
160
+ private constructor(
161
+ worker: WorkerLike,
162
+ workerCleanup: (() => void) | undefined,
163
+ timeout: number,
164
+ )
165
+ {
166
+ this.worker = worker;
167
+ this.workerCleanup = workerCleanup;
168
+ this.timeout = timeout;
169
+ }
170
+
171
+ /**
172
+ * Create a browser sandbox from a pre-compiled bundle string.
173
+ * The bundle should be the output of MatchSandbox.compile() (or equivalent IIFE
174
+ * that sets globalThis.__fight and globalThis.__simulate).
175
+ */
176
+ static async fromBundle(
177
+ bundle: string,
178
+ options?: BrowserSandboxOptions,
179
+ ): Promise<BrowserMatchSandbox>
180
+ {
181
+ const timeout = options?.timeoutMs ?? 30000;
182
+ const code = createWorkerScript(bundle);
183
+
184
+ let worker: WorkerLike;
185
+ let cleanup: (() => void) | undefined;
186
+
187
+ if (options?.createWorker)
188
+ {
189
+ const created = options.createWorker(code);
190
+ worker = created.worker;
191
+ cleanup = created.cleanup;
192
+ }
193
+ else
194
+ {
195
+ // Browser default: Blob URL Worker
196
+ // Uses globalThis to avoid TypeScript error in Node.js builds (no DOM lib)
197
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- runtime browser check
198
+ const BrowserWorker = (globalThis as Record<string, unknown>).Worker as
199
+ {new (url: string | URL): WorkerLike} | undefined;
200
+ if (!BrowserWorker)
201
+ {
202
+ throw new Error(
203
+ 'Web Worker API not available. Pass a createWorker factory for Node.js usage.',
204
+ );
205
+ }
206
+ const blob = new Blob([code], {type: 'application/javascript'});
207
+ const blobUrl = URL.createObjectURL(blob);
208
+ cleanup = (): void => URL.revokeObjectURL(blobUrl);
209
+ worker = new BrowserWorker(blobUrl);
210
+ }
211
+
212
+ const sandbox = new BrowserMatchSandbox(worker, cleanup, timeout);
213
+
214
+ try
215
+ {
216
+ await sandbox.waitForReady(timeout);
217
+ }
218
+ catch(error)
219
+ {
220
+ sandbox.dispose();
221
+ throw error;
222
+ }
223
+
224
+ return sandbox;
225
+ }
226
+
227
+ /**
228
+ * Wait for the worker to post {type: 'ready'}, then attach permanent handlers.
229
+ */
230
+ private waitForReady(timeout: number): Promise<void>
231
+ {
232
+ return new Promise<void>((resolve, reject) =>
233
+ {
234
+ const timer = setTimeout(() =>
235
+ {
236
+ reject(new Error('Worker initialization timed out'));
237
+ }, timeout);
238
+
239
+ const readyListener = (ev: unknown): void =>
240
+ {
241
+ // Unwrap MessageEvent (browser) or raw data (Node.js adapter)
242
+ const data = this.unwrapEvent(ev);
243
+ if (data && typeof data === 'object' && 'type' in data && data.type === 'ready')
244
+ {
245
+ clearTimeout(timer);
246
+ this.worker.removeEventListener('message', readyListener);
247
+ this.attachHandlers();
248
+ resolve();
249
+ }
250
+ };
251
+
252
+ this.worker.addEventListener('message', readyListener);
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Extract message data from a browser MessageEvent or raw Node.js data.
258
+ */
259
+ private unwrapEvent(ev: unknown): Record<string, unknown> | null
260
+ {
261
+ if (ev == null) return null;
262
+ if (typeof ev !== 'object') return null;
263
+
264
+ // Browser MessageEvent has .data
265
+ const maybeEvent = ev as {data?: unknown};
266
+ const raw = maybeEvent.data !== undefined ? maybeEvent.data : ev;
267
+
268
+ if (raw != null && typeof raw === 'object')
269
+ {
270
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- runtime type-checked above
271
+ return raw as Record<string, unknown>;
272
+ }
273
+ return null;
274
+ }
275
+
276
+ /**
277
+ * Attach permanent message and error handlers for fight/simulate responses.
278
+ */
279
+ private attachHandlers(): void
280
+ {
281
+ this.messageHandler = (ev: unknown): void =>
282
+ {
283
+ const data = this.unwrapEvent(ev);
284
+ if (!data || data.id == null) return;
285
+
286
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- id is always a number from our protocol
287
+ const id = data.id as number;
288
+ const handler = this.pending.get(id);
289
+ if (!handler) return;
290
+
291
+ clearTimeout(handler.timer);
292
+ this.pending.delete(id);
293
+
294
+ if (data.error)
295
+ {
296
+ handler.reject(new Error(String(data.error)));
297
+ }
298
+ else
299
+ {
300
+ handler.resolve(data.result);
301
+ }
302
+ };
303
+
304
+ this.errorHandler = (ev: unknown): void =>
305
+ {
306
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ErrorEvent shape from Worker
307
+ const message = (ev as {message?: string}).message ?? 'Worker error';
308
+ // Reject all pending calls
309
+ for (const [, handler] of this.pending)
310
+ {
311
+ clearTimeout(handler.timer);
312
+ handler.reject(new Error(message));
313
+ }
314
+ this.pending.clear();
315
+ };
316
+
317
+ this.worker.addEventListener('message', this.messageHandler);
318
+ this.worker.addEventListener('error', this.errorHandler);
319
+ }
320
+
321
+ // ============================================================
322
+ // PUBLIC API
323
+ // ============================================================
324
+
325
+ /**
326
+ * Run a full fight (10 matches: 5 spawn distances x 2 sides).
327
+ * Returns a Promise because Worker communication is async.
328
+ */
329
+ async fight(options?: {seed?: number; maxTicks?: number}): Promise<FightResult>
330
+ {
331
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- worker returns FightResult shape
332
+ return this.call('fight', options ?? {}) as Promise<FightResult>;
333
+ }
334
+
335
+ /**
336
+ * Run a single simulation. Returns a Promise.
337
+ *
338
+ * @param options.params1 - useParam overrides for bot 1 (wizard-1)
339
+ * @param options.params2 - useParam overrides for bot 2 (wizard-2)
340
+ */
341
+ async simulate(options?: {
342
+ seed?: number;
343
+ maxTicks?: number;
344
+ spawnDistance?: number;
345
+ skipHistory?: boolean;
346
+ params1?: Record<string, number>;
347
+ params2?: Record<string, number>;
348
+ }): Promise<SimulateResult>
349
+ {
350
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- worker returns SimulateResult shape
351
+ return this.call('simulate', options ?? {}) as Promise<SimulateResult>;
352
+ }
353
+
354
+ /**
355
+ * Dispose the worker and free all resources.
356
+ * The sandbox cannot be used after disposal.
357
+ */
358
+ dispose(): void
359
+ {
360
+ if (!this.disposed)
361
+ {
362
+ this.disposed = true;
363
+
364
+ // Reject all pending calls
365
+ for (const [, handler] of this.pending)
366
+ {
367
+ clearTimeout(handler.timer);
368
+ handler.reject(new Error('BrowserMatchSandbox has been disposed'));
369
+ }
370
+ this.pending.clear();
371
+
372
+ // Remove event listeners
373
+ if (this.messageHandler)
374
+ {
375
+ this.worker.removeEventListener('message', this.messageHandler);
376
+ }
377
+ if (this.errorHandler)
378
+ {
379
+ this.worker.removeEventListener('error', this.errorHandler);
380
+ }
381
+
382
+ // Terminate worker
383
+ this.worker.terminate();
384
+
385
+ // Clean up Blob URL (or other resources)
386
+ this.workerCleanup?.();
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Whether this sandbox has been disposed.
392
+ */
393
+ get isDisposed(): boolean
394
+ {
395
+ return this.disposed;
396
+ }
397
+
398
+ // ============================================================
399
+ // INTERNAL
400
+ // ============================================================
401
+
402
+ /**
403
+ * Send a generic method call to the worker. Used by sibling sandboxes
404
+ * (e.g. BrowserManualMatchSandbox) that need to dispatch to method
405
+ * names other than fight/simulate. The worker bootstrap looks up
406
+ * `globalThis['__' + method]` and calls it with `options`.
407
+ */
408
+ callRaw(method: string, options: unknown): Promise<unknown>
409
+ {
410
+ return this.call(method, options);
411
+ }
412
+
413
+ /**
414
+ * Send a method call to the worker and wait for the response.
415
+ * Times out and terminates the worker if no response within timeout.
416
+ */
417
+ private call(method: string, options: unknown): Promise<unknown>
418
+ {
419
+ this.ensureNotDisposed();
420
+ const id = this.nextId++;
421
+
422
+ return new Promise((resolve, reject) =>
423
+ {
424
+ const timer = setTimeout(() =>
425
+ {
426
+ this.pending.delete(id);
427
+ // Worker is stuck — terminate and clean up all resources
428
+ this.worker.terminate();
429
+ this.workerCleanup?.();
430
+ this.disposed = true;
431
+ reject(new Error(`BrowserMatchSandbox timed out after ${this.timeout}ms`));
432
+ }, this.timeout);
433
+
434
+ this.pending.set(id, {resolve, reject, timer});
435
+ this.worker.postMessage({id, method, options});
436
+ });
437
+ }
438
+
439
+ private ensureNotDisposed(): void
440
+ {
441
+ if (this.disposed)
442
+ {
443
+ throw new Error('BrowserMatchSandbox has been disposed');
444
+ }
445
+ }
446
+ }
447
+
448
+ // ============================================================
449
+ // CONVENIENCE FUNCTIONS
450
+ // ============================================================
451
+
452
+ /**
453
+ * One-shot browser-sandboxed fight. Creates worker, runs fight, disposes.
454
+ */
455
+ export async function browserSandboxFight(
456
+ bundle: string,
457
+ options?: {seed?: number; maxTicks?: number} & BrowserSandboxOptions,
458
+ ): Promise<FightResult>
459
+ {
460
+ const sandbox = await BrowserMatchSandbox.fromBundle(bundle, options);
461
+ try
462
+ {
463
+ return await sandbox.fight({seed: options?.seed, maxTicks: options?.maxTicks});
464
+ }
465
+ finally
466
+ {
467
+ sandbox.dispose();
468
+ }
469
+ }
470
+
471
+ /**
472
+ * One-shot browser-sandboxed simulate. Creates worker, runs simulate, disposes.
473
+ */
474
+ export async function browserSandboxSimulate(
475
+ bundle: string,
476
+ options?: {
477
+ seed?: number;
478
+ maxTicks?: number;
479
+ spawnDistance?: number;
480
+ skipHistory?: boolean;
481
+ params1?: Record<string, number>;
482
+ params2?: Record<string, number>;
483
+ } & BrowserSandboxOptions,
484
+ ): Promise<SimulateResult>
485
+ {
486
+ const sandbox = await BrowserMatchSandbox.fromBundle(bundle, options);
487
+ try
488
+ {
489
+ return await sandbox.simulate({
490
+ seed: options?.seed,
491
+ maxTicks: options?.maxTicks,
492
+ spawnDistance: options?.spawnDistance,
493
+ skipHistory: options?.skipHistory,
494
+ params1: options?.params1,
495
+ params2: options?.params2,
496
+ });
497
+ }
498
+ finally
499
+ {
500
+ sandbox.dispose();
501
+ }
502
+ }
503
+
504
+ // ============================================================
505
+ // BROWSER MANUAL-MATCH SANDBOX
506
+ // ============================================================
507
+
508
+ /**
509
+ * Options accepted by `__manualMatchInit` (worker-side). Mirrors
510
+ * `ManualMatchOptions` from manual-match.ts, but without the constructor's
511
+ * AI parameters since the player AI is a worker-local stub.
512
+ */
513
+ export interface ManualMatchInitOptions
514
+ {
515
+ seed?: number;
516
+ spawnDistance?: number;
517
+ maxTicks?: number;
518
+ initialHumanActions?: WizardActions;
519
+ }
520
+
521
+ export interface ManualMatchStepRequest
522
+ {
523
+ humanActions?: WizardActions;
524
+ humanMissileTargets?: Record<string, {x: number; y: number}>;
525
+ count?: number;
526
+ }
527
+
528
+ /**
529
+ * Long-lived Web Worker sandbox holding a single ManualMatch instance.
530
+ *
531
+ * Unlike BrowserMatchSandbox (which runs one batched fight/simulate per
532
+ * worker), BrowserManualMatchSandbox keeps the worker alive across many
533
+ * step calls so the engine state and hook state persist between ticks.
534
+ * This is what manual play mode uses: one worker per session, disposed
535
+ * when the user leaves the page or starts a new match.
536
+ *
537
+ * Usage:
538
+ * ```ts
539
+ * const sandbox = await BrowserManualMatchSandbox.fromBundle(opponentBundle);
540
+ * await sandbox.init({seed: 42});
541
+ * for (let i = 0; i < 100; i++) {
542
+ * await sandbox.step({humanActions: {move: {x: 100, y: 0}}, count: 1});
543
+ * }
544
+ * await sandbox.dispose();
545
+ * ```
546
+ */
547
+ export class BrowserManualMatchSandbox
548
+ {
549
+ private readonly inner: BrowserMatchSandbox;
550
+
551
+ private constructor(inner: BrowserMatchSandbox)
552
+ {
553
+ this.inner = inner;
554
+ }
555
+
556
+ /**
557
+ * Create a manual-match sandbox from a pre-compiled bundle. The bundle
558
+ * must be the output of `compileManualMatchBundle()` — the regular
559
+ * `compileMatchBundle()` output won't work since it doesn't expose the
560
+ * `__manualMatch*` globals.
561
+ */
562
+ static async fromBundle(
563
+ bundle: string,
564
+ options?: BrowserSandboxOptions,
565
+ ): Promise<BrowserManualMatchSandbox>
566
+ {
567
+ const inner = await BrowserMatchSandbox.fromBundle(bundle, options);
568
+ return new BrowserManualMatchSandbox(inner);
569
+ }
570
+
571
+ /**
572
+ * Initialize the worker-side ManualMatch instance.
573
+ * Returns the initial GameState (tick 0).
574
+ */
575
+ async init(options?: ManualMatchInitOptions): Promise<GameState>
576
+ {
577
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- worker returns GameState shape
578
+ return this.inner.callRaw('manualMatchInit', options ?? {}) as Promise<GameState>;
579
+ }
580
+
581
+ /**
582
+ * Advance the match by `request.count` ticks (default 1), updating
583
+ * the player's human actions and any hijacked missile targets first.
584
+ */
585
+ async step(request: ManualMatchStepRequest = {}): Promise<StepResult>
586
+ {
587
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- worker returns StepResult shape
588
+ return this.inner.callRaw('manualMatchStep', request) as Promise<StepResult>;
589
+ }
590
+
591
+ /**
592
+ * Replace a missile's AI with a worker-local hijack stub that reads
593
+ * from `latestHumanMissileTargets[projectileId]`. Subsequent step()
594
+ * calls with `humanMissileTargets` populated for this id steer the
595
+ * missile.
596
+ */
597
+ async hijackMissile(projectileId: string): Promise<void>
598
+ {
599
+ await this.inner.callRaw('manualMatchHijack', {projectileId});
600
+ }
601
+
602
+ /**
603
+ * Restore a hijacked missile's original AI.
604
+ */
605
+ async releaseMissile(projectileId: string): Promise<void>
606
+ {
607
+ await this.inner.callRaw('manualMatchRelease', {projectileId});
608
+ }
609
+
610
+ /**
611
+ * Toggle invincibility for a wizard.
612
+ */
613
+ async setInvincible(wizardIndex: 0 | 1, on: boolean): Promise<void>
614
+ {
615
+ await this.inner.callRaw('manualMatchSetInvincible', {wizardIndex, on});
616
+ }
617
+
618
+ /**
619
+ * Get the current game state without advancing.
620
+ */
621
+ async getState(): Promise<GameState>
622
+ {
623
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- worker returns GameState shape
624
+ return this.inner.callRaw('manualMatchGetState', {}) as Promise<GameState>;
625
+ }
626
+
627
+ /**
628
+ * Get the full SimulateResult-compatible result object (history + winner).
629
+ */
630
+ async getResult(): Promise<SimulateResult>
631
+ {
632
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- worker returns SimulateResult shape
633
+ return this.inner.callRaw('manualMatchGetResult', {}) as Promise<SimulateResult>;
634
+ }
635
+
636
+ /**
637
+ * Dispose the worker-side ManualMatch instance. Does NOT terminate the
638
+ * worker — call dispose() for that.
639
+ */
640
+ async resetMatch(): Promise<void>
641
+ {
642
+ await this.inner.callRaw('manualMatchDispose', {});
643
+ }
644
+
645
+ /**
646
+ * Terminate the worker and free all resources.
647
+ */
648
+ dispose(): void
649
+ {
650
+ this.inner.dispose();
651
+ }
652
+
653
+ get isDisposed(): boolean
654
+ {
655
+ return this.inner.isDisposed;
656
+ }
657
+ }
658
+
659
+ /**
660
+ * Re-import-friendly types (re-exported here so consumers don't need to
661
+ * cherry-pick from internal modules).
662
+ */
663
+ export type {
664
+ BotError,
665
+ GameState,
666
+ MatchWinner,
667
+ MissileAIFunction,
668
+ ProjectileState,
669
+ StepResult,
670
+ WizardActions,
671
+ };