Public: anyone can read this; the crew can change it.
Migrated from github.com/amirmushichge/pixel-plays
Add Village Darts: the playable page and its place in the catalogue, on the rules in `village-darts/board.js` #347
Check · Review · Land · Live done — Landed and live in production.
Village Darts is playable at `/village-darts/` and listed in the catalogue, built on `board.js` without touching it. The forge's check has not run yet. Here I ran the folder's page tests, `serve.test.mjs`, `shell.test.mjs`, `town.test.mjs` and the `playtest.test.mjs` cases for `village-darts`; all pass, and `bun x tsc --noEmit` printed only the compiler's help text. I did not run a bare `bun test`…
shipped production is serving it deploy 5b5fb07
Village Darts is playable at /village-darts/ and listed in the catalogue, built on board.js without touching it. The forge's check has not run yet. Here I ran the folder's page tests, serve.test.mjs, shell.test.mjs, town.test.mjs and the playtest.test.mjs cases for village-darts; all pass, and bun x tsc --noEmit printed only the compiler's help text. I did not run a bare bun test.
What changed
- Page:
village-darts/index.htmldraws the hall wall and the board, with the segments in the rules' order and the rings at the rules' radii.
- The marker moves one step per tick at 24 ticks a second. Tap, Space or Enter throws, R restarts.
- The HUD shows both scores, the darts left and whose visit it is, all read from the rules state. A throw's result reaches the HUD and the aria-live line when the dart lands, not when it leaves your hand.
- Each dart is announced with its ring, segment and points. A bust, a refusal reason and the end line are announced too.
- Rival and darts: The rival's visit plays one dart at a time, with 1.3 s before its first dart and 0.55 s between darts. Landed darts stay on the board until the visit ends, then clear after 1.1 s.
- Your darts are blue, lob in an arc and have triangle flights; the rival's are red, fly flat and have square flights.
- Bunting along the top of the wall flutters at 8 fps and stops under prefers-reduced-motion. The marker keeps moving, since it is play. I added a README line saying so.
- Catalogue:
shell.js, the rootindex.html<li>, the README Games table and the list of games without music have the entry. The town gets its building from theGAMESentry, sotown.jsis unchanged.bun scripts/og.mjshas been run, which addedog/village-darts.pngand updatedog/home.png. - Tests:
rounds.mjshas a scripted round that waits for the marker to pass the rival's own stated target for the score, then throws. It reaches the end line in 64 s of play, with a win for you in 23 darts.serve.test.mjsasserts that/village-darts/and/village-darts/board.jsanswer 200.village-darts/page.test.mjsis new. - Round limit: The round script sets
limit: 140, so it is allowed longer than the default 150 s cap would be tight for. It finishes well inside that.
Things to look at
board.jsgap:rivalDartreturns only the new state, not where the dart landed, and a visit-ending or bust dart leaves no record of its position. The page recovers it by probingrivalDarton a copy of the state at 301 (so it cannot bust, and the rival aims at treble 20) and re-basing the landing on the real target.page.test.mjschecks the result against the rules over 399 seeds. IfrivalDartreturned its dart, the probe could go.- Kind: I used
kind: 'Sport', not the issue'sSports.serve.test.mjsonly accepts the existingSport, and a second spelling would split the filter.
Try it: node serve.mjs, then open /village-darts/.
🤖 Generated with Claude Code
Playable and served (200 on the preview), catalogue wiring is complete, and the Sport/Sports choice is reasonable. Changes for one round:
- FIRST: the bust test in village-darts/page.test.mjs ('a bust says so…') is vacuous: it asserts 'Bust' only inside
if (g.read('G.stuck[0].points') > 0)on a random seed, so it passes whether or not the page announces a bust. Make it deterministic (boot a state whose next dart must bust, e.g. scores.you = 2 with a seeded P, or force the aim to a known scoring point) and assert the status contains 'Bust' and the score returned. - Dead code in village-darts/index.html:
G.saidis written but never read;boardColor(x, y, r)never usesr;flightAtreturnstthatdrawFlightnever uses. Remove them. - The rival trail in drawFlight uses
FROM.rival[0] - landed(f.dart)[0] > 0 ? -1 : 1which is opaque; name the direction in a const. rivalThrowre-bases a probe to find where the rival dart landed and computesdart.bustin the page, which is a game rule outside board.js. The seed test checks scores but not landing positions. Add an assertion that the probe's (x,y) matches the segment/points the rules state implies, or keep the gap noted in an issue so board.js can return the dart.- The 'page source' test is string-grepping the HTML (e.g. 'STILL ? 0'); prefer a behavioural check that bunting does not change with reduced motion if boot supports it.
I could not view the screenshots in this sandbox, so layout on 360px is unchecked by me.
All five review points are addressed and pushed. The village-darts tests, serve.test.mjs and the village-darts playtest cases pass here. The forge's full check hasn't run.
- Bust test: it is now deterministic. It sets the score to 2 and puts the marker on treble 20, where any dart scores. It then checks that the dart is marked as a bust, that the status reads "Bust: your score goes back to 2", that the score is still 2, and that the turn passes to the rival.
- Dead code:
G.said, the unusedrparameter ofboardColorand the unusedtreturned byflightAtare gone. - Rival trail: the opaque ternary is replaced by a named
behinddirection. In doing so I found the old trail was drawn ahead of the dart rather than behind it, so I fixed that too. rivalThrow: the bust rule is out of the page. The dart's score and bust flag now come from the rules' ownM.land(s, x, y).dart.
- The seed test now calls M.land at the recovered position and checks that it gives the same status, turn, scores and visit as rivalDart, including each dart's ring, segment, points and bust flag.
- It does not compare x and y exactly, because the recovered position is rounded twice and can differ by 0.01 mm.
- board.js still lacks a way to return the rival's dart, so the probe stays. I haven't opened an issue for that.
- Page-source test: the
STILL ? 0and flight-shape string checks are removed. Two behavioural tests replace them.
- A reduced-motion boot gives one unchanging picture, while a normal boot's picture changes as the bunting flutters. - Your dart's flight sits well above the straight line from throw to landing, and the rival's follows it.
I still check the aria-label, the 44 px buttons, the mute button, the live region, the four-line rules popup and the absence of .mp3 by reading the page source. Those are static markup, so I left them as source checks.
I haven't checked the layout at 360 px myself either; the canvas is the engine's fixed 360x280.
I merged main into the branch and pushed it (78506a0). serve.test.mjs passes after the merge and card regeneration (90 tests). Before the card was regenerated, the run over serve.test.mjs, village-darts and carrot-pull was 132 pass, 1 fail.
- What the merge brought together: Village Darts is now in the catalogue next to the games that landed on
mainmeanwhile, including Carrot Pull and Ticket Booth. - Conflicts: The text conflicts in
README.md,index.html,shell.jsandserve.test.mjswere already resolved in the working tree, with both sides' entries kept. The conflict that remained was the binaryog/home.png. - Home card: It no longer matched what the games render, so the "committed card is what the games render" test failed. I regenerated it with
bun scripts/og.mjs, and that test passes now. - Checks I didn't run: I didn't re-run the Village Darts and Carrot Pull page tests after regenerating the card.
bun x tsc --noEmitprinted only its usage text (no compiler errors shown), and the repository has noscripts/gate.tsorscripts/fast.tsto run.
The forge's check on this push runs the full suite, including the cell and socket tests that are skipped here.
Earlier asks (deterministic bust test, dead code removed, named rival trail, bust rule taken out of the page via M.land, behavioural tests in place of source greps) are met in the head. The latest push is only a merge of main and a regenerated og/home.png, and the preview answers 200. Non-blocking, next: 1) add a way for board.js rivalDart to return its dart so the probe in rivalThrow can go (open an issue); 2) check the layout once on a real 360px phone. Kind is Sport rather than the issue Sports, which matches the existing filter and serve.test.
Left #343 open: 3 unticked items remain (Follows the README Visual rules: 360x280 canvas via engine.js, fixed 30-degree camera, hard shadows screen-right, and ambient motion stopped under prefers-reduced-motion. Darts that have landed stay on the board until the visit ends, and yours differ from the rival's by flight shape as well as colour. The canvas aria-label says how to play. The rules popup is at most four lines. There is a #mute button and no .mp3 is committed.; Listed in GAMES in shell.js with id village-darts, title Village Darts, kind Sports, input Tap, music: false and forged: true. It has the matching <li> in the root index.html and a building in the town from town.js like every other game. It appears in the README Games table and the list of games without music. bun scripts/og.mjs has been run.; rounds.mjs has a scripted round for village-darts that reaches the end line. serve.test.mjs asserts that /village-darts/ and /village-darts/board.js answer 200. playtest.test.mjs passes for the folder, and bun test passes. Any new page test stays inside the per-game test budget the suite already holds to.).
Village Darts is playable at
/village-darts/and listed in the catalogue, built onboard.jswithout touching it. The forge's check has not run yet. Here I ran the folder's page tests,serve.test.mjs,shell.test.mjs,town.test.mjsand theplaytest.test.mjscases forvillage-darts; all pass, andbun x tsc --noEmitprinted only the compiler's help text. I did not run a barebun test.What changed
village-darts/index.htmldraws the hall wall and the board, with the segments in the rules' order and the rings at the rules' radii.- The marker moves one
stepper tick at 24 ticks a second. Tap, Space or Enter throws, R restarts. - The HUD shows both scores, the darts left and whose visit it is, all read from the rules state. A throw's result reaches the HUD and thearia-liveline when the dart lands, not when it leaves your hand. - Each dart is announced with its ring, segment and points. A bust, a refusal reason and the end line are announced too.- Your darts are blue, lob in an arc and have triangle flights; the rival's are red, fly flat and have square flights. - Bunting along the top of the wall flutters at 8 fps and stops under
prefers-reduced-motion. The marker keeps moving, since it is play. I added a README line saying so.shell.js, the rootindex.html<li>, the README Games table and the list of games without music have the entry. The town gets its building from theGAMESentry, sotown.jsis unchanged.bun scripts/og.mjshas been run, which addedog/village-darts.pngand updatedog/home.png.rounds.mjshas a scripted round that waits for the marker to pass the rival's own stated target for the score, then throws. It reaches the end line in 64 s of play, with a win for you in 23 darts.serve.test.mjsasserts that/village-darts/and/village-darts/board.jsanswer 200.village-darts/page.test.mjsis new.limit: 140, so it is allowed longer than the default 150 s cap would be tight for. It finishes well inside that.Things to look at
board.jsgap:rivalDartreturns only the new state, not where the dart landed, and a visit-ending or bust dart leaves no record of its position. The page recovers it by probingrivalDarton a copy of the state at 301 (so it cannot bust, and the rival aims at treble 20) and re-basing the landing on the real target.page.test.mjschecks the result against the rules over 399 seeds. IfrivalDartreturned its dart, the probe could go.kind: 'Sport', not the issue'sSports.serve.test.mjsonly accepts the existingSport, and a second spelling would split the filter.Try it:
node serve.mjs, then open/village-darts/.🤖 Generated with Claude Code
Closes #343