vibemancer 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/cli.js +1193 -0
- package/dist/cli.js.map +1 -0
- package/package.json +43 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/dev.ts
|
|
4
|
+
import { execFile } from "child_process";
|
|
5
|
+
|
|
6
|
+
// src/bot-discovery.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
async function discoverBot(projectDir, overridePath) {
|
|
10
|
+
const absDir = path.resolve(projectDir);
|
|
11
|
+
if (overridePath) {
|
|
12
|
+
const absPath = path.resolve(absDir, overridePath);
|
|
13
|
+
if (!fs.existsSync(absPath)) {
|
|
14
|
+
throw new Error(`Bot file not found: ${absPath}`);
|
|
15
|
+
}
|
|
16
|
+
const exportName = await findExportName(absPath);
|
|
17
|
+
return { sourcePath: absPath, exportName };
|
|
18
|
+
}
|
|
19
|
+
const configPath = path.join(absDir, "vibemancer.json");
|
|
20
|
+
if (fs.existsSync(configPath)) {
|
|
21
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
22
|
+
let config;
|
|
23
|
+
try {
|
|
24
|
+
config = JSON.parse(raw);
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(`Invalid JSON in vibemancer.json: ${configPath}`);
|
|
27
|
+
}
|
|
28
|
+
if (config.bot !== null && config.bot !== void 0 && typeof config.bot !== "string") {
|
|
29
|
+
throw new Error(`Invalid "bot" field in vibemancer.json: expected string, got ${typeof config.bot}`);
|
|
30
|
+
}
|
|
31
|
+
if (config.export !== null && config.export !== void 0 && typeof config.export !== "string") {
|
|
32
|
+
throw new Error(`Invalid "export" field in vibemancer.json: expected string, got ${typeof config.export}`);
|
|
33
|
+
}
|
|
34
|
+
if (config.bot) {
|
|
35
|
+
const botPath = path.resolve(absDir, config.bot);
|
|
36
|
+
if (!fs.existsSync(botPath)) {
|
|
37
|
+
throw new Error(`Bot file from vibemancer.json not found: ${botPath}`);
|
|
38
|
+
}
|
|
39
|
+
const exportName = config.export || await findExportName(botPath);
|
|
40
|
+
return { sourcePath: botPath, exportName };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const defaultPath = path.join(absDir, "src", "bot.ts");
|
|
44
|
+
if (fs.existsSync(defaultPath)) {
|
|
45
|
+
const exportName = await findExportName(defaultPath);
|
|
46
|
+
return { sourcePath: defaultPath, exportName };
|
|
47
|
+
}
|
|
48
|
+
throw new Error(
|
|
49
|
+
'Could not find bot source file.\nCreate src/bot.ts or add a vibemancer.json with {"bot": "path/to/bot.ts"}'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
async function findExportName(filePath) {
|
|
53
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
54
|
+
const match = content.match(/export\s+(?:function|const|class)\s+([A-Z]\w*)/);
|
|
55
|
+
if (match?.[1]) {
|
|
56
|
+
return match[1];
|
|
57
|
+
}
|
|
58
|
+
const reExport = content.match(/export\s*\{\s*([A-Z]\w*)/);
|
|
59
|
+
if (reExport?.[1]) {
|
|
60
|
+
return reExport[1];
|
|
61
|
+
}
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Could not find a named export in ${filePath}.
|
|
64
|
+
Bot export must start with a capital letter (PascalCase).
|
|
65
|
+
Example: export function MyWizard() { ... }`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
var SCAN_SKIP_FILE_PATTERNS = [
|
|
69
|
+
/\.d\.ts$/,
|
|
70
|
+
/\.test\.tsx?$/,
|
|
71
|
+
/\.spec\.tsx?$/
|
|
72
|
+
];
|
|
73
|
+
var SCAN_SKIP_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
74
|
+
"node_modules",
|
|
75
|
+
"dist",
|
|
76
|
+
"build",
|
|
77
|
+
".cache",
|
|
78
|
+
".turbo",
|
|
79
|
+
"__tests__"
|
|
80
|
+
]);
|
|
81
|
+
var SCAN_SKIP_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
82
|
+
"index.ts",
|
|
83
|
+
"index.tsx",
|
|
84
|
+
"types.ts",
|
|
85
|
+
"types.tsx",
|
|
86
|
+
"helpers.ts",
|
|
87
|
+
"helpers.tsx"
|
|
88
|
+
]);
|
|
89
|
+
function listTsFilesRecursively(rootDir) {
|
|
90
|
+
const out = [];
|
|
91
|
+
const stack = [rootDir];
|
|
92
|
+
while (stack.length > 0) {
|
|
93
|
+
const dir = stack.pop();
|
|
94
|
+
if (dir === void 0) continue;
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const full = path.join(dir, entry.name);
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
if (SCAN_SKIP_DIR_NAMES.has(entry.name)) continue;
|
|
105
|
+
if (entry.name.startsWith(".")) continue;
|
|
106
|
+
stack.push(full);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!entry.isFile()) continue;
|
|
110
|
+
if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".tsx")) continue;
|
|
111
|
+
if (SCAN_SKIP_FILE_NAMES.has(entry.name)) continue;
|
|
112
|
+
if (SCAN_SKIP_FILE_PATTERNS.some((re) => re.test(entry.name))) continue;
|
|
113
|
+
out.push(full);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
async function discoverAllBots(projectDir) {
|
|
119
|
+
const absDir = path.resolve(projectDir);
|
|
120
|
+
const srcDir = path.join(absDir, "src");
|
|
121
|
+
if (!fs.existsSync(srcDir)) return [];
|
|
122
|
+
const files = listTsFilesRecursively(srcDir);
|
|
123
|
+
const bots = [];
|
|
124
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
let exportName;
|
|
127
|
+
try {
|
|
128
|
+
exportName = await findExportName(file);
|
|
129
|
+
} catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (seenNames.has(exportName)) continue;
|
|
133
|
+
seenNames.add(exportName);
|
|
134
|
+
bots.push({ sourcePath: file, exportName });
|
|
135
|
+
}
|
|
136
|
+
bots.sort((a, b) => a.exportName.localeCompare(b.exportName));
|
|
137
|
+
return bots;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/server.ts
|
|
141
|
+
import http from "http";
|
|
142
|
+
|
|
143
|
+
// src/compile-single-bot.ts
|
|
144
|
+
import { build } from "esbuild";
|
|
145
|
+
|
|
146
|
+
// src/opponent-resolver.ts
|
|
147
|
+
import path2 from "path";
|
|
148
|
+
import fs2 from "fs";
|
|
149
|
+
import { fileURLToPath } from "url";
|
|
150
|
+
import { BotBundle } from "@vibemancer/core";
|
|
151
|
+
function findCoreSourceDir() {
|
|
152
|
+
const monorepoCore = path2.resolve(import.meta.dirname, "..", "..", "core", "src");
|
|
153
|
+
if (fs2.existsSync(path2.join(monorepoCore, "engine", "simulation.ts"))) {
|
|
154
|
+
return monorepoCore;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const coreIndex = import.meta.resolve("@vibemancer/core");
|
|
158
|
+
const coreIndexPath = fileURLToPath(coreIndex);
|
|
159
|
+
const coreRoot = path2.resolve(path2.dirname(coreIndexPath), "..");
|
|
160
|
+
const srcDir = path2.join(coreRoot, "src");
|
|
161
|
+
if (fs2.existsSync(path2.join(srcDir, "engine", "simulation.ts"))) {
|
|
162
|
+
return srcDir;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
throw new Error(
|
|
167
|
+
"Could not find @vibemancer/core source directory.\nEnsure @vibemancer/core is installed and includes its src/ directory."
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
var BUILTIN_BOTS = {
|
|
171
|
+
// Standalone
|
|
172
|
+
TargetDummy: "standalone/TargetDummy.ts",
|
|
173
|
+
Critter: "standalone/Critter.ts",
|
|
174
|
+
Rookie: "standalone/Rookie.ts",
|
|
175
|
+
Hogger: "standalone/Hogger.ts",
|
|
176
|
+
Doombringer: "standalone/Doombringer.ts",
|
|
177
|
+
// Defensive
|
|
178
|
+
Turtle: "defensive/01_Turtle.ts",
|
|
179
|
+
Sentinel: "defensive/02_Sentinel.ts",
|
|
180
|
+
Golem: "defensive/03_Golem.ts",
|
|
181
|
+
// Melee
|
|
182
|
+
Shadowblade: "melee/01_Shadowblade.ts",
|
|
183
|
+
Nightblade: "melee/02_Nightblade.ts",
|
|
184
|
+
Voidblade: "melee/03_Voidblade.ts",
|
|
185
|
+
// Homing
|
|
186
|
+
Bonemancer: "homing/01_Bonemancer.ts",
|
|
187
|
+
Lich: "homing/02_Lich.ts",
|
|
188
|
+
Archlich: "homing/03_Archlich.ts",
|
|
189
|
+
// Caster
|
|
190
|
+
Flamecaller: "caster/01_Flamecaller.ts",
|
|
191
|
+
Pyromancer: "caster/02_Pyromancer.ts",
|
|
192
|
+
Infernalist: "caster/03_Infernalist.ts",
|
|
193
|
+
// Sniper
|
|
194
|
+
Spellshot: "sniper/01_Spellshot.ts",
|
|
195
|
+
Spelltracer: "sniper/02_Spelltracer.ts",
|
|
196
|
+
Spellseeker: "sniper/03_Spellseeker.ts",
|
|
197
|
+
// Duelist
|
|
198
|
+
Battlemage: "duelist/01_Battlemage.ts",
|
|
199
|
+
Warmage: "duelist/02_Warmage.ts",
|
|
200
|
+
Archmage: "duelist/03_Archmage.ts",
|
|
201
|
+
// Berserker
|
|
202
|
+
Stormchaser: "berserker/01_Stormchaser.ts",
|
|
203
|
+
Stormcaller: "berserker/02_Stormcaller.ts",
|
|
204
|
+
Stormforger: "berserker/03_Stormforger.ts",
|
|
205
|
+
// Kiter
|
|
206
|
+
Spellspinner: "kiter/01_Spellspinner.ts",
|
|
207
|
+
Spellweaver: "kiter/02_Spellweaver.ts",
|
|
208
|
+
Spellbinder: "kiter/03_Spellbinder.ts"
|
|
209
|
+
};
|
|
210
|
+
function getCoreCompileOptions() {
|
|
211
|
+
const coreSourceDir = findCoreSourceDir();
|
|
212
|
+
return {
|
|
213
|
+
alias: {
|
|
214
|
+
"@vibemancer/core": coreSourceDir + "/index-browser.ts"
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function getBuiltinBotNames() {
|
|
219
|
+
return Object.keys(BUILTIN_BOTS);
|
|
220
|
+
}
|
|
221
|
+
function resolveOpponent(name) {
|
|
222
|
+
const relativePath = BUILTIN_BOTS[name];
|
|
223
|
+
if (!relativePath) {
|
|
224
|
+
const available = Object.keys(BUILTIN_BOTS).join(", ");
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Unknown opponent "${name}".
|
|
227
|
+
Available built-in bots: ${available}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const coreSourceDir = findCoreSourceDir();
|
|
231
|
+
const sourcePath = path2.join(coreSourceDir, "bots", relativePath);
|
|
232
|
+
if (!fs2.existsSync(sourcePath)) {
|
|
233
|
+
throw new Error(`Built-in bot source not found: ${sourcePath}`);
|
|
234
|
+
}
|
|
235
|
+
return new BotBundle(sourcePath, name);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/compile-single-bot.ts
|
|
239
|
+
async function compileSingleBotBundle(sourcePath, exportName) {
|
|
240
|
+
const coreSourceDir = findCoreSourceDir();
|
|
241
|
+
const result = await build({
|
|
242
|
+
entryPoints: [sourcePath],
|
|
243
|
+
bundle: true,
|
|
244
|
+
write: false,
|
|
245
|
+
format: "iife",
|
|
246
|
+
globalName: "__botExport",
|
|
247
|
+
platform: "neutral",
|
|
248
|
+
target: "es2022",
|
|
249
|
+
logLevel: "error",
|
|
250
|
+
footer: { js: `globalThis.__injectedBot1 = __botExport.${exportName};` },
|
|
251
|
+
external: [
|
|
252
|
+
"isolated-vm",
|
|
253
|
+
"esbuild",
|
|
254
|
+
"node:*"
|
|
255
|
+
],
|
|
256
|
+
alias: {
|
|
257
|
+
"@vibemancer/core": coreSourceDir + "/index-browser.ts"
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
if (!result.outputFiles?.[0]) {
|
|
261
|
+
throw new Error("esbuild produced no output");
|
|
262
|
+
}
|
|
263
|
+
return result.outputFiles[0].text;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/server.ts
|
|
267
|
+
function botInfoToWire(info) {
|
|
268
|
+
return {
|
|
269
|
+
name: info.exportName,
|
|
270
|
+
exportName: info.exportName,
|
|
271
|
+
sourcePath: info.sourcePath
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function startServer(options) {
|
|
275
|
+
const { port, projectDir } = options;
|
|
276
|
+
async function loadBots() {
|
|
277
|
+
return discoverAllBots(projectDir);
|
|
278
|
+
}
|
|
279
|
+
const server = http.createServer(async (req, res) => {
|
|
280
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
281
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
282
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
283
|
+
if (req.method === "OPTIONS") {
|
|
284
|
+
res.writeHead(204);
|
|
285
|
+
res.end();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
289
|
+
const pathname = url.pathname;
|
|
290
|
+
try {
|
|
291
|
+
if (pathname === "/health") {
|
|
292
|
+
respond(res, 200, { status: "ok" });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (pathname === "/local-bots") {
|
|
296
|
+
const bots = await loadBots();
|
|
297
|
+
const body = { bots: bots.map(botInfoToWire) };
|
|
298
|
+
respond(res, 200, body);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const bundleMatch = /^\/local-bots\/([A-Za-z_][A-Za-z0-9_]*)\/bundle$/.exec(pathname);
|
|
302
|
+
if (bundleMatch) {
|
|
303
|
+
const requestedName = bundleMatch[1];
|
|
304
|
+
const bots = await loadBots();
|
|
305
|
+
const target = bots.find((b) => b.exportName === requestedName);
|
|
306
|
+
if (!target) {
|
|
307
|
+
respond(res, 404, { error: `No local bot named "${requestedName}". Found: ${bots.map((b) => b.exportName).join(", ") || "(none)"}` });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const start = Date.now();
|
|
311
|
+
const bundle = await compileSingleBotBundle(target.sourcePath, target.exportName);
|
|
312
|
+
const elapsed = Date.now() - start;
|
|
313
|
+
console.log(` Compiled ${target.exportName} (${(bundle.length / 1024).toFixed(1)} KB) in ${elapsed}ms`);
|
|
314
|
+
res.writeHead(200, { "Content-Type": "text/javascript" });
|
|
315
|
+
res.end(bundle);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
respond(res, 404, { error: `Not found: ${pathname}` });
|
|
319
|
+
} catch (error) {
|
|
320
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
321
|
+
console.error(` Error: ${message}`);
|
|
322
|
+
respond(res, 500, { error: message });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
server.listen(port, () => {
|
|
326
|
+
console.log(`
|
|
327
|
+
Vibemancer dev server running at http://localhost:${port}`);
|
|
328
|
+
void (async () => {
|
|
329
|
+
const bots = await loadBots();
|
|
330
|
+
if (bots.length === 0) {
|
|
331
|
+
console.log(" (no bots discovered in src/ \u2014 add a .ts file with a PascalCase export)");
|
|
332
|
+
} else {
|
|
333
|
+
console.log(` Bots: ${bots.map((b) => b.exportName).join(", ")}`);
|
|
334
|
+
}
|
|
335
|
+
console.log("");
|
|
336
|
+
console.log("Open your browser to play:");
|
|
337
|
+
console.log(` https://vibemancer.com/#botserver=localhost:${port}
|
|
338
|
+
`);
|
|
339
|
+
console.log("Endpoints:");
|
|
340
|
+
console.log(" GET /local-bots - List of locally-discovered bots");
|
|
341
|
+
console.log(" GET /local-bots/<name>/bundle - Compile a single bot to a sandbox-ready bundle");
|
|
342
|
+
console.log(" GET /health - Server health check\n");
|
|
343
|
+
})();
|
|
344
|
+
});
|
|
345
|
+
return server;
|
|
346
|
+
}
|
|
347
|
+
function respond(res, status, data) {
|
|
348
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
349
|
+
res.end(JSON.stringify(data));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/commands/dev.ts
|
|
353
|
+
async function runDev(options) {
|
|
354
|
+
const projectDir = process.cwd();
|
|
355
|
+
const bots = await discoverAllBots(projectDir);
|
|
356
|
+
if (bots.length === 0) {
|
|
357
|
+
console.log("No bots discovered in src/. Add a .ts file with a PascalCase export, e.g.:");
|
|
358
|
+
console.log(" export function MyWizard(props) { return {move: {x: 0, y: 0}}; }");
|
|
359
|
+
} else {
|
|
360
|
+
console.log(`Discovered ${bots.length} bot${bots.length === 1 ? "" : "s"}: ${bots.map((b) => b.exportName).join(", ")}`);
|
|
361
|
+
}
|
|
362
|
+
const server = startServer({
|
|
363
|
+
port: options.port,
|
|
364
|
+
projectDir
|
|
365
|
+
});
|
|
366
|
+
server.once("listening", () => {
|
|
367
|
+
const url = `https://vibemancer.com/#botserver=localhost:${options.port}`;
|
|
368
|
+
openBrowser(url);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function openBrowser(url) {
|
|
372
|
+
const platform = process.platform;
|
|
373
|
+
try {
|
|
374
|
+
if (platform === "darwin") {
|
|
375
|
+
execFile("open", [url], () => {
|
|
376
|
+
});
|
|
377
|
+
} else if (platform === "win32") {
|
|
378
|
+
execFile("cmd", ["/c", "start", "", url], () => {
|
|
379
|
+
});
|
|
380
|
+
} else {
|
|
381
|
+
execFile("xdg-open", [url], (err) => {
|
|
382
|
+
if (err) execFile("wslview", [url], () => {
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/commands/test.ts
|
|
391
|
+
import { execFileSync } from "child_process";
|
|
392
|
+
async function runTest(_options) {
|
|
393
|
+
console.log("\n Running tests...\n");
|
|
394
|
+
try {
|
|
395
|
+
execFileSync("npx", ["vitest", "run"], {
|
|
396
|
+
stdio: "inherit",
|
|
397
|
+
cwd: process.cwd()
|
|
398
|
+
});
|
|
399
|
+
} catch {
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/commands/fight.ts
|
|
405
|
+
import fs3 from "fs";
|
|
406
|
+
import path3 from "path";
|
|
407
|
+
import { BotBundle as BotBundle2, sandboxFight, scoreFight } from "@vibemancer/core";
|
|
408
|
+
async function runFight(options) {
|
|
409
|
+
if (options.opponent) {
|
|
410
|
+
return runSingleFight({ ...options, opponent: options.opponent });
|
|
411
|
+
}
|
|
412
|
+
return runFullFight(options);
|
|
413
|
+
}
|
|
414
|
+
async function runSingleFight(options) {
|
|
415
|
+
const projectDir = process.cwd();
|
|
416
|
+
const botInfo = await discoverBot(projectDir, options.bot);
|
|
417
|
+
console.log(`
|
|
418
|
+
Bot: ${botInfo.exportName}`);
|
|
419
|
+
console.log(` Opponent: ${options.opponent}
|
|
420
|
+
`);
|
|
421
|
+
const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
|
|
422
|
+
const opponentBundle = resolveOpponent(options.opponent);
|
|
423
|
+
const start = Date.now();
|
|
424
|
+
const result = await sandboxFight(userBundle, opponentBundle, {
|
|
425
|
+
seed: options.seed,
|
|
426
|
+
compileOptions: getCoreCompileOptions()
|
|
427
|
+
});
|
|
428
|
+
const elapsed = Date.now() - start;
|
|
429
|
+
const { wizard1Wins, wizard2Wins, draws } = result;
|
|
430
|
+
const total = wizard1Wins + wizard2Wins + draws;
|
|
431
|
+
const outcome = result.winner === "wizard-1" ? "WIN" : result.winner === "wizard-2" ? "LOSS" : "DRAW";
|
|
432
|
+
console.log(` Result: ${outcome}`);
|
|
433
|
+
console.log(` ${botInfo.exportName}: ${wizard1Wins}W | ${options.opponent}: ${wizard2Wins}W | Draws: ${draws}`);
|
|
434
|
+
console.log(` (${total} matches in ${elapsed}ms)
|
|
435
|
+
`);
|
|
436
|
+
if (result.winner === "wizard-2") {
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function runFullFight(options) {
|
|
441
|
+
const projectDir = process.cwd();
|
|
442
|
+
const botInfo = await discoverBot(projectDir, options.bot);
|
|
443
|
+
const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
|
|
444
|
+
const opponents = getBuiltinBotNames();
|
|
445
|
+
console.log(`
|
|
446
|
+
Fighting ${botInfo.exportName} against ${opponents.length} built-in bots...
|
|
447
|
+
`);
|
|
448
|
+
const entries = [];
|
|
449
|
+
const overallStart = Date.now();
|
|
450
|
+
for (const opponentName of opponents) {
|
|
451
|
+
const opponentBundle = resolveOpponent(opponentName);
|
|
452
|
+
const start = Date.now();
|
|
453
|
+
const result = await sandboxFight(userBundle, opponentBundle, {
|
|
454
|
+
seed: options.seed,
|
|
455
|
+
compileOptions: getCoreCompileOptions()
|
|
456
|
+
});
|
|
457
|
+
const elapsed = Date.now() - start;
|
|
458
|
+
const score = scoreFight(result);
|
|
459
|
+
entries.push({
|
|
460
|
+
opponent: opponentName,
|
|
461
|
+
winner: result.winner,
|
|
462
|
+
wizard1Wins: result.wizard1Wins,
|
|
463
|
+
wizard2Wins: result.wizard2Wins,
|
|
464
|
+
draws: result.draws,
|
|
465
|
+
score,
|
|
466
|
+
elapsedMs: elapsed
|
|
467
|
+
});
|
|
468
|
+
const outcome = result.winner === "wizard-1" ? "W" : result.winner === "wizard-2" ? "L" : "D";
|
|
469
|
+
const pad = opponentName.padEnd(14);
|
|
470
|
+
console.log(` ${pad} ${outcome} ${result.wizard1Wins}-${result.wizard2Wins}-${result.draws} (${elapsed}ms)`);
|
|
471
|
+
}
|
|
472
|
+
const totalElapsed = Date.now() - overallStart;
|
|
473
|
+
const wins = entries.filter((e) => e.winner === "wizard-1").length;
|
|
474
|
+
const losses = entries.filter((e) => e.winner === "wizard-2").length;
|
|
475
|
+
const drawCount = entries.filter((e) => e.winner === "draw").length;
|
|
476
|
+
const totalScore = entries.reduce((sum, e) => sum + e.score, 0);
|
|
477
|
+
const maxScore = opponents.length * 17.5;
|
|
478
|
+
console.log(`
|
|
479
|
+
Summary: ${wins}W ${losses}L ${drawCount}D out of ${opponents.length} opponents`);
|
|
480
|
+
console.log(` Score: ${totalScore.toFixed(1)} / ${maxScore.toFixed(1)} (${(totalScore / maxScore * 100).toFixed(1)}%)`);
|
|
481
|
+
console.log(` Total time: ${(totalElapsed / 1e3).toFixed(1)}s`);
|
|
482
|
+
const history = loadHistory(projectDir);
|
|
483
|
+
const previous = history.length > 0 ? history[history.length - 1] : null;
|
|
484
|
+
if (previous && previous.botName === botInfo.exportName) {
|
|
485
|
+
showDiff(entries, previous.results);
|
|
486
|
+
}
|
|
487
|
+
const current = {
|
|
488
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
489
|
+
botName: botInfo.exportName,
|
|
490
|
+
results: entries,
|
|
491
|
+
summary: { wins, losses, draws: drawCount, score: totalScore, maxScore }
|
|
492
|
+
};
|
|
493
|
+
saveHistory(projectDir, history, current);
|
|
494
|
+
console.log("");
|
|
495
|
+
}
|
|
496
|
+
function getHistoryPath(projectDir) {
|
|
497
|
+
return path3.join(projectDir, ".vibemancer", "history.json");
|
|
498
|
+
}
|
|
499
|
+
function loadHistory(projectDir) {
|
|
500
|
+
const historyPath = getHistoryPath(projectDir);
|
|
501
|
+
if (!fs3.existsSync(historyPath)) return [];
|
|
502
|
+
try {
|
|
503
|
+
const raw = fs3.readFileSync(historyPath, "utf-8");
|
|
504
|
+
return JSON.parse(raw);
|
|
505
|
+
} catch {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function saveHistory(projectDir, history, current) {
|
|
510
|
+
const historyPath = getHistoryPath(projectDir);
|
|
511
|
+
const dir = path3.dirname(historyPath);
|
|
512
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
513
|
+
const updated = [...history.slice(-19), current];
|
|
514
|
+
fs3.writeFileSync(historyPath, JSON.stringify(updated, null, " ") + "\n");
|
|
515
|
+
}
|
|
516
|
+
function showDiff(current, previous) {
|
|
517
|
+
const prevMap = new Map(previous.map((e) => [e.opponent, e]));
|
|
518
|
+
const changes = [];
|
|
519
|
+
for (const entry of current) {
|
|
520
|
+
const prev = prevMap.get(entry.opponent);
|
|
521
|
+
if (!prev) continue;
|
|
522
|
+
const prevOutcome = prev.winner === "wizard-1" ? "W" : prev.winner === "wizard-2" ? "L" : "D";
|
|
523
|
+
const curOutcome = entry.winner === "wizard-1" ? "W" : entry.winner === "wizard-2" ? "L" : "D";
|
|
524
|
+
if (prevOutcome !== curOutcome) {
|
|
525
|
+
changes.push(` ${entry.opponent.padEnd(14)} ${prevOutcome} -> ${curOutcome}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const prevScore = previous.reduce((sum, e) => sum + e.score, 0);
|
|
529
|
+
const curScore = current.reduce((sum, e) => sum + e.score, 0);
|
|
530
|
+
const diff = curScore - prevScore;
|
|
531
|
+
if (changes.length > 0 || Math.abs(diff) > 0.1) {
|
|
532
|
+
console.log("\n vs last run:");
|
|
533
|
+
if (Math.abs(diff) > 0.1) {
|
|
534
|
+
const sign = diff > 0 ? "+" : "";
|
|
535
|
+
console.log(` Score: ${sign}${diff.toFixed(1)}`);
|
|
536
|
+
}
|
|
537
|
+
for (const change of changes) {
|
|
538
|
+
console.log(change);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/commands/trace.ts
|
|
544
|
+
import {
|
|
545
|
+
BotBundle as BotBundle3,
|
|
546
|
+
sandboxSimulate,
|
|
547
|
+
extractTraceEvents,
|
|
548
|
+
summarizeTrace,
|
|
549
|
+
formatTraceEvents,
|
|
550
|
+
formatTraceSummary,
|
|
551
|
+
diagnoseTrace,
|
|
552
|
+
formatDiagnosis,
|
|
553
|
+
extractStats,
|
|
554
|
+
formatStats
|
|
555
|
+
} from "@vibemancer/core";
|
|
556
|
+
async function runTrace(options) {
|
|
557
|
+
const projectDir = process.cwd();
|
|
558
|
+
const botInfo = await discoverBot(projectDir, options.bot);
|
|
559
|
+
const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
|
|
560
|
+
const opponentBundle = resolveOpponent(options.opponent);
|
|
561
|
+
const distance = options.distance ?? 600;
|
|
562
|
+
console.log(`
|
|
563
|
+
Trace: ${botInfo.exportName} (W1) vs ${options.opponent} (W2) | distance: ${distance} | seed: ${options.seed ?? 1}
|
|
564
|
+
`);
|
|
565
|
+
const result = await sandboxSimulate(userBundle, opponentBundle, {
|
|
566
|
+
seed: options.seed ?? 1,
|
|
567
|
+
spawnDistance: distance,
|
|
568
|
+
maxTicks: options.maxTicks ?? 3e3,
|
|
569
|
+
compileOptions: getCoreCompileOptions()
|
|
570
|
+
});
|
|
571
|
+
const history = result.history;
|
|
572
|
+
if (history.length === 0) {
|
|
573
|
+
console.log(" No history available.\n");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const events = extractTraceEvents(history, result.errors);
|
|
577
|
+
console.log(formatTraceEvents(events));
|
|
578
|
+
const summary = summarizeTrace(events, result, botInfo.exportName, options.opponent);
|
|
579
|
+
console.log("\n" + formatTraceSummary(summary));
|
|
580
|
+
const stats = extractStats(result);
|
|
581
|
+
console.log("");
|
|
582
|
+
console.log(formatStats(stats, botInfo.exportName));
|
|
583
|
+
const tips = diagnoseTrace(events, summary);
|
|
584
|
+
if (tips.length > 0) {
|
|
585
|
+
console.log("");
|
|
586
|
+
console.log(formatDiagnosis(tips));
|
|
587
|
+
}
|
|
588
|
+
console.log("");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/commands/tournament.ts
|
|
592
|
+
import { BotBundle as BotBundle4, sandboxFight as sandboxFight2, scoreFight as scoreFight2, scoreFightAsWizard2 } from "@vibemancer/core";
|
|
593
|
+
async function runTournament(options) {
|
|
594
|
+
const projectDir = process.cwd();
|
|
595
|
+
const botInfo = await discoverBot(projectDir, options.bot);
|
|
596
|
+
const userBundle = new BotBundle4(botInfo.sourcePath, botInfo.exportName);
|
|
597
|
+
const opponentNames = options.opponents && options.opponents.length > 0 ? options.opponents : getBuiltinBotNames();
|
|
598
|
+
const participants = [
|
|
599
|
+
{ name: botInfo.exportName, bundle: userBundle }
|
|
600
|
+
];
|
|
601
|
+
for (const name of opponentNames) {
|
|
602
|
+
participants.push({ name, bundle: resolveOpponent(name) });
|
|
603
|
+
}
|
|
604
|
+
console.log(`
|
|
605
|
+
Tournament: ${participants.length} participants (${participants.length * (participants.length - 1) / 2} pairings)
|
|
606
|
+
`);
|
|
607
|
+
const pairings = [];
|
|
608
|
+
for (let i = 0; i < participants.length; i++) {
|
|
609
|
+
for (let j = i + 1; j < participants.length; j++) {
|
|
610
|
+
pairings.push({
|
|
611
|
+
bot1Name: participants[i].name,
|
|
612
|
+
bot2Name: participants[j].name,
|
|
613
|
+
bot1Bundle: participants[i].bundle,
|
|
614
|
+
bot2Bundle: participants[j].bundle
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const results = [];
|
|
619
|
+
const overallStart = Date.now();
|
|
620
|
+
for (const pairing of pairings) {
|
|
621
|
+
const result = await sandboxFight2(pairing.bot1Bundle, pairing.bot2Bundle, {
|
|
622
|
+
compileOptions: getCoreCompileOptions()
|
|
623
|
+
});
|
|
624
|
+
results.push({
|
|
625
|
+
bot1Name: pairing.bot1Name,
|
|
626
|
+
bot2Name: pairing.bot2Name,
|
|
627
|
+
result
|
|
628
|
+
});
|
|
629
|
+
const outcome = result.winner === "wizard-1" ? `${pairing.bot1Name} wins` : result.winner === "wizard-2" ? `${pairing.bot2Name} wins` : "Draw";
|
|
630
|
+
console.log(` ${pairing.bot1Name} vs ${pairing.bot2Name}: ${result.wizard1Wins}-${result.wizard2Wins}-${result.draws} (${outcome})`);
|
|
631
|
+
}
|
|
632
|
+
const points = /* @__PURE__ */ new Map();
|
|
633
|
+
const wins = /* @__PURE__ */ new Map();
|
|
634
|
+
for (const p of participants) {
|
|
635
|
+
points.set(p.name, 0);
|
|
636
|
+
wins.set(p.name, 0);
|
|
637
|
+
}
|
|
638
|
+
for (const r of results) {
|
|
639
|
+
const score1 = scoreFight2(r.result);
|
|
640
|
+
const score2 = scoreFightAsWizard2(r.result);
|
|
641
|
+
points.set(r.bot1Name, (points.get(r.bot1Name) ?? 0) + score1);
|
|
642
|
+
points.set(r.bot2Name, (points.get(r.bot2Name) ?? 0) + score2);
|
|
643
|
+
wins.set(r.bot1Name, (wins.get(r.bot1Name) ?? 0) + r.result.wizard1Wins);
|
|
644
|
+
wins.set(r.bot2Name, (wins.get(r.bot2Name) ?? 0) + r.result.wizard2Wins);
|
|
645
|
+
}
|
|
646
|
+
const totalElapsed = Date.now() - overallStart;
|
|
647
|
+
const standings = [...points.entries()].sort((a, b) => {
|
|
648
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
649
|
+
return (wins.get(b[0]) ?? 0) - (wins.get(a[0]) ?? 0);
|
|
650
|
+
});
|
|
651
|
+
console.log("\n Standings:");
|
|
652
|
+
console.log(" " + "-".repeat(40));
|
|
653
|
+
for (let i = 0; i < standings.length; i++) {
|
|
654
|
+
const [name, pts] = standings[i];
|
|
655
|
+
const w = wins.get(name) ?? 0;
|
|
656
|
+
const rank = `#${(i + 1).toString().padStart(2)}`;
|
|
657
|
+
const isUser = name === botInfo.exportName ? " *" : "";
|
|
658
|
+
console.log(` ${rank} ${name.padEnd(16)} ${pts.toFixed(1)} pts ${w}W${isUser}`);
|
|
659
|
+
}
|
|
660
|
+
console.log(`
|
|
661
|
+
Total time: ${(totalElapsed / 1e3).toFixed(1)}s
|
|
662
|
+
`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/commands/optimize.ts
|
|
666
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
667
|
+
import { BotBundle as BotBundle5, MatchSandbox, scoreFight as scoreFight3, generateCandidates, getEffectiveRange } from "@vibemancer/core";
|
|
668
|
+
var USEPAR_RE = /useParam\(\s*['"](\w+)['"]\s*,\s*([^,)]+?)\s*(?:,\s*\{([^}]*)\})?\s*\)/g;
|
|
669
|
+
function parseNumValue(s) {
|
|
670
|
+
const n = parseFloat(s.trim());
|
|
671
|
+
if (Number.isNaN(n)) throw new Error(`useParam default is not a number: "${s.trim()}"`);
|
|
672
|
+
return n;
|
|
673
|
+
}
|
|
674
|
+
function parseParams(source) {
|
|
675
|
+
const params = [];
|
|
676
|
+
const re = new RegExp(USEPAR_RE.source, "g");
|
|
677
|
+
let match;
|
|
678
|
+
while ((match = re.exec(source)) !== null) {
|
|
679
|
+
const name = match[1];
|
|
680
|
+
const defaultValue = parseNumValue(match[2]);
|
|
681
|
+
const optsStr = match[3];
|
|
682
|
+
let min;
|
|
683
|
+
let max;
|
|
684
|
+
let step;
|
|
685
|
+
if (optsStr) {
|
|
686
|
+
const minMatch = optsStr.match(/min\s*:\s*([^,}]+)/);
|
|
687
|
+
const maxMatch = optsStr.match(/max\s*:\s*([^,}]+)/);
|
|
688
|
+
const stepMatch = optsStr.match(/step\s*:\s*([^,}]+)/);
|
|
689
|
+
if (minMatch) min = parseNumValue(minMatch[1]);
|
|
690
|
+
if (maxMatch) max = parseNumValue(maxMatch[1]);
|
|
691
|
+
if (stepMatch) step = parseNumValue(stepMatch[1]);
|
|
692
|
+
}
|
|
693
|
+
params.push({ name, defaultValue, min, max, step });
|
|
694
|
+
}
|
|
695
|
+
return params;
|
|
696
|
+
}
|
|
697
|
+
function toParamDeclaration(p) {
|
|
698
|
+
return {
|
|
699
|
+
name: p.name,
|
|
700
|
+
value: p.defaultValue,
|
|
701
|
+
min: p.min,
|
|
702
|
+
max: p.max,
|
|
703
|
+
steps: p.step ?? 5
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
var SPAWN_DISTANCES = [200, 300, 400, 500, 600];
|
|
707
|
+
var SEEDS = [42, 137, 256];
|
|
708
|
+
function runFightWithParams(sandbox, params1) {
|
|
709
|
+
let w1 = 0;
|
|
710
|
+
let w2 = 0;
|
|
711
|
+
let draws = 0;
|
|
712
|
+
const matches = [];
|
|
713
|
+
for (const seed of SEEDS) {
|
|
714
|
+
for (const dist of SPAWN_DISTANCES) {
|
|
715
|
+
const r = sandbox.simulate({
|
|
716
|
+
seed,
|
|
717
|
+
spawnDistance: dist,
|
|
718
|
+
skipHistory: true,
|
|
719
|
+
params1
|
|
720
|
+
});
|
|
721
|
+
if (r.winner === "wizard-1") w1++;
|
|
722
|
+
else if (r.winner === "wizard-2") w2++;
|
|
723
|
+
else draws++;
|
|
724
|
+
matches.push(r);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
wizard1Wins: w1,
|
|
729
|
+
wizard2Wins: w2,
|
|
730
|
+
draws,
|
|
731
|
+
winner: w1 > w2 ? "wizard-1" : w2 > w1 ? "wizard-2" : "draw",
|
|
732
|
+
matches
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
async function evaluateParams(userBundle, opponentBundles, params1) {
|
|
736
|
+
let totalScore = 0;
|
|
737
|
+
for (const opponent of opponentBundles) {
|
|
738
|
+
const sandbox = await MatchSandbox.create(userBundle, opponent, {
|
|
739
|
+
compileOptions: getCoreCompileOptions()
|
|
740
|
+
});
|
|
741
|
+
try {
|
|
742
|
+
const result = runFightWithParams(sandbox, params1);
|
|
743
|
+
totalScore += scoreFight3(result);
|
|
744
|
+
} finally {
|
|
745
|
+
sandbox.dispose();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return totalScore;
|
|
749
|
+
}
|
|
750
|
+
function resolveOpponentNames(opponents) {
|
|
751
|
+
if (!opponents || opponents.length === 0) {
|
|
752
|
+
return getBuiltinBotNames();
|
|
753
|
+
}
|
|
754
|
+
const allNames = getBuiltinBotNames();
|
|
755
|
+
for (const name of opponents) {
|
|
756
|
+
if (!allNames.includes(name)) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
`Unknown opponent: "${name}". Available bots:
|
|
759
|
+
${allNames.join(", ")}`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return opponents;
|
|
764
|
+
}
|
|
765
|
+
async function runOptimize(options) {
|
|
766
|
+
const projectDir = process.cwd();
|
|
767
|
+
const botInfo = await discoverBot(projectDir, options.bot);
|
|
768
|
+
const source = readFileSync(botInfo.sourcePath, "utf-8");
|
|
769
|
+
const params = parseParams(source);
|
|
770
|
+
if (params.length === 0) {
|
|
771
|
+
console.log("\n No useParam() calls found in your bot.");
|
|
772
|
+
console.log(' Add useParam("paramName", defaultValue, {min, max}) to enable optimization.\n');
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
console.log(`
|
|
776
|
+
Bot: ${botInfo.exportName}`);
|
|
777
|
+
console.log(` Parameters: ${params.length}`);
|
|
778
|
+
params.forEach((p) => console.log(` ${p.name} = ${p.defaultValue} [${p.min ?? "auto"} .. ${p.max ?? "auto"}]`));
|
|
779
|
+
const steps = options.steps ?? 5;
|
|
780
|
+
const maxRounds = options.rounds ?? 3;
|
|
781
|
+
const opponentNames = resolveOpponentNames(options.opponents);
|
|
782
|
+
const opponentBundles = opponentNames.map((name) => resolveOpponent(name));
|
|
783
|
+
const userBundle = new BotBundle5(botInfo.sourcePath, botInfo.exportName);
|
|
784
|
+
console.log(` Opponents: ${opponentBundles.length}`);
|
|
785
|
+
console.log(` Steps per param: ${steps}`);
|
|
786
|
+
console.log(` Max rounds: ${maxRounds}
|
|
787
|
+
`);
|
|
788
|
+
const best = {};
|
|
789
|
+
for (const p of params) best[p.name] = p.defaultValue;
|
|
790
|
+
let bestScore = await evaluateParams(userBundle, opponentBundles, best);
|
|
791
|
+
console.log(` Baseline score: ${bestScore.toFixed(1)} / ${(opponentBundles.length * SPAWN_DISTANCES.length * SEEDS.length * 3.5).toFixed(1)}
|
|
792
|
+
`);
|
|
793
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
794
|
+
let improved = false;
|
|
795
|
+
console.log(` Round ${round + 1}:`);
|
|
796
|
+
for (const p of params) {
|
|
797
|
+
const decl = toParamDeclaration(p);
|
|
798
|
+
const range = getEffectiveRange(decl);
|
|
799
|
+
const candidates = generateCandidates(range.min, range.max, steps);
|
|
800
|
+
let paramBest = best[p.name];
|
|
801
|
+
let paramBestScore = bestScore;
|
|
802
|
+
for (const candidate of candidates) {
|
|
803
|
+
if (Math.abs(candidate - paramBest) < 1e-3) continue;
|
|
804
|
+
const trial = { ...best, [p.name]: candidate };
|
|
805
|
+
const score = await evaluateParams(userBundle, opponentBundles, trial);
|
|
806
|
+
if (score > paramBestScore) {
|
|
807
|
+
paramBest = candidate;
|
|
808
|
+
paramBestScore = score;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (paramBest !== best[p.name]) {
|
|
812
|
+
console.log(` ${p.name}: ${best[p.name]} -> ${paramBest} (+${(paramBestScore - bestScore).toFixed(1)})`);
|
|
813
|
+
best[p.name] = paramBest;
|
|
814
|
+
bestScore = paramBestScore;
|
|
815
|
+
improved = true;
|
|
816
|
+
} else {
|
|
817
|
+
console.log(` ${p.name}: ${best[p.name]} (no improvement)`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (!improved) {
|
|
821
|
+
console.log(" No improvements found, stopping.\n");
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
console.log(` Round ${round + 1} score: ${bestScore.toFixed(1)}
|
|
825
|
+
`);
|
|
826
|
+
}
|
|
827
|
+
let updated = source;
|
|
828
|
+
for (const p of params) {
|
|
829
|
+
const newVal = best[p.name];
|
|
830
|
+
if (newVal !== p.defaultValue) {
|
|
831
|
+
const pattern = new RegExp(
|
|
832
|
+
`(useParam\\(\\s*['"]${escapeRegex(p.name)}['"]\\s*,\\s*)${escapeRegex(String(p.defaultValue))}`
|
|
833
|
+
);
|
|
834
|
+
updated = updated.replace(pattern, `$1${newVal}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (updated !== source) {
|
|
838
|
+
writeFileSync(botInfo.sourcePath, updated);
|
|
839
|
+
console.log(` Source updated: ${botInfo.sourcePath}`);
|
|
840
|
+
} else {
|
|
841
|
+
console.log(" No parameter changes to write.");
|
|
842
|
+
}
|
|
843
|
+
console.log(` Final score: ${bestScore.toFixed(1)} / ${(opponentBundles.length * SPAWN_DISTANCES.length * SEEDS.length * 3.5).toFixed(1)}
|
|
844
|
+
`);
|
|
845
|
+
}
|
|
846
|
+
function escapeRegex(s) {
|
|
847
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/commands/build.ts
|
|
851
|
+
import fs4 from "fs";
|
|
852
|
+
import path4 from "path";
|
|
853
|
+
import { BotBundle as BotBundle6, compileMatchBundle } from "@vibemancer/core";
|
|
854
|
+
async function runBuild(options) {
|
|
855
|
+
const projectDir = process.cwd();
|
|
856
|
+
const botInfo = await discoverBot(projectDir, options.bot);
|
|
857
|
+
const coreSourceDir = findCoreSourceDir();
|
|
858
|
+
const userBundle = new BotBundle6(botInfo.sourcePath, botInfo.exportName);
|
|
859
|
+
const opponentBundle = resolveOpponent(options.opponent);
|
|
860
|
+
console.log(`
|
|
861
|
+
Compiling ${botInfo.exportName} vs ${options.opponent}...`);
|
|
862
|
+
const start = Date.now();
|
|
863
|
+
const bundle = await compileMatchBundle(userBundle, opponentBundle, {
|
|
864
|
+
alias: {
|
|
865
|
+
"@vibemancer/core": coreSourceDir + "/index-browser.ts"
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
const elapsed = Date.now() - start;
|
|
869
|
+
const outFile = options.output ?? `dist/${botInfo.exportName}-vs-${options.opponent}.js`;
|
|
870
|
+
const outDir = path4.dirname(path4.resolve(outFile));
|
|
871
|
+
fs4.mkdirSync(outDir, { recursive: true });
|
|
872
|
+
fs4.writeFileSync(path4.resolve(outFile), bundle);
|
|
873
|
+
const sizeKb = (bundle.length / 1024).toFixed(1);
|
|
874
|
+
console.log(` Output: ${outFile} (${sizeKb} KB)`);
|
|
875
|
+
console.log(` Compiled in ${elapsed}ms
|
|
876
|
+
`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/commands/bots.ts
|
|
880
|
+
import { BOT_GROUPS, ALL_BOTS } from "@vibemancer/core";
|
|
881
|
+
function runBots(options) {
|
|
882
|
+
if (options.name) {
|
|
883
|
+
showBotDetail(options.name);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
listAllBots();
|
|
887
|
+
}
|
|
888
|
+
function listAllBots() {
|
|
889
|
+
console.log("\n Built-in Bots (29 total, ranked weakest \u2192 strongest)\n");
|
|
890
|
+
for (const group of BOT_GROUPS) {
|
|
891
|
+
console.log(` ${group.label}:`);
|
|
892
|
+
for (const bot of group.bots) {
|
|
893
|
+
const rank = ALL_BOTS.indexOf(bot) + 1;
|
|
894
|
+
const tierLabel = bot.tier ? `T${bot.tier}` : " ";
|
|
895
|
+
const rankStr = `#${String(rank).padStart(2)}`;
|
|
896
|
+
console.log(` ${rankStr} ${tierLabel} ${bot.name.padEnd(14)} ${bot.description}`);
|
|
897
|
+
}
|
|
898
|
+
console.log("");
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function showBotDetail(name) {
|
|
902
|
+
const bot = ALL_BOTS.find((b) => b.name.toLowerCase() === name.toLowerCase());
|
|
903
|
+
if (!bot) {
|
|
904
|
+
const available = ALL_BOTS.map((b) => b.name).join(", ");
|
|
905
|
+
console.error(`
|
|
906
|
+
Unknown bot: "${name}"
|
|
907
|
+
Available: ${available}
|
|
908
|
+
`);
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
const rank = ALL_BOTS.indexOf(bot) + 1;
|
|
912
|
+
console.log(`
|
|
913
|
+
${bot.name}`);
|
|
914
|
+
console.log(` ${"\u2500".repeat(40)}`);
|
|
915
|
+
console.log(` Rank: #${rank} of ${ALL_BOTS.length}`);
|
|
916
|
+
console.log(` Group: ${bot.group}`);
|
|
917
|
+
if (bot.tier) console.log(` Tier: ${bot.tier} of 3`);
|
|
918
|
+
console.log(` Description: ${bot.description}`);
|
|
919
|
+
console.log(` Style: ${getStyleDescription(bot)}`);
|
|
920
|
+
if (bot.tier && bot.group !== "Standalone") {
|
|
921
|
+
const groupBots = BOT_GROUPS.find((g) => g.label === bot.group)?.bots ?? [];
|
|
922
|
+
if (groupBots.length > 1) {
|
|
923
|
+
console.log(`
|
|
924
|
+
${bot.group} progression:`);
|
|
925
|
+
for (const gb of groupBots) {
|
|
926
|
+
const gbRank = ALL_BOTS.indexOf(gb) + 1;
|
|
927
|
+
const marker = gb.name === bot.name ? " \u2190" : "";
|
|
928
|
+
console.log(` T${gb.tier} ${gb.name.padEnd(14)} #${gbRank} \u2014 ${gb.description}${marker}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
console.log(`
|
|
933
|
+
To fight: vibemancer fight --opponent ${bot.name}`);
|
|
934
|
+
console.log(` To trace: vibemancer trace --opponent ${bot.name}
|
|
935
|
+
`);
|
|
936
|
+
}
|
|
937
|
+
function getStyleDescription(bot) {
|
|
938
|
+
switch (bot.group) {
|
|
939
|
+
case "Standalone":
|
|
940
|
+
if (bot.name === "TargetDummy") return "Does nothing. Use for basic testing.";
|
|
941
|
+
if (bot.name === "Critter") return "Random actions. Tests handling of unpredictable opponents.";
|
|
942
|
+
if (bot.name === "Rookie") return "Simple homing missiles. Good first benchmark.";
|
|
943
|
+
if (bot.name === "Hogger") return "Random but with real damage. Chaos test.";
|
|
944
|
+
if (bot.name === "Doombringer") return "One huge missile. Tests shield timing.";
|
|
945
|
+
return bot.description;
|
|
946
|
+
case "Defensive":
|
|
947
|
+
return "Prioritizes shields and survival. Punishes aggression with counter-missiles. Weak to chip damage and shield baiting.";
|
|
948
|
+
case "Melee":
|
|
949
|
+
return "Blinks in close, fires fast low-range stabs. Weak to kiting and ranged pressure.";
|
|
950
|
+
case "Homing":
|
|
951
|
+
return "Slow tracking missiles that are hard to dodge. Weak to shields and fast burst.";
|
|
952
|
+
case "Caster":
|
|
953
|
+
return "Medium-range homing with adaptive missile fitting. Balanced offense and defense.";
|
|
954
|
+
case "Sniper":
|
|
955
|
+
return "Intercept-predicted straight shots. High accuracy, weak to erratic movement.";
|
|
956
|
+
case "Duelist":
|
|
957
|
+
return "Close-range fighters with balanced offense/defense. Jack of all trades.";
|
|
958
|
+
case "Berserker":
|
|
959
|
+
return "Aggressive traders who close distance fast. Weak to kiting and strong defense.";
|
|
960
|
+
case "Kiter":
|
|
961
|
+
return "Maintains distance while firing homing missiles. Weak to fast closers and blink gap-close.";
|
|
962
|
+
default:
|
|
963
|
+
return bot.description;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/commands/upload.ts
|
|
968
|
+
import { createHash } from "crypto";
|
|
969
|
+
async function runUpload(options) {
|
|
970
|
+
const projectDir = process.cwd();
|
|
971
|
+
const botInfo = await discoverBot(projectDir, options.bot);
|
|
972
|
+
console.log(`
|
|
973
|
+
Compiling ${botInfo.exportName}...`);
|
|
974
|
+
const bundle = await compileSingleBotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
975
|
+
const sizeKb = (bundle.length / 1024).toFixed(1);
|
|
976
|
+
console.log(` Bundle: ${sizeKb} KB`);
|
|
977
|
+
if (bundle.length > 500 * 1024) {
|
|
978
|
+
console.error(` Error: Bundle too large (${sizeKb} KB). Max 500 KB.`);
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
const nameMatch = bundle.match(/export\s+(?:const|var|let)\s+name\s*=\s*["']([^"']+)["']/);
|
|
982
|
+
const wizardName = nameMatch?.[1] ?? botInfo.exportName;
|
|
983
|
+
const bundleHash = createHash("sha256").update(bundle).digest("hex");
|
|
984
|
+
console.log(` Wizard: ${wizardName}`);
|
|
985
|
+
console.log(` Hash: ${bundleHash.slice(0, 12)}...`);
|
|
986
|
+
console.log(" Uploading to VibeMancer...");
|
|
987
|
+
try {
|
|
988
|
+
const { initializeApp } = await import("firebase/app");
|
|
989
|
+
const { getFunctions, httpsCallable } = await import("firebase/functions");
|
|
990
|
+
const app = initializeApp({
|
|
991
|
+
apiKey: "AIzaSyDSUJ2jlk5_vlpGOypMtvqKjHhDmbgomIY",
|
|
992
|
+
authDomain: "le-vibemancer.firebaseapp.com",
|
|
993
|
+
projectId: "le-vibemancer"
|
|
994
|
+
});
|
|
995
|
+
const functions = getFunctions(app, "us-central1");
|
|
996
|
+
const uploadFn = httpsCallable(functions, "uploadWizard");
|
|
997
|
+
const result = await uploadFn({
|
|
998
|
+
bundle,
|
|
999
|
+
name: wizardName,
|
|
1000
|
+
exportName: botInfo.exportName
|
|
1001
|
+
});
|
|
1002
|
+
const data = result.data;
|
|
1003
|
+
if (data.active) {
|
|
1004
|
+
if (data.reactivated) {
|
|
1005
|
+
console.log(` \u21BA ${data.message}`);
|
|
1006
|
+
} else {
|
|
1007
|
+
console.log(` \u2713 ${data.message}`);
|
|
1008
|
+
}
|
|
1009
|
+
console.log(` Wizard ID: ${data.wizardId}`);
|
|
1010
|
+
console.log(` Your wizard "${wizardName}" is now competing.
|
|
1011
|
+
`);
|
|
1012
|
+
} else {
|
|
1013
|
+
console.error(` \u2717 ${data.message}`);
|
|
1014
|
+
console.error(" Fix the error and try again.\n");
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1019
|
+
console.error(` \u2717 Upload failed: ${message}
|
|
1020
|
+
`);
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/cli.ts
|
|
1026
|
+
var args = process.argv.slice(2);
|
|
1027
|
+
var command = args[0];
|
|
1028
|
+
function parseFlag(flag) {
|
|
1029
|
+
const idx = args.indexOf(flag);
|
|
1030
|
+
if (idx !== -1 && idx + 1 < args.length) {
|
|
1031
|
+
return args[idx + 1];
|
|
1032
|
+
}
|
|
1033
|
+
return void 0;
|
|
1034
|
+
}
|
|
1035
|
+
function parseIntFlag(flag, fallback) {
|
|
1036
|
+
const str = parseFlag(flag);
|
|
1037
|
+
if (str === void 0) return fallback;
|
|
1038
|
+
const n = parseInt(str, 10);
|
|
1039
|
+
if (Number.isNaN(n)) {
|
|
1040
|
+
console.error(`Error: ${flag} must be a number, got "${str}".`);
|
|
1041
|
+
process.exit(1);
|
|
1042
|
+
}
|
|
1043
|
+
return n;
|
|
1044
|
+
}
|
|
1045
|
+
function printHelp() {
|
|
1046
|
+
console.log(`
|
|
1047
|
+
Vibemancer CLI - Development tools for wizard bots
|
|
1048
|
+
|
|
1049
|
+
Usage:
|
|
1050
|
+
vibemancer <command> [options]
|
|
1051
|
+
|
|
1052
|
+
Commands:
|
|
1053
|
+
dev Start the development server (auto-opens browser)
|
|
1054
|
+
test Run your test suite (vitest)
|
|
1055
|
+
fight Fight against all 29 built-in bots (or a single opponent)
|
|
1056
|
+
bots List all built-in bots with descriptions
|
|
1057
|
+
trace Per-tick debug trace of a single match
|
|
1058
|
+
tournament Round-robin tournament between your bot + selected opponents
|
|
1059
|
+
optimize Optimize bot parameters via coordinate descent
|
|
1060
|
+
build Compile bot to a standalone bundle
|
|
1061
|
+
upload Upload bot to VibeMancer for online competition
|
|
1062
|
+
|
|
1063
|
+
Common options:
|
|
1064
|
+
--bot <path> Path to bot source file (default: auto-discover)
|
|
1065
|
+
|
|
1066
|
+
Dev options:
|
|
1067
|
+
--port <n> Server port (default: 4242)
|
|
1068
|
+
|
|
1069
|
+
Fight options:
|
|
1070
|
+
--opponent <name> Fight a single opponent instead of all bots
|
|
1071
|
+
--seed <n> Random seed
|
|
1072
|
+
|
|
1073
|
+
Trace options:
|
|
1074
|
+
--opponent <name> Opponent bot name (required)
|
|
1075
|
+
--seed <n> Random seed (default: 1)
|
|
1076
|
+
--distance <n> Spawn distance (default: 600)
|
|
1077
|
+
|
|
1078
|
+
Build options:
|
|
1079
|
+
--opponent <name> Opponent bot name (required)
|
|
1080
|
+
--output <path> Output file path (default: dist/<Bot>-vs-<Opponent>.js)
|
|
1081
|
+
|
|
1082
|
+
Optimize options:
|
|
1083
|
+
--steps <n> Candidates per parameter (default: 5)
|
|
1084
|
+
--rounds <n> Max optimization rounds (default: 3)
|
|
1085
|
+
--opponents <list> Comma-separated bot names to optimize against (default: all)
|
|
1086
|
+
|
|
1087
|
+
Examples:
|
|
1088
|
+
vibemancer dev
|
|
1089
|
+
vibemancer test
|
|
1090
|
+
vibemancer fight
|
|
1091
|
+
vibemancer fight --opponent Battlemage
|
|
1092
|
+
vibemancer trace --opponent Battlemage
|
|
1093
|
+
vibemancer trace --opponent Nightblade --distance 300
|
|
1094
|
+
vibemancer tournament Battlemage Warmage Archmage
|
|
1095
|
+
vibemancer optimize
|
|
1096
|
+
vibemancer build --opponent Battlemage
|
|
1097
|
+
`);
|
|
1098
|
+
}
|
|
1099
|
+
async function main() {
|
|
1100
|
+
if (!command || command === "--help" || command === "-h") {
|
|
1101
|
+
printHelp();
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
switch (command) {
|
|
1105
|
+
case "dev": {
|
|
1106
|
+
const port = parseIntFlag("--port", 4242);
|
|
1107
|
+
const bot = parseFlag("--bot");
|
|
1108
|
+
await runDev({ port, bot });
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
case "test": {
|
|
1112
|
+
const bot = parseFlag("--bot");
|
|
1113
|
+
await runTest({ bot });
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
case "fight": {
|
|
1117
|
+
const opponent = parseFlag("--opponent");
|
|
1118
|
+
const bot = parseFlag("--bot");
|
|
1119
|
+
const seed = parseFlag("--seed") !== void 0 ? parseIntFlag("--seed", 0) : void 0;
|
|
1120
|
+
await runFight({ opponent, bot, seed });
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
case "trace": {
|
|
1124
|
+
const opponent = parseFlag("--opponent");
|
|
1125
|
+
if (!opponent) {
|
|
1126
|
+
console.error("Error: --opponent is required for trace command.");
|
|
1127
|
+
console.error("Usage: vibemancer trace --opponent Battlemage");
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
const bot = parseFlag("--bot");
|
|
1131
|
+
const seed = parseFlag("--seed") !== void 0 ? parseIntFlag("--seed", 0) : void 0;
|
|
1132
|
+
const distance = parseFlag("--distance") !== void 0 ? parseIntFlag("--distance", 600) : void 0;
|
|
1133
|
+
await runTrace({ opponent, bot, seed, distance });
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
case "bots": {
|
|
1137
|
+
const name = parseFlag("--name") ?? args[1];
|
|
1138
|
+
runBots({ name: name?.startsWith("--") ? void 0 : name });
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
case "tournament": {
|
|
1142
|
+
const bot = parseFlag("--bot");
|
|
1143
|
+
const flagIndices = /* @__PURE__ */ new Set();
|
|
1144
|
+
for (let i = 1; i < args.length; i++) {
|
|
1145
|
+
if (args[i].startsWith("--")) {
|
|
1146
|
+
flagIndices.add(i);
|
|
1147
|
+
flagIndices.add(i + 1);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const opponents = args.slice(1).filter((_, i) => !flagIndices.has(i + 1));
|
|
1151
|
+
await runTournament({ opponents, bot });
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
case "optimize": {
|
|
1155
|
+
const bot = parseFlag("--bot");
|
|
1156
|
+
const opponentsStr = parseFlag("--opponents");
|
|
1157
|
+
const opponents = opponentsStr ? opponentsStr.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1158
|
+
await runOptimize({
|
|
1159
|
+
bot,
|
|
1160
|
+
steps: parseFlag("--steps") !== void 0 ? parseIntFlag("--steps", 5) : void 0,
|
|
1161
|
+
rounds: parseFlag("--rounds") !== void 0 ? parseIntFlag("--rounds", 3) : void 0,
|
|
1162
|
+
opponents
|
|
1163
|
+
});
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
case "build": {
|
|
1167
|
+
const opponent = parseFlag("--opponent");
|
|
1168
|
+
if (!opponent) {
|
|
1169
|
+
console.error("Error: --opponent is required for build command.");
|
|
1170
|
+
console.error("Usage: vibemancer build --opponent Battlemage");
|
|
1171
|
+
process.exit(1);
|
|
1172
|
+
}
|
|
1173
|
+
const bot = parseFlag("--bot");
|
|
1174
|
+
const output = parseFlag("--output");
|
|
1175
|
+
await runBuild({ opponent, bot, output });
|
|
1176
|
+
break;
|
|
1177
|
+
}
|
|
1178
|
+
case "upload": {
|
|
1179
|
+
const bot = parseFlag("--bot");
|
|
1180
|
+
await runUpload({ bot });
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
default:
|
|
1184
|
+
console.error(`Unknown command: ${command}`);
|
|
1185
|
+
printHelp();
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
main().catch((error) => {
|
|
1190
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
1191
|
+
process.exit(1);
|
|
1192
|
+
});
|
|
1193
|
+
//# sourceMappingURL=cli.js.map
|