Sneekie

How the live bot thinks

The Live page does not play a recording. It loads the real game in an <iframe>, injects a JavaScript bot into that running game, and lets the bot press the same arrow-key scancodes a player would press. The bot is a small planner: every move it rebuilds the snake state, prioritizes nearby food, rejects tempting routes that become traps, watches for tail-following loops, and falls back to pure survival when no safe food path exists.

Important distinction. The actual 1988 game in Play stays faithful to the BASIC source. The bot is only a modern addition in live.html; it reads the port's live variables and sends keys, but it does not change the game rules.

1. Where the bot runs

The live page has one iframe whose source is index.html. On every iframe load, live.html appends CSS that hides the surrounding game chrome, then runs the bot string with eval() inside the iframe:

cell.frame.contentWindow.eval('var IDX=0,TARGET=' + activeLevel + ';' + BOT);

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 code evaluated in the same script realm, but they are not ordinary window properties. Injecting the bot into the iframe 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.

Real input

It sends DOS-style extended-key strings: up ' H', down ' P', left ' K', right ' M'.

Live status

It reports score and remaining items to the parent page with parent.botStatus(...), and reports success or failure with parent.botEnd(...).

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 valueMeaning to the botPlanning effect
32Empty cellFreely enterable.
3 HeartFood worth 10 points; grows the snake.
5 ClubFood worth 25 points; grows the snake.
1 SmileyNormally avoided; allowed only in escape mode.
10 StonePushable if the cell behind it is empty.
24, 26, 27 ↑→←Moving arrowsTreated as fatal, including cells they are about to enter.
219Snake headUsed by the game for collision; the bot also tracks the head through T[BTEL].

For simulated routes, the bot keeps an overlay map called cells. This overlay records only changes caused by imaginary moves, such as a pushed stone or an emptied tail cell. If a cell is not in the overlay, the bot reads the real game screen with peek(). That keeps route simulation cheap without cloning the whole 4000-byte VRAM array for every branch.

3. One decision cycle

Every bot move follows the same ladder. The first strategy that returns a direction wins; the bot presses that key, waits according to the speed slider, then recalculates from the new live game state.

1. Reset danger cache

Arrow danger is recalculated for this tick, with memoization so repeated checks are cheap.

2. Nearby clean food

First try a shallow search for close hearts or clubs, without eating smileys.

3. Wider clean route

If no nearby pickup is safe, run the deeper food planner without smileys.

4. Smiley escape routes

If clean routes fail, repeat nearby and wider food searches with smileys allowed.

5. Loop pressure

If the head is repeating positions or the score has been idle, push toward food instead of circling the tail.

6. Follow tail

Only when not under loop pressure, try to reach the moving tail and buy space.

7. Survive locally

If all planners fail, pick the best legal one-step survival move.

const decide = (idle, looping) => {
  resetDanger();
  const urgent = idle >= 45 || looping;
  return nearFood(false) ?? routeFood(false) ?? nearFood(true) ?? routeFood(true) ??
    (urgent ? (pressureFood(false, true) ?? pressureFood(true, true)) : null) ??
    (!urgent ? tailFirst() : null) ??
    pressureFood(false, urgent) ?? pressureFood(true, urgent) ??
    survivalMove();
};

The looping flag comes from a short head-position trail. 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 before the red stuck timer has to fire.

4. Arrow danger model

The bot treats arrow levels differently because a square can be empty now and fatal one tick later. The danger(offset) function marks a destination unsafe if an arrow is already there, if an arrow will step into it on the next enemy update, or if a wrapping arrow will reappear there.

Current cellpeek(o) is 24, 26, or 27.
Up arrowsAn below the square will move upward into it.
Right arrowsA to the left will move right into it.
Left arrowsA to the right will move left into it.
Wrap edgesSpecial checks cover arrows that jump from the far edge back to the start.

This is a one-tick hazard model, not a full future enemy simulation. That is intentional: the bot moves, the real game advances, and the next decision recalculates danger from the fresh screen. That gives good arrow avoidance without making every search branch simulate all enemy routines.

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.

RuleHow the bot applies it
No instant reverseIf the requested scancode is opposite to the current direction, reject it.
No dangerIf danger(next) is true, reject the move.
No self-hitIf the next square is in the simulated bodySet, reject it.
Smileys optionalReject smileys during normal search; allow them during escape search.
Stones pushA stone can move one cell forward only if that cell is empty and not occupied by the simulated body.
GrowthHearts, clubs, and smileys grow the body. Empty moves remove the tail first.
First move preservedEvery 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.

PlannerWhen it runsLimitsMain bias
nearFood()Before every wider search, first without smileys and then with smileys if needed.Depth 9 normally, 12 when 6 or fewer items remain; at most 260 states.Very strong distance penalty, so safe nearby food wins.
routeFood()The normal cautious route finder.Depth 78 normally, 115 when 6 or fewer items remain; at most 950 states; can stop after 28 food candidates once a good route exists.Survival first: tail reach, future exits, and open space beat shortness.
pressureFood()When the bot is idle, looping, or when tail-following did not help.Depth 70-125 depending on endgame and urgency; 720-1050 states; 22-38 checked food candidates.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 only allows them after clean heart/club searches fail.

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 three different 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.

The minimum space requirement grows with the snake length: the longer the snake, the more room a candidate must leave behind. In the final items, the bot demands more space and a deeper survival horizon.

8. Fallback moves

The bot has three fallback behaviors after the normal safe food planners. They keep it from freezing, but they are ordered carefully so it does not spend too long following its own tail while food is available.

FallbackPurposeBehavior
pressureFood()Break loopsRuns when idle >= 45 or the recent head trail shows repeated positions. It searches for food with lighter survival gates, first without smileys, then with smileys.
tailFirst()Buy timeRuns a BFS toward the current tail only while the bot is not under loop pressure. Following the tail is safe as a short bridge, but it is skipped once it starts looking like a circle.
survivalMove()Last resortScores each immediately legal move by reachable space, tail reach, exits, food, smiley penalty, stone penalty, and straight-ahead preference.

This gives the bot a survival instinct without letting that instinct become endless circling. If the map says "do not eat yet", it can follow the tail briefly or move into a larger open region, but repeated head positions make the next decisions prefer food pressure instead.

9. Route scoring

Once a food candidate passes the safety gates, it gets a score. The normal route score is intentionally biased toward survival first, points second, and shortness only after those are satisfied:

score = tailReach ? +100000 : 0
score += survivalDepth * 5600
score += exits * 2400
score += reachableSpace * 16
score += itemPoints * 150
score -= routeDistance * 260
score -= smileysEaten * 1200
score -= stonesPushed * 55
score -= oneExitAfterEating ? 18000 : 0

The large tail-reach and survival-depth weights are the key anti-trap behavior. A slightly longer route that keeps access to the tail beats a short route into a tight pocket. Clubs still matter because they are worth more points than hearts, but they do not override the survival tests.

The two newer food planners deliberately use different weights:

PlannerImportant 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, tail reach, exits, direct food, smiley cost, stone cost, and straight-ahead preference.

10. Speed limits

The bot has to think between visible game moves. Several caps keep that work bounded:

The slider maps 0–100 onto a nonlinear delay. At low values the bot waits longer between moves; at high values it presses keys much faster, down to roughly 45 ms per move.

delay = round(45 + 375 * ((100 - sliderValue) / 100) ** 1.6)

11. Win, stuck, restart

The live page has 16 selectable tabs: levels 1-8 and 25-32. A win flashes green, a failure flashes red, and both outcomes advance to the next tab. After level 32 it wraps back to level 1. The flash pulses five times, one second apart, before the next level loads.

ConditionResult
LEVEL === TARGET + 1 && LIVE > 0Clean clear. Flash green and advance to the next listed level.
LEVEL !== TARGETThe game jumped away or ended. Treat as failure and flash red.
BTEL stops advancingThe snake is boxed, dying, or not moving. Flash red after the stall threshold.
No safe moveAll planners failed. Flash red.
No score gain past the dynamic idle limitProgress stalled. Flash red. The limit is 160 moves normally, then 210, 280, 400, or 520 moves as the remaining item count drops to 8, 4, 2, or 1.

Score is used for the idle timer instead of item count because late hearts can spawn clubs. Near the end this matters: the last club can require a long safe path without changing the score, so the dynamic timeout gives it more room before declaring the bot stuck.

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.

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 its tail, open space, and future exits more than the nearest unsafe points.