Skip to content

click-b8/slither-bot

Repository files navigation

slither.io bot

Heuristic autonomous bot for slither.io. Drives a real Chromium browser via Playwright; reads game state from the page's JS globals; steers by moving the OS mouse.

Setup (≤ 5 min)

# 1. Install dependencies
npm install

# 2. Download the Chromium Playwright uses (first time only)
npx playwright install chromium

# 3. Run
npm start

Requires Node.js ≥ 20.

What it does

  1. Opens https://slither.io in a visible Chromium window.
  2. Fills in the nickname, dismisses any consent banner, clicks Play.
  3. Every tickIntervalMs (~50 ms): reads the player's position/heading, the food array, and all other snakes from window.* globals, then picks a target angle using:
    • Avoid — any enemy segment inside avoidanceRadiusPx creates a blocked angular band; if the desired heading is inside one, slide to the nearest clear angle.
    • Food — sum size / √distance into 36 angular buckets within foodRadiusPx, steer at the best bucket.
    • Idle sweep — default gentle turn so we never hold perfectly straight into empty space.
  4. On death (window.slither becomes null), logs length, clicks Play again, repeats up to maxRestarts times. Prints a session summary.

Config

All tunables live in src/config.ts. No magic numbers in logic files.

Field Default Meaning
nickname claude-bot Shown on the snake in-game
url https://slither.io Game URL
maxRestarts 5 Lives per session
tickIntervalMs 50 Decision loop period
avoidanceRadiusPx 220 Enemy segments inside this radius create blocked angular bands
foodRadiusPx 500 Food outside this radius is ignored
idleSweepRadians 0.03 Per-tick drift of the idle heading fallback
headless false Set true to run without a visible window
slowMoMs 0 Playwright slowMo — useful when debugging
logLevel debug debug prints every 20 ticks; info is summaries only

Project layout

src/
  config.ts      all tunables
  logger.ts      level-filtered logger
  launcher.ts    opens slither.io, enters nickname, clicks Play, handles respawn
  state.ts       typed reader over window.slither / slithers / foods / view_*
  steering.ts    canvas-relative mousemove primitive
  decision.ts    pure decide(state) → target angle (avoid / food / idle)
  index.ts       main loop: spawn → tick → die → respawn → summary
  inspect.ts     optional discovery probe (npx tsx src/inspect.ts)

Scripts

  • npm start — run the bot end-to-end (default).
  • npm run inspect — launch a quick browser session and dump the shape of window.slither, window.slithers, window.foods etc. Use this first if a game update breaks state extraction.
  • npm run typechecktsc --noEmit.
  • npm run lint — ESLint over src/.

Known fragilities

Velocity units — slither.sp is units/tick, not units/second

This bug silently crippled collision lookahead in v1. The v1 projection did vx = cos(e.angle) * e.speed and used it with dt = (tickIntervalMs / 1000) * lookahead. That treats e.speed as units-per-second, but sp is actually units-per-tick at the game's ~30 fps, so the projection was roughly 60× under-weighted (1 tick ≈ 1/30 s not 1 s) and the ghost-band for head lead-targeting sat ~2 world units ahead of the current position — barely moved at all. Enemy heads could close on us inside the blind window without any band widening.

Fix in v2 (src/state.tsStateTracker.read): we now measure vx/vy from actual world-position deltas across real tick intervals (dt = (now - prev.t) / 1000), so the units are correct by construction (world-units per second). decide() uses this measured velocity with the measured dt. Don't reintroduce cos(angle) * sp as a seconds-based velocity — it's only meaningful as units/tick for the current frame.

Other fragilities

  • Minified variable names. The bot reads window.slither, window.slithers, window.foods, window.view_xx, window.view_yy, window.gsc, and the per-snake fields xx, yy, ang, wang, sp, sc, pts, id, nk. If the game is re-minified with different names, all state extraction will break silently. Re-run npm run inspect to rediscover; update src/state.ts. The slitherPtsSample entries may include stale slots flagged dying: true — the reader filters these out.
  • DOM selectors. Landing page uses #nick and .btnt.sadg1 for the Play button. If redesigned, src/launcher.ts needs updating.
  • Consent banner. Not seen at the test geo; defensive dismissal tries "AGREE" / "Accept" buttons across frames. If a new CMP ships, add a case to dismissConsent().
  • Map boundary. The decision module does not know about the circular world edge. A bot headed straight at the edge with no enemies nearby will die there. Low priority for empty/low-density servers.
  • No speed-boost (W / click). The bot never sprints. Length grows purely by eating.
  • Single-server assumption. No server selection — uses whatever slither.io assigns on load.
  • 33 ms tick floor. page.evaluate round-trip through Playwright is ~30 ms per call; with steering + decision overhead, the achievable tick interval floors around 33 ms (matches slither.io's ~30 fps). Getting below that would need Chrome DevTools Protocol streaming instead of per-tick evaluate. See obsidian/future-work.md.
  • Encirclement detector saturates. The angular-bucket corridor metric hits 0° on every life — once enemies are near, every 15° bucket has at least one segment in it. The metric is still emitted to CSV for observability but is NOT used for steering (the override/cautious variant was tried in v2 and removed — see obsidian/iteration-log.md).

Disclaimer

Automating slither.io may violate the site's Terms of Service. This code is for local experimentation and educational use only. Run at your own risk; do not deploy to rank-manipulate or hammer the game's infrastructure.

Observed performance

On 60-sec rolling samples during development: survived 47–86 s per life, final length 40–60. Peaks depend heavily on server crowding.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors