← brag

~projects

Side quests. Built because the question was interesting, not because the answer mattered.

sepentia — chess engine

A chess engine running entirely in the browser. UCI-style search: iterative deepening, alpha-beta pruning, quiescence search, a transposition table that actually works this time, and a hand-tuned evaluation that knows what a king should be doing at each phase of the game.

Play it at gaurav.bar/things/sepentia · How it was built: gaurav.bar/things/sepentia/architecture

The engine started two years ago as a Python script running in a pygame window on my laptop. To play it you had to clone the repo, install Python, install pygame, and run a file. Nobody did.

one ambiguity

The Python version was slow — 5 to 10 seconds per move, UI frozen solid while it thought. The natural instinct was to optimize: profile the hot loops, try numpy, maybe rewrite the inner search. I spent time going down that path.

The actual answer wasn't to fix Python. It was to change the runtime.

V8 — the JavaScript engine inside every modern browser — is a JIT compiler. When it sees the same function called millions of times (chess search is exactly that), it compiles it to native machine code. CPython is an interpreter that reads bytecode fresh on every operation. The algorithms are identical in both versions. Nodes per second jumped from roughly 10–30K to 100–500K. Not from better algorithms. Just from a better runtime.

The ambiguity was: is this a Python is slow problem, or a runtime problem? Those have completely different solutions. The first sends you optimizing the same code. The second sends you to TypeScript.

one trade-off

Web Workers. A Web Worker does not make code run faster — it runs code on a separate thread so the UI doesn't freeze. Without it, the board locks up every time the AI thinks. With it, you get a smooth UI while the engine churns.

The cost: one core running the engine, the other seven sitting idle. True multi-core parallel search requires shared memory between workers — a real project in itself. I chose UI responsiveness over raw compute. For a side project in a browser, that was the right call. But it's worth being honest about: the web worker didn't make the engine stronger. It made the experience usable.

one mistake

The transposition table existed in the original engine. All the code was there. The logic was right. It just wasn't being called.

The game loop had two search functions: one that used the TT, one that didn't. It always called the one that didn't. The smarter version was live in the repo, fully implemented, completely unreachable — dead code standing at a window, never getting orders.

The engine re-analyzed identical positions thousands of times per move. It wasn't slow. It was wrong about what it had already seen. Before you optimize, check that what you built is actually running.

one review comment

"The king's position contributes zero to the score."

That's it. Someone reading the evaluation function noticed that the most important piece on the board had no positional awareness at all. The engine could see material, could see pawn structure, could see knight outposts — but had no model of whether the king was safely castled or walking into an open file.

What changed: evaluation isn't arithmetic. It's a model of what you understand about chess. If the king isn't in the model, the engine plays like someone who memorized tactics but never learned to castle. Adding tapered king tables — different tables for middlegame and endgame, interpolated by material — was one of the biggest single ELO jumps in the rebuild.

A review comment changed the scope of the problem. Not a bug fix. A lens shift.

← brag