@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.
- package/README.md +28 -0
- package/dist/chunk-L7Z7OFXD.js +9140 -0
- package/dist/chunk-L7Z7OFXD.js.map +1 -0
- package/dist/index-browser.d.ts +2602 -0
- package/dist/index-browser.js +407 -0
- package/dist/index-browser.js.map +1 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +750 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
- package/src/bots/berserker/01_Stormchaser.ts +457 -0
- package/src/bots/berserker/02_Stormcaller.ts +417 -0
- package/src/bots/berserker/03_Stormforger.ts +481 -0
- package/src/bots/caster/01_Flamecaller.ts +286 -0
- package/src/bots/caster/02_Pyromancer.ts +350 -0
- package/src/bots/caster/03_Infernalist.ts +492 -0
- package/src/bots/defensive/01_Turtle.ts +151 -0
- package/src/bots/defensive/02_Sentinel.ts +134 -0
- package/src/bots/defensive/03_Golem.ts +357 -0
- package/src/bots/duelist/01_Battlemage.ts +433 -0
- package/src/bots/duelist/02_Warmage.ts +438 -0
- package/src/bots/duelist/03_Archmage.ts +588 -0
- package/src/bots/homing/01_Bonemancer.ts +67 -0
- package/src/bots/homing/02_Lich.ts +356 -0
- package/src/bots/homing/03_Archlich.ts +220 -0
- package/src/bots/index.ts +30 -0
- package/src/bots/kiter/01_Spellspinner.ts +398 -0
- package/src/bots/kiter/02_Spellweaver.ts +378 -0
- package/src/bots/kiter/03_Spellbinder.ts +448 -0
- package/src/bots/melee/01_Shadowblade.ts +270 -0
- package/src/bots/melee/02_Nightblade.ts +437 -0
- package/src/bots/melee/03_Voidblade.ts +582 -0
- package/src/bots/registry.ts +207 -0
- package/src/bots/shared.ts +472 -0
- package/src/bots/sniper/01_Spellshot.ts +385 -0
- package/src/bots/sniper/02_Spelltracer.ts +441 -0
- package/src/bots/sniper/03_Spellseeker.ts +546 -0
- package/src/bots/standalone/Critter.ts +89 -0
- package/src/bots/standalone/Doombringer.ts +91 -0
- package/src/bots/standalone/Hogger.ts +228 -0
- package/src/bots/standalone/Rookie.ts +50 -0
- package/src/bots/standalone/TargetDummy.ts +21 -0
- package/src/bots/test/cheater.ts +405 -0
- package/src/bots/test/crasher.ts +81 -0
- package/src/engine/hooks-runtime.ts +394 -0
- package/src/engine/manual-match.ts +289 -0
- package/src/engine/missile-templates.ts +155 -0
- package/src/engine/optimizer.ts +220 -0
- package/src/engine/params-runtime.ts +189 -0
- package/src/engine/physics.ts +143 -0
- package/src/engine/sandbox-browser.ts +671 -0
- package/src/engine/sandbox-compile.ts +197 -0
- package/src/engine/sandbox-harness.ts +367 -0
- package/src/engine/sandbox.ts +332 -0
- package/src/engine/simulation.ts +828 -0
- package/src/engine/spells.ts +128 -0
- package/src/engine-version.ts +11 -0
- package/src/hooks/action-builders.ts +210 -0
- package/src/hooks/bot-wrapper.ts +84 -0
- package/src/hooks/index.ts +75 -0
- package/src/hooks/state-hooks.ts +354 -0
- package/src/hooks/threat-analysis.ts +365 -0
- package/src/hooks/types.ts +142 -0
- package/src/index-browser.ts +30 -0
- package/src/index.ts +24 -0
- package/src/rules.ts +254 -0
- package/src/stats.ts +262 -0
- package/src/testing.ts +207 -0
- package/src/trace.ts +430 -0
- package/src/types.ts +193 -0
- package/src/utils/angles.ts +47 -0
- package/src/utils/combat.ts +279 -0
- package/src/utils/distance.ts +21 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/movement.ts +108 -0
- package/src/utils/random.ts +65 -0
- package/src/utils/spatial.ts +63 -0
- 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
|
+
}
|