@vibemancer/core 0.1.0 → 0.1.2

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 (50) hide show
  1. package/README.md +28 -28
  2. package/dist/{chunk-L7Z7OFXD.js → chunk-OL7ETV6V.js} +3 -3
  3. package/dist/chunk-OL7ETV6V.js.map +1 -0
  4. package/dist/index-browser.d.ts +1 -1
  5. package/dist/index-browser.js +1 -1
  6. package/dist/index.js +1 -1
  7. package/package.json +79 -78
  8. package/src/bots/berserker/01_Stormchaser.ts +457 -457
  9. package/src/bots/berserker/02_Stormcaller.ts +417 -417
  10. package/src/bots/berserker/03_Stormforger.ts +481 -481
  11. package/src/bots/caster/01_Flamecaller.ts +286 -286
  12. package/src/bots/caster/02_Pyromancer.ts +350 -350
  13. package/src/bots/caster/03_Infernalist.ts +492 -492
  14. package/src/bots/defensive/01_Turtle.ts +151 -151
  15. package/src/bots/defensive/02_Sentinel.ts +134 -134
  16. package/src/bots/defensive/03_Golem.ts +357 -357
  17. package/src/bots/duelist/01_Battlemage.ts +433 -433
  18. package/src/bots/duelist/02_Warmage.ts +438 -438
  19. package/src/bots/duelist/03_Archmage.ts +588 -588
  20. package/src/bots/homing/01_Bonemancer.ts +67 -67
  21. package/src/bots/homing/02_Lich.ts +356 -356
  22. package/src/bots/homing/03_Archlich.ts +220 -220
  23. package/src/bots/kiter/01_Spellspinner.ts +398 -398
  24. package/src/bots/kiter/02_Spellweaver.ts +378 -378
  25. package/src/bots/kiter/03_Spellbinder.ts +448 -448
  26. package/src/bots/melee/01_Shadowblade.ts +270 -270
  27. package/src/bots/melee/02_Nightblade.ts +437 -437
  28. package/src/bots/melee/03_Voidblade.ts +582 -582
  29. package/src/bots/sniper/01_Spellshot.ts +385 -385
  30. package/src/bots/sniper/02_Spelltracer.ts +441 -441
  31. package/src/bots/sniper/03_Spellseeker.ts +546 -546
  32. package/src/bots/standalone/Critter.ts +89 -89
  33. package/src/bots/standalone/Doombringer.ts +91 -91
  34. package/src/bots/standalone/Hogger.ts +228 -228
  35. package/src/bots/standalone/Rookie.ts +50 -50
  36. package/src/bots/standalone/TargetDummy.ts +21 -21
  37. package/src/bots/test/cheater.ts +405 -405
  38. package/src/bots/test/crasher.ts +81 -81
  39. package/src/engine/hooks-runtime.ts +394 -394
  40. package/src/engine/manual-match.ts +289 -289
  41. package/src/engine/missile-templates.ts +155 -155
  42. package/src/engine/physics.ts +143 -143
  43. package/src/engine/simulation.ts +828 -828
  44. package/src/engine/spells.ts +128 -128
  45. package/src/engine-version.ts +1 -1
  46. package/src/index.ts +23 -23
  47. package/src/rules.ts +254 -254
  48. package/src/types.ts +193 -193
  49. package/src/utils/index.ts +6 -6
  50. package/dist/chunk-L7Z7OFXD.js.map +0 -1
@@ -1,289 +1,289 @@
1
- /**
2
- * VIBEMANCER - MANUAL MATCH
3
- *
4
- * Tick-by-tick match runner used by manual play mode. Wraps the same
5
- * `tick()` primitive that `simulate()` uses, so stepping forward
6
- * produces the same trajectory as a batched `simulate()` with the
7
- * same seed.
8
- *
9
- * Extra capabilities over `simulate()`:
10
- * - step(N) advances N ticks at a time (default 1) — caller controls pacing
11
- * - replaceMissileAI(id, ai) hot-swaps a missile's AI mid-flight (used by
12
- * the missile-hijack feature in manual play)
13
- * - setInvincible(wizardIndex, on) toggles damage immunity per wizard
14
- */
15
-
16
- import {
17
- ARENA_HEIGHT,
18
- ARENA_WIDTH,
19
- MATCH_DURATION,
20
- SPAWN_DISTANCE,
21
- TICKS_PER_SECOND,
22
- WIZARD_HEALTH,
23
- } from '../rules.js';
24
- import type {
25
- GameConfig,
26
- GameState,
27
- MissileAIFunction,
28
- ProjectileState,
29
- WizardFunction,
30
- } from '../types.js';
31
- import {clearHooks} from './hooks-runtime.js';
32
- import {
33
- type BotError,
34
- type InternalWizardState,
35
- type MatchWinner,
36
- type SimulateResult,
37
- getPlayerState,
38
- tick,
39
- } from './simulation.js';
40
-
41
- export interface ManualMatchOptions
42
- {
43
- seed?: number;
44
- spawnDistance?: number;
45
- maxTicks?: number;
46
- }
47
-
48
- export interface StepResult
49
- {
50
- gameState: GameState;
51
- errors: BotError[];
52
- done: boolean;
53
- }
54
-
55
- /**
56
- * Build the initial pair of internal wizards for a match. Mirrors the
57
- * inline setup in simulate() so both code paths produce identical state.
58
- */
59
- function createInitialWizards(spawnDistance: number): InternalWizardState[]
60
- {
61
- return [
62
- {
63
- id: 'wizard-1',
64
- position: {x: ARENA_WIDTH / 2 - spawnDistance / 2, y: ARENA_HEIGHT / 2},
65
- rotation: 0,
66
- health: WIZARD_HEALTH,
67
- maxHealth: WIZARD_HEALTH,
68
- state: 'idle',
69
- blinkCooldown: 0,
70
- velocity: {x: 0, y: 0},
71
- damageDealt: 0,
72
- damageTaken: 0,
73
- lastHitTick: 0,
74
- },
75
- {
76
- id: 'wizard-2',
77
- position: {x: ARENA_WIDTH / 2 + spawnDistance / 2, y: ARENA_HEIGHT / 2},
78
- rotation: 180,
79
- health: WIZARD_HEALTH,
80
- maxHealth: WIZARD_HEALTH,
81
- state: 'idle',
82
- blinkCooldown: 0,
83
- velocity: {x: 0, y: 0},
84
- damageDealt: 0,
85
- damageTaken: 0,
86
- lastHitTick: 0,
87
- },
88
- ];
89
- }
90
-
91
- export class ManualMatch
92
- {
93
- private readonly wizard1AI: WizardFunction;
94
- private readonly wizard2AI: WizardFunction;
95
- private readonly seed: number;
96
- private readonly maxTicks: number;
97
- private readonly config: GameConfig;
98
-
99
- private wizards: InternalWizardState[];
100
- private projectiles: ProjectileState[] = [];
101
- private missileAIs: Map<string, MissileAIFunction> = new Map();
102
- /** First-replacement originals for hijacked missiles. Used by restoreMissileAI. */
103
- private originalMissileAIs: Map<string, MissileAIFunction> = new Map();
104
- private currentTick = 0;
105
- private done = false;
106
- private allErrors: BotError[] = [];
107
- private history: GameState[];
108
-
109
- constructor(wizard1AI: WizardFunction, wizard2AI: WizardFunction, options: ManualMatchOptions = {})
110
- {
111
- if (typeof wizard1AI !== 'function')
112
- {
113
- throw new Error('wizard1AI must be a function');
114
- }
115
- if (typeof wizard2AI !== 'function')
116
- {
117
- throw new Error('wizard2AI must be a function');
118
- }
119
-
120
- this.wizard1AI = wizard1AI;
121
- this.wizard2AI = wizard2AI;
122
-
123
- const {
124
- seed: rawSeed = Date.now(),
125
- spawnDistance: rawSpawn,
126
- maxTicks: rawMaxTicks = MATCH_DURATION,
127
- } = options;
128
-
129
- this.seed = Number.isFinite(rawSeed) ? Math.floor(rawSeed) & 0x7FFFFFFF : Date.now() & 0x7FFFFFFF;
130
- this.maxTicks = Number.isFinite(rawMaxTicks) && rawMaxTicks > 0
131
- ? Math.min(Math.floor(rawMaxTicks), 100000)
132
- : MATCH_DURATION;
133
- const spawnDistance = Number.isFinite(rawSpawn) && rawSpawn! > 0
134
- ? Math.min(rawSpawn!, Math.min(ARENA_WIDTH, ARENA_HEIGHT) - 100)
135
- : SPAWN_DISTANCE;
136
-
137
- this.config = Object.freeze({
138
- arenaSize: Object.freeze({width: ARENA_WIDTH, height: ARENA_HEIGHT}),
139
- tickRate: TICKS_PER_SECOND,
140
- maxTicks: this.maxTicks,
141
- });
142
-
143
- // Hook state must start clean — bot useState/useRef from a previous
144
- // match would otherwise leak in.
145
- clearHooks('wizard-1');
146
- clearHooks('wizard-2');
147
-
148
- this.wizards = createInitialWizards(spawnDistance);
149
- this.history = [getPlayerState(0, this.wizards, this.projectiles, 0)];
150
- }
151
-
152
- /**
153
- * Advance the match by `count` ticks (default 1). Stops early if the
154
- * match completes mid-batch.
155
- */
156
- step(count = 1): StepResult
157
- {
158
- const errorsThisCall: BotError[] = [];
159
- for (let i = 0; i < count; i++)
160
- {
161
- if (this.done) break;
162
- const result = tick(
163
- this.currentTick,
164
- this.wizard1AI,
165
- this.wizard2AI,
166
- this.config,
167
- this.wizards,
168
- this.projectiles,
169
- this.missileAIs,
170
- this.seed,
171
- );
172
- this.currentTick = result.nextTick;
173
- this.wizards = result.wizards;
174
- this.projectiles = result.projectiles;
175
- if (result.errors.length > 0)
176
- {
177
- this.allErrors.push(...result.errors);
178
- errorsThisCall.push(...result.errors);
179
- }
180
-
181
- this.history.push(getPlayerState(0, this.wizards, this.projectiles, this.currentTick));
182
-
183
- if (
184
- this.wizards[0]!.health <= 0
185
- || this.wizards[1]!.health <= 0
186
- || this.currentTick >= this.maxTicks
187
- )
188
- {
189
- this.done = true;
190
- }
191
- }
192
-
193
- return {
194
- gameState: this.getGameState(),
195
- errors: errorsThisCall,
196
- done: this.done,
197
- };
198
- }
199
-
200
- /**
201
- * Hot-swap a missile's AI function. Used by the missile-hijack feature
202
- * in manual play. The first replacement remembers the original AI so
203
- * `restoreMissileAI` can put it back. Subsequent replacements update
204
- * the active AI but leave the remembered original alone.
205
- *
206
- * No-op if the projectile id doesn't exist.
207
- */
208
- replaceMissileAI(projectileId: string, ai: MissileAIFunction): void
209
- {
210
- const existing = this.missileAIs.get(projectileId);
211
- if (!existing) return;
212
- if (!this.originalMissileAIs.has(projectileId))
213
- {
214
- this.originalMissileAIs.set(projectileId, existing);
215
- }
216
- this.missileAIs.set(projectileId, ai);
217
- }
218
-
219
- /**
220
- * Restore a previously hijacked missile's original AI function.
221
- * No-op if the projectile id doesn't exist or was never hijacked.
222
- */
223
- restoreMissileAI(projectileId: string): void
224
- {
225
- const original = this.originalMissileAIs.get(projectileId);
226
- if (!original) return;
227
- if (!this.missileAIs.has(projectileId)) return;
228
- this.missileAIs.set(projectileId, original);
229
- this.originalMissileAIs.delete(projectileId);
230
- }
231
-
232
- /**
233
- * Set the invincibility flag for a wizard. Invincible wizards take 0
234
- * damage from all sources.
235
- */
236
- setInvincible(wizardIndex: 0 | 1, on: boolean): void
237
- {
238
- const wizard = this.wizards[wizardIndex];
239
- if (!wizard) return;
240
- wizard.invincible = on;
241
- }
242
-
243
- /**
244
- * Get the current game state from wizard-1's perspective.
245
- * Returned object is a deep clone — safe to mutate.
246
- */
247
- getGameState(): GameState
248
- {
249
- return getPlayerState(0, this.wizards, this.projectiles, this.currentTick);
250
- }
251
-
252
- getCurrentTick(): number
253
- {
254
- return this.currentTick;
255
- }
256
-
257
- isComplete(): {done: boolean; winner: MatchWinner}
258
- {
259
- return {done: this.done, winner: this.done ? this.computeWinner() : null};
260
- }
261
-
262
- getResult(): SimulateResult
263
- {
264
- return {
265
- winner: this.computeWinner(),
266
- ticks: this.currentTick,
267
- finalState: this.getGameState(),
268
- history: this.history,
269
- errors: this.allErrors,
270
- };
271
- }
272
-
273
- dispose(): void
274
- {
275
- clearHooks('wizard-1');
276
- clearHooks('wizard-2');
277
- this.history = [];
278
- }
279
-
280
- private computeWinner(): MatchWinner
281
- {
282
- const w1 = this.wizards[0]!;
283
- const w2 = this.wizards[1]!;
284
- if (w1.health <= 0 && w2.health <= 0) return 'draw';
285
- if (w2.health <= 0) return 'wizard-1';
286
- if (w1.health <= 0) return 'wizard-2';
287
- return null;
288
- }
289
- }
1
+ /**
2
+ * VIBEMANCER - MANUAL MATCH
3
+ *
4
+ * Tick-by-tick match runner used by manual play mode. Wraps the same
5
+ * `tick()` primitive that `simulate()` uses, so stepping forward
6
+ * produces the same trajectory as a batched `simulate()` with the
7
+ * same seed.
8
+ *
9
+ * Extra capabilities over `simulate()`:
10
+ * - step(N) advances N ticks at a time (default 1) — caller controls pacing
11
+ * - replaceMissileAI(id, ai) hot-swaps a missile's AI mid-flight (used by
12
+ * the missile-hijack feature in manual play)
13
+ * - setInvincible(wizardIndex, on) toggles damage immunity per wizard
14
+ */
15
+
16
+ import {
17
+ ARENA_HEIGHT,
18
+ ARENA_WIDTH,
19
+ MATCH_DURATION,
20
+ SPAWN_DISTANCE,
21
+ TICKS_PER_SECOND,
22
+ WIZARD_HEALTH,
23
+ } from '../rules.js';
24
+ import type {
25
+ GameConfig,
26
+ GameState,
27
+ MissileAIFunction,
28
+ ProjectileState,
29
+ WizardFunction,
30
+ } from '../types.js';
31
+ import {clearHooks} from './hooks-runtime.js';
32
+ import {
33
+ type BotError,
34
+ type InternalWizardState,
35
+ type MatchWinner,
36
+ type SimulateResult,
37
+ getPlayerState,
38
+ tick,
39
+ } from './simulation.js';
40
+
41
+ export interface ManualMatchOptions
42
+ {
43
+ seed?: number;
44
+ spawnDistance?: number;
45
+ maxTicks?: number;
46
+ }
47
+
48
+ export interface StepResult
49
+ {
50
+ gameState: GameState;
51
+ errors: BotError[];
52
+ done: boolean;
53
+ }
54
+
55
+ /**
56
+ * Build the initial pair of internal wizards for a match. Mirrors the
57
+ * inline setup in simulate() so both code paths produce identical state.
58
+ */
59
+ function createInitialWizards(spawnDistance: number): InternalWizardState[]
60
+ {
61
+ return [
62
+ {
63
+ id: 'wizard-1',
64
+ position: {x: ARENA_WIDTH / 2 - spawnDistance / 2, y: ARENA_HEIGHT / 2},
65
+ rotation: 0,
66
+ health: WIZARD_HEALTH,
67
+ maxHealth: WIZARD_HEALTH,
68
+ state: 'idle',
69
+ blinkCooldown: 0,
70
+ velocity: {x: 0, y: 0},
71
+ damageDealt: 0,
72
+ damageTaken: 0,
73
+ lastHitTick: 0,
74
+ },
75
+ {
76
+ id: 'wizard-2',
77
+ position: {x: ARENA_WIDTH / 2 + spawnDistance / 2, y: ARENA_HEIGHT / 2},
78
+ rotation: 180,
79
+ health: WIZARD_HEALTH,
80
+ maxHealth: WIZARD_HEALTH,
81
+ state: 'idle',
82
+ blinkCooldown: 0,
83
+ velocity: {x: 0, y: 0},
84
+ damageDealt: 0,
85
+ damageTaken: 0,
86
+ lastHitTick: 0,
87
+ },
88
+ ];
89
+ }
90
+
91
+ export class ManualMatch
92
+ {
93
+ private readonly wizard1AI: WizardFunction;
94
+ private readonly wizard2AI: WizardFunction;
95
+ private readonly seed: number;
96
+ private readonly maxTicks: number;
97
+ private readonly config: GameConfig;
98
+
99
+ private wizards: InternalWizardState[];
100
+ private projectiles: ProjectileState[] = [];
101
+ private missileAIs: Map<string, MissileAIFunction> = new Map();
102
+ /** First-replacement originals for hijacked missiles. Used by restoreMissileAI. */
103
+ private originalMissileAIs: Map<string, MissileAIFunction> = new Map();
104
+ private currentTick = 0;
105
+ private done = false;
106
+ private allErrors: BotError[] = [];
107
+ private history: GameState[];
108
+
109
+ constructor(wizard1AI: WizardFunction, wizard2AI: WizardFunction, options: ManualMatchOptions = {})
110
+ {
111
+ if (typeof wizard1AI !== 'function')
112
+ {
113
+ throw new Error('wizard1AI must be a function');
114
+ }
115
+ if (typeof wizard2AI !== 'function')
116
+ {
117
+ throw new Error('wizard2AI must be a function');
118
+ }
119
+
120
+ this.wizard1AI = wizard1AI;
121
+ this.wizard2AI = wizard2AI;
122
+
123
+ const {
124
+ seed: rawSeed = Date.now(),
125
+ spawnDistance: rawSpawn,
126
+ maxTicks: rawMaxTicks = MATCH_DURATION,
127
+ } = options;
128
+
129
+ this.seed = Number.isFinite(rawSeed) ? Math.floor(rawSeed) & 0x7FFFFFFF : Date.now() & 0x7FFFFFFF;
130
+ this.maxTicks = Number.isFinite(rawMaxTicks) && rawMaxTicks > 0
131
+ ? Math.min(Math.floor(rawMaxTicks), 100000)
132
+ : MATCH_DURATION;
133
+ const spawnDistance = Number.isFinite(rawSpawn) && rawSpawn! > 0
134
+ ? Math.min(rawSpawn!, Math.min(ARENA_WIDTH, ARENA_HEIGHT) - 100)
135
+ : SPAWN_DISTANCE;
136
+
137
+ this.config = Object.freeze({
138
+ arenaSize: Object.freeze({width: ARENA_WIDTH, height: ARENA_HEIGHT}),
139
+ tickRate: TICKS_PER_SECOND,
140
+ maxTicks: this.maxTicks,
141
+ });
142
+
143
+ // Hook state must start clean — bot useState/useRef from a previous
144
+ // match would otherwise leak in.
145
+ clearHooks('wizard-1');
146
+ clearHooks('wizard-2');
147
+
148
+ this.wizards = createInitialWizards(spawnDistance);
149
+ this.history = [getPlayerState(0, this.wizards, this.projectiles, 0)];
150
+ }
151
+
152
+ /**
153
+ * Advance the match by `count` ticks (default 1). Stops early if the
154
+ * match completes mid-batch.
155
+ */
156
+ step(count = 1): StepResult
157
+ {
158
+ const errorsThisCall: BotError[] = [];
159
+ for (let i = 0; i < count; i++)
160
+ {
161
+ if (this.done) break;
162
+ const result = tick(
163
+ this.currentTick,
164
+ this.wizard1AI,
165
+ this.wizard2AI,
166
+ this.config,
167
+ this.wizards,
168
+ this.projectiles,
169
+ this.missileAIs,
170
+ this.seed,
171
+ );
172
+ this.currentTick = result.nextTick;
173
+ this.wizards = result.wizards;
174
+ this.projectiles = result.projectiles;
175
+ if (result.errors.length > 0)
176
+ {
177
+ this.allErrors.push(...result.errors);
178
+ errorsThisCall.push(...result.errors);
179
+ }
180
+
181
+ this.history.push(getPlayerState(0, this.wizards, this.projectiles, this.currentTick));
182
+
183
+ if (
184
+ this.wizards[0]!.health <= 0
185
+ || this.wizards[1]!.health <= 0
186
+ || this.currentTick >= this.maxTicks
187
+ )
188
+ {
189
+ this.done = true;
190
+ }
191
+ }
192
+
193
+ return {
194
+ gameState: this.getGameState(),
195
+ errors: errorsThisCall,
196
+ done: this.done,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Hot-swap a missile's AI function. Used by the missile-hijack feature
202
+ * in manual play. The first replacement remembers the original AI so
203
+ * `restoreMissileAI` can put it back. Subsequent replacements update
204
+ * the active AI but leave the remembered original alone.
205
+ *
206
+ * No-op if the projectile id doesn't exist.
207
+ */
208
+ replaceMissileAI(projectileId: string, ai: MissileAIFunction): void
209
+ {
210
+ const existing = this.missileAIs.get(projectileId);
211
+ if (!existing) return;
212
+ if (!this.originalMissileAIs.has(projectileId))
213
+ {
214
+ this.originalMissileAIs.set(projectileId, existing);
215
+ }
216
+ this.missileAIs.set(projectileId, ai);
217
+ }
218
+
219
+ /**
220
+ * Restore a previously hijacked missile's original AI function.
221
+ * No-op if the projectile id doesn't exist or was never hijacked.
222
+ */
223
+ restoreMissileAI(projectileId: string): void
224
+ {
225
+ const original = this.originalMissileAIs.get(projectileId);
226
+ if (!original) return;
227
+ if (!this.missileAIs.has(projectileId)) return;
228
+ this.missileAIs.set(projectileId, original);
229
+ this.originalMissileAIs.delete(projectileId);
230
+ }
231
+
232
+ /**
233
+ * Set the invincibility flag for a wizard. Invincible wizards take 0
234
+ * damage from all sources.
235
+ */
236
+ setInvincible(wizardIndex: 0 | 1, on: boolean): void
237
+ {
238
+ const wizard = this.wizards[wizardIndex];
239
+ if (!wizard) return;
240
+ wizard.invincible = on;
241
+ }
242
+
243
+ /**
244
+ * Get the current game state from wizard-1's perspective.
245
+ * Returned object is a deep clone — safe to mutate.
246
+ */
247
+ getGameState(): GameState
248
+ {
249
+ return getPlayerState(0, this.wizards, this.projectiles, this.currentTick);
250
+ }
251
+
252
+ getCurrentTick(): number
253
+ {
254
+ return this.currentTick;
255
+ }
256
+
257
+ isComplete(): {done: boolean; winner: MatchWinner}
258
+ {
259
+ return {done: this.done, winner: this.done ? this.computeWinner() : null};
260
+ }
261
+
262
+ getResult(): SimulateResult
263
+ {
264
+ return {
265
+ winner: this.computeWinner(),
266
+ ticks: this.currentTick,
267
+ finalState: this.getGameState(),
268
+ history: this.history,
269
+ errors: this.allErrors,
270
+ };
271
+ }
272
+
273
+ dispose(): void
274
+ {
275
+ clearHooks('wizard-1');
276
+ clearHooks('wizard-2');
277
+ this.history = [];
278
+ }
279
+
280
+ private computeWinner(): MatchWinner
281
+ {
282
+ const w1 = this.wizards[0]!;
283
+ const w2 = this.wizards[1]!;
284
+ if (w1.health <= 0 && w2.health <= 0) return 'draw';
285
+ if (w2.health <= 0) return 'wizard-1';
286
+ if (w1.health <= 0) return 'wizard-2';
287
+ return null;
288
+ }
289
+ }