games: the ladder gets played, and the rack learns where to stand
Trivia had every Go test passing and had never been in a browser, which this plan's own rule says means nothing. So: play it. The game itself holds up. The clock drains honestly and does not restart on a reload, the multiple compounds, walking pays exactly what the felt quoted, the reveal marks the right answer, and the auto-submit at zero lands as a timeout rather than an illegal move. The next question's answer never crosses the wire. Two bugs only the browser could show: - The spot printed double the stake after every settled game. standing() set spot.amount and *then* poured the chips on, and pour grows the pile from what it is told is already there. The money was always right; the number under the chips was not, which is the one rule the felt is built on. - The house rack sat on top of the multiplier at 390px. Its 5.75rem inset is not a margin, it is the width of blackjack's shoe — so on a phone the rack sits in the middle of the felt. It now shrinks on small screens and pulls into the corner where the corner is empty; data-at says which rack is which, because pulling blackjack's to the edge slides it under the deck. The dev rig seeds its own question bank now (one real OpenTDB batch per difficulty), because a fresh dev database 503s every start otherwise.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -9,6 +10,10 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"pete/internal/games/trivia"
|
||||
"pete/internal/opentdb"
|
||||
"pete/internal/storage"
|
||||
)
|
||||
|
||||
// TestDevCasino is not a test. It is the casino, running, on a port, with one
|
||||
@@ -30,6 +35,7 @@ func TestDevCasino(t *testing.T) {
|
||||
|
||||
s := newCasino(t)
|
||||
fund(t, 5000)
|
||||
seedTriviaBank(t)
|
||||
|
||||
payload, _ := json.Marshal(SessionUser{
|
||||
Sub: "sub-1", Username: "reala", Name: "Reala",
|
||||
@@ -63,3 +69,43 @@ func TestDevCasino(t *testing.T) {
|
||||
t.Cleanup(func() { _ = srv.Close() })
|
||||
_ = srv.Serve(ln)
|
||||
}
|
||||
|
||||
// seedTriviaBank puts enough questions in the bank to deal a ladder of each
|
||||
// difficulty.
|
||||
//
|
||||
// The rig does not run StartTriviaBank — a dev casino that spends its first two
|
||||
// minutes dripping four hundred questions per difficulty out of a free API is a
|
||||
// dev casino you cannot use. But a fresh database has an empty bank, and an
|
||||
// empty bank means every start 503s, so the rig would be unable to show you the
|
||||
// one game it exists to show you.
|
||||
//
|
||||
// One real batch per difficulty, through the real client: fifty questions is
|
||||
// four ladders' worth, and it means what the browser renders came out of OpenTDB
|
||||
// and through the same decode-and-store path production uses, entities and all.
|
||||
func seedTriviaBank(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
client := opentdb.New()
|
||||
|
||||
for i, tier := range trivia.Tiers {
|
||||
have, err := storage.CountTrivia(tier.Difficulty)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if have >= trivia.Rungs {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
time.Sleep(opentdb.Politeness) // the API asks; asking faster earns nothing
|
||||
}
|
||||
qs, err := client.Fetch(ctx, tier.Difficulty, opentdb.Batch)
|
||||
if err != nil {
|
||||
t.Fatalf("seeding the trivia bank (%s): %v", tier.Difficulty, err)
|
||||
}
|
||||
added, err := storage.AddTriviaQuestions(tier.Difficulty, qs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Printf("BANK %-6s %d questions\n", tier.Difficulty, added)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,6 +860,26 @@ html[data-phase="night"] {
|
||||
.pete-rack span[data-chip="100"] { --chip: #2b2118; }
|
||||
.pete-rack span[data-chip="500"] { --chip: #b079d6; }
|
||||
|
||||
/* On a phone the rack has to get out of the way. At its desk size it is a
|
||||
147px block sitting 92px in from the edge — a corner on a wide felt, but the
|
||||
middle of the table on a 390px one, where it sat on top of trivia's
|
||||
multiplier. So on a small screen it gets smaller, and where there's nothing
|
||||
in the corner it pulls into the corner.
|
||||
|
||||
Where the rack sits is the one thing that differs per table, which is what
|
||||
data-at says. Unmarked, it's alone in the corner. "shoe" means blackjack,
|
||||
where the 5.75rem inset is not a margin but the width of the shoe it sits
|
||||
beside — pull that one to the edge and it slides under the deck. "rail" means
|
||||
solitaire, where the rack isn't on the felt at all (position: static). */
|
||||
@media (max-width: 639px) {
|
||||
.pete-rack:not([data-at="rail"]) {
|
||||
gap: 0.25rem;
|
||||
padding: 0.35rem 0.4rem 0.3rem;
|
||||
}
|
||||
.pete-rack:not([data-at="rail"]) span { width: 1.15rem; }
|
||||
.pete-rack:not([data-at]) { right: 1rem; }
|
||||
}
|
||||
|
||||
/* The burst. A natural gets confetti; nothing else in the room does, which is
|
||||
what keeps it worth something. */
|
||||
.pete-spark {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -278,7 +278,10 @@
|
||||
}
|
||||
bet = amount;
|
||||
showBet();
|
||||
spot.amount = amount;
|
||||
// pour grows the pile from whatever is on the spot, and settle has just swept
|
||||
// it clean — so it must not be told the chips are already there. Setting the
|
||||
// amount first counted the standing bet twice, and the spot printed 400 under
|
||||
// a 200 stake.
|
||||
return spot.pour(purseEl, amount);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
always travelling between one of those and the spot in front of you. -->
|
||||
<section class="pete-felt relative overflow-hidden rounded-3xl p-6 sm:p-10 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
||||
|
||||
<div class="pete-rack" data-house aria-hidden="true">
|
||||
<div class="pete-rack" data-at="shoe" data-house aria-hidden="true">
|
||||
<span data-chip="500" style="--stack: 5"></span>
|
||||
<span data-chip="100" style="--stack: 7"></span>
|
||||
<span data-chip="25" style="--stack: 4"></span>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<!-- The meter and the ladder. This row is the only one level with the house
|
||||
rack in the corner, so it is the only one that has to keep clear of it. -->
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-3 pr-24 sm:pr-28">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-3 pr-32 sm:pr-28">
|
||||
<div class="pete-meter" data-meter>
|
||||
<span class="pete-meter-label">Worth</span>
|
||||
<span data-multiple class="pete-meter-value">—</span>
|
||||
|
||||
@@ -244,10 +244,9 @@ A multi-session build. This section is the handover; read it before anything els
|
||||
hits the rack, nothing happens, and it looks like the bet is broken. Blackjack's
|
||||
action buttons are also `[data-move="stand"]`, not `[data-stand]`.
|
||||
|
||||
- **Trivia, and it plays for chips.** *(2026-07-14. Built, tested, and — say it
|
||||
plainly — **NOT YET DRIVEN IN A BROWSER**. Every Go test passes and `go vet` is
|
||||
clean, which per this plan's own hard-won rule means nothing at all about the
|
||||
table. Do that first next session; see the bottom of this entry.)*
|
||||
- **Trivia, and it plays for chips.** *(2026-07-14. Built, and now **played** — see
|
||||
"Driven in a browser" at the bottom of this entry, which is where the two bugs
|
||||
were.)*
|
||||
- **A ladder.** Stake once, then answer a run of twelve. Every right answer
|
||||
multiplies what you're holding, a wrong one loses the lot, and you may walk
|
||||
with what you've built. Clearing all twelve ends the run and banks it — a
|
||||
@@ -284,30 +283,52 @@ A multi-session build. This section is the handover; read it before anything els
|
||||
seconds, stops early when a batch adds nothing new), and a round never waits on
|
||||
it. Answers are shuffled per-game against the game's own seed, so where the right
|
||||
answer sits in the table tells a player nothing.
|
||||
- **Left to do, and it is the whole of the risk:** `PETE_DEV_CASINO=:7788 go test
|
||||
./internal/web -run TestDevCasino -timeout 0` and *play it*. Watch especially:
|
||||
the bank is empty on a fresh dev database, so the first start will 503 with "the
|
||||
question bank is still filling up" until the refill loop has run — the dev rig
|
||||
does not start that loop, so it likely needs seeding by hand or a call to
|
||||
`refillTriviaBank`. Then: does the clock bar drain honestly, does the auto-submit
|
||||
at zero land as a timeout rather than a "that move isn't legal" (the GRACE window
|
||||
in trivia.js is what defends this), does the reveal mark the right answer, and
|
||||
does the money settle onto the shared spot the way the other three tables do.
|
||||
- **The dev rig seeds its own bank.** A fresh dev database has an empty bank and
|
||||
every start 503s, so `TestDevCasino` now takes one real batch per difficulty
|
||||
from OpenTDB (`seedTriviaBank`) — fifty questions each, four ladders' worth,
|
||||
through the same fetch-decode-store path production uses. It does *not* run
|
||||
`StartTriviaBank`: a rig that spends its first two minutes dripping four hundred
|
||||
questions per difficulty out of a free API is a rig you cannot use.
|
||||
- **Driven in a browser, 2026-07-14, and the clock and the money hold up.** The
|
||||
ladder plays: a 200 stake on Easy dealt a real OpenTDB question with its
|
||||
entities decoded, the clock bar drained honestly (847px → 711px over three
|
||||
seconds, countdown 18.7s → 15.7s), two right answers compounded 1.00× → 1.26×
|
||||
→ 1.58×, and walking paid exactly the 311 the felt had been quoting. The
|
||||
reveal marks the wrong pick red and the right answer green. A reload mid-rung
|
||||
brought the board back and — the thing that matters — the server's clock kept
|
||||
running through it (17.5s left before, 16.2s after; it does not restart).
|
||||
**The timeout lands as a timeout**, which was the loudest worry: going quiet
|
||||
through a 20s question fired the auto-submit at zero, came back 200 with a
|
||||
`timeout` event and "Out of time.", not a "that move isn't legal". The next
|
||||
question's answer is never sent (`correct: -1` in the ask event); only the
|
||||
decided one reveals.
|
||||
- **Two bugs, and only a browser could have found either.**
|
||||
1. **The spot printed double the stake after every settled game.** `standing()`
|
||||
set `spot.amount` and *then* poured the chips on, and `pour` grows the pile
|
||||
from whatever it is told is already there — so a 200 stake settled to a spot
|
||||
reading 400, and a 400 one to 800. This is exactly the rule the felt is built
|
||||
on ("the number under the pile is a readout of the pile") failing quietly:
|
||||
the money was always right, the *number under the chips* was not. Blackjack
|
||||
and hangman pour without pre-setting; trivia now does too.
|
||||
2. **The house rack sat on top of the multiplier at 390px.** The rack is a 147px
|
||||
block inset 5.75rem from the edge, and that inset is not a margin — it is the
|
||||
width of *blackjack's shoe*, which the rack sits beside. On a phone that puts
|
||||
it in the middle of the felt, on top of trivia's "1.53×". On small screens the
|
||||
rack now shrinks and, where there is nothing in the corner, pulls into the
|
||||
corner. Which rack is which is what `data-at` says: unmarked is alone in the
|
||||
corner, `shoe` is blackjack (pull that one to the edge and it slides under the
|
||||
deck — this was caught after doing exactly that), `rail` is solitaire, whose
|
||||
rack isn't on the felt at all. All four tables re-checked at 390px and 1280px,
|
||||
live games on the felt: no overlap with text, no overlap with the shoe, no
|
||||
horizontal overflow, desktop geometry unchanged.
|
||||
|
||||
### Next, in order
|
||||
|
||||
1. **Drive trivia in a browser** (see above) — it has never been played, and that
|
||||
means nothing about it is known.
|
||||
2. Phase 2's other half is now built but unproven: the question bank is
|
||||
**prefetched from OpenTDB into a local table** (a per-question fetch in a web
|
||||
game loop is a latency and rate-limit problem gogobee never had), through
|
||||
`internal/safehttp`. It stakes chips too. The shape that fits the room is a
|
||||
**ladder**: stake once, answer a run of questions, each right answer compounds
|
||||
the multiple and a wrong one loses the lot, with the option to walk. Note the
|
||||
real risk — trivia answers are googlable, so a tight per-question clock is the
|
||||
only thing making a slow-but-correct answer worth less than a fast one. Score
|
||||
the clock server-side; the browser's countdown is decoration.
|
||||
2. Phase 3 (UNO), Phase 4 (hold'em) as below.
|
||||
1. Phase 3 (UNO), Phase 4 (hold'em) as below.
|
||||
2. Trivia is played but not deployed. Hangman, solitaire and trivia are all still
|
||||
sitting on main un-deployed — the server runs `StartTriviaBank`, so its bank
|
||||
fills itself once the binary is out there, but the first player to try a ladder
|
||||
in the first minute after a deploy gets the 503.
|
||||
|
||||
Still open on the table itself, none of it blocking: **split** isn't implemented (the
|
||||
engine has no move for it), the felt is roomy at desktop widths with only one seat on
|
||||
|
||||
Reference in New Issue
Block a user