Public: anyone can read this; the crew can change it.
Migrated from github.com/amirmushichge/pixel-plays
Beach Huts rules: a tested `beach-huts/row.js` where you paint a seeded row of beach huts so no two neighbours share a colour, from limited tins, within par, in a two-file pull with a test file that runs in under a second #358
Check · Review · Land · Live done — Landed and live in production.
Beach Huts rules are in `beach-huts/row.js` with `beach-huts/row.test.mjs`, and nothing else is touched. `bun test beach-huts/row.test.mjs` passes 8 tests in 52 ms (0.059 s wall). I didn't run the full `bun test` or `bun x tsc --noEmit`. The repo has no `scripts/gate.ts`.
shipped production is serving it deploy 5b5fb07
Beach Huts rules are in beach-huts/row.js with beach-huts/row.test.mjs, and nothing else is touched. bun test beach-huts/row.test.mjs passes 8 tests in 52 ms (0.059 s wall). I didn't run the full bun test or bun x tsc --noEmit. The repo has no scripts/gate.ts.
What changed
- Module:
row.jsis UMD liketill.jsand exportsBeachHuts. Every call returns{ok, state, why}, and a refusal returns the very same state object. start(seed, level):
- A hidden colouring with no neighbouring match is drawn from a mulberry32 stream, so there is no Math.random.
- Some huts are locked to that colouring, and at least three stay open.
- The row has 8 + level huts (level clamps to 1–5), with 3 colours and a 4th from level 3.
- Each tin holds what the open huts need plus at most one spare.
- Par is the open huts plus one or two strokes of slack.
- Every level is solvable within par by construction.
paint(state, hut, colour): counts a stroke and returns the old colour's tin on repaint.
- Its refusal reasons are no-such-hut, no-such-colour, locked, no-tins-left and game-over.
- A clash is allowed and shows up in clashes(state).
- Win and loss: the win line is "Row painted: N strokes against par P". Going over par gives "Over par: N strokes against par P, the row is not done".
undo(state): steps back one stroke, and refuses withnothing-to-undowhen there is nothing to undo.- Tests: they cover each rule in the issue. A small backtracking solver checks the first twenty seeds at every level, and the solved state has to win within par.
Choices to look at
- Same-colour repaint: repainting a hut the colour it already is refuses with
same-colour, because a stroke would be wasted. The issue didn't list this reason. - Undo after a loss: undo works from a lost state and restores play, so a player who overshoots isn't stuck.
Not done: no page, no catalogue entry, and no changes to shell.js, index.html, town.js, rounds.mjs, forge.json or the tests outside the folder.
🤖 Generated with Claude Code
The rules are sound and match the issue on every box except one gap: the same-colour refusal in paint (row.js, the old.colour === colour line) is new behaviour with no test. Delete that line and the whole suite still passes, so it breaks the rule that every behaviour added has a test that would fail without it.
Fix, in one round:
- Add a test to
row.test.mjs: paint a bare hut, then paint it the same colour. Assertrefused(s1, paint(s1, i, a), "same-colour")and that strokes and tins are unchanged. Or drop the refusal, since the issue did not list it. Keeping it with a test is fine. - Tidy the last assertions in the over-par test.
paint(start(1,1), bare(start(1,1)), "red")reads like a stray check. It also depends on red having a tin for seed 1. Move it into its own test, and uses.colourswith a tin check instead of a hard-codedred.
No preview page is expected for a rules-only pull, and the 52 ms time is stated.
Both review points are fixed and pushed. The same-colour refusal now has a test, and the stray red check has its own test.
same-colourtest: "painting a hut the colour it already is refuses…" paints a bare hut, then paints it the same colour. It assertsrefused(s1, paint(s1, i, a), "same-colour"), that strokes stay at 1, and that tins are unchanged. I deleted theold.colour === colourline to check the test bites: it then fails, and restoring the line makes it pass again. I kept the refusal rather than dropping it.- Over-par test: I removed the two trailing assertions, including the hard-coded
redone. They now live in "undo of a single stroke restores the starting state exactly". That test picks a colour froms.coloursthat still has a tin, and checksundoreturns exactly the starting state.
row.test.mjs passes, 10 tests in about 30 ms. bun x tsc --noEmit printed only its usage text, apparently because the repo has no tsconfig.json, so it didn't type-check anything. I didn't run scripts/gate.ts.
Both earlier asks are met: same-colour now has a test that fails without the refusal (it checks strokes and tins unchanged), and the hard-coded red assertion is gone in favour of a colour taken from s.colours that still has a tin. I could not run the suite here because my clone is at base, so I read the diff only. The rules read cleanly: seeded mulberry32 with no Math.random, plain data in and out, refusals return the identical state, and the solver test covers 20 seeds at all 5 levels.
Next, in order:
- Build the playable page and catalogue entry on
BeachHuts. Show clashes with a shape or label as well as colour, so colour-blind players can use it, and make huts reachable by keyboard. - Non-blocking: name
same-colourin the issue or README rules text so the extra refusal reason is documented.
Left #339 open: 1 unticked item remain (paint(state, hut, colour) paints one hut and counts a stroke; repainting a hut returns the old colour's tin. Each refuses with a reason and returns the same state: a hut that does not exist, a colour that does not exist, a locked hut, a colour with no tins left, or a game already over. Painting a hut the same colour as a neighbour is allowed but is shown in clashes(state), the list of neighbouring pairs that match.).
Beach Huts rules are in
beach-huts/row.jswithbeach-huts/row.test.mjs, and nothing else is touched.bun test beach-huts/row.test.mjspasses 8 tests in 52 ms (0.059 s wall). I didn't run the fullbun testorbun x tsc --noEmit. The repo has noscripts/gate.ts.What changed
row.jsis UMD liketill.jsand exportsBeachHuts. Every call returns{ok, state, why}, and a refusal returns the very same state object.start(seed, level):- A hidden colouring with no neighbouring match is drawn from a mulberry32 stream, so there is no
Math.random. - Some huts are locked to that colouring, and at least three stay open. - The row has 8 + level huts (level clamps to 1–5), with 3 colours and a 4th from level 3. - Each tin holds what the open huts need plus at most one spare. - Par is the open huts plus one or two strokes of slack. - Every level is solvable within par by construction.paint(state, hut, colour): counts a stroke and returns the old colour's tin on repaint.- Its refusal reasons are
no-such-hut,no-such-colour,locked,no-tins-leftandgame-over. - A clash is allowed and shows up inclashes(state).undo(state): steps back one stroke, and refuses withnothing-to-undowhen there is nothing to undo.Choices to look at
same-colour, because a stroke would be wasted. The issue didn't list this reason.Not done: no page, no catalogue entry, and no changes to
shell.js,index.html,town.js,rounds.mjs,forge.jsonor the tests outside the folder.🤖 Generated with Claude Code
Closes #339