Skip to content

feat(drills): which shapes you get right, per kind - #94

Merged
will-lamerton merged 1 commit into
mainfrom
feat/drills-shape-breakdown
Aug 29, 2026
Merged

feat(drills): which shapes you get right, per kind#94
will-lamerton merged 1 commit into
mainfrom
feat/drills-shape-breakdown

Conversation

@pip-robot

@pip-robot pip-robot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

The per-kind progress piece of drills part B (technology#55). The kind's totals were already on /stats. What was nowhere is the question a player actually has after twenty spots, which is which of these am I getting wrong.

The thing #55 asked for that I did not build, and why

The spec names "you are reading pot odds better than you were". I measured whether that sentence can be said honestly and it cannot, at any volume a player will reach.

The rating is a mean-reverting random walk, so for a player whose reading has not changed at all, the rating still moves. Simulated against both kinds' real difficulty spreads at three skill levels, 60 runs each, reading the change over the last 20 answers once answered >= 50:

threshold fires on a player who has not changed median answers to catch a real +200 step never catches it
80 1.5% to 2.6% of readings 19 0/60
100 0.3% to 0.6% 46 17/60
120 ~0.07% never 41/60

There is no setting that is both quiet on a settled player and awake to a real one, because the rating's climb through a genuine improvement is transient: widen the window to keep the signal in view and the noise widens with it (|Δ| p95 goes 65 at W=20 to 98 at W=60). The information floor is about 100 spots per half, roughly 200 answers, to separate a 100-point change from noise. That sentence is a mood ring at the volumes we have. Written up on technology#55 with the numbers so nobody rebuilds it.

What is here instead, and it is a count rather than an inference

DrillRecord gains a per-shape split of the two counters it already keeps, incremented in recordDrill off drill.settledBy (the same reading that set the answer and the difficulty). /stats prints each shape as the fraction it is:

kickers            7 of 21
the hand rankings  38 of 44

"7 of 21", never 33%. A percentage hides its sample size and invites a comparison between rows that eleven answers cannot support; a fraction carries the sample size in it and lets a player weigh their own numbers. Nothing is ranked, coloured or called a weakness. A shape you have never seen is absent rather than shown at zero. Still a mirror: no clock, no target, nothing to complete.

Rows are driven by the kind's ladder in lib/drills/standing.ts rather than by the keys on the record. Three things fall out: the row and the standing sentence call a shape the same thing, a retired or renamed shape sitting in a year-old profile cannot render a row with no label, and pot odds gets its breakdown for free the day #93 lands.

The generators were measured, not assumed

Over 2,000 seeds a kind: which-hand-wins deals category 50%, kicker 23%, rank 21%, split 5.5%; count-your-outs deals one-draw 56%, two-draws 38%, many-draws 5.9%.

The rarest shape is the hardest one on both ladders, so the row a player would most want is the one that takes longest to appear: at one spot in eighteen, split and many-draws need around 200 answers to clear a floor of 10. That is a real property of the generators and it is the number to change if we ever want the hard shapes readable sooner. A test holds every shape above 2% of spots, because a generator change that drops one to nearly never would leave a row that quietly cannot exist and nothing else would notice.

PERSIST_VERSION 16

Existing records migrate to an empty breakdown, and cannot do anything else: the totals do not record which shapes they were. Splitting them by the measured frequencies above would put four rows on the screen that describe the generator rather than the player.

The sync merge takes the per-shape counters shape by shape rather than wholesale from the busier device, so neither side loses a shape the other has never seen, and correct <= answered survives on every row (it holds on each side, and the larger of two correct cannot exceed the larger of two answered).

The gate, and what it does not cover

pnpm test:all green, 515 tests (up from 505), and pnpm build green.

The tests were checked by breaking the code they cover, not by reading them. Five mutants, all killed: iterating the record instead of the ladder (kills the order test and the drop test), never incrementing a shape's correct, deleting the v16 migration branch, taking shapes wholesale from the busier side, and taking a shape's correct from a different side than its answered.

The UI half is unverified. This ran in a runner with no browser and no device, so the rows under each drill card on /stats have not been looked at on a screen: not the spacing, not the truncation of a long shape label at narrow widths, not how the card reads with four rows under it. That needs a human to click.

One thing worth a real player's eye: the free kind's split row will not appear until about 200 answers, and until then the breakdown shows three rows out of four with no explanation of the fourth's absence. I think silence is right (a row you have not earned is not a row), but it is a judgement and somebody should disagree with it if they want to.

Conflicts with nothing in #93: that touches DrillRunner's render, rating.ts, standing.ts, types.ts and the drills config; this touches the store, the merge, /stats and one line of DrillRunner's pick.

Part of drills part B (technology#55): the per-kind progress piece.

The kind's totals were already on /stats. What was not anywhere is the
question a player has after twenty spots, which is which of these am I
getting wrong. `DrillRecord` gains a per-shape split of the same two
counters, counted in `recordDrill` off `drill.settledBy`, and /stats
prints it as the fraction it is ("7 of 21", never 33%).

No claim is made, so no confidence is needed. A percentage hides its
sample size and invites a comparison between rows that eleven answers
cannot support; a fraction carries the sample size in it. Nothing is
ranked, coloured or called a weakness, and a shape you have not seen is
absent rather than shown at zero.

The rows are driven by the kind's ladder in lib/drills/standing.ts, so
the breakdown and the standing sentence call a shape the same thing, a
retired shape sitting in an old profile cannot render a row with no
label, and pot odds gets its breakdown for free the day it lands.

Measured rather than assumed, over 2,000 seeds a kind: which-hand-wins
deals category 50%, kicker 23%, rank 21%, split 5.5%; count-your-outs
deals one-draw 56%, two-draws 38%, many-draws 5.9%. The rarest shape is
the hardest one on both ladders, so the row a player most wants needs
about 200 answers to clear the floor of 10. A test holds every shape
above 2% of spots, because a generator change that drops one to nearly
never would leave a row that quietly cannot exist.

PERSIST_VERSION 16. Existing records migrate to an empty breakdown and
cannot do anything else: the totals do not record which shapes they
were, and splitting them by the measured frequencies would put numbers
on the screen that describe the generator rather than the player.

The sync merge takes the per-shape counters shape by shape, so neither
device loses a shape the other has never seen, and `correct <= answered`
survives on every row.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying pip-web with  Cloudflare Pages  Cloudflare Pages

Latest commit: 77568aa
Status: ✅  Deploy successful!
Preview URL: https://2a98ec4e.pip-web-9oj.pages.dev
Branch Preview URL: https://feat-drills-shape-breakdown.pip-web-9oj.pages.dev

View logs

@will-lamerton
will-lamerton merged commit 1fbb748 into main Aug 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant