-
-
Set up a position, ask the coach questions, and practise ideas directly on the board.
-
Zugzwang’s core loop: analyze a game, find the key decision, explain your intent, save the lesson, and practise it.
-
A full game review with coverage stats, evaluation context, and the biggest learning opportunity highlighted.
-
Tune the coach’s directness, creativity, and voice without changing the underlying engine analysis.
-
Adjustable AI opponents, move feedback, guided warnings, and instant “Review this game” after checkmate.
-
Hosted RevenueCat checkout for Zugzwang Pro, shown in sandbox mode for testing.
-
RevenueCat Web Billing powers the Pro tier for deeper improvement history, more saved lessons, and more practice re-tests.
-
Zugzwang groups mistakes across games into recurring themes with evidence and practice actions.
-
Pro unlocks deeper recurring mistake history, showing patterns across many analyzed games.
-
The logo of the android APK.
-
Week 1 chat, the core question Zugzwang is still answering.
-
A look into the very early layout of the build, before the branding.
-
Alpha, it played chess. That was enough to keep going.
-
The moment it stopped being a chess bot and started being a learning tool.
Inspiration
It started on a walk. I was playing chess against Claude on my phone and kept noticing something: the AI would make moves, but never explain why in a way that actually helped me understand what I'd missed. That bothered me more than losing.
I looked at the tools that existed. They all did roughly the same thing, show you the engine's preferred move, tell you it was inaccurate, move on. Useful, but not corrective. I kept making the same kinds of mistakes because knowing the better move and understanding why I chose wrong are completely different problems.
I wanted something that would ask what I was trying to do, then explain where my thinking broke down. That became Zugzwang.
Zugzwang turns your chess games into saved lessons and targeted practice so you stop repeating the same mistakes.
What it does
Zugzwang is built around one loop:
- Upload or import a real chess game.
- Get a post-game review with engine-backed move analysis.
- Find the biggest learning opportunity in the game.
- Explain what you were trying to do in that position.
- Get an intent-aware saved lesson.
- Practise the idea directly on the board.
- Return to the reviewed game without losing context.
The key difference is that Zugzwang does not only say "the engine wanted this move." It asks what the player was trying to accomplish, then explains the mistake relative to their intent, not a generic engine verdict.
The app also includes:
- guest access for immediate review;
- Play mode with adjustable AI opponents;
- Guided Play prompts that teach what to notice, not just what to play;
- Learn / sandbox tools for exploring positions and asking questions;
- saved lessons that persist across sessions;
- an Improvement Profile for recurring mistake patterns across many games;
- light and dark themes;
- RevenueCat Web Billing integration for a Pro tier;
- a tested Android Capacitor wrapper.
How I built it
This is my first software project. I had no prior coding experience when I started. Zugzwang was built over approximately one month.
It started as a broken repository from an AI challenge, a basic chess board with a "Make AI Move" button and not much else. I fixed it, then kept going because the core idea wouldn't leave me alone.
The frontend is React, TypeScript, and Vite with a custom UI system built from scratch. The backend is FastAPI with python-chess and Stockfish handling all deterministic chess work. AI explanations sit on top of engine evidence: the chess truth comes from the engine, the language layer turns that evidence into coaching. This separation was the most important architectural decision I made, and I made it early, it's what kept the app honest when everything else was still rough.
I ran a closed beta with real playtesters. Their feedback directly changed the product: the difficulty system was rebuilt from a flat 1–20 scale into Elo-bracket-based opponent personas after testers said low-difficulty felt inhuman. The board layout was stabilized after reports of erratic scaling across different devices. The correction flow was tightened after watching people get lost between review and practice.
Security went through multiple passes, peppered password hashing, per-account AES-GCM encryption, CSRF protection, rate limiting, and session hardening. RevenueCat is integrated with server-side entitlement verification so Pro status is never trusted from the client alone. The app fails closed to Free if verification is unavailable.
The Android wrapper was added close to the deadline, Capacitor wrapping the live web product, built and smoke-tested on a physical Galaxy S23.
Challenges I faced
The hardest part wasn't any single technical problem. It was making the app feel like a real learning loop rather than another engine analysis screen with a chat box bolted on. Getting there required solving problems across every layer of the stack.
Getting the architecture right from the start
The most important decision I made early was treating Stockfish as the source of chess truth and the LLM as a language layer on top of it, never the other way around. Chess is a domain where an LLM will confidently explain illegal or objectively bad ideas if you let it reason freely about board state. Every coaching explanation in Zugzwang is grounded in deterministic engine evidence first. The LLM explains what Stockfish found; it never decides what's true. That principle held for the entire build and is the reason the coaching doesn't hallucinate.
Frontend/backend state desync
One of the earliest and most damaging bugs was the frontend's local chess.js copy going stale after AI moves. This caused broken castling, blocked check-escape captures, and general piece freezes that were nearly impossible to reproduce consistently. The fix was re-syncing from the backend's authoritative game state before computing legal moves on the frontend, but finding the root cause required tracing the desync through the full request cycle rather than patching symptoms.
AI provider reliability
Gemini latency occasionally reached 24 seconds, dangerously close to polling windows and causing turn desyncs. An earlier version of the fallback chain could produce seven provider calls for a single user action during retry storms. After tracing the exact call paths, I enforced attempt ceilings, added per-provider cooldowns on 429 and 503 responses, introduced request deadlines, and coalesced in-flight duplicate requests on the backend. Daily API usage dropped dramatically with no loss in product quality. The design principle became: AI should enhance the coaching experience, but the product should not collapse if the provider fails.
Making practice preserve review context
Practice mode had to happen on the main board without destroying the original reviewed game's state. The user needed to enter a correction attempt, make moves, and exit back to exactly the position they were reviewing, with the original PGN intact and review navigation unaffected. This required careful state layering: the practice board runs as a mode over the review state rather than replacing it, with the original game preserved separately and restored on exit.
The correction loop state machine
The full correction flow, select a mistake, capture user intent, generate a grounded explanation, produce a practice position with a verified single-best answer, validate the user's attempt server-side, and return to the original review, required keeping multiple interdependent state objects coherent simultaneously: the original PGN, the current review position, the correction target, the practice board, the answer validation result, and the saved lesson. Getting all of these to update correctly without cross-contaminating each other was one of the harder engineering problems in the build.
Board layout stability
useFittedBoardSize shrinks the board whenever the page overflows the viewport. Three things changed height after load, a practice brief in the identity row, a moves pill that only rendered after the first move, and a take-over hint that went from empty to two lines when the board became playable. Each one triggered a new overflow correction, causing the board to visibly shrink mid-session. The fix required reserving constant height for all three from first paint so the fitter computed size once and had nothing left to correct. Verified stable across 1366×768, 820×1180, and 390×844 viewports.
Security hardening
The app went through multiple security passes: PBKDF2 password hashing with a pepper layer, per-account AES-GCM encryption with wrapped keys, CSRF protection, HTTP-only session cookies with server-side validation, rate limiting on auth endpoints, protected route enforcement, and Row-Level Security on the database verified by actual cross-account access testing. RevenueCat billing is handled with the secret key server-side only, the backend verifies entitlement and fails closed to Free if verification is unavailable. Pro status is never trusted from the client.
A near-disaster with version control
One of the lowest points in the build was a git checkout that silently wiped the first weeks of uncommitted work from the working tree, the anti-repetition fix, the eval bar, the difficulty rework, all of it. Recovered with zero data loss by mathematically reversing the patch that caused it against a verified post-patch copy, independently cross-checked against an older zip archive. Every significant change got committed properly after that. Both patch scripts got double-application guards so it could not recur.
RevenueCat integration complexity
RevenueCat brought its own debugging surface: frontend public key, backend secret key, entitlement ID, offering and package IDs, sandbox checkout, customer and app user ID matching, status refresh timing, Pro gate behavior, fail-closed logic, and a production environment key mismatch that was initially misread as an authentication failure. The final behavior is: frontend opens checkout, backend verifies entitlement, if verification succeeds and entitlement exists Pro is active, if verification fails the app stays Free with a clear reason shown, but getting there required tracing the full provider handshake.
What I learned
Building with AI is not about adding a chat box. The real challenge is deciding where AI belongs in the loop, and more importantly, where it doesn't.
The separation of concerns between engine and LLM
The single most important technical lesson was that chess coaching requires a deterministic ground truth layer. An LLM reasoning freely about board state will produce confident, plausible, and sometimes wrong explanations. Stockfish doesn't. Keeping these responsibilities completely separate, engine for evidence, LLM for language, is what makes the coaching trustworthy. This principle now applies to how I think about any AI-assisted system: identify what needs to be deterministically correct, remove the LLM from that responsibility entirely, and use it only where language and reasoning are the actual value.
Graceful degradation as a first-class requirement
AI providers are not reliable infrastructure. They rate-limit, they go down, they return 503 under load. Treating them as enhancement layers rather than foundations, building cooldowns, fallback chains, attempt ceilings, and deterministic fallbacks from the start rather than as patches, is the architecture that survives real usage. The honest "coach could not be reached, so this is the engine's reading only" message in the UI is the right behavior: degrade gracefully and tell the user what happened.
State management at product scale
Managing game state, review state, correction state, practice state, and account state simultaneously, each with their own update rules, persistence requirements, and failure modes, taught me that state is the hardest part of a real product. Not the features. The state transitions between features.
Security is a design decision, not a layer you add later
Adding security after the fact is significantly more expensive than building it in. Peppered hashing, per-account encryption, RLS, and server-side billing verification all went in incrementally, but each one required touching multiple layers of the stack. Designing for these from the beginning, even roughly, would have been faster.
Root-cause debugging over symptom patching
Every significant bug in this build, the frontend desync, the board shrink loop, the API spike, the git incident, had a specific, traceable root cause. Patching the visible symptom without finding the cause would have left the underlying problem intact and made the next bug harder to find. The discipline of always asking "why did this actually happen" rather than "what change makes the error go away" is the single most transferable thing I learned.
Building in public creates accountability
A beta with real playtesters is different from testing yourself. Real users find failure modes you'd never construct intentionally, erratic board scaling on a specific viewport, a piece freeze triggered by a specific move sequence, a billing status that did not refresh correctly after checkout. Fixing things that real people actually hit is a different kind of engineering than fixing things you predicted might break.
The difference between a demo and a product
A demo shows the happy path. A product handles what happens when the happy path breaks, the provider is down, the user imports a malformed PGN, the practice position has no verified answer yet, the checkout conflicts with an existing session. Building the product version of Zugzwang meant explicitly deciding what every failure mode should look like from the user's perspective, and then building that behavior deliberately rather than leaving it to chance.
What's next
I want Zugzwang to become genuinely longitudinal.
Right now it can identify a recurring mistake pattern after ten games. What it cannot yet prove is whether a player actually stops making that mistake after completing a correction cycle. That is the real claim, not "here are your errors" but "here is evidence your errors are decreasing", and it is the next thing I want to build toward.
The long-term goal is a personal improvement record that gets more accurate the more you play. One game gives you a lesson. Many games reveal your pattern. Enough practice shows whether the pattern is actually changing.
That is what Zugzwang is trying to become.
Built With
- android
- billing
- capacitor
- chess
- css
- docker
- fastapi
- gemini
- neon
- playwright
- postgresql
- python
- python-chess
- react
- render
- revenuecat
- stockfish
- typescript
- vercel
- vite
- web

Log in or sign up for Devpost to join the conversation.