Felt Dojo: Duplicate poker
Companion to the project brief. What it is, why it's hard, what it measured, and what it can't do.
The idea
Poker is a game where you can play well and lose, repeatedly, for a long time. That's what makes it worth playing and what makes it almost impossible to learn from. If you and I sit at different tables for an evening and you finish up and I finish down, we have learned nothing about which of us played better.
Duplicate bridge solved this in 1914: deal the same hands to every table, and score players against each other on identical cards. Card luck stops mattering because everyone gets the same luck.
Duplicate poker applies that to Hold'em. Two or more players play the identical run of hands: same hole cards, same flops, turns and rivers, same bots in the same seats, same blinds, same starting stacks. The only thing that differs is how each of you plays them. You play on your own schedule, asynchronously; nobody has to be online at the same time.
As far as I know, nobody else has built this for poker.
What you actually get
A leaderboard with three scores, because they answer different questions:
- Raw chips: what you won from the bots at the table. Everyone can be up at once; this is the number that feels like poker.
- Relative chips: your result on each hand minus the field average on that same hand. You can win 100 chips on a hand and still be −700 relative, because everyone else won more with the same cards. This is the number that answers "did I play it better."
- Matchpoints: a bridge-style score: one point per participant you beat on a hand, half a point for a tie. Insensitive to one enormous pot swamping everything else.
A hand-by-hand review, which is the actual product. The leaderboard is the hook; the review is why the feature exists. It finds the first decision where your line and another player's diverged, opens at that exact moment with the identical history folded away above it, and then plays both lines out side by side to their separate endings. It also surfaces the hands that decided the match , the ones where somebody finished furthest from everyone else (which is rarely the biggest pot), the hands where you were the only one to fold or the only one to get your stack in, and which street your hands tend to be won and lost on.
A style comparison across everyone in the match: how often each of you played a hand, raised first in, three-bet, went to showdown, and how aggressively you bet, over the identical deals.
Free accounts get a 24-hand 6-max match seating five people, fully scored with the complete review. Play one alone and it is scored against a single Intermediate benchmark line instead, so it still closes with a leaderboard. Plus adds the Advanced benchmark, matches up to 1,000 hands, 2–9 seats, custom stakes, longer deadlines, and fields of ten rather than five.
You bring people in either by emailing them an invitation or by sending a share link. Both routes go through the same gate, so a match that has closed its field cannot be joined by either, which matters, because a share link leaves no record you could otherwise check against.
Why this is harder than it sounds
The whole feature rests on one claim: every participant faced exactly the same thing. If that is false anywhere, every number the leaderboard shows is meaningless, and it fails silently, because a slightly different bot decision just looks like a slightly different hand.
Fixing the deck is the easy half. A full 52-card permutation is generated per hand and dealt by fixed index, so one player folding preflop cannot shift the turn card the way it would if cards were dealt on demand.
The bots were the hard half, and the original plan under-estimated it by an order of magnitude. The design assumed the risk lived in one per-hand random roll. An audit before any of it was built measured the truth: replaying identical hands with identical inputs, 15% of all-Intermediate hands diverged, 37% of all-Beginner ones. The plan had estimated 2%. The cause was the Monte Carlo equity estimators, which sample a few hundred runouts per decision and had no seeded source.
Worse, and completely invisible: the preflop equity table was computed per-process and sampled on first use, so it drifted up to 2.4 equity points between server restarts. Two people who played the same match on either side of a deploy faced measurably different opponents, and nothing anywhere would have reported it.
Both are closed. Every bot random draw is now addressed by its coordinates, which hand, which seat, which street, which action ordinal for that seat, rather than drawn from a stream, because a stream desynchronises the instant one player's line creates one extra decision. The preflop table is a committed 7,605-entry resource rather than something each server computes for itself.
And the obvious test for this does not work. Replaying a scripted hand in a fresh JVM passes even with the frozen equity table deleted, because a scripted hand makes the cache keys arrive in the same order in every process. The test that actually works reads preflop equity directly across processes: without the committed table, three JVMs give three different answers; with it, the same answer three times. That distinction was found by deliberately breaking the fix and watching the test stay green.
What it measured, and why it paid for itself before shipping
Duplicate poker was built to let players compare themselves. Its first real use was measuring the bots, and it immediately found a defect that had been live for months.
Playing the identical spot four ways (a human plus three bot benchmarks, same cards, same seats) makes a bot's behaviour directly comparable in a way no amount of aggregate statistics does. A 300-hand run produced 3,357 preflop decisions, and among them the finding that facing a raise before the flop, the Intermediate bot folded 92% of the time and three-bet almost as often as it called. It had no calling range at all; the tier could only fold or re-raise. One absent range explained every symptom that had been separately noticed and never connected: the 4% call rate, a limp that folded 100% of the time to any raise, a price curve that was flat and slightly inverted, and over-bluffing that the same table's over-folding made profitable.
Fixed, and verified twice on independent deals:
| facing a preflop raise | before | after |
|---|---|---|
| fold / call / raise | 92.0 / 4.2 / 3.8 | 81.7 / 16.0 / 2.4 |
| call : raise ratio | 1.1 : 1 | 6.7 : 1 |
| continues after limping | 0.4% | 17.2% |
| sees a flop | 9.6% | 19.9% |
The feature did not create that defect; cash and tournament behaviour was verified unchanged. It made it visible.
The honest limit: what a short match can and cannot tell you
This is the part I'd want stated plainly if someone else were making these claims.
Most hands of poker are identical no matter who plays them. In a real 24-hand heads-up match, 20 of the 24 ended the same way: both players folded, nothing happened. Those hands contribute exactly zero to any comparison, so the effective sample is the divergence rate times the hand count, not the hand count.
Consequences, measured rather than assumed:
- A chip win rate is not measurable at any hand count a person would actually play: ±170 bb/100 at 1,000 hands, ±54 bb/100 at 10,000.
- Matchpoints over all hands is mostly noise heads-up, because a tie pays half a point and four fifths of hands tie. The heads-up board now scores over divergent hands only, and says so.
- On a 300-hand run, one human's 18,334-chip edge over Intermediate sits at 0.69 standard errors, nowhere near a verdict. Advanced finishing below Intermediate on the same run is 0.32 SE, which is noise, not a tier inversion.
- Moving 0.69 SE to a defensible 2.0 needs about 8.4× the hands, so ranking two closely-matched players wants roughly 2,500 hands. An earlier estimate of 600 in the project's own notes was too optimistic and has been corrected.
None of that is a defect in the format; raw-chip variance without matched deals would be far larger. It's a limit worth being loud about, so nobody reads a leaderboard from a 24-hand match as a verdict. The leaderboard is the hook; the decisions are the instrument, and they're what made every bot finding above conclusive off a single run.
Integrity, and the one hole that can't be closed
Results, hand histories, and even progress stay sealed until a match closes:
- The leaderboard refuses to serve until then. Duplicate hands are excluded from your ordinary hand history and global stats while sealed, enforced at the database-access layer with a test that fails if anyone adds an unfiltered read, so endpoints nobody has written yet are already covered.
- Another participant's row shows participation only. Even a count of how many times they paused is withheld, because a pause is attached to a specific decision: "parked twice on hand 40" tells a rival exactly which spot their opponent is agonising over.
- The match seed is withheld until close, then published, so anyone can recompute the decks afterwards and verify they matched what was committed at the start.
The sealing used to have one blunt edge: a match opened at its deadline, not when everyone had finished. A match that four friends all played on the same evening could sit sealed and unscored for up to a week. Found in the first production play-test; fixed and deployed 2026-08-19: a match now closes as soon as nobody new can join and nobody in it is still outstanding, so the worst case a player experiences went from seven days to about five minutes. The reason it was not a one-line change is worth stating: the obvious version (just look at matches earlier) hands a match that is not actually due to a routine that forfeits everyone still outstanding, which would silently burn players who had days left. The early path is therefore a separate route that structurally cannot reach the forfeit loop, and each of its negative tests was verified to fail against the naive version before shipping.
The hole that redaction cannot close, found in play-testing: every participant plays the identical deal, so anyone who has played hand 12 can describe it completely to anyone who hasn't. Same cards, same seats, same bots, no showdown required. A participant is themselves the leak. Asynchronous duplicate is therefore a trust boundary, fine among friends and not a basis for ranked play. The structural fix is simultaneous play, which is exactly what duplicate bridge relies on, and it's specified but not built.
Where it stands
Built across nine milestones plus an operator benchmark harness and a bot probe harness, and live on feltdojo.com. Load-tested with concurrent runs (2026-08-19, on the dev machine): 12 and 24 simultaneous runs behaved identically (p95 action latency 7 ms at both); the cap refused the 25th cleanly, and identical conditions held across every participant, which is the promise the format cannot break. The honest caveat: that was the dev machine, so it says "the design holds", not "here is what the production box can take."
Open, and all of it product work rather than defects:
- Think time is recorded but not yet presented. It deliberately counts only the sitting in which an action was taken, so a decision you park mid-thought and come back to under-counts.
- The divergent-hands board is heads-up only. The definition generalises cleanly to more players, but at four participants a hand is divergent if any run differs, so the rate may approach 100% and the board may stop earning its space.
- Creating a 300-hand benchmark match is silent for about 105 seconds, which is indistinguishable from a hang. Cheap while it's operator-only; it becomes a first-run experience the moment a free player creates a long match.
What's next
1. Heads-up scoring rework: at two players the relative board is arithmetically identical to raw chips, so it earns no space; matchpoints move to divergent hands only, and the divergence count becomes a first-class statistic rather than a footnote. 2. Think-time totals, and the divergent board beyond heads-up.
(Early close, scoring a match as soon as nothing further can happen to it, used to head this list, and shipped 2026-08-19.)
The one thing deliberately not planned is a fix for the participant-is-the-leak problem above. Simultaneous play would close it, and asynchronous play is the reason anyone can use this feature at all.