@vibemancer/core 1.0.8 → 1.0.10
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-BLioGWvO.d.ts} +97 -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 +227 -200
- package/src/trace.ts +497 -490
- package/src/types.ts +9 -3
- package/dist/chunk-MUU3ELH5.js.map +0 -1
package/src/trace.ts
CHANGED
|
@@ -1,490 +1,497 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VIBEMANCER - FIGHT TRACE
|
|
3
|
-
*
|
|
4
|
-
* Extracts a structured event log from a SimulateResult history.
|
|
5
|
-
* Both bots' actions are tracked: state changes, missile launches with
|
|
6
|
-
* full config, hits, damage, dodge proximity, movement patterns.
|
|
7
|
-
*
|
|
8
|
-
* Used by:
|
|
9
|
-
* - vibemancer trace (CLI debug command)
|
|
10
|
-
* - scripts/fight-trace.ts (internal diagnostic)
|
|
11
|
-
* - User tests that want event-level analysis
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type {GameState, ProjectileState, SimEvent} from './types.js';
|
|
15
|
-
import type {SimulateResult, BotError} from './engine/simulation.js';
|
|
16
|
-
import {distanceTo} from './utils/distance.js';
|
|
17
|
-
|
|
18
|
-
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
export interface TraceEvent
|
|
21
|
-
{
|
|
22
|
-
tick: number;
|
|
23
|
-
/** 'W1' = wizard-1, 'W2' = wizard-2 */
|
|
24
|
-
actor: 'W1' | 'W2';
|
|
25
|
-
type: TraceEventType;
|
|
26
|
-
detail: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type TraceEventType =
|
|
30
|
-
| 'STATE' // State transition (idle→casting, etc.)
|
|
31
|
-
| 'FIRE' // Missile launched (includes config + range)
|
|
32
|
-
| 'HIT' // Damage dealt to opponent
|
|
33
|
-
| 'HURT' // Damage taken from opponent
|
|
34
|
-
| 'DEATH' // Wizard killed (missile hit)
|
|
35
|
-
| 'LAVA_DEATH' // Wizard killed by lava (knockback/walk into border)
|
|
36
|
-
| 'SHIELD_BLOCK' // Shield absorbed damage (shows blocked + through amounts)
|
|
37
|
-
| 'KNOCKBACK' // Knocked back by missile impact
|
|
38
|
-
| 'BLINK' // Blink teleport (from → to positions)
|
|
39
|
-
| 'DODGE_START' // Enemy missile enters 150u proximity
|
|
40
|
-
| 'DODGE_CLOSE' // Enemy missile enters 50u proximity
|
|
41
|
-
| 'MOVE' // Movement summary (every 100 ticks)
|
|
42
|
-
| 'ERROR' // Bot or missile AI threw a runtime error
|
|
43
|
-
| 'WARNING'; // Common mistake detected (move/blink confusion, etc.)
|
|
44
|
-
|
|
45
|
-
export interface TraceSummary
|
|
46
|
-
{
|
|
47
|
-
winner: string;
|
|
48
|
-
ticks: number;
|
|
49
|
-
w1Name: string;
|
|
50
|
-
w2Name: string;
|
|
51
|
-
w1FinalHp: number;
|
|
52
|
-
w2FinalHp: number;
|
|
53
|
-
w1: TraceBotSummary;
|
|
54
|
-
w2: TraceBotSummary;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface TraceBotSummary
|
|
58
|
-
{
|
|
59
|
-
missilesLaunched: number;
|
|
60
|
-
hits: number;
|
|
61
|
-
damageDealt: number;
|
|
62
|
-
damageReceived: number;
|
|
63
|
-
shields: number;
|
|
64
|
-
blinks: number;
|
|
65
|
-
dodgeEncounters: number;
|
|
66
|
-
causeOfDeath: 'missile' | 'lava' | 'alive' | 'timeout';
|
|
67
|
-
movement: {strafe: number; approach: number; retreat: number; still: number};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ─── Event Extraction ────────────────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Extract a structured event log from simulation history.
|
|
74
|
-
* Tracks both bots' state changes, missile launches (with full config + range),
|
|
75
|
-
* hits, damage, dodge proximity, movement patterns, and runtime errors.
|
|
76
|
-
*/
|
|
77
|
-
export function extractTraceEvents(history: GameState[], errors?: BotError[]): TraceEvent[]
|
|
78
|
-
{
|
|
79
|
-
const events: TraceEvent[] = [];
|
|
80
|
-
|
|
81
|
-
let prevW1State = '';
|
|
82
|
-
let prevW2State = '';
|
|
83
|
-
let prevW1Health = history[0]?.health ?? 60;
|
|
84
|
-
let prevW2Health = history[0]?.enemies[0]?.health ?? 60;
|
|
85
|
-
let prevW1Spell = '';
|
|
86
|
-
let prevW2Spell = '';
|
|
87
|
-
const prevProjectileIds = new Set<string>();
|
|
88
|
-
|
|
89
|
-
for (let i = 0; i < history.length; i++)
|
|
90
|
-
{
|
|
91
|
-
const state = history[i]!;
|
|
92
|
-
const tick = state.tick;
|
|
93
|
-
const enemy = state.enemies[0]!;
|
|
94
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
95
|
-
|
|
96
|
-
// --- W1 state transitions ---
|
|
97
|
-
const w1State = state.state;
|
|
98
|
-
const w1Spell = state.castingSpell ?? state.channelingSpell ?? '';
|
|
99
|
-
if (w1State !== prevW1State || w1Spell !== prevW1Spell)
|
|
100
|
-
{
|
|
101
|
-
let detail = `${prevW1State || '?'}→${w1State}`;
|
|
102
|
-
if (w1Spell) detail += ` (${w1Spell})`;
|
|
103
|
-
if (w1State === 'casting' && state.castDuration) detail += ` [${state.castDuration}t]`;
|
|
104
|
-
detail += ` dist=${distance.toFixed(0)}`;
|
|
105
|
-
events.push({tick, actor: 'W1', type: 'STATE', detail});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// --- W2 state transitions ---
|
|
109
|
-
const w2State = enemy.state;
|
|
110
|
-
const w2Spell = enemy.castingSpell ?? enemy.channelingSpell ?? '';
|
|
111
|
-
if (w2State !== prevW2State || w2Spell !== prevW2Spell)
|
|
112
|
-
{
|
|
113
|
-
let detail = `${prevW2State || '?'}→${w2State}`;
|
|
114
|
-
if (w2Spell) detail += ` (${w2Spell})`;
|
|
115
|
-
if (w2State === 'casting' && enemy.castDuration) detail += ` [${enemy.castDuration}t]`;
|
|
116
|
-
detail += ` dist=${distance.toFixed(0)}`;
|
|
117
|
-
events.push({tick, actor: 'W2', type: 'STATE', detail});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// --- Damage events ---
|
|
121
|
-
if (enemy.health < prevW2Health)
|
|
122
|
-
{
|
|
123
|
-
const dmg = prevW2Health - enemy.health;
|
|
124
|
-
events.push({
|
|
125
|
-
tick, actor: 'W1', type: 'HIT',
|
|
126
|
-
detail: `dealt ${dmg.toFixed(1)} → enemy HP=${enemy.health.toFixed(1)} dist=${distance.toFixed(0)}`,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
if (state.health < prevW1Health)
|
|
130
|
-
{
|
|
131
|
-
const dmg = prevW1Health - state.health;
|
|
132
|
-
const stateTag = state.state === 'channeling' ? ' (SHIELDED)'
|
|
133
|
-
: state.state === 'casting' ? ` (CASTING ${state.castingSpell})`
|
|
134
|
-
: state.state === 'gcd_locked' ? ' (GCD)'
|
|
135
|
-
: ` (${state.state})`;
|
|
136
|
-
events.push({
|
|
137
|
-
tick, actor: 'W1', type: 'HURT',
|
|
138
|
-
detail: `took ${dmg.toFixed(1)} → HP=${state.health.toFixed(1)}${stateTag} dist=${distance.toFixed(0)}`,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
// W2 damage taken = W1 damage dealt (already covered above as HIT)
|
|
142
|
-
// W2 damage dealt = W1 damage taken (already covered above as HURT)
|
|
143
|
-
|
|
144
|
-
// --- Missile launches (with full config) ---
|
|
145
|
-
const currentIds = new Set<string>();
|
|
146
|
-
for (const proj of state.projectiles)
|
|
147
|
-
{
|
|
148
|
-
currentIds.add(proj.id);
|
|
149
|
-
if (!prevProjectileIds.has(proj.id))
|
|
150
|
-
{
|
|
151
|
-
const owner: 'W1' | 'W2' = proj.ownerId === 'wizard-1' ? 'W1' : 'W2';
|
|
152
|
-
const range = (proj.speed * proj.remainingTicks).toFixed(0);
|
|
153
|
-
events.push({
|
|
154
|
-
tick, actor: owner, type: 'FIRE',
|
|
155
|
-
detail: `dmg=${proj.damage} spd=${proj.speed} turn=${proj.turnRate} life=${proj.remainingTicks} range=${range} dist=${distance.toFixed(0)}`,
|
|
156
|
-
});
|
|
157
|
-
// Warn if missile range < distance (will expire before reaching target)
|
|
158
|
-
if (proj.speed * proj.remainingTicks < distance * 0.8 && proj.turnRate >= 0)
|
|
159
|
-
{
|
|
160
|
-
events.push({
|
|
161
|
-
tick, actor: owner, type: 'WARNING',
|
|
162
|
-
detail: `missile range (${range}) may be too short for distance (${distance.toFixed(0)}) — consider increasing duration`,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
prevProjectileIds.clear();
|
|
168
|
-
for (const id of currentIds) prevProjectileIds.add(id);
|
|
169
|
-
|
|
170
|
-
// --- Dodge proximity events (enemy missiles approaching W1) ---
|
|
171
|
-
if (i > 0)
|
|
172
|
-
{
|
|
173
|
-
const speed = Math.sqrt(state.velocity.x ** 2 + state.velocity.y ** 2);
|
|
174
|
-
for (const proj of state.projectiles)
|
|
175
|
-
{
|
|
176
|
-
if (proj.ownerId === 'wizard-1') continue;
|
|
177
|
-
const missileDistance = distanceTo(state.position, proj.position);
|
|
178
|
-
const prevProj = history[i - 1]!.projectiles.find((p) => p.id === proj.id);
|
|
179
|
-
const prevMissileDist = prevProj ? distanceTo(history[i - 1]!.position, prevProj.position) : Infinity;
|
|
180
|
-
|
|
181
|
-
if (missileDistance <= 150 && prevMissileDist > 150)
|
|
182
|
-
{
|
|
183
|
-
events.push({
|
|
184
|
-
tick, actor: 'W1', type: 'DODGE_START',
|
|
185
|
-
detail: formatDodgeDetail(state, proj, missileDistance, speed),
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
if (missileDistance <= 50 && prevMissileDist > 50)
|
|
189
|
-
{
|
|
190
|
-
events.push({
|
|
191
|
-
tick, actor: 'W1', type: 'DODGE_CLOSE',
|
|
192
|
-
detail: formatDodgeDetail(state, proj, missileDistance, speed),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// --- Simulation events (death, lava, shield block, knockback, blink) ---
|
|
199
|
-
if (state.events)
|
|
200
|
-
{
|
|
201
|
-
for (const ev of state.events)
|
|
202
|
-
{
|
|
203
|
-
const actor = actorFromId(ev);
|
|
204
|
-
if (ev.type === 'wizard-death')
|
|
205
|
-
{
|
|
206
|
-
events.push({tick, actor, type: 'DEATH', detail: `killed at (${ev.position.x.toFixed(0)},${ev.position.y.toFixed(0)})`});
|
|
207
|
-
}
|
|
208
|
-
else if (ev.type === 'wizard-lava-death')
|
|
209
|
-
{
|
|
210
|
-
events.push({tick, actor, type: 'LAVA_DEATH', detail: `walked/knocked into lava at (${ev.position.x.toFixed(0)},${ev.position.y.toFixed(0)})`});
|
|
211
|
-
}
|
|
212
|
-
else if (ev.type === 'shield-block')
|
|
213
|
-
{
|
|
214
|
-
events.push({tick, actor, type: 'SHIELD_BLOCK', detail: `blocked ${ev.damageBlocked.toFixed(1)} (${ev.damageThrough.toFixed(1)} through)`});
|
|
215
|
-
}
|
|
216
|
-
else if (ev.type === 'missile-hit')
|
|
217
|
-
{
|
|
218
|
-
const target = ev.targetId === 'wizard-1' ? 'W1' : 'W2';
|
|
219
|
-
const owner = ev.ownerId === 'wizard-1' ? 'W1' : 'W2';
|
|
220
|
-
if (ev.actualDamage < ev.damage)
|
|
221
|
-
{
|
|
222
|
-
events.push({tick, actor: target, type: 'HURT', detail: `hit by ${owner} missile: ${ev.damage} raw → ${ev.actualDamage.toFixed(1)} actual (shielded) dist=${distance.toFixed(0)}`});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
else if (ev.type === 'blink')
|
|
226
|
-
{
|
|
227
|
-
const blinkDist = distanceTo(ev.from, ev.to);
|
|
228
|
-
events.push({tick, actor, type: 'BLINK', detail: `(${ev.from.x.toFixed(0)},${ev.from.y.toFixed(0)}) → (${ev.to.x.toFixed(0)},${ev.to.y.toFixed(0)}) ${blinkDist.toFixed(0)}u`});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// --- Movement summary every 100 ticks ---
|
|
234
|
-
if (tick > 0 && tick % 100 === 0)
|
|
235
|
-
{
|
|
236
|
-
const speed = Math.sqrt(state.velocity.x ** 2 + state.velocity.y ** 2);
|
|
237
|
-
const moveType = classifyMovement(state, enemy, speed);
|
|
238
|
-
const incomingMissiles = state.projectiles.filter((p) => p.ownerId !== 'wizard-1').length;
|
|
239
|
-
events.push({
|
|
240
|
-
tick, actor: 'W1', type: 'MOVE',
|
|
241
|
-
detail: `${moveType} spd=${speed.toFixed(2)} dist=${distance.toFixed(0)} missiles=${incomingMissiles} ${state.state} hp=${state.health.toFixed(1)}`,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
prevW1State = w1State;
|
|
246
|
-
prevW2State = w2State;
|
|
247
|
-
prevW1Health = state.health;
|
|
248
|
-
prevW2Health = enemy.health;
|
|
249
|
-
prevW1Spell = w1Spell;
|
|
250
|
-
prevW2Spell = w2Spell;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// --- Inject runtime errors as ERROR events ---
|
|
254
|
-
if (errors && errors.length > 0)
|
|
255
|
-
{
|
|
256
|
-
for (const err of errors)
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
tips.push(`
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
function
|
|
461
|
-
{
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function
|
|
477
|
-
{
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1
|
+
/**
|
|
2
|
+
* VIBEMANCER - FIGHT TRACE
|
|
3
|
+
*
|
|
4
|
+
* Extracts a structured event log from a SimulateResult history.
|
|
5
|
+
* Both bots' actions are tracked: state changes, missile launches with
|
|
6
|
+
* full config, hits, damage, dodge proximity, movement patterns.
|
|
7
|
+
*
|
|
8
|
+
* Used by:
|
|
9
|
+
* - vibemancer trace (CLI debug command)
|
|
10
|
+
* - scripts/fight-trace.ts (internal diagnostic)
|
|
11
|
+
* - User tests that want event-level analysis
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {GameState, ProjectileState, SimEvent} from './types.js';
|
|
15
|
+
import type {SimulateResult, BotError} from './engine/simulation.js';
|
|
16
|
+
import {distanceTo} from './utils/distance.js';
|
|
17
|
+
|
|
18
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface TraceEvent
|
|
21
|
+
{
|
|
22
|
+
tick: number;
|
|
23
|
+
/** 'W1' = wizard-1, 'W2' = wizard-2 */
|
|
24
|
+
actor: 'W1' | 'W2';
|
|
25
|
+
type: TraceEventType;
|
|
26
|
+
detail: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TraceEventType =
|
|
30
|
+
| 'STATE' // State transition (idle→casting, etc.)
|
|
31
|
+
| 'FIRE' // Missile launched (includes config + range)
|
|
32
|
+
| 'HIT' // Damage dealt to opponent
|
|
33
|
+
| 'HURT' // Damage taken from opponent
|
|
34
|
+
| 'DEATH' // Wizard killed (missile hit)
|
|
35
|
+
| 'LAVA_DEATH' // Wizard killed by lava (knockback/walk into border)
|
|
36
|
+
| 'SHIELD_BLOCK' // Shield absorbed damage (shows blocked + through amounts)
|
|
37
|
+
| 'KNOCKBACK' // Knocked back by missile impact
|
|
38
|
+
| 'BLINK' // Blink teleport (from → to positions)
|
|
39
|
+
| 'DODGE_START' // Enemy missile enters 150u proximity
|
|
40
|
+
| 'DODGE_CLOSE' // Enemy missile enters 50u proximity
|
|
41
|
+
| 'MOVE' // Movement summary (every 100 ticks)
|
|
42
|
+
| 'ERROR' // Bot or missile AI threw a runtime error
|
|
43
|
+
| 'WARNING'; // Common mistake detected (move/blink confusion, etc.)
|
|
44
|
+
|
|
45
|
+
export interface TraceSummary
|
|
46
|
+
{
|
|
47
|
+
winner: string;
|
|
48
|
+
ticks: number;
|
|
49
|
+
w1Name: string;
|
|
50
|
+
w2Name: string;
|
|
51
|
+
w1FinalHp: number;
|
|
52
|
+
w2FinalHp: number;
|
|
53
|
+
w1: TraceBotSummary;
|
|
54
|
+
w2: TraceBotSummary;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TraceBotSummary
|
|
58
|
+
{
|
|
59
|
+
missilesLaunched: number;
|
|
60
|
+
hits: number;
|
|
61
|
+
damageDealt: number;
|
|
62
|
+
damageReceived: number;
|
|
63
|
+
shields: number;
|
|
64
|
+
blinks: number;
|
|
65
|
+
dodgeEncounters: number;
|
|
66
|
+
causeOfDeath: 'missile' | 'lava' | 'alive' | 'timeout';
|
|
67
|
+
movement: {strafe: number; approach: number; retreat: number; still: number};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Event Extraction ────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract a structured event log from simulation history.
|
|
74
|
+
* Tracks both bots' state changes, missile launches (with full config + range),
|
|
75
|
+
* hits, damage, dodge proximity, movement patterns, and runtime errors.
|
|
76
|
+
*/
|
|
77
|
+
export function extractTraceEvents(history: GameState[], errors?: BotError[]): TraceEvent[]
|
|
78
|
+
{
|
|
79
|
+
const events: TraceEvent[] = [];
|
|
80
|
+
|
|
81
|
+
let prevW1State = '';
|
|
82
|
+
let prevW2State = '';
|
|
83
|
+
let prevW1Health = history[0]?.health ?? 60;
|
|
84
|
+
let prevW2Health = history[0]?.enemies[0]?.health ?? 60;
|
|
85
|
+
let prevW1Spell = '';
|
|
86
|
+
let prevW2Spell = '';
|
|
87
|
+
const prevProjectileIds = new Set<string>();
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < history.length; i++)
|
|
90
|
+
{
|
|
91
|
+
const state = history[i]!;
|
|
92
|
+
const tick = state.tick;
|
|
93
|
+
const enemy = state.enemies[0]!;
|
|
94
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
95
|
+
|
|
96
|
+
// --- W1 state transitions ---
|
|
97
|
+
const w1State = state.state;
|
|
98
|
+
const w1Spell = state.castingSpell ?? state.channelingSpell ?? '';
|
|
99
|
+
if (w1State !== prevW1State || w1Spell !== prevW1Spell)
|
|
100
|
+
{
|
|
101
|
+
let detail = `${prevW1State || '?'}→${w1State}`;
|
|
102
|
+
if (w1Spell) detail += ` (${w1Spell})`;
|
|
103
|
+
if (w1State === 'casting' && state.castDuration) detail += ` [${state.castDuration}t]`;
|
|
104
|
+
detail += ` dist=${distance.toFixed(0)}`;
|
|
105
|
+
events.push({tick, actor: 'W1', type: 'STATE', detail});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- W2 state transitions ---
|
|
109
|
+
const w2State = enemy.state;
|
|
110
|
+
const w2Spell = enemy.castingSpell ?? enemy.channelingSpell ?? '';
|
|
111
|
+
if (w2State !== prevW2State || w2Spell !== prevW2Spell)
|
|
112
|
+
{
|
|
113
|
+
let detail = `${prevW2State || '?'}→${w2State}`;
|
|
114
|
+
if (w2Spell) detail += ` (${w2Spell})`;
|
|
115
|
+
if (w2State === 'casting' && enemy.castDuration) detail += ` [${enemy.castDuration}t]`;
|
|
116
|
+
detail += ` dist=${distance.toFixed(0)}`;
|
|
117
|
+
events.push({tick, actor: 'W2', type: 'STATE', detail});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Damage events ---
|
|
121
|
+
if (enemy.health < prevW2Health)
|
|
122
|
+
{
|
|
123
|
+
const dmg = prevW2Health - enemy.health;
|
|
124
|
+
events.push({
|
|
125
|
+
tick, actor: 'W1', type: 'HIT',
|
|
126
|
+
detail: `dealt ${dmg.toFixed(1)} → enemy HP=${enemy.health.toFixed(1)} dist=${distance.toFixed(0)}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (state.health < prevW1Health)
|
|
130
|
+
{
|
|
131
|
+
const dmg = prevW1Health - state.health;
|
|
132
|
+
const stateTag = state.state === 'channeling' ? ' (SHIELDED)'
|
|
133
|
+
: state.state === 'casting' ? ` (CASTING ${state.castingSpell})`
|
|
134
|
+
: state.state === 'gcd_locked' ? ' (GCD)'
|
|
135
|
+
: ` (${state.state})`;
|
|
136
|
+
events.push({
|
|
137
|
+
tick, actor: 'W1', type: 'HURT',
|
|
138
|
+
detail: `took ${dmg.toFixed(1)} → HP=${state.health.toFixed(1)}${stateTag} dist=${distance.toFixed(0)}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// W2 damage taken = W1 damage dealt (already covered above as HIT)
|
|
142
|
+
// W2 damage dealt = W1 damage taken (already covered above as HURT)
|
|
143
|
+
|
|
144
|
+
// --- Missile launches (with full config) ---
|
|
145
|
+
const currentIds = new Set<string>();
|
|
146
|
+
for (const proj of state.projectiles)
|
|
147
|
+
{
|
|
148
|
+
currentIds.add(proj.id);
|
|
149
|
+
if (!prevProjectileIds.has(proj.id))
|
|
150
|
+
{
|
|
151
|
+
const owner: 'W1' | 'W2' = proj.ownerId === 'wizard-1' ? 'W1' : 'W2';
|
|
152
|
+
const range = (proj.speed * proj.remainingTicks).toFixed(0);
|
|
153
|
+
events.push({
|
|
154
|
+
tick, actor: owner, type: 'FIRE',
|
|
155
|
+
detail: `dmg=${proj.damage} spd=${proj.speed} turn=${proj.turnRate} life=${proj.remainingTicks} range=${range} dist=${distance.toFixed(0)}`,
|
|
156
|
+
});
|
|
157
|
+
// Warn if missile range < distance (will expire before reaching target)
|
|
158
|
+
if (proj.speed * proj.remainingTicks < distance * 0.8 && proj.turnRate >= 0)
|
|
159
|
+
{
|
|
160
|
+
events.push({
|
|
161
|
+
tick, actor: owner, type: 'WARNING',
|
|
162
|
+
detail: `missile range (${range}) may be too short for distance (${distance.toFixed(0)}) — consider increasing duration`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
prevProjectileIds.clear();
|
|
168
|
+
for (const id of currentIds) prevProjectileIds.add(id);
|
|
169
|
+
|
|
170
|
+
// --- Dodge proximity events (enemy missiles approaching W1) ---
|
|
171
|
+
if (i > 0)
|
|
172
|
+
{
|
|
173
|
+
const speed = Math.sqrt(state.velocity.x ** 2 + state.velocity.y ** 2);
|
|
174
|
+
for (const proj of state.projectiles)
|
|
175
|
+
{
|
|
176
|
+
if (proj.ownerId === 'wizard-1') continue;
|
|
177
|
+
const missileDistance = distanceTo(state.position, proj.position);
|
|
178
|
+
const prevProj = history[i - 1]!.projectiles.find((p) => p.id === proj.id);
|
|
179
|
+
const prevMissileDist = prevProj ? distanceTo(history[i - 1]!.position, prevProj.position) : Infinity;
|
|
180
|
+
|
|
181
|
+
if (missileDistance <= 150 && prevMissileDist > 150)
|
|
182
|
+
{
|
|
183
|
+
events.push({
|
|
184
|
+
tick, actor: 'W1', type: 'DODGE_START',
|
|
185
|
+
detail: formatDodgeDetail(state, proj, missileDistance, speed),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (missileDistance <= 50 && prevMissileDist > 50)
|
|
189
|
+
{
|
|
190
|
+
events.push({
|
|
191
|
+
tick, actor: 'W1', type: 'DODGE_CLOSE',
|
|
192
|
+
detail: formatDodgeDetail(state, proj, missileDistance, speed),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Simulation events (death, lava, shield block, knockback, blink) ---
|
|
199
|
+
if (state.events)
|
|
200
|
+
{
|
|
201
|
+
for (const ev of state.events)
|
|
202
|
+
{
|
|
203
|
+
const actor = actorFromId(ev);
|
|
204
|
+
if (ev.type === 'wizard-death')
|
|
205
|
+
{
|
|
206
|
+
events.push({tick, actor, type: 'DEATH', detail: `killed at (${ev.position.x.toFixed(0)},${ev.position.y.toFixed(0)})`});
|
|
207
|
+
}
|
|
208
|
+
else if (ev.type === 'wizard-lava-death')
|
|
209
|
+
{
|
|
210
|
+
events.push({tick, actor, type: 'LAVA_DEATH', detail: `walked/knocked into lava at (${ev.position.x.toFixed(0)},${ev.position.y.toFixed(0)})`});
|
|
211
|
+
}
|
|
212
|
+
else if (ev.type === 'shield-block')
|
|
213
|
+
{
|
|
214
|
+
events.push({tick, actor, type: 'SHIELD_BLOCK', detail: `blocked ${ev.damageBlocked.toFixed(1)} (${ev.damageThrough.toFixed(1)} through)`});
|
|
215
|
+
}
|
|
216
|
+
else if (ev.type === 'missile-hit')
|
|
217
|
+
{
|
|
218
|
+
const target = ev.targetId === 'wizard-1' ? 'W1' : 'W2';
|
|
219
|
+
const owner = ev.ownerId === 'wizard-1' ? 'W1' : 'W2';
|
|
220
|
+
if (ev.actualDamage < ev.damage)
|
|
221
|
+
{
|
|
222
|
+
events.push({tick, actor: target, type: 'HURT', detail: `hit by ${owner} missile: ${ev.damage} raw → ${ev.actualDamage.toFixed(1)} actual (shielded) dist=${distance.toFixed(0)}`});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (ev.type === 'blink')
|
|
226
|
+
{
|
|
227
|
+
const blinkDist = distanceTo(ev.from, ev.to);
|
|
228
|
+
events.push({tick, actor, type: 'BLINK', detail: `(${ev.from.x.toFixed(0)},${ev.from.y.toFixed(0)}) → (${ev.to.x.toFixed(0)},${ev.to.y.toFixed(0)}) ${blinkDist.toFixed(0)}u`});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Movement summary every 100 ticks ---
|
|
234
|
+
if (tick > 0 && tick % 100 === 0)
|
|
235
|
+
{
|
|
236
|
+
const speed = Math.sqrt(state.velocity.x ** 2 + state.velocity.y ** 2);
|
|
237
|
+
const moveType = classifyMovement(state, enemy, speed);
|
|
238
|
+
const incomingMissiles = state.projectiles.filter((p) => p.ownerId !== 'wizard-1').length;
|
|
239
|
+
events.push({
|
|
240
|
+
tick, actor: 'W1', type: 'MOVE',
|
|
241
|
+
detail: `${moveType} spd=${speed.toFixed(2)} dist=${distance.toFixed(0)} missiles=${incomingMissiles} ${state.state} hp=${state.health.toFixed(1)}`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
prevW1State = w1State;
|
|
246
|
+
prevW2State = w2State;
|
|
247
|
+
prevW1Health = state.health;
|
|
248
|
+
prevW2Health = enemy.health;
|
|
249
|
+
prevW1Spell = w1Spell;
|
|
250
|
+
prevW2Spell = w2Spell;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- Inject runtime errors as ERROR events ---
|
|
254
|
+
if (errors && errors.length > 0)
|
|
255
|
+
{
|
|
256
|
+
for (const err of errors)
|
|
257
|
+
{
|
|
258
|
+
// A missile's throw is recorded against the PROJECTILE, and projectile ids are built
|
|
259
|
+
// as `missile-${wizard.id}-${tick}`, so the owner is in the middle of the string.
|
|
260
|
+
// This branch existed with the comment "missile errors attributed to owner" and
|
|
261
|
+
// both arms returned 'W1', so every missile fault in the game was blamed on wizard
|
|
262
|
+
// 1 — telling an idle bot it had thrown 1,333 times for its opponent's code.
|
|
263
|
+
const missileOwner = /^missile-(wizard-[12])-/.exec(err.entityId);
|
|
264
|
+
const actor: 'W1' | 'W2' = err.entityId === 'wizard-1' ? 'W1'
|
|
265
|
+
: err.entityId === 'wizard-2' ? 'W2'
|
|
266
|
+
: missileOwner ? (missileOwner[1] === 'wizard-2' ? 'W2' : 'W1')
|
|
267
|
+
: 'W1';
|
|
268
|
+
events.push({
|
|
269
|
+
tick: err.tick,
|
|
270
|
+
actor,
|
|
271
|
+
type: 'ERROR',
|
|
272
|
+
detail: err.message,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// Re-sort by tick so errors appear in chronological order
|
|
276
|
+
events.sort((a, b) => a.tick - b.tick || (a.type === 'ERROR' ? 1 : 0));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return events;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─── Summary ─────────────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Generate a summary from trace events and the simulation result.
|
|
286
|
+
*/
|
|
287
|
+
export function summarizeTrace(
|
|
288
|
+
events: TraceEvent[],
|
|
289
|
+
result: SimulateResult,
|
|
290
|
+
w1Name: string,
|
|
291
|
+
w2Name: string,
|
|
292
|
+
): TraceSummary
|
|
293
|
+
{
|
|
294
|
+
const final = result.history[result.history.length - 1];
|
|
295
|
+
|
|
296
|
+
function botSummary(actor: 'W1' | 'W2'): TraceBotSummary
|
|
297
|
+
{
|
|
298
|
+
const myEvents = events.filter((e) => e.actor === actor);
|
|
299
|
+
const moveEvents = myEvents.filter((e) => e.type === 'MOVE');
|
|
300
|
+
return {
|
|
301
|
+
missilesLaunched: myEvents.filter((e) => e.type === 'FIRE').length,
|
|
302
|
+
hits: myEvents.filter((e) => e.type === 'HIT').length,
|
|
303
|
+
damageDealt: myEvents.filter((e) => e.type === 'HIT').reduce((sum, e) =>
|
|
304
|
+
{
|
|
305
|
+
const m = e.detail.match(/dealt ([\d.]+)/);
|
|
306
|
+
return sum + (m ? parseFloat(m[1]!) : 0);
|
|
307
|
+
}, 0),
|
|
308
|
+
damageReceived: myEvents.filter((e) => e.type === 'HURT').reduce((sum, e) =>
|
|
309
|
+
{
|
|
310
|
+
const m = e.detail.match(/took ([\d.]+)/);
|
|
311
|
+
return sum + (m ? parseFloat(m[1]!) : 0);
|
|
312
|
+
}, 0),
|
|
313
|
+
shields: myEvents.filter((e) => e.type === 'STATE' && e.detail.includes('shield')).length,
|
|
314
|
+
blinks: myEvents.filter((e) => e.type === 'STATE' && e.detail.includes('blink')).length,
|
|
315
|
+
dodgeEncounters: myEvents.filter((e) => e.type.startsWith('DODGE')).length,
|
|
316
|
+
causeOfDeath: myEvents.some((e) => e.type === 'LAVA_DEATH') ? 'lava'
|
|
317
|
+
: myEvents.some((e) => e.type === 'DEATH') ? 'missile'
|
|
318
|
+
: (final && ((actor === 'W1' ? final.health : final.enemies[0]?.health ?? 0) <= 0)) ? 'missile'
|
|
319
|
+
: result.winner === null ? 'timeout' : 'alive',
|
|
320
|
+
movement: {
|
|
321
|
+
strafe: moveEvents.filter((e) => e.detail.startsWith('STRAFE')).length,
|
|
322
|
+
approach: moveEvents.filter((e) => e.detail.startsWith('APPROACH')).length,
|
|
323
|
+
retreat: moveEvents.filter((e) => e.detail.startsWith('RETREAT')).length,
|
|
324
|
+
still: moveEvents.filter((e) => e.detail.startsWith('STILL')).length,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const winnerName = result.winner === 'wizard-1' ? w1Name
|
|
330
|
+
: result.winner === 'wizard-2' ? w2Name
|
|
331
|
+
: result.winner === 'draw' ? 'draw'
|
|
332
|
+
: 'timeout';
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
winner: winnerName,
|
|
336
|
+
ticks: result.ticks,
|
|
337
|
+
w1Name,
|
|
338
|
+
w2Name,
|
|
339
|
+
w1FinalHp: final?.health ?? 0,
|
|
340
|
+
w2FinalHp: final?.enemies[0]?.health ?? 0,
|
|
341
|
+
w1: botSummary('W1'),
|
|
342
|
+
w2: botSummary('W2'),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
/** Format trace events as a human-readable string. */
|
|
349
|
+
export function formatTraceEvents(events: TraceEvent[]): string
|
|
350
|
+
{
|
|
351
|
+
return events.map((e) =>
|
|
352
|
+
{
|
|
353
|
+
const t = String(e.tick).padStart(5);
|
|
354
|
+
const a = e.actor.padEnd(2);
|
|
355
|
+
const ty = e.type.padEnd(12);
|
|
356
|
+
return ` T${t} ${a} ${ty} ${e.detail}`;
|
|
357
|
+
}).join('\n');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Format a full trace summary as a human-readable string. */
|
|
361
|
+
export function formatTraceSummary(summary: TraceSummary): string
|
|
362
|
+
{
|
|
363
|
+
const {w1Name, w2Name, w1, w2} = summary;
|
|
364
|
+
function formatBot(name: string, b: TraceBotSummary): string
|
|
365
|
+
{
|
|
366
|
+
const hitPct = b.missilesLaunched > 0 ? ((b.hits / b.missilesLaunched) * 100).toFixed(0) : 'N/A';
|
|
367
|
+
const death = b.causeOfDeath === 'lava' ? ' [LAVA DEATH]'
|
|
368
|
+
: b.causeOfDeath === 'missile' ? ' [KILLED]'
|
|
369
|
+
: b.causeOfDeath === 'timeout' ? '' : '';
|
|
370
|
+
return ` ${name}: ${b.missilesLaunched}m ${b.hits}h (${hitPct}%), ${b.damageDealt.toFixed(1)} dealt, ${b.damageReceived.toFixed(1)} taken, ${b.shields}s ${b.blinks}b${death}`;
|
|
371
|
+
}
|
|
372
|
+
const lines = [
|
|
373
|
+
` Winner: ${summary.winner} @ T${summary.ticks}`,
|
|
374
|
+
` Final HP: ${w1Name}=${summary.w1FinalHp.toFixed(1)}, ${w2Name}=${summary.w2FinalHp.toFixed(1)}`,
|
|
375
|
+
'',
|
|
376
|
+
formatBot(w1Name, w1),
|
|
377
|
+
formatBot(w2Name, w2),
|
|
378
|
+
'',
|
|
379
|
+
` ${w1Name} movement: ${w1.movement.approach} approach, ${w1.movement.strafe} strafe, ${w1.movement.retreat} retreat, ${w1.movement.still} still | ${w1.dodgeEncounters} dodge encounters`,
|
|
380
|
+
];
|
|
381
|
+
return lines.join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Generate diagnostic tips based on trace analysis.
|
|
386
|
+
* Identifies common problems and suggests fixes.
|
|
387
|
+
* Returns an array of human-readable tips (empty if no issues found).
|
|
388
|
+
*/
|
|
389
|
+
export function diagnoseTrace(events: TraceEvent[], summary: TraceSummary): string[]
|
|
390
|
+
{
|
|
391
|
+
const tips: string[] = [];
|
|
392
|
+
const {w1} = summary;
|
|
393
|
+
|
|
394
|
+
// Check for runtime errors
|
|
395
|
+
const errors = events.filter((e) => e.actor === 'W1' && e.type === 'ERROR');
|
|
396
|
+
if (errors.length > 0)
|
|
397
|
+
{
|
|
398
|
+
const firstErr = errors[0]!;
|
|
399
|
+
tips.push(`BOT ERROR: Your bot threw "${firstErr.detail}" on tick ${firstErr.tick}. The bot does nothing when it throws. Fix this first.`);
|
|
400
|
+
if (errors.length > 1) tips.push(` (${errors.length} total errors — likely the same bug repeating every tick)`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check for range warnings
|
|
404
|
+
const rangeWarnings = events.filter((e) => e.actor === 'W1' && e.type === 'WARNING' && e.detail.includes('range'));
|
|
405
|
+
if (rangeWarnings.length > 0)
|
|
406
|
+
{
|
|
407
|
+
tips.push(`RANGE: ${rangeWarnings.length} of your missiles had range shorter than target distance. Increase missile duration or get closer before firing.`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// No missiles fired
|
|
411
|
+
if (w1.missilesLaunched === 0 && errors.length === 0)
|
|
412
|
+
{
|
|
413
|
+
tips.push('NO OFFENSE: Your bot fired 0 missiles. Check that you return missile() when useTicksUntilReady() === 0.');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Very low hit rate
|
|
417
|
+
if (w1.missilesLaunched >= 3 && w1.hits === 0)
|
|
418
|
+
{
|
|
419
|
+
tips.push(`ACCURACY: Fired ${w1.missilesLaunched} missiles but 0 hit. Consider: more homing (higher turnRate), or use getLeadPosition() for straight missiles, or get closer.`);
|
|
420
|
+
}
|
|
421
|
+
else if (w1.missilesLaunched >= 5 && w1.hits / w1.missilesLaunched < 0.25)
|
|
422
|
+
{
|
|
423
|
+
tips.push(`LOW ACCURACY: Only ${((w1.hits / w1.missilesLaunched) * 100).toFixed(0)}% hit rate. Try increasing turnRate for more homing, or use getLeadPosition() for prediction.`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// No shields against incoming damage
|
|
427
|
+
if (w1.damageReceived > 20 && w1.shields === 0)
|
|
428
|
+
{
|
|
429
|
+
tips.push(`NO DEFENSE: Took ${w1.damageReceived.toFixed(0)} damage with 0 shields. Use useThreats() to detect incoming missiles and shield() when you can't dodge.`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Too much idle time
|
|
433
|
+
if (summary.ticks > 500)
|
|
434
|
+
{
|
|
435
|
+
const idleMovements = w1.movement.still;
|
|
436
|
+
const totalMovements = idleMovements + w1.movement.approach + w1.movement.strafe + w1.movement.retreat;
|
|
437
|
+
if (totalMovements > 0 && idleMovements / totalMovements > 0.5)
|
|
438
|
+
{
|
|
439
|
+
tips.push(`IDLE: Your bot was stationary ${((idleMovements / totalMovements) * 100).toFixed(0)}% of the time. Move to dodge missiles and close distance.`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return tips;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Format diagnostic tips as a human-readable string. */
|
|
447
|
+
export function formatDiagnosis(tips: string[]): string
|
|
448
|
+
{
|
|
449
|
+
if (tips.length === 0) return '';
|
|
450
|
+
const lines = [' Diagnosis:'];
|
|
451
|
+
for (const tip of tips)
|
|
452
|
+
{
|
|
453
|
+
lines.push(` - ${tip}`);
|
|
454
|
+
}
|
|
455
|
+
return lines.join('\n');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
function formatDodgeDetail(state: GameState, proj: ProjectileState, missileDistance: number, speed: number): string
|
|
461
|
+
{
|
|
462
|
+
const angle = dodgeAngle(state.velocity, proj.rotation);
|
|
463
|
+
return `missile at ${missileDistance.toFixed(0)}u rot=${proj.rotation.toFixed(0)}° dodge_angle=${angle.toFixed(0)}° ` +
|
|
464
|
+
`spd=${speed.toFixed(2)} ${state.state}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function dodgeAngle(wizardVelocity: {x: number; y: number}, missileRotation: number): number
|
|
468
|
+
{
|
|
469
|
+
const speed = Math.sqrt(wizardVelocity.x ** 2 + wizardVelocity.y ** 2);
|
|
470
|
+
if (speed < 0.01) return 0;
|
|
471
|
+
const missileRad = missileRotation * (Math.PI / 180);
|
|
472
|
+
const dot = Math.cos(missileRad) * (wizardVelocity.x / speed) + Math.sin(missileRad) * (wizardVelocity.y / speed);
|
|
473
|
+
return Math.acos(Math.max(-1, Math.min(1, Math.abs(dot)))) * (180 / Math.PI);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function actorFromId(ev: SimEvent): 'W1' | 'W2'
|
|
477
|
+
{
|
|
478
|
+
if ('wizardId' in ev) return ev.wizardId === 'wizard-1' ? 'W1' : 'W2';
|
|
479
|
+
if ('ownerId' in ev) return ev.ownerId === 'wizard-1' ? 'W1' : 'W2';
|
|
480
|
+
return 'W1';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function classifyMovement(state: GameState, enemy: GameState['enemies'][0], speed: number): string
|
|
484
|
+
{
|
|
485
|
+
if (speed < 0.1) return 'STILL';
|
|
486
|
+
const toEnemyAngle = Math.atan2(enemy.position.y - state.position.y, enemy.position.x - state.position.x);
|
|
487
|
+
const moveAngle = Math.atan2(state.velocity.y, state.velocity.x);
|
|
488
|
+
let angleDiff = ((moveAngle - toEnemyAngle) * 180 / Math.PI) % 360;
|
|
489
|
+
if (angleDiff > 180) angleDiff -= 360;
|
|
490
|
+
if (angleDiff < -180) angleDiff += 360;
|
|
491
|
+
const absAngle = Math.abs(angleDiff);
|
|
492
|
+
if (absAngle < 30) return 'APPROACH';
|
|
493
|
+
if (absAngle > 150) return 'RETREAT';
|
|
494
|
+
if (absAngle >= 60 && absAngle <= 120) return 'STRAFE';
|
|
495
|
+
if (absAngle < 60) return 'APPROACH+STRAFE';
|
|
496
|
+
return 'RETREAT+STRAFE';
|
|
497
|
+
}
|