No description
  • TypeScript 97.9%
  • CSS 0.8%
  • JavaScript 0.6%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-02 16:43:54 +01:00
.claude The table stops overlapping: the notes under a seat (its range, a shown hand) move like bets do, to the nearest free place beside the seat and never across the table, so a seat at the top no longer hangs them over the pot; the middle is flat, the board with one line under it for the pot and the price, and moves as one piece when a top seat's plate still reaches it. Checked at 1440 px and on a phone: nothing on the table overlaps on any street. Also a second dev stack (gold-rail-iso, on 5175 and 8788) for checking changes while another one runs. 2026-10-31 22:11:42 +01:00
.forgejo/workflows The server's config goes out with each release: compose.yml, nginx.conf, headers.inc and the backup scripts ride in the release and deploy.sh writes them in place (keeping the old ones as .prev), refuses a compose.yml that does not work with the server's .env before switching anything, restarts nginx when its config changed, and takes the old config back with the old release when the new API does not come up. .env and deploy.sh itself still stay by hand. The deployment notes say how to check the nightly backup, prove it restores, keep a copy off the server, and why .env belongs in a password manager. 2026-11-01 18:36:11 +01:00
deploy The server's config goes out with each release: compose.yml, nginx.conf, headers.inc and the backup scripts ride in the release and deploy.sh writes them in place (keeping the old ones as .prev), refuses a compose.yml that does not work with the server's .env before switching anything, restarts nginx when its config changed, and takes the old config back with the old release when the new API does not come up. .env and deploy.sh itself still stay by hand. The deployment notes say how to check the nightly backup, prove it restores, keep a copy off the server, and why .env belongs in a password manager. 2026-11-01 18:36:11 +01:00
docs The server's config goes out with each release: compose.yml, nginx.conf, headers.inc and the backup scripts ride in the release and deploy.sh writes them in place (keeping the old ones as .prev), refuses a compose.yml that does not work with the server's .env before switching anything, restarts nginx when its config changed, and takes the old config back with the old release when the new API does not come up. .env and deploy.sh itself still stay by hand. The deployment notes say how to check the nightly backup, prove it restores, keep a copy off the server, and why .env belongs in a password manager. 2026-11-01 18:36:11 +01:00
public Home page as a dashboard with a compact import strip; progress and errors on import; error boundary and splash; structure: screens split into hands and train, report parts, shared format helpers 2026-10-02 19:51:11 +02:00
scripts Remove a leftover editing script 2026-10-25 16:33:29 +01:00
server zod 4 and React Router 7, which close the last known advisories (zod: memory exhaustion validating oversized collections, high; react-router: moderate): npm audit now finds nothing in the app or the server. 2026-11-01 18:15:53 +01:00
src A hand's card says more. In the chart explorer it now shows the hand as cards, its strength (rank of 169, top share), its all-in equity against any hand, how many steps it sits from the edge of the range, and the maths written out with the spot's numbers: what a call needs and why, the equity and the margin, what an all-in call is worth, what a pure steal needs and how the hand does when played back at, and the solver's value over folding. Where the price and the chart disagree, a line says why. A looked-up hand is no longer badged as yours. In the hand analysis, a range's combos are drawn as cards, and their kind wraps instead of being cut off. 2026-11-02 16:43:54 +01:00
.gitattributes Backend: accounts, hand storage, sandbox and rooms schema on PostgreSQL; deployment and Forgejo pipeline 2026-10-02 21:01:27 +02:00
.gitignore Redesign: flat, bigger, with cards and seat maps instead of letters; optional account and sync 2026-10-03 02:21:03 +02:00
.nvmrc M4+M8+M9: drill UI, range grid, packs/stats/about screens, README 2026-09-09 04:12:14 +02:00
eslint.config.js Two-factor sign-in and a way back after a forgotten password. 2026-10-31 21:15:03 +01:00
index.html Graphite and gold as the look a new device starts with; a look picked in Settings is kept with the account (users.prefs, PUT /api/me/prefs, merged so one device's room keeps another's accent) and taken over at sign-in, or sent up when the account has none yet. A look never picked follows the default. Drilling a game's mistakes moves into the bar of its hand list, beside the filters, and the game tiles take the full width with their figures on one line 2026-10-31 20:01:52 +01:00
package-lock.json zod 4 and React Router 7, which close the last known advisories (zod: memory exhaustion validating oversized collections, high; react-router: moderate): npm audit now finds nothing in the app or the server. 2026-11-01 18:15:53 +01:00
package.json zod 4 and React Router 7, which close the last known advisories (zod: memory exhaustion validating oversized collections, high; react-router: moderate): npm audit now finds nothing in the app or the server. 2026-11-01 18:15:53 +01:00
postcss.config.js M0-M2: scaffold, card/range engine, charts 2026-09-09 03:50:31 +02:00
README.md Range model: an overpair, or two pair or better made with the player's own cards, always stays in a range as it narrows - a strong range no longer loses its aces to its own sets and straights; grader version 5 regrades stored hands; documented 2026-10-28 02:13:46 +01:00
tailwind.config.js A look to choose in Settings: five rooms (emerald, graphite, midnight, bordeaux, aubergine) for the dark and its light, four accents (gold, champagne, silver, copper) for what is active, switched at once and put on the page before it draws; wins, losses and grades keep their colours in every look. The game tiles on Hands wrap to a second row on a wider screen instead of running off to the right where a mouse cannot follow 2026-10-31 19:31:51 +01:00
tsconfig.json M0-M2: scaffold, card/range engine, charts 2026-09-09 03:50:31 +02:00
vite.config.ts The table stops overlapping: the notes under a seat (its range, a shown hand) move like bets do, to the nearest free place beside the seat and never across the table, so a seat at the top no longer hangs them over the pot; the middle is flat, the board with one line under it for the pot and the price, and moves as one piece when a top seat's plate still reaches it. Checked at 1440 px and on a phone: nothing on the table overlaps on any street. Also a second dev stack (gold-rail-iso, on 5175 and 8788) for checking changes while another one runs. 2026-10-31 22:11:42 +01:00

Gold Rail

A local-first React app that drills multi-table-tournament decisions. It presents a spot, you pick an action, it grades you and explains why.

The rail is where you stand and watch rather than play, which is the only place this tool belongs — see Honest scope below.

It has two halves, kept apart on purpose. Home is a dashboard of the last day you played; My hands (/hands) is the part that matters: import your real GGPoker hand histories (txt files or a zip), then read a report per game, per session, the leak finder over your real play, replays of the hands, and screenshot review. Train is practice with generated hands, organised like the GGPoker lobby: Poker (All-in or Fold, Rush & Cash, Hold'em, PLO, Short Deck) and Tournaments (WSOP Online, Zodiac Festival, Bounty Hunters, GGMasters, WSOP Express, Spin & Gold, Mystery Battle Royale, Flip & Go). Each game has a page with what it is and what its rake costs, charts and a drill, plus a link to your own report for that game. Drill progress and the live table sit under Train. Old links to /analyse, /stats and /live?tab=replays still work.

Beyond drilling, it looks back at how you actually play: every decision is logged, a leak finder ranks your repeated mistakes, real GGPoker hand histories are graded against the same charts, and mistakes come back for spaced replay. Screenshots, photos and session videos are read on the device. A live table deals hands that play out at table speed, and your own hands can be watched again like a video. Study tools cover the charts, equity and ICM. It installs on a phone and works offline.

The app is complete on its own: no accounts, no network calls, no AI service and nothing to pay for. Every image, video and hand history is read in the browser and never leaves it. An optional server (server/, deployed with deploy/) adds accounts and a copy of your hands on a database you run yourself, so they follow you to another device; see Account and sync below and docs/DEPLOYMENT.md. Nothing in the app needs it, and nothing is sent anywhere unless you sign in.

Honest scope

  • This is not a solver. Correct answers come from hand-authored charts and heuristics. The EV loss shown after a wrong answer is derived from how far the hand sits from a range boundary, not from equity maths. The UI says so. (The equity and ICM calculators are real maths; the grading is not.)
  • Postflop is coarse. Board-texture buckets plus range-advantage heuristics.
  • Not for use while playing. GGPoker's Security & Ecology policy bans charts and assistance tools during play. That goes double for the analysis side: importing hand histories or reading a screenshot while a hand is live is real-time assistance. Every one of those screens says so, and screenshot review makes you confirm a reading before anything is graded rather than answering in one tap. The camera takes one photo when you press the button; nothing watches a live feed, and the live table only ever deals its own generated hands.

Running it

Node 26's bundled npm crashes on this machine (Class extends value undefined is not a constructor). Use Node 24, which .nvmrc pins:

nvm use 24.18.1
npm install
npm run dev

Other scripts: npm test (Vitest), npm run typecheck, npm run lint, npm run build, and npm run check for all three of types, lint and tests.

To work with an account, start the server next to the app (an in-memory Postgres, nothing to install) and the dev server passes /api on to it:

cd server && npm install && npm run dev

Then open Settings and create the first account, which becomes the administrator. The server's own checks are npm run check inside server/.

Architecture

Hard rule: src/engine/, src/analysis/ and src/vision/ never import from src/ui/, and contain no React. They are pure TypeScript that take inputs and return spots, gradings, readings and replays, which is why nearly everything is unit-tested. The one exception is vision/browser.ts, the only file that touches a canvas: it turns files and video frames into pixel buffers and draws the font glyphs the reader compares against. Tests paint synthetic tables into plain pixel buffers instead.

src/
  engine/          pure logic: cards, ranges, charts, generator, grader, explanations,
                   7-card evaluator, equity, ICM
    ranges/        chart data + lookup with stack-bracket interpolation
    generator/     seeded RNG, six spot templates, each a deterministic build function
    nash/          preflop equity table (generated) and the push/fold solver
    modes/         spot builders and drills for AoF, Spin & Gold and cash games
  analysis/        decision records, leak finding, spaced replay, screenshot drafts,
                   the hand replayer, grade confidence, loss reports, how a hand ended
    handHistory/   GGPoker parser, PokerCraft zip reader, decision extraction
    modes/         game detection, per-game extractors, results, rake and fee measurement,
                   luck at all-ins, the AoF pool model
    postflop/      range estimates, call/fold grading, tracker stats, bet/check findings
  vision/          the screenshot reader: cards, glyphs, numbers, seats, video scanning
  packs/           Zod schema, JSON repair + semantic validation, LLM prompt builder
  content/         glossary, GGPoker game modes (format, rake, verdict, strategy)
  pwa/             generated service worker, install and update state
  store/           Zustand stores over localStorage and IndexedDB, backup and restore
  ui/              screens/hands   the home page: import, reports per game, sessions, leaks, replays
                   screens/train   practice: lobby, game pages and charts, drills, live table, packs
                   screens/tools   chart explorer, equity, ICM; plus Settings, Glossary, About
                   report/         the per-game report and its parts (mistakes, pool, luck, losses)
                   components/     shared pieces (cards, replay table and player, import panel)
  format.ts        number formatting shared by the engine, analysis and screens
scripts/           icon, preflop-equity and Short Deck generators (outputs committed)

The four preflop templates are split into a deterministic build… function and a thin random wrapper. Drills, imported hand histories and screenshot review all grade through the same builders, so a real hand is judged exactly the way a drill spot is.

Checks

npm run check runs the type check (strict, with unused code and unchecked indexing as errors), ESLint and the tests. ESLint (eslint.config.js) adds what the compiler cannot see: dropped promises, wrongly used hooks and effects, non-exhaustive switches, and the layering rule above - engine, analysis and vision may not import the UI, stores or React. Where an effect deliberately resets state because it starts or stops something outside React (the camera, an object URL, an equity calculation), the line carries a eslint-disable with the reason.

Engine notes

  • rangeParser.ts handles 77+, 22-99, ATs+, A2s-A5s, AQo+, KQ and comma-separated combinations. + on a non-pair keeps the high card fixed and climbs the kicker, so 97s+ is 97s, 98s and not the connector ladder.
  • handRank.ts holds a hand-authored ordering of all 169 hands. A test asserts it is a complete, duplicate-free cover of the grid.
  • Stack brackets: 12 BB and under uses the push/fold chart, 20 BB and up uses the opening chart, and 12-20 BB uses the opening chart with the bottom third shoving instead of raising.
  • On hard difficulty about 60% of hands are drawn from a window around the chart boundary, because uniform sampling produces mostly trivial spots.
  • Some mistakes are categorical rather than marginal (limping, raise/folding a short stack, sized 4-bets with no room behind). Those carry a structuralNotes entry and are always graded as blunders with their own explanation.

Chart caveat

The published push/fold charts in the spec are slightly tighter than the opening charts at CO and BTN. The data is used as authored; ranges.test.ts asserts the invariants that actually hold rather than papering over it.

Packs

Packs → Create prompt builds a prompt with the schema inlined; paste it into any chat LLM, copy the reply, and paste it under Import. The importer strips markdown fences and surrounding prose, accepts a bare array, normalises card and position spellings (AH, A♥, BU, UTG+1), then validates with Zod plus the semantic checks LLMs reliably fail: duplicate cards, board length against the street, unknown correctOptionId, duplicate option ids or labels, sizes above the hero stack, and pots below the blinds.

A single bad spot never rejects the whole pack. Failures come back as { index, field, message } so the screen can point at the exact spot.

Game modes

Solved push/fold. scripts/generate-preflop-equity.ts deals every pair of the 169 starting hands 10,000 times and commits the heads-up equity table (38 kB). The solver in engine/nash runs fictitious play on it: callers best-respond to the average shoving ranges, shovers to the average calling ranges, until they settle - about 100 ms for four players, cached. Everyone has the same stack, chips are what counts, and once someone calls the model has everyone behind fold, as common push/fold tools do. Heads-up at 10 BB it gives the known answer: the small blind shoves 58%, the big blind calls 36%. Each hand gets an EV in BB against folding, so charts are "EV above zero" and a real decision is graded by what it cost under the model, not by chart distance.

Reading the charts. A table has a handful of situations (four-handed: CO, BTN and SB shoving first in, and BB, SB and BTN calling a shove from each earlier seat: nine). Each is one 13 by 13 chart with ranks on both axes, and a hand is in one of three states that never change meaning: solid (clearly worth playing: at least 0.1 BB better than folding), striped (a close call, within 0.1 BB either way, so either play is fine) or dark (fold). Type a hand ("k9o") and a card lists it in all nine situations side by side with its EV. The 0.1 BB line is deliberate: the equity table is dealt 10,000 times per matchup, so hands closer to the line than that cannot be called by it, and about one hand in eight sits inside it at the cutoff and button. Below the charts a table puts four numbers side by side for each situation (the equilibrium, what the pool did in your hands, the best reply to that, and what you did), each missing where there is no data. The solver is checked against a separate calculation of the same EVs (pushFold.verify.test.ts), and its ranges nest (what is shoved from the cutoff is shoved from every later seat), which the tiers rely on.

All-in or Fold, Spin & Gold, Mystery Battle Royale and Flip & Go use it for charts at any table size and stack. AoF and Spin & Gold also use it for drills and for grading imported hands (Spin & Gold at 12 BB or less).

Other games. Hold'em and Rush & Cash get hand-authored 6-max 100 BB charts (first in, and facing one open). PLO, with 16,432 starting hands, gets a points system you can see into - pairs, suits, straight potential, high cards - turned into percentiles of 30,000 random hands, with opening shares by seat. Short Deck gets its own evaluator (flush over full house, trips over straight, A-6-7-8-9) and a ranking of its 81 hands by all-in equity against a random hand; no seat-by-seat ranges, because GGPoker's ante structure could not be checked. Tournament series use the tournament charts.

Rake. Each page says how the house is paid and gives a rule-of-thumb burden; the lobby ranks games from cheapest to most expensive. No published percentages are quoted. What is measured is measured from your own hands: rake from pots you won (the summary line), and for All-in or Fold a fixed fee per hand that no line reports - it shows up as the gap between one hand's closing stack and the next one's opening stack. In the user's 414 AoF hands at $0.05/$0.10 that gap was exactly $0.02 every hand: 20 BB per 100 hands.

Facts that could not be verified (exact formats of Mystery Battle Royale, WSOP Express, Flip & Go, Zodiac Festival) are marked as such on their pages.

My hands (analysis)

Decision log. Every answer - drilled, replayed, imported or reviewed from a screenshot - becomes a DecisionRecord: seat, effective stack, hand, phase, what was chosen and what the chart wanted, each placed on a fold / passive / raise / all-in ladder. That ladder is what turns "wrong" into "too passive" or "too aggressive". Each source is capped separately, so a big import never evicts drill history.

Leak finder. Groups mistakes by spot and direction, ranks them by heuristic BB lost, and titles each by its most common exact error - "You open hands the chart folds", "You min-raise when you should shove" - with the breakdown underneath. Also: a seat-by-stack accuracy heatmap, the hands you miss most, and whether fast decisions cost you. Any leak can be drilled directly.

Hand histories. Importing is never tied to a poker account: PokerCraft sits behind the login, and this app does not sign in anywhere. Chrome and Edge can instead watch a folder (the one the exports are saved to) and take new files in with one click. Or drop a PokerCraft .zip, any number of .txt files, or paste hands, from any game. Hands are sorted by game (AoF hands are numbered AF..., Rush & Cash RC..., tournaments are recognised by event name) and each game gets its own report: results after rake and fees, how often you played each spot against how often the chart would have, mistake patterns, and every hand in one list (what you did, the grade, the result, and at an all-in how the cards ran, with filters for "unlucky" and "lucky" so a well-played loss can be told from a mistake). AoF and Spin & Gold grade first-in shoves and calls of one shove with the solver; cash grades opens and responses to one open; PLO grades first-in decisions; Short Deck reports results only. Tournament hands are graded as below: an unopened pot, the big blind facing one raise, and an open that gets 3-bet. Everything else is counted with a reason - limped pot, walk, cash game, not hold'em - so the report never claims to have judged a spot it has no chart for. Positions are counted clockwise from the button; a raise committing 40% of the stack counts as a shove. Re-importing the same hands is deduped by hand number.

The test fixtures are written from the published format rather than copied from a real export. If a real export ever parses differently, add it as a fixture before changing the parser.

Screenshot review (under Tools). Paste a screenshot of a finished hand, photograph one on another screen, or open a recording of a past session. The table is read on the device by a reader written for this app (src/vision), with no model and nothing downloaded:

  • Cards. Card faces are white blobs. Every corner index inside one is read, so overlapping hole cards come apart. Ranks are matched against glyphs drawn at load time in the fonts installed on the device, compared on a small blurred grid. A "1" and a "0" side by side read as a ten.
  • Suits come from colour first. One blue or green card proves a four-colour deck, and then red can only be hearts and black only spades. On a two-colour deck the symbol's shape decides between the two.
  • Numbers. Stacks, bets, the pot and the blind level are light text found by local contrast, so any theme works. Glyphs are chained into lines and matched against a small alphabet (digits, K, M, B, "/", "$"). Only lines that parse as amounts survive, so player names drop out. Touching bold digits are cut where the pieces read best. A separator followed by three digits groups thousands, whether it was read as a comma or a point.
  • Seats. The dealer button is a light disc with a D. Stacks ring the rail, bets sit between a stack and the middle, and positions go clockwise from the button. The bets say what hero faced: folded to, a single raise in the big blind, or an open that got 3-bet. A gap in the ring of seats warns that the position may be off.

The reading is drawn over the image and fills in the review form. Any card can be corrected with one tap. A correction, or grading with the cards as read, remembers those exact glyph shapes (mtt:glyphs), so the next screenshot from the same client reads better. That is a list of remembered shapes, not training.

Checked against tables painted in fonts the templates are not made from: Gadugi, Segoe UI, Verdana, Arial, Century Gothic and Lucida read cards, stacks, blinds, pot and positions correctly. Known weak spots are fonts with old-style figures (Corbel), screenshots much smaller than 1000 px, card decks with coloured backgrounds, and photos taken at an angle. It has not yet been run on a real GGPoker screenshot.

Session videos. A recording plays and scrubs in the page. "Find the hands" samples it every 1-5 seconds and reads only card corners. A hand is a run of frames with the same hole cards, surviving a few unreadable frames, and each one jumps to its last preflop frame for a full reading. Browser-made WebM recordings, which report no length until played through, are handled.

Play analysis

A game's report (My hands, Games tab) has an "Analyse my play" button that reads the stored hands again (it runs on request because it parses every hand). It reports:

  • Luck at all-ins (analysis/modes/luck.ts): heads-up all-ins with both hands shown, hero's equity when the chips went in (exact from the flop on, a seeded sample before it) against what the board did.
  • Tracker numbers (analysis/postflop/stats.ts): VPIP, PFR, 3-bet, c-bet, fold to c-bet, went to showdown, won at showdown and aggression factor, each with its sample size and a rule-of-thumb 6-max range. Under 30 chances a number is marked "too few hands to judge". The ranges are not targets.
  • Checks and bets a rule of thumb questions (analysis/postflop/aggression.ts): a strong hand checked or bet small, bluffs into several players, oversized bluffs, and flop c-bets against the same texture table the drills use. They are judgement against an estimated range and carry no BB figure.
  • How the pool plays (All-in or Fold, analysis/modes/pool.ts): for every opponent decision in your hands - shove or fold when it was folded to them, call or fold facing one shove - the share per seat, with its 95% Wilson interval, next to what the solver does at 10 BB. When the solver sits outside the interval the difference is real, and the report says what to change: a pool that shoves wider than the solver means call wider; one that calls less means shove wider. Under 20 chances a row says "too few to judge". Your own decisions and opponents far from 10 BB are not counted.
  • Best play against this pool (bestResponse in engine/nash/pushFold.ts): where a pool tendency is clearly different from the solver (enough chances, solver outside the interval), the shove and call ranges are worked out again against the pool's real rates, and the report lists the hands to add and drop per seat. One step of best response, not a new equilibrium: it assumes the opponents keep playing the same way.
  • Where the BB went: BB lost per seat and per situation, with groups of under 10 decisions marked as possibly chance. The mistake list has a hand search, severity and seat filters, and downloads the filtered mistakes (or every hand's result) as CSV.
  • How the hand ended (analysis/handOutcome.ts): at the end of a replay and in an opened mistake, the board, who showed what, who won and with which hand ("a pair of Aces, ahead of Queen high"), and, when you folded, what your own cards would have made on the finished board.
  • By street: accuracy and BB lost on preflop, flop, turn and river from the graded decisions.

Every hero check or bet after the flop also gets a "Your play" card in the replay: the board texture, your hand, your equity against the estimated range, and any finding. Calls and folds still stop the replay and ask.

Dashboard

The logo opens a page about the last day you played (yesterday, if you played): result, sessions, the games, mistakes and how often you matched the chart, the best and worst hand, the unluckiest all-in and the costliest mistake, plus the last two weeks as bars and a week-by-week table (BB/100 only from 30 hands a week). A tournament you finished is a card at the top; the top three get a gold-edged card with a medal. GGPoker does not write the place into the hand histories. It is in the separate tournament summary file that PokerCraft offers next to them ("Tournament #317771713 - ....txt": place, prize, players, buy-in). Drop that file in like any other; it is recognised, read, and tied to the hands by the tournament number the hands carry. The hands must be (re)imported once after this was added, so that each hand knows its tournament.

Honest scope of cash grades

Cash opens and responses are graded against hand-authored charts and shown as "clear mistake" or "borderline", not in BB. An EV model of cash preflop was tried and dropped: built from these same charts it rated almost every hand a small plus from every seat, so its numbers would have looked like measurements and meant nothing. Pricing cash mistakes honestly needs a real solver with post-flop equity realisation.

How grading works

The app has a page for it, linked in the footer and from every opened decision ("How this is calculated"): src/ui/screens/Method.tsx. It reads the grader's numbers from the code (the range shares, the bluff share, the sample sizes), so it cannot drift from what is really done. In short:

  • Kinds of grade: Solved (push/fold solver, EV in BB), Estimate (equity against a range guessed from the opponent's actions, EV in BB), Chart (hand-authored range, no BB figure), Rule of thumb.
  • Price: a call of C into a pot P needs C / (P + C). Equity: preflop from the 169 x 169 table, exact on turn and river, a seeded sample on the flop.
  • Preflop cash: the action is graded against the chart; a raise below 75% of the usual size is an inaccuracy. Push/fold: fictitious play, chip EV, one caller at most.
  • Postflop: facing a bet heads-up. The bettor's range comes from their preflop role, then each action narrows it: a call keeps the strongest 65% plus most draws; a bet keeps a value share by size (narrower on the river, and narrower again for a lead after only checking and calling) plus bluffs
    • draws before the river, and on the river the weakest hands at 8% of bets, because low-stakes players bluff little. An overpair, or two pair or better made with the player's own cards, always stays in a range, so a strong range keeps its aces. Every river grade also shows the equity with no bluffs and with one bet in four a bluff, and is marked close when the answer flips.
  • Your bets and raises (heads-up, after the flop): against checking, or calling and folding. The opponent defends pot / (pot + bet) of their range with the strongest hands plus good draws; a bet is worth fold share x pot + call share x (equity against the callers x final pot - your chips). No re-raise is modelled.
  • Luck: heads-up all-ins with both hands shown and a card to come; luck = result - equity x pot.
  • Grader version (src/analysis/gradeVersion.ts): raised when grades change; stored hands are graded again once, in the background (src/store/regrade.ts).

Prizes (ICM) and custom drills

The push/fold solver values every all-in in chips by default, which is exact for All-in or Fold and the winner- take-all Spin & Gold. Given a payout ladder it values each outcome by prize equity instead (Malmuth-Harville, engine/icm.ts), which is what makes calls tighter near a bubble: the tournament games' charts have a Prizes setting (chips only, bubble, final-table ladder). Equal stacks only, and the pot is shared among the others when you fold, a standard simplification. Each game's Train tab can be narrowed to one situation, one seat and, for Spin & Gold, one stack, so you can drill exactly the spot you keep getting wrong.

Coming back from a replay

A report remembers how you left it (store/reportUiStore.ts): the game, which mistakes were open, the sort and filters, and how far down the page you were, so "Back to the report" lands where you were. The mistake list sorts by costliest, cheapest, newest, oldest, hand or seat. Only solver and equity grades show a BB figure; a chart grade has no EV behind it, so it says how clear the mistake was ("clear mistake", "borderline") and is left out of BB totals instead of showing the same invented 1.2 or 1.5 BB every time.

How far to trust a grade

Grades are not equally reliable, so each carries a label (analysis/confidence.ts): Solved (the push/fold solver, for All-in or Fold and Spin & Gold), Chart (a hand-authored range, no EV behind it), Estimate (postflop equity against a range guessed from the opponent's actions) and Rule of thumb (c-bet buckets). A grade also says what weakens it - Spin & Gold ignores the prize pool, a postflop range is a guess, flop equity is a sample, later streets are not counted - and is marked Marginal when the other choice costs under 0.1 BB (or a chart hand sits at the edge of its range). The label shows in the feedback after every drill answer and on every mistake row. Sample warnings sit beside the report numbers: BB/100 comes with its 95% margin and says when the sample cannot tell a winning from a losing result, a leak built on fewer than 3 mistakes or 10 decisions is flagged as possibly chance, and spot frequencies under 20 hands are marked rough.

Live & replay

Replays. Importing hand histories keeps every hand you played in (the raw text, in IndexedDB, mistakes kept first when over the cap). A replay is the real hand event by event: antes, blinds, every action on every street, uncalled bets returned, the showdown and who won. Stacks, bets and the pot are rebuilt from the start for each step, which makes scrubbing backwards exact, and a test walks a hand to showdown checking that no chip appears or vanishes. It plays like a video, with play, pause, stepping, 0.5-4x speed and a scrubber marked with streets and decisions. At each decision the charts graded, it stops and asks before showing what you actually did. Mistakes from the replay queue play out the action that led up to them.

Range grid. Under the table of every graded decision (replay after answering, drill, opened hand) and in the chart tools: before the flop the chart with each hand's equity against the opponent's range and the price; after the flop the range the opponent was put on, taken apart against your hand (src/analysis/postflop/rangeDetail.ts): your equity and how much of it you are ahead of, what it is made of (click a kind to light those hands), how it narrowed action by action (exploreRange, worked out only when opened), the assumed bluffs striped, the hand they showed down ringed, and per hand the exact combos left and what your cards block.

Review mistakes. An opened session has a "Review N mistakes" button: every hand of the session with a mistake plays in the replay, in the order played, stopping at each graded decision (bets and raises after the flop included) to ask what is best. A bar shows the hand and the way through; the end says how many of the mistakes from the table were answered right this time (src/analysis/mistakeReview.ts, src/store/reviewStore.ts, kept for the tab so a reload carries on).

Live table. Hands are generated with the Lobby's drill types, dealt one after another and played out at table speed until the action reaches you. Then a shot clock runs (off, 20, 12 or 7 seconds), and running out folds, as a time bank would. Answers count exactly like drill answers.

Mistake replay

Inaccuracies and blunders - from drills, imports and screenshots - are scheduled for replay on a 1, 3, 7-day ladder. A right answer climbs a rung, three in a row graduates the spot, and a wrong answer sends it back to the start. Answering before a card is due never promotes it. The Lobby shows what is due.

Tools

  • Stats: VPIP, PFR, 3-bet, c-bet, fold to c-bet, went to and won at showdown, aggression and BB/100 over every imported hand, in total and seat by seat, by game and dates, against the ranges most winning regulars sit in (src/analysis/trackerStats.ts; each hand is read once per visit).
  • Chart explorer: every chart the grader uses - tournament or cash, then opening, facing an open (the big blind's defence is the BB seat here; cash has every seat and the opener) or facing a 3-bet, then the seat - by stack and open size, with the bubble toggle, a hand lookup, and two charts side by side.
  • Equity vs the grader: your hand on a board against the range the postflop grader would put an opponent on: their role and seat before the flop, what they did on each street, the river bluff share. Shows the range's make-up and your equity, with the grader's own functions (src/analysis/postflop/rangeLab.ts, narrowBySteps shared with estimateRange).
  • Pot odds & sizing: what a call needs, how often a bluff must work, what share of a range has to defend, what a call is worth at a given equity, and what a raise asks - for any pot and bet, and a table of the usual sizes (src/analysis/sizing.ts).
  • Equity: hand against hand or range, up to six players. Exact enumeration when every hand is known and at most two board cards are missing; seeded Monte Carlo otherwise, with the sampling margin shown.
  • ICM: Malmuth-Harville equity and a bubble-factor matrix, with presets for a bubble, a final table and a satellite.

Account and sync

Settings has an Account section. With no server it says so and nothing else changes. With one, you sign in (or, on a fresh server, create the first account, which becomes its administrator; after that registration follows the server's REGISTRATION setting: open, invite code, or closed).

  • What is sent: the hands you have imported (their compact text and the summary the lists need), the money results and the sessions. Never a verdict: the account stores no grades, and a device that receives hands works them out again with the same code that graded them, so a better chart later improves old hands everywhere.
  • How: src/sync/engine.ts. Pushing sends what is waiting in the outbox in batches of up to 100 hands, and the server keeps each row by its id and ignores one it already has, so a retry after a dropped connection costs nothing. Pulling reads everything newer than the last change number this device saw and adds what it lacks. Both save their progress after every batch, so an interrupted sync carries on where it stopped. The first sign-in on a device queues everything it has.
  • Deletions are not mirrored onto devices. Forgetting hands removes them where you forget them. The account keeps its copy until you delete it (Settings, Remove my hands from the server), so one tap on one device cannot empty the others.
  • Automatic: after an import the new rows are sent a moment later, if that is switched on.
  • Administrator: the first account can invite people (a code or a link, for so many people and so many days) and disable accounts, from Settings.
  • The session is a cookie (gr_session, HttpOnly, SameSite=Lax) that the browser keeps; the app never sees a token. The service worker never caches /api.
  • Where it is configured: the app calls /api on its own address (nginx on the server, the Vite proxy in development), or VITE_API_URL when the server lives elsewhere.

Settings

Backup and restore in one file. Merge adds what a device lacks without overwriting; replace makes it match the backup. It includes the imported hands kept for replay. What goes into a backup is an explicit allowlist, so the session in progress and the learned glyph shapes (which belong to that device's screenshots) are left out.

Phone app

Installable, and offline once loaded. The service worker is generated at build time by a small Vite plugin: it precaches that build's hashed files under a cache named after them, serves the page network-first so updates arrive, and never touches other origins. Icons come from scripts/generate-icons.mjs (no image libraries) and are committed.

Installing needs the build served over https. vite preview or any static host works; opening dist/index.html from disk runs the app but cannot install it.

Storage

Two places, chosen by how fast the data grows.

localStorage (small, read at start-up): mtt:packs, mtt:stats, mtt:settings, mtt:session:current, mtt:replay, mtt:glyphs and mtt:sync (which account this device mirrors, how far it has read it, and the ids waiting to go up; left out of backups, because it describes this device), each a versioned envelope with a migration switch ready.

IndexedDB (gold-rail, large, written behind): everything an import produces - hands, handTexts, results, decisions, sessions, one object store each, keyed by id. The app opens it and loads the collections before it starts (main.tsx), so every screen still reads them synchronously. A save compares the new list with what is on disk and writes only the rows that were added, replaced or removed, in one transaction: importing 500 hands into a library of 50,000 writes 500 rows. The raw text of each hand, the bulk of the data, is not held in memory; it is read when a hand is replayed, re-graded for its chart, or backed up. The caps (100,000 hands, 200,000 results and decisions) are sanity limits, not storage ones. The first start after the upgrade moves the old mtt:hands, mtt:results, mtt:decisions and mtt:sessions out of localStorage and deletes them there once they are safely written. If IndexedDB cannot be opened the app keeps working from localStorage as before (hand text then lasts for the visit only). An earlier version kept an Anthropic API key under mtt:ai; that key is deleted on start-up. All of it goes through store/persistence.ts and store/bulk.ts.

The mtt: prefix predates the Gold Rail name and stays as it is on purpose: renaming the keys would orphan every stat and imported pack already on disk, for no user-visible gain. It moves only behind a migration.

Glossary

#/glossary explains every piece of jargon the app puts on screen, plus the vocabulary you meet in a GGPoker lobby: 143 terms in nine categories, searchable by the English term, the abbreviation, the German word, or a raw tag name. / focuses the search box, Escape clears it, and the URL carries the query, so #/glossary?q=cbet is a deep link. The tag names on the Analyse overview link straight into it, because that screen is where unexplained words actually turn up.

Laid out as a cheat sheet rather than an article: entries are dictionary-style headwords flowing through two or three CSS columns, with no card chrome, and the route gets a wider container than the rest of the app (useLocation in App.tsx picks it).

The Seats and position section opens with a clickable 9-handed table diagram (ui/components/PositionMap.tsx). Seat definitions are all relative - "two seats right of the button" - which is circular until you can see where the button is. Seats are numbered in preflop action order and clicking one jumps to its entry. The geometry comes from ui/components/tableGeometry.ts, shared with the real drill table so the two can never disagree about where a seat sits.

GGPoker entries describe what each format or feature is, never buy-ins or schedules - those go stale - and the category carries a note saying so.

Content lives in content/glossary.ts, which imports nothing from the engine. The tests do the opposite: they generate 400 spots and assert every tag those spots emit has an entry, and they assert the position map cannot link at a term that does not exist. Adding a template without a glossary entry fails the build rather than shipping a word nobody can look up.

Card notation: T vs 10

The engine speaks T. Every range string, every 169-grid key and every hand code uses it, because thirteen columns only line up when each rank is one character wide, and because that is how poker charts are written everywhere.

Nothing that looks like a playing card is allowed to say T. displayRank() in engine/cards.ts maps T to 10, and card faces, formatCard(), describeCard() and the board list on the result screen all go through it. So a spot reads ATs in the grid and deals a card printed 10, which is what both audiences expect.

Look

A card room at night, drawn flat: near-black chrome with a visible border on every surface, brass for the one thing to press, green baize where a table is drawn. No gradients on controls, no glass, no glow. The rules it follows:

  • Big and readable. The root size is 112.5% and everything is in rem, so one number scales the whole interface. Nothing meant to be read is below 13.5px; body text is 18px, section titles are condensed display type. Targets are at least 40px, with a 3px focus ring on everything.
  • Phone big, desktop dense. From 1024px up the root size is 100% (16px text), panels are tighter and the page is up to 96rem wide. The home page becomes a dashboard: totals, the game picker and the drill button in a sticky column on the left, the report on the right. Report sections (Collapsible) start closed as one row each, two to a row on wide screens, and an open one takes the full width; which are open is remembered per report. A phone keeps the big single column.
  • Fonts ship with the app (@fontsource, Latin only, so it works offline): Atkinson Hyperlegible Next for text, which was designed for legibility, and Barlow Condensed for titles and big numbers.
  • Pictures instead of letters. A hand is drawn as its two cards (HandFace: a pair is a spade and a heart, suited cards are both hearts, offsuit are a spade and a diamond, so black beside blue always means "different suits"; four cards for Omaha). A seat is drawn on a small table (SeatMap: the big blind is always at the bottom, your seat is brass, an opponent is red, the white disc is the dealer button). The words stay in the tooltip and the accessible name, and next to them wherever a line is about the seat itself.
  • Tables are drawn as tables (ui/components/table/TableView): walnut rail, baize, round seats with chip stacks and cards, a pot, the board, a shot-clock ring. Replays and drills share it; on a phone it stands up into a portrait oval. Chips are SVG, so nothing has to load.
  • Each game has an emblem (ModeEmblem), drawn for this app and nothing like any room's own artwork: a fork for all-in or fold, a wheel for the spin, a bolt for fast fold. Colours and marks that carry meaning (a verdict, a fold, a call) always come with a word or an icon too.
  • Ranges are shown as a dot and a band (DotTrack): the figure, and the range it is compared with. Outside the band means a real difference, not luck.

Card faces use real card anatomy: corner index (rank over suit), a large centre pip, and a mirrored index bottom-right on the large size. Four-colour deck with the whole face coloured, as online rooms draw it: spades black, hearts red, diamonds blue, clubs green.

Keyboard

1-5 pick an action, H shows the hint, Space advances to the next spot. There is no auto-advance in drills.

In a replay, Space plays and pauses (or continues after a decision), ← and → step back and forward, and 1-5 answer a decision.