Felt Dojo

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.

Started2026-07-19, five weeks ago
Commits827
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 tests2,654, all passing: 1,753 backend, 901 frontend
Database migrations32
Written documentation88 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:

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.

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:

What Claude contributed:

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:

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.