@vibemancer/core 0.1.3 → 0.1.5

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.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-W7HWJFQ4.js +10599 -0
  3. package/dist/chunk-W7HWJFQ4.js.map +1 -0
  4. package/dist/index-browser.d.ts +1300 -1051
  5. package/dist/index-browser.js +55 -17
  6. package/dist/index.d.ts +53 -3
  7. package/dist/index.js +200 -54
  8. package/dist/index.js.map +1 -1
  9. package/package.json +21 -15
  10. package/src/bots/Hero.ts +867 -0
  11. package/src/bots/berserker/01_Stormchaser.ts +162 -120
  12. package/src/bots/berserker/02_Stormcaller.ts +160 -116
  13. package/src/bots/berserker/03_Stormforger.ts +162 -129
  14. package/src/bots/caster/01_Flamecaller.ts +126 -84
  15. package/src/bots/caster/02_Pyromancer.ts +148 -101
  16. package/src/bots/caster/03_Infernalist.ts +180 -160
  17. package/src/bots/defensive/01_Turtle.ts +48 -44
  18. package/src/bots/defensive/02_Sentinel.ts +57 -53
  19. package/src/bots/defensive/03_Golem.ts +85 -97
  20. package/src/bots/duelist/01_Battlemage.ts +157 -122
  21. package/src/bots/duelist/02_Warmage.ts +166 -121
  22. package/src/bots/duelist/03_Archmage.ts +198 -178
  23. package/src/bots/homing/01_Bonemancer.ts +20 -32
  24. package/src/bots/homing/02_Lich.ts +145 -93
  25. package/src/bots/homing/03_Archlich.ts +129 -60
  26. package/src/bots/kiter/01_Spellspinner.ts +145 -107
  27. package/src/bots/kiter/02_Spellweaver.ts +153 -105
  28. package/src/bots/kiter/03_Spellbinder.ts +168 -123
  29. package/src/bots/melee/01_Shadowblade.ts +131 -75
  30. package/src/bots/melee/02_Nightblade.ts +174 -130
  31. package/src/bots/melee/03_Voidblade.ts +266 -168
  32. package/src/bots/registry.ts +44 -37
  33. package/src/bots/shared.ts +185 -25
  34. package/src/bots/sniper/01_Spellshot.ts +151 -98
  35. package/src/bots/sniper/02_Spelltracer.ts +173 -128
  36. package/src/bots/sniper/03_Spellseeker.ts +177 -152
  37. package/src/bots/standalone/Critter.ts +46 -52
  38. package/src/bots/standalone/Doombringer.ts +36 -40
  39. package/src/bots/standalone/Hogger.ts +120 -89
  40. package/src/bots/standalone/Rookie.ts +21 -23
  41. package/src/bots/standalone/TargetDummy.ts +5 -6
  42. package/src/bots/test/cheater.ts +91 -85
  43. package/src/bots/test/crasher.ts +26 -28
  44. package/src/engine/bundle-fight.ts +242 -0
  45. package/src/engine/hooks-runtime.ts +58 -18
  46. package/src/engine/manual-match.ts +33 -25
  47. package/src/engine/missile-templates.ts +25 -43
  48. package/src/engine/optimizer.ts +7 -7
  49. package/src/engine/params-runtime.ts +3 -3
  50. package/src/engine/physics.ts +33 -23
  51. package/src/engine/sandbox-browser.ts +8 -7
  52. package/src/engine/sandbox-compile.ts +1 -1
  53. package/src/engine/sandbox-harness.ts +299 -367
  54. package/src/engine/sandbox.ts +3 -3
  55. package/src/engine/simulation.ts +291 -76
  56. package/src/engine/spells.ts +9 -12
  57. package/src/engine-version.ts +1 -1
  58. package/src/hooks/action-builders.ts +76 -21
  59. package/src/hooks/index.ts +17 -9
  60. package/src/hooks/state-hooks.ts +61 -25
  61. package/src/hooks/threat-analysis.ts +44 -20
  62. package/src/hooks/types.ts +62 -5
  63. package/src/index.ts +2 -0
  64. package/src/rules.ts +209 -63
  65. package/src/stats.ts +2 -2
  66. package/src/testing.ts +6 -30
  67. package/src/trace.ts +63 -3
  68. package/src/types.ts +127 -14
  69. package/src/utils/angles.ts +1 -0
  70. package/src/utils/combat.ts +98 -6
  71. package/src/utils/spatial.ts +3 -3
  72. package/dist/chunk-B7YYYBEC.js +0 -9140
  73. package/dist/chunk-B7YYYBEC.js.map +0 -1
  74. package/src/hooks/bot-wrapper.ts +0 -84
@@ -7,13 +7,13 @@
7
7
 
8
8
  import {Position, ProjectileState} from '../types.js';
9
9
  import {
10
+ RULES,
10
11
  COLLISION_RADIUS,
11
12
  MISSILE_BASE_RADIUS,
12
13
  MISSILE_RADIUS_PER_DAMAGE,
13
- MOVE_SPEED,
14
- SHIELD_CAST_TIME,
15
- ARENA_WIDTH,
16
- ARENA_HEIGHT,
14
+ ARENA_SIZE,
15
+ ARENA_MIN,
16
+ ARENA_MAX,
17
17
  ARENA_WATER_BUFFER,
18
18
  } from '../rules.js';
19
19
  import {distanceTo} from '../utils/distance.js';
@@ -55,6 +55,7 @@ export function analyzeThreats(
55
55
  (projectile.position.x - myPos.x) ** 2 +
56
56
  (projectile.position.y - myPos.y) ** 2,
57
57
  );
58
+ // Stryker disable next-line EqualityOperator: < vs <= equivalent (exact distance never equals constant in practice)
58
59
  if (analysis.willHit || missileDistance < THREAT_RELEVANCE_DISTANCE)
59
60
  {
60
61
  threats.push(analysis);
@@ -83,6 +84,7 @@ function analyzeOneThreat(
83
84
  // Dodge simulation uses a larger collision distance to account for swept circle collision.
84
85
  // The real game checks the entire missile path each tick (swept), but the simulation only
85
86
  // checks endpoints. Adding half the missile speed compensates for this gap.
87
+ // Stryker disable next-line ArithmeticOperator: +speed*0.5 vs -speed*0.5 or /0.5 — only affects tight-margin dodge outcomes which are inherently stochastic
86
88
  const dodgeCollisionDist = collisionDist + projectile.speed * 0.5;
87
89
 
88
90
  // Simulate missile trajectory toward stationary target
@@ -115,10 +117,11 @@ function analyzeOneThreat(
115
117
 
116
118
  // Calculate shield timing
117
119
  // Can block if: ticksUntilReady + shield cast time < ticksToImpact
118
- const canBlockInTime = ticksToImpact > ticksUntilReady + SHIELD_CAST_TIME + 1;
120
+ // Stryker disable next-line EqualityOperator,ArithmeticOperator: ±1 tick boundary is intentional buffer, not precisely testable
121
+ const canBlockInTime = ticksToImpact > ticksUntilReady + RULES.SHIELD_CAST_TIME + 1;
119
122
 
120
123
  // When to start casting shield (leave 1 tick buffer)
121
- const ticksToStartShield = Math.max(0, ticksToImpact - SHIELD_CAST_TIME - 1);
124
+ const ticksToStartShield = Math.max(0, ticksToImpact - RULES.SHIELD_CAST_TIME - 1);
122
125
 
123
126
  return {
124
127
  id: projectile.id,
@@ -150,19 +153,22 @@ function simulateMissileToTarget(
150
153
  const speed = projectile.speed;
151
154
  const turnRate = projectile.turnRate;
152
155
 
156
+ // Stryker disable next-line EqualityOperator: <= vs < equivalent (off-by-one at max tick boundary is non-observable)
153
157
  for (let tick = 1; tick <= maxTicks; tick++)
154
158
  {
155
- // Apply homing (turn toward target)
159
+ // Stryker disable next-line ConditionalExpression,EqualityOperator: turnRate>=0 equivalent (homing with rate 0 is no-op)
156
160
  if (turnRate > 0)
157
161
  {
158
162
  const desiredAngle = angleTo(pos, targetPos);
159
163
  const diff = angleDiff(rotation, desiredAngle);
160
164
  const maxTurn = turnRate;
161
165
 
166
+ // Stryker disable all: homing snap — equivalent mutations (removing snap or changing boundary doesn't affect hit outcome)
162
167
  if (Math.abs(diff) <= maxTurn)
163
168
  {
164
169
  rotation = desiredAngle;
165
170
  }
171
+ // Stryker restore all
166
172
  else
167
173
  {
168
174
  rotation = normalizeAngle(rotation + Math.sign(diff) * maxTurn);
@@ -184,6 +190,7 @@ function simulateMissileToTarget(
184
190
 
185
191
  // Check collision
186
192
  const dist = distanceTo(pos, targetPos);
193
+ // Stryker disable next-line EqualityOperator: <= vs < equivalent (float distance never exactly equals collisionDist)
187
194
  if (dist <= collisionDist)
188
195
  {
189
196
  return {willHit: true, ticksToImpact: tick};
@@ -208,11 +215,12 @@ function simulateDodge(
208
215
  // Calculate dodge direction vector
209
216
  const dodgeDir = getDodgeDirection(projectile, startPos, direction);
210
217
 
211
- // If we can't determine a dodge direction, fail
218
+ // Stryker disable all: guard is equivalent — zero vector means zero movement, missile still hits, returns false either way
212
219
  if (dodgeDir.x === 0 && dodgeDir.y === 0)
213
220
  {
214
221
  return false;
215
222
  }
223
+ // Stryker restore all
216
224
 
217
225
  let targetPos = {...startPos};
218
226
  let missilePos = {...projectile.position};
@@ -221,25 +229,29 @@ function simulateDodge(
221
229
  const speed = projectile.speed;
222
230
  const turnRate = projectile.turnRate;
223
231
 
232
+ // Stryker disable next-line EqualityOperator,UpdateOperator: <= vs < off-by-one equivalent; tick-- causes timeout not assertion failure
224
233
  for (let tick = 1; tick <= maxTicks; tick++)
225
234
  {
226
- // Move target
227
- const newX = targetPos.x + dodgeDir.x * MOVE_SPEED;
228
- const newY = targetPos.y + dodgeDir.y * MOVE_SPEED;
235
+ // Stryker disable next-line ArithmeticOperator: RULES.MOVEMENT_SPEED=1 makes * and / identical; sign flip equivalent when wizard is wall-clamped
236
+ const newX = targetPos.x + dodgeDir.x * RULES.MOVEMENT_SPEED;
237
+ // Stryker disable next-line ArithmeticOperator: same as above
238
+ const newY = targetPos.y + dodgeDir.y * RULES.MOVEMENT_SPEED;
229
239
 
230
- // Clamp to arena bounds
240
+ // Stryker disable all: arena clamp sign — +/- COLLISION_RADIUS equivalent (wizard rarely reaches exact wall boundary during dodge)
231
241
  targetPos = {
232
- x: Math.max(COLLISION_RADIUS, Math.min(ARENA_WIDTH - COLLISION_RADIUS, newX)),
233
- y: Math.max(COLLISION_RADIUS, Math.min(ARENA_HEIGHT - COLLISION_RADIUS, newY)),
242
+ x: Math.max(ARENA_MIN + COLLISION_RADIUS, Math.min(ARENA_MAX - COLLISION_RADIUS, newX)),
243
+ y: Math.max(ARENA_MIN + COLLISION_RADIUS, Math.min(ARENA_MAX - COLLISION_RADIUS, newY)),
234
244
  };
245
+ // Stryker restore all
235
246
 
236
- // Apply missile homing toward new target position
247
+ // Stryker disable next-line ConditionalExpression,EqualityOperator: turnRate>=0 equivalent (homing with rate 0 is no-op)
237
248
  if (turnRate > 0)
238
249
  {
239
250
  const desiredAngle = angleTo(missilePos, targetPos);
240
251
  const diff = angleDiff(missileRotation, desiredAngle);
241
252
  const maxTurn = turnRate;
242
253
 
254
+ // Stryker disable all: homing snap/incremental — equivalent mutations (removing either branch doesn't change dodge outcome)
243
255
  if (Math.abs(diff) <= maxTurn)
244
256
  {
245
257
  missileRotation = desiredAngle;
@@ -248,6 +260,7 @@ function simulateDodge(
248
260
  {
249
261
  missileRotation = normalizeAngle(missileRotation + Math.sign(diff) * maxTurn);
250
262
  }
263
+ // Stryker restore all
251
264
  }
252
265
 
253
266
  // Move missile
@@ -257,14 +270,16 @@ function simulateDodge(
257
270
  y: missilePos.y + Math.sin(rad) * speed,
258
271
  };
259
272
 
260
- // Check if missile is out of bounds
273
+ // Stryker disable all: OOB early-return equivalent — removing it means missile expires instead, still returns true
261
274
  if (isOutOfBounds(missilePos))
262
275
  {
263
- return true; // Dodged - missile left arena
276
+ return true;
264
277
  }
278
+ // Stryker restore all
265
279
 
266
280
  // Check collision
267
281
  const dist = distanceTo(missilePos, targetPos);
282
+ // Stryker disable next-line EqualityOperator: <= vs < equivalent (float dist never exactly equals collisionDist)
268
283
  if (dist <= collisionDist)
269
284
  {
270
285
  return false; // Got hit while dodging
@@ -327,7 +342,7 @@ function calculateBestDodgeDirection(
327
342
  canOutrun: boolean,
328
343
  ): Position | null
329
344
  {
330
- // Prefer sidestep over outrunning (faster result)
345
+ // Stryker disable next-line ConditionalExpression: replacing with false just falls through to the `else if (canDodgeLeft)` which returns the same value
331
346
  if (canDodgeLeft && canDodgeRight)
332
347
  {
333
348
  // Both sides work - pick the one closer to where we want to be
@@ -358,8 +373,17 @@ function isOutOfBounds(pos: Position): boolean
358
373
  {
359
374
  return (
360
375
  pos.x < -ARENA_WATER_BUFFER ||
361
- pos.x > ARENA_WIDTH + ARENA_WATER_BUFFER ||
376
+ pos.x > ARENA_SIZE + ARENA_WATER_BUFFER ||
362
377
  pos.y < -ARENA_WATER_BUFFER ||
363
- pos.y > ARENA_HEIGHT + ARENA_WATER_BUFFER
378
+ pos.y > ARENA_SIZE + ARENA_WATER_BUFFER
364
379
  );
365
380
  }
381
+
382
+ /** @internal Exposed for mutation testing only. */
383
+ export const _testing = {
384
+ simulateMissileToTarget,
385
+ simulateDodge,
386
+ getDodgeDirection,
387
+ calculateBestDodgeDirection,
388
+ isOutOfBounds,
389
+ };
@@ -11,7 +11,7 @@
11
11
  * Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
12
12
  */
13
13
 
14
- import {Position, Velocity, ProjectileState, MissileConfig, MissileAIFunction, WizardActions} from '../types.js';
14
+ import {Position, Velocity, ProjectileState, MissileConfig, WizardActions, GameState, MissileActions} from '../types.js';
15
15
 
16
16
  /**
17
17
  * Enemy wizard state as seen by your bot.
@@ -31,6 +31,14 @@ export interface EnemyState
31
31
  status: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
32
32
  /** Which spell enemy is casting, or null. */
33
33
  castingSpell: 'missile' | 'shield' | 'blink' | null;
34
+ /** Cast progress in ticks (0 if not casting). */
35
+ castProgress: number;
36
+ /** Total cast duration in ticks (0 if not casting). */
37
+ castDuration: number;
38
+ /** Remaining GCD ticks (0 if not in GCD). */
39
+ gcdRemaining: number;
40
+ /** Duration the enemy has been channeling in ticks (0 if not channeling). */
41
+ channelDuration: number;
34
42
  /** Enemy shield block multiplier (0 if not shielding, 0.3-0.9 if shielding). */
35
43
  shieldStrength: number;
36
44
  }
@@ -93,15 +101,15 @@ export interface ActionBuilder extends FinalAction
93
101
  }
94
102
 
95
103
  /**
96
- * Bot function type for the hooks API.
104
+ * Wizard function type for the hooks API.
97
105
  * Called every tick. Read state with hooks, return an action.
98
106
  */
99
- export type BotFunction = () => FinalAction;
107
+ export type WizardFunction = () => FinalAction;
100
108
 
101
109
  /**
102
110
  * Internal context for game state hooks.
103
111
  */
104
- export interface BotContext
112
+ export interface WizardContext
105
113
  {
106
114
  entityId: string;
107
115
  tick: number;
@@ -117,6 +125,7 @@ export interface BotContext
117
125
  channelDuration?: number;
118
126
  gcdRemaining?: number;
119
127
  blinkCooldown: number;
128
+ lastMissileConfig?: MissileConfig;
120
129
  enemies: Array<{
121
130
  id: string;
122
131
  position: Position;
@@ -124,6 +133,9 @@ export interface BotContext
124
133
  health: number;
125
134
  state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
126
135
  castingSpell?: 'missile' | 'shield' | 'blink';
136
+ castProgress?: number;
137
+ castDuration?: number;
138
+ gcdRemaining?: number;
127
139
  channelingSpell?: 'shield';
128
140
  channelDuration?: number;
129
141
  }>;
@@ -134,9 +146,54 @@ export interface BotContext
134
146
  damageDealt: number;
135
147
  damageTaken: number;
136
148
  lastHitTick: number;
149
+ random: () => number;
137
150
  }
138
151
 
152
+ /**
153
+ * Context available to missile AI functions via getMissileContext().
154
+ * Provides missile state, the full game state from the owner's perspective,
155
+ * and a seeded PRNG.
156
+ */
157
+ export interface MissileContext
158
+ {
159
+ /** Missile position in world coordinates. */
160
+ position: Position;
161
+ /** Missile heading in degrees (0=right, 90=down). */
162
+ rotation: number;
163
+ /** Missile speed in units/tick. */
164
+ speed: number;
165
+ /** Missile turn rate in degrees/tick. */
166
+ turnRate: number;
167
+ /** Missile damage on hit. */
168
+ damage: number;
169
+ /** Ticks remaining before the missile expires. */
170
+ remainingTicks: number;
171
+ /** ID of the wizard who owns this missile. */
172
+ ownerId: string;
173
+ /** Full game state from the missile owner's perspective. */
174
+ worldState: GameState;
175
+ /** Seeded PRNG [0, 1). Deterministic per missile per tick. */
176
+ random: () => number;
177
+ }
178
+
179
+ /**
180
+ * Action returned by a missile AI function.
181
+ * Call turnToward(x, y) to steer, or flyStraight() to coast.
182
+ */
183
+ export interface MissileAction
184
+ {
185
+ /** Internal: extract the MissileActions */
186
+ readonly _toMissileAction: () => MissileActions;
187
+ }
188
+
189
+ /**
190
+ * Missile AI function type for the hooks API.
191
+ * Called every tick for each in-flight missile.
192
+ * Read state with getMissileContext(), return a MissileAction.
193
+ */
194
+ export type MissileFunction = () => MissileAction;
195
+
139
196
  /**
140
197
  * Missile configuration for action builders.
141
198
  */
142
- export type {MissileConfig, MissileAIFunction};
199
+ export type {MissileConfig};
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ export * from './engine/params-runtime.js';
14
14
  export * from './engine/optimizer.js';
15
15
  export {BotBundle, compileMatchBundle, MatchSandbox, sandboxFight, sandboxSimulate} from './engine/sandbox.js';
16
16
  export type {CompileOptions, SandboxOptions} from './engine/sandbox.js';
17
+ export {buildMatchTemplate, runBundleFight, runBundleSimulate} from './engine/bundle-fight.js';
18
+ export type {RunBundleFightOptions, RunBundleSimulateOptions} from './engine/bundle-fight.js';
17
19
  export {BrowserMatchSandbox, BrowserManualMatchSandbox, browserSandboxFight, browserSandboxSimulate, createWorkerScript} from './engine/sandbox-browser.js';
18
20
  export type {BrowserSandboxOptions, ManualMatchInitOptions, ManualMatchStepRequest, WorkerFactory, WorkerLike} from './engine/sandbox-browser.js';
19
21
  export {testBot} from './testing.js';