games: the uno table opens its doors
Driven in a browser for the first time, which is where three bugs were. Every visit to /games/uno was a 500: the page was never added to the list server.go parses into the games template set, so render() answered "unknown page". The casino tests all call their handlers directly and never go through render(), so nothing saw it. TestEveryCasinoPageRenders now walks the mux and asks for every page the casino routes to. The play script hid the first card that lit up rather than the one you clicked, so playing any other playable card made an innocent card vanish. And on a phone the discard sized its box but not its card, which takes its size from --uno-h, so a full-size card hung out of a small hole and covered the colour in play. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
50
internal/web/games_render_test.go
Normal file
50
internal/web/games_render_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Every page the casino routes to must be in the games template set. This is not
|
||||
// a fussy test: uno shipped with its handler wired, its engine tested and its
|
||||
// template written, and every visit to the table answered "unknown page" with a
|
||||
// 500 — because the page was never added to the list server.go parses. Nothing
|
||||
// else caught it, since the other tests call the handlers straight and never go
|
||||
// through render(). Add a game, add it here.
|
||||
func TestEveryCasinoPageRenders(t *testing.T) {
|
||||
s := newCasino(t)
|
||||
|
||||
pages := []string{
|
||||
"/games",
|
||||
"/games/blackjack",
|
||||
"/games/hangman",
|
||||
"/games/solitaire",
|
||||
"/games/trivia",
|
||||
"/games/uno",
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
s.casinoRoutes(mux)
|
||||
|
||||
for _, path := range pages {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, as(t, s, "reala", "GET", path, nil))
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("GET %s = %d, want 200 (body: %s)", path, w.Code, strings.TrimSpace(w.Body.String()))
|
||||
}
|
||||
// render() writes the 500 as a body and a page that fails halfway
|
||||
// through still comes back 200, so look at what was actually served.
|
||||
body := w.Body.String()
|
||||
if strings.Contains(body, "unknown page") {
|
||||
t.Fatalf("GET %s served the unknown-page error: the template is missing from the games set in server.go", path)
|
||||
}
|
||||
if !strings.Contains(body, "</html>") {
|
||||
t.Fatalf("GET %s did not render a whole page (%d bytes) — the template blew up mid-render", path, len(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled boo
|
||||
pages []string
|
||||
}{
|
||||
{"layout", []string{"_card"}, []string{"index", "channel", "weather", "bookmarks", "for-you", "status", "story"}},
|
||||
{"games_layout", []string{"_chipbar"}, []string{"games", "blackjack", "hangman", "solitaire", "trivia"}},
|
||||
{"games_layout", []string{"_chipbar"}, []string{"games", "blackjack", "hangman", "solitaire", "trivia", "uno"}},
|
||||
}
|
||||
tpls := make(map[string]*template.Template)
|
||||
for _, set := range sets {
|
||||
|
||||
@@ -1755,7 +1755,10 @@ html[data-phase="night"] {
|
||||
@media (max-width: 639px) {
|
||||
.pete-uno-hand { --uno-h: 5.2rem; --uno-w: 3.5rem; gap: 0.3rem; }
|
||||
.pete-uno-deck { --uno-h: 5.2rem; --uno-w: 3.5rem; }
|
||||
.pete-uno-discard { height: 5.2rem; width: 3.5rem; }
|
||||
/* The vars, not just the box: the card in the discard is a .pete-uno-card and
|
||||
takes its size from them, so sizing the box alone left a full-size card
|
||||
hanging out of a small hole, on top of the colour in play. */
|
||||
.pete-uno-discard { --uno-h: 5.2rem; --uno-w: 3.5rem; height: 5.2rem; width: 3.5rem; }
|
||||
.pete-uno-hand .pete-uno-card { padding: 0.22rem; }
|
||||
.pete-uno-oval { font-size: 1.15rem; }
|
||||
.pete-uno-seat { padding: 0.35rem 0.45rem; }
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -63,6 +63,8 @@
|
||||
var game = null; // the game as the server last described it
|
||||
var tier = "table";
|
||||
var pendingWild = -1; // the wild you clicked, waiting on a colour
|
||||
var played = -1; // the card you just played, so the script lifts that one out
|
||||
// of the hand and not merely the first one that lit up
|
||||
|
||||
var reduced = FX.reduced;
|
||||
function pace(ms) { return reduced ? 0 : ms; }
|
||||
@@ -399,9 +401,10 @@
|
||||
var from = seatAnchor(e.seat);
|
||||
// Your own card leaves the hand it was in, so the hand has to lose it
|
||||
// before the flight or the card is briefly in two places.
|
||||
if (e.seat === 0) {
|
||||
var live = handEl.querySelector('.pete-uno-card[data-on="1"][data-at]');
|
||||
if (e.seat === 0 && played >= 0) {
|
||||
var live = handEl.querySelector('.pete-uno-card[data-at="' + played + '"]');
|
||||
if (live) live.style.visibility = "hidden";
|
||||
played = -1;
|
||||
}
|
||||
bump(e.seat, e.left);
|
||||
return throwCard(node, from, discardEl, { index: n })
|
||||
@@ -555,6 +558,7 @@
|
||||
if (cancelWild) cancelWild.addEventListener("click", hideWild);
|
||||
|
||||
function move(body) {
|
||||
played = body.kind === "play" ? body.index : -1;
|
||||
send("/api/games/uno/move", body, gameMsgEl);
|
||||
}
|
||||
|
||||
|
||||
@@ -322,9 +322,9 @@ A multi-session build. This section is the handover; read it before anything els
|
||||
live games on the felt: no overlap with text, no overlap with the shoe, no
|
||||
horizontal overflow, desktop geometry unchanged.
|
||||
|
||||
- **UNO, and it plays for chips.** *(2026-07-14. Built and tested, **not yet driven
|
||||
in a browser** — see "Next" below, because in this room that means it is not
|
||||
finished.)*
|
||||
- **UNO, and it plays for chips.** *(2026-07-14. Built, tested, and now **played** —
|
||||
see "Driven in a browser" at the bottom of this entry, which is where the three
|
||||
bugs were.)*
|
||||
- **You beat the table, or you don't.** The user's call between three money
|
||||
models: stake once, go out first and take the tier's multiple; anybody else
|
||||
going out first takes the stake. **The table size is the tier**, which is the
|
||||
@@ -367,22 +367,48 @@ A multi-session build. This section is the handover; read it before anything els
|
||||
- The felt has no corner free for the house rack (bots along the top, piles in the
|
||||
middle, your hand at the bottom), so it takes solitaire's **rail** instead:
|
||||
`data-at="rail"`, off the felt, no collision to check for.
|
||||
- **Driven in a browser, 2026-07-14, and it plays.** A Full House game went the
|
||||
distance: the bots' turns come back as a readable script (a card flies from the
|
||||
seat that played it, SKIPPED and +2 land on somebody), the wild picker takes a
|
||||
colour and the felt changes to it, a reload mid-game brings back the hand, the
|
||||
counts, the colour in play and the stake, and the money is right — a Duel staked
|
||||
200 and won paid 428 back into a 4,600 stack (2.2× is 240 of winnings, less the
|
||||
5% rake, so +228 net), while a lost Full House took the stake and nothing else.
|
||||
A thirteen-card hand wraps to three rows at 390px with no sideways overflow and
|
||||
nothing colliding. Console silent.
|
||||
- **Three bugs, and the first one was the whole table.**
|
||||
1. **Every visit to `/games/uno` was a 500.** The handler was wired, the route
|
||||
was in `casinoRoutes()`, the template was written — and `uno` was never added
|
||||
to the list of pages `server.go` parses into the games template set, so
|
||||
`render()` answered "unknown page". No Go test saw it because the casino tests
|
||||
all call the handlers *directly* and never go through `render()`. There is now
|
||||
a test that does: `TestEveryCasinoPageRenders` walks the mux, asks for every
|
||||
page the casino routes to, and fails on a 500 or a half-rendered body. **Add a
|
||||
game, add it there.**
|
||||
2. **The wrong card left your hand.** The play script hid `.pete-uno-card[data-on="1"]`
|
||||
— the *first* card that lit up, not the one you clicked — so playing any other
|
||||
playable card made an innocent one vanish while the card you played sat there
|
||||
and a copy of it flew to the discard. It self-corrected on the re-render, which
|
||||
is why it read as a flicker rather than a bug. The index you played is now kept
|
||||
(`played`) and that card is the one lifted out.
|
||||
3. **On a phone the card in play sat on top of the colour in play.** The mobile
|
||||
query shrank `.pete-uno-discard`'s *box* with a raw height and width, but the
|
||||
card inside it is a `.pete-uno-card` and takes its size from `--uno-h`/`--uno-w`,
|
||||
which the discard never set — so a full-size card hung out of a small hole and
|
||||
covered the RED/BLUE pill under it. The vars go on the discard now. Worth
|
||||
remembering as a rule: **size a card by its vars, never by the box you put it
|
||||
in.**
|
||||
|
||||
### Next, in order
|
||||
|
||||
1. **Drive UNO in a browser.** It is built, the engine is tested and the handlers are
|
||||
tested, but nothing on this table has been *looked at* — and every game before it
|
||||
found bugs in that step that no Go test could see (a white-on-white pill, a rack on
|
||||
top of a multiplier, a spot printing double the stake). Specifically worth watching:
|
||||
the bots' turns play back as a readable script rather than a blur; the wild colour
|
||||
picker; a hand of ten cards at 390px with no sideways overflow; and a reload
|
||||
mid-game bringing the board back. `PETE_DEV_CASINO=:7788 go test ./internal/web
|
||||
-run TestDevCasino -timeout 0`, then Playwright.
|
||||
2. Phase 4 (hold'em) as below.
|
||||
3. Trivia is played but not deployed. Hangman, solitaire, trivia and now UNO 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.
|
||||
1. **Phase 4 — hold'em**, as below. It is the last game on the list, and the first
|
||||
one where a hand has other people in it: the spot is a singleton and the chip
|
||||
animations are tuned for one seat (see "still open" below).
|
||||
2. **Deploy.** Hangman, solitaire, trivia and UNO are all played, and all four are
|
||||
still sitting on main un-deployed — the live casino is blackjack and nothing else.
|
||||
The server runs `StartTriviaBank`, so trivia's 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