@vibemancer/core 1.0.10 → 1.0.12
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-OLGKLCCB.js → chunk-KGRG7TZS.js} +253 -32
- package/dist/chunk-KGRG7TZS.js.map +1 -0
- package/dist/{index-browser-BLioGWvO.d.ts → index-browser-Dc-Vl1HI.d.ts} +152 -19
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +5 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +51 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/bot-compute-budget.ts +10 -0
- package/src/engine/bot-error-capture.ts +199 -0
- package/src/engine/manual-match.ts +2 -0
- package/src/engine/physics.ts +76 -0
- package/src/engine/sandbox-harness.ts +46 -0
- package/src/engine/sandbox.ts +347 -341
- package/src/engine/simulation.ts +202 -10
- package/src/engine/spells.ts +13 -1
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +132 -5
- package/src/hooks/state-hooks.ts +413 -407
- package/src/hooks/threat-analysis.ts +86 -25
- package/src/hooks/types.ts +2 -1
- package/src/rules.ts +8 -0
- package/src/types.ts +11 -6
- package/src/utils/combat.ts +379 -371
- package/dist/chunk-OLGKLCCB.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,11 +516,60 @@ 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") {
|
|
541
|
+
const got = describeReturned(finalAction);
|
|
542
|
+
throw new Error(
|
|
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.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const action = finalAction._toAction();
|
|
547
|
+
if (action === null || typeof action !== "object" || typeof action.move !== "object" || action.move === null) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
"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."
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
return normaliseActionNumbers(action);
|
|
553
|
+
}
|
|
554
|
+
function normaliseActionNumbers(action) {
|
|
555
|
+
const num = (value) => Number(value);
|
|
556
|
+
const normalised = {
|
|
557
|
+
...action,
|
|
558
|
+
move: { x: num(action.move?.x), y: num(action.move?.y) }
|
|
559
|
+
};
|
|
560
|
+
if (action.aimDirection !== void 0) normalised.aimDirection = num(action.aimDirection);
|
|
561
|
+
if (action.startCast?.spell === "missile") {
|
|
562
|
+
normalised.startCast = {
|
|
563
|
+
...action.startCast,
|
|
564
|
+
...action.startCast.direction !== void 0 ? { direction: num(action.startCast.direction) } : {}
|
|
565
|
+
};
|
|
566
|
+
} else if (action.startCast?.spell === "blink") {
|
|
567
|
+
normalised.startCast = {
|
|
568
|
+
...action.startCast,
|
|
569
|
+
target: { x: num(action.startCast.target?.x), y: num(action.startCast.target?.y) }
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return normalised;
|
|
524
573
|
}
|
|
525
574
|
|
|
526
575
|
// src/engine/physics.ts
|
|
@@ -551,6 +600,27 @@ function moveProjectile(projectile, deltaTicks) {
|
|
|
551
600
|
y: projectile.position.y + Math.sin(radians) * projectile.speed * deltaTicks
|
|
552
601
|
};
|
|
553
602
|
}
|
|
603
|
+
function clampToSafeZone(position, radius) {
|
|
604
|
+
const min = ARENA_MIN + radius;
|
|
605
|
+
const max = ARENA_MAX - radius;
|
|
606
|
+
const centre = ARENA_SIZE / 2;
|
|
607
|
+
const axis = (value) => {
|
|
608
|
+
if (!Number.isFinite(value)) return centre;
|
|
609
|
+
return Math.max(min, Math.min(max, value));
|
|
610
|
+
};
|
|
611
|
+
return { x: axis(position?.x), y: axis(position?.y) };
|
|
612
|
+
}
|
|
613
|
+
function moveTowardSafely(from, target) {
|
|
614
|
+
const dx = target?.x - from?.x;
|
|
615
|
+
const dy = target?.y - from?.y;
|
|
616
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) return { x: 0, y: 0 };
|
|
617
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
618
|
+
if (distance === 0) return { x: 0, y: 0 };
|
|
619
|
+
if (distance >= RULES.MOVEMENT_SPEED) return { x: dx / distance, y: dy / distance };
|
|
620
|
+
const travel = Math.max(0, distance - 1e-6);
|
|
621
|
+
const fraction = travel / RULES.MOVEMENT_SPEED;
|
|
622
|
+
return { x: dx / distance * fraction, y: dy / distance * fraction };
|
|
623
|
+
}
|
|
554
624
|
function clampToArena(position, radius) {
|
|
555
625
|
return {
|
|
556
626
|
x: Math.max(radius, Math.min(ARENA_SIZE - radius, position.x)),
|
|
@@ -605,7 +675,8 @@ function startCast(wizard, spell, config) {
|
|
|
605
675
|
wizard.castingSpell = spell;
|
|
606
676
|
wizard.castProgress = 0;
|
|
607
677
|
if (spell === "missile" && config) {
|
|
608
|
-
const
|
|
678
|
+
const validated = validateMissileConfig(config);
|
|
679
|
+
const castTimeSec = calculateMissileCastTime(validated, wizard.lastMissileConfig);
|
|
609
680
|
wizard.castDuration = Math.max(1, Math.round(castTimeSec * TICKS_PER_SECOND));
|
|
610
681
|
wizard.warmupMultiplier = calculateWarmupMultiplier(wizard.lastMissileConfig, config);
|
|
611
682
|
} else if (spell === "shield") {
|
|
@@ -729,6 +800,70 @@ function createEntitySeed(matchSeed, entityId, tick2) {
|
|
|
729
800
|
return combined;
|
|
730
801
|
}
|
|
731
802
|
|
|
803
|
+
// src/engine/bot-error-capture.ts
|
|
804
|
+
var MAX_MESSAGE_LENGTH = 1e3;
|
|
805
|
+
var MAX_ERRORS_PER_MATCH = 500;
|
|
806
|
+
function describeThrown(thrown) {
|
|
807
|
+
const { text, alreadyBounded } = rawDescription(thrown);
|
|
808
|
+
return alreadyBounded ? text : truncate(text);
|
|
809
|
+
}
|
|
810
|
+
function rawDescription(thrown) {
|
|
811
|
+
if (thrown instanceof Error) {
|
|
812
|
+
const message = typeof thrown.message === "string" ? thrown.message : "";
|
|
813
|
+
if (message.length > MAX_MESSAGE_LENGTH) {
|
|
814
|
+
return { text: truncate(message) + " [stack omitted: message too large]", alreadyBounded: true };
|
|
815
|
+
}
|
|
816
|
+
const stack = typeof thrown.stack === "string" ? stripBundleCoordinates(thrown.stack.trim()) : "";
|
|
817
|
+
if (stack) return { text: stack, alreadyBounded: false };
|
|
818
|
+
return { text: message || `${thrown.name || "Error"} (thrown with no message)`, alreadyBounded: false };
|
|
819
|
+
}
|
|
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 };
|
|
823
|
+
try {
|
|
824
|
+
const json = JSON.stringify(thrown);
|
|
825
|
+
if (typeof json === "string" && json !== "{}") return { text: json, alreadyBounded: false };
|
|
826
|
+
} catch {
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
const text = String(thrown);
|
|
830
|
+
if (text === "[object Object]") return { text: describeOpaqueObject(thrown), alreadyBounded: false };
|
|
831
|
+
return { text, alreadyBounded: false };
|
|
832
|
+
} catch {
|
|
833
|
+
return { text: "(a value was thrown that could not be described)", alreadyBounded: false };
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function describeOpaqueObject(thrown) {
|
|
837
|
+
let name = "object";
|
|
838
|
+
try {
|
|
839
|
+
const ctor = thrown.constructor;
|
|
840
|
+
if (ctor && typeof ctor.name === "string" && ctor.name) name = ctor.name;
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
let keys = [];
|
|
844
|
+
try {
|
|
845
|
+
keys = Object.keys(thrown).slice(0, 12);
|
|
846
|
+
} catch {
|
|
847
|
+
}
|
|
848
|
+
const shape = keys.length > 0 ? ` with keys: ${keys.join(", ")}` : " with no enumerable keys";
|
|
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)`;
|
|
850
|
+
}
|
|
851
|
+
function stripBundleCoordinates(stack) {
|
|
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>");
|
|
853
|
+
}
|
|
854
|
+
function truncate(message) {
|
|
855
|
+
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
856
|
+
const dropped = message.length - MAX_MESSAGE_LENGTH;
|
|
857
|
+
return `${message.slice(0, MAX_MESSAGE_LENGTH)}\u2026 [truncated, ${dropped} more characters]`;
|
|
858
|
+
}
|
|
859
|
+
function cappedNotice(entityId, tick2) {
|
|
860
|
+
return {
|
|
861
|
+
tick: tick2,
|
|
862
|
+
entityId,
|
|
863
|
+
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.`
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
732
867
|
// src/engine/simulation.ts
|
|
733
868
|
function createInitialState(_seed, spawnDist = SPAWN_DISTANCE) {
|
|
734
869
|
const center = { x: ARENA_SIZE / 2, y: ARENA_SIZE / 2 };
|
|
@@ -812,9 +947,22 @@ function buildWizardContext(state, config, random) {
|
|
|
812
947
|
lastHitTick: state.lastHitTick
|
|
813
948
|
};
|
|
814
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
|
+
}
|
|
815
962
|
function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, missileAIs, matchSeed, budgets) {
|
|
816
963
|
const nextTick = currentTick + 1;
|
|
817
964
|
const errors = [];
|
|
965
|
+
const budgetExhausted = [];
|
|
818
966
|
const events = [];
|
|
819
967
|
const random1 = createRandom(createEntitySeed(matchSeed, wizards[0].id, nextTick));
|
|
820
968
|
const random2 = createRandom(createEntitySeed(matchSeed, wizards[1].id, nextTick));
|
|
@@ -822,13 +970,25 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
822
970
|
const nextBudgets = budgets ? [budgets.states[0], budgets.states[1]] : void 0;
|
|
823
971
|
const runBot = (index, entityId, invoke) => {
|
|
824
972
|
const state = nextBudgets?.[index];
|
|
825
|
-
if (state && !mayAct(state))
|
|
973
|
+
if (state && !mayAct(state)) {
|
|
974
|
+
if (!state.reported) {
|
|
975
|
+
state.reported = true;
|
|
976
|
+
budgetExhausted.push({ tick: nextTick, entityId, spentMs: state.spentMs });
|
|
977
|
+
}
|
|
978
|
+
return IDLE_ACTION();
|
|
979
|
+
}
|
|
826
980
|
const startedAt = state ? Date.now() : 0;
|
|
827
981
|
let action;
|
|
828
982
|
try {
|
|
829
983
|
action = invoke();
|
|
830
984
|
} catch (e) {
|
|
831
|
-
|
|
985
|
+
const alreadyDead = (wizards[index]?.health ?? 1) <= 0;
|
|
986
|
+
errors.push({
|
|
987
|
+
tick: nextTick,
|
|
988
|
+
entityId,
|
|
989
|
+
message: describeThrown(e),
|
|
990
|
+
...alreadyDead ? { afterDeath: true } : {}
|
|
991
|
+
});
|
|
832
992
|
action = IDLE_ACTION();
|
|
833
993
|
}
|
|
834
994
|
if (state && budgets) {
|
|
@@ -911,10 +1071,12 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
911
1071
|
delete wizard.missileConfig;
|
|
912
1072
|
delete wizard.missileAI;
|
|
913
1073
|
} else if (spell === "blink" && wizard.blinkTarget) {
|
|
914
|
-
const
|
|
915
|
-
const
|
|
1074
|
+
const targetX = Number.isFinite(wizard.blinkTarget.x) ? wizard.blinkTarget.x : wizard.position.x;
|
|
1075
|
+
const targetY = Number.isFinite(wizard.blinkTarget.y) ? wizard.blinkTarget.y : wizard.position.y;
|
|
1076
|
+
const dx = targetX - wizard.position.x;
|
|
1077
|
+
const dy = targetY - wizard.position.y;
|
|
916
1078
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
917
|
-
let targetPos =
|
|
1079
|
+
let targetPos = { x: targetX, y: targetY };
|
|
918
1080
|
if (distance > RULES.BLINK_RANGE) {
|
|
919
1081
|
const scale = RULES.BLINK_RANGE / distance;
|
|
920
1082
|
targetPos = {
|
|
@@ -940,6 +1102,17 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
940
1102
|
}
|
|
941
1103
|
const oldPos = wizard.position;
|
|
942
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
|
+
}
|
|
943
1116
|
wizard.position = moveWizard(wizard, move2, 1);
|
|
944
1117
|
if (wizard.knockbackDelay !== void 0 && wizard.knockbackDelay > 0) {
|
|
945
1118
|
wizard.knockbackDelay--;
|
|
@@ -986,6 +1159,15 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
986
1159
|
const castingSpell = action.startCast.spell;
|
|
987
1160
|
startCast(wizard, castingSpell, castingSpell === "missile" ? action.startCast.config : void 0);
|
|
988
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
|
+
}
|
|
989
1171
|
wizard.missileConfig = action.startCast.config;
|
|
990
1172
|
wizard.missileAI = action.startCast.missileAI;
|
|
991
1173
|
if (action.startCast.direction !== void 0) {
|
|
@@ -1043,12 +1225,12 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
1043
1225
|
() => extractMissileAction(withMissileContext(missileCtx, ai))
|
|
1044
1226
|
) ?? {};
|
|
1045
1227
|
} catch (e) {
|
|
1046
|
-
errors.push({ tick: nextTick, entityId: projectile.id, message:
|
|
1228
|
+
errors.push({ tick: nextTick, entityId: projectile.id, message: describeThrown(e) });
|
|
1047
1229
|
}
|
|
1048
1230
|
targetAngle = missileActions.turnToward ? angleTo(projectile.position, missileActions.turnToward) : missileActions.turnToAngle !== void 0 ? missileActions.turnToAngle : null;
|
|
1049
1231
|
}
|
|
1050
1232
|
}
|
|
1051
|
-
if (targetAngle !== null) {
|
|
1233
|
+
if (targetAngle !== null && Number.isFinite(targetAngle)) {
|
|
1052
1234
|
const diff = angleDiff(projectile.rotation, targetAngle);
|
|
1053
1235
|
const turn = Math.max(-projectile.turnRate, Math.min(projectile.turnRate, diff));
|
|
1054
1236
|
projectile.rotation = normalizeAngle(projectile.rotation + turn);
|
|
@@ -1109,6 +1291,7 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
1109
1291
|
projectiles: remainingProjectiles,
|
|
1110
1292
|
events,
|
|
1111
1293
|
errors,
|
|
1294
|
+
budgetExhausted,
|
|
1112
1295
|
budgets: nextBudgets
|
|
1113
1296
|
};
|
|
1114
1297
|
}
|
|
@@ -1248,6 +1431,7 @@ var FIGHT_SPAWN_DISTANCES = [500, 550, 600, 650, 700];
|
|
|
1248
1431
|
function fight(wizard1AI, wizard2AI, options = {}) {
|
|
1249
1432
|
const matches = [];
|
|
1250
1433
|
const allErrors = [];
|
|
1434
|
+
const allBudgetExhausted = [];
|
|
1251
1435
|
let matchNumber = 1;
|
|
1252
1436
|
let wizard1Wins = 0;
|
|
1253
1437
|
let wizard2Wins = 0;
|
|
@@ -1262,6 +1446,7 @@ function fight(wizard1AI, wizard2AI, options = {}) {
|
|
|
1262
1446
|
});
|
|
1263
1447
|
matches.push(result);
|
|
1264
1448
|
allErrors.push(...result.errors.map((e) => ({ ...e, match: matchNumber })));
|
|
1449
|
+
allBudgetExhausted.push(...result.budgetExhausted.map((b) => ({ ...b, match: matchNumber })));
|
|
1265
1450
|
matchNumber++;
|
|
1266
1451
|
if (result.winner === "wizard-1") {
|
|
1267
1452
|
wizard1Wins++;
|
|
@@ -1282,6 +1467,7 @@ function fight(wizard1AI, wizard2AI, options = {}) {
|
|
|
1282
1467
|
budget.states = [swappedBudget.states[1], swappedBudget.states[0]];
|
|
1283
1468
|
}
|
|
1284
1469
|
allErrors.push(...swapped.errors.map((e) => ({ ...e, entityId: swapSide(e.entityId), match: matchNumber })));
|
|
1470
|
+
allBudgetExhausted.push(...swapped.budgetExhausted.map((b) => ({ ...b, entityId: swapSide(b.entityId), match: matchNumber })));
|
|
1285
1471
|
matchNumber++;
|
|
1286
1472
|
if (swapped.winner === "wizard-1") {
|
|
1287
1473
|
wizard2Wins++;
|
|
@@ -1292,7 +1478,7 @@ function fight(wizard1AI, wizard2AI, options = {}) {
|
|
|
1292
1478
|
}
|
|
1293
1479
|
}
|
|
1294
1480
|
const winner = wizard1Wins > wizard2Wins ? "wizard-1" : wizard2Wins > wizard1Wins ? "wizard-2" : "draw";
|
|
1295
|
-
return { wizard1Wins, wizard2Wins, draws, winner, matches, allErrors };
|
|
1481
|
+
return { wizard1Wins, wizard2Wins, draws, winner, matches, allErrors, budgetExhausted: allBudgetExhausted };
|
|
1296
1482
|
}
|
|
1297
1483
|
function simulate(wizard1AI, wizard2AI, options = {}) {
|
|
1298
1484
|
if (typeof wizard1AI !== "function") {
|
|
@@ -1345,6 +1531,7 @@ function simulate(wizard1AI, wizard2AI, options = {}) {
|
|
|
1345
1531
|
let projectiles = [];
|
|
1346
1532
|
const missileAIs = /* @__PURE__ */ new Map();
|
|
1347
1533
|
const allErrors = [];
|
|
1534
|
+
const allBudgetExhausted = [];
|
|
1348
1535
|
let deathTick = null;
|
|
1349
1536
|
const sharedBudget = options.budget;
|
|
1350
1537
|
const budgetLimits = sharedBudget?.limits ?? options.budgetLimits;
|
|
@@ -1368,7 +1555,15 @@ function simulate(wizard1AI, wizard2AI, options = {}) {
|
|
|
1368
1555
|
currentTick = result.nextTick;
|
|
1369
1556
|
wizards = result.wizards;
|
|
1370
1557
|
projectiles = result.projectiles;
|
|
1371
|
-
|
|
1558
|
+
for (const error of result.errors) {
|
|
1559
|
+
if (allErrors.length > MAX_ERRORS_PER_MATCH) break;
|
|
1560
|
+
if (allErrors.length === MAX_ERRORS_PER_MATCH) {
|
|
1561
|
+
allErrors.push(cappedNotice(error.entityId, error.tick));
|
|
1562
|
+
break;
|
|
1563
|
+
}
|
|
1564
|
+
allErrors.push(error);
|
|
1565
|
+
}
|
|
1566
|
+
if (result.budgetExhausted.length > 0) allBudgetExhausted.push(...result.budgetExhausted);
|
|
1372
1567
|
if (!skipHistory) {
|
|
1373
1568
|
history.push(getPlayerState(0, wizards, projectiles, currentTick, result.events));
|
|
1374
1569
|
}
|
|
@@ -1393,7 +1588,8 @@ function simulate(wizard1AI, wizard2AI, options = {}) {
|
|
|
1393
1588
|
ticks: currentTick,
|
|
1394
1589
|
finalState,
|
|
1395
1590
|
history: skipHistory ? [] : history,
|
|
1396
|
-
errors: allErrors
|
|
1591
|
+
errors: allErrors,
|
|
1592
|
+
budgetExhausted: allBudgetExhausted
|
|
1397
1593
|
};
|
|
1398
1594
|
}
|
|
1399
1595
|
|
|
@@ -1567,7 +1763,9 @@ var ManualMatch = class {
|
|
|
1567
1763
|
ticks: this.currentTick,
|
|
1568
1764
|
finalState: this.getGameState(),
|
|
1569
1765
|
history: this.history,
|
|
1570
|
-
errors: this.allErrors
|
|
1766
|
+
errors: this.allErrors,
|
|
1767
|
+
// Manual play is unbudgeted — the clock is never read, so nobody can be cut off.
|
|
1768
|
+
budgetExhausted: []
|
|
1571
1769
|
};
|
|
1572
1770
|
}
|
|
1573
1771
|
dispose() {
|
|
@@ -1936,13 +2134,14 @@ function fitMissileForEscapingTarget(currentDistance, budgetTicks, options) {
|
|
|
1936
2134
|
}
|
|
1937
2135
|
|
|
1938
2136
|
// src/hooks/threat-analysis.ts
|
|
2137
|
+
var CANCEL_COST_TICKS = 1;
|
|
1939
2138
|
var THREAT_RELEVANCE_DISTANCE = 500;
|
|
1940
|
-
function analyzeThreats(myPos, projectiles, myProjectiles, ticksUntilReady) {
|
|
2139
|
+
function analyzeThreats(myPos, projectiles, myProjectiles, ticksUntilReady, options) {
|
|
1941
2140
|
const myProjectileIds = new Set(myProjectiles.map((p) => p.id));
|
|
1942
2141
|
const enemyProjectiles = projectiles.filter((p) => !myProjectileIds.has(p.id));
|
|
1943
2142
|
const threats = [];
|
|
1944
2143
|
for (const projectile of enemyProjectiles) {
|
|
1945
|
-
const analysis = analyzeOneThreat(myPos, projectile, ticksUntilReady);
|
|
2144
|
+
const analysis = analyzeOneThreat(myPos, projectile, ticksUntilReady, options?.canCancelCurrentCast ?? false);
|
|
1946
2145
|
const missileDistance = Math.sqrt(
|
|
1947
2146
|
(projectile.position.x - myPos.x) ** 2 + (projectile.position.y - myPos.y) ** 2
|
|
1948
2147
|
);
|
|
@@ -1953,7 +2152,7 @@ function analyzeThreats(myPos, projectiles, myProjectiles, ticksUntilReady) {
|
|
|
1953
2152
|
threats.sort((a, b) => a.ticksToImpact - b.ticksToImpact);
|
|
1954
2153
|
return threats;
|
|
1955
2154
|
}
|
|
1956
|
-
function analyzeOneThreat(targetPos, projectile, ticksUntilReady) {
|
|
2155
|
+
function analyzeOneThreat(targetPos, projectile, ticksUntilReady, canCancelCurrentCast) {
|
|
1957
2156
|
const missileRadius = calculateMissileRadius(projectile.damage);
|
|
1958
2157
|
const collisionDist = COLLISION_RADIUS + missileRadius;
|
|
1959
2158
|
const dodgeCollisionDist = collisionDist + projectile.speed * 0.5;
|
|
@@ -1975,11 +2174,14 @@ function analyzeOneThreat(targetPos, projectile, ticksUntilReady) {
|
|
|
1975
2174
|
targetPos,
|
|
1976
2175
|
canDodgeLeft,
|
|
1977
2176
|
canDodgeRight,
|
|
1978
|
-
canOutrun
|
|
2177
|
+
canOutrun,
|
|
2178
|
+
ticksToImpact
|
|
1979
2179
|
);
|
|
1980
2180
|
}
|
|
1981
|
-
const
|
|
1982
|
-
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);
|
|
1983
2185
|
return {
|
|
1984
2186
|
id: projectile.id,
|
|
1985
2187
|
projectile,
|
|
@@ -2090,15 +2292,28 @@ function getDodgeDirection(projectile, targetPos, direction) {
|
|
|
2090
2292
|
return { x: 0, y: 0 };
|
|
2091
2293
|
}
|
|
2092
2294
|
}
|
|
2093
|
-
function
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2295
|
+
function roomBeforeLava(from, dir) {
|
|
2296
|
+
const min = ARENA_MIN + COLLISION_RADIUS;
|
|
2297
|
+
const max = ARENA_MAX - COLLISION_RADIUS;
|
|
2298
|
+
const along = (pos, d, lo, hi) => {
|
|
2299
|
+
if (Math.abs(d) < 1e-9) return Number.POSITIVE_INFINITY;
|
|
2300
|
+
return d > 0 ? (hi - pos) / d : (lo - pos) / d;
|
|
2301
|
+
};
|
|
2302
|
+
return Math.max(0, Math.min(along(from.x, dir.x, min, max), along(from.y, dir.y, min, max)));
|
|
2303
|
+
}
|
|
2304
|
+
function calculateBestDodgeDirection(projectile, targetPos, canDodgeLeft, canDodgeRight, canOutrun, ticksToImpact) {
|
|
2305
|
+
const travel = RULES.MOVEMENT_SPEED * Math.max(1, Math.min(ticksToImpact, 600));
|
|
2306
|
+
const survives = (dir) => roomBeforeLava(targetPos, dir) >= travel;
|
|
2307
|
+
const sides = [];
|
|
2308
|
+
if (canDodgeLeft) sides.push(getDodgeDirection(projectile, targetPos, "left"));
|
|
2309
|
+
if (canDodgeRight) sides.push(getDodgeDirection(projectile, targetPos, "right"));
|
|
2310
|
+
const safeSides = sides.filter(survives);
|
|
2311
|
+
if (safeSides.length > 0) {
|
|
2312
|
+
return safeSides.reduce((best, dir) => roomBeforeLava(targetPos, dir) > roomBeforeLava(targetPos, best) ? dir : best);
|
|
2313
|
+
}
|
|
2314
|
+
if (canOutrun) {
|
|
2315
|
+
const away = getDodgeDirection(projectile, targetPos, "away");
|
|
2316
|
+
if (survives(away)) return away;
|
|
2102
2317
|
}
|
|
2103
2318
|
return null;
|
|
2104
2319
|
}
|
|
@@ -2241,9 +2456,13 @@ function useThreats() {
|
|
|
2241
2456
|
ctx.position,
|
|
2242
2457
|
ctx.projectiles,
|
|
2243
2458
|
ctx.myProjectiles,
|
|
2244
|
-
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" }
|
|
2245
2464
|
);
|
|
2246
|
-
}, [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]);
|
|
2247
2466
|
}
|
|
2248
2467
|
function useClosestThreat() {
|
|
2249
2468
|
const threats = useThreats();
|
|
@@ -10585,6 +10804,8 @@ export {
|
|
|
10585
10804
|
extractAction,
|
|
10586
10805
|
moveWizard,
|
|
10587
10806
|
moveProjectile,
|
|
10807
|
+
clampToSafeZone,
|
|
10808
|
+
moveTowardSafely,
|
|
10588
10809
|
clampToArena,
|
|
10589
10810
|
isInLava,
|
|
10590
10811
|
resolveWizardCollision,
|
|
@@ -10723,4 +10944,4 @@ export {
|
|
|
10723
10944
|
diagnoseTrace,
|
|
10724
10945
|
formatDiagnosis
|
|
10725
10946
|
};
|
|
10726
|
-
//# sourceMappingURL=chunk-
|
|
10947
|
+
//# sourceMappingURL=chunk-KGRG7TZS.js.map
|