Skip to content

Commit facf2a2

Browse files
committed
Add chess-widget entry
1 parent a170d83 commit facf2a2

6 files changed

Lines changed: 143 additions & 0 deletions

File tree

_projects/chess-widget.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
title: "chess-widget"
3+
date: 2026-05-28
4+
description: "An embeddable chess game analysis widget. PGN in, annotated replay with Stockfish 18 eval, move classification and bookmarks out. Built to scratch a chess.com itch."
5+
tags:
6+
[
7+
"chess",
8+
"stockfish",
9+
"rails",
10+
"postgres",
11+
"web-components",
12+
"no-build",
13+
"javascript",
14+
"cucumber",
15+
]
16+
---
17+
18+
# Chess Widget
19+
20+
I started playing chess "seriously". Decent against bots, still pretty low aura against humans, practicing. I wanted to embed annotated games on my page. The chess.com share widget is nice but not customizable and ties you to their player. I could not find a widget that did what I wanted, so I built my own.
21+
22+
What I wanted from it:
23+
24+
- Simple. Paste PGN, get a self-contained block I can drop into any HTML page.
25+
- Customizable via CSS. People should be able to embed it in their blog and have it match their theme.
26+
- Real analysis: blunders, mistakes, checkmates, move-by-move eval, sound on moves.
27+
- Jump to key moves. Bookmarks for every blunder and mistake so you skip the boring parts.
28+
- In control. No dependency on an external service at runtime, stateless, embeddable anywhere.
29+
30+
Repository: [https://github.com/cb341/chess-widget](https://github.com/cb341/chess-widget)
31+
32+
### chess.com's widget
33+
34+
![The chess.com share widget. Looks fine, but no way to customize the board, no eval chart, and you are stuck on their player.](/assets/projects/chess-widget/chess_com-widget.webp)
35+
36+
### lichess embed
37+
38+
![Lichess study embed. Functional but takes you out of the page, no eval-over-time, no move classification on top.](/assets/projects/chess-widget/lichess.webp)
39+
40+
### ChessBase / Fritz embed
41+
42+
![ChessBase Fritz embed. Heavier UI, locked to their styling, not something you would drop into a personal blog post.](/assets/projects/chess-widget/embedfritz.webp)
43+
44+
### chess.cb341.dev widget (MINE)
45+
46+
![My widget. Newspaper-style board, eval-over-time chart, bookmarks panel for every blunder and mistake in the game.](/assets/projects/chess-widget/chess_cb341_dev-widget.webp)
47+
48+
## Architecture
49+
50+
Two parts, cleanly separated:
51+
52+
- **Widget.** A custom element `<chess-widget>`, no build step, no framework. Reads a precomputed analysis JSON payload embedded in the page and renders the board, stepper, eval chart and bookmarks. Stateless. Makes zero API calls at runtime. Does not depend on `chess.cb341.dev`. The host page brings the payload, the widget renders it.
53+
- **Analysis.** A Rails service that takes a PGN, replays SAN to FENs, shells out to a local [Stockfish 18](https://github.com/official-stockfish/Stockfish) binary for per-position evaluation, classifies moves and emits the widget-ready JSON.
54+
55+
PostgreSQL is purely for bookkeeping on the analysis side. Games are keyed by a deterministic SHA-256 prefix of the PGN, so submitting the same game twice is a no-op. The widget itself has no idea Postgres exists.
56+
57+
The widget could eventually opt in to live calls against `chess.cb341.dev` (on-the-fly analysis from a PGN string), but that is not a dependency, it is a future enhancement.
58+
59+
## Hosting
60+
61+
- [chess.cb341.dev](https://chess.cb341.dev). Rails analysis app, hosted on [deplo.io](https://deplo.io)
62+
- [Stockfish 18](https://github.com/official-stockfish/Stockfish) runs alongside Rails in the same container
63+
- PostgreSQL for bookkeeping of analyzed games
64+
65+
## Compression
66+
67+
The analysis JSON for a 45-move game is not tiny. Embedding it raw in the HTML is wasteful, so the payload is gzipped, base64 encoded and dropped into a `<script type="application/x-gzip-json">`. The widget decompresses it in the browser using the built-in `DecompressionStream` API. No external libraries, works everywhere modern.
68+
69+
```js
70+
async function decompressJson(base64) {
71+
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
72+
const stream = new Blob([bytes])
73+
.stream()
74+
.pipeThrough(new DecompressionStream("gzip"));
75+
const text = await new Response(stream).text();
76+
return JSON.parse(text);
77+
}
78+
```
79+
80+
## Testing
81+
82+
High-level Cucumber specs drive both the analysis service and the rendering paths. I do not care much about low-level unit tests here. The value is in the end-to-end shape of the response and the rendered output.
83+
84+
```gherkin
85+
Feature: Server-side chess analysis
86+
The analysis app turns a pasted PGN into text analysis and widget-ready JSON.
87+
88+
Background:
89+
Given the sample Chess.com PGN from the project prompt
90+
91+
Scenario: Analyze the sample game
92+
When I submit the PGN to the analysis app
93+
Then I see progress while Stockfish analysis is running
94+
Then the response includes parsed game metadata
95+
And the response includes 46 board positions
96+
And the response includes 45 analyzed moves
97+
And move 11 for White is marked as castling
98+
And move 23 for White is marked as checkmate
99+
And every position includes an evaluation bar
100+
101+
Scenario: Render a plain text analysis
102+
When I submit the PGN to the analysis app
103+
Then the text analysis includes Unicode chess pieces
104+
And the text analysis includes compact annotations such as "!", "?!", "??", or "!!"
105+
And the text analysis includes a text evaluation bar
106+
107+
Scenario: Render a Markdown analysis response
108+
When I submit the PGN to the analysis app
109+
Then the Markdown analysis includes Unicode chess pieces
110+
And the Markdown analysis includes compact annotations such as "!", "?!", "??", or "!!"
111+
And the Markdown analysis includes a move table
112+
And the Markdown analysis includes a Markdown-safe evaluation bar
113+
114+
Scenario: Use Stockfish when available
115+
Given Stockfish 18 is available in the container
116+
When the analysis app evaluates a position
117+
Then it asks Stockfish for a UCI evaluation
118+
And it falls back to material evaluation if Stockfish fails or times out
119+
120+
Scenario: Review saved analyses
121+
Given at least one game has been analyzed
122+
When I open the analyses index
123+
Then I see the saved games
124+
And I can open a saved game
125+
And I can inspect its metadata, moves, evaluations, and board snapshots
126+
```
127+
128+
The Stockfish fallback scenario is the one I care about most. If the binary is missing or hangs, the service returns a material-only evaluation and flags the result as approximate, instead of failing the request outright.
129+
130+
## Agentic Workflow
131+
132+
First project I built almost entirely in an agentic manner. I wrote the spec in Markdown and mostly stayed high-level: architecture, route layout, widget UX, what the analysis payload should look like. I nudged the LLM into the decisions that mattered. The database schema, the shape of the widget's custom element API, how bookmarks should hang off the move list without coupling to it. I did not read every line of code.
133+
134+
## Future Development
135+
136+
- Comments in analysis mode. Annotate a move and the widget will display the comment when you land on that ply.
137+
- Threat lines. Show the engine's suggested continuation as an overlay on the board.
138+
- Puzzle mode. Pick a position from a game and let the reader try to find the best move before revealing it.
139+
- Piece and board image customization. Swap piece sets and board themes via CSS variables, no rebuild.
140+
141+
## Demo
142+
143+
Live demo at [chess.cb341.dev/about](https://chess.cb341.dev/about). A game not particularly proud of but shows what the widget is about.
59.5 KB
Loading
39.4 KB
Loading
58.3 KB
Loading
94.1 KB
Loading
115 KB
Loading

0 commit comments

Comments
 (0)