trevato/chess

Public: anyone can read this; the crew can change it.

Chess against an engine written by hand: legal move generation with castling, en passant and promotion, check and mate detection, an alpha-beta engine with iterative deepening and a small evaluation, four difficulty levels, PGN import/export, an opening-book of a few hundred lines, and a puzzles mode from a seeded set. The board is keyboard and touch playable; games are saved and resumable by URL.

Running 243864e https://chess--forge-trevato-apps.open-platform.sh in production 21d 14h

README.md

chess

Chess against an engine written by hand. The rules, the notation, the search, the evaluation, the opening book and the puzzles are all in this repository: there is no chess library and no dependency of any kind beyond Bun.

PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts   # run it here
bun test                                                # the check every pull runs
FORGE_APP_DATA=./data bun run src/tty.ts                # the terminal interface

What you can do

  • Play at one of four levels. Start a game on /, choose a side (or take

both), and the game gets its own address: /g/<22 characters>. That link saves and resumes it — bookmark it, or open it on your phone.

  • Move by tapping a piece and then a square. Selecting is a GET, moving

is a POST; both are plain links and buttons, so the board works with a keyboard (<kbd>Tab</kbd>, <kbd>Enter</kbd>), with a screen reader, and with a finger. static/board.*.js adds arrow-key navigation and nothing else — turn JavaScript off and the game still plays.

  • Promote by choosing from four buttons; the pawn waits until you do.
  • Take back a move, flip the board, print the page (there is a print

stylesheet), and export the game as PGN from /g/<id>/pgn.

  • Import a PGN on the front page and carry on from where it ends. If a

token cannot be read the import keeps everything before it and says so.

  • Solve puzzles at /puzzles. One is the day's, picked by seeding a

generator with the date, so everybody gets the same one.

  • /about states how the engine works and what it assumes.

What is real and what is not

Everything on the page is real. The engine actually searches; the scores in the evaluation chart are the ones it returned; the opening book is 262 named lines compiled from SAN at startup; the puzzles are a fixed set with their solutions written down. Nothing is simulated, and nothing is fetched from anywhere.

Two things are deliberately simple, and the app says so on /about:

  • A puzzle accepts the move stored in its solution. Another move that also wins

is not credited.

  • The engine thinks on the request thread, so its reply takes as long as the

level's budget (30 ms to 900 ms). Every other request answers in single-digit milliseconds.

How it is put together

ModuleOne concept
src/chess.tsthe rules: 0x88 board, move generation, make/unmake, check, mate, draws, FEN
src/san.tsstandard algebraic notation, both directions
src/game.tsa game is a start FEN plus moves; everything else is replayed
src/pgn.tsPGN in and out
src/evaluate.tsthe evaluation, in centipawns
src/engine.tsalpha-beta, iterative deepening, killers, quiescence, four levels
src/book.tsthe opening lines and the lookup built from them
src/puzzles.tsthe puzzle set and the seeded chooser
src/play.tsbook first, search after
src/db.tsthe schema and the queries
src/board.ts, src/html.ts, src/pages/*rendering and escaping
src/routes.tsthe table of paths, and the validation at each one
src/body.tsthe ceiling on the bytes a request body may spend
src/server.tsthe security floor, the session cookie, static files

Correctness is measured, not asserted. bun test runs perft from five standard positions and compares the node counts with the published figures (197,281 nodes at depth four from the initial position, 97,862 from Kiwipete at depth three, and so on). Castling, en passant and promotion cannot be quietly wrong and still produce those numbers. Every book line is replayed and must be legal to the last move; every puzzle's solution is replayed, and one that claims mate must end in mate.

Security. Every input is validated where it arrives: a square is matched against [a-h][1-8], a level against four known keys, a game id against 22 base64url characters, and a move must be found in the list of legal moves or it does not happen. Every output goes through escape. src/server.ts puts the same headers on every response — CSP, nosniff, referrer policy, HSTS over https — and refuses a POST that is not same-origin. No route reads an unbounded body: src/body.ts counts the bytes as they arrive and answers 413 past 256 KiB (262,144 bytes, which holds the 200,000-character PGN import with room for multipart framing), so a chunked upload with no Content-Length is cut off rather than buffered and parsed. HEAD is answered exactly as GET, minus the body, so a health check on / sees 200. The session is an HttpOnly cookie and decides only which games are listed on the front page; a game's address is its credential.

Speed. The first paint is server-rendered HTML with an inline stylesheet; the only script is 30 lines, named by content and cached for a year. The opening book costs about 100 ms to compile and is built after the server starts listening, so no request waits for it.

The terminal

The app has a terminal: the exact command is in FORGE_APP_SSH when the app runs, and the app prints that variable verbatim on /about (when it is unset, /about says the terminal is off). It opens src/tty.ts on a real pty over the same database: j/k and o to open a game, n for a new one, then type a move in algebraic or long algebraic (e4, Nf3, e2e4), u to take back, q to leave. A game played there is the same game the web page shows.

Environment

The forge sets PORT, FORGE_APP_DATA, FORGE_APP_ENV (production or preview), FORGE_APP_URL and FORGE_APP_COMMIT when it starts the app. Everything the app keeps lives in one SQLite file under FORGE_APP_DATA, which is what makes production data and preview data two different things. forge.json says how to run it.

↑↓ navigate ↵ open esc close

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs