@vibemancer/core 1.0.11 → 1.0.13
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/dist/{chunk-EO7JO2RZ.js → chunk-KGRG7TZS.js} +80 -34
- package/dist/chunk-KGRG7TZS.js.map +1 -0
- package/dist/{index-browser-CM0uuzWp.d.ts → index-browser-Dc-Vl1HI.d.ts} +11 -5
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/engine/bot-error-capture.ts +33 -13
- package/src/engine/simulation.ts +82 -0
- package/src/engine/spells.ts +13 -1
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +35 -15
- package/src/hooks/state-hooks.ts +5 -1
- package/src/hooks/threat-analysis.ts +30 -8
- package/src/types.ts +1 -0
- package/dist/chunk-EO7JO2RZ.js.map +0 -1
|
@@ -213,7 +213,7 @@ function currentRuleset() {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
// src/engine-version.ts
|
|
216
|
-
var ENGINE_VERSION =
|
|
216
|
+
var ENGINE_VERSION = 4256327861353618;
|
|
217
217
|
var EQUIVALENT_ENGINE_VERSIONS = [1688330189011034];
|
|
218
218
|
|
|
219
219
|
// src/replay-compat.ts
|
|
@@ -516,23 +516,28 @@ function flyStraight() {
|
|
|
516
516
|
_toMissileAction: () => ({})
|
|
517
517
|
};
|
|
518
518
|
}
|
|
519
|
+
function describeReturned(value) {
|
|
520
|
+
if (value === null) return "null";
|
|
521
|
+
if (value === void 0) return "undefined";
|
|
522
|
+
if (Array.isArray(value)) return "an array";
|
|
523
|
+
if (typeof value === "object") {
|
|
524
|
+
if ("then" in value && typeof value.then === "function") {
|
|
525
|
+
return "a Promise \u2014 your function is `async`, and it must not be: remove the async keyword";
|
|
526
|
+
}
|
|
527
|
+
return "a plain object";
|
|
528
|
+
}
|
|
529
|
+
return `a ${typeof value}`;
|
|
530
|
+
}
|
|
519
531
|
function extractMissileAction(action) {
|
|
532
|
+
if (action === null || action === void 0 || typeof action._toMissileAction !== "function") {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`Your missile AI returned ${describeReturned(action)}. Return turnToward(x, y), turnToAngle(degrees) or flyStraight(). The missile AI runs once per tick for every missile you have in flight, and it must return one of those every time.`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
520
537
|
return action._toMissileAction();
|
|
521
538
|
}
|
|
522
539
|
function extractAction(finalAction) {
|
|
523
540
|
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
541
|
const got = describeReturned(finalAction);
|
|
537
542
|
throw new Error(
|
|
538
543
|
`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.`
|
|
@@ -670,7 +675,8 @@ function startCast(wizard, spell, config) {
|
|
|
670
675
|
wizard.castingSpell = spell;
|
|
671
676
|
wizard.castProgress = 0;
|
|
672
677
|
if (spell === "missile" && config) {
|
|
673
|
-
const
|
|
678
|
+
const validated = validateMissileConfig(config);
|
|
679
|
+
const castTimeSec = calculateMissileCastTime(validated, wizard.lastMissileConfig);
|
|
674
680
|
wizard.castDuration = Math.max(1, Math.round(castTimeSec * TICKS_PER_SECOND));
|
|
675
681
|
wizard.warmupMultiplier = calculateWarmupMultiplier(wizard.lastMissileConfig, config);
|
|
676
682
|
} else if (spell === "shield") {
|
|
@@ -798,32 +804,33 @@ function createEntitySeed(matchSeed, entityId, tick2) {
|
|
|
798
804
|
var MAX_MESSAGE_LENGTH = 1e3;
|
|
799
805
|
var MAX_ERRORS_PER_MATCH = 500;
|
|
800
806
|
function describeThrown(thrown) {
|
|
801
|
-
|
|
807
|
+
const { text, alreadyBounded } = rawDescription(thrown);
|
|
808
|
+
return alreadyBounded ? text : truncate(text);
|
|
802
809
|
}
|
|
803
810
|
function rawDescription(thrown) {
|
|
804
811
|
if (thrown instanceof Error) {
|
|
805
812
|
const message = typeof thrown.message === "string" ? thrown.message : "";
|
|
806
813
|
if (message.length > MAX_MESSAGE_LENGTH) {
|
|
807
|
-
return truncate(message) + " [stack omitted: message too large]";
|
|
814
|
+
return { text: truncate(message) + " [stack omitted: message too large]", alreadyBounded: true };
|
|
808
815
|
}
|
|
809
816
|
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)
|
|
817
|
+
if (stack) return { text: stack, alreadyBounded: false };
|
|
818
|
+
return { text: message || `${thrown.name || "Error"} (thrown with no message)`, alreadyBounded: false };
|
|
812
819
|
}
|
|
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";
|
|
820
|
+
if (typeof thrown === "string") return { text: thrown || "(empty string thrown)", alreadyBounded: false };
|
|
821
|
+
if (thrown === null) return { text: "null was thrown", alreadyBounded: false };
|
|
822
|
+
if (thrown === void 0) return { text: "undefined was thrown", alreadyBounded: false };
|
|
816
823
|
try {
|
|
817
824
|
const json = JSON.stringify(thrown);
|
|
818
|
-
if (typeof json === "string" && json !== "{}") return json;
|
|
825
|
+
if (typeof json === "string" && json !== "{}") return { text: json, alreadyBounded: false };
|
|
819
826
|
} catch {
|
|
820
827
|
}
|
|
821
828
|
try {
|
|
822
829
|
const text = String(thrown);
|
|
823
|
-
if (text === "[object Object]") return describeOpaqueObject(thrown);
|
|
824
|
-
return text;
|
|
830
|
+
if (text === "[object Object]") return { text: describeOpaqueObject(thrown), alreadyBounded: false };
|
|
831
|
+
return { text, alreadyBounded: false };
|
|
825
832
|
} catch {
|
|
826
|
-
return "(a value was thrown that could not be described)";
|
|
833
|
+
return { text: "(a value was thrown that could not be described)", alreadyBounded: false };
|
|
827
834
|
}
|
|
828
835
|
}
|
|
829
836
|
function describeOpaqueObject(thrown) {
|
|
@@ -842,7 +849,7 @@ function describeOpaqueObject(thrown) {
|
|
|
842
849
|
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
850
|
}
|
|
844
851
|
function stripBundleCoordinates(stack) {
|
|
845
|
-
return stack.replace(/\s*\((?:[^()\s]*match-bundle\.js|<isolated-vm>)[^()]*\)/g, "");
|
|
852
|
+
return stack.replace(/\s*\((?:[^()\s]*match-bundle\.js|<isolated-vm>)[^()]*\)/g, "").replace(/(\s*at )(?:[^()\s]*match-bundle\.js|<isolated-vm>)[^\s)]*/g, "$1<your bot>");
|
|
846
853
|
}
|
|
847
854
|
function truncate(message) {
|
|
848
855
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -940,6 +947,18 @@ function buildWizardContext(state, config, random) {
|
|
|
940
947
|
lastHitTick: state.lastHitTick
|
|
941
948
|
};
|
|
942
949
|
}
|
|
950
|
+
function nonFiniteField(move2, action, wizard) {
|
|
951
|
+
const bad = (v) => typeof v === "number" && !Number.isFinite(v);
|
|
952
|
+
if (bad(move2?.x)) return { what: "move(x, y): x", value: String(move2.x) };
|
|
953
|
+
if (bad(move2?.y)) return { what: "move(x, y): y", value: String(move2.y) };
|
|
954
|
+
const target = action.startCast?.spell === "blink" ? action.startCast.target : void 0;
|
|
955
|
+
if (bad(target?.x)) return { what: "blink(x, y): x", value: String(target.x) };
|
|
956
|
+
if (bad(target?.y)) return { what: "blink(x, y): y", value: String(target.y) };
|
|
957
|
+
if (bad(wizard.blinkTarget?.x)) return { what: "blink(x, y): x", value: String(wizard.blinkTarget.x) };
|
|
958
|
+
if (bad(wizard.blinkTarget?.y)) return { what: "blink(x, y): y", value: String(wizard.blinkTarget.y) };
|
|
959
|
+
if (bad(action.aimDirection)) return { what: "aim(degrees)", value: String(action.aimDirection) };
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
943
962
|
function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, missileAIs, matchSeed, budgets) {
|
|
944
963
|
const nextTick = currentTick + 1;
|
|
945
964
|
const errors = [];
|
|
@@ -1083,6 +1102,17 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
1083
1102
|
}
|
|
1084
1103
|
const oldPos = wizard.position;
|
|
1085
1104
|
const move2 = action.move ?? { x: 0, y: 0 };
|
|
1105
|
+
if (!wizard.advised?.nonFinite) {
|
|
1106
|
+
const bad = nonFiniteField(move2, action, wizard);
|
|
1107
|
+
if (bad) {
|
|
1108
|
+
wizard.advised = { ...wizard.advised, nonFinite: true };
|
|
1109
|
+
errors.push({
|
|
1110
|
+
tick: nextTick,
|
|
1111
|
+
entityId: wizard.id,
|
|
1112
|
+
message: `${bad.what} is not a finite number (it was ${bad.value}). The engine ignored it to keep the match running \u2014 the value is treated as zero, so your wizard simply does not do what you asked. This is almost always a divide-by-zero or a subtraction of two equal positions somewhere in your maths. Reported once per match.`
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1086
1116
|
wizard.position = moveWizard(wizard, move2, 1);
|
|
1087
1117
|
if (wizard.knockbackDelay !== void 0 && wizard.knockbackDelay > 0) {
|
|
1088
1118
|
wizard.knockbackDelay--;
|
|
@@ -1129,6 +1159,15 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
1129
1159
|
const castingSpell = action.startCast.spell;
|
|
1130
1160
|
startCast(wizard, castingSpell, castingSpell === "missile" ? action.startCast.config : void 0);
|
|
1131
1161
|
if (castingSpell === "missile") {
|
|
1162
|
+
if ((wizard.castDuration ?? 0) > MATCH_DURATION && !wizard.advised?.unfinishableCast) {
|
|
1163
|
+
wizard.advised = { ...wizard.advised, unfinishableCast: true };
|
|
1164
|
+
const config2 = action.startCast.config;
|
|
1165
|
+
errors.push({
|
|
1166
|
+
tick: nextTick,
|
|
1167
|
+
entityId: wizard.id,
|
|
1168
|
+
message: `This missile takes ${wizard.castDuration} ticks to cast, but a match is only ${MATCH_DURATION} ticks long, so the cast can never finish and this wizard will stand still for the rest of the match. The cost is dominated by turnRate (${config2?.turnRate ?? "?"}) \u2014 it is the most expensive field by far, and unlike the others it is not clamped to a sane maximum. Try a turnRate in the single digits, then use missile-calc or getMissileCastTime() to check the cast time before committing to a config.`
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1132
1171
|
wizard.missileConfig = action.startCast.config;
|
|
1133
1172
|
wizard.missileAI = action.startCast.missileAI;
|
|
1134
1173
|
if (action.startCast.direction !== void 0) {
|
|
@@ -2095,13 +2134,14 @@ function fitMissileForEscapingTarget(currentDistance, budgetTicks, options) {
|
|
|
2095
2134
|
}
|
|
2096
2135
|
|
|
2097
2136
|
// src/hooks/threat-analysis.ts
|
|
2137
|
+
var CANCEL_COST_TICKS = 1;
|
|
2098
2138
|
var THREAT_RELEVANCE_DISTANCE = 500;
|
|
2099
|
-
function analyzeThreats(myPos, projectiles, myProjectiles, ticksUntilReady) {
|
|
2139
|
+
function analyzeThreats(myPos, projectiles, myProjectiles, ticksUntilReady, options) {
|
|
2100
2140
|
const myProjectileIds = new Set(myProjectiles.map((p) => p.id));
|
|
2101
2141
|
const enemyProjectiles = projectiles.filter((p) => !myProjectileIds.has(p.id));
|
|
2102
2142
|
const threats = [];
|
|
2103
2143
|
for (const projectile of enemyProjectiles) {
|
|
2104
|
-
const analysis = analyzeOneThreat(myPos, projectile, ticksUntilReady);
|
|
2144
|
+
const analysis = analyzeOneThreat(myPos, projectile, ticksUntilReady, options?.canCancelCurrentCast ?? false);
|
|
2105
2145
|
const missileDistance = Math.sqrt(
|
|
2106
2146
|
(projectile.position.x - myPos.x) ** 2 + (projectile.position.y - myPos.y) ** 2
|
|
2107
2147
|
);
|
|
@@ -2112,7 +2152,7 @@ function analyzeThreats(myPos, projectiles, myProjectiles, ticksUntilReady) {
|
|
|
2112
2152
|
threats.sort((a, b) => a.ticksToImpact - b.ticksToImpact);
|
|
2113
2153
|
return threats;
|
|
2114
2154
|
}
|
|
2115
|
-
function analyzeOneThreat(targetPos, projectile, ticksUntilReady) {
|
|
2155
|
+
function analyzeOneThreat(targetPos, projectile, ticksUntilReady, canCancelCurrentCast) {
|
|
2116
2156
|
const missileRadius = calculateMissileRadius(projectile.damage);
|
|
2117
2157
|
const collisionDist = COLLISION_RADIUS + missileRadius;
|
|
2118
2158
|
const dodgeCollisionDist = collisionDist + projectile.speed * 0.5;
|
|
@@ -2138,8 +2178,10 @@ function analyzeOneThreat(targetPos, projectile, ticksUntilReady) {
|
|
|
2138
2178
|
ticksToImpact
|
|
2139
2179
|
);
|
|
2140
2180
|
}
|
|
2141
|
-
const
|
|
2142
|
-
const
|
|
2181
|
+
const delayBeforeShield = canCancelCurrentCast ? CANCEL_COST_TICKS : ticksUntilReady;
|
|
2182
|
+
const ticksNeededToBlock = delayBeforeShield + RULES.SHIELD_CAST_TIME + 1;
|
|
2183
|
+
const canBlockInTime = ticksToImpact >= ticksNeededToBlock;
|
|
2184
|
+
const ticksToStartShield = Math.max(0, ticksToImpact - ticksNeededToBlock);
|
|
2143
2185
|
return {
|
|
2144
2186
|
id: projectile.id,
|
|
2145
2187
|
projectile,
|
|
@@ -2414,9 +2456,13 @@ function useThreats() {
|
|
|
2414
2456
|
ctx.position,
|
|
2415
2457
|
ctx.projectiles,
|
|
2416
2458
|
ctx.myProjectiles,
|
|
2417
|
-
ticksUntilReady
|
|
2459
|
+
ticksUntilReady,
|
|
2460
|
+
// Mid-CAST you are one cancel() away from being able to shield, so the analysis
|
|
2461
|
+
// must not charge you the whole remainder of the cast. A GCD cannot be cancelled,
|
|
2462
|
+
// so it keeps the pessimistic reading.
|
|
2463
|
+
{ canCancelCurrentCast: ctx.state === "casting" }
|
|
2418
2464
|
);
|
|
2419
|
-
}, [ctx.projectiles, ctx.myProjectiles, ctx.position.x, ctx.position.y, ticksUntilReady]);
|
|
2465
|
+
}, [ctx.projectiles, ctx.myProjectiles, ctx.position.x, ctx.position.y, ticksUntilReady, ctx.state]);
|
|
2420
2466
|
}
|
|
2421
2467
|
function useClosestThreat() {
|
|
2422
2468
|
const threats = useThreats();
|
|
@@ -10898,4 +10944,4 @@ export {
|
|
|
10898
10944
|
diagnoseTrace,
|
|
10899
10945
|
formatDiagnosis
|
|
10900
10946
|
};
|
|
10901
|
-
//# sourceMappingURL=chunk-
|
|
10947
|
+
//# sourceMappingURL=chunk-KGRG7TZS.js.map
|