@vibemancer/core 1.0.9 → 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.
- package/dist/{chunk-OLGKLCCB.js → chunk-EO7JO2RZ.js} +199 -24
- package/dist/chunk-EO7JO2RZ.js.map +1 -0
- package/dist/{index-browser-BTHlrB_s.d.ts → index-browser-CM0uuzWp.d.ts} +156 -18
- 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 +179 -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 +120 -10
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +112 -5
- package/src/hooks/state-hooks.ts +409 -407
- package/src/hooks/threat-analysis.ts +57 -18
- package/src/hooks/types.ts +16 -4
- package/src/rules.ts +8 -0
- package/src/types.ts +10 -6
- package/src/utils/combat.ts +379 -371
- package/dist/chunk-OLGKLCCB.js.map +0 -1
|
@@ -119,6 +119,7 @@ function analyzeOneThreat(
|
|
|
119
119
|
canDodgeLeft,
|
|
120
120
|
canDodgeRight,
|
|
121
121
|
canOutrun,
|
|
122
|
+
ticksToImpact,
|
|
122
123
|
);
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -244,12 +245,14 @@ function simulateDodge(
|
|
|
244
245
|
// Stryker disable next-line ArithmeticOperator: same as above
|
|
245
246
|
const newY = targetPos.y + dodgeDir.y * RULES.MOVEMENT_SPEED;
|
|
246
247
|
|
|
247
|
-
//
|
|
248
|
+
// Clamped to the survivable band, which models "dodge as far as you safely can" —
|
|
249
|
+
// the wizard stops at the lava rather than walking into it. Feasibility is judged on
|
|
250
|
+
// that basis; whether the ADVICE points at the lava is decided in
|
|
251
|
+
// calculateBestDodgeDirection, which is where the real defect was.
|
|
248
252
|
targetPos = {
|
|
249
253
|
x: Math.max(ARENA_MIN + COLLISION_RADIUS, Math.min(ARENA_MAX - COLLISION_RADIUS, newX)),
|
|
250
254
|
y: Math.max(ARENA_MIN + COLLISION_RADIUS, Math.min(ARENA_MAX - COLLISION_RADIUS, newY)),
|
|
251
255
|
};
|
|
252
|
-
// Stryker restore all
|
|
253
256
|
|
|
254
257
|
// Stryker disable next-line ConditionalExpression,EqualityOperator: turnRate>=0 equivalent (homing with rate 0 is no-op)
|
|
255
258
|
if (turnRate > 0)
|
|
@@ -341,35 +344,71 @@ function getDodgeDirection(
|
|
|
341
344
|
/**
|
|
342
345
|
* Calculate the best dodge direction based on available options.
|
|
343
346
|
*/
|
|
347
|
+
/**
|
|
348
|
+
* How far a wizard can travel from `from` along `dir` before its edge touches lava.
|
|
349
|
+
*
|
|
350
|
+
* Used only to break a tie between two dodges that both work. Larger is better: it is the
|
|
351
|
+
* margin the bot has if it keeps moving, which is what bots actually do.
|
|
352
|
+
*/
|
|
353
|
+
function roomBeforeLava(from: Position, dir: Position): number
|
|
354
|
+
{
|
|
355
|
+
const min = ARENA_MIN + COLLISION_RADIUS;
|
|
356
|
+
const max = ARENA_MAX - COLLISION_RADIUS;
|
|
357
|
+
const along = (pos: number, d: number, lo: number, hi: number): number =>
|
|
358
|
+
{
|
|
359
|
+
// A component that is effectively zero means this axis never runs out — not that it
|
|
360
|
+
// runs out immediately. Comparing against exact zero was not enough: standing on the
|
|
361
|
+
// boundary, a direction of (1, -1.8e-16) gave (35 - 35) / -1.8e-16 = 0, so the SAFE
|
|
362
|
+
// direction scored no room and lost to the lava-ward one. And standing exactly on the
|
|
363
|
+
// boundary is precisely what clampToSafeZone hands back.
|
|
364
|
+
if (Math.abs(d) < 1e-9) return Number.POSITIVE_INFINITY;
|
|
365
|
+
return d > 0 ? (hi - pos) / d : (lo - pos) / d;
|
|
366
|
+
};
|
|
367
|
+
// The binding axis is whichever runs out first.
|
|
368
|
+
return Math.max(0, Math.min(along(from.x, dir.x, min, max), along(from.y, dir.y, min, max)));
|
|
369
|
+
}
|
|
370
|
+
|
|
344
371
|
function calculateBestDodgeDirection(
|
|
345
372
|
projectile: ProjectileState,
|
|
346
373
|
targetPos: Position,
|
|
347
374
|
canDodgeLeft: boolean,
|
|
348
375
|
canDodgeRight: boolean,
|
|
349
376
|
canOutrun: boolean,
|
|
377
|
+
ticksToImpact: number,
|
|
350
378
|
): Position | null
|
|
351
379
|
{
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
380
|
+
// Every feasible candidate is checked for whether you SURVIVE it, not just the two-sided
|
|
381
|
+
// case. The first version of this fix guarded only `canDodgeLeft && canDodgeRight`, so
|
|
382
|
+
// when one side was feasible it was returned unchecked — and simulateDodge clamps the
|
|
383
|
+
// wizard at the band edge, so it calls a dodge feasible when following it walks into the
|
|
384
|
+
// lava. A sweep of 38,280 suggestions found 114 still doing that.
|
|
385
|
+
// A dodge is only worth recommending if the wizard is still alive at impact.
|
|
386
|
+
const travel = RULES.MOVEMENT_SPEED * Math.max(1, Math.min(ticksToImpact, 600));
|
|
387
|
+
const survives = (dir: Position): boolean => roomBeforeLava(targetPos, dir) >= travel;
|
|
388
|
+
|
|
389
|
+
// Sidesteps first, outrunning last — a sidestep leaves the missile's path while outrunning
|
|
390
|
+
// merely delays it, and that ordering predates the lava work. Room only breaks the tie
|
|
391
|
+
// BETWEEN the two sidesteps.
|
|
392
|
+
const sides: Position[] = [];
|
|
393
|
+
if (canDodgeLeft) sides.push(getDodgeDirection(projectile, targetPos, 'left'));
|
|
394
|
+
if (canDodgeRight) sides.push(getDodgeDirection(projectile, targetPos, 'right'));
|
|
395
|
+
|
|
396
|
+
const safeSides = sides.filter(survives);
|
|
397
|
+
if (safeSides.length > 0)
|
|
360
398
|
{
|
|
361
|
-
return
|
|
399
|
+
return safeSides.reduce((best, dir) =>
|
|
400
|
+
(roomBeforeLava(targetPos, dir) > roomBeforeLava(targetPos, best) ? dir : best));
|
|
362
401
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return getDodgeDirection(projectile, targetPos, 'right');
|
|
366
|
-
}
|
|
367
|
-
else if (canOutrun)
|
|
402
|
+
|
|
403
|
+
if (canOutrun)
|
|
368
404
|
{
|
|
369
|
-
|
|
405
|
+
const away = getDodgeDirection(projectile, targetPos, 'away');
|
|
406
|
+
if (survives(away)) return away;
|
|
370
407
|
}
|
|
371
408
|
|
|
372
|
-
//
|
|
409
|
+
// Nothing survivable. Saying so is the honest answer: a caller reading null shields or
|
|
410
|
+
// blinks instead, which is the right response to "there is nowhere to go". Naming a
|
|
411
|
+
// direction that kills you is not a dodge.
|
|
373
412
|
return null;
|
|
374
413
|
}
|
|
375
414
|
|
package/src/hooks/types.ts
CHANGED
|
@@ -22,7 +22,8 @@ import {Position, Velocity, ProjectileState, MissileConfig, WizardActions, GameS
|
|
|
22
22
|
*/
|
|
23
23
|
export interface EnemyState
|
|
24
24
|
{
|
|
25
|
-
/** Enemy position in world coordinates
|
|
25
|
+
/** Enemy position in world coordinates. The arena is 0-860; the survivable playfield is
|
|
26
|
+
* [30, 830], and outside that band is lava. */
|
|
26
27
|
position: Position;
|
|
27
28
|
/** Enemy velocity in units/tick. */
|
|
28
29
|
velocity: Velocity;
|
|
@@ -108,9 +109,20 @@ export interface ActionBuilder extends FinalAction
|
|
|
108
109
|
* to `missile()` is overwritten before the shot leaves. That makes the enemy impossible
|
|
109
110
|
* to LEAD: you always fire where they are, never where they will be.
|
|
110
111
|
*
|
|
111
|
-
* `lockAim()` holds your angle for the whole cast
|
|
112
|
-
* and
|
|
113
|
-
*
|
|
112
|
+
* `lockAim()` holds your angle for the whole cast — but it is USUALLY THE WRONG TOOL for
|
|
113
|
+
* leading, and this comment used to claim the opposite. Measured over 1,140 matches
|
|
114
|
+
* against the built-ins: auto-aim wins 80.5%, lockAim with a computed lead wins 29.3%,
|
|
115
|
+
* and it degrades monotonically the more shots you lock.
|
|
116
|
+
*
|
|
117
|
+
* The reason is cast time. A cast runs 88-240 ticks — up to 2.4 SECONDS — and lockAim
|
|
118
|
+
* freezes the angle at cast START, so it is stale long before the missile leaves. No bot
|
|
119
|
+
* holds a heading that long.
|
|
120
|
+
*
|
|
121
|
+
* TO LEAD A TARGET, call `aim(degrees)` on every tick of the cast and recompute the lead
|
|
122
|
+
* each time: same harness, 85.4%. That is what makes `getLeadPosition` worth calling.
|
|
123
|
+
*
|
|
124
|
+
* lockAim is for aiming at a PLACE rather than a bot — a lava edge, an escape lane, an
|
|
125
|
+
* area you want denied — where a fixed angle is the point.
|
|
114
126
|
*
|
|
115
127
|
* Chainable — `missile(cfg, ai, angle).lockAim().move(0, 1)` aims, fires and kites.
|
|
116
128
|
*/
|
package/src/rules.ts
CHANGED
|
@@ -199,6 +199,14 @@ export function validateMissileConfig(config: MissileConfig): MissileConfig
|
|
|
199
199
|
* If lastMissileConfig is a MissileConfig, applies warmup multiplier:
|
|
200
200
|
* - Similar to previous: up to 20% faster
|
|
201
201
|
* - Very different: up to 20% slower (switching penalty)
|
|
202
|
+
*
|
|
203
|
+
* @returns cast time in SECONDS — multiply by TICKS_PER_SECOND for ticks.
|
|
204
|
+
*
|
|
205
|
+
* Bot authors almost always want `getMissileCastTime` from utils/combat.ts instead, which
|
|
206
|
+
* has the identical signature and returns TICKS, the unit the guide and every other number in
|
|
207
|
+
* the API use. Both are exported and both appear in vibemancer_api as `(config, last?) =>
|
|
208
|
+
* number`, so the unit was impossible to tell apart: 2.747 read as three ticks rather than
|
|
209
|
+
* 275.
|
|
202
210
|
*/
|
|
203
211
|
export function calculateMissileCastTime(config: MissileConfig, lastMissileConfig: MissileConfig | undefined | null = null): number
|
|
204
212
|
{
|
package/src/types.ts
CHANGED
|
@@ -113,17 +113,21 @@ export interface GameState
|
|
|
113
113
|
* Actions returned by wizard each tick.
|
|
114
114
|
*
|
|
115
115
|
* Movement uses world-space coordinates:
|
|
116
|
-
* - x:
|
|
117
|
-
* - y:
|
|
118
|
-
* -
|
|
116
|
+
* - x: positive = right, negative = left
|
|
117
|
+
* - y: positive = down, negative = up
|
|
118
|
+
* - Only the DIRECTION matters: the vector is normalised, and magnitude is capped at 1.
|
|
119
|
+
* move(1, 0), move(5, 0) and move(100, 0) are identical; move(0.5, 0) is half speed.
|
|
119
120
|
* - No rotation tracking - just output (x, y) direction
|
|
121
|
+
*
|
|
122
|
+
* The old wording here described a [-100, 100] scale, which has not been true for a long
|
|
123
|
+
* time and is 100x off.
|
|
120
124
|
*/
|
|
121
125
|
export interface WizardActions
|
|
122
126
|
{
|
|
123
127
|
/**
|
|
124
|
-
* Movement direction in world-space.
|
|
125
|
-
*
|
|
126
|
-
*
|
|
128
|
+
* Movement direction in world-space. Only the direction matters — the vector is
|
|
129
|
+
* normalised and its magnitude is capped at 1, so move(100, 0) and move(1, 0) are the
|
|
130
|
+
* same full-speed step. A magnitude below 1 moves proportionally slower.
|
|
127
131
|
*/
|
|
128
132
|
move: {
|
|
129
133
|
x: number;
|