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.
main
- src
- static
- .gitignore 37 B · ~11 tok
- AGENTS.md 2.5 KB · ~636 tok
- forge.json 111 B · ~37 tok
- package.json 142 B · ~48 tok
- README.md 5.7 KB · ~1.5k tok
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.
/aboutstates 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
| Module | One concept |
|---|---|
src/chess.ts | the rules: 0x88 board, move generation, make/unmake, check, mate, draws, FEN |
src/san.ts | standard algebraic notation, both directions |
src/game.ts | a game is a start FEN plus moves; everything else is replayed |
src/pgn.ts | PGN in and out |
src/evaluate.ts | the evaluation, in centipawns |
src/engine.ts | alpha-beta, iterative deepening, killers, quiescence, four levels |
src/book.ts | the opening lines and the lookup built from them |
src/puzzles.ts | the puzzle set and the seeded chooser |
src/play.ts | book first, search after |
src/db.ts | the schema and the queries |
src/board.ts, src/html.ts, src/pages/* | rendering and escaping |
src/routes.ts | the table of paths, and the validation at each one |
src/body.ts | the ceiling on the bytes a request body may spend |
src/server.ts | the 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.