.-"""-.
.' '.
/ .---. \
| / o o \ |
| \ ‿ / |
\ '---' /
'. .'
'-...-'
local chess companion · live commentary engine · 100% offline
a low-latency chess overlay that watches your board, thinks with a fully-local stockfish 18,
- — rebuilt from a.c.a.s to run multi-threaded where a.c.a.s can't.*
#cba6f7 · #b4befe · #dabffc
avocado is a manifest v3 chrome extension. open a game on lichess or chess.com and it:
- sees every move the instant the board changes (no polling) — on chess.com it reads the board's
own game state directly (
wc-chess-board.game.getFEN(), ~0.7µs, always exact: right side-to-move, castling, en-passant), on lichess a zero-alloc piece scan — and ignores mere clicks/selections, - thinks with stockfish 18 — multi-threaded, simd, full nnue, entirely on your machine — searching to a depth ceiling and then stopping (a static position never pins your cores), and respawning itself if the wasm ever traps, so it can't get stuck,
- shows a mauve eval bar and ranked best-move arrows numbered by priority (#1 best, #2, #3), plus a translucent opponent-reply guess — and never draws a move that isn't legal on the board,
- talks — "wait, is that a juicer??", "oh no, my queen!!", "absolute positional dominance, buddy" — driven by the real eval swing and tactical motifs. commentary lives exclusively in the pip window (your moves on one line, the opponent's on the other) so the site itself stays clean,
- calls the finish — checkmate, stalemate, flagging on time.
- pops out — a
⧉button opens a picture-in-picture companion window (eval, numbered arrows, both commentary lines, and a toggleable board drawn from the live position) that floats over any tab.
Warning
fair play. engine hints during a rated game against a human are cheating on chess.com and lichess and can get you banned. avocado keeps best-move arrows off by default in live human games (the eval bar and commentary still run). there's an opt-in toggle in the popup. avocado never auto-moves, never simulates input, and ships no detection-evasion features.
a.c.a.s is the inspiration and it's great — but its architecture caps it. avocado clears that ceiling
on every axis. run the demo yourself: npm run bench.
board → fen (per move)
avocado ██████████████████████████████████ 4.96m ops/s
a.c.a.s ████······························ 600k ops/s → 8.3× faster
move detection (diff two positions)
avocado ██████████████████████████████████ 19.0m ops/s
a.c.a.s █································· 471k ops/s → 40× faster
ipc serialization (per eval frame)
avocado ██████████████████████████████████ 5.29m ops/s
a.c.a.s ██████████████████················ 2.85m ops/s → 1.9× faster
move-detection allocations avocado 0 objects · a.c.a.s ~9 objects
move-detection latency avocado ~0 ms · a.c.a.s up to 250 ms poll
move validation and the click/flicker guard run outside these hot paths — real moves still emit with zero added latency; only the analyse/ipc/parse loops are measured above.
the benchmark runs avocado's real modules against a faithful port of a.c.a.s's actual code
(getBasicFen, getBoardSquareChangeAmount, its json comm bus, its 250ms poll — all cited to line
numbers in a.c.a.s/acas.user.js), on the same positions. it's not a strawman.
| axis | a.c.a.s | avocado |
|---|---|---|
| engine threads | single-threaded wasm (hosted gui can't be cross-origin-isolated) | multi-threaded simd, threads = cores−1 |
| move detection | string-fen diff, ~9 allocs/move | typed-array mailbox diff, zero alloc |
| move latency | mutationobserver + 250ms poll fallback | pure event-driven, microtask-coalesced |
| ipc | json over a message bus (~250ms waits) | packed strings + a lock-free SAB ring |
| distribution | userscript + hosted gui tab + electron app | one self-contained extension |
| engine assets | 66–108mb refetched over the network | vendored once, loads from disk |
stockfish 18 ██████████████████████████████████ ~3653 ◀ what avocado runs
stockfish 17 ████████████████████████████████·· ~3607 old browser / a.c.a.s default
stockfish 16 ██████████████████████████████···· ~3561
carlsen peak ███······························· ~2882 strongest human ever
avocado runs stockfish 18 (the sfnnv10 "threat inputs" net, +46 elo over sf17, released
2026-01-31). and because it's multi-threaded, nodes/sec scale ~linearly with your cores — so at
equal time it searches deeper than the single-threaded build a.c.a.s and most in-browser tools are
stuck on. same engine family, more compute, stronger play.
engine builds are popup-selectable:
| build | threads | net | size | use |
|---|---|---|---|---|
stockfish-18-lite.js |
multi | embedded | ~7mb | default |
stockfish-18-lite-single.js |
single | embedded | ~7mb | low-core / non-isolated fallback |
stockfish-18.js |
multi | full net | ~113mb | max strength (vendor it first) |
lichess / chess.com page (content script, isolated world)
capture.ts ─ mutationobserver → wc-chess-board.game.getFEN() (chess.com) │ mailbox diff (lichess) → fen
overlay.ts ─ canvas eval bar / ranked multi-pv arrows / status pill (raf only)
commentary ─ eval delta + bitboard motifs → priority state machine → pip window
│ chrome.runtime port — packed strings, never json
service-worker.ts (router + offscreen lifecycle)
│ port relay
offscreen document (crossoriginisolated via coop/coep manifest keys)
engine-host.ts ─ drains the eval ring, relays frames
│ postmessage (commands) ··· SAB ring buffer (eval frames)
engine-worker.ts ─ bridges uci (serialized `go depth N`, self-stops + auto-respawns), parses info zero-copy
└─ stockfish-18-lite.js (N threads + simd, net embedded)
the offscreen document is the trick: an extension page can set coop/coep and become
crossoriginisolated, which is exactly what unlocks sharedarraybuffer + threaded wasm — the thing
a.c.a.s's github-pages gui can't have. the SAB ring carries the high-frequency eval frames so we never
pay a structured-clone per info line; commands are one-per-move, so plain postmessage is fine there.
never stuck. stockfish traps the wasm if you hand it a new position mid-search, so the worker
serializes: a new move only stops the running search, and the next position/go waits for the
bestmove. each search runs go depth N (not go infinite) so it ends itself and a quiet board drops
to idle. and if the engine ever crashes anyway, it respawns and re-analyses the live position — so it
recovers instead of hanging on "engine waking…", even joining an already-active game mid-blitz.
picture-in-picture. the ⧉ companion window uses the modern document picture-in-picture api
(chrome 116+) — a real dom window we draw into directly, rAF-throttled and allocation-free. a.c.a.s
renders to a canvas, captures a MediaStream, pipes it through a <video>, and requests video-pip;
that encode→decode→present path adds frames of latency we skip entirely. the pip board is drawn from
the position avocado already tracks (unicode glyphs), so it needs none of chess.com's sprites to render.
npm install
npm run vendor # one-time: pull stockfish 18 wasm into vendor/stockfish/
npm run build # → dist/
npm test # 57 unit tests (board, protocol, ring, commentary, side-to-move, real-engine-output parser)
npm run bench # the avocado vs a.c.a.s speed demo abovethen: chrome://extensions → enable developer mode → load unpacked → pick dist/.
open lichess.org/analysis or a chess.com bot game and start playing.
npm run watch rebuilds on change (reload the extension to pick up content/worker edits).
src/shared/ board.ts (bitboards · 16-bit moves · single-alloc fen) · protocol.ts · theme.ts
src/content/ main.ts · capture.ts · overlay.ts · pip.ts · adapters/{lichess,chesscom}.ts · commentary/*
src/offscreen/ engine-host.ts · engine-worker.ts · ring-buffer.ts · uci-parse.ts
src/background/ service-worker.ts
src/popup/ popup.html · popup.ts
bench/ compare.ts (real modules vs faithful a.c.a.s port) · acas-model.ts
vendor/stockfish/ vendored engine — committed, runtime is 100% local
a.c.a.s/ is the upstream reference source (gitignored, not part of the build).