@vibemancer/core 0.1.0
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/README.md +28 -0
- package/dist/chunk-L7Z7OFXD.js +9140 -0
- package/dist/chunk-L7Z7OFXD.js.map +1 -0
- package/dist/index-browser.d.ts +2602 -0
- package/dist/index-browser.js +407 -0
- package/dist/index-browser.js.map +1 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +750 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
- package/src/bots/berserker/01_Stormchaser.ts +457 -0
- package/src/bots/berserker/02_Stormcaller.ts +417 -0
- package/src/bots/berserker/03_Stormforger.ts +481 -0
- package/src/bots/caster/01_Flamecaller.ts +286 -0
- package/src/bots/caster/02_Pyromancer.ts +350 -0
- package/src/bots/caster/03_Infernalist.ts +492 -0
- package/src/bots/defensive/01_Turtle.ts +151 -0
- package/src/bots/defensive/02_Sentinel.ts +134 -0
- package/src/bots/defensive/03_Golem.ts +357 -0
- package/src/bots/duelist/01_Battlemage.ts +433 -0
- package/src/bots/duelist/02_Warmage.ts +438 -0
- package/src/bots/duelist/03_Archmage.ts +588 -0
- package/src/bots/homing/01_Bonemancer.ts +67 -0
- package/src/bots/homing/02_Lich.ts +356 -0
- package/src/bots/homing/03_Archlich.ts +220 -0
- package/src/bots/index.ts +30 -0
- package/src/bots/kiter/01_Spellspinner.ts +398 -0
- package/src/bots/kiter/02_Spellweaver.ts +378 -0
- package/src/bots/kiter/03_Spellbinder.ts +448 -0
- package/src/bots/melee/01_Shadowblade.ts +270 -0
- package/src/bots/melee/02_Nightblade.ts +437 -0
- package/src/bots/melee/03_Voidblade.ts +582 -0
- package/src/bots/registry.ts +207 -0
- package/src/bots/shared.ts +472 -0
- package/src/bots/sniper/01_Spellshot.ts +385 -0
- package/src/bots/sniper/02_Spelltracer.ts +441 -0
- package/src/bots/sniper/03_Spellseeker.ts +546 -0
- package/src/bots/standalone/Critter.ts +89 -0
- package/src/bots/standalone/Doombringer.ts +91 -0
- package/src/bots/standalone/Hogger.ts +228 -0
- package/src/bots/standalone/Rookie.ts +50 -0
- package/src/bots/standalone/TargetDummy.ts +21 -0
- package/src/bots/test/cheater.ts +405 -0
- package/src/bots/test/crasher.ts +81 -0
- package/src/engine/hooks-runtime.ts +394 -0
- package/src/engine/manual-match.ts +289 -0
- package/src/engine/missile-templates.ts +155 -0
- package/src/engine/optimizer.ts +220 -0
- package/src/engine/params-runtime.ts +189 -0
- package/src/engine/physics.ts +143 -0
- package/src/engine/sandbox-browser.ts +671 -0
- package/src/engine/sandbox-compile.ts +197 -0
- package/src/engine/sandbox-harness.ts +367 -0
- package/src/engine/sandbox.ts +332 -0
- package/src/engine/simulation.ts +828 -0
- package/src/engine/spells.ts +128 -0
- package/src/engine-version.ts +11 -0
- package/src/hooks/action-builders.ts +210 -0
- package/src/hooks/bot-wrapper.ts +84 -0
- package/src/hooks/index.ts +75 -0
- package/src/hooks/state-hooks.ts +354 -0
- package/src/hooks/threat-analysis.ts +365 -0
- package/src/hooks/types.ts +142 -0
- package/src/index-browser.ts +30 -0
- package/src/index.ts +24 -0
- package/src/rules.ts +254 -0
- package/src/stats.ts +262 -0
- package/src/testing.ts +207 -0
- package/src/trace.ts +430 -0
- package/src/types.ts +193 -0
- package/src/utils/angles.ts +47 -0
- package/src/utils/combat.ts +279 -0
- package/src/utils/distance.ts +21 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/movement.ts +108 -0
- package/src/utils/random.ts +65 -0
- package/src/utils/spatial.ts +63 -0
- package/src/utils/targeting.ts +45 -0
package/src/trace.ts
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
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} 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
|
+
| 'DODGE_START' // Enemy missile enters 150u proximity
|
|
35
|
+
| 'DODGE_CLOSE' // Enemy missile enters 50u proximity
|
|
36
|
+
| 'MOVE' // Movement summary (every 100 ticks)
|
|
37
|
+
| 'ERROR' // Bot or missile AI threw a runtime error
|
|
38
|
+
| 'WARNING'; // Common mistake detected (move/blink confusion, etc.)
|
|
39
|
+
|
|
40
|
+
export interface TraceSummary
|
|
41
|
+
{
|
|
42
|
+
winner: string;
|
|
43
|
+
ticks: number;
|
|
44
|
+
w1Name: string;
|
|
45
|
+
w2Name: string;
|
|
46
|
+
w1FinalHp: number;
|
|
47
|
+
w2FinalHp: number;
|
|
48
|
+
w1: TraceBotSummary;
|
|
49
|
+
w2: TraceBotSummary;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TraceBotSummary
|
|
53
|
+
{
|
|
54
|
+
missilesLaunched: number;
|
|
55
|
+
hits: number;
|
|
56
|
+
damageDealt: number;
|
|
57
|
+
damageReceived: number;
|
|
58
|
+
shields: number;
|
|
59
|
+
blinks: number;
|
|
60
|
+
dodgeEncounters: number;
|
|
61
|
+
movement: {strafe: number; approach: number; retreat: number; still: number};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Event Extraction ────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract a structured event log from simulation history.
|
|
68
|
+
* Tracks both bots' state changes, missile launches (with full config + range),
|
|
69
|
+
* hits, damage, dodge proximity, movement patterns, and runtime errors.
|
|
70
|
+
*/
|
|
71
|
+
export function extractTraceEvents(history: GameState[], errors?: BotError[]): TraceEvent[]
|
|
72
|
+
{
|
|
73
|
+
const events: TraceEvent[] = [];
|
|
74
|
+
|
|
75
|
+
let prevW1State = '';
|
|
76
|
+
let prevW2State = '';
|
|
77
|
+
let prevW1Health = history[0]?.health ?? 60;
|
|
78
|
+
let prevW2Health = history[0]?.enemies[0]?.health ?? 60;
|
|
79
|
+
let prevW1Spell = '';
|
|
80
|
+
let prevW2Spell = '';
|
|
81
|
+
const prevProjectileIds = new Set<string>();
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < history.length; i++)
|
|
84
|
+
{
|
|
85
|
+
const state = history[i]!;
|
|
86
|
+
const tick = state.tick;
|
|
87
|
+
const enemy = state.enemies[0]!;
|
|
88
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
89
|
+
|
|
90
|
+
// --- W1 state transitions ---
|
|
91
|
+
const w1State = state.state;
|
|
92
|
+
const w1Spell = state.castingSpell ?? state.channelingSpell ?? '';
|
|
93
|
+
if (w1State !== prevW1State || w1Spell !== prevW1Spell)
|
|
94
|
+
{
|
|
95
|
+
let detail = `${prevW1State || '?'}→${w1State}`;
|
|
96
|
+
if (w1Spell) detail += ` (${w1Spell})`;
|
|
97
|
+
if (w1State === 'casting' && state.castDuration) detail += ` [${state.castDuration}t]`;
|
|
98
|
+
detail += ` dist=${distance.toFixed(0)}`;
|
|
99
|
+
events.push({tick, actor: 'W1', type: 'STATE', detail});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- W2 state transitions ---
|
|
103
|
+
const w2State = enemy.state;
|
|
104
|
+
const w2Spell = enemy.castingSpell ?? enemy.channelingSpell ?? '';
|
|
105
|
+
if (w2State !== prevW2State || w2Spell !== prevW2Spell)
|
|
106
|
+
{
|
|
107
|
+
let detail = `${prevW2State || '?'}→${w2State}`;
|
|
108
|
+
if (w2Spell) detail += ` (${w2Spell})`;
|
|
109
|
+
if (w2State === 'casting' && enemy.castDuration) detail += ` [${enemy.castDuration}t]`;
|
|
110
|
+
detail += ` dist=${distance.toFixed(0)}`;
|
|
111
|
+
events.push({tick, actor: 'W2', type: 'STATE', detail});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Damage events ---
|
|
115
|
+
if (enemy.health < prevW2Health)
|
|
116
|
+
{
|
|
117
|
+
const dmg = prevW2Health - enemy.health;
|
|
118
|
+
events.push({
|
|
119
|
+
tick, actor: 'W1', type: 'HIT',
|
|
120
|
+
detail: `dealt ${dmg.toFixed(1)} → enemy HP=${enemy.health.toFixed(1)} dist=${distance.toFixed(0)}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (state.health < prevW1Health)
|
|
124
|
+
{
|
|
125
|
+
const dmg = prevW1Health - state.health;
|
|
126
|
+
const stateTag = state.state === 'channeling' ? ' (SHIELDED)'
|
|
127
|
+
: state.state === 'casting' ? ` (CASTING ${state.castingSpell})`
|
|
128
|
+
: state.state === 'gcd_locked' ? ' (GCD)'
|
|
129
|
+
: ` (${state.state})`;
|
|
130
|
+
events.push({
|
|
131
|
+
tick, actor: 'W1', type: 'HURT',
|
|
132
|
+
detail: `took ${dmg.toFixed(1)} → HP=${state.health.toFixed(1)}${stateTag} dist=${distance.toFixed(0)}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// W2 damage taken = W1 damage dealt (already covered above as HIT)
|
|
136
|
+
// W2 damage dealt = W1 damage taken (already covered above as HURT)
|
|
137
|
+
|
|
138
|
+
// --- Missile launches (with full config) ---
|
|
139
|
+
const currentIds = new Set<string>();
|
|
140
|
+
for (const proj of state.projectiles)
|
|
141
|
+
{
|
|
142
|
+
currentIds.add(proj.id);
|
|
143
|
+
if (!prevProjectileIds.has(proj.id))
|
|
144
|
+
{
|
|
145
|
+
const owner: 'W1' | 'W2' = proj.ownerId === 'wizard-1' ? 'W1' : 'W2';
|
|
146
|
+
const range = (proj.speed * proj.remainingTicks).toFixed(0);
|
|
147
|
+
events.push({
|
|
148
|
+
tick, actor: owner, type: 'FIRE',
|
|
149
|
+
detail: `dmg=${proj.damage} spd=${proj.speed} turn=${proj.turnRate} life=${proj.remainingTicks} range=${range} dist=${distance.toFixed(0)}`,
|
|
150
|
+
});
|
|
151
|
+
// Warn if missile range < distance (will expire before reaching target)
|
|
152
|
+
if (proj.speed * proj.remainingTicks < distance * 0.8 && proj.turnRate >= 0)
|
|
153
|
+
{
|
|
154
|
+
events.push({
|
|
155
|
+
tick, actor: owner, type: 'WARNING',
|
|
156
|
+
detail: `missile range (${range}) may be too short for distance (${distance.toFixed(0)}) — consider increasing duration`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
prevProjectileIds.clear();
|
|
162
|
+
for (const id of currentIds) prevProjectileIds.add(id);
|
|
163
|
+
|
|
164
|
+
// --- Dodge proximity events (enemy missiles approaching W1) ---
|
|
165
|
+
if (i > 0)
|
|
166
|
+
{
|
|
167
|
+
const speed = Math.sqrt(state.velocity.x ** 2 + state.velocity.y ** 2);
|
|
168
|
+
for (const proj of state.projectiles)
|
|
169
|
+
{
|
|
170
|
+
if (proj.ownerId === 'wizard-1') continue;
|
|
171
|
+
const missileDistance = distanceTo(state.position, proj.position);
|
|
172
|
+
const prevProj = history[i - 1]!.projectiles.find((p) => p.id === proj.id);
|
|
173
|
+
const prevMissileDist = prevProj ? distanceTo(history[i - 1]!.position, prevProj.position) : Infinity;
|
|
174
|
+
|
|
175
|
+
if (missileDistance <= 150 && prevMissileDist > 150)
|
|
176
|
+
{
|
|
177
|
+
events.push({
|
|
178
|
+
tick, actor: 'W1', type: 'DODGE_START',
|
|
179
|
+
detail: formatDodgeDetail(state, proj, missileDistance, speed),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (missileDistance <= 50 && prevMissileDist > 50)
|
|
183
|
+
{
|
|
184
|
+
events.push({
|
|
185
|
+
tick, actor: 'W1', type: 'DODGE_CLOSE',
|
|
186
|
+
detail: formatDodgeDetail(state, proj, missileDistance, speed),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Movement summary every 100 ticks ---
|
|
193
|
+
if (tick > 0 && tick % 100 === 0)
|
|
194
|
+
{
|
|
195
|
+
const speed = Math.sqrt(state.velocity.x ** 2 + state.velocity.y ** 2);
|
|
196
|
+
const moveType = classifyMovement(state, enemy, speed);
|
|
197
|
+
const incomingMissiles = state.projectiles.filter((p) => p.ownerId !== 'wizard-1').length;
|
|
198
|
+
events.push({
|
|
199
|
+
tick, actor: 'W1', type: 'MOVE',
|
|
200
|
+
detail: `${moveType} spd=${speed.toFixed(2)} dist=${distance.toFixed(0)} missiles=${incomingMissiles} ${state.state} hp=${state.health.toFixed(1)}`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
prevW1State = w1State;
|
|
205
|
+
prevW2State = w2State;
|
|
206
|
+
prevW1Health = state.health;
|
|
207
|
+
prevW2Health = enemy.health;
|
|
208
|
+
prevW1Spell = w1Spell;
|
|
209
|
+
prevW2Spell = w2Spell;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- Inject runtime errors as ERROR events ---
|
|
213
|
+
if (errors && errors.length > 0)
|
|
214
|
+
{
|
|
215
|
+
for (const err of errors)
|
|
216
|
+
{
|
|
217
|
+
const actor: 'W1' | 'W2' = err.entityId === 'wizard-1' ? 'W1'
|
|
218
|
+
: err.entityId === 'wizard-2' ? 'W2'
|
|
219
|
+
: err.entityId.startsWith('missile') ? (/* missile errors attributed to owner */ 'W1') : 'W1';
|
|
220
|
+
events.push({
|
|
221
|
+
tick: err.tick,
|
|
222
|
+
actor,
|
|
223
|
+
type: 'ERROR',
|
|
224
|
+
detail: err.message,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// Re-sort by tick so errors appear in chronological order
|
|
228
|
+
events.sort((a, b) => a.tick - b.tick || (a.type === 'ERROR' ? 1 : 0));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return events;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─── Summary ─────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Generate a summary from trace events and the simulation result.
|
|
238
|
+
*/
|
|
239
|
+
export function summarizeTrace(
|
|
240
|
+
events: TraceEvent[],
|
|
241
|
+
result: SimulateResult,
|
|
242
|
+
w1Name: string,
|
|
243
|
+
w2Name: string,
|
|
244
|
+
): TraceSummary
|
|
245
|
+
{
|
|
246
|
+
const final = result.history[result.history.length - 1];
|
|
247
|
+
|
|
248
|
+
function botSummary(actor: 'W1' | 'W2'): TraceBotSummary
|
|
249
|
+
{
|
|
250
|
+
const myEvents = events.filter((e) => e.actor === actor);
|
|
251
|
+
const moveEvents = myEvents.filter((e) => e.type === 'MOVE');
|
|
252
|
+
return {
|
|
253
|
+
missilesLaunched: myEvents.filter((e) => e.type === 'FIRE').length,
|
|
254
|
+
hits: myEvents.filter((e) => e.type === 'HIT').length,
|
|
255
|
+
damageDealt: myEvents.filter((e) => e.type === 'HIT').reduce((sum, e) =>
|
|
256
|
+
{
|
|
257
|
+
const m = e.detail.match(/dealt ([\d.]+)/);
|
|
258
|
+
return sum + (m ? parseFloat(m[1]!) : 0);
|
|
259
|
+
}, 0),
|
|
260
|
+
damageReceived: myEvents.filter((e) => e.type === 'HURT').reduce((sum, e) =>
|
|
261
|
+
{
|
|
262
|
+
const m = e.detail.match(/took ([\d.]+)/);
|
|
263
|
+
return sum + (m ? parseFloat(m[1]!) : 0);
|
|
264
|
+
}, 0),
|
|
265
|
+
shields: myEvents.filter((e) => e.type === 'STATE' && e.detail.includes('shield')).length,
|
|
266
|
+
blinks: myEvents.filter((e) => e.type === 'STATE' && e.detail.includes('blink')).length,
|
|
267
|
+
dodgeEncounters: myEvents.filter((e) => e.type.startsWith('DODGE')).length,
|
|
268
|
+
movement: {
|
|
269
|
+
strafe: moveEvents.filter((e) => e.detail.startsWith('STRAFE')).length,
|
|
270
|
+
approach: moveEvents.filter((e) => e.detail.startsWith('APPROACH')).length,
|
|
271
|
+
retreat: moveEvents.filter((e) => e.detail.startsWith('RETREAT')).length,
|
|
272
|
+
still: moveEvents.filter((e) => e.detail.startsWith('STILL')).length,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const winnerName = result.winner === 'wizard-1' ? w1Name
|
|
278
|
+
: result.winner === 'wizard-2' ? w2Name
|
|
279
|
+
: result.winner === 'draw' ? 'draw'
|
|
280
|
+
: 'timeout';
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
winner: winnerName,
|
|
284
|
+
ticks: result.ticks,
|
|
285
|
+
w1Name,
|
|
286
|
+
w2Name,
|
|
287
|
+
w1FinalHp: final?.health ?? 0,
|
|
288
|
+
w2FinalHp: final?.enemies[0]?.health ?? 0,
|
|
289
|
+
w1: botSummary('W1'),
|
|
290
|
+
w2: botSummary('W2'),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
/** Format trace events as a human-readable string. */
|
|
297
|
+
export function formatTraceEvents(events: TraceEvent[]): string
|
|
298
|
+
{
|
|
299
|
+
return events.map((e) =>
|
|
300
|
+
{
|
|
301
|
+
const t = String(e.tick).padStart(5);
|
|
302
|
+
const a = e.actor.padEnd(2);
|
|
303
|
+
const ty = e.type.padEnd(12);
|
|
304
|
+
return ` T${t} ${a} ${ty} ${e.detail}`;
|
|
305
|
+
}).join('\n');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Format a full trace summary as a human-readable string. */
|
|
309
|
+
export function formatTraceSummary(summary: TraceSummary): string
|
|
310
|
+
{
|
|
311
|
+
const {w1Name, w2Name, w1, w2} = summary;
|
|
312
|
+
const lines = [
|
|
313
|
+
` Winner: ${summary.winner} @ T${summary.ticks}`,
|
|
314
|
+
` Final HP: ${w1Name}=${summary.w1FinalHp.toFixed(1)}, ${w2Name}=${summary.w2FinalHp.toFixed(1)}`,
|
|
315
|
+
'',
|
|
316
|
+
` ${w1Name}: ${w1.missilesLaunched} missiles, ${w1.hits} hits (${w1.missilesLaunched > 0 ? ((w1.hits / w1.missilesLaunched) * 100).toFixed(0) : 'N/A'}%), ${w1.damageDealt.toFixed(1)} dmg dealt, ${w1.damageReceived.toFixed(1)} dmg taken, ${w1.shields} shields, ${w1.blinks} blinks`,
|
|
317
|
+
` ${w2Name}: ${w2.missilesLaunched} missiles, ${w2.hits} hits (${w2.missilesLaunched > 0 ? ((w2.hits / w2.missilesLaunched) * 100).toFixed(0) : 'N/A'}%), ${w2.damageDealt.toFixed(1)} dmg dealt, ${w2.damageReceived.toFixed(1)} dmg taken, ${w2.shields} shields, ${w2.blinks} blinks`,
|
|
318
|
+
'',
|
|
319
|
+
` ${w1Name} movement: ${w1.movement.approach} approach, ${w1.movement.strafe} strafe, ${w1.movement.retreat} retreat, ${w1.movement.still} still | ${w1.dodgeEncounters} dodge encounters`,
|
|
320
|
+
];
|
|
321
|
+
return lines.join('\n');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Generate diagnostic tips based on trace analysis.
|
|
326
|
+
* Identifies common problems and suggests fixes.
|
|
327
|
+
* Returns an array of human-readable tips (empty if no issues found).
|
|
328
|
+
*/
|
|
329
|
+
export function diagnoseTrace(events: TraceEvent[], summary: TraceSummary): string[]
|
|
330
|
+
{
|
|
331
|
+
const tips: string[] = [];
|
|
332
|
+
const {w1} = summary;
|
|
333
|
+
|
|
334
|
+
// Check for runtime errors
|
|
335
|
+
const errors = events.filter((e) => e.actor === 'W1' && e.type === 'ERROR');
|
|
336
|
+
if (errors.length > 0)
|
|
337
|
+
{
|
|
338
|
+
const firstErr = errors[0]!;
|
|
339
|
+
tips.push(`BOT ERROR: Your bot threw "${firstErr.detail}" on tick ${firstErr.tick}. The bot does nothing when it throws. Fix this first.`);
|
|
340
|
+
if (errors.length > 1) tips.push(` (${errors.length} total errors — likely the same bug repeating every tick)`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for range warnings
|
|
344
|
+
const rangeWarnings = events.filter((e) => e.actor === 'W1' && e.type === 'WARNING' && e.detail.includes('range'));
|
|
345
|
+
if (rangeWarnings.length > 0)
|
|
346
|
+
{
|
|
347
|
+
tips.push(`RANGE: ${rangeWarnings.length} of your missiles had range shorter than target distance. Increase missile duration or get closer before firing.`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// No missiles fired
|
|
351
|
+
if (w1.missilesLaunched === 0 && errors.length === 0)
|
|
352
|
+
{
|
|
353
|
+
tips.push('NO OFFENSE: Your bot fired 0 missiles. Check that you return missile() when useTicksUntilReady() === 0.');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Very low hit rate
|
|
357
|
+
if (w1.missilesLaunched >= 3 && w1.hits === 0)
|
|
358
|
+
{
|
|
359
|
+
tips.push(`ACCURACY: Fired ${w1.missilesLaunched} missiles but 0 hit. Consider: more homing (higher turnRate), or use getLeadPosition() for straight missiles, or get closer.`);
|
|
360
|
+
}
|
|
361
|
+
else if (w1.missilesLaunched >= 5 && w1.hits / w1.missilesLaunched < 0.25)
|
|
362
|
+
{
|
|
363
|
+
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.`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// No shields against incoming damage
|
|
367
|
+
if (w1.damageReceived > 20 && w1.shields === 0)
|
|
368
|
+
{
|
|
369
|
+
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.`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Too much idle time
|
|
373
|
+
if (summary.ticks > 500)
|
|
374
|
+
{
|
|
375
|
+
const idleMovements = w1.movement.still;
|
|
376
|
+
const totalMovements = idleMovements + w1.movement.approach + w1.movement.strafe + w1.movement.retreat;
|
|
377
|
+
if (totalMovements > 0 && idleMovements / totalMovements > 0.5)
|
|
378
|
+
{
|
|
379
|
+
tips.push(`IDLE: Your bot was stationary ${((idleMovements / totalMovements) * 100).toFixed(0)}% of the time. Move to dodge missiles and close distance.`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return tips;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Format diagnostic tips as a human-readable string. */
|
|
387
|
+
export function formatDiagnosis(tips: string[]): string
|
|
388
|
+
{
|
|
389
|
+
if (tips.length === 0) return '';
|
|
390
|
+
const lines = [' Diagnosis:'];
|
|
391
|
+
for (const tip of tips)
|
|
392
|
+
{
|
|
393
|
+
lines.push(` - ${tip}`);
|
|
394
|
+
}
|
|
395
|
+
return lines.join('\n');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
function formatDodgeDetail(state: GameState, proj: ProjectileState, missileDistance: number, speed: number): string
|
|
401
|
+
{
|
|
402
|
+
const angle = dodgeAngle(state.velocity, proj.rotation);
|
|
403
|
+
return `missile at ${missileDistance.toFixed(0)}u rot=${proj.rotation.toFixed(0)}° dodge_angle=${angle.toFixed(0)}° ` +
|
|
404
|
+
`spd=${speed.toFixed(2)} ${state.state}`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function dodgeAngle(wizardVelocity: {x: number; y: number}, missileRotation: number): number
|
|
408
|
+
{
|
|
409
|
+
const speed = Math.sqrt(wizardVelocity.x ** 2 + wizardVelocity.y ** 2);
|
|
410
|
+
if (speed < 0.01) return 0;
|
|
411
|
+
const missileRad = missileRotation * (Math.PI / 180);
|
|
412
|
+
const dot = Math.cos(missileRad) * (wizardVelocity.x / speed) + Math.sin(missileRad) * (wizardVelocity.y / speed);
|
|
413
|
+
return Math.acos(Math.max(-1, Math.min(1, Math.abs(dot)))) * (180 / Math.PI);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function classifyMovement(state: GameState, enemy: GameState['enemies'][0], speed: number): string
|
|
417
|
+
{
|
|
418
|
+
if (speed < 0.1) return 'STILL';
|
|
419
|
+
const toEnemyAngle = Math.atan2(enemy.position.y - state.position.y, enemy.position.x - state.position.x);
|
|
420
|
+
const moveAngle = Math.atan2(state.velocity.y, state.velocity.x);
|
|
421
|
+
let angleDiff = ((moveAngle - toEnemyAngle) * 180 / Math.PI) % 360;
|
|
422
|
+
if (angleDiff > 180) angleDiff -= 360;
|
|
423
|
+
if (angleDiff < -180) angleDiff += 360;
|
|
424
|
+
const absAngle = Math.abs(angleDiff);
|
|
425
|
+
if (absAngle < 30) return 'APPROACH';
|
|
426
|
+
if (absAngle > 150) return 'RETREAT';
|
|
427
|
+
if (absAngle >= 60 && absAngle <= 120) return 'STRAFE';
|
|
428
|
+
if (absAngle < 60) return 'APPROACH+STRAFE';
|
|
429
|
+
return 'RETREAT+STRAFE';
|
|
430
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VIBEMANCER - TYPES
|
|
3
|
+
*
|
|
4
|
+
* This file contains all the TypeScript interfaces used by the game engine.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Position in 2D space.
|
|
9
|
+
*/
|
|
10
|
+
export interface Position
|
|
11
|
+
{
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Velocity in 2D space (units per tick).
|
|
18
|
+
*/
|
|
19
|
+
export interface Velocity
|
|
20
|
+
{
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wizard state.
|
|
27
|
+
*/
|
|
28
|
+
export interface WizardState
|
|
29
|
+
{
|
|
30
|
+
id: string;
|
|
31
|
+
position: Position;
|
|
32
|
+
rotation: number; // degrees
|
|
33
|
+
health: number;
|
|
34
|
+
maxHealth: number;
|
|
35
|
+
state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
|
|
36
|
+
castingSpell?: 'missile' | 'shield' | 'blink';
|
|
37
|
+
castProgress?: number; // ticks elapsed
|
|
38
|
+
castDuration?: number; // total ticks needed
|
|
39
|
+
channelingSpell?: 'shield';
|
|
40
|
+
channelDuration?: number; // ticks spent channeling (for decay calc)
|
|
41
|
+
gcdRemaining?: number; // ticks remaining
|
|
42
|
+
blinkCooldown: number; // ticks remaining
|
|
43
|
+
velocity: Velocity;
|
|
44
|
+
lastMissileConfig?: MissileConfig; // last fired missile (for warmup system)
|
|
45
|
+
warmupMultiplier?: number; // warmup multiplier applied to most recent missile cast (0.80 = full bonus, 1.20 = full penalty)
|
|
46
|
+
invincible?: boolean; // manual play mode: damage is ignored when true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Projectile (missile) state.
|
|
51
|
+
*/
|
|
52
|
+
export interface ProjectileState
|
|
53
|
+
{
|
|
54
|
+
id: string;
|
|
55
|
+
type: 'missile';
|
|
56
|
+
ownerId: string;
|
|
57
|
+
position: Position;
|
|
58
|
+
rotation: number; // degrees
|
|
59
|
+
speed: number; // units per tick
|
|
60
|
+
turnRate: number; // degrees per tick
|
|
61
|
+
damage: number;
|
|
62
|
+
remainingTicks: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Game configuration (read-only).
|
|
67
|
+
*/
|
|
68
|
+
export interface GameConfig
|
|
69
|
+
{
|
|
70
|
+
arenaSize: {
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
};
|
|
74
|
+
tickRate: number;
|
|
75
|
+
maxTicks: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Full game state passed to wizard.
|
|
80
|
+
*/
|
|
81
|
+
export interface GameState
|
|
82
|
+
{
|
|
83
|
+
tick: number;
|
|
84
|
+
position: Position;
|
|
85
|
+
rotation: number;
|
|
86
|
+
health: number;
|
|
87
|
+
maxHealth: number;
|
|
88
|
+
state: WizardState['state'];
|
|
89
|
+
castingSpell?: 'missile' | 'shield' | 'blink';
|
|
90
|
+
castProgress?: number;
|
|
91
|
+
castDuration?: number;
|
|
92
|
+
channelingSpell?: 'shield';
|
|
93
|
+
channelDuration?: number;
|
|
94
|
+
gcdRemaining?: number;
|
|
95
|
+
blinkCooldown: number;
|
|
96
|
+
velocity: Velocity;
|
|
97
|
+
lastMissileConfig?: MissileConfig; // last fired missile (for warmup system)
|
|
98
|
+
warmupMultiplier?: number; // warmup multiplier applied to most recent missile cast (0.80 = full bonus, 1.20 = full penalty)
|
|
99
|
+
enemies: WizardState[];
|
|
100
|
+
projectiles: ProjectileState[]; // all projectiles (yours and enemy's)
|
|
101
|
+
myProjectiles: ProjectileState[]; // only your active projectiles
|
|
102
|
+
|
|
103
|
+
// Combat tracking (for new hooks API)
|
|
104
|
+
damageDealt: number; // total damage dealt this match
|
|
105
|
+
damageTaken: number; // total damage taken this match
|
|
106
|
+
lastHitTick: number; // tick when last took damage (0 if never)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Actions returned by wizard each tick.
|
|
111
|
+
*
|
|
112
|
+
* Movement uses world-space coordinates:
|
|
113
|
+
* - x: +100 = right, -100 = left
|
|
114
|
+
* - y: +100 = down, -100 = up
|
|
115
|
+
* - Diagonal movement is normalized (magnitude capped at 100)
|
|
116
|
+
* - No rotation tracking - just output (x, y) direction
|
|
117
|
+
*/
|
|
118
|
+
export interface WizardActions
|
|
119
|
+
{
|
|
120
|
+
/**
|
|
121
|
+
* Movement direction in world-space.
|
|
122
|
+
* Values are clamped to [-100, 100] range.
|
|
123
|
+
* Magnitude is normalized to max 100 for diagonal movement.
|
|
124
|
+
*/
|
|
125
|
+
move: {
|
|
126
|
+
x: number;
|
|
127
|
+
y: number;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Start a new cast (only works if state === 'idle').
|
|
132
|
+
*/
|
|
133
|
+
startCast?: {
|
|
134
|
+
spell: 'missile';
|
|
135
|
+
config: MissileConfig;
|
|
136
|
+
missileAI: MissileAIFunction;
|
|
137
|
+
direction?: number; // initial rotation override (auto-aims at enemy by default)
|
|
138
|
+
} | {
|
|
139
|
+
spell: 'shield';
|
|
140
|
+
} | {
|
|
141
|
+
spell: 'blink';
|
|
142
|
+
target: Position; // clamped to max range
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Cancel current cast or channel (works if casting or channeling).
|
|
147
|
+
*/
|
|
148
|
+
cancel?: boolean;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Update aim direction while casting a missile (degrees).
|
|
152
|
+
* The missile fires in this direction at launch, allowing tracking during cast.
|
|
153
|
+
* Only applies while state === 'casting' and castingSpell === 'missile'.
|
|
154
|
+
*/
|
|
155
|
+
aimDirection?: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Missile configuration.
|
|
160
|
+
*/
|
|
161
|
+
export interface MissileConfig
|
|
162
|
+
{
|
|
163
|
+
damage: number;
|
|
164
|
+
speed: number; // units per tick
|
|
165
|
+
turnRate: number; // degrees per tick
|
|
166
|
+
duration: number; // ticks (determines range and cast time)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Missile AI function type.
|
|
171
|
+
*/
|
|
172
|
+
export type MissileAIFunction = (props: {
|
|
173
|
+
missileState: ProjectileState;
|
|
174
|
+
worldState: GameState;
|
|
175
|
+
random: () => number; // Seeded PRNG, isolated per entity, 0-1 range
|
|
176
|
+
}) => MissileActions;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Actions returned by missile each tick.
|
|
180
|
+
*/
|
|
181
|
+
export interface MissileActions
|
|
182
|
+
{
|
|
183
|
+
turnToward?: Position; // engine handles turn rate limit
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Main wizard function type.
|
|
188
|
+
*/
|
|
189
|
+
export type WizardFunction = (props: {
|
|
190
|
+
state: GameState;
|
|
191
|
+
config: GameConfig;
|
|
192
|
+
random: () => number; // Seeded PRNG, isolated per entity, 0-1 range
|
|
193
|
+
}) => WizardActions;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {Position} from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculate the angle from one point to another in degrees.
|
|
5
|
+
* 0° = right, 90° = down, 180° = left, 270° = up.
|
|
6
|
+
*/
|
|
7
|
+
export function angleTo(from: Position, to: Position): number
|
|
8
|
+
{
|
|
9
|
+
const dx = to.x - from.x;
|
|
10
|
+
const dy = to.y - from.y;
|
|
11
|
+
const radians = Math.atan2(dy, dx);
|
|
12
|
+
return normalizeAngle(radians * (180 / Math.PI));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalize an angle to the 0-360 range.
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeAngle(angle: number): number
|
|
19
|
+
{
|
|
20
|
+
let normalized = angle % 360;
|
|
21
|
+
if (normalized < 0)
|
|
22
|
+
{
|
|
23
|
+
normalized += 360;
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Calculate the shortest difference between two angles (-180 to +180).
|
|
30
|
+
*/
|
|
31
|
+
export function angleDiff(angleA: number, angleB: number): number
|
|
32
|
+
{
|
|
33
|
+
let diff = (angleB - angleA + 180) % 360 - 180;
|
|
34
|
+
if (diff < -180)
|
|
35
|
+
{
|
|
36
|
+
diff += 360;
|
|
37
|
+
}
|
|
38
|
+
return diff;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if an angle is within a certain range of a target angle.
|
|
43
|
+
*/
|
|
44
|
+
export function angleInRange(angle: number, target: number, range: number): boolean
|
|
45
|
+
{
|
|
46
|
+
return Math.abs(angleDiff(angle, target)) <= range;
|
|
47
|
+
}
|