vibemancer 0.1.4 → 0.1.6
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 +2 -2
- package/dist/chunk-4DUS7IHW.js +101 -0
- package/dist/chunk-4DUS7IHW.js.map +1 -0
- package/dist/chunk-AA2UJPPN.js +14 -0
- package/dist/chunk-AA2UJPPN.js.map +1 -0
- package/dist/cli.js +262 -164
- package/dist/cli.js.map +1 -1
- package/dist/firebase-config-F6COE3NY.js +8 -0
- package/dist/firebase-config-F6COE3NY.js.map +1 -0
- package/dist/opponent-resolver-U2UK2QEJ.js +14 -0
- package/dist/opponent-resolver-U2UK2QEJ.js.map +1 -0
- package/package.json +11 -10
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findCoreSourceDir,
|
|
4
|
+
getBuiltinBotNames,
|
|
5
|
+
getCoreCompileOptions,
|
|
6
|
+
resolveOpponent
|
|
7
|
+
} from "./chunk-4DUS7IHW.js";
|
|
8
|
+
import {
|
|
9
|
+
FIREBASE_CONFIG
|
|
10
|
+
} from "./chunk-AA2UJPPN.js";
|
|
2
11
|
|
|
3
12
|
// src/commands/dev.ts
|
|
4
13
|
import { execFile } from "child_process";
|
|
@@ -40,13 +49,25 @@ async function discoverBot(projectDir, overridePath) {
|
|
|
40
49
|
return { sourcePath: botPath, exportName };
|
|
41
50
|
}
|
|
42
51
|
}
|
|
52
|
+
const allBots = await discoverAllBots(absDir);
|
|
53
|
+
if (allBots.length === 1) {
|
|
54
|
+
return allBots[0];
|
|
55
|
+
}
|
|
56
|
+
if (allBots.length > 1) {
|
|
57
|
+
const list = allBots.map((b) => ` --bot ${path.relative(absDir, b.sourcePath).replace(/\\/g, "/")} (${b.exportName})`).join("\n");
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Found ${allBots.length} bots. Pick one with --bot:
|
|
60
|
+
|
|
61
|
+
${list}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
43
64
|
const defaultPath = path.join(absDir, "src", "bot.ts");
|
|
44
65
|
if (fs.existsSync(defaultPath)) {
|
|
45
66
|
const exportName = await findExportName(defaultPath);
|
|
46
67
|
return { sourcePath: defaultPath, exportName };
|
|
47
68
|
}
|
|
48
69
|
throw new Error(
|
|
49
|
-
|
|
70
|
+
"Could not find bot source file.\nCreate a .ts file in src/ with a PascalCase export, e.g.:\n export function MyWizard() { ... }"
|
|
50
71
|
);
|
|
51
72
|
}
|
|
52
73
|
async function findExportName(filePath) {
|
|
@@ -142,100 +163,6 @@ import http from "http";
|
|
|
142
163
|
|
|
143
164
|
// src/compile-single-bot.ts
|
|
144
165
|
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
166
|
async function compileSingleBotBundle(sourcePath, exportName) {
|
|
240
167
|
const coreSourceDir = findCoreSourceDir();
|
|
241
168
|
const result = await build({
|
|
@@ -355,7 +282,7 @@ async function runDev(options) {
|
|
|
355
282
|
const bots = await discoverAllBots(projectDir);
|
|
356
283
|
if (bots.length === 0) {
|
|
357
284
|
console.log("No bots discovered in src/. Add a .ts file with a PascalCase export, e.g.:");
|
|
358
|
-
console.log(" export function MyWizard(
|
|
285
|
+
console.log(" export function MyWizard() { return move(0, 0); }");
|
|
359
286
|
} else {
|
|
360
287
|
console.log(`Discovered ${bots.length} bot${bots.length === 1 ? "" : "s"}: ${bots.map((b) => b.exportName).join(", ")}`);
|
|
361
288
|
}
|
|
@@ -402,9 +329,74 @@ async function runTest(_options) {
|
|
|
402
329
|
}
|
|
403
330
|
|
|
404
331
|
// src/commands/fight.ts
|
|
405
|
-
import
|
|
406
|
-
import
|
|
407
|
-
import { BotBundle
|
|
332
|
+
import fs2 from "fs";
|
|
333
|
+
import path2 from "path";
|
|
334
|
+
import { BotBundle, sandboxFight, scoreFight, runBundleFight } from "@vibemancer/core";
|
|
335
|
+
|
|
336
|
+
// src/remote-opponent.ts
|
|
337
|
+
import { initializeApp, getApps } from "firebase/app";
|
|
338
|
+
import { getFirestore, collection, query, where, limit, getDocs, connectFirestoreEmulator } from "firebase/firestore";
|
|
339
|
+
import { getStorage, ref, getBytes, connectStorageEmulator } from "firebase/storage";
|
|
340
|
+
import { isBannedBotName } from "@vibemancer/core";
|
|
341
|
+
function isHandleSelector(opponent) {
|
|
342
|
+
const trimmed = opponent.trim();
|
|
343
|
+
const slash = trimmed.indexOf("/");
|
|
344
|
+
return slash > 0 && slash < trimmed.length - 1;
|
|
345
|
+
}
|
|
346
|
+
var cachedDb = null;
|
|
347
|
+
var cachedStorage = null;
|
|
348
|
+
function getApp() {
|
|
349
|
+
const apps = getApps();
|
|
350
|
+
return apps.length > 0 ? apps[0] : initializeApp(FIREBASE_CONFIG);
|
|
351
|
+
}
|
|
352
|
+
function getDb() {
|
|
353
|
+
if (!cachedDb) {
|
|
354
|
+
cachedDb = getFirestore(getApp());
|
|
355
|
+
if (process.env.VIBEMANCER_EMULATOR === "1") connectFirestoreEmulator(cachedDb, "127.0.0.1", 8085);
|
|
356
|
+
}
|
|
357
|
+
return cachedDb;
|
|
358
|
+
}
|
|
359
|
+
function getBucket() {
|
|
360
|
+
if (!cachedStorage) {
|
|
361
|
+
cachedStorage = getStorage(getApp());
|
|
362
|
+
if (process.env.VIBEMANCER_EMULATOR === "1") connectStorageEmulator(cachedStorage, "127.0.0.1", 9199);
|
|
363
|
+
}
|
|
364
|
+
return cachedStorage;
|
|
365
|
+
}
|
|
366
|
+
async function resolveRemoteOpponent(selector) {
|
|
367
|
+
const slash = selector.indexOf("/");
|
|
368
|
+
const handle = selector.slice(0, slash).trim().toLowerCase();
|
|
369
|
+
const botName = selector.slice(slash + 1).trim();
|
|
370
|
+
if (!handle || !botName) {
|
|
371
|
+
throw new Error(`Invalid opponent "${selector}" \u2014 expected handle/botname (e.g. happy-golden-banana/FireMage).`);
|
|
372
|
+
}
|
|
373
|
+
if (isBannedBotName(botName)) {
|
|
374
|
+
throw new Error(`No active bot "${botName}" found for handle "${handle}". Check the handle + bot name (both case-insensitive) on the leaderboard.`);
|
|
375
|
+
}
|
|
376
|
+
const db = getDb();
|
|
377
|
+
const snap = await getDocs(query(
|
|
378
|
+
collection(db, "wizards"),
|
|
379
|
+
where("ownerHandle", "==", handle),
|
|
380
|
+
where("nameLower", "==", botName.toLowerCase()),
|
|
381
|
+
where("active", "==", true),
|
|
382
|
+
limit(1)
|
|
383
|
+
));
|
|
384
|
+
if (snap.empty) {
|
|
385
|
+
throw new Error(`No active bot "${botName}" found for handle "${handle}". Check the handle + bot name (both case-insensitive) on the leaderboard.`);
|
|
386
|
+
}
|
|
387
|
+
const docSnap = snap.docs[0];
|
|
388
|
+
const data = docSnap.data();
|
|
389
|
+
const exportName = typeof data.exportName === "string" ? data.exportName : "";
|
|
390
|
+
if (!exportName) {
|
|
391
|
+
throw new Error(`Bot "${handle}/${botName}" is missing its export name.`);
|
|
392
|
+
}
|
|
393
|
+
const bundlePath = typeof data.bundlePath === "string" ? data.bundlePath : `bundles/${docSnap.id}.js`;
|
|
394
|
+
const bytes = await getBytes(ref(getBucket(), bundlePath));
|
|
395
|
+
const bundle = new TextDecoder().decode(bytes);
|
|
396
|
+
return { bundle, exportName, label: `${handle}/${botName}` };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/commands/fight.ts
|
|
408
400
|
async function runFight(options) {
|
|
409
401
|
if (options.opponent) {
|
|
410
402
|
return runSingleFight({ ...options, opponent: options.opponent });
|
|
@@ -418,13 +410,21 @@ async function runSingleFight(options) {
|
|
|
418
410
|
Bot: ${botInfo.exportName}`);
|
|
419
411
|
console.log(` Opponent: ${options.opponent}
|
|
420
412
|
`);
|
|
421
|
-
const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
|
|
422
|
-
const opponentBundle = resolveOpponent(options.opponent);
|
|
423
413
|
const start = Date.now();
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
414
|
+
let result;
|
|
415
|
+
if (isHandleSelector(options.opponent)) {
|
|
416
|
+
console.log(" Resolving uploaded opponent...");
|
|
417
|
+
const remote = await resolveRemoteOpponent(options.opponent);
|
|
418
|
+
const userBundle = await compileSingleBotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
419
|
+
result = await runBundleFight(userBundle, remote.bundle, { seed: options.seed ?? 1 });
|
|
420
|
+
} else {
|
|
421
|
+
const userBundle = new BotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
422
|
+
const opponentBundle = resolveOpponent(options.opponent);
|
|
423
|
+
result = await sandboxFight(userBundle, opponentBundle, {
|
|
424
|
+
seed: options.seed,
|
|
425
|
+
compileOptions: getCoreCompileOptions()
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
428
|
const elapsed = Date.now() - start;
|
|
429
429
|
const { wizard1Wins, wizard2Wins, draws } = result;
|
|
430
430
|
const total = wizard1Wins + wizard2Wins + draws;
|
|
@@ -440,7 +440,7 @@ async function runSingleFight(options) {
|
|
|
440
440
|
async function runFullFight(options) {
|
|
441
441
|
const projectDir = process.cwd();
|
|
442
442
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
443
|
-
const userBundle = new
|
|
443
|
+
const userBundle = new BotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
444
444
|
const opponents = getBuiltinBotNames();
|
|
445
445
|
console.log(`
|
|
446
446
|
Fighting ${botInfo.exportName} against ${opponents.length} built-in bots...
|
|
@@ -494,13 +494,13 @@ async function runFullFight(options) {
|
|
|
494
494
|
console.log("");
|
|
495
495
|
}
|
|
496
496
|
function getHistoryPath(projectDir) {
|
|
497
|
-
return
|
|
497
|
+
return path2.join(projectDir, ".vibemancer", "history.json");
|
|
498
498
|
}
|
|
499
499
|
function loadHistory(projectDir) {
|
|
500
500
|
const historyPath = getHistoryPath(projectDir);
|
|
501
|
-
if (!
|
|
501
|
+
if (!fs2.existsSync(historyPath)) return [];
|
|
502
502
|
try {
|
|
503
|
-
const raw =
|
|
503
|
+
const raw = fs2.readFileSync(historyPath, "utf-8");
|
|
504
504
|
return JSON.parse(raw);
|
|
505
505
|
} catch {
|
|
506
506
|
return [];
|
|
@@ -508,10 +508,10 @@ function loadHistory(projectDir) {
|
|
|
508
508
|
}
|
|
509
509
|
function saveHistory(projectDir, history, current) {
|
|
510
510
|
const historyPath = getHistoryPath(projectDir);
|
|
511
|
-
const dir =
|
|
512
|
-
|
|
511
|
+
const dir = path2.dirname(historyPath);
|
|
512
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
513
513
|
const updated = [...history.slice(-19), current];
|
|
514
|
-
|
|
514
|
+
fs2.writeFileSync(historyPath, JSON.stringify(updated, null, " ") + "\n");
|
|
515
515
|
}
|
|
516
516
|
function showDiff(current, previous) {
|
|
517
517
|
const prevMap = new Map(previous.map((e) => [e.opponent, e]));
|
|
@@ -542,8 +542,9 @@ function showDiff(current, previous) {
|
|
|
542
542
|
|
|
543
543
|
// src/commands/trace.ts
|
|
544
544
|
import {
|
|
545
|
-
BotBundle as
|
|
545
|
+
BotBundle as BotBundle2,
|
|
546
546
|
sandboxSimulate,
|
|
547
|
+
runBundleSimulate,
|
|
547
548
|
extractTraceEvents,
|
|
548
549
|
summarizeTrace,
|
|
549
550
|
formatTraceEvents,
|
|
@@ -556,18 +557,30 @@ import {
|
|
|
556
557
|
async function runTrace(options) {
|
|
557
558
|
const projectDir = process.cwd();
|
|
558
559
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
559
|
-
const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
|
|
560
|
-
const opponentBundle = resolveOpponent(options.opponent);
|
|
561
560
|
const distance = options.distance ?? 600;
|
|
562
561
|
console.log(`
|
|
563
562
|
Trace: ${botInfo.exportName} (W1) vs ${options.opponent} (W2) | distance: ${distance} | seed: ${options.seed ?? 1}
|
|
564
563
|
`);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
564
|
+
let result;
|
|
565
|
+
if (isHandleSelector(options.opponent)) {
|
|
566
|
+
console.log(" Resolving uploaded opponent...");
|
|
567
|
+
const remote = await resolveRemoteOpponent(options.opponent);
|
|
568
|
+
const userBundle = await compileSingleBotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
569
|
+
result = await runBundleSimulate(userBundle, remote.bundle, {
|
|
570
|
+
seed: options.seed ?? 1,
|
|
571
|
+
spawnDistance: distance,
|
|
572
|
+
maxTicks: options.maxTicks ?? 3e3
|
|
573
|
+
});
|
|
574
|
+
} else {
|
|
575
|
+
const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
|
|
576
|
+
const opponentBundle = resolveOpponent(options.opponent);
|
|
577
|
+
result = await sandboxSimulate(userBundle, opponentBundle, {
|
|
578
|
+
seed: options.seed ?? 1,
|
|
579
|
+
spawnDistance: distance,
|
|
580
|
+
maxTicks: options.maxTicks ?? 3e3,
|
|
581
|
+
compileOptions: getCoreCompileOptions()
|
|
582
|
+
});
|
|
583
|
+
}
|
|
571
584
|
const history = result.history;
|
|
572
585
|
if (history.length === 0) {
|
|
573
586
|
console.log(" No history available.\n");
|
|
@@ -589,11 +602,11 @@ async function runTrace(options) {
|
|
|
589
602
|
}
|
|
590
603
|
|
|
591
604
|
// src/commands/tournament.ts
|
|
592
|
-
import { BotBundle as
|
|
605
|
+
import { BotBundle as BotBundle3, sandboxFight as sandboxFight2, scoreFight as scoreFight2, scoreFightAsWizard2 } from "@vibemancer/core";
|
|
593
606
|
async function runTournament(options) {
|
|
594
607
|
const projectDir = process.cwd();
|
|
595
608
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
596
|
-
const userBundle = new
|
|
609
|
+
const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
|
|
597
610
|
const opponentNames = options.opponents && options.opponents.length > 0 ? options.opponents : getBuiltinBotNames();
|
|
598
611
|
const participants = [
|
|
599
612
|
{ name: botInfo.exportName, bundle: userBundle }
|
|
@@ -664,7 +677,7 @@ async function runTournament(options) {
|
|
|
664
677
|
|
|
665
678
|
// src/commands/optimize.ts
|
|
666
679
|
import { readFileSync, writeFileSync } from "fs";
|
|
667
|
-
import { BotBundle as
|
|
680
|
+
import { BotBundle as BotBundle4, MatchSandbox, scoreFight as scoreFight3, generateCandidates, getEffectiveRange } from "@vibemancer/core";
|
|
668
681
|
var USEPAR_RE = /useParam\(\s*['"](\w+)['"]\s*,\s*([^,)]+?)\s*(?:,\s*\{([^}]*)\})?\s*\)/g;
|
|
669
682
|
function parseNumValue(s) {
|
|
670
683
|
const n = parseFloat(s.trim());
|
|
@@ -780,7 +793,7 @@ async function runOptimize(options) {
|
|
|
780
793
|
const maxRounds = options.rounds ?? 3;
|
|
781
794
|
const opponentNames = resolveOpponentNames(options.opponents);
|
|
782
795
|
const opponentBundles = opponentNames.map((name) => resolveOpponent(name));
|
|
783
|
-
const userBundle = new
|
|
796
|
+
const userBundle = new BotBundle4(botInfo.sourcePath, botInfo.exportName);
|
|
784
797
|
console.log(` Opponents: ${opponentBundles.length}`);
|
|
785
798
|
console.log(` Steps per param: ${steps}`);
|
|
786
799
|
console.log(` Max rounds: ${maxRounds}
|
|
@@ -848,14 +861,14 @@ function escapeRegex(s) {
|
|
|
848
861
|
}
|
|
849
862
|
|
|
850
863
|
// src/commands/build.ts
|
|
851
|
-
import
|
|
852
|
-
import
|
|
853
|
-
import { BotBundle as
|
|
864
|
+
import fs3 from "fs";
|
|
865
|
+
import path3 from "path";
|
|
866
|
+
import { BotBundle as BotBundle5, compileMatchBundle } from "@vibemancer/core";
|
|
854
867
|
async function runBuild(options) {
|
|
855
868
|
const projectDir = process.cwd();
|
|
856
869
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
857
870
|
const coreSourceDir = findCoreSourceDir();
|
|
858
|
-
const userBundle = new
|
|
871
|
+
const userBundle = new BotBundle5(botInfo.sourcePath, botInfo.exportName);
|
|
859
872
|
const opponentBundle = resolveOpponent(options.opponent);
|
|
860
873
|
console.log(`
|
|
861
874
|
Compiling ${botInfo.exportName} vs ${options.opponent}...`);
|
|
@@ -867,9 +880,9 @@ async function runBuild(options) {
|
|
|
867
880
|
});
|
|
868
881
|
const elapsed = Date.now() - start;
|
|
869
882
|
const outFile = options.output ?? `dist/${botInfo.exportName}-vs-${options.opponent}.js`;
|
|
870
|
-
const outDir =
|
|
871
|
-
|
|
872
|
-
|
|
883
|
+
const outDir = path3.dirname(path3.resolve(outFile));
|
|
884
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
885
|
+
fs3.writeFileSync(path3.resolve(outFile), bundle);
|
|
873
886
|
const sizeKb = (bundle.length / 1024).toFixed(1);
|
|
874
887
|
console.log(` Output: ${outFile} (${sizeKb} KB)`);
|
|
875
888
|
console.log(` Compiled in ${elapsed}ms
|
|
@@ -966,7 +979,7 @@ function getStyleDescription(bot) {
|
|
|
966
979
|
|
|
967
980
|
// src/commands/upload.ts
|
|
968
981
|
import { createHash } from "crypto";
|
|
969
|
-
import
|
|
982
|
+
import fs4 from "fs";
|
|
970
983
|
async function runUpload(options) {
|
|
971
984
|
const projectDir = process.cwd();
|
|
972
985
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
@@ -979,25 +992,21 @@ async function runUpload(options) {
|
|
|
979
992
|
console.error(` Error: Bundle too large (${sizeKb} KB). Max 500 KB.`);
|
|
980
993
|
process.exit(1);
|
|
981
994
|
}
|
|
982
|
-
const
|
|
983
|
-
const wizardName = nameMatch?.[1] ?? botInfo.exportName;
|
|
995
|
+
const wizardName = botInfo.exportName;
|
|
984
996
|
const bundleHash = createHash("sha256").update(bundle).digest("hex");
|
|
985
997
|
console.log(` Wizard: ${wizardName}`);
|
|
986
998
|
console.log(` Hash: ${bundleHash.slice(0, 12)}...`);
|
|
987
999
|
console.log(" Uploading to VibeMancer...");
|
|
988
1000
|
try {
|
|
989
|
-
const { initializeApp } = await import("firebase/app");
|
|
1001
|
+
const { initializeApp: initializeApp2 } = await import("firebase/app");
|
|
990
1002
|
const { getFunctions, httpsCallable } = await import("firebase/functions");
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
authDomain: "le-vibemancer.firebaseapp.com",
|
|
994
|
-
projectId: "le-vibemancer"
|
|
995
|
-
});
|
|
1003
|
+
const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
|
|
1004
|
+
const app = initializeApp2(FIREBASE_CONFIG2);
|
|
996
1005
|
const functions = getFunctions(app, "us-central1");
|
|
997
1006
|
const uploadFn = httpsCallable(functions, "uploadWizard");
|
|
998
1007
|
let sourceCode;
|
|
999
1008
|
try {
|
|
1000
|
-
sourceCode =
|
|
1009
|
+
sourceCode = fs4.readFileSync(botInfo.sourcePath, "utf-8");
|
|
1001
1010
|
} catch {
|
|
1002
1011
|
}
|
|
1003
1012
|
const result = await uploadFn({
|
|
@@ -1030,38 +1039,47 @@ async function runUpload(options) {
|
|
|
1030
1039
|
}
|
|
1031
1040
|
|
|
1032
1041
|
// src/commands/pull.ts
|
|
1033
|
-
import
|
|
1042
|
+
import fs5 from "fs";
|
|
1034
1043
|
async function runPull(options) {
|
|
1035
1044
|
const projectDir = process.cwd();
|
|
1036
1045
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
1037
1046
|
console.log(`
|
|
1038
1047
|
Pulling latest source for ${botInfo.exportName}...`);
|
|
1039
1048
|
try {
|
|
1040
|
-
const { initializeApp } = await import("firebase/app");
|
|
1049
|
+
const { initializeApp: initializeApp2 } = await import("firebase/app");
|
|
1041
1050
|
const { getAuth, signInWithPopup, GoogleAuthProvider } = await import("firebase/auth");
|
|
1042
|
-
const { getFirestore, collection, query, where, getDocs, doc, getDoc, limit: fbLimit } = await import("firebase/firestore");
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
authDomain: "le-vibemancer.firebaseapp.com",
|
|
1046
|
-
projectId: "le-vibemancer"
|
|
1047
|
-
});
|
|
1051
|
+
const { getFirestore: getFirestore2, collection: collection2, query: query2, where: where2, getDocs: getDocs2, doc, getDoc, limit: fbLimit } = await import("firebase/firestore");
|
|
1052
|
+
const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
|
|
1053
|
+
const app = initializeApp2(FIREBASE_CONFIG2);
|
|
1048
1054
|
const auth = getAuth(app);
|
|
1049
1055
|
console.log(" Signing in...");
|
|
1050
1056
|
const cred = await signInWithPopup(auth, new GoogleAuthProvider());
|
|
1051
1057
|
const userId = cred.user.uid;
|
|
1052
1058
|
console.log(` Signed in as ${cred.user.displayName ?? cred.user.email ?? userId}`);
|
|
1053
|
-
const db =
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
+
const db = getFirestore2(app);
|
|
1060
|
+
const wizardSnap = await getDocs2(query2(
|
|
1061
|
+
collection2(db, "wizards"),
|
|
1062
|
+
where2("ownerId", "==", userId),
|
|
1063
|
+
where2("name", "==", botInfo.exportName),
|
|
1064
|
+
where2("active", "==", true),
|
|
1059
1065
|
fbLimit(1)
|
|
1060
|
-
);
|
|
1061
|
-
const wizardSnap = await getDocs(wizardQuery);
|
|
1066
|
+
));
|
|
1062
1067
|
if (wizardSnap.empty) {
|
|
1063
|
-
|
|
1064
|
-
|
|
1068
|
+
const allBots = await getDocs2(query2(
|
|
1069
|
+
collection2(db, "wizards"),
|
|
1070
|
+
where2("ownerId", "==", userId),
|
|
1071
|
+
where2("active", "==", true),
|
|
1072
|
+
fbLimit(20)
|
|
1073
|
+
));
|
|
1074
|
+
if (allBots.empty) {
|
|
1075
|
+
console.error(" No active wizards found on your account.");
|
|
1076
|
+
console.error(" Upload first with: vibemancer upload");
|
|
1077
|
+
} else {
|
|
1078
|
+
const names = allBots.docs.map((d) => d.data().name).filter(Boolean);
|
|
1079
|
+
console.error(` No wizard named "${botInfo.exportName}" found. Your active wizards:`);
|
|
1080
|
+
for (const n of names) console.error(` - ${n}`);
|
|
1081
|
+
console.error("\n Rename your local export to match, or upload first.");
|
|
1082
|
+
}
|
|
1065
1083
|
process.exit(1);
|
|
1066
1084
|
}
|
|
1067
1085
|
const wizardId = wizardSnap.docs[0].id;
|
|
@@ -1077,7 +1095,7 @@ async function runPull(options) {
|
|
|
1077
1095
|
console.error(" Source code is empty. Re-upload to store it.");
|
|
1078
1096
|
process.exit(1);
|
|
1079
1097
|
}
|
|
1080
|
-
|
|
1098
|
+
fs5.writeFileSync(botInfo.sourcePath, sourceCode);
|
|
1081
1099
|
console.log(` \u2713 Updated ${botInfo.sourcePath}`);
|
|
1082
1100
|
console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
|
|
1083
1101
|
} catch (err) {
|
|
@@ -1090,6 +1108,60 @@ async function runPull(options) {
|
|
|
1090
1108
|
}
|
|
1091
1109
|
}
|
|
1092
1110
|
|
|
1111
|
+
// src/commands/feedback.ts
|
|
1112
|
+
async function runFeedback(options) {
|
|
1113
|
+
const message = options.message.trim();
|
|
1114
|
+
if (!message) {
|
|
1115
|
+
console.error(" Error: feedback message cannot be empty.");
|
|
1116
|
+
process.exit(1);
|
|
1117
|
+
}
|
|
1118
|
+
console.log("\n Submitting feedback...");
|
|
1119
|
+
try {
|
|
1120
|
+
const { initializeApp: initializeApp2 } = await import("firebase/app");
|
|
1121
|
+
const { getFunctions, httpsCallable } = await import("firebase/functions");
|
|
1122
|
+
const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
|
|
1123
|
+
const app = initializeApp2(FIREBASE_CONFIG2);
|
|
1124
|
+
const functions = getFunctions(app, "us-central1");
|
|
1125
|
+
const submitFn = httpsCallable(functions, "submitFeedback");
|
|
1126
|
+
await submitFn({ message });
|
|
1127
|
+
console.log(" Sent! Thanks for the feedback.\n");
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1130
|
+
console.error(` Failed to submit: ${msg}
|
|
1131
|
+
`);
|
|
1132
|
+
process.exit(1);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/commands/missile-calc.ts
|
|
1137
|
+
import { calculateMissileCastTime, GCD_DURATION, TICKS_PER_SECOND, calculateMissileRadius } from "@vibemancer/core";
|
|
1138
|
+
function runMissileCalc(options) {
|
|
1139
|
+
const config = {
|
|
1140
|
+
damage: options.damage,
|
|
1141
|
+
speed: options.speed,
|
|
1142
|
+
duration: options.duration,
|
|
1143
|
+
turnRate: options.turnRate
|
|
1144
|
+
};
|
|
1145
|
+
const castTimeSec = calculateMissileCastTime(config);
|
|
1146
|
+
const castFrames = Math.max(1, Math.round(castTimeSec * TICKS_PER_SECOND));
|
|
1147
|
+
const gcdSec = GCD_DURATION / TICKS_PER_SECOND;
|
|
1148
|
+
const totalCycleSec = castTimeSec + gcdSec;
|
|
1149
|
+
const dps = config.damage / totalCycleSec;
|
|
1150
|
+
const range = config.speed * config.duration;
|
|
1151
|
+
const radius = calculateMissileRadius(config.damage);
|
|
1152
|
+
console.log(`
|
|
1153
|
+
Missile Calculator
|
|
1154
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1155
|
+
Config: damage=${config.damage} speed=${config.speed} duration=${config.duration} turnRate=${config.turnRate}
|
|
1156
|
+
Cast time: ${castTimeSec.toFixed(3)}s (${castFrames} frames)
|
|
1157
|
+
GCD: ${gcdSec}s (${GCD_DURATION} frames)
|
|
1158
|
+
Full cycle: ${totalCycleSec.toFixed(3)}s (cast + GCD)
|
|
1159
|
+
Eff. DPS: ${dps.toFixed(2)} HP/s
|
|
1160
|
+
Range: ${range} units (speed \xD7 duration)
|
|
1161
|
+
Hitbox: ${radius.toFixed(2)} radius
|
|
1162
|
+
`);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1093
1165
|
// src/cli.ts
|
|
1094
1166
|
var args = process.argv.slice(2);
|
|
1095
1167
|
var command = args[0];
|
|
@@ -1128,6 +1200,8 @@ Commands:
|
|
|
1128
1200
|
build Compile bot to a standalone bundle
|
|
1129
1201
|
upload Upload bot to VibeMancer for online competition
|
|
1130
1202
|
pull Download latest source code from VibeMancer
|
|
1203
|
+
feedback Submit a bug report or suggestion
|
|
1204
|
+
missile-calc Calculate missile cast time and DPS for a config
|
|
1131
1205
|
|
|
1132
1206
|
Common options:
|
|
1133
1207
|
--bot <path> Path to bot source file (default: auto-discover)
|
|
@@ -1136,11 +1210,12 @@ Dev options:
|
|
|
1136
1210
|
--port <n> Server port (default: 4242)
|
|
1137
1211
|
|
|
1138
1212
|
Fight options:
|
|
1139
|
-
--opponent <name> Fight a single opponent
|
|
1213
|
+
--opponent <name> Fight a single opponent: a built-in name OR another
|
|
1214
|
+
user's uploaded bot as handle/botname
|
|
1140
1215
|
--seed <n> Random seed
|
|
1141
1216
|
|
|
1142
1217
|
Trace options:
|
|
1143
|
-
--opponent <name> Opponent
|
|
1218
|
+
--opponent <name> Opponent: a built-in name OR handle/botname (required)
|
|
1144
1219
|
--seed <n> Random seed (default: 1)
|
|
1145
1220
|
--distance <n> Spawn distance (default: 600)
|
|
1146
1221
|
|
|
@@ -1163,6 +1238,8 @@ Examples:
|
|
|
1163
1238
|
vibemancer tournament Battlemage Warmage Archmage
|
|
1164
1239
|
vibemancer optimize
|
|
1165
1240
|
vibemancer build --opponent Battlemage
|
|
1241
|
+
vibemancer feedback "missiles go through walls sometimes"
|
|
1242
|
+
vibemancer missile-calc --damage 15 --speed 7 --duration 200 --turnRate 3
|
|
1166
1243
|
`);
|
|
1167
1244
|
}
|
|
1168
1245
|
async function main() {
|
|
@@ -1192,7 +1269,14 @@ async function main() {
|
|
|
1192
1269
|
case "trace": {
|
|
1193
1270
|
const opponent = parseFlag("--opponent");
|
|
1194
1271
|
if (!opponent) {
|
|
1195
|
-
|
|
1272
|
+
const { getBuiltinBotNames: getBuiltinBotNames2 } = await import("./opponent-resolver-U2UK2QEJ.js");
|
|
1273
|
+
const names = getBuiltinBotNames2();
|
|
1274
|
+
const list = names.map((n) => ` --opponent ${n}`).join("\n");
|
|
1275
|
+
console.error("Error: --opponent is required for trace.\n");
|
|
1276
|
+
console.error(`Available opponents:
|
|
1277
|
+
|
|
1278
|
+
${list}
|
|
1279
|
+
`);
|
|
1196
1280
|
console.error("Usage: vibemancer trace --opponent Battlemage");
|
|
1197
1281
|
process.exit(1);
|
|
1198
1282
|
}
|
|
@@ -1254,6 +1338,20 @@ async function main() {
|
|
|
1254
1338
|
await runPull({ bot });
|
|
1255
1339
|
break;
|
|
1256
1340
|
}
|
|
1341
|
+
case "feedback": {
|
|
1342
|
+
const message = args.slice(1).join(" ");
|
|
1343
|
+
await runFeedback({ message });
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
case "missile-calc": {
|
|
1347
|
+
runMissileCalc({
|
|
1348
|
+
damage: parseIntFlag("--damage", 15),
|
|
1349
|
+
speed: parseIntFlag("--speed", 7),
|
|
1350
|
+
duration: parseIntFlag("--duration", 200),
|
|
1351
|
+
turnRate: parseIntFlag("--turnRate", 0)
|
|
1352
|
+
});
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1257
1355
|
default:
|
|
1258
1356
|
console.error(`Unknown command: ${command}`);
|
|
1259
1357
|
printHelp();
|