diff --git a/design/docs-md/requirements.md b/design/docs-md/requirements.md index 1db7b7eb..0b84db3f 100644 --- a/design/docs-md/requirements.md +++ b/design/docs-md/requirements.md @@ -61,10 +61,10 @@ Each requirement is testable and traces to the [engineering spec](spec.md) (§) | ID | Level | Requirement | Trace | |---|---|---|---| | R-40 | MUST | Each completed lap deals one card from a seeded, shuffled multi-deck shoe; the deal is deterministic and replayable from the stored seed; empty shoe reshuffles with an audit entry. | §4 | -| R-41 | MUST | Standings rank entries by best 5-card hand from all held cards, jokers fully wild, five of a kind above royal flush, and — on equal class and kickers — fewer jokers above more, so a natural five of a kind beats a wild one of the same rank; ranking table per §5 stored locally. | §5 | +| R-41 | MUST | Standings rank entries by best 5-card hand from all held cards, jokers fully wild, five of a kind above royal flush, and — on equal class and kickers — fewer jokers above more; ranking table per §5 stored locally. | §5 | | R-42 | MUST | The hand algorithm handles 0–2+ jokers and any card count up to the cap; whole-field evaluation (180 entries × 12 cards) completes in under 1 s. | §5/§11 | | R-43 | MUST | Identical hands — same class, same kickers and same joker count — resolve by the configured tie-break order. Where high-card draw is configured, the finish has already drawn one card per tied entry, so a fully drawn group orders by the highest drawn card (rank first, then suit, spades highest; `cards.draw_key`) and renders that card in the results. A tie the sort still cannot separate — no drawn cards, or more tied entries than the 52-card deck holds — is flagged "draw required" — the results window's ⚠ badge, explained on a row double-click — never silently ordered. | §5 · [resultsframe](xrc-windows.md) | -| R-44 | MUST | Evaluator self-test (7,462 ranks + joker vectors + five-of-a-kind ordering, natural above wild, plus a compare() total-order sweep and a best_hand() joker-count bound — six checks) runs at launch and on demand from Help. Five checks are BLOCKING and the whole-field 180×12 timing check is ADVISORY — it measures the host's speed, never the evaluator's correctness, so a slow machine can never strand a multi-hour ride. A failing blocking check no longer blocks finishing outright: the finish route names every failed check and the operator may "Finish anyway", which records those names on the finish event (`self_test_failed_checks`) and marks the results self-test unverified — shown in the Standings window and in the HTML/PDF exports. Cancel leaves the ride unfinished and finishable. | §12 · [selftestdlg](xrc-windows.md) | +| R-44 | MUST | Evaluator self-test (7,462 ranks + joker vectors + five-of-a-kind ordering, plus a compare() total-order sweep and a best_hand() joker-count bound — six checks) runs at launch and on demand from Help. Five checks are BLOCKING and the whole-field 180×12 timing check is ADVISORY — it measures the host's speed, never the evaluator's correctness, so a slow machine can never strand a multi-hour ride. A failing blocking check no longer blocks finishing outright: the finish route names every failed check and the operator may "Finish anyway", which records those names on the finish event (`self_test_failed_checks`) and marks the results self-test unverified — shown in the Standings window and in the HTML/PDF exports. Cancel leaves the ride unfinished and finishable. | §12 · [selftestdlg](xrc-windows.md) | | R-45 | MUST | Team-mode scoring: every lap's card credits the **team entry**, never an individual rider — on `rider_pooled` a member's laps deal into the team's pooled hand (R-16); on `team_relay` the entry's crossings deal into the team's hand (R-22). Riders never hold or score a hand of their own. | §1/§5 | ### 6 · Resilience diff --git a/design/docs-md/spec.md b/design/docs-md/spec.md index 1df02efd..d32f8cee 100644 --- a/design/docs-md/spec.md +++ b/design/docs-md/spec.md @@ -64,7 +64,7 @@ The shoe's jokers follow the ride's **jokers mode** (`ride.jokers_mode`, the set | # | Hand — best → worst | Example | |---|---|---| -| 1 | Five of a Kind, wild or natural | 9 9 9 9 ★ | +| 1 | Five of a Kind (wild card only) | 9 9 9 9 ★ | | 2 | Royal Flush | A K Q J 10 suited | | 3 | Straight Flush | 8 7 6 5 4 suited | | 4 | Four of a Kind | Q Q Q Q 7 | @@ -78,7 +78,7 @@ The shoe's jokers follow the ride's **jokers mode** (`ride.jokers_mode`, the set Within a class, standard kicker comparison — the 7,462 distinct natural ranks are stored as one integer per entry, so sorting the field is a plain sort. Ship the rank table locally; self-test on startup against known vectors (wheel straight, joker five-of-a-kind, …). -**Natural beats wild.** Among hands of equal class and kicker, the hand that uses fewer jokers wins. A natural hand beats an equal wild hand, and one joker beats two, two beats three, up to five. This is part of hand strength, so `hands.compare` applies it before the ride's laps/time tie-breaks, and the pair is never a "draw required" tie. A wild is played only where it improves the hand, so a surplus joker is left unused and an equal-rank natural hand always wins; a *natural* five of a kind also beats a *wild* five of a kind of the same rank. +**Natural beats wild.** Among hands of equal class and kicker, the hand that uses fewer jokers wins. A natural hand beats an equal wild hand, and one joker beats two, two beats three, up to five. This is part of hand strength, so `hands.compare` applies it before the ride's laps/time tie-breaks, and the pair is never a "draw required" tie. A wild is played only where it improves the hand, so a surplus joker is left unused and an equal-rank natural hand always wins. ``` best_hand(cards): # any n — R-16 pools are uncapped @@ -154,7 +154,7 @@ Within `eval5` (exactly 5 cards) and a partial hand (fewer than 5), a joker alwa | A | Five of a Kind, aces | | (none) | Five of a Kind, aces — the `k = 5` row of the pseudocode above | -**Card cap X** (optional per ride): only the first X dealt cards score; later laps still count for laps/time. Entries holding fewer than 5 cards still rank: their cards form the best partial hand, and a missing kicker always ranks below any present one (a 4-card ace-high sits under every 5-card ace-high). A multi-deck shoe can deal one entry two physically identical cards, and within that entry's own hand they rank exactly as the physical cards they are — a pair, three, or four of a kind, or a flush whose kickers happen to repeat a rank, never a dealing error — with five identical cards, wild-assisted or all natural, either way Five of a Kind. Hands this produces outside the 7,462-entry natural table order the same way every hand does: by class, then by the standard kicker comparison. **Ties** between identical hand ranks — same class, same kickers *and* the same joker count — resolve by the ride's ordered rules, applied to a FINISHED ride's results: the stored order defaults to ① high-card draw ② most laps ③ shortest total time. The venue's draw is real, not deferred: `RideEngine.finish()` draws one card per tied entry from one fresh 52-card deck (`cards.high_card_draw`, no jokers) seeded from the ride's stored `rng_seed` salted with a fixed constant (`ride._TIEBREAK_DRAW_SEED_XOR`, so the draw replays from the one stored number yet can never coincide with the shoe's own shuffle), records the cards as an audited `tiebreak_draw` event, and `snapshot()` hands each entry its own card as `EntryResult.tiebreak_card`. `standings` orders a tie group whose every entry holds its drawn card by `cards.draw_key` — rank major, suit minor, spades highest (clubs < diamonds < hearts < spades), 52 distinct keys — so the highest card wins and is the decider, and the drawn card renders in the Standings window's Draw column and in all three exports. A group with even one undrawn entry keeps the barrier, and a pair the sort still cannot separate (no drawn cards, or more tied entries than the 52-card deck holds) is flagged “draw required” at the venue. `reopen()` and a continue discard the draws, so a corrected ride redraws afresh at its next finish; a replayed `tiebreak_draw` restores them from its payload. A ride that is not yet finished ignores the stored order — its standings auto-rank ① most laps ② shortest total time, never a live venue draw. The order is editable after the finish; standings re-run instantly. Hand names render in one style everywhere — title-case em-dash ("Four of a Kind — Nines", "Full House — Aces over Fours"), produced by `rivercrossing.standings.hand_name` (E6.1.1; the frozen results-window sample row and the golden exports agree on it). +**Card cap X** (optional per ride): only the first X dealt cards score; later laps still count for laps/time. Entries holding fewer than 5 cards still rank: their cards form the best partial hand, and a missing kicker always ranks below any present one (a 4-card ace-high sits under every 5-card ace-high). A multi-deck shoe can deal one entry two physically identical cards, and within that entry's own hand they rank exactly as the physical cards they are — a pair, three, or four of a kind, or a flush whose kickers happen to repeat a rank, never a dealing error — with five of a kind only via a wild card; five natural identical cards are four of a kind. Hands this produces outside the 7,462-entry natural table order the same way every hand does: by class, then by the standard kicker comparison. **Ties** between identical hand ranks — same class, same kickers *and* the same joker count — resolve by the ride's ordered rules, applied to a FINISHED ride's results: the stored order defaults to ① high-card draw ② most laps ③ shortest total time. The venue's draw is real, not deferred: `RideEngine.finish()` draws one card per tied entry from one fresh 52-card deck (`cards.high_card_draw`, no jokers) seeded from the ride's stored `rng_seed` salted with a fixed constant (`ride._TIEBREAK_DRAW_SEED_XOR`, so the draw replays from the one stored number yet can never coincide with the shoe's own shuffle), records the cards as an audited `tiebreak_draw` event, and `snapshot()` hands each entry its own card as `EntryResult.tiebreak_card`. `standings` orders a tie group whose every entry holds its drawn card by `cards.draw_key` — rank major, suit minor, spades highest (clubs < diamonds < hearts < spades), 52 distinct keys — so the highest card wins and is the decider, and the drawn card renders in the Standings window's Draw column and in all three exports. A group with even one undrawn entry keeps the barrier, and a pair the sort still cannot separate (no drawn cards, or more tied entries than the 52-card deck holds) is flagged “draw required” at the venue. `reopen()` and a continue discard the draws, so a corrected ride redraws afresh at its next finish; a replayed `tiebreak_draw` restores them from its payload. A ride that is not yet finished ignores the stored order — its standings auto-rank ① most laps ② shortest total time, never a live venue draw. The order is editable after the finish; standings re-run instantly. Hand names render in one style everywhere — title-case em-dash ("Four of a Kind — Nines", "Full House — Aces over Fours"), produced by `rivercrossing.standings.hand_name` (E6.1.1; the frozen results-window sample row and the golden exports agree on it). **Mixed rides rank two sections, Teams and Solo (Phase 3).** A team's pooled cards would dominate most solo hands, so teams rank against teams and solos against solos — never one combined field. `standings.rank_by_kind` runs the ranking once per kind (teams, then solos), each section numbered from 1 with DNF entrants excluded (R-65). The results window and both exports render the split — the HTML/PDF full fields carry a "Teams" section and a "Solo" section (a kind absent from the ride has no section) — and the §15 standings CSV's `type` column labels each row (`place, plate, entry, type, sex, laps, hand, draw[, total_time]` — `draw` being the card the entry drew for the venue's tie-break, blank for every entry that drew none). diff --git a/docs/user-guide.html b/docs/user-guide.html index e210fa4e..569abda5 100644 --- a/docs/user-guide.html +++ b/docs/user-guide.html @@ -269,7 +269,7 @@

When the ride is finished

Hand rankings

This list is the exact order the app uses, strongest first. A hand lower on the list beats every hand below it.

    -
  1. Five of a kind — needs a joker, or five identical physical cards from a multi-deck shoe.
  2. +
  3. Five of a kind — needs a joker (wild).
  4. Royal flush — A-K-Q-J-10 of one suit.
  5. Straight flush — five in a row of one suit.
  6. Four of a kind.
  7. @@ -281,7 +281,7 @@

    Hand rankings

  8. One pair.
  9. High card.
-

Duplicates across one entry are legal. A multi-deck shoe can deal the same card twice, so 9H 9H is a pair of nines. Two entries can also hold the same card code, because the shoe is shuffled, not dealt from a single deck.

+

Duplicates across one entry are legal. A multi-deck shoe can deal the same card twice, so 9H 9H is a pair of nines. Five natural cards of one rank score as four of a kind, because five of a kind needs a joker. Two entries can also hold the same card code, because the shoe is shuffled, not dealt from a single deck.

Scoring references

These sources describe the rules the app follows. They corroborate the hand rankings and the shuffle.