vibemancer 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +258 -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,70 @@ 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
|
+
function isHandleSelector(opponent) {
|
|
341
|
+
const trimmed = opponent.trim();
|
|
342
|
+
const slash = trimmed.indexOf("/");
|
|
343
|
+
return slash > 0 && slash < trimmed.length - 1;
|
|
344
|
+
}
|
|
345
|
+
var cachedDb = null;
|
|
346
|
+
var cachedStorage = null;
|
|
347
|
+
function getApp() {
|
|
348
|
+
const apps = getApps();
|
|
349
|
+
return apps.length > 0 ? apps[0] : initializeApp(FIREBASE_CONFIG);
|
|
350
|
+
}
|
|
351
|
+
function getDb() {
|
|
352
|
+
if (!cachedDb) {
|
|
353
|
+
cachedDb = getFirestore(getApp());
|
|
354
|
+
if (process.env.VIBEMANCER_EMULATOR === "1") connectFirestoreEmulator(cachedDb, "127.0.0.1", 8085);
|
|
355
|
+
}
|
|
356
|
+
return cachedDb;
|
|
357
|
+
}
|
|
358
|
+
function getBucket() {
|
|
359
|
+
if (!cachedStorage) {
|
|
360
|
+
cachedStorage = getStorage(getApp());
|
|
361
|
+
if (process.env.VIBEMANCER_EMULATOR === "1") connectStorageEmulator(cachedStorage, "127.0.0.1", 9199);
|
|
362
|
+
}
|
|
363
|
+
return cachedStorage;
|
|
364
|
+
}
|
|
365
|
+
async function resolveRemoteOpponent(selector) {
|
|
366
|
+
const slash = selector.indexOf("/");
|
|
367
|
+
const handle = selector.slice(0, slash).trim().toLowerCase();
|
|
368
|
+
const botName = selector.slice(slash + 1).trim();
|
|
369
|
+
if (!handle || !botName) {
|
|
370
|
+
throw new Error(`Invalid opponent "${selector}" \u2014 expected handle/botname (e.g. happy-golden-banana/FireMage).`);
|
|
371
|
+
}
|
|
372
|
+
const db = getDb();
|
|
373
|
+
const snap = await getDocs(query(
|
|
374
|
+
collection(db, "wizards"),
|
|
375
|
+
where("ownerHandle", "==", handle),
|
|
376
|
+
where("nameLower", "==", botName.toLowerCase()),
|
|
377
|
+
where("active", "==", true),
|
|
378
|
+
limit(1)
|
|
379
|
+
));
|
|
380
|
+
if (snap.empty) {
|
|
381
|
+
throw new Error(`No active bot "${botName}" found for handle "${handle}". Check the handle + bot name (both case-insensitive) on the leaderboard.`);
|
|
382
|
+
}
|
|
383
|
+
const docSnap = snap.docs[0];
|
|
384
|
+
const data = docSnap.data();
|
|
385
|
+
const exportName = typeof data.exportName === "string" ? data.exportName : "";
|
|
386
|
+
if (!exportName) {
|
|
387
|
+
throw new Error(`Bot "${handle}/${botName}" is missing its export name.`);
|
|
388
|
+
}
|
|
389
|
+
const bundlePath = typeof data.bundlePath === "string" ? data.bundlePath : `bundles/${docSnap.id}.js`;
|
|
390
|
+
const bytes = await getBytes(ref(getBucket(), bundlePath));
|
|
391
|
+
const bundle = new TextDecoder().decode(bytes);
|
|
392
|
+
return { bundle, exportName, label: `${handle}/${botName}` };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/commands/fight.ts
|
|
408
396
|
async function runFight(options) {
|
|
409
397
|
if (options.opponent) {
|
|
410
398
|
return runSingleFight({ ...options, opponent: options.opponent });
|
|
@@ -418,13 +406,21 @@ async function runSingleFight(options) {
|
|
|
418
406
|
Bot: ${botInfo.exportName}`);
|
|
419
407
|
console.log(` Opponent: ${options.opponent}
|
|
420
408
|
`);
|
|
421
|
-
const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
|
|
422
|
-
const opponentBundle = resolveOpponent(options.opponent);
|
|
423
409
|
const start = Date.now();
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
410
|
+
let result;
|
|
411
|
+
if (isHandleSelector(options.opponent)) {
|
|
412
|
+
console.log(" Resolving uploaded opponent...");
|
|
413
|
+
const remote = await resolveRemoteOpponent(options.opponent);
|
|
414
|
+
const userBundle = await compileSingleBotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
415
|
+
result = await runBundleFight(userBundle, remote.bundle, { seed: options.seed ?? 1 });
|
|
416
|
+
} else {
|
|
417
|
+
const userBundle = new BotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
418
|
+
const opponentBundle = resolveOpponent(options.opponent);
|
|
419
|
+
result = await sandboxFight(userBundle, opponentBundle, {
|
|
420
|
+
seed: options.seed,
|
|
421
|
+
compileOptions: getCoreCompileOptions()
|
|
422
|
+
});
|
|
423
|
+
}
|
|
428
424
|
const elapsed = Date.now() - start;
|
|
429
425
|
const { wizard1Wins, wizard2Wins, draws } = result;
|
|
430
426
|
const total = wizard1Wins + wizard2Wins + draws;
|
|
@@ -440,7 +436,7 @@ async function runSingleFight(options) {
|
|
|
440
436
|
async function runFullFight(options) {
|
|
441
437
|
const projectDir = process.cwd();
|
|
442
438
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
443
|
-
const userBundle = new
|
|
439
|
+
const userBundle = new BotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
444
440
|
const opponents = getBuiltinBotNames();
|
|
445
441
|
console.log(`
|
|
446
442
|
Fighting ${botInfo.exportName} against ${opponents.length} built-in bots...
|
|
@@ -494,13 +490,13 @@ async function runFullFight(options) {
|
|
|
494
490
|
console.log("");
|
|
495
491
|
}
|
|
496
492
|
function getHistoryPath(projectDir) {
|
|
497
|
-
return
|
|
493
|
+
return path2.join(projectDir, ".vibemancer", "history.json");
|
|
498
494
|
}
|
|
499
495
|
function loadHistory(projectDir) {
|
|
500
496
|
const historyPath = getHistoryPath(projectDir);
|
|
501
|
-
if (!
|
|
497
|
+
if (!fs2.existsSync(historyPath)) return [];
|
|
502
498
|
try {
|
|
503
|
-
const raw =
|
|
499
|
+
const raw = fs2.readFileSync(historyPath, "utf-8");
|
|
504
500
|
return JSON.parse(raw);
|
|
505
501
|
} catch {
|
|
506
502
|
return [];
|
|
@@ -508,10 +504,10 @@ function loadHistory(projectDir) {
|
|
|
508
504
|
}
|
|
509
505
|
function saveHistory(projectDir, history, current) {
|
|
510
506
|
const historyPath = getHistoryPath(projectDir);
|
|
511
|
-
const dir =
|
|
512
|
-
|
|
507
|
+
const dir = path2.dirname(historyPath);
|
|
508
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
513
509
|
const updated = [...history.slice(-19), current];
|
|
514
|
-
|
|
510
|
+
fs2.writeFileSync(historyPath, JSON.stringify(updated, null, " ") + "\n");
|
|
515
511
|
}
|
|
516
512
|
function showDiff(current, previous) {
|
|
517
513
|
const prevMap = new Map(previous.map((e) => [e.opponent, e]));
|
|
@@ -542,8 +538,9 @@ function showDiff(current, previous) {
|
|
|
542
538
|
|
|
543
539
|
// src/commands/trace.ts
|
|
544
540
|
import {
|
|
545
|
-
BotBundle as
|
|
541
|
+
BotBundle as BotBundle2,
|
|
546
542
|
sandboxSimulate,
|
|
543
|
+
runBundleSimulate,
|
|
547
544
|
extractTraceEvents,
|
|
548
545
|
summarizeTrace,
|
|
549
546
|
formatTraceEvents,
|
|
@@ -556,18 +553,30 @@ import {
|
|
|
556
553
|
async function runTrace(options) {
|
|
557
554
|
const projectDir = process.cwd();
|
|
558
555
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
559
|
-
const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
|
|
560
|
-
const opponentBundle = resolveOpponent(options.opponent);
|
|
561
556
|
const distance = options.distance ?? 600;
|
|
562
557
|
console.log(`
|
|
563
558
|
Trace: ${botInfo.exportName} (W1) vs ${options.opponent} (W2) | distance: ${distance} | seed: ${options.seed ?? 1}
|
|
564
559
|
`);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
560
|
+
let result;
|
|
561
|
+
if (isHandleSelector(options.opponent)) {
|
|
562
|
+
console.log(" Resolving uploaded opponent...");
|
|
563
|
+
const remote = await resolveRemoteOpponent(options.opponent);
|
|
564
|
+
const userBundle = await compileSingleBotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
565
|
+
result = await runBundleSimulate(userBundle, remote.bundle, {
|
|
566
|
+
seed: options.seed ?? 1,
|
|
567
|
+
spawnDistance: distance,
|
|
568
|
+
maxTicks: options.maxTicks ?? 3e3
|
|
569
|
+
});
|
|
570
|
+
} else {
|
|
571
|
+
const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
|
|
572
|
+
const opponentBundle = resolveOpponent(options.opponent);
|
|
573
|
+
result = await sandboxSimulate(userBundle, opponentBundle, {
|
|
574
|
+
seed: options.seed ?? 1,
|
|
575
|
+
spawnDistance: distance,
|
|
576
|
+
maxTicks: options.maxTicks ?? 3e3,
|
|
577
|
+
compileOptions: getCoreCompileOptions()
|
|
578
|
+
});
|
|
579
|
+
}
|
|
571
580
|
const history = result.history;
|
|
572
581
|
if (history.length === 0) {
|
|
573
582
|
console.log(" No history available.\n");
|
|
@@ -589,11 +598,11 @@ async function runTrace(options) {
|
|
|
589
598
|
}
|
|
590
599
|
|
|
591
600
|
// src/commands/tournament.ts
|
|
592
|
-
import { BotBundle as
|
|
601
|
+
import { BotBundle as BotBundle3, sandboxFight as sandboxFight2, scoreFight as scoreFight2, scoreFightAsWizard2 } from "@vibemancer/core";
|
|
593
602
|
async function runTournament(options) {
|
|
594
603
|
const projectDir = process.cwd();
|
|
595
604
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
596
|
-
const userBundle = new
|
|
605
|
+
const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
|
|
597
606
|
const opponentNames = options.opponents && options.opponents.length > 0 ? options.opponents : getBuiltinBotNames();
|
|
598
607
|
const participants = [
|
|
599
608
|
{ name: botInfo.exportName, bundle: userBundle }
|
|
@@ -664,7 +673,7 @@ async function runTournament(options) {
|
|
|
664
673
|
|
|
665
674
|
// src/commands/optimize.ts
|
|
666
675
|
import { readFileSync, writeFileSync } from "fs";
|
|
667
|
-
import { BotBundle as
|
|
676
|
+
import { BotBundle as BotBundle4, MatchSandbox, scoreFight as scoreFight3, generateCandidates, getEffectiveRange } from "@vibemancer/core";
|
|
668
677
|
var USEPAR_RE = /useParam\(\s*['"](\w+)['"]\s*,\s*([^,)]+?)\s*(?:,\s*\{([^}]*)\})?\s*\)/g;
|
|
669
678
|
function parseNumValue(s) {
|
|
670
679
|
const n = parseFloat(s.trim());
|
|
@@ -780,7 +789,7 @@ async function runOptimize(options) {
|
|
|
780
789
|
const maxRounds = options.rounds ?? 3;
|
|
781
790
|
const opponentNames = resolveOpponentNames(options.opponents);
|
|
782
791
|
const opponentBundles = opponentNames.map((name) => resolveOpponent(name));
|
|
783
|
-
const userBundle = new
|
|
792
|
+
const userBundle = new BotBundle4(botInfo.sourcePath, botInfo.exportName);
|
|
784
793
|
console.log(` Opponents: ${opponentBundles.length}`);
|
|
785
794
|
console.log(` Steps per param: ${steps}`);
|
|
786
795
|
console.log(` Max rounds: ${maxRounds}
|
|
@@ -848,14 +857,14 @@ function escapeRegex(s) {
|
|
|
848
857
|
}
|
|
849
858
|
|
|
850
859
|
// src/commands/build.ts
|
|
851
|
-
import
|
|
852
|
-
import
|
|
853
|
-
import { BotBundle as
|
|
860
|
+
import fs3 from "fs";
|
|
861
|
+
import path3 from "path";
|
|
862
|
+
import { BotBundle as BotBundle5, compileMatchBundle } from "@vibemancer/core";
|
|
854
863
|
async function runBuild(options) {
|
|
855
864
|
const projectDir = process.cwd();
|
|
856
865
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
857
866
|
const coreSourceDir = findCoreSourceDir();
|
|
858
|
-
const userBundle = new
|
|
867
|
+
const userBundle = new BotBundle5(botInfo.sourcePath, botInfo.exportName);
|
|
859
868
|
const opponentBundle = resolveOpponent(options.opponent);
|
|
860
869
|
console.log(`
|
|
861
870
|
Compiling ${botInfo.exportName} vs ${options.opponent}...`);
|
|
@@ -867,9 +876,9 @@ async function runBuild(options) {
|
|
|
867
876
|
});
|
|
868
877
|
const elapsed = Date.now() - start;
|
|
869
878
|
const outFile = options.output ?? `dist/${botInfo.exportName}-vs-${options.opponent}.js`;
|
|
870
|
-
const outDir =
|
|
871
|
-
|
|
872
|
-
|
|
879
|
+
const outDir = path3.dirname(path3.resolve(outFile));
|
|
880
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
881
|
+
fs3.writeFileSync(path3.resolve(outFile), bundle);
|
|
873
882
|
const sizeKb = (bundle.length / 1024).toFixed(1);
|
|
874
883
|
console.log(` Output: ${outFile} (${sizeKb} KB)`);
|
|
875
884
|
console.log(` Compiled in ${elapsed}ms
|
|
@@ -966,7 +975,7 @@ function getStyleDescription(bot) {
|
|
|
966
975
|
|
|
967
976
|
// src/commands/upload.ts
|
|
968
977
|
import { createHash } from "crypto";
|
|
969
|
-
import
|
|
978
|
+
import fs4 from "fs";
|
|
970
979
|
async function runUpload(options) {
|
|
971
980
|
const projectDir = process.cwd();
|
|
972
981
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
@@ -979,25 +988,21 @@ async function runUpload(options) {
|
|
|
979
988
|
console.error(` Error: Bundle too large (${sizeKb} KB). Max 500 KB.`);
|
|
980
989
|
process.exit(1);
|
|
981
990
|
}
|
|
982
|
-
const
|
|
983
|
-
const wizardName = nameMatch?.[1] ?? botInfo.exportName;
|
|
991
|
+
const wizardName = botInfo.exportName;
|
|
984
992
|
const bundleHash = createHash("sha256").update(bundle).digest("hex");
|
|
985
993
|
console.log(` Wizard: ${wizardName}`);
|
|
986
994
|
console.log(` Hash: ${bundleHash.slice(0, 12)}...`);
|
|
987
995
|
console.log(" Uploading to VibeMancer...");
|
|
988
996
|
try {
|
|
989
|
-
const { initializeApp } = await import("firebase/app");
|
|
997
|
+
const { initializeApp: initializeApp2 } = await import("firebase/app");
|
|
990
998
|
const { getFunctions, httpsCallable } = await import("firebase/functions");
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
authDomain: "le-vibemancer.firebaseapp.com",
|
|
994
|
-
projectId: "le-vibemancer"
|
|
995
|
-
});
|
|
999
|
+
const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
|
|
1000
|
+
const app = initializeApp2(FIREBASE_CONFIG2);
|
|
996
1001
|
const functions = getFunctions(app, "us-central1");
|
|
997
1002
|
const uploadFn = httpsCallable(functions, "uploadWizard");
|
|
998
1003
|
let sourceCode;
|
|
999
1004
|
try {
|
|
1000
|
-
sourceCode =
|
|
1005
|
+
sourceCode = fs4.readFileSync(botInfo.sourcePath, "utf-8");
|
|
1001
1006
|
} catch {
|
|
1002
1007
|
}
|
|
1003
1008
|
const result = await uploadFn({
|
|
@@ -1030,38 +1035,47 @@ async function runUpload(options) {
|
|
|
1030
1035
|
}
|
|
1031
1036
|
|
|
1032
1037
|
// src/commands/pull.ts
|
|
1033
|
-
import
|
|
1038
|
+
import fs5 from "fs";
|
|
1034
1039
|
async function runPull(options) {
|
|
1035
1040
|
const projectDir = process.cwd();
|
|
1036
1041
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
1037
1042
|
console.log(`
|
|
1038
1043
|
Pulling latest source for ${botInfo.exportName}...`);
|
|
1039
1044
|
try {
|
|
1040
|
-
const { initializeApp } = await import("firebase/app");
|
|
1045
|
+
const { initializeApp: initializeApp2 } = await import("firebase/app");
|
|
1041
1046
|
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
|
-
});
|
|
1047
|
+
const { getFirestore: getFirestore2, collection: collection2, query: query2, where: where2, getDocs: getDocs2, doc, getDoc, limit: fbLimit } = await import("firebase/firestore");
|
|
1048
|
+
const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
|
|
1049
|
+
const app = initializeApp2(FIREBASE_CONFIG2);
|
|
1048
1050
|
const auth = getAuth(app);
|
|
1049
1051
|
console.log(" Signing in...");
|
|
1050
1052
|
const cred = await signInWithPopup(auth, new GoogleAuthProvider());
|
|
1051
1053
|
const userId = cred.user.uid;
|
|
1052
1054
|
console.log(` Signed in as ${cred.user.displayName ?? cred.user.email ?? userId}`);
|
|
1053
|
-
const db =
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1055
|
+
const db = getFirestore2(app);
|
|
1056
|
+
const wizardSnap = await getDocs2(query2(
|
|
1057
|
+
collection2(db, "wizards"),
|
|
1058
|
+
where2("ownerId", "==", userId),
|
|
1059
|
+
where2("name", "==", botInfo.exportName),
|
|
1060
|
+
where2("active", "==", true),
|
|
1059
1061
|
fbLimit(1)
|
|
1060
|
-
);
|
|
1061
|
-
const wizardSnap = await getDocs(wizardQuery);
|
|
1062
|
+
));
|
|
1062
1063
|
if (wizardSnap.empty) {
|
|
1063
|
-
|
|
1064
|
-
|
|
1064
|
+
const allBots = await getDocs2(query2(
|
|
1065
|
+
collection2(db, "wizards"),
|
|
1066
|
+
where2("ownerId", "==", userId),
|
|
1067
|
+
where2("active", "==", true),
|
|
1068
|
+
fbLimit(20)
|
|
1069
|
+
));
|
|
1070
|
+
if (allBots.empty) {
|
|
1071
|
+
console.error(" No active wizards found on your account.");
|
|
1072
|
+
console.error(" Upload first with: vibemancer upload");
|
|
1073
|
+
} else {
|
|
1074
|
+
const names = allBots.docs.map((d) => d.data().name).filter(Boolean);
|
|
1075
|
+
console.error(` No wizard named "${botInfo.exportName}" found. Your active wizards:`);
|
|
1076
|
+
for (const n of names) console.error(` - ${n}`);
|
|
1077
|
+
console.error("\n Rename your local export to match, or upload first.");
|
|
1078
|
+
}
|
|
1065
1079
|
process.exit(1);
|
|
1066
1080
|
}
|
|
1067
1081
|
const wizardId = wizardSnap.docs[0].id;
|
|
@@ -1077,7 +1091,7 @@ async function runPull(options) {
|
|
|
1077
1091
|
console.error(" Source code is empty. Re-upload to store it.");
|
|
1078
1092
|
process.exit(1);
|
|
1079
1093
|
}
|
|
1080
|
-
|
|
1094
|
+
fs5.writeFileSync(botInfo.sourcePath, sourceCode);
|
|
1081
1095
|
console.log(` \u2713 Updated ${botInfo.sourcePath}`);
|
|
1082
1096
|
console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
|
|
1083
1097
|
} catch (err) {
|
|
@@ -1090,6 +1104,60 @@ async function runPull(options) {
|
|
|
1090
1104
|
}
|
|
1091
1105
|
}
|
|
1092
1106
|
|
|
1107
|
+
// src/commands/feedback.ts
|
|
1108
|
+
async function runFeedback(options) {
|
|
1109
|
+
const message = options.message.trim();
|
|
1110
|
+
if (!message) {
|
|
1111
|
+
console.error(" Error: feedback message cannot be empty.");
|
|
1112
|
+
process.exit(1);
|
|
1113
|
+
}
|
|
1114
|
+
console.log("\n Submitting feedback...");
|
|
1115
|
+
try {
|
|
1116
|
+
const { initializeApp: initializeApp2 } = await import("firebase/app");
|
|
1117
|
+
const { getFunctions, httpsCallable } = await import("firebase/functions");
|
|
1118
|
+
const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
|
|
1119
|
+
const app = initializeApp2(FIREBASE_CONFIG2);
|
|
1120
|
+
const functions = getFunctions(app, "us-central1");
|
|
1121
|
+
const submitFn = httpsCallable(functions, "submitFeedback");
|
|
1122
|
+
await submitFn({ message });
|
|
1123
|
+
console.log(" Sent! Thanks for the feedback.\n");
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1126
|
+
console.error(` Failed to submit: ${msg}
|
|
1127
|
+
`);
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/commands/missile-calc.ts
|
|
1133
|
+
import { calculateMissileCastTime, GCD_DURATION, TICKS_PER_SECOND, calculateMissileRadius } from "@vibemancer/core";
|
|
1134
|
+
function runMissileCalc(options) {
|
|
1135
|
+
const config = {
|
|
1136
|
+
damage: options.damage,
|
|
1137
|
+
speed: options.speed,
|
|
1138
|
+
duration: options.duration,
|
|
1139
|
+
turnRate: options.turnRate
|
|
1140
|
+
};
|
|
1141
|
+
const castTimeSec = calculateMissileCastTime(config);
|
|
1142
|
+
const castFrames = Math.max(1, Math.round(castTimeSec * TICKS_PER_SECOND));
|
|
1143
|
+
const gcdSec = GCD_DURATION / TICKS_PER_SECOND;
|
|
1144
|
+
const totalCycleSec = castTimeSec + gcdSec;
|
|
1145
|
+
const dps = config.damage / totalCycleSec;
|
|
1146
|
+
const range = config.speed * config.duration;
|
|
1147
|
+
const radius = calculateMissileRadius(config.damage);
|
|
1148
|
+
console.log(`
|
|
1149
|
+
Missile Calculator
|
|
1150
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1151
|
+
Config: damage=${config.damage} speed=${config.speed} duration=${config.duration} turnRate=${config.turnRate}
|
|
1152
|
+
Cast time: ${castTimeSec.toFixed(3)}s (${castFrames} frames)
|
|
1153
|
+
GCD: ${gcdSec}s (${GCD_DURATION} frames)
|
|
1154
|
+
Full cycle: ${totalCycleSec.toFixed(3)}s (cast + GCD)
|
|
1155
|
+
Eff. DPS: ${dps.toFixed(2)} HP/s
|
|
1156
|
+
Range: ${range} units (speed \xD7 duration)
|
|
1157
|
+
Hitbox: ${radius.toFixed(2)} radius
|
|
1158
|
+
`);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1093
1161
|
// src/cli.ts
|
|
1094
1162
|
var args = process.argv.slice(2);
|
|
1095
1163
|
var command = args[0];
|
|
@@ -1128,6 +1196,8 @@ Commands:
|
|
|
1128
1196
|
build Compile bot to a standalone bundle
|
|
1129
1197
|
upload Upload bot to VibeMancer for online competition
|
|
1130
1198
|
pull Download latest source code from VibeMancer
|
|
1199
|
+
feedback Submit a bug report or suggestion
|
|
1200
|
+
missile-calc Calculate missile cast time and DPS for a config
|
|
1131
1201
|
|
|
1132
1202
|
Common options:
|
|
1133
1203
|
--bot <path> Path to bot source file (default: auto-discover)
|
|
@@ -1136,11 +1206,12 @@ Dev options:
|
|
|
1136
1206
|
--port <n> Server port (default: 4242)
|
|
1137
1207
|
|
|
1138
1208
|
Fight options:
|
|
1139
|
-
--opponent <name> Fight a single opponent
|
|
1209
|
+
--opponent <name> Fight a single opponent: a built-in name OR another
|
|
1210
|
+
user's uploaded bot as handle/botname
|
|
1140
1211
|
--seed <n> Random seed
|
|
1141
1212
|
|
|
1142
1213
|
Trace options:
|
|
1143
|
-
--opponent <name> Opponent
|
|
1214
|
+
--opponent <name> Opponent: a built-in name OR handle/botname (required)
|
|
1144
1215
|
--seed <n> Random seed (default: 1)
|
|
1145
1216
|
--distance <n> Spawn distance (default: 600)
|
|
1146
1217
|
|
|
@@ -1163,6 +1234,8 @@ Examples:
|
|
|
1163
1234
|
vibemancer tournament Battlemage Warmage Archmage
|
|
1164
1235
|
vibemancer optimize
|
|
1165
1236
|
vibemancer build --opponent Battlemage
|
|
1237
|
+
vibemancer feedback "missiles go through walls sometimes"
|
|
1238
|
+
vibemancer missile-calc --damage 15 --speed 7 --duration 200 --turnRate 3
|
|
1166
1239
|
`);
|
|
1167
1240
|
}
|
|
1168
1241
|
async function main() {
|
|
@@ -1192,7 +1265,14 @@ async function main() {
|
|
|
1192
1265
|
case "trace": {
|
|
1193
1266
|
const opponent = parseFlag("--opponent");
|
|
1194
1267
|
if (!opponent) {
|
|
1195
|
-
|
|
1268
|
+
const { getBuiltinBotNames: getBuiltinBotNames2 } = await import("./opponent-resolver-U2UK2QEJ.js");
|
|
1269
|
+
const names = getBuiltinBotNames2();
|
|
1270
|
+
const list = names.map((n) => ` --opponent ${n}`).join("\n");
|
|
1271
|
+
console.error("Error: --opponent is required for trace.\n");
|
|
1272
|
+
console.error(`Available opponents:
|
|
1273
|
+
|
|
1274
|
+
${list}
|
|
1275
|
+
`);
|
|
1196
1276
|
console.error("Usage: vibemancer trace --opponent Battlemage");
|
|
1197
1277
|
process.exit(1);
|
|
1198
1278
|
}
|
|
@@ -1254,6 +1334,20 @@ async function main() {
|
|
|
1254
1334
|
await runPull({ bot });
|
|
1255
1335
|
break;
|
|
1256
1336
|
}
|
|
1337
|
+
case "feedback": {
|
|
1338
|
+
const message = args.slice(1).join(" ");
|
|
1339
|
+
await runFeedback({ message });
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
case "missile-calc": {
|
|
1343
|
+
runMissileCalc({
|
|
1344
|
+
damage: parseIntFlag("--damage", 15),
|
|
1345
|
+
speed: parseIntFlag("--speed", 7),
|
|
1346
|
+
duration: parseIntFlag("--duration", 200),
|
|
1347
|
+
turnRate: parseIntFlag("--turnRate", 0)
|
|
1348
|
+
});
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1257
1351
|
default:
|
|
1258
1352
|
console.error(`Unknown command: ${command}`);
|
|
1259
1353
|
printHelp();
|