by ken · built Librarymo.de · 107 versions, latest Sep 13
A poker coach that deals you hands, watches how you play, and adapts its teaching to your weak spots. Built with Claude Code for Mac, headed for iPhone.
About this project (the project's own write-up)
Poker Tutor
A poker coach that watches you play, finds the holes in your game, and keeps raising the difficulty as you close them. Mac first, iPhone and iPad from the same codebase later.
Full plan: ~/.claude-personal/plans/linked-swimming-bunny.md
Where things stand
| Phase | What it is | Status |
|---|---|---|
| 0 | Cards, shuffling, hand ranking, equity | done |
| 1 | Betting engine, side pots, tournament structure, ICM | done |
| 2 | Computer opponents with personalities and a skill dial | done |
| 3 | The coach: mistake detection, leak taxonomy, skill rating | done |
| 4 | The Mac app you can actually see and play | done |
| 5 | Assessment, drills, ladder, hand history | done |
| 6 | Cash game mode | engine and table done (no-limit, pot-limit, fixed-limit; carry-over stacks, rebuys); the cash coaching is next — docs/SPLIT-PLAN.md |
| 7 | iPhone and iPad | builds and runs in the simulator; never been on a phone |
| 8 | The App Store, iPhone then Mac | blocked on an Apple Developer account — App/AppStore/SUBMISSION.md |
| 9 | Android, from the same Swift | builds, installs and plays on a Pixel 7 emulator; never been on a real phone |
Phase 5 is finished in the sense that all four things exist, are tested and can be used end to end: sit an assessment, read the scorecard it produces, turn any row of it into a drill, get a check back days later, and climb a ladder of tables off the hands that came out of all of it. What is not finished is anybody having played a few hundred hands through it, which is the only thing that will say whether the bars are set in the right places.
Phase 7 is honest rather than modest. The phone app compiles and runs, every screen lays itself out for a thumb, and the suites cover the parts it draws — but nothing has been on a real device, so nothing is known about how it feels to play a hand on one, and "runs in the simulator" has never been the same claim as "works".
Running it
./Scripts/build-app.sh # builds build/PokerTutor.app — double-click it, or `open` it
./Scripts/build-app.sh release cash # builds build/PokerTutorCash.app, the cash product
open App/PokerTutorPhone.xcodeproj # the iPhone app: two schemes, PokerTutorPhone and PokerTutorCashPhone
Two products come out of this one codebase — Poker Tutor (tournaments) and Poker Tutor
Cash — told apart by AppVariant, which the app shell chooses once. Each has its own bundle
id, history folder, settings keys and subscription. The cash app lets the player pick the blinds
and how betting is limited (fixed-limit, pot-limit or no-limit), sits everybody down with a
hundred big blinds, carries stacks from hand to hand, buys the busted back in, and counts the
session in big blinds when they cash out. The engine takes the structure in HandConfig;
pokerlab --format cash --structure fixedLimit sits the command line at that table.
Both are free for the first hundred hands, the forty-hand assessment included, and then a yearly
subscription. What the free hands buy is deliberately one thing: the player's most common
habit — not the most expensive, which is how the scorecard itself ranks — named in full with its
drill open, while every other habit on the scorecard, the report and the drills list is present
but blurred, so somebody can see how much else was found without being told what. FreeView in
PokerSession is the one place that decides; every screen asks it and none works it out alone.
The naming rule does not bend for it: a habit is shown only once the hands support naming it, and
with nothing proved yet the most common watched habit is shown and labelled as not proved.
The phone app runs in the simulator as it stands. Putting it on a real iPhone needs an Apple ID set as the signing team in Xcode — nothing else about it changes.
Android
The Android app is the same Swift, compiled for Android by Skip and drawn
through Skip's SwiftUI. Nothing about it is written twice: Sources/PokerTutorAndroid is a
twenty-line shell around RootView, Skip.env names the app, and Android/ is the Gradle
project Android needs to start anything at all.
export JAVA_HOME=/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home
export ANDROID_HOME=~/Library/Android/sdk
export PATH=~/.swiftly/bin:$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
~/Library/Android/sdk/emulator/emulator -avd pokerphone & # a Pixel 7, once it exists
gradle -p Android installDebug # build the app and put it on the phone
adb shell am start -n com.thedigitaladlab.pokertutor/poker.tutor.android.MainActivity
Everything Android-only in Package.swift is declared only when ANDROID_HOME is set, so the Mac
and iPhone builds neither fetch nor link any of it. Three things Skip taught us the hard way, all
recorded in the code where they bit: every module with views in it must carry a Skip/skip.yml
and the skipstone plugin or its views draw as nothing; view state and view types cannot be
private; and a public signature cannot say UserDefaults bare, because the plugin shadows the
name. If a Gradle build "succeeds" but the app looks stale, grep its log for error: — a failed
Swift step does not always fail the build. Scripts/android_port.py applies the whole recipe to a
sibling repo, which is how PLO Tutor got its Android app.
Not there yet: table sounds (silent on Android), Google Play billing (the paywall says subscriptions are not available on this device), the tab icons Skip cannot map, and a real phone.
Tables are nine handed: you and eight opponents. Tournaments start with nine and pay three.
swift build -c release
./.build/release/pokerlab test # all test suites
./.build/release/pokerlab test --filter "side pots" # one suite
./.build/release/pokerlab hand --players 6 --seed 12 # deal and play one hand
./.build/release/pokerlab tournament --players 9 # play a tournament to a winner
./.build/release/pokerlab shootout # measure the opponents against each other
./.build/release/pokerlab ablate --style solidRegular # what each parameter is actually worth
./.build/release/pokerlab diagnose # where the chips go in one matchup
./.build/release/pokerlab coach --player bb --hands 1000 # read the coach's verdicts
./.build/release/pokerlab assess --player bb # sit a scripted player through an assessment
./.build/release/pokerlab sweep --player control --hands 10000 --seeds 16 # where every bar came from
--player picks a scripted player with one deliberate flaw: bb folds every big blind,
limps never raises first in, river never value bets the river, turn never value bets the
turn, flop folds to every flop bet, hero calls every river bet, minbet and overbet size
every flop bet wrongly in opposite directions, loose plays far too many hands, control has no
flaw at all. Every one of them exists so a bar can be measured against both the player who has the
habit and the player who does not; a bar set from one population only is how three of these were
wrong.
Give each one enough hands to clear the floor its habit sits behind, or the coach will correctly
say it cannot tell yet and you will read that as the coach missing it: bb 1,000, limps and
loose 420, flop 1,500, turn 4,000, river 8,000, hero 12,000, and control 14,000 before
silence means anything.
Layout
Sources/
PokerCore/ the rules of poker — no UI, no platform code. The iPhone app reuses this untouched.
PokerAI/ the opponents: styles, the skill dial, what they infer from watching you
PokerCoach/ mistake detection, the leak taxonomy, the skill rating
PokerSession/ one sitting at a table: deals hands, runs the opponents, feeds the coach,
writes every hand down, and reads a stack of them back as analysis
PokerUI/ every screen, for both platforms, plus the table sounds
PokerTutorApp/the Mac app — a window around PokerUI, and nothing else
TestKit/ a small test harness (Apple's frameworks need a full Xcode install; this doesn't)
PokerTests/ the suites
pokerlab/ command-line driver
App/ the iPhone and iPad app — one file, and an Xcode project around it
The rule that keeps the iPhone port cheap: no poker logic in the app layer. PokerCore knows
nothing about screens, and TableSession — which is what actually plays a session — knows nothing
about SwiftUI. AppState only decides which screen is showing. That is also what makes a session
testable: the suites drive TableSession by calling act exactly as a person would, so the parts
the app merely draws are covered by tests rather than by clicking.
AppState itself is driven the same way now. It has a second initialiser that takes a session and
makes no noise, and one method — stepOpponent — that is the dealer's own work with the waiting
taken out, so the suite plays a hand exactly as the window does and can then ask what the window
would be showing. That was worth adding the day a table that kept dealing to an empty chair survived
four rounds of review: everything about it was true only while somebody was clicking.
Two things that look like details and are not:
- Anything remembered about a player is keyed by who they are, not where they sit. Seats are rebuilt every hand as players bust, so a seat-keyed lookup starts putting the wrong style badge under the wrong name a few hands in.
- An illegal action is refused and changes nothing — including the coach's record. A mis-click must never be filed against the player as a mistake.
- The opponents are dealt out one decision at a time on a timer, so a hand can be watched instead of just arrived at. That is pacing only: a test plays the same seed both ways and asserts the poker is identical, because the day it is not, every measurement in the project is wrong.
- The opponents' playing styles are never shown at the table. Working out who is loose and who is
a nit is the read the player is there to learn to make; printing it under their name hands them
the answer.
TableSessionstill knows, for the coach and the tests. - The coach interrupts only when it has something to say. A hand played well gets a thumbs up on the felt and deals on — stopping someone to tell them they did nothing wrong costs attention and teaches nothing.
- Leaving the table by the sidebar or the tab bar pauses it. The opponents stop acting, and a hand that finishes while the player is on another screen opens nothing: the review waits under the felt for them to come back. Nothing is discarded — a tournament somebody is forty hands into is not something to throw away because they glanced at their stats — and nothing is dealt to an empty chair. The Leave button is still the way to put a sitting down.
The blind ladder is written out rather than generated — 25/50, 50/100, 100/200, 200/400, 500/1000, 1000/2000, 2000/4000, 4000/8000 and up. Every level, and every ante, is payable in the chips that are actually on the table; a 150 blind is not a level anyone would run when the smallest chip is 25.
Everyone bets in the same denominations. The opponents round every bet to whole chips through the one place their actions are already made legal, so a bot never pushes out 437 next to a chip tray that cannot make 437. The chip in play scales with the blinds, the way a real tournament colours the small ones up and takes them off the table.
Bets are counted out in chips — 25 up to 10,000 — rather than dragged on a slider. Click the hundred twice and the fifty once and you have bet 250. It costs a click or two more than a slider and is far more deliberate, which is the point: a bet you had to count out is a bet you thought about. Undo puts the last chip back, All in counts the stack out for you, and the Bet button stays unavailable until what is on the table is actually a legal raise.
There is one screen layout, not two. Metrics decides the sizes from the space available, and the
controls get larger on a phone, not smaller — a thumb needs more room than a pointer.
The table sounds are synthesised by Scripts/make-sounds.py rather than recorded, so nothing has
to be licensed and the repo stays self-contained.
The history
Every hand is appended to hands.jsonl in Application Support as it finishes, one JSON object per
line. A line per hand rather than a database, for a specific reason: a write cut off half way loses
one hand instead of the history, and a test asserts exactly that. Appending finishes the broken
line before it starts a new one, which is not fussiness: a torn write leaves the file ending without
a newline, and writing straight onto the end of that fused the next hand into the fragment and lost
it too — so a crash cost two hands, the second of them played after the crash, in the one file
format chosen specifically to survive being cut off half way. It is also plain text — anyone can
open it and check the app is telling them the truth, which matters for a tool whose whole job is to
be honest about someone's game.
Saved hands are made of strings and numbers, never the game's own enums. A hand recorded today has to still be readable after those types have been renamed, and they will be.
Resetting the history moves the old file to hands.previous.jsonl rather than deleting it. Reset
is one tap away from a year of hands, and a mis-tap should not be the end of them.
Both screens default to everything ever played, not the current sitting. Defaulting to the sitting meant that on a fresh launch — a brand new, empty sitting — a full history looked like no history at all, which is exactly how it was first reported: "hands does not seem to be storing history". It was storing it. It was filtering it out.
The Hands and My game screens both read the history rather than memory, scoped to this sitting, to one earlier sitting, or to everything. The numbers are the ones a poker player already knows — VPIP, PFR, showdown rates, big blinds per hundred, by seat — so they are comparable with what they read anywhere else. Under 200 hands the screen says so rather than letting a win rate off 30 hands look like a fact.
My game leads with the scorecard: the player's weaknesses, worst first, ranked by what they have actually cost in big blinds. It is built from the written-down hands rather than from the coach's memory, which is what lets it follow the same scope bar as everything else on the screen and be there again tomorrow — memory starts empty every launch, so a scorecard read off it tells someone with a year of hands behind them that they have no leaks. Four refusals run through it and they are all the same refusal: say only what the hands support. A mistake seen twice is never ranked above a habit seen forty times — it is put under a heading that says so, with the number of chances it would take to call it. A cost that cannot be worked out from what was recorded is reported as unknown, never as nought, because nought reads as "this one is free". How often a habit happens is counted over every hand in view and what it costs is not: an assessment sits somebody in the big blind and on a short stack far more often than a real table does, so "about 8.0 for every 100 hands you play" is worked out over the hands that were dealt rather than arranged, and where every hand in view was arranged the money is left blank. The rate is the fair half of an assessment and the money never was — the app already kept those hands out of the win rate, the VPIP and the by-seat table, and then let them into the one figure it prints in big blinds. And a player with nothing much wrong is told exactly that instead of being handed a manufactured list; the empty scorecard is the good outcome, and a coach that invents work to look busy is one nobody believes twice. Each row also says which seat the mistake mostly comes from, but only where it really is mostly one seat — three in five, off at least three instances — rather than finding a pattern in noise.
The per-hand screens agree with that card, which took a third state to arrange. The coach speaks only about a mistake worth stopping the game over; the scorecard counts anything it put a price on at all, because opening too many hands costs a third of a big blind at a time and is ruinous forty times an hour. Those two bars are different on purpose and the higher one is not moving. What was wrong was the other end: a hand holding three counted instances of the habit at the top of somebody's list was shown to them as "clean", with "no comment" against each decision and the replay saying calling it a mistake would be guessing. Every screen they opened to check the coach's work denied the finding. So a hand now reads clean, counted or flagged, a decision reads too close to call, counted or flagged, and the coach still says nothing at the table about the middle one.
Two numbers are called "rating" and they are both on screen at once on a Mac, so they are named apart rather than reconciled. The one in the corner is every hand ever played; the ladder's is at most the last four hundred at the table you are on now, and it is the one a promotion turns on. A forty-hand assessment moves the first and cannot touch the second, so they disagree, and "you rate 47, you need 52 to move up" beside a permanent "RATING 62" reads as an app that has forgotten to promote somebody. Making them one number would mean either promoting off hands played at a different table or throwing away the all-time figure, and both are worse than a label.
Before there is a hand behind it, neither of them is a number at all. An empty history rates everybody at 50 on purpose — the ladder needs somewhere to place a new player from — but the middle of a scale is not a finding, and Home used to draw "50" in inch-high type beside "Solid amateur" over "0 hands watched", about somebody the app had never seen play. It is a dash now, with no label and no bar, which is the same refusal the statistics panel three inches away already made.
Seats are numbered rather than only named. The small blind is 1 and the dealer is always the last number, so "seat 3 of 9" says the thing "UTG" was standing in for: where you are sitting and how many people still get to speak after you. The traditional names stay alongside the numbers, because everyone else at a card table uses them, and picking a seat on the My game screen draws the table and says in plain words who acts after you from there. Heads-up is numbered big blind 1, dealer 2 — the button posts the small blind two-handed, and numbering it 1 would make the number go down as the seat got better. A saved hand records how many were dealt in as well as which seat, because the cutoff is seat 8 of nine and seat 5 of six and a tournament table passes through both; that field is optional, so every hand written down before it existed still reads.
Any hand can be played back a step at a time, forwards or backwards: the shared cards come out as
they came out, the pot grows the way it grew, every action is attributed to a numbered seat, and the
coach's read sits under the decision it was actually about. It is deliberately not the table — it
steps in both directions, nothing is on a timer, and it never makes a sound, because a screen
somebody opened to think quietly on is not a screen that should start dealing chips at them. The
transcript this needs is written down with the hand — who did what, in which round, and what the pot
stood at afterwards — as optional fields, so hands recorded before it existed still load and simply
offer the summary instead. Adding a required field would have been the expensive mistake here:
HistoryStore.load skips any line it cannot decode, so one non-optional addition would have deleted
every saved hand with no error message anywhere. A test loads a line written in the old format and
fails if it ever stops reading.
The reconstruction is in PokerSession, not in the screen, for the same reason as everything else:
a test plays a hand through a real session, saves it, reloads it the way the next launch would, and
asserts the replay reproduces the same rounds of betting, the same actions in the same order, and
the same pot after every one of them.
The assessment
Forty hands with a beginning and an end, which produce a starting rating and the first list of what a player's game is costing them. It is a measured sitting rather than a game, and the whole of what that means is said before it starts rather than in a footnote at the end.
Each hand is set up to look at whatever is least understood so far. Three things are chosen: which seat the player sits in, how many big blinds everybody has in front of them, and which of the eight opponents sits where — the last one because sitting the people who raise constantly on somebody's right is how you get them a hand that starts "somebody has raised, now what". The cards are never chosen. That is the line the whole measurement rests on: within any one situation the hands dealt are as random as they are anywhere else, so how often the player got a decision wrong in that situation is a fair number however often they were put there. What it is not fair for is money — they spend far more time in the big blind and on a short stack than anyone does at a real table — so assessment hands are counted towards the leaks and kept out of the win rate, VPIP and by-seat tables, with the screen saying so rather than letting the numbers quietly stop adding up.
Which hand to deal next is a greedy choice: for each way of setting the table up, add up how likely it is to produce each kind of decision, weighted by how little is known about that kind of decision, and take the best total. Two things in that sentence were learned the expensive way.
The first is that "how likely it is to produce" cannot be a fixed belief. A setup that promises river decisions and then never gets anybody as far as the river will keep promising them for forty hands, and the first version of this dealt the same setup forty times waiting for a river that was never coming. What actually happened now outvotes what was expected after about three hands of it.
The second is that "how little is known" cannot only be a standard error. A situation seen four times has a respectable-looking standard error and is worth nothing, because nothing can be said off four chances — so until there have been six, another hand there is worth exactly as much as a hand in a situation nobody has ever been in. Above that floor, a situation whose error rate is sitting above where a steady player sits pulls harder than one that came back clean, because forty hands cannot measure eleven situations properly and the ones that are already settled should be left alone.
The report is in two tiers and the split is the honest part. Something is named only when even the cautious end of what those chances can show is still worse than a steady player; everything else that turned up too often is listed as where to look first, saying in as many words that it is not proved. Forty hands clears the first bar for a loud habit — playing far too many hands shows up in every unopened pot — and often cannot clear it for a quiet one, because folding the big blind too much only shows up when somebody raises at it and the hand was worth continuing with, which is six to eleven chances in a sitting this long. The rating comes with the range the hands actually support rather than as a number on its own, and it is held near the middle on purpose: at forty hands only a fifth of what they say is let through, because forty hands cannot tell an unlucky evening from a real weakness. The situations it never got near are listed by name, because silence about a spot nobody was ever in reads as "you are fine there".
The report and the scorecard used to be two bars for one question, and on a fresh install they contradicted each other on the same screen. The report names a habit when even the cautious end of what its chances can show is still worse than the bar, which an assessment can clear off eight; the scorecard names one when there have been as many chances as the catalogue says it takes, which for that habit was twenty-five. So the first thing anybody ever did produced a card reading "One habit was already clear enough to be worth naming" beside another reading "Nothing confirmed yet", over a row whose own note said nothing is called a habit here until there have been 25. Neither bar moved. The report carries the row instead — printing its own numbers and saying where they came from, exactly as a failed check does — and only where the hands in view have too few chances to speak for themselves. Past that they are the better evidence and they decide, in either direction.
Stopping half way costs nothing. The hands played stay in the history as the ordinary evidence they are, and the run is rebuilt by replaying them — there is no second copy of the progress to fall out of step, because there is no second copy. A test stops a sitting at fifteen hands, throws the session away, builds a new one off the same file the way a fresh launch would, and asserts the picture of what has been seen came back identical.
The drills
Any habit on the scorecard turns into a set of hands that do nothing but hand you that one situation. The arrangement is the assessment's, aimed at one spot instead of eleven: the table is set up whichever way is likeliest to produce the decision, and if that arrangement stops producing it — which happens, because beliefs about poker are wrong all the time — it is dropped after about three hands rather than repeated for forty. The cards are never chosen here either.
The arrangement is chosen per habit, not per situation, and the difference is not a nicety. Three habits live in "an unopened pot" and two of them want opposite ends of the table: playing too many hands is a first-seat problem and not attacking the blinds is a last-seat one. Ranked on the situation alone, all three went to the first seat to speak — where every hand is an opening chance and almost none of them is worth raising, so twenty-odd correct folds finished the drill and finished it for a player with the habit too, while the sheet a few lines above the felt said "seats 9 and 8, the button and the cutoff". Where a habit names the arrangement it needs, that one is the only one on offer and the yield above cannot wander off it, because for those habits the seat is what the habit is. Six seeds of the full drill, sittings cleared, sound player against the matching flawed one: playing too many hands 6 and 0, limping 5 and 1, not attacking the blinds 5 and 3. The last of those is honest rather than good, and why is worth knowing — the habit can only show itself in the third or so of unopened pots that were worth raising, while the number of chances it takes to name it is counted over all of them. Making that tighter means re-deriving two numbers for all eighteen habits from a sweep rather than editing one.
When it ends is the whole design. Not after a round number of hands: twenty hands of practice prove nothing if the situation only came up twice, and one good decision proves less than that. It ends when two things are true at once — the rate is at or under what a steady player manages, with one imaginary slip added to the count so the arithmetic errs against clearing, and there have been at least as many chances as the catalogue already says it takes to name that habit. Both are needed. Without the rate it would clear on time served; without the sample it would clear on four hands of luck. That second number is per habit, so folding the big blind too often takes 50 chances to clear and folding to every re-raise takes 15, and the drill tells you how many more it wants as it goes. Get one wrong and it wants a few more, which is the point of it. A drill that runs its full length without clearing says so in as many words, because a drill that reported a fix there would be lying about the only thing it is for.
The banner that says a sitting cannot finish today now says why, because there are two reasons and it used to blame the player for both. Too many slips is one. The situation simply not coming up often enough per hand is the other, and it is the common one — on hand one of a turn drill, with nothing played and nothing wrong, the banner read "Too many have gone wrong for this sitting to clear it". The two are told apart by running the same arithmetic again with the slips taken out: if a spotless record would not have reached the finish either, the deal is what is in the way, and the sentence says that instead.
Clearing it is not the end. An evening of getting something right while you are watching for it is not the same as having stopped doing it, so clearing puts a check in the diary five days out rather than closing the matter. The check is twelve chances at the same situation and then an answer either way — a check allowed to run until it liked the result would not be a check — and one slip does not fail it, because twelve chances cannot tell one bad decision from a habit coming back. The check is judged on the number it prints, which sounds like a truism and was not one: it used to be judged on the raw rate while the screen showed the rate with the imaginary slip in it, so a check could read "It held up — 2 of 12 went wrong" beside "which makes it 21%, against the 18% a steady player sits at", with the bar past the mark. The forgiven slip is now the only place the answer and the printed number part company, and the report says which of the two carried it rather than leaving somebody to work out why 14% passed against an 8% bar. Pass and the gap doubles: the first check asks whether the fix survived the week, the third asks whether it survived not thinking about it at all. Fail and the habit goes back on the scorecard with the row saying exactly what happened, rather than being quietly forgotten. Reopening something needs less evidence than naming it did, and deliberately: it was already measured well enough to be worth drilling. The numbers on that row are the check's own, and they have to be: drills are read over everything ever played while the scorecard follows the scope bar, so pointing the screen at one sitting used to produce a row under the heading "what is costing you" whose next sentence read "This has not come up yet".
Where the diary lives is the same answer as the ladder and the assessment: nowhere. Every drill hand records which habit it was drilling and whether it was a drill or a check, and the date the next check is owed is counted back off those hands. There is no second file holding a date that could disagree with what was played, which is exactly why a check scheduled for next Tuesday survives the app being closed — the evidence for it survives, so it does. A test writes a cleared drill to a file, opens it again the way the next launch would, and fails if the check is not sitting there waiting. The clock is passed in rather than read, so a fortnight of scheduling is tested in a millisecond.
Drill hands are kept out of the leak scorecard, and that is a judgement rather than an oversight. Practice at one situation, with you watching for it, is not how you play when it turns up unannounced — and letting those decisions into the count would let anybody drill a habit off their own record without playing a single hand of poker differently. They stay out of the win rate too, for the same reason assessment hands do, and the screen says so. What counts is the check days later.
The bar is demanding rather than impossible, and it was measured rather than guessed: across four seeds a sound player clears the big blind drill three times in four, and a player who folds every big blind clears it none. Both halves of that matter — a drill nobody can finish teaches as little as one everybody finishes.
The ladder
Five tables, and what makes one harder than the last is who is sitting at it. That is not a stylistic choice — the shootouts measured style as worth roughly ten times what the skill dial is worth, so a rung is written down as a cast of eight people rather than as a number. The bottom rung seats seven players who call too much and raise too little; the top seats none of them. The skill each of them plays at climbs alongside, second, and is pulled into whatever band the style can exist in, because a calling station is a losing style by definition and cannot be an expert one. The cast is fixed and the seating is not: the same eight kinds of player are shuffled into different chairs every sitting, so two evenings at the same table are not the same evening twice.
Promotion and demotion are both real, and both need two hundred hands at the current table before anything happens. Somebody can win a whole tournament and still be exactly where they were, which is the point — over forty hands a good player and a lucky one are the same player. The rating that moves you up off a rung is always twenty points clear of the one that would send you back down to it. Without that gap, a player sitting on a bar gets carried up on one sample and back down on the next for as long as they keep playing, and a ladder that moves somebody every fortnight in alternating directions has told them nothing. A test feeds it evenings that alternate between well past the promotion bar and well under the demotion one and fails if the player is ever moved.
Where the standing lives is the same answer as everywhere else here: nowhere. Every hand records the rung it was played at, and the standing is counted back off the file, so there is no second copy to disagree with the hands. Everyone starts at the bottom unless there are already two hundred hands saying otherwise — the rating off a forty-hand assessment is held near the middle on purpose, and placing somebody off it would be reading a number the hands do not support. Assessment hands are not results at all: the seat and the stacks were chosen, so they carry no rung and can never move anybody.
Finding this needed one real bug fixed on the way. A hand can be over the moment it is dealt — once a stack is short enough that the blind puts it all in and everybody else covers it, nobody has a decision to make, so nothing ever steps the hand and nothing closed it. No review, no row in the history, and the tournament was never told anybody busted, so the table dealt hand after hand that finished before they started, for ever. It was only reachable at a table loose enough to leave somebody sitting on a stack that small, which is exactly what the bottom rung is.
Reading it
The colours can be switched to a scheme with no red-green pairing anywhere, because the default says good in green and bad in red — the one pairing eight percent of men cannot separate. Chips carry their value in writing as well as in colour, so the colour is always a shortcut rather than the only signal.
The type comes in four sizes, small through extra large, chosen in Settings by reading a sample of each. Everything moves, but not all of it moves the same distance: the coach's write-ups — the reason the app exists — go all the way, while words with a fixed-size box around them stop at Large. A seat name, a chip and a button under the felt cannot grow past the thing they are printed on without pushing the table off the screen, so they don't.
Any poker word the app could not avoid can be tapped, and the plain English opens where you are reading rather than on some other screen — the statistics on My game, the old seat names, the words in the coach's write-ups. Only the first appearance in a paragraph is underlined: a paragraph with every occurrence marked is one nobody reads. The whole list is in Settings to be browsed instead of stumbled on. None of this is a licence to write jargon — it is there for the words that have no plain equivalent, which is essentially the statistics, and for the traditional seat names now printed under the numbers.
The rule is enforced rather than remembered: a test walks every word the screens show and every line
the coach writes and fails if either uses a term with nothing behind it, which is how "range",
"shove" and "heads-up" came out of the drills. That test used to stop at the edge of the screens,
and the hole showed. It walked the leak catalogue, the assessment, the ladder and the drills —
everything with a testable string on it — while the writing a person actually reads sat in Text(…)
calls no test could open, and the commit whose whole purpose was taking one poker word out of the
app left that same word on the My game screen. The screens' prose now lives in Copy, in PokerUI,
and the suite reads it. What moved there is the writing: sentences, paragraphs, the subtitle under a
heading. What stayed where it was is the labels — buttons, column headings, a number with a word
beside it — because there is no sentence in them to get wrong. That is the limit, and it is worth
knowing: a poker word used as a button label would still have to be caught by reading.
The same suite runs a second list over all of it, and over every comment in Sources: the handful of
phrases that are banned outright rather than merely unexplained. Those are habits of writing rather
than vocabulary, they spread by being copied, and each of the ones on the list was found by hand in
one file after surviving in four others.
Notes for anyone reading the code
- Every hand is driven by a seed, so any hand can be replayed card-for-card. The hand history browser and the tests both depend on this.
HandEngineis the only thing that decides what is legal. Bots, the coach and the UI all readlegalActions()and callapply(_:)— none of them re-implement the rules.- Antes are dead money, tracked separately from wagers. Treating them as bets creates a phantom side pot the big blind wins back every hand, and can stall a tournament permanently.
- Uncalled bets are refunded against the current betting round, not the whole hand — dead money otherwise disguises part of the overbet.
- Nothing that decides anything may read a dictionary or a set in its own order. Swift shuffles
that order every time the program starts, so anything downstream of it answers differently on
each launch from the same seed. A range of hands is stored as a dictionary, and two things read
its keys in order: drawing a sample for an equity estimate, and breaking the tie between two
hands of exactly equal strength when a bet narrows the range. Both were therefore giving the
coach a slightly different answer every run — same seed, same cards, different verdict. It
surfaced as a check count that would not sit still between two runs of the same binary, which is
a small symptom of a large problem: a hand that cannot be replayed to the same conclusion makes
every measurement in the project a measurement of the launch it was taken on.
Combo.orderis the fixed order, and the keys go through it before anything is done with them. - A test that loops over whatever a session produced has to check the loop ran. An assertion inside a loop over an empty list is an assertion that never happened, and the suite reports it as a pass. Several of the loops here now count first and fail on nought.
The cash app's coach
Poker Tutor Cash reads a second edition of everything above that depends on the game: the habit
catalogue (the two short-stack habits go; putting a whole stack in with one pair, calling a re-raise
with a small pair the money behind does not pay for, and folding a hand it does pay for come in),
the rating line (a cash table charges a sound player about fifteen big blinds a hundred to a
tournament's ten, because every pot is deep, so the two ratings are not comparable across the
apps; one decision is charged at most thirty, and a
preflop fold at most six, because a single misjudged four-hundred-blind pot should not rate
anyone zero), the ladder (four stakes, 25/50 to 500/1000, promotion on the same four hundred hands), the
assessment's arrangements (two hundred big blinds in the cutoff instead of eleven in the hijack),
the lessons and the tutorial. The bars were re-swept at the app's own nine-handed cash tables and
the bands are in Leaks.swift beside each number; docs/SPLIT-PLAN.md has the ledger. A player
who puts a whole stack in with one pair on every flop costs sixty big blinds a hundred by this
pricing and rates ten, the bottom of the scale, which is where that player belongs — though the
habit itself is priced and not named, because no sound player ever reached the twelve stack-off
spots its bar would need, so the bar was never measured against one.
Pick fixed-limit on the setup screen and the app reads a third edition, because fixed-limit is a
different game and none of the arithmetic about bet sizes applies to it. A bet is a fraction of the
pot by rule, so the price of a call is nearly always right and the expensive habits are folds: a
draw let go for one small bet, a river folded to one bet in a pot of twelve. A bet folds almost
nobody, so raising is how the best hand is paid, and the player who acts last can raise a draw on
the flop and, about half the time, see the turn card for free. So the limit coach judges with its own ranges
(ReferenceRanges.Limit: wider everywhere, raise-or-fold first in, cold-call only with hands that
play, defend the big blind very wide, cap with the top of the deck), its own postflop judgement
(LimitEvaluator.swift: one bet or raise on offer, a fold rate a few percent, the free card priced
at a few tenths of a small bet — the raise is about even with a call on price alone, and earns
its keep from the pots it takes down and the extra bet in when the draw arrives), its own habits (folding a draw to one bet, folding the turn with the
price, calling two bets cold, calling the last bet with nothing, folding the river to one bet,
capping light — and two it prices but does not name, calling with the best hand instead of raising
and missing the free-card raise, because the sweep could not separate the player who has each from
the player who does not), its own lessons, and bots that play the game (wider, calling at the
price, raising top pair, the free-card raise, calling down with a pair). The tournament and the
no-limit cash coach are byte-for-byte unchanged by it: the tournament control still reads 150
decisions and 13.2 bb/100 on seed 4242, and the no-limit cash control's sweep is identical between
the code before and after. On the limit line a sound player costs about nine big blinds a hundred
and rates 66; the player who folds every draw costs forty-three and rates 10.
What the measurements showed
Every claim about the opponents is measured, not asserted. Two things are worth knowing before changing any of it.
Measuring poker is harder than playing it. A straight match cannot tell a small skill edge from a run of good cards inside any sane number of hands. Matches are therefore duplicated — every hand is dealt to both sides with the seats swapped, and each seat's random choices are reseeded identically, so two identical bots measure at exactly zero rather than approximately zero. A test asserts that. Full tables rotate the measured player through every seat on the same deal. Without this, tuning is just reacting to noise, which is what the first three attempts here turned out to be.
Two of these measurements were passing on luck, and only admitted it when chip rounding reshuffled the seeds. A single 700-hand match between an accurate reader and a careless one swung between 1 and 26 bb/100 depending on nothing but the seed. The rating comparison was worse: at 420 hands it called the flawed player correctly five times out of ten — a coin flip — and needs four thousand hands before it separates cleanly. Both now measure across several seeds at a sample that means something. The suite takes about twenty seconds as a result, which is the price of the numbers being true.
And 2,500 hands was still not enough for the first of them. The fix for that 700-hand match was 2,500 hands across three seeds, which read as stable because the three seeds it happened to pick were. Swept properly — 24 seeds, same bots, same 2,500 hands — it runs from -2.6 to +38.4 bb/100, and on one seed in twenty-four the accurate reader loses outright. The same 24 seeds at 6,000 hands run from 12.8 to 37.7, mean 23.8, which is the number this measurement has always been claimed to be. So the test is six seeds at 6,000 hands now, and it prints the mean and all six seeds as it passes, because a number nobody can see without breaking something is a number that drifts.
That is also how the bars under it were chosen, rather than by taste. The per-seed bar is 5.0 and the mean bar 15.0: the first is under half the worst of the 24 seeds measured, the second is three and a half standard errors below the measured mean. Both are far enough below the truth to survive an unlucky evening and far enough above nought to catch a real loss of the skill dial — the bar they replaced was "every seed above nought", which would have failed on a seed with nothing wrong with the build, and passed a version of the app with two thirds of its skill dial missing.
Chip rounding itself cost nothing measurable: the readError edge went from 23.2 to 22.2 bb/100 over 6,000 duplicated hands, which is inside the variation between seeds.
And the coach was naming a habit in a player who did not have one. pokerlab coach --player
control names the leaks it finds in a scripted player with no planted flaw at all, and it kept
naming "not betting the river with the best hand". The first thing to establish was whether that was
one unlucky seed, because this project has twice been burned by concluding from a single run. It was
not: swept over twenty seeds at 1,000 hands the control player was named for that habit in fifteen
of them. Nor was it a false positive in the narrow sense — at 4,000 hands over ten seeds the control
player really does miss river value 15% to 23% of the time (mean 19.3), against a bar that said a
steady player sits at 15%. The bar was inside the band of the player it existed to clear, so every
extra hand made the wrong answer surer rather than rarer, and no minimum sample could have fixed it.
Both numbers were re-derived from the same sweep: the same measurement puts a player who never bets
a river at all at 38–46% (mean 40.2), so the bar goes between the two at 30%, and the chances it
takes to say anything go from 12 to 25 — because at 12 chances the same flawless player reads
anywhere from 0% to 42%. What that costs is real and is the right price: at 320 hands the coach now
says nothing about the river either way, because 320 hands cannot see it. The test moved with the
numbers — it used to catch the planted river player at 420 hands, where the flawed player measured
31% and the sound one 29%, which is to say it was passing on a margin that would have convicted
somebody with nothing wrong.
The habit next door had exactly the same shape, and it took a lot more hands to fix. Over 4,000 hands the control player misses turn value 13–21% (mean 17.7, twenty seeds) against a bar that said 18%, so the same fault was sitting on "not betting the turn with a good hand". A planted player who checks every turn he is allowed to check settles at 25–33% (mean 29.0) on the same measurement, and at 8,000 hands the two are 15–21% and 28–31% — so the real rates are about 17.5% and 29.4% and everything below 4,000 hands is noise around them.
The sample was the expensive half. Measured at four sizes, twenty seeds each, the two populations overlap almost completely until about 250 turn decisions:
| chances | control | never bets the turn |
|---|---|---|
| 25 | 5–35% | 17–38% |
| 50 | 7–36% | 18–36% |
| 110 | 10–28% | 21–34% |
| 270 | 13–21% | 25–33% |
There is no honest bar until the bottom row, so the bar goes at 24% — three points above the worst of twenty control seeds and one below the best of twenty flawed ones, catching 20 of 20 flawed sittings and convicting 0 of 20 clean ones — and the chances it takes to say anything go from 14 to 250, which is roughly 3,800 hands of poker. Until then the habit sits under "too soon to call" with its count on show and no accusation attached. That is a real cost and it is the right one: the app can wait, and a player told their turn play is leaking when it is not stops believing the rest of the list.
Two things fell out of that measurement. The control player at 420 hands is now handed one habit rather than two, because the second one was this bar. And a full ninety-hand drill at the turn habit produces only five to thirteen turn decisions, which is nowhere near enough to say anything either way — so the drill's ending has a third answer in it now ("not enough to say") instead of telling somebody their habit is still there off six chances. Making that drill actually deal turns is the next measurement, and it is a real one: the arrangement promises 0.40 turn decisions a hand and delivers about 0.09.
Then the same fault turned up in five more habits at once, and the reason nobody had seen it is worth more than the fix. Every sweep up to this point read the coach's profile at the end of a long run. A player does not read it at the end. They read it after three hundred hands, and after eight hundred, and a bar that convicts somebody at three hundred and clears them again by four thousand has still told them something untrue — the finishing rate never shows it. So the sweep now replays the decisions in the order they happened and applies the coach's own three tests after every one, and what it writes down is the worst the running rate ever reached once a habit had a given number of chances behind it. That number is nothing like the finishing rate. Folding the big blind finishes at 11-14%; on the way there it passes through 28% with twenty-five chances behind it, and the bar was 18% over 25.
Sixteen seeds of ten thousand hands of the control player, who has no planted flaw at all:
| habit | old bar | finishes at | worst running rate, from 25 / 50 / 150 chances | named |
|---|---|---|---|---|
| Folding the big blind too often | 18% over 25 | 11-14% | 28% / 24% / 22% | 10 of 16 |
| Folding to the first bet too often | 20% over 14 | 10-17% | 25% / 21% / 18% | 3 of 16 |
| Not betting the river with the best hand | 30% over 25 | 16-23% | 41% / 34% / 27% | 2 of 16 |
| Calling flop bets with too little | 18% over 16 | 4-9% | 20% / 13% / — | 1 of 16 |
| Bluffing into people who cannot fold | 15% over 14 | 3-8% | 13% / 10% / — | 1 of 16 |
Each was fixed from that table and from a matching sweep of the player who has the habit. Folding the big blind goes to 30% over 50 chances, against a player who folds every big blind measuring 53%. Folding to the first bet goes to 30% over 25, against 48-58% for a player who folds to every flop bet. The river-value bar was right and its floor was not, so that goes from 25 chances to 200. Calling flop bets and bluffing keep their bars and take their floors to 50, which is a change that can only ever make the app quieter and so needs the clean measurement and nothing else.
"Paying off on the river" was the one a reviewer found, and it needed a new player built first. A bar can only be set by measuring the player who has the habit against the player who does not, and there was no player who calls every river bet. With one, at 8,000 hands over fourteen seeds: control 15-27% (mean 21.4), a player who calls every river bet 34-56% (mean 48.7). At 16,000 hands the two settle at 17-23% and 43-56%. The whole difficulty is the sample —
| chances | control | calls every river bet |
|---|---|---|
| 65 | 11-33% | 30-60% |
| 90 | 13-27% | 32-56% |
| 120 | 15-27% | 34-56% |
— because at 65 chances the two bands overlap and no bar exists there. The bar goes at 30% and the floor at 150, which is where the running rate of a flawless player finally drops clear of it.
And one habit cannot be measured at all, so it is never named. "Betting the wrong amount" had a bar of 25% over 25 chances and the control player measured 20-29% (mean 24.9), which is the same fault again — except that re-deriving the bar from two populations does not fix it, because the two populations are the same population. Twelve seeds of 4,000 hands: no planted flaw 20-29%, a player who min-bets every flop 0-5%, a player who shoves his whole stack on every flop 22-34%. A shove is unambiguously the wrong size and moves the number three points; a min-bet reads better than playing well, because the coach's own preferred size is often the minimum and a size counts as wrong whenever it differs from that by a single chip. Separating a three-point difference from a four-point seed spread would take on the order of a hundred thousand hands. So the habit carries a flag saying it may not be named, and the app is silent about it — no row, no watch list, no drill — while what a badly priced bet costs still counts towards the rating. A habit that can never be named honestly is better than one named wrongly, and the honest version of this one is silence.
The sweeps are a command rather than a one-off script, because the next person to move a threshold should have to run them:
./.build/release/pokerlab sweep --player control --hands 10000 --seeds 16
./.build/release/pokerlab sweep --player hero --against control --hands 8000 --seeds 14
The suite holds both halves of this now, at samples where each claim is honest. Each planted player is caught at the sample its own habit's floor needs — 420 hands for the loud ones, twelve thousand for paying off the river — and the "and nobody else" half is one sweep of the whole catalogue against a player with no flaw, long enough that every floor in it is actually cleared. That last part is the bit that used to be missing: the run the reviewers used was 320 hands against a floor of 14, and the river-call habit produced 10 to 13 chances, so it passed by never being asked. The test now fails if any habit was left unjudged, as well as if any was named. The two short-stack habits are the exception and are called out as one: this table is a hundred big blinds deep from the first hand to the last, so they never come up and their bars are unswept.
Difficulty comes mostly from who is at the table, not from a number. Style beats skill by an order of magnitude: a competent regular beats a table of calling stations by well over a hundred big blinds per hundred hands, and beats a better version of its own style by single digits. That is a real property of poker rather than a shortcoming, and it is why the ladder replaces the opponents as you climb rather than just turning a dial.
Two things were tuned into the bots and then measured out again, and both are recorded in the code so they do not come back:
- Making the "expert" version of a style play wider ranges and bluff more made it lose to its own novice. Wider ranges only pay if you can play the extra hands well after the flop. Skill now only removes things that are unambiguously mistakes.
- Erratic bet sizing turned out not to be a leak against these opponents at all, so it is no longer treated as one.
The backbone of the skill dial is readError: how far off a player's judgement of their own hand
is. It is the one parameter that costs money against everyone, and the ablation confirms it
carries the difference.
Versions (107 — latest: Seat mode: My game says where you lose most and why, any seat can be drilled every hand, named only once the hands support it.)
- Version 107 — Sep 13, 4:41pm — Seat mode: My game says where you lose most and why, any seat can be drilled every hand, named only once the hands support it. latest
- Version 106 — Sep 13, 2:09pm — Seat mode: My game says which seat you lose most from and why, and a drill sits you there every hand until it stops
- Version 105 — Sep 13, 2:01pm — Checkpoint: My game names the seat you lose most from, and any seat can be drilled
- Version 104 — Sep 13, 2:00pm — Checkpoint: a drill can be aimed at a seat as well as a habit
- Version 103 — Sep 13, 1:55pm — The seat diagnosis: which seat most decisions go wrong in, with bars swept against a flawless player and one planted bad in seat 4
- Version 102 — Sep 13, 1:42pm — Checkpoint: a per-seat diagnosis off the dealt hands (bars still to be measured)
- Version 101 — Sep 12, 2:41pm — The free tier: 100 hands including the assessment; a free player sees their most common habit in full, every other habit counted but blurred, and one drill; a subscription opens everything.
- Version 100 — Sep 12, 2:40pm — The listings, the README and the allowance notice say what the free hands buy: your most common habit, and the drill for it
- Version 99 — Sep 12, 2:33pm — The free tier on the screens: one habit in full, the rest blurred behind a lock, one drill open, the price a tap away
- Version 98 — Sep 12, 2:26pm — A hundred free hands, every hand counting, and the one rule that decides what a free player is shown: their most common habit
- Version 97 — Sep 12, 2:23pm — The plan for the free tier: 100 hands, the most common habit in full, the rest blurred, one drill
- Version 96 — Sep 12, 1:50pm — A test holds the sound cash player under one interruption in twelve hands at every structure; the lab can now list every review of a run and break the charges down by situation.
- Version 95 — Sep 12, 1:49pm — A sound cash player is stopped on fewer than one hand in twelve, and a test holds it there
- Version 94 — Sep 12, 1:42pm — Checkpoint: a sound cash player reads a bet as a bet, and the lab can say what the coach charged by kind
- Version 93 — Sep 12, 1:30pm — Bigger, consistent type: every piece of text has a role with a floor (body 14, captions 12 at the normal setting) that the text-size setting scales; My game raised throughout; a test catches any screen that bypasses it.
- Version 92 — Sep 12, 1:29pm — Every piece of type plays a role, and My game is set at a size somebody can read
- Version 91 — Sep 12, 1:19pm — Cash review fixes: the money-behind rule now depends on your seat and the raiser's stack, every cash bar re-measured, lessons' arithmetic corrected, recommended bets rounded to real chips, a preflop fold can cost at most 6 big blinds.
- Version 90 — Sep 12, 1:18pm — The cash review's fix round: the money-behind rule knows the seat and whose money it is, a stack-off fold is a flop fold again, the confidence check builds the candidates the verdict built, and both cash scales are re-anchored
- Version 89 — Sep 12, 12:25pm — Poker Tutor Cash store kit: sixteen screenshots cut unattended (eight per product, the tournament set refreshed), the App Store listing, and the cash app's About and privacy links point at its own pages.
- Version 88 — Sep 12, 12:24pm — The cash app's About and privacy links go to its own pages, and its store listing is written
- Version 87 — Sep 12, 12:24pm — Eight screenshots per product, with the felt and the review played by the app itself from a fixed seed
- Version 86 — Sep 12, 12:08pm — The fixed-limit coach: limit ranges, a limit evaluator (the price of a draw, raising for value and free cards, calling down, capping), seven limit habits with bars from sweeps, limit lessons; no-limit, pot-limit and tournament coaching untouched.
- Version 85 — Sep 12, 12:07pm — The fixed-limit rating line from the limit calibration, and the record of what the limit measurements showed
- Version 84 — Sep 12, 12:02pm — The fixed-limit coach: its own edition of the ranges, the habits, the lessons and the bots, every bar swept at the app's limit table (checkpoint; rating line still provisional)
- Version 83 — Sep 11, 6:47pm — Cash coaching for no-limit and pot-limit: implied odds, not stacking off with one pair, a stakes ladder (25/50 up to 500/1000), cash lessons and tutorial; every cash habit's bar set from sweeps at cash tables. Tournament coaching untouched.
- Version 82 — Sep 11, 6:28pm — The cash coach: the money behind, the stack that goes in with one pair, a ladder of stakes, and every bar re-swept at the app's own cash tables
- Version 81 — Sep 11, 5:22pm — Android brief: two products per repo now
- Version 80 — Sep 11, 4:43pm — Two products from one codebase: Poker Tutor (tournaments) and Poker Tutor Cash (pick stakes and fixed/pot/no-limit, buy in 100 big blinds, rebuys, cash out with a session result). Tournament app unchanged.
- Version 79 — Sep 11, 4:42pm — A real cash table: pick the blinds and the limit, sit down with a hundred big blinds, keep what you win, buy back in when you bust, cash out with a result
- Version 78 — Sep 11, 4:25pm — Two products from one codebase: Poker Tutor and Poker Tutor Cash, each with its own identity, history, keys and store product
- Version 77 — Sep 11, 4:20pm — The engine deals no-limit, pot-limit or fixed-limit, and a short big blind no longer lowers the price
- Version 76 — Sep 11, 4:11pm — The plan for splitting each game into a tournament app and a cash app
- Version 75 — Sep 11, 4:09pm — A dealer button on the felt in front of the button seat, and SB/BB markers in front of the blinds.
- Version 74 — Sep 11, 4:07pm — A dealer button and the two blind chips on the felt, in front of the seats that hold them
- Version 73 — Sep 11, 3:42pm — Home screen explains its numbers: which hands each card counts, what the rating is made of, a 0 explained; the rating can no longer be dragged to zero by habit penalties; "Read the report" is readable.
- Version 72 — Sep 11, 3:42pm — A note for the Android session on what is landing on main and how to share it
- Version 71 — Sep 11, 3:27pm — A rating that cannot be sunk to nought by its habits, and a Home that says which hands each of its numbers counts
- Version 70 — Sep 11, 10:14am — Android polish: every icon draws (hand-drawn tab glyphs), table sounds work through Skip's audio library, launcher icons and Google Play listing text ready, release bundle builds; Mac suite still 442 tests
- Version 69 — Sep 11, 10:03am — Keep the Gradle cache folder out of saved versions
- Version 68 — Sep 11, 10:03am — Android: every icon draws, the table has its sounds, and the store listing is written
- Version 67 — Sep 10, 7:40pm — Poker Tutor now builds and runs on Android through Skip: same Swift, tutorial, home and a tournament hand verified on a Pixel 7 emulator; Mac suite still 442 tests
- Version 66 — Sep 10, 7:40pm — README: how the Android app is built and run, and what Skip taught us
- Version 65 — Sep 10, 7:39pm — Poker Tutor runs on Android, from the same Swift, through Skip
- Version 64 — Sep 10, 2:26pm — The player's two cards get a row of their own under the felt, like PLO Tutor, with a test that fails if they ever overlap
- Version 63 — Sep 10, 2:18pm — 1.0 is for the iPhone only, as decided on 2026-08-09
- Version 62 — Sep 4, 12:43pm — Keep the build scratch folder and the private submission notes out of the saved project
- Version 61 — Sep 4, 8:38am — Show The (Digital) Ad Lab splash on startup (shared with PolyTerm/KalTerm)
- Version 60 — Aug 24, 1:14pm — The app deals a different tournament every time it opens
- Version 59 — Aug 23, 8:57am — A row of stats that cannot outgrow the screen it is on
- Version 58 — Aug 22, 9:37pm — A bet you can read, which needs more room than a seat has
- Version 57 — Aug 22, 9:12pm — A definition is about the hand it was asked in, and no longer
- Version 56 — Aug 22, 9:00pm — Diamonds go orange, and the same fault in the colour-blind deck
- Version 55 — Aug 22, 8:49pm — Bigger cards in the middle, and the reason they stop where they do
- Version 54 — Aug 22, 8:38pm — A colour per suit, and a card you can hold up to read
- Version 53 — Aug 22, 4:36pm — The line that was stranding the table, which was always the bug
- Version 52 — Aug 22, 4:10pm — Four things from the first evening on a phone
- Version 51 — Aug 22, 3:19pm — The introduction is read before anybody can turn the type up
- Version 50 — Aug 22, 2:35pm — The conversion request is in, and the reason it took two weeks
- Version 49 — Aug 10, 12:29pm — The registered name has a comma in it
- Version 48 — Aug 10, 12:10pm — The entity written down the way Apple will ask for it
- Version 47 — Aug 10, 11:51am — The legal name where it is a legal claim, the brand where it is a brand
- Version 46 — Aug 9, 11:34pm — Stop pinning development signing, now that there is a real account
- Version 45 — Aug 9, 11:58am — The listing written down, and the things Apple asks before it will read it
- Version 44 — Aug 9, 11:58am — The price, and four places the app refuses to ask for it
- Version 43 — Aug 9, 11:58am — Apple's half of it, kept where it cannot decide anything
- Version 42 — Aug 9, 11:57am — A count of free hands that Start over cannot reset
- Version 41 — Aug 2, 7:16pm — Anchor the rating on the game the app actually ships
- Version 40 — Aug 2, 6:30pm — A way to sit a bot of any strength in the hero's seat
- Version 39 — Aug 2, 6:22pm — The coach speaking, which is the shot the whole listing needed
- Version 38 — Aug 2, 6:20pm — The table screenshot, taken by hand
- Version 37 — Aug 2, 5:49pm — Branding where it is seen, the actions first, and the store screenshots
- Version 36 — Aug 2, 5:18pm — Drills get their own place, an icon, and the Ad Lab's name on it
- Version 35 — Aug 2, 4:50pm — A way into the drills that does not require a habit first
- Version 34 — Aug 2, 4:40pm — The basics nobody says out loud, and a way to practise each one
- Version 33 — Aug 2, 4:20pm — The coach's writing is set a step above everything else
- Version 32 — Aug 2, 4:09pm — The coach's writing and the table grow with the setting too
- Version 31 — Aug 2, 12:24am — Document the sample each scripted player needs before silence means anything
- Version 30 — Aug 1, 11:26pm — No bar names a habit until a player with none has been measured against it
- Version 29 — Aug 1, 9:38pm — The table stops when nobody is looking at it, and the turn bar is measured
- Version 28 — Aug 1, 8:53pm — Price a habit over the hands that were dealt, and stop the screens denying the card
- Version 27 — Aug 1, 7:45pm — The coach stops naming a habit in a player who does not have it
- Version 26 — Aug 1, 7:12pm — Drill the habit rather than the situation, and judge a check on the number it prints
- Version 25 — Aug 1, 6:20pm — The suite reads the screens, and a seeded hand plays out the same twice
- Version 24 — Aug 1, 5:49pm — The drill stops claiming more than twelve chances can pay for
- Version 23 — Aug 1, 5:36pm — One rating, one leak count, and one thing on screen at a time
- Version 22 — Aug 1, 4:58pm — Turn a leak into a drill, and check back days later that it stuck
- Version 21 — Aug 1, 4:31pm — Climb a ladder of tables where the people change, not a number
- Version 20 — Aug 1, 4:09pm — Sit down for forty hands and find out where your game is
- Version 19 — Aug 1, 3:37pm — Play any hand back, a step at a time
- Version 18 — Aug 1, 3:26pm — A scorecard that ranks your leaks by what they cost
- Version 17 — Aug 1, 3:18pm — Tap any poker word and read what it means
- Version 16 — Aug 1, 3:01pm — Seats you can find without being taught the words
- Version 15 — Aug 1, 2:46pm — Text you can actually read, in four sizes
- Version 14 — Jul 31, 9:56pm — Blinds on a ladder that the chips can actually pay
- Version 13 — Jul 31, 12:47pm — A way in for new players, settings, and history that stops hiding itself
- Version 12 — Jul 31, 9:57am — Opponents bet in whole chips too, and two tests stop passing on luck
- Version 11 — Jul 30, 6:47pm — Bet by counting out chips
- Version 10 — Jul 30, 6:18pm — Feedback the moment the hand is over for you, and a Review button that opens
- Version 9 — Jul 30, 3:48pm — A reset button, and stop telling the player who they are up against
- Version 8 — Jul 30, 2:20pm — Bet-size buttons that are actually different from each other
- Version 7 — Jul 30, 1:19pm — Put App/ outside the Sources listing in the README
- Version 6 — Jul 30, 1:19pm — Keep every hand, and let the player pick their game apart
- Version 5 — Jul 30, 1:11pm — The iPhone app, and a table you can watch and hear
- Version 4 — Jul 30, 12:43pm — The Mac app: a nine-handed table, and the coach speaking after every hand
- Version 3 — Jul 30, 12:20pm — The coach: find the mistake, price it, and explain it in plain English
- Version 2 — Jul 30, 12:00pm — Opponents: eight styles, a skill dial, and a way to measure both
- Version 1 — Jul 30, 11:21am — Poker engine: cards, hand ranking, betting, side pots, tournaments
Files (235 · 11.8 MB)
- Android/34 files · 73.4 KB
- App/34 files · 9.7 MB
- Scripts/6 files · 47.5 KB
- Sources/154 files · 1.9 MB
- docs/3 files · 30.4 KB
- .gitignore7.4 KB
- Package.swift5.2 KB
- README.md56.2 KB
- Skip.env790 bytes
Open a version to see exactly what changed in it, or download the zip for everything.
Get it on your computer
With the Library Mode tool: librarymode get ken/poker-tutor
Or tell Claude Code / OpenCode: "get ken/poker-tutor from Library Mode"