@vibemancer/core 1.0.8 → 1.0.9
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-MUU3ELH5.js → chunk-OLGKLCCB.js} +36 -11
- package/dist/chunk-OLGKLCCB.js.map +1 -0
- package/dist/{index-browser-ClmR9SkR.d.ts → index-browser-BTHlrB_s.d.ts} +86 -12
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bots/sniper/01_Spellshot.ts +11 -5
- package/src/engine/physics.ts +12 -2
- package/src/engine/simulation.ts +79 -2
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +299 -266
- package/src/hooks/index.ts +84 -83
- package/src/hooks/types.ts +216 -200
- package/src/trace.ts +497 -490
- package/src/types.ts +9 -3
- package/dist/chunk-MUU3ELH5.js.map +0 -1
package/src/engine/physics.ts
CHANGED
|
@@ -9,8 +9,18 @@ import {RULES, ARENA_SIZE, WIZARD_RADIUS, ARENA_MIN, ARENA_MAX} from '../rules.j
|
|
|
9
9
|
*/
|
|
10
10
|
export function moveWizard(wizard: WizardState, move: {x: number; y: number}, deltaTicks: number): Position
|
|
11
11
|
{
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// A bot's move vector is untrusted input, and it must not be able to poison the
|
|
13
|
+
// simulation. This was `move.x || 0`, which neutralises NaN (NaN is falsy) and does
|
|
14
|
+
// nothing about Infinity — and `Infinity / magnitude` is NaN, so the position became NaN,
|
|
15
|
+
// `isInLava(NaN)` was false so the wizard never died, and `resolveWizardCollision` then
|
|
16
|
+
// divided by a NaN distance and wrote NaN into the OPPONENT's position too. Both wizards
|
|
17
|
+
// froze, nothing could hit anything, and every match was a silent draw.
|
|
18
|
+
//
|
|
19
|
+
// On a rated ladder that made `move(1/0, 0)` a one-line way to deny every opponent a win
|
|
20
|
+
// and never lose — and it is reachable by accident from any divide-by-zero scale factor.
|
|
21
|
+
// A non-finite component is treated as no input on that axis, exactly like move(0, 0).
|
|
22
|
+
let dx = Number.isFinite(move?.x) ? move.x : 0;
|
|
23
|
+
let dy = Number.isFinite(move?.y) ? move.y : 0;
|
|
14
24
|
|
|
15
25
|
const magnitude = Math.sqrt(dx * dx + dy * dy);
|
|
16
26
|
// Stryker disable next-line EqualityOperator: > vs >= equivalent (dividing by exactly 1 is identity)
|
package/src/engine/simulation.ts
CHANGED
|
@@ -47,6 +47,15 @@ export interface InternalWizardState extends WizardState
|
|
|
47
47
|
knockbackVy?: number;
|
|
48
48
|
// Delayed knockback (waits for explosion to expand)
|
|
49
49
|
knockbackDelay?: number;
|
|
50
|
+
/**
|
|
51
|
+
* A heading this wizard LOCKED for the duration of a missile cast, via `lockAim()`.
|
|
52
|
+
*
|
|
53
|
+
* Auto-aim rewrites `rotation` every tick and a missile launches with the rotation at cast
|
|
54
|
+
* completion, so an angle applied only at cast start never survived. Holding it here is
|
|
55
|
+
* what lets a bot lead a moving target. Absent for every bot that does not opt in, which
|
|
56
|
+
* is why adding it changed no existing behaviour.
|
|
57
|
+
*/
|
|
58
|
+
castAimDirection?: number;
|
|
50
59
|
knockbackPendingVx?: number;
|
|
51
60
|
knockbackPendingVy?: number;
|
|
52
61
|
}
|
|
@@ -292,12 +301,19 @@ export function tick(
|
|
|
292
301
|
}
|
|
293
302
|
}
|
|
294
303
|
|
|
295
|
-
// Auto-aim: face the enemy by default
|
|
304
|
+
// Auto-aim: face the enemy by default; a bot overrides with aimDirection (`aim()`), and
|
|
305
|
+
// a missile cast that opted in with `lockAim()` holds its heading until it launches.
|
|
306
|
+
// Without that hold the override ran every tick and the requested angle never reached
|
|
307
|
+
// cast completion, which is the rotation the missile is actually fired with.
|
|
296
308
|
const enemy = wizards[1 - i]!;
|
|
297
309
|
if (action.aimDirection !== undefined && Number.isFinite(action.aimDirection))
|
|
298
310
|
{
|
|
299
311
|
wizard.rotation = action.aimDirection;
|
|
300
312
|
}
|
|
313
|
+
else if (wizard.state === 'casting' && wizard.castingSpell === 'missile' && wizard.castAimDirection !== undefined)
|
|
314
|
+
{
|
|
315
|
+
wizard.rotation = wizard.castAimDirection;
|
|
316
|
+
}
|
|
301
317
|
else
|
|
302
318
|
{
|
|
303
319
|
wizard.rotation = angleTo(wizard.position, enemy.position);
|
|
@@ -328,6 +344,9 @@ export function tick(
|
|
|
328
344
|
remainingTicks: validConfig.duration,
|
|
329
345
|
};
|
|
330
346
|
projectiles.push(projectile);
|
|
347
|
+
// The lock belonged to this cast; the next action auto-aims again unless it
|
|
348
|
+
// asks for something else.
|
|
349
|
+
wizard.castAimDirection = undefined;
|
|
331
350
|
missileAIs.set(id, wizard.missileAI);
|
|
332
351
|
events.push({type: 'missile-launch', position: {...wizard.position}, damage: validConfig.damage, speed: validConfig.speed, ownerId: wizard.id, rotation: wizard.rotation});
|
|
333
352
|
// Track last missile for warmup system
|
|
@@ -455,6 +474,11 @@ export function tick(
|
|
|
455
474
|
{
|
|
456
475
|
wizard.rotation = action.startCast.direction;
|
|
457
476
|
}
|
|
477
|
+
// Only a bot that called lockAim() sends aimDirection, so auto-aim stays
|
|
478
|
+
// the default for everything that came before this existed.
|
|
479
|
+
wizard.castAimDirection = (action.aimDirection !== undefined && Number.isFinite(action.aimDirection))
|
|
480
|
+
? action.aimDirection
|
|
481
|
+
: undefined;
|
|
458
482
|
}
|
|
459
483
|
else if (castingSpell === 'blink')
|
|
460
484
|
{
|
|
@@ -832,6 +856,15 @@ export interface BotError
|
|
|
832
856
|
tick: number;
|
|
833
857
|
entityId: string;
|
|
834
858
|
message: string;
|
|
859
|
+
/**
|
|
860
|
+
* 1-based index within the fight's TEN matches, present only on `FightResult.allErrors`.
|
|
861
|
+
*
|
|
862
|
+
* A single `simulate()` knows nothing about a series, so it does not set this. `fight()`
|
|
863
|
+
* stamps it, numbering 1..10 in the order matches are played. Every tool description
|
|
864
|
+
* promises "a 10-match series (5 spawn distances, each played twice with sides swapped)",
|
|
865
|
+
* so a match index that only ever reached 5 was uninterpretable to the player reading it.
|
|
866
|
+
*/
|
|
867
|
+
match?: number;
|
|
835
868
|
}
|
|
836
869
|
|
|
837
870
|
export interface SimulateResult
|
|
@@ -860,6 +893,37 @@ export interface FightResult
|
|
|
860
893
|
* Used for visual playback in the web viewer. Scoring includes both sides.
|
|
861
894
|
*/
|
|
862
895
|
matches: SimulateResult[];
|
|
896
|
+
/**
|
|
897
|
+
* Every bot error from ALL TEN matches, including the swapped half that `matches` drops.
|
|
898
|
+
*
|
|
899
|
+
* `matches` exists for visual playback, so it holds only the five non-swapped results.
|
|
900
|
+
* Anything asking "did this bot crash?" needs the whole series: a bot that throws only
|
|
901
|
+
* when it spawns on one side was previously invisible, reporting a clean `success: true`
|
|
902
|
+
* while losing every match.
|
|
903
|
+
*
|
|
904
|
+
* Entity ids are in the CALLER's frame of reference. In a swapped match the engine calls
|
|
905
|
+
* bot 2 "wizard-1", so those records are remapped on the way out — passing them through
|
|
906
|
+
* raw would blame each side's faults on the other.
|
|
907
|
+
*/
|
|
908
|
+
allErrors: BotError[];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Flip an entity id between the two sides.
|
|
913
|
+
*
|
|
914
|
+
* A swapped match runs bot 2 in the wizard-1 slot, so every id it produces is the mirror of
|
|
915
|
+
* what the caller means by it. Projectile ids carry their owner (`missile-<wizard>-<tick>`),
|
|
916
|
+
* so they are mirrored too — otherwise a missile-AI fault from the swapped half would be
|
|
917
|
+
* attributed to the wrong bot, which is exactly the defect that made a passive bot look like
|
|
918
|
+
* it had thrown 3,695 times.
|
|
919
|
+
*/
|
|
920
|
+
function swapSide(entityId: string): string
|
|
921
|
+
{
|
|
922
|
+
if (entityId === 'wizard-1') return 'wizard-2';
|
|
923
|
+
if (entityId === 'wizard-2') return 'wizard-1';
|
|
924
|
+
if (entityId.startsWith('missile-wizard-1-')) return 'missile-wizard-2-' + entityId.slice('missile-wizard-1-'.length);
|
|
925
|
+
if (entityId.startsWith('missile-wizard-2-')) return 'missile-wizard-1-' + entityId.slice('missile-wizard-2-'.length);
|
|
926
|
+
return entityId;
|
|
863
927
|
}
|
|
864
928
|
|
|
865
929
|
/** Spawn distances for the 5 matches in a fight (creates butterfly effect variation) */
|
|
@@ -891,6 +955,10 @@ export function fight(
|
|
|
891
955
|
): FightResult
|
|
892
956
|
{
|
|
893
957
|
const matches: SimulateResult[] = [];
|
|
958
|
+
const allErrors: BotError[] = [];
|
|
959
|
+
// 1..10 in the order matches are played, so the number a player is shown is the number
|
|
960
|
+
// every tool description promises.
|
|
961
|
+
let matchNumber = 1;
|
|
894
962
|
let wizard1Wins = 0;
|
|
895
963
|
let wizard2Wins = 0;
|
|
896
964
|
let draws = 0;
|
|
@@ -909,6 +977,8 @@ export function fight(
|
|
|
909
977
|
});
|
|
910
978
|
|
|
911
979
|
matches.push(result);
|
|
980
|
+
allErrors.push(...result.errors.map((e) => ({...e, match: matchNumber})));
|
|
981
|
+
matchNumber++;
|
|
912
982
|
|
|
913
983
|
if (result.winner === 'wizard-1')
|
|
914
984
|
{
|
|
@@ -946,6 +1016,13 @@ export function fight(
|
|
|
946
1016
|
budget.states = [swappedBudget.states[1], swappedBudget.states[0]];
|
|
947
1017
|
}
|
|
948
1018
|
|
|
1019
|
+
// The swapped match is dropped from `matches` (playback only), but its errors are not
|
|
1020
|
+
// dropped — half a fight's worth of crashes used to vanish here. Ids are flipped back
|
|
1021
|
+
// into the caller's frame first: this match ran bot 2 as "wizard-1", so passing the
|
|
1022
|
+
// records through raw would report each bot's faults against the other.
|
|
1023
|
+
allErrors.push(...swapped.errors.map((e) => ({...e, entityId: swapSide(e.entityId), match: matchNumber})));
|
|
1024
|
+
matchNumber++;
|
|
1025
|
+
|
|
949
1026
|
// Don't push swapped match to matches array (it's only for scoring)
|
|
950
1027
|
if (swapped.winner === 'wizard-1')
|
|
951
1028
|
{
|
|
@@ -966,7 +1043,7 @@ export function fight(
|
|
|
966
1043
|
: wizard2Wins > wizard1Wins ? 'wizard-2'
|
|
967
1044
|
: 'draw';
|
|
968
1045
|
|
|
969
|
-
return {wizard1Wins, wizard2Wins, draws, winner, matches};
|
|
1046
|
+
return {wizard1Wins, wizard2Wins, draws, winner, matches, allErrors};
|
|
970
1047
|
}
|
|
971
1048
|
|
|
972
1049
|
/**
|
package/src/engine-version.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Used to gate spectator replays: a recorded match can only be re-simulated when
|
|
10
10
|
* the runtime engine version matches the version that produced the match.
|
|
11
11
|
*/
|
|
12
|
-
export const ENGINE_VERSION =
|
|
12
|
+
export const ENGINE_VERSION = 293192982913104;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Historical versions that denote the SAME engine as `ENGINE_VERSION`.
|