Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions design/docs-md/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions design/docs-md/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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
Expand Down Expand Up @@ -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).

Expand Down
6 changes: 3 additions & 3 deletions docs/user-guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ <h3>When the ride is finished</h3>
<h2 id="hand-rankings">Hand rankings</h2>
<p>This list is the exact order the app uses, strongest first. A hand lower on the list beats every hand below it.</p>
<ol>
<li><strong>Five of a kind</strong> — needs a joker, or five identical physical cards from a multi-deck shoe.</li>
<li><strong>Five of a kind</strong> — needs a joker (wild).</li>
<li><strong>Royal flush</strong> — A-K-Q-J-10 of one suit.</li>
<li><strong>Straight flush</strong> — five in a row of one suit.</li>
<li><strong>Four of a kind</strong>.</li>
Expand All @@ -281,7 +281,7 @@ <h2 id="hand-rankings">Hand rankings</h2>
<li><strong>One pair</strong>.</li>
<li><strong>High card</strong>.</li>
</ol>
<p>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.</p>
<p>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.</p>
<h2 id="scoring-references">Scoring references</h2>
<p>These sources describe the rules the app follows. They corroborate the hand rankings and the shuffle.</p>
<ul>
Expand All @@ -297,7 +297,7 @@ <h2 id="evaluator-self-test">Evaluator self-test</h2>
<ol>
<li><strong>7,462 distinct ranks</strong> — the packaged rank table sorts to exactly the 7,462 natural 5-card ranks, with no gap and no repeat.</li>
<li><strong>Joker vector table (28)</strong> — the 28 hand-authored wild-card vectors each still evaluate to their expected hand class and kickers.</li>
<li><strong>Five-of-a-kind ordering</strong> — five of a kind outranks a royal flush, and a natural five of a kind outranks a wild one.</li>
<li><strong>Five-of-a-kind ordering</strong> — five of a kind outranks a royal flush.</li>
<li><strong>Whole-field 180×12 timing</strong> — a seeded 180-entry field of 12-card hands scores inside its budget.</li>
<li><strong>compare() total order</strong> — hand comparison is still a strict total order, so the standings sort by the hands, not by crossing order.</li>
<li><strong>best_hand() joker bound</strong> — a best hand never plays more than five jokers, even when the pool holds six or seven.</li>
Expand Down
6 changes: 3 additions & 3 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ DNF entries are excluded entirely: not placed, not listed, not exported. On a mi

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.
1. **Five of a kind** — needs a joker (wild).
2. **Royal flush** — A-K-Q-J-10 of one suit.
3. **Straight flush** — five in a row of one suit.
4. **Four of a kind**.
Expand All @@ -304,7 +304,7 @@ This list is the exact order the app uses, strongest first. A hand lower on the
10. **One pair**.
11. **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 {: #scoring-references }

Expand All @@ -324,7 +324,7 @@ The six checks, in order:

1. **7,462 distinct ranks** — the packaged rank table sorts to exactly the 7,462 natural 5-card ranks, with no gap and no repeat.
2. **Joker vector table (28)** — the 28 hand-authored wild-card vectors each still evaluate to their expected hand class and kickers.
3. **Five-of-a-kind ordering** — five of a kind outranks a royal flush, and a natural five of a kind outranks a wild one.
3. **Five-of-a-kind ordering** — five of a kind outranks a royal flush.
4. **Whole-field 180×12 timing** — a seeded 180-entry field of 12-card hands scores inside its budget.
5. **compare() total order** — hand comparison is still a strict total order, so the standings sort by the hands, not by crossing order.
6. **best_hand() joker bound** — a best hand never plays more than five jokers, even when the pool holds six or seven.
Expand Down
Loading
Loading