@vibemancer/core 1.0.10 → 1.0.11

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.
@@ -213,7 +213,7 @@ function currentRuleset() {
213
213
  }
214
214
 
215
215
  // src/engine-version.ts
216
- var ENGINE_VERSION = 293192982913104;
216
+ var ENGINE_VERSION = 2983169471988242;
217
217
  var EQUIVALENT_ENGINE_VERSIONS = [1688330189011034];
218
218
 
219
219
  // src/replay-compat.ts
@@ -520,7 +520,51 @@ function extractMissileAction(action) {
520
520
  return action._toMissileAction();
521
521
  }
522
522
  function extractAction(finalAction) {
523
- return finalAction._toAction();
523
+ if (finalAction === null || finalAction === void 0 || typeof finalAction._toAction !== "function") {
524
+ const describeReturned = (value) => {
525
+ if (value === null) return "null";
526
+ if (value === void 0) return "undefined";
527
+ if (Array.isArray(value)) return "an array";
528
+ if (typeof value === "object") {
529
+ if ("then" in value && typeof value.then === "function") {
530
+ return "a Promise \u2014 your bot function is `async`, and it must not be: remove the async keyword";
531
+ }
532
+ return "a plain object";
533
+ }
534
+ return `a ${typeof value}`;
535
+ };
536
+ const got = describeReturned(finalAction);
537
+ throw new Error(
538
+ `Your bot returned ${got}. Return one of: idle(), move(x, y), missile(config, ai, angle), shield(), blink(x, y), cancel() or aim(degrees). A bot must return an action every tick \u2014 returning nothing throws, it does not skip the tick.`
539
+ );
540
+ }
541
+ const action = finalAction._toAction();
542
+ if (action === null || typeof action !== "object" || typeof action.move !== "object" || action.move === null) {
543
+ throw new Error(
544
+ "Your bot returned something that is not a valid action. Build actions with the provided functions \u2014 idle(), move(x, y), missile(config, ai, angle), shield(), blink(x, y), cancel() or aim(degrees) \u2014 rather than constructing the object yourself."
545
+ );
546
+ }
547
+ return normaliseActionNumbers(action);
548
+ }
549
+ function normaliseActionNumbers(action) {
550
+ const num = (value) => Number(value);
551
+ const normalised = {
552
+ ...action,
553
+ move: { x: num(action.move?.x), y: num(action.move?.y) }
554
+ };
555
+ if (action.aimDirection !== void 0) normalised.aimDirection = num(action.aimDirection);
556
+ if (action.startCast?.spell === "missile") {
557
+ normalised.startCast = {
558
+ ...action.startCast,
559
+ ...action.startCast.direction !== void 0 ? { direction: num(action.startCast.direction) } : {}
560
+ };
561
+ } else if (action.startCast?.spell === "blink") {
562
+ normalised.startCast = {
563
+ ...action.startCast,
564
+ target: { x: num(action.startCast.target?.x), y: num(action.startCast.target?.y) }
565
+ };
566
+ }
567
+ return normalised;
524
568
  }
525
569
 
526
570
  // src/engine/physics.ts
@@ -551,6 +595,27 @@ function moveProjectile(projectile, deltaTicks) {
551
595
  y: projectile.position.y + Math.sin(radians) * projectile.speed * deltaTicks
552
596
  };
553
597
  }
598
+ function clampToSafeZone(position, radius) {
599
+ const min = ARENA_MIN + radius;
600
+ const max = ARENA_MAX - radius;
601
+ const centre = ARENA_SIZE / 2;
602
+ const axis = (value) => {
603
+ if (!Number.isFinite(value)) return centre;
604
+ return Math.max(min, Math.min(max, value));
605
+ };
606
+ return { x: axis(position?.x), y: axis(position?.y) };
607
+ }
608
+ function moveTowardSafely(from, target) {
609
+ const dx = target?.x - from?.x;
610
+ const dy = target?.y - from?.y;
611
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) return { x: 0, y: 0 };
612
+ const distance = Math.sqrt(dx * dx + dy * dy);
613
+ if (distance === 0) return { x: 0, y: 0 };
614
+ if (distance >= RULES.MOVEMENT_SPEED) return { x: dx / distance, y: dy / distance };
615
+ const travel = Math.max(0, distance - 1e-6);
616
+ const fraction = travel / RULES.MOVEMENT_SPEED;
617
+ return { x: dx / distance * fraction, y: dy / distance * fraction };
618
+ }
554
619
  function clampToArena(position, radius) {
555
620
  return {
556
621
  x: Math.max(radius, Math.min(ARENA_SIZE - radius, position.x)),
@@ -729,6 +794,69 @@ function createEntitySeed(matchSeed, entityId, tick2) {
729
794
  return combined;
730
795
  }
731
796
 
797
+ // src/engine/bot-error-capture.ts
798
+ var MAX_MESSAGE_LENGTH = 1e3;
799
+ var MAX_ERRORS_PER_MATCH = 500;
800
+ function describeThrown(thrown) {
801
+ return truncate(rawDescription(thrown));
802
+ }
803
+ function rawDescription(thrown) {
804
+ if (thrown instanceof Error) {
805
+ const message = typeof thrown.message === "string" ? thrown.message : "";
806
+ if (message.length > MAX_MESSAGE_LENGTH) {
807
+ return truncate(message) + " [stack omitted: message too large]";
808
+ }
809
+ const stack = typeof thrown.stack === "string" ? stripBundleCoordinates(thrown.stack.trim()) : "";
810
+ if (stack) return stack;
811
+ return message || `${thrown.name || "Error"} (thrown with no message)`;
812
+ }
813
+ if (typeof thrown === "string") return thrown || "(empty string thrown)";
814
+ if (thrown === null) return "null was thrown";
815
+ if (thrown === void 0) return "undefined was thrown";
816
+ try {
817
+ const json = JSON.stringify(thrown);
818
+ if (typeof json === "string" && json !== "{}") return json;
819
+ } catch {
820
+ }
821
+ try {
822
+ const text = String(thrown);
823
+ if (text === "[object Object]") return describeOpaqueObject(thrown);
824
+ return text;
825
+ } catch {
826
+ return "(a value was thrown that could not be described)";
827
+ }
828
+ }
829
+ function describeOpaqueObject(thrown) {
830
+ let name = "object";
831
+ try {
832
+ const ctor = thrown.constructor;
833
+ if (ctor && typeof ctor.name === "string" && ctor.name) name = ctor.name;
834
+ } catch {
835
+ }
836
+ let keys = [];
837
+ try {
838
+ keys = Object.keys(thrown).slice(0, 12);
839
+ } catch {
840
+ }
841
+ const shape = keys.length > 0 ? ` with keys: ${keys.join(", ")}` : " with no enumerable keys";
842
+ return `a non-serialisable ${name} was thrown${shape} (it could not be converted to text \u2014 usually a circular reference, or a getter that throws)`;
843
+ }
844
+ function stripBundleCoordinates(stack) {
845
+ return stack.replace(/\s*\((?:[^()\s]*match-bundle\.js|<isolated-vm>)[^()]*\)/g, "");
846
+ }
847
+ function truncate(message) {
848
+ if (message.length <= MAX_MESSAGE_LENGTH) return message;
849
+ const dropped = message.length - MAX_MESSAGE_LENGTH;
850
+ return `${message.slice(0, MAX_MESSAGE_LENGTH)}\u2026 [truncated, ${dropped} more characters]`;
851
+ }
852
+ function cappedNotice(entityId, tick2) {
853
+ return {
854
+ tick: tick2,
855
+ entityId,
856
+ message: `\u2026 stopped recording errors after ${MAX_ERRORS_PER_MATCH} in this match. The bot is still being called and still failing; only the recording stopped.`
857
+ };
858
+ }
859
+
732
860
  // src/engine/simulation.ts
733
861
  function createInitialState(_seed, spawnDist = SPAWN_DISTANCE) {
734
862
  const center = { x: ARENA_SIZE / 2, y: ARENA_SIZE / 2 };
@@ -815,6 +943,7 @@ function buildWizardContext(state, config, random) {
815
943
  function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, missileAIs, matchSeed, budgets) {
816
944
  const nextTick = currentTick + 1;
817
945
  const errors = [];
946
+ const budgetExhausted = [];
818
947
  const events = [];
819
948
  const random1 = createRandom(createEntitySeed(matchSeed, wizards[0].id, nextTick));
820
949
  const random2 = createRandom(createEntitySeed(matchSeed, wizards[1].id, nextTick));
@@ -822,13 +951,25 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
822
951
  const nextBudgets = budgets ? [budgets.states[0], budgets.states[1]] : void 0;
823
952
  const runBot = (index, entityId, invoke) => {
824
953
  const state = nextBudgets?.[index];
825
- if (state && !mayAct(state)) return IDLE_ACTION();
954
+ if (state && !mayAct(state)) {
955
+ if (!state.reported) {
956
+ state.reported = true;
957
+ budgetExhausted.push({ tick: nextTick, entityId, spentMs: state.spentMs });
958
+ }
959
+ return IDLE_ACTION();
960
+ }
826
961
  const startedAt = state ? Date.now() : 0;
827
962
  let action;
828
963
  try {
829
964
  action = invoke();
830
965
  } catch (e) {
831
- errors.push({ tick: nextTick, entityId, message: e instanceof Error ? e.message : String(e) });
966
+ const alreadyDead = (wizards[index]?.health ?? 1) <= 0;
967
+ errors.push({
968
+ tick: nextTick,
969
+ entityId,
970
+ message: describeThrown(e),
971
+ ...alreadyDead ? { afterDeath: true } : {}
972
+ });
832
973
  action = IDLE_ACTION();
833
974
  }
834
975
  if (state && budgets) {
@@ -911,10 +1052,12 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
911
1052
  delete wizard.missileConfig;
912
1053
  delete wizard.missileAI;
913
1054
  } else if (spell === "blink" && wizard.blinkTarget) {
914
- const dx = wizard.blinkTarget.x - wizard.position.x;
915
- const dy = wizard.blinkTarget.y - wizard.position.y;
1055
+ const targetX = Number.isFinite(wizard.blinkTarget.x) ? wizard.blinkTarget.x : wizard.position.x;
1056
+ const targetY = Number.isFinite(wizard.blinkTarget.y) ? wizard.blinkTarget.y : wizard.position.y;
1057
+ const dx = targetX - wizard.position.x;
1058
+ const dy = targetY - wizard.position.y;
916
1059
  const distance = Math.sqrt(dx * dx + dy * dy);
917
- let targetPos = wizard.blinkTarget;
1060
+ let targetPos = { x: targetX, y: targetY };
918
1061
  if (distance > RULES.BLINK_RANGE) {
919
1062
  const scale = RULES.BLINK_RANGE / distance;
920
1063
  targetPos = {
@@ -1043,12 +1186,12 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
1043
1186
  () => extractMissileAction(withMissileContext(missileCtx, ai))
1044
1187
  ) ?? {};
1045
1188
  } catch (e) {
1046
- errors.push({ tick: nextTick, entityId: projectile.id, message: e instanceof Error ? e.message : String(e) });
1189
+ errors.push({ tick: nextTick, entityId: projectile.id, message: describeThrown(e) });
1047
1190
  }
1048
1191
  targetAngle = missileActions.turnToward ? angleTo(projectile.position, missileActions.turnToward) : missileActions.turnToAngle !== void 0 ? missileActions.turnToAngle : null;
1049
1192
  }
1050
1193
  }
1051
- if (targetAngle !== null) {
1194
+ if (targetAngle !== null && Number.isFinite(targetAngle)) {
1052
1195
  const diff = angleDiff(projectile.rotation, targetAngle);
1053
1196
  const turn = Math.max(-projectile.turnRate, Math.min(projectile.turnRate, diff));
1054
1197
  projectile.rotation = normalizeAngle(projectile.rotation + turn);
@@ -1109,6 +1252,7 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
1109
1252
  projectiles: remainingProjectiles,
1110
1253
  events,
1111
1254
  errors,
1255
+ budgetExhausted,
1112
1256
  budgets: nextBudgets
1113
1257
  };
1114
1258
  }
@@ -1248,6 +1392,7 @@ var FIGHT_SPAWN_DISTANCES = [500, 550, 600, 650, 700];
1248
1392
  function fight(wizard1AI, wizard2AI, options = {}) {
1249
1393
  const matches = [];
1250
1394
  const allErrors = [];
1395
+ const allBudgetExhausted = [];
1251
1396
  let matchNumber = 1;
1252
1397
  let wizard1Wins = 0;
1253
1398
  let wizard2Wins = 0;
@@ -1262,6 +1407,7 @@ function fight(wizard1AI, wizard2AI, options = {}) {
1262
1407
  });
1263
1408
  matches.push(result);
1264
1409
  allErrors.push(...result.errors.map((e) => ({ ...e, match: matchNumber })));
1410
+ allBudgetExhausted.push(...result.budgetExhausted.map((b) => ({ ...b, match: matchNumber })));
1265
1411
  matchNumber++;
1266
1412
  if (result.winner === "wizard-1") {
1267
1413
  wizard1Wins++;
@@ -1282,6 +1428,7 @@ function fight(wizard1AI, wizard2AI, options = {}) {
1282
1428
  budget.states = [swappedBudget.states[1], swappedBudget.states[0]];
1283
1429
  }
1284
1430
  allErrors.push(...swapped.errors.map((e) => ({ ...e, entityId: swapSide(e.entityId), match: matchNumber })));
1431
+ allBudgetExhausted.push(...swapped.budgetExhausted.map((b) => ({ ...b, entityId: swapSide(b.entityId), match: matchNumber })));
1285
1432
  matchNumber++;
1286
1433
  if (swapped.winner === "wizard-1") {
1287
1434
  wizard2Wins++;
@@ -1292,7 +1439,7 @@ function fight(wizard1AI, wizard2AI, options = {}) {
1292
1439
  }
1293
1440
  }
1294
1441
  const winner = wizard1Wins > wizard2Wins ? "wizard-1" : wizard2Wins > wizard1Wins ? "wizard-2" : "draw";
1295
- return { wizard1Wins, wizard2Wins, draws, winner, matches, allErrors };
1442
+ return { wizard1Wins, wizard2Wins, draws, winner, matches, allErrors, budgetExhausted: allBudgetExhausted };
1296
1443
  }
1297
1444
  function simulate(wizard1AI, wizard2AI, options = {}) {
1298
1445
  if (typeof wizard1AI !== "function") {
@@ -1345,6 +1492,7 @@ function simulate(wizard1AI, wizard2AI, options = {}) {
1345
1492
  let projectiles = [];
1346
1493
  const missileAIs = /* @__PURE__ */ new Map();
1347
1494
  const allErrors = [];
1495
+ const allBudgetExhausted = [];
1348
1496
  let deathTick = null;
1349
1497
  const sharedBudget = options.budget;
1350
1498
  const budgetLimits = sharedBudget?.limits ?? options.budgetLimits;
@@ -1368,7 +1516,15 @@ function simulate(wizard1AI, wizard2AI, options = {}) {
1368
1516
  currentTick = result.nextTick;
1369
1517
  wizards = result.wizards;
1370
1518
  projectiles = result.projectiles;
1371
- if (result.errors.length > 0) allErrors.push(...result.errors);
1519
+ for (const error of result.errors) {
1520
+ if (allErrors.length > MAX_ERRORS_PER_MATCH) break;
1521
+ if (allErrors.length === MAX_ERRORS_PER_MATCH) {
1522
+ allErrors.push(cappedNotice(error.entityId, error.tick));
1523
+ break;
1524
+ }
1525
+ allErrors.push(error);
1526
+ }
1527
+ if (result.budgetExhausted.length > 0) allBudgetExhausted.push(...result.budgetExhausted);
1372
1528
  if (!skipHistory) {
1373
1529
  history.push(getPlayerState(0, wizards, projectiles, currentTick, result.events));
1374
1530
  }
@@ -1393,7 +1549,8 @@ function simulate(wizard1AI, wizard2AI, options = {}) {
1393
1549
  ticks: currentTick,
1394
1550
  finalState,
1395
1551
  history: skipHistory ? [] : history,
1396
- errors: allErrors
1552
+ errors: allErrors,
1553
+ budgetExhausted: allBudgetExhausted
1397
1554
  };
1398
1555
  }
1399
1556
 
@@ -1567,7 +1724,9 @@ var ManualMatch = class {
1567
1724
  ticks: this.currentTick,
1568
1725
  finalState: this.getGameState(),
1569
1726
  history: this.history,
1570
- errors: this.allErrors
1727
+ errors: this.allErrors,
1728
+ // Manual play is unbudgeted — the clock is never read, so nobody can be cut off.
1729
+ budgetExhausted: []
1571
1730
  };
1572
1731
  }
1573
1732
  dispose() {
@@ -1975,7 +2134,8 @@ function analyzeOneThreat(targetPos, projectile, ticksUntilReady) {
1975
2134
  targetPos,
1976
2135
  canDodgeLeft,
1977
2136
  canDodgeRight,
1978
- canOutrun
2137
+ canOutrun,
2138
+ ticksToImpact
1979
2139
  );
1980
2140
  }
1981
2141
  const canBlockInTime = ticksToImpact > ticksUntilReady + RULES.SHIELD_CAST_TIME + 1;
@@ -2090,15 +2250,28 @@ function getDodgeDirection(projectile, targetPos, direction) {
2090
2250
  return { x: 0, y: 0 };
2091
2251
  }
2092
2252
  }
2093
- function calculateBestDodgeDirection(projectile, targetPos, canDodgeLeft, canDodgeRight, canOutrun) {
2094
- if (canDodgeLeft && canDodgeRight) {
2095
- return getDodgeDirection(projectile, targetPos, "left");
2096
- } else if (canDodgeLeft) {
2097
- return getDodgeDirection(projectile, targetPos, "left");
2098
- } else if (canDodgeRight) {
2099
- return getDodgeDirection(projectile, targetPos, "right");
2100
- } else if (canOutrun) {
2101
- return getDodgeDirection(projectile, targetPos, "away");
2253
+ function roomBeforeLava(from, dir) {
2254
+ const min = ARENA_MIN + COLLISION_RADIUS;
2255
+ const max = ARENA_MAX - COLLISION_RADIUS;
2256
+ const along = (pos, d, lo, hi) => {
2257
+ if (Math.abs(d) < 1e-9) return Number.POSITIVE_INFINITY;
2258
+ return d > 0 ? (hi - pos) / d : (lo - pos) / d;
2259
+ };
2260
+ return Math.max(0, Math.min(along(from.x, dir.x, min, max), along(from.y, dir.y, min, max)));
2261
+ }
2262
+ function calculateBestDodgeDirection(projectile, targetPos, canDodgeLeft, canDodgeRight, canOutrun, ticksToImpact) {
2263
+ const travel = RULES.MOVEMENT_SPEED * Math.max(1, Math.min(ticksToImpact, 600));
2264
+ const survives = (dir) => roomBeforeLava(targetPos, dir) >= travel;
2265
+ const sides = [];
2266
+ if (canDodgeLeft) sides.push(getDodgeDirection(projectile, targetPos, "left"));
2267
+ if (canDodgeRight) sides.push(getDodgeDirection(projectile, targetPos, "right"));
2268
+ const safeSides = sides.filter(survives);
2269
+ if (safeSides.length > 0) {
2270
+ return safeSides.reduce((best, dir) => roomBeforeLava(targetPos, dir) > roomBeforeLava(targetPos, best) ? dir : best);
2271
+ }
2272
+ if (canOutrun) {
2273
+ const away = getDodgeDirection(projectile, targetPos, "away");
2274
+ if (survives(away)) return away;
2102
2275
  }
2103
2276
  return null;
2104
2277
  }
@@ -10585,6 +10758,8 @@ export {
10585
10758
  extractAction,
10586
10759
  moveWizard,
10587
10760
  moveProjectile,
10761
+ clampToSafeZone,
10762
+ moveTowardSafely,
10588
10763
  clampToArena,
10589
10764
  isInLava,
10590
10765
  resolveWizardCollision,
@@ -10723,4 +10898,4 @@ export {
10723
10898
  diagnoseTrace,
10724
10899
  formatDiagnosis
10725
10900
  };
10726
- //# sourceMappingURL=chunk-OLGKLCCB.js.map
10901
+ //# sourceMappingURL=chunk-EO7JO2RZ.js.map