@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
|
@@ -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
|
+
}
|