How the bot thinks
The Bot page does not play a recording. It runs the real game, a Rust/WebAssembly planner loaded by bot-engine.js, a small worker pool when the browser exposes spare cores, and a thin driver in bot.js in the same page. The landing-page previews load the same Rust/WebAssembly planner, and like the Bot page they wait for WebAssembly before driving the game. The planner returns a DOS arrow scancode, and the driver presses the same key a player would press.
bot.html; it reads the port's live variables and presses keys like a player would. Beyond one bookkeeping write — jumping to the selected LEVEL — it leaves the game's rules and engine untouched.1. Where the bot runs
The bot page hosts the real game directly. It loads game.js, then bot-engine.js (the WebAssembly planner and worker coordinator), then bot.js. The driver waits until bot-engine.wasm is ready before pressing a move. It still handles tabs, restarts, speed, and key presses:
const stalled = idle > 120 || (idle > 24 && repeats >= 3);
const forceRisk = stalled || movesSincePickup >= 250;
let sc = await planner.decide({ idle, looping, headTrail, budgetMs: routeBudget(forceRisk), forceRisk });
if(sc === null) sc = randomLegalScancode(); // planner gave up or a safeguard tripped
if(sc !== null) pushKey(keyOf[sc]); // else: no legal move at all -> stuck flash
That placement is deliberate. The game uses const and let globals such as T, BTEL, ETEL, LEVEL, HART, KLAVER, and pushKey(). Those names are visible to scripts running in the same page, but they are not ordinary window properties. Keeping the bot in the same script realm lets it call peek(), read T[BTEL], and press keys through pushKey() as if it were part of the page.
Level jump
It dismisses level 1, sets LEVEL = TARGET - 1, presses F10 once, and lets the game's own loop enter the selected level.
Snapshot planner
Each decision copies the CP437 playfield into a compact array, then searches that snapshot instead of repeatedly reading the rendered game state.
Real input
It sends DOS-style extended-key strings: up ' H', down ' P', left ' K', right ' M'.
Restart handling
If the snake dies, the driver detects the unwind or life drop, queues the next late level, and dismisses the restart prompt.
2. The data it sees
The bot uses the same screen-memory model as the game. It does not have a neat list of "objects"; it reads characters from VRAM with peek(offset). The snake body is reconstructed from the game's T array, from tail index ETEL through head index BTEL.
| Game value | Meaning to the bot | Planning effect |
|---|---|---|
32 |
Empty cell | Freely enterable. |
3 ♥ |
Heart | Food worth 10 points; grows the snake. |
5 ♣ |
Club | Food worth 25 points; grows the snake. |
1 ☺ |
Smiley | Costs 50 points, still grows the snake, and spawns a replacement; spent deliberately when that is what keeps the path back to the tail open. |
10 ◙ |
Stone | Pushable if the cell behind it is empty. |
24, 26, 27 ↑→← |
Moving arrows | Treated as fatal, including cells they are about to enter. |
219 |
Snake head | Used by the game for collision; the bot also tracks the head through T[BTEL]. |
At the start of each decision, bot-engine.js copies the CP437 character bytes, the snake offsets from T, the arrow enemy table D, and the recent head-position trail into fixed WebAssembly memory buffers. When workers are available, each worker owns its own WebAssembly instance and receives the same snapshot, so it can think without sharing mutable game state. For simulated routes, the Rust planner records only changes caused by imaginary moves, such as a pushed stone or an emptied tail cell, instead of cloning the whole VRAM array for every branch.
3. One decision cycle
Every bot move follows the same ladder. In single-thread mode, the first strategy that returns a direction wins. In worker mode, the coordinator runs the baseline planner beside deeper search profiles: a deeper full planner, a deeper urgent/finish planner, and an aggressive stall-breaker when enough cores are available. Results are tagged by strategy tier, so a deeper worker can replace the baseline only when it proves an equally good or better kind of move. The bot then presses that key, waits according to the speed slider, and recalculates from the new live game state. Two commitments sit on top of the ladder. In the endgame it gets a zeroth rung: with only a few items left, the planner first tries to certify one complete simulated tour that eats everything remaining, and takes it whole. And whenever a food search wins, the planner hands back its entire certified route — the driver replays it step by step, re-validating each move, instead of replanning from scratch every tick.
Arrow danger is recalculated for this tick, with memoization so repeated checks are cheap.
First try a shallow search for close hearts or clubs, without eating smileys.
If the board is cramped or the snake is long, choose a proved open-space move before chasing distant food.
If no nearby pickup or breathing move wins, run the deeper food planner. A smiley route can be tried after clean routes fail, but it scores well only when it keeps return access open or unlocks clustered food.
If the head is repeating positions or the score has been idle, push toward food instead of circling the tail. If no fully proved route is found in time, use a one-step food-directed pressure move.
As the final fallback, pick a one-step move that still satisfies the local safety checks.
If no route can be proven, still return the best immediately legal move. The bot never presses Escape for itself.
const decide = options => {
model = capture();
resetDanger();
const urgent = options.idle >= 18 || options.looping;
const forceRisk = options.forceRisk === true;
routeDeadline = now() + options.budgetMs;
let proved = null;
try {
proved = forceRisk
? pressureFood(true, true) ??
nearFood(true) ??
routeFood(true)
: nearFood(false) ??
routeFood(false) ??
pressureFood(false, urgent);
} finally {
routeDeadline = 0;
}
return forceRisk
? proved ?? pressureStep([false, true]) ?? riskyMove() ??
survivalMove([false, true]) ?? lastChanceMove()
: proved ?? (urgent ? pressureStep() : null) ??
survivalMove() ?? lastChanceMove();
};
The looping flag comes from a short head-position trail. The WebAssembly planner also receives that trail directly, so simulated routes that walk back over hot recent cells pay a memory debt unless they make food progress or preserve tail access. If the current head offset has appeared repeatedly while the score has not changed, the bot treats tail-following as suspicious and switches to food pressure. The forceRisk branch shown above is the planner's own escape ladder — the most aggressive food grab, the stone-dig pressure step, the long-snake tail-follow, then the local survival moves, all while quadrupling that recent-cell debt so every forced move walks into fresh cells. The live driver raises that branch when the snake stalls or goes too long without a real pickup, giving the Wasm planner a larger budget before random fallback is considered. The breathingMove() gate is different: it runs before distant food routes when the current region is cramped, and it values tail reach, exits, and survival depth over immediate points. In that constrained mode, a smiley can beat a distant clean pickup if the smiley route keeps a route back to the moving tail. If the proof planners time out or cannot certify a route, pressureStep() chooses a safe immediate move that lowers the distance to reachable food. If even that fails, lastChanceMove() returns the least-bad immediately legal move; bot pages keep moving until the snake is really stuck (no legal move at all) or the level clears.
4. Arrow danger model
The bot treats arrow levels differently because a square can be empty now and fatal a few ticks later. The
danger(offset) function marks a destination unsafe if an arrow is already there, if a projected future arrow will step into it, or if a wrapping arrow will reappear there.
peek(o) is 24, 26, or 27.
The arrows are fully deterministic, so the WebAssembly planner simulates them exactly, 28 future ticks deep — including the game's stall rule: an arrow blocked by a wall or by the snake's own body waits in place instead of flying past. Each simulated branch checks the mask for its own future step, so a route can genuinely time a lane crossing: squares that look empty now but will be crossed are rejected, and a stalled arrow is expected exactly where it really waits.
5. Move simulation
The core planner primitive is move(state, scancode, allowSmile). It returns a new imaginary state if the move is legal, or null if it would collide, reverse, die, or push an impossible stone.
| Rule | How the bot applies it |
|---|---|
| No instant reverse | If the requested scancode is opposite to the current direction, reject it. |
| No danger | If danger(next) is true, reject the move. |
| No self-hit | If the next square is in the simulated bodySet, reject it. |
| Smileys optional | Reject smileys during clean search; allow them later only when the growth is offset by return safety, room access, or several reachable pickups. |
| Stones push | A stone can move one cell forward only if that cell is empty and not occupied by the simulated body. |
| Growth | Hearts, clubs, and smileys grow the body. Empty moves remove the tail first. |
| First move preserved | Every simulated branch remembers only the first real key to press once that branch wins. |
This is why the bot can reason about pushing stones and about its own tail moving away. It is not only finding a geometric path through the current picture; it is simulating what the snake's body will look like after each step on that path.
6. Food search
Food search is split into three planners. They all run breadth-first from the current head position, simulate the snake body and pushed stones, and preserve only the first key press from a winning route. The split exists because the bot needs different behavior in different moments: grab safe nearby food quickly, make cautious long routes when there is time, and force progress when it starts looping.
| Planner | When it runs | Limits | Main bias |
|---|---|---|---|
nearFood() |
Before every wider search. In cramped positions, the smiley pass can run before a distant clean route. | Depth 10 normally, 14 when 6 or fewer items remain; a shallow first pass before wider planning. | Very strong distance penalty, so safe nearby food wins. |
routeFood() |
The normal cautious route finder. | Depth 98 normally, 145 when 6 or fewer items remain; up to 3500 states normally and 7000 in endgame or urgent searches. | Survival first: route-out proof, tail reach, future exits, and open space beat shortness. |
pressureFood() |
When the bot is idle, looping, or when ordinary safe food searches did not help. | Depth 85-155 depending on endgame and urgency; up to 5000 states normally and 9000 when urgent. | Looser survival gates and a smaller distance penalty, to break circles without grabbing obvious traps. |
The endgame gets deeper search because the last few hearts and clubs are exactly where traps and long detours become most expensive. Earlier in the level, a shallower search is usually enough and keeps the bot fast. Smileys are still second-class targets: the bot prefers clean food, but it may eat one if that keeps the return route open or opens a useful cluster of future pickups.
The stone mazes need one extra trick. There, almost every remaining heart or club ends up walled behind stones, and plain distance treats a stone as a wall — so the food search finds nothing and the bot would just circle an open pocket. When that happens the pressure step falls back to a dig distance: a step-count that may pass through a stone cell when the cell just beyond it is empty, meaning that stone could be pushed. The bot then heads along that gradient and pushes its way toward the walled-off food instead of orbiting. The dig heading is only a pull, not a promise, so it is used for steering, never to override the trap checks below.
7. Trap checks
Reaching a heart is not enough. Most bad snake bots die because they follow the shortest route to food and discover too late that the food was in a cul-de-sac. Sneekie's bot therefore tests a candidate route with several survival signals.
Exit count
legalCount() asks how many moves will still be available immediately after eating. Zero exits reject the route; one-exit corridors are heavily penalized.
Reachable space
spaceInfo() flood-fills from the simulated head, respecting current direction, walls, body, stones, food, and danger.
Tail reach
The same flood fill records whether the simulated head can reach the tail. If the tail is reachable, the snake probably has a moving escape route.
Survival depth
survivalDepth() runs a small beam search to see how many future moves remain possible after the candidate is eaten.
Route out
escapeProof() simulates extra post-pickup moves and requires a path into open space or back toward the moving tail before the food route can win.
Room doors
On the room-grid layouts, the planner detects two-cell doors and checks that at least one door lane plus the cells immediately before and after it remain clear.
Return gates
Outside room levels, narrow passages are scored like fragile gates: a route that leaves only one thin way back to the tail pays a strategic debt.
Recent trail
Routes that revisit the same recent cells are penalized, with smaller penalties when that revisit opens tail reach or actually moves closer to food.
Local sweep
Every search measures the distance to the nearest reachable food and charges candidates per cell beyond that anchor; on the room layouts, leaving a room that still holds food adds a region debt. Nearby hearts get eaten before the bot wanders across the screen.
Region plan
On the line and room mazes the planner decomposes the maze into regions joined by narrow corridors and commits to sweeping the current region clean; food elsewhere pays a debt until then. Pockets stop being left half-eaten behind the growing body.
The minimum space requirement grows with the snake length: the longer the snake, the more room a candidate must leave behind. Normal food routes need a proved return path to the moving tail after the pickup; urgent pressure may use a very roomy disconnected region only as a last resort, and only in its smiley pass — a clean route never trades the return path away, because a −50 bridge that keeps the tail is always the better deal. Tiny regions, one-cell chokepoints, blocked room exits, degraded door lanes, fragile return gates, and early tail cutoffs are filtered before their short-term reward can win. The last word belongs to the self-seal guard: on every level, endgame included, a move that boxes the head into a pocket too small for the body is overridden toward the roomiest safe direction — and when the only way out is a smiley, the bot spends the −50 rather than entomb itself.
8. Fallback moves
The bot has four fallback behaviors around the normal safe food planners. They keep it from freezing, and the last one is a deliberate long-snake "follow the tail" discipline.
| Fallback | Purpose | Behavior |
|---|---|---|
breathingMove() |
Avoid pockets | Runs before distant food routes when the board is cramped or the snake is long. It prefers moves that preserve tail reach, future exits, room-door lanes, and open space. |
pressureFood() |
Break loops | Runs when idle >= 18 or the recent head trail shows repeated positions. It searches for food with lighter survival gates, first without smileys, then with smileys. |
tailChaseMove() |
Long-snake endgame | Once the body reaches about 80 cells on open boards, or about 24 cells on cramped maze layouts, it picks the safe move with the deepest guaranteed survival while keeping the tail reachable — in practice it trails its own tail and fills space rather than sealing itself in. Short snakes skip it. |
survivalMove() |
Short bridge | The lighter final fallback. It scores each immediately legal move by reachable space, exits, a global pull toward the nearest reachable food, tail-return room, smiley cost or credit, stone penalty, and straight-ahead preference. |
This gives the bot a survival instinct without letting that instinct become the first answer to every hard board. If the map says "do not eat yet", it may move into a larger open region briefly, but repeated head positions make the next decisions prefer food pressure. If pressure still cannot prove a route, the bot keeps moving only while the local fallback can find a survivable step.
9. Route scoring
Once a food candidate passes the safety gates, it gets a score. The WebAssembly planner also rolls promising candidates forward for up to three pickups, discounting later food but requiring each pickup to keep an escape proof. The weight table itself is tunable: an offline tuner can override every constant through a WebAssembly buffer and hill-climb better values against the game simulator. The normal route score is intentionally biased toward survival first, points second, and shortness only after those are satisfied:
score += survivalDepth * 5600
score += exits * 2400
score += reachableSpace * 16
score += escapeSpace * 8
score += escapeTailReach ? +44000 : 0
score += itemPoints * 150
score -= routeDistance * 260
score -= cellsBeyondNearestFood * 900 (capped)
score -= leavesARoomStillHoldingFood ? regionDebt : 0
score += returnPreservingSmileys * 8800
score += smileyEscapeBonus
score += preservedRoomDoorLanes
score -= smileysEaten * 10500
score -= blockedRoomDoorLanes
score -= stonesPushed * 55
score -= oneExitAfterEating ? 18000 : 0
The large tail-reach, survival-depth, route-out, room-door, return-gate, and recent-trail weights are the key anti-trap behavior. A slightly longer route that keeps access to the tail and proves an exit beats a short route into a tight pocket or another lap through the same stale cells. Clubs still matter because they are worth more points than hearts, but they do not override the survival tests. The route scorer also looks beyond the first pickup: reachable food clusters and follow-up pickups add value discounted by distance, so a rich cluster across the screen no longer outbids a heart sitting beside the head, while smileys still carry a cost unless they preserve access or open that cluster safely. A pickup tucked against a wall gets only a mild deferral that grows with body length, so pocketed hearts are cleared early, while the snake is still short and the pocket is still safe.
The two newer food planners deliberately use different weights:
| Planner | Important scoring difference |
|---|---|
nearFood() |
Applies a very large -distance * 6200 term. This is what stops the bot from ignoring safe food that is only a few moves away. |
pressureFood() |
Uses smaller trap and smiley penalties when urgent, plus a smaller distance penalty. This lets it escape tail loops by making real progress. |
survivalMove() |
Does not search for a full food route. It scores immediate legal moves by open space, a pull toward the nearest reachable food, exits, smiley cost, stone cost, and straight-ahead preference. |
Two more pressures shape the score. A smiley is worth −50, and on these levels every pickup spawns a fresh one, so the board fills with smileys; the planner only discounts eating one when it genuinely bridges toward reachable food, and otherwise keeps the full penalty so the bot does not nibble smileys for nothing. As a final check after a move is chosen, the bot refuses to step onto a smiley at all when a heart is reachable without crossing one, so it never pays the −50 for a pickup it could have reached a step around — unless that smiley is exactly what keeps the path back to the tail open. A deliberate escape smiley is never overridden, and the self-seal guard, which always runs last, may itself steer onto one when every clean direction would seal the body in. And because the level bonus drains every move and banks into the score when the level clears, a small extra distance penalty — scaled by the remaining bonus — kicks in once only a few items are left, nudging the bot to take the shortest finishing route rather than dawdle on the last pickups. Both are tie-breakers: they never override the survival gates.
10. Speed limits
The bot has to think between visible game moves. Several caps keep that work bounded:
danger()uses a generation-stamped cache so repeated hazard checks in one decision are cheap.nearFood()is deliberately shallow and runs before the wider searches.routeFood()can scan up to 3500 states normally and 7000 in endgame or urgent searches.pressureFood()can scan up to 5000 states normally and 9000 when urgent.survivalDepth()is a beam search: after each depth, WebAssembly keeps up to 128 states with the most immediate exits.escapeProof()keeps up to 160 post-pickup states per depth while proving that a food route has a way out.- Food search is shallower on ordinary mid-level decisions and deeper only near the end of a level.
- The page speed slider sets the target cadence between key presses; planner time is counted inside that cadence when it fits, and spare worker threads get larger route-search budgets than the baseline.
The slider snaps to the page's speed choices: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100. Low values make the bot wait longer between moves; high values press keys much faster, down to roughly 45 ms per move. Normal planning time is subtracted from that wait; if a search overruns the target cadence, the next key is sent immediately.
delay = round(45 + 375 * ((100 - speedValue) / 100) ** 1.6)
11. Win, death, restart
The bot page has seven selectable tabs: levels 2-8. A win flashes green, a failure flashes red, and both outcomes advance to the next tab. After level 8 it wraps back to level 2. The clear flash uses the same quick pulse cadence as the stuck flash before the next level loads.
| Condition | Result |
|---|---|
LEVEL === TARGET + 1 && LIVE > 0 |
Clean clear. Flash green and advance to the next listed level. |
LEVEL !== TARGET |
The game jumped away or ended. Treat as failure and flash red. |
BTEL stops advancing |
The snake is boxed or not moving. Keep making moves; the run ends in death only when no legal move remains, otherwise it ends in a clear. |
| Planner returns no move | Fall back to a random legal move. The stuck sequence, including the red flash, is used only when no legal move exists at all — the snake is really stuck. |
| No score gain for 120 decisions, or 24 with repeated head positions | Keep asking the WebAssembly planner, but set forceRisk. That makes recent-cell loop debt much heavier, gives the planner more time, and favors fresh cells, food pressure, stone digging, and return-path-preserving moves. Random legal fallback is used only if the planner returns no move. |
| 250 moves without eating a heart or club (smileys do not count) | The same forceRisk planner path, so a long dry run becomes a stronger Wasm escape search instead of a restart. It is no longer a death or restart trigger. |
Score is used for the idle signal instead of item count because late hearts can spawn clubs. Near the end this matters: the last club can require a long route without changing the score, so the bot keeps searching rather than reading the quiet score as failure. The separate 250-move pickup cap counts only real food — a −50 smiley is not progress, so nibbling smileys can no longer keep the cap from tripping — and when it trips it only switches the bot into forced Wasm escape planning, never a death or restart.
12. Limits and tradeoffs
The bot is deliberately practical rather than perfect. It does not solve the whole level as one enormous plan, because the board changes after every pickup, pushed stone, spawned club, and enemy tick. Instead it replans constantly from the live state. That makes it resilient and fast enough to watch.
- It simulates the snake body and pushed stones, but not a full multi-tick future of every enemy routine.
- It treats current and next-tick arrow danger conservatively, then recalculates after the real game advances.
- Its state hash is compact and meant for pruning, not for preserving every possible board detail.
- It may choose a smiley when clean food routes fail or when the board is cramped and the smiley keeps the return path open.
- The survival beam favors positions with many exits, so it can miss rare narrow routes that are technically safe but expensive to prove.
In short: the bot thinks like a cautious snake player. It wants nearby food first, but only when that food leaves breathing room. When the board gets tight it values open space and future exits, in a stone maze it digs toward walled food, and once it grows long it falls back to following its own tail to keep the deepest survival and fill space.