@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.
Files changed (78) hide show
  1. package/README.md +28 -0
  2. package/dist/chunk-L7Z7OFXD.js +9140 -0
  3. package/dist/chunk-L7Z7OFXD.js.map +1 -0
  4. package/dist/index-browser.d.ts +2602 -0
  5. package/dist/index-browser.js +407 -0
  6. package/dist/index-browser.js.map +1 -0
  7. package/dist/index.d.ts +150 -0
  8. package/dist/index.js +750 -0
  9. package/dist/index.js.map +1 -0
  10. package/package.json +79 -0
  11. package/src/bots/berserker/01_Stormchaser.ts +457 -0
  12. package/src/bots/berserker/02_Stormcaller.ts +417 -0
  13. package/src/bots/berserker/03_Stormforger.ts +481 -0
  14. package/src/bots/caster/01_Flamecaller.ts +286 -0
  15. package/src/bots/caster/02_Pyromancer.ts +350 -0
  16. package/src/bots/caster/03_Infernalist.ts +492 -0
  17. package/src/bots/defensive/01_Turtle.ts +151 -0
  18. package/src/bots/defensive/02_Sentinel.ts +134 -0
  19. package/src/bots/defensive/03_Golem.ts +357 -0
  20. package/src/bots/duelist/01_Battlemage.ts +433 -0
  21. package/src/bots/duelist/02_Warmage.ts +438 -0
  22. package/src/bots/duelist/03_Archmage.ts +588 -0
  23. package/src/bots/homing/01_Bonemancer.ts +67 -0
  24. package/src/bots/homing/02_Lich.ts +356 -0
  25. package/src/bots/homing/03_Archlich.ts +220 -0
  26. package/src/bots/index.ts +30 -0
  27. package/src/bots/kiter/01_Spellspinner.ts +398 -0
  28. package/src/bots/kiter/02_Spellweaver.ts +378 -0
  29. package/src/bots/kiter/03_Spellbinder.ts +448 -0
  30. package/src/bots/melee/01_Shadowblade.ts +270 -0
  31. package/src/bots/melee/02_Nightblade.ts +437 -0
  32. package/src/bots/melee/03_Voidblade.ts +582 -0
  33. package/src/bots/registry.ts +207 -0
  34. package/src/bots/shared.ts +472 -0
  35. package/src/bots/sniper/01_Spellshot.ts +385 -0
  36. package/src/bots/sniper/02_Spelltracer.ts +441 -0
  37. package/src/bots/sniper/03_Spellseeker.ts +546 -0
  38. package/src/bots/standalone/Critter.ts +89 -0
  39. package/src/bots/standalone/Doombringer.ts +91 -0
  40. package/src/bots/standalone/Hogger.ts +228 -0
  41. package/src/bots/standalone/Rookie.ts +50 -0
  42. package/src/bots/standalone/TargetDummy.ts +21 -0
  43. package/src/bots/test/cheater.ts +405 -0
  44. package/src/bots/test/crasher.ts +81 -0
  45. package/src/engine/hooks-runtime.ts +394 -0
  46. package/src/engine/manual-match.ts +289 -0
  47. package/src/engine/missile-templates.ts +155 -0
  48. package/src/engine/optimizer.ts +220 -0
  49. package/src/engine/params-runtime.ts +189 -0
  50. package/src/engine/physics.ts +143 -0
  51. package/src/engine/sandbox-browser.ts +671 -0
  52. package/src/engine/sandbox-compile.ts +197 -0
  53. package/src/engine/sandbox-harness.ts +367 -0
  54. package/src/engine/sandbox.ts +332 -0
  55. package/src/engine/simulation.ts +828 -0
  56. package/src/engine/spells.ts +128 -0
  57. package/src/engine-version.ts +11 -0
  58. package/src/hooks/action-builders.ts +210 -0
  59. package/src/hooks/bot-wrapper.ts +84 -0
  60. package/src/hooks/index.ts +75 -0
  61. package/src/hooks/state-hooks.ts +354 -0
  62. package/src/hooks/threat-analysis.ts +365 -0
  63. package/src/hooks/types.ts +142 -0
  64. package/src/index-browser.ts +30 -0
  65. package/src/index.ts +24 -0
  66. package/src/rules.ts +254 -0
  67. package/src/stats.ts +262 -0
  68. package/src/testing.ts +207 -0
  69. package/src/trace.ts +430 -0
  70. package/src/types.ts +193 -0
  71. package/src/utils/angles.ts +47 -0
  72. package/src/utils/combat.ts +279 -0
  73. package/src/utils/distance.ts +21 -0
  74. package/src/utils/index.ts +7 -0
  75. package/src/utils/movement.ts +108 -0
  76. package/src/utils/random.ts +65 -0
  77. package/src/utils/spatial.ts +63 -0
  78. package/src/utils/targeting.ts +45 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * VIBEMANCER - PARAMETER RUNTIME
3
+ *
4
+ * Provides the useParam() hook for bots to declare tunable parameters,
5
+ * and the infrastructure for the optimizer to inject/discover parameter values.
6
+ *
7
+ * Design: Module-level state (JS is single-threaded, no race conditions).
8
+ * The optimizer sets param values before running a bot, and clears them after.
9
+ * During discovery, all useParam calls are recorded.
10
+ *
11
+ * ## useParam API
12
+ *
13
+ * ```typescript
14
+ * // Basic: just a value, no optimizer config
15
+ * const damage = useParam('damage', 15);
16
+ *
17
+ * // With range: optimizer searches value ± range (sliding window)
18
+ * const distance = useParam('distance', 350, {range: 150, min: 0});
19
+ *
20
+ * // With fixed min/max: optimizer searches [min, max] (fixed bounds)
21
+ * const damage = useParam('damage', 15, {min: 5, max: 25});
22
+ *
23
+ * // With all: range defines search radius, min/max clamp it
24
+ * const fraction = useParam('fraction', 0.25, {range: 0.2, min: 0, max: 1});
25
+ *
26
+ * // With custom step count: optimizer tests 20 values instead of default 10
27
+ * const distance = useParam('distance', 500, {range: 200, min: 0, steps: 20});
28
+ * ```
29
+ *
30
+ * - **Arg 1** `name` — unique parameter name (must be consistent across ticks)
31
+ * - **Arg 2** `value` — the actual value used in gameplay. This is what your bot
32
+ * uses during matches. The optimizer script automatically updates this value.
33
+ * - **Arg 3** `config` (optional) — optimizer search configuration:
34
+ * - `range` — search radius: optimizer checks `value ± range`. The auto-optimizer
35
+ * rewrites `value` after each run, so the search window slides automatically.
36
+ * - `min` / `max` — hard constraints (e.g., distance ≥ 0, fraction ≤ 1).
37
+ * When `range` is omitted, these define fixed search bounds (old-style).
38
+ * - `steps` — how many evenly-spaced values the optimizer tests per pass (default: 10).
39
+ * - `substeps` — steps to use in refinement passes (passes 2+). Set to 0 to freeze
40
+ * after pass 1 (ideal for boolean params). Defaults to `steps` if not specified.
41
+ * - At least `range` or both `min` + `max` must be provided.
42
+ *
43
+ * Without optimizer config, useParam simply returns `value` every tick.
44
+ * With optimizer config, the offline optimizer script can override the value during search.
45
+ *
46
+ * ## Rules of Hooks
47
+ * useParam follows the same rules as useState/useEffect/etc:
48
+ * - Must be called at the top level of your bot function (not inside conditionals)
49
+ * - Must be called in the same order every tick
50
+ * - Violations are detected and throw errors
51
+ */
52
+
53
+ import type {WizardFunction} from '../types.js';
54
+ import {validateHookCall} from './hooks-runtime.js';
55
+
56
+ /**
57
+ * Declaration of a tunable parameter, as discovered by the optimizer.
58
+ */
59
+ export interface ParamDeclaration
60
+ {
61
+ name: string;
62
+ value: number;
63
+ range?: number;
64
+ min?: number;
65
+ max?: number;
66
+ steps: number;
67
+ /** Steps to use in refinement passes (passes 2+). 0 = freeze after pass 1. Defaults to `steps`. */
68
+ substeps?: number;
69
+ }
70
+
71
+ // --- Module-level state ---
72
+
73
+ /** Current parameter values injected by the optimizer. null = use defaults. */
74
+ let paramValues: Map<string, number> | null = null;
75
+
76
+ /** Params discovered during discovery mode. */
77
+ const discoveredParams = new Map<string, ParamDeclaration>();
78
+
79
+ /** Whether we're in discovery mode (collecting param declarations). */
80
+ let discovering = false;
81
+
82
+ // --- Public API for bot authors ---
83
+
84
+ /**
85
+ * Declare a tunable parameter. Returns the current value (optimizer-injected or the provided value).
86
+ *
87
+ * @param name - Unique parameter name (consistent across ticks)
88
+ * @param value - The gameplay value. The auto-optimizer rewrites this in source code.
89
+ * @param config - Optional optimizer search configuration
90
+ * @returns The optimizer-injected value during optimization, or `value` during normal play
91
+ *
92
+ * @example
93
+ * // Simple: no optimizer config
94
+ * const damage = useParam('damage', 15);
95
+ *
96
+ * // With range: optimizer searches value ± range (sliding window)
97
+ * const distance = useParam('distance', 350, {range: 150, min: 0});
98
+ *
99
+ * // With fixed min/max: optimizer searches [min, max]
100
+ * const damage = useParam('damage', 15, {min: 5, max: 25});
101
+ */
102
+ export function useParam(name: string, value: number, config?: {
103
+ range?: number;
104
+ min?: number;
105
+ max?: number;
106
+ steps?: number;
107
+ substeps?: number;
108
+ }): number
109
+ {
110
+ // Participate in hook index validation (same rules as useState, useEffect, etc.)
111
+ validateHookCall('useParam');
112
+
113
+ // Register for discovery (only if optimizer config is provided)
114
+ if (discovering && config)
115
+ {
116
+ discoveredParams.set(name, {
117
+ name,
118
+ value,
119
+ range: config.range,
120
+ min: config.min,
121
+ max: config.max,
122
+ steps: config.steps ?? 10,
123
+ substeps: config.substeps,
124
+ });
125
+ }
126
+
127
+ // Return optimizer value or the provided value
128
+ return paramValues?.get(name) ?? value;
129
+ }
130
+
131
+ // --- Optimizer API ---
132
+
133
+ /**
134
+ * Inject parameter values for the next bot execution.
135
+ * The wrapped bot will read these values via useParam().
136
+ */
137
+ export function setParamValues(values: Record<string, number>): void
138
+ {
139
+ paramValues = new Map(Object.entries(values));
140
+ }
141
+
142
+ /**
143
+ * Clear injected parameter values. useParam() will return its provided value.
144
+ */
145
+ export function clearParamValues(): void
146
+ {
147
+ paramValues = null;
148
+ }
149
+
150
+ /**
151
+ * Start discovery mode. All subsequent useParam() calls with optimizer config
152
+ * will register their declarations.
153
+ */
154
+ export function startDiscovery(): void
155
+ {
156
+ discovering = true;
157
+ discoveredParams.clear();
158
+ }
159
+
160
+ /**
161
+ * Stop discovery mode and return all discovered parameter declarations.
162
+ */
163
+ export function stopDiscovery(): ParamDeclaration[]
164
+ {
165
+ discovering = false;
166
+ const result = [...discoveredParams.values()];
167
+ discoveredParams.clear();
168
+ return result;
169
+ }
170
+
171
+ /**
172
+ * Wrap a bot function to inject specific parameter values.
173
+ * The returned function sets params before calling the bot and clears them after.
174
+ */
175
+ export function wrapWithParams(bot: WizardFunction, params: Record<string, number>): WizardFunction
176
+ {
177
+ return (props) =>
178
+ {
179
+ setParamValues(params);
180
+ try
181
+ {
182
+ return bot(props);
183
+ }
184
+ finally
185
+ {
186
+ clearParamValues();
187
+ }
188
+ };
189
+ }
@@ -0,0 +1,143 @@
1
+ import {Position, WizardState, ProjectileState} from '../types.js';
2
+ import {ARENA_WIDTH, ARENA_HEIGHT, MOVEMENT_SPEED, CASTING_MOVEMENT_MULT, WIZARD_RADIUS} from '../rules.js';
3
+
4
+ /**
5
+ * Move a wizard based on world-space input direction.
6
+ *
7
+ * Movement model (from design doc):
8
+ * - x: +100 = right, -100 = left
9
+ * - y: +100 = down, -100 = up
10
+ * - Values are clamped to [-100, 100]
11
+ * - Diagonal movement is normalized (magnitude capped at 100)
12
+ * - No rotation tracking - just output (x, y) direction
13
+ */
14
+ export function moveWizard(wizard: WizardState, move: {x: number; y: number}, deltaTicks: number): Position
15
+ {
16
+ // Clamp input values to [-100, 100]
17
+ let dx = Math.max(-100, Math.min(100, move.x || 0));
18
+ let dy = Math.max(-100, Math.min(100, move.y || 0));
19
+
20
+ // Normalize diagonal movement (cap magnitude at 100)
21
+ const magnitude = Math.sqrt(dx * dx + dy * dy);
22
+ if (magnitude > 100)
23
+ {
24
+ dx = dx * 100 / magnitude;
25
+ dy = dy * 100 / magnitude;
26
+ }
27
+
28
+ // Convert from [-100, 100] to [-1, 1] direction
29
+ dx = dx / 100;
30
+ dy = dy / 100;
31
+
32
+ let speed = MOVEMENT_SPEED;
33
+ if (wizard.state === 'casting')
34
+ {
35
+ speed *= CASTING_MOVEMENT_MULT;
36
+ }
37
+ else if (wizard.state === 'channeling')
38
+ {
39
+ // Movement keys ignored during channeling (design doc line 197, 1120)
40
+ speed = 0;
41
+ }
42
+
43
+ const newPos = {
44
+ x: wizard.position.x + dx * speed * deltaTicks,
45
+ y: wizard.position.y + dy * speed * deltaTicks,
46
+ };
47
+
48
+ return clampToArena(newPos, WIZARD_RADIUS);
49
+ }
50
+
51
+ /**
52
+ * Move a projectile in its current rotation direction.
53
+ */
54
+ export function moveProjectile(projectile: ProjectileState, deltaTicks: number): Position
55
+ {
56
+ const radians = projectile.rotation * (Math.PI / 180);
57
+ return {
58
+ x: projectile.position.x + Math.cos(radians) * projectile.speed * deltaTicks,
59
+ y: projectile.position.y + Math.sin(radians) * projectile.speed * deltaTicks,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Clamp a position to the arena boundaries.
65
+ */
66
+ export function clampToArena(position: Position, radius: number): Position
67
+ {
68
+ return {
69
+ x: Math.max(radius, Math.min(ARENA_WIDTH - radius, position.x)),
70
+ y: Math.max(radius, Math.min(ARENA_HEIGHT - radius, position.y)),
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Resolve body collision between two wizards.
76
+ * Pushes both apart equally so they don't overlap. Neither is blocked — they just can't stack.
77
+ * Iterates until stable: wizard push → wall clamp → re-check overlap → repeat.
78
+ */
79
+ export function resolveWizardCollision(wizard1: WizardState, wizard2: WizardState): void
80
+ {
81
+ const minDist = WIZARD_RADIUS * 2;
82
+ const maxIterations = 5; // Prevents infinite loops in edge cases
83
+
84
+ for (let i = 0; i < maxIterations; i++)
85
+ {
86
+ const dx = wizard2.position.x - wizard1.position.x;
87
+ const dy = wizard2.position.y - wizard1.position.y;
88
+ const dist = Math.sqrt(dx * dx + dy * dy);
89
+
90
+ if (dist >= minDist) return; // No overlap — done
91
+
92
+ if (dist === 0)
93
+ {
94
+ // Exactly overlapping — push apart along arbitrary axis
95
+ wizard1.position = {x: wizard1.position.x - WIZARD_RADIUS, y: wizard1.position.y};
96
+ wizard2.position = {x: wizard2.position.x + WIZARD_RADIUS, y: wizard2.position.y};
97
+ }
98
+ else
99
+ {
100
+ const overlap = minDist - dist;
101
+ const pushX = (dx / dist) * (overlap / 2);
102
+ const pushY = (dy / dist) * (overlap / 2);
103
+
104
+ wizard1.position = {x: wizard1.position.x - pushX, y: wizard1.position.y - pushY};
105
+ wizard2.position = {x: wizard2.position.x + pushX, y: wizard2.position.y + pushY};
106
+ }
107
+
108
+ // Clamp both to arena — this may re-introduce overlap if one is pushed into a wall
109
+ wizard1.position = clampToArena(wizard1.position, WIZARD_RADIUS);
110
+ wizard2.position = clampToArena(wizard2.position, WIZARD_RADIUS);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Perform swept circle collision detection between a moving point (projectile) and a stationary circle (wizard).
116
+ * Returns true if a collision occurred during the movement from oldPos to newPos.
117
+ */
118
+ export function sweptCircleCollision(oldPos: Position, newPos: Position, radius: number, targetPos: Position, targetRadius: number): boolean
119
+ {
120
+ // Distance from point to line segment
121
+ const totalRadius = radius + targetRadius;
122
+
123
+ // Vector from oldPos to newPos
124
+ const dx = newPos.x - oldPos.x;
125
+ const dy = newPos.y - oldPos.y;
126
+ const d2 = dx * dx + dy * dy;
127
+
128
+ if (d2 === 0)
129
+ {
130
+ const dist2 = (oldPos.x - targetPos.x) ** 2 + (oldPos.y - targetPos.y) ** 2;
131
+ return dist2 <= totalRadius * totalRadius;
132
+ }
133
+
134
+ // Projection of targetPos onto the line segment (oldPos, newPos)
135
+ let t = ((targetPos.x - oldPos.x) * dx + (targetPos.y - oldPos.y) * dy) / d2;
136
+ t = Math.max(0, Math.min(1, t));
137
+
138
+ const closestX = oldPos.x + t * dx;
139
+ const closestY = oldPos.y + t * dy;
140
+
141
+ const dist2 = (targetPos.x - closestX) ** 2 + (targetPos.y - closestY) ** 2;
142
+ return dist2 <= totalRadius * totalRadius;
143
+ }