Felt Dojo: Project Brief
A poker training platform built with Claude Code. What it is, how it was built, and who did what.
By the numbers
Measured 2026-08-23. These are refreshed by hand every so often, so read them as a recent snapshot rather than a live counter; the shape is the point, not the last digit.
| Started | 2026-07-19, five weeks ago |
| Commits | 827 |
| Production code | ~93,600 lines: 47,100 of Java across 421 files, 46,400 of TypeScript and React across 193 |
| Test code | ~70,500 lines across 308 files |
| Automated tests | 2,654, all passing: 1,753 backend, 901 frontend |
| Database migrations | 32 |
| Written documentation | 88 documents, ~402,000 words |
Three of those rows say something the others don't.
There is about three quarters of a line of test code for every line of production code. That ratio is deliberate and it is what makes the rest of this document's claims checkable: the bots' behaviour, chip conservation, tournament payouts and the duplicate-poker determinism guarantee are all pinned by tests that have been watched to fail before being trusted.
The test count roughly tripled in eleven days. On 2026-08-12 the project's own historical record noted 767 backend and 96 frontend tests; on 2026-08-23 it is 1,753 and 901. Most of that is not new features; it is existing behaviour being pinned down.
The documentation is not a byproduct. ~402,000 words is more than the codebase, and it exists because the project is built with an AI assistant that starts every session knowing nothing: the architecture notes, the record of traps already hit, and the written reasoning behind decisions are what stop the same mistake being made twice. Roughly a third of it is design specs and postmortems that no user will ever read.
The tool
Felt Dojo is a place to learn poker by playing real poker. You play real hands (real rules, real blinds, real side pots) against bots that are meant to feel like actual opponents rather than difficulty sliders, and you review what happened afterwards. No money, no deposit, no strangers.
What it is for, and what it is not
The thesis, in the owner's words (2026-08-21): "My goal is to create a way for new players to learn the game and onboard into it in a supported, interactive way. I'm creating a sandbox where people can play real poker, better than play money or social sites where real human players don't care and do all kind of bad things. I want to allow players to avoid having to deposit real money on a site they don't know and may not trust, just to play microstakes with little learning opportunities."
That is the product. Between the play-money apps (free, but the opponents shove every hand and the product is about buying chips) and real-money microstakes (real poker, but a deposit on a site you don't know, and still nothing that teaches), nobody owns "real poker, safely, with an on-ramp". Felt Dojo is built for that square: the complete beginner, the curious casual who wants to try without depositing anywhere, the returning player knocking off rust, and, later, friends who want a home game without a real-money site between them.
What it is not, said plainly because the owner said it plainly: "I cannot pretend that I am a poker expert and I cannot expect the AI coaching and bots to act as poker expert." Felt Dojo does not teach anyone to win, does not claim GTO or solver authority, and does not compete with training sites, coaches or solvers; it is where you practise what they teach. The site shows, compares and repeats; it does not judge. Every hand is recorded and replayable, your stats and trends are yours, duplicate poker compares you with a friend on identical cards, and none of it tells you what was "correct", because nothing here is entitled to. You can never buy chips, either.
What works today:
- Three game types: No-Limit Hold'em, Pot-Limit Omaha, and 5-card PLO, each with its own hand evaluator and betting rules. Hold'em is free; PLO and PLO5 sit behind Plus while their realism is still unverified, so a stranger finding the site doesn't stumble into a variant nobody's checked yet.
- Cash games: configurable stakes, table size, buy-ins, rake, and opponent mix. Leaving a table parks it with your chips still on it so you can come back; only "End game" cashes you out.
- Tournaments: multi-table up to 10,000 entrants, automatic table balancing, blind-level escalation, late registration, re-entry, bounty formats (regular and progressive), custom blind structures and payout curves, plus pause/resume that survives a server restart. Five built-in blind structures, including the real 2025 WSOP Main Event schedule transcribed level-for-level from the published structure sheet.
- Duplicate poker: two or more players play the identical run of hands and are scored against each other under matched conditions. This is the newest and most distinctive feature; it has its own brief. Players are invited by email or by a share link, and nobody sees anyone else's cards, progress or results until the match closes.
- Three opponent tiers: Beginner, Intermediate and Advanced, described below and in the bots brief.
- Hand history and replay: every hand you play is recorded as the real sequence of game states and replayed from that record, not reconstructed. Free accounts see exactly what they'd have seen live; Plus adds a reveal-all toggle that shows every hole card for study purposes.
- Statistics: VPIP, PFR, aggression factor, win rate, computed from per-hand data.
- Accounts: including a guest mode that puts you at a table without signing up, and converts to a real account later without losing your bankroll or history. Signing up sends a real welcome email and opens a short, skippable first-run tour.
- A Help page and a "What's new" panel, both maintained by hand and kept current: every player-visible change gets an entry the day it ships.
What's free and what's Plus. Hold'em, cash games at any stakes, tournaments up to 200 entrants, two blind structures, Beginner and Intermediate opponents, full hand history and stats, and a real scored duplicate match are all free. Plus adds PLO/PLO5, Advanced opponents, fields up to 10,000, the remaining three blind structures, bounty tournaments, custom blind and payout structures, reveal-all study mode, and longer/larger duplicate matches. Paid tiers aren't live yet; the entitlement plumbing is, and the operator account is the only one that has it.
The bots, which are what makes the sandbox real
A sandbox is only worth practising in if the opponents behave like people. Anyone can write a poker bot that folds below a threshold. The hard part is bots that feel like people: recognisable kinds of players, badly in believable ways, and that get genuinely better as you move up the tiers. The claim is believable, never good: nobody has measured them against a real population at a real stake, and these documents don't pretend otherwise.
- Beginner plays like a beginner, deliberately: calls too wide postflop, chases draws with the wrong math, can't fold jacks to a shove, sizes bets in a way that gives the hand away.
- Intermediate understands position, pot odds, board texture and betting patterns. It tilts, it loses focus occasionally, it builds reads on opponents who keep betting big.
- Advanced models opponents as weighted ranges rather than categories, narrowing them action by action, filtering by how the board connects, and picks lines by comparing expected value across candidate bet sizes rather than by threshold.
The tiers are measured, not asserted. A purpose-built harness plays thousands of hands headlessly and reports each tier's statistics against real-world player profiles, plus head-to-head win rates in big blinds per 100 hands with confidence intervals. Variance is controlled with duplicate dealing (the same deals played from both sides) so the comparisons resolve at a practical number of hands.
Full detail, including where each tier is still weak, is in the bots brief.
The technology
Backend: Java 21, Spring Boot 4, PostgreSQL with Flyway migrations, JWT authentication with argon2id password hashing, WebSockets for live table state, Redis, Maven.
Frontend: React with TypeScript, Vite, Vitest.
Infrastructure: Docker Compose on a Hetzner box behind Caddy, GitHub Actions CI running both test suites on every push plus a real-Postgres job and a bot-behaviour golden-file job, automated nightly database backups with an off-site copy, transactional email through Resend, and error tracking through Sentry (deliberately without session replay, which would ship a recording of a player's hole cards to a third party). Engagement metrics are a nightly rollup rather than a live query, specifically so the numbers stay true after hand-history retention starts deleting the hands behind them.
Test coverage: 1,672 backend tests and 642 frontend tests, all passing. That includes property tests, a hand-evaluator cross-check against an independent implementation, an engine fuzz test, a 10,000-entrant headless tournament run, committed golden vectors pinning the duplicate randomness encoding, and a real-Postgres CI job for the handful of tests where the database dialect is itself the thing under test.
How we worked
Conversational, in Claude Code, over many sessions. Not "generate an app": a running collaboration with a clear division of labour.
What Marc contributed:
- The product. What to build, what to cut, what to defer. The fourth bot tier was designed, then deliberately shelved as conditional, and then re-planned as machine learning. PLO bot separation is fully planned and explicitly not scheduled. The free/Plus line has been redrawn several times as the product got clearer; most recently pulling three blind structures and large tournament fields behind Plus while keeping a full scored duplicate match free. Each of those was a judgement call, and each was his.
- The poker. This is the part no amount of code quality substitutes for. Corrections that changed the design, not just the numbers:
- "Nobody would ever fold KK to an all-in." Rejecting the framing that folding kings could be defensible at some stack depth led to a hard floor in the model (premium hands never fold to a preflop all-in) rather than another threshold tweak.
- "Bet sizing changes in tournaments, especially as stacks get shallow." A single hardcoded formula was serving every tier, both formats and every stack depth.
- "A good player raises and folds far more than they call." This redirected a whole tuning pass away from a target that was invented rather than real.
- "The WSOP Main Event structure is not 60 minutes, it's two hours", with a link to the official structure sheet. It was wrong twice over: the level length, and the fact that it had been reusing a different template's blind curve rather than the real event's own.
- Playtesting that found real defects. Playing the product surfaced bugs no test suite caught: a pot total updating before the chips moved, a Beginner calling three streets with an underpair, an Advanced bot flat-calling aces, a name plate covering the one word that said whether a player had raised or called, and an entire results panel that was empty because nothing had ever written the data behind it. Each report was specific enough to reproduce.
- Pushing back when the work was wrong. Including on claims that turned out to be overstated.
What Claude contributed:
- Implementation across the whole stack: engine, bots, persistence, API, WebSocket layer, UI.
- The measurement discipline: the tuning harness, the statistical gates, the duplicate-poker variance reduction, the bot fingerprinting and probe harness, and the habit of refusing to conclude anything from a single run.
- Architecture chosen for what comes next: a strategy seam that lets a trained fourth tier be a data swap rather than a rewrite, and that doubles as the analysis layer's interface.
- Documentation, including honest write-ups of what was measured, what missed its target, and what was deliberately left undone.
What that division actually produced
The interesting pattern is how often the two halves caught different things.
Tests caught what tests are good at. When a fix to Beginner's postflop calling was first written, it also disabled multiway bluff restraint in two tiers; the existing suite caught that in seconds. When bot memory was made to adapt properly, two tests went red; investigating rather than adjusting them showed the bots had started raising where they used to call, which was an improvement the tests were scoring as a regression.
Playtesting caught what tests structurally cannot. No unit test knows that a bot folding kings looks absurd, that a table feels too slow between hands, or that a debug panel is quietly lying. Those needed someone who plays poker to sit down and play.
And the most expensive class of defect was invisible to both. Duplicate poker shipped nine milestones with both halves of hand recording built, every test passing, and nothing joining them: the write side and the read side were built in different milestones and neither owned the seam. Every test was satisfied by its own half: the gating tests wrote their fixture rows by hand and genuinely proved the gate held, over rows nothing in production produced. It surfaced when Marc opened the results page and found a panel empty. The rule that came out of it is now written down: for any A-writes/B-reads pair built in separate milestones, one test has to run A for real and assert on what B actually returns.
The same shape appeared in the browser. A returning visitor kept getting the previous build, because static files were served with no cache headers at all and browsers fall back to guessing, so every frontend fix reached new visitors and nobody else, silently, for days. Nothing reports that: the deploy succeeds, the files on disk are correct, and curl shows the new build because curl has no cache.
Where it stands
The platform is live at feltdojo.com and functional end to end: sign up or play as a guest, sit down at a cash table, run a tournament of up to ten thousand players, challenge a friend to an identical run of hands, and review every hand afterwards. 1,672 backend tests and 642 frontend tests pass.
Capacity is measured rather than estimated. Ramped on the production box, no functional limit appeared up to about 150 concurrent players; the configured cap is 40 by choice, not by limit, to leave headroom for tournaments, backups and deploys. Large tournaments are cheap because only the table a human is sitting at plays real hands; the rest of the field is simulated against the real blind structure, which is what makes a 10,000-entrant tournament resolve in about six seconds instead of never. That measurement corrected two beliefs that had been held confidently and were wrong: that the strongest bot tier was the most expensive to run (it is the cheapest, because it folds more and thinks less often), and that a first-run benchmark meant anything at all before the JIT had warmed up. Backups run nightly, copy off-site, and email if they fail. Uptime is watched from outside.
It is not finished, and the open items are written down as honestly as the finished ones:
- The Omaha bots play legally but their realism is measured and bad. A separate Omaha decision layer is designed and deliberately not scheduled.
- The fourth bot tier remains conditional: now planned as machine learning, trained against our own bots and never on player hands; satellite payouts are specified and unbuilt.
- The analysis layer, once called "coaching" and once called "the actual differentiator"; neither phrase survives the owner's 2026-08-21 positioning. It is fully specified across eight milestones and not started, and it will be built lowest rung first: the player's own frequencies beside the bot tiers' measured ones, then a curated post-session review, then, only on the leaks no competent player disputes, a second look in the voice of "a solid regular usually raises here". Never a verdict, never a claim of expertise; the owner is not a poker expert and has said the software must not pretend to be one.
- Multi-human tables (friends at one table, with or without bots) are assessed (an XL) and not scheduled, but they are now a stated direction rather than a maybe: a home game online, with no money and no real-money site between the players, is exactly the sandbox the site is for. Invite-only by design, so the strangers the site exists to avoid never arrive.
- A full learn-to-play course is built and deliberately unreachable. Four belts, eighteen of twenty-one chapters, walked screen by screen by a human and revised heavily against those notes, but the entry points are postponed until the course is finished, so no player can find it yet.
- One test hangs intermittently under heavy machine load and has resisted two rounds of diagnosis. It now captures a thread dump into its own failure message, so the next occurrence arrives with its own evidence.
What's next
The ranked list lives in priorities.md; the short version, in order:
1. Confirm the Intermediate fix in play rather than only in the harness: the last open bot item. 2. Finish the beginner course. The "one larger bet" this list used to describe as undecided has been decided by doing it: a full learn-to-play course is built through eighteen of twenty-one chapters and play-tested end to end. What's left is the last three chapters and the entry points, which come last on purpose; the course stays unreachable until it's finished. 3. A deep code review over the never-reviewed paths: the one hard gate left before announcing the site publicly, deliberately run last, after the feature work stops moving the code it would review. 4. Then the launch plan: announce free first, and only price and build billing against real usage: you cannot price what nobody has used.
The analysis layer and drills stay fully specified and ready to start, at the level the positioning allows: drills already decided on outcome and frequencies, no verdicts for v1, and the analysis layer starts with mirrors rather than grades. Three of the larger candidates want the same thing first: an ICM model, which is what makes tournament advice correct near the money, and it is deliberately planned as its own piece rather than built three times inside three features.
See docs/roadmap.md, plan.md and priorities.md, which list both halves.