@homie-rocks/studio 0.48.4 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/app-starters/gathering/README.md +4 -0
  3. package/app-starters/gathering/app.json +162 -0
  4. package/app-starters/gathering/index.html +3 -0
  5. package/app-starters/gathering/map/main.json +1 -0
  6. package/app-starters/gathering/src/move.ts +3 -0
  7. package/app-starters/gathering/src/rules.ts +21 -0
  8. package/app-starters/gathering/src/view.ts +74 -0
  9. package/app-starters/giving/README.md +5 -0
  10. package/app-starters/giving/app.json +162 -0
  11. package/app-starters/giving/index.html +3 -0
  12. package/app-starters/giving/map/main.json +1 -0
  13. package/app-starters/giving/src/move.ts +3 -0
  14. package/app-starters/giving/src/rules.ts +21 -0
  15. package/app-starters/giving/src/view.ts +74 -0
  16. package/app-starters/jobs/README.md +4 -0
  17. package/app-starters/jobs/app.json +162 -0
  18. package/app-starters/jobs/index.html +3 -0
  19. package/app-starters/jobs/map/main.json +1 -0
  20. package/app-starters/jobs/src/move.ts +3 -0
  21. package/app-starters/jobs/src/rules.ts +21 -0
  22. package/app-starters/jobs/src/view.ts +74 -0
  23. package/app-starters/quiz/README.md +4 -0
  24. package/app-starters/quiz/app.json +162 -0
  25. package/app-starters/quiz/index.html +3 -0
  26. package/app-starters/quiz/map/main.json +1 -0
  27. package/app-starters/quiz/src/move.ts +3 -0
  28. package/app-starters/quiz/src/rules.ts +21 -0
  29. package/app-starters/quiz/src/view.ts +74 -0
  30. package/app-starters/welcome/app.json +25 -2
  31. package/app-starters/welcome/index.html +1 -1
  32. package/app-starters/welcome/src/view.ts +20 -8
  33. package/bin/homie-studio.mjs +20 -16
  34. package/functions/FUNCTIONS.md +1 -1
  35. package/guides/app/SKILL.md +178 -0
  36. package/guides/art/SKILL.md +154 -0
  37. package/guides/art/references/RIGHTS.md +20 -0
  38. package/guides/art/scripts/art.mjs +308 -0
  39. package/guides/models/SKILL.md +198 -0
  40. package/guides/models/references/RIGHTS.md +46 -0
  41. package/guides/models/references/models.json +175 -0
  42. package/guides/models/scripts/models.mjs +483 -0
  43. package/guides/music/SKILL.md +197 -0
  44. package/guides/music/references/COMPOSITION.md +59 -0
  45. package/guides/music/references/GAME-AUDIO.md +50 -0
  46. package/guides/music/references/RIGHTS.md +43 -0
  47. package/guides/music/scripts/lib/audio.mjs +258 -0
  48. package/guides/music/scripts/lib/eleven.mjs +260 -0
  49. package/guides/music/scripts/lib/studio.mjs +147 -0
  50. package/guides/music/scripts/music.mjs +644 -0
  51. package/guides/parts/SKILL.md +158 -0
  52. package/guides/plan/SKILL.md +126 -0
  53. package/guides/plan/references/CODEX.md +105 -0
  54. package/guides/plan/references/INTERVIEW.md +86 -0
  55. package/guides/playtest/SKILL.md +205 -0
  56. package/guides/playtest/references/METHOD.md +124 -0
  57. package/guides/playtest/references/REVIEWER.md +78 -0
  58. package/guides/playtest/scripts/lib/exit.mjs +31 -0
  59. package/guides/playtest/scripts/lib/judge.mjs +502 -0
  60. package/guides/playtest/scripts/lib/pixels.mjs +128 -0
  61. package/guides/playtest/scripts/lib/preflight.mjs +69 -0
  62. package/guides/playtest/scripts/playtest.mjs +870 -0
  63. package/guides/shop/SKILL.md +153 -0
  64. package/guides/sound/SKILL.md +156 -0
  65. package/guides/sound/references/GAME-SOUND.md +100 -0
  66. package/guides/sound/references/SCORE.md +131 -0
  67. package/guides/sound/references/TRAPS.md +93 -0
  68. package/guides/sound/scripts/lib/measure.mjs +252 -0
  69. package/guides/sound/scripts/lib/score.mjs +415 -0
  70. package/guides/sound/scripts/lib/sfx.mjs +222 -0
  71. package/guides/sound/scripts/lib/synth.mjs +509 -0
  72. package/guides/sound/scripts/sound.mjs +520 -0
  73. package/guides/studio-setup/SKILL.md +419 -0
  74. package/guides/style/SKILL.md +105 -0
  75. package/lib/app-needs.mjs +63 -0
  76. package/lib/authoring-guides.mjs +5 -4
  77. package/lib/build.mjs +5 -4
  78. package/lib/check-site.mjs +6 -6
  79. package/lib/check.mjs +18 -5
  80. package/lib/codex.mjs +95 -18
  81. package/lib/decisions.mjs +9 -7
  82. package/lib/dev.mjs +4 -2
  83. package/lib/doctor.mjs +1 -1
  84. package/lib/handoff.mjs +13 -8
  85. package/lib/mcp-tools.mjs +19 -17
  86. package/lib/mcp.mjs +1 -1
  87. package/lib/parts.mjs +6 -6
  88. package/lib/scaffold.mjs +45 -11
  89. package/lib/shop-links.mjs +2 -2
  90. package/lib/shop.mjs +5 -3
  91. package/lib/studio.mjs +7 -6
  92. package/lib/template-history.json +84 -0
  93. package/package.json +4 -4
  94. package/shop/SHOP.md +4 -0
  95. package/shop/shop.ts +4 -4
  96. package/worker/app-format.mjs +1 -1
  97. package/worker/build-id.mjs +1 -1
  98. package/worker/functions-schema.mjs +15 -0
  99. package/worker/index.mjs +2 -1
  100. package/worker/parts.mjs +4 -4
  101. package/worker/shop-page.mjs +22 -16
  102. package/worker/shop-rules.mjs +4 -3
  103. package/worker/shop.mjs +6 -2
  104. package/worker/site.mjs +4 -2
  105. package/worker/version.mjs +1 -1
@@ -0,0 +1,205 @@
1
+ ---
2
+ name: playtest
3
+ description: Playtest a studio's app against its owner's Codex needs, or its game the way Homie holds its own games to a bar — real browsers on a computer and a phone held both ways, the first ten seconds timed, the look measured (black or flat frames, contrast, detail), how much of the screen the UI covers, the game's real sound captured and measured, a round played with one person trying and one doing nothing, the owner control tests and two strangers finishing a round — then a blind review by a fresh reviewer that never saw the code, and a ranked list of what is weak. Use when someone asks to playtest, test, review, critique or judge a game, asks what is weak or what to fix next, before calling a game finished or publishing it, and after every change that should make it better.
4
+ ---
5
+
6
+ ## Apps: judge the place against its owner’s words
7
+
8
+ For an app, read `apps/<id>/CODEX.md` first. This is the app branch of the same playtest: no rounds, scores or invented entertainment. Run build and `check <id> --shots .checks` (including its Codex needs), then open the wall and phone in real Chrome. The game script below measures games; do not run a round instrument on an app.
9
+
10
+ Walk in as the owner. Read **Their words** aloud to yourself, then inspect the wall capture: is the pictured place there, do recognisable things move/fill/arrive/light up as requested, and can someone across the room understand the work? A generic board of boxes fails a living-scene request even if every label is correct. Check an empty and a busy state without passing invented records off as real customers. Inspect state before and after a real action, not only a pretty still.
11
+
12
+ Now arrive as a new customer on a phone: find the offer and correct price, book/give/log/sign up, understand success or failure, save the return link, reload and follow your own record. Use a second phone to check the link follows the same thing. As staff, sign in and perform the real work; as a public visitor, verify privileged writes are refused. Close all browsers and return: the work must remain. A simulated paid event is not a real charge; record payment connection gaps honestly.
13
+
14
+ For every need, write `met / partly / not`, the exact capture or flow that demonstrates it, and what remains. Check phone portrait/landscape, wall readability, kiosk reset if present, native controls, keyboard focus and reduced motion. Do not infer usability from a build or a shared counter. Inspect the saved captures yourself; automated selector assertions cannot judge the dreamed scene. Copy the verdict and evidence into the Codex and redraw its page. Keep unfinished needs visibly unfinished. No approval question is needed for local checks or your visual review.
15
+
16
+ A local review is labelled local, not independent. If a fresh reviewer was requested, give that reviewer the owner's original request, the Codex and real captures/flow addresses; ask them to judge as owner and customer, need by need. When comparing several businesses, they must read as different places with different work, not a common template recoloured. If one fails, identify what the builder read or skipped, repair that making path, and build/test again before saying done.
17
+
18
+ # Playtest a game
19
+
20
+ The question is never "does it run". It is: **would a stranger who pressed Play stay for a whole
21
+ round, and come back?** Instruments measure what can be measured; a fresh reviewer who never saw
22
+ the code answers the rest. A builder never grades its own work.
23
+
24
+ One script: `scripts/playtest.mjs` in this skill's folder (Claude Code:
25
+ `node "${CLAUDE_PLUGIN_ROOT}/skills/playtest/scripts/playtest.mjs" <command>`), run from inside the
26
+ studio. It needs Node 22, Chrome and the studio's `@homie-rocks/studio` (for puppeteer-core and its
27
+ checks). It opens at most two browsers at a time, all muted: nothing plays out loud.
28
+
29
+ For rules games, build first: the guard, strict types and deterministic play check must pass
30
+ before Chrome opens. Both online browsers are replicas of a server host; killing the first
31
+ browser tests continuity, not host election. Use the real predicted pose in the probe, never
32
+ fake movement or scores. Test local movement with 150 ms delay and also verify hits, scores,
33
+ turns and results agree after authority arrives. A turn game, quiz or live app needs its actual
34
+ buttons and workflow exercised: movement-only rows do not prove it works, and an app has no
35
+ required game round. Use the app check for shared actions and reconnect. Report inapplicable
36
+ rows honestly, and add a manual two-browser trial of the requested mechanic.
37
+
38
+ ## 1. Run the site and the instruments
39
+
40
+ Start the site as a background task that outlives the command (Claude Code: the Bash tool's
41
+ `run_in_background`), wait until the play page answers, then:
42
+
43
+ ```sh
44
+ npm run dev # background: http://127.0.0.1:8787 (another port: npx --no-install homie-studio dev --port <n>)
45
+ node <playtest.mjs> run <game id> --url http://127.0.0.1:8787
46
+ ```
47
+
48
+ It takes five to ten minutes; poll its output, never end your turn while it runs, and do not rebuild
49
+ while it runs (a rebuild under `npm run dev` can stop the dev server). `--only first,look,ui,sound`
50
+ runs some rows; `--seconds 30` plays longer. The live site works too (`--url` the studio's address).
51
+ Stop the site afterwards with `npx --no-install homie-studio dev --stop`.
52
+
53
+ | row | what it does | fails when |
54
+ | --- | --- | --- |
55
+ | `first <device>` | opens the play page like a stranger on a computer, a phone, a phone on its side, and times four separate things: a seat, the page's first paint (the loading card, not the game), the loading cover lifting, and the first picture of the game itself (cover gone, the game's own frames advancing); frames per second; the GPU | no seat, a cover that never lifts, or no picture of the game in 10 s |
56
+ | `move <device>` | waits for a live round and a body that is the player's, then presses right and times the body moving; when nothing moves it presses left too and reports both, with the position, round phase and time left at each press | the body answers after 1.5 s, or neither way moves it in a live round. WARN when only the opposite way moved (a wall on that side looks like this). BLOCKED when no press fit inside a live round with a free body |
57
+ | `look <device>` | plays like a person (holds a direction most of a second, sometimes the action, changes its mind) and shoots the screen: brightness, contrast, colour, visible detail, black or one-colour frames, holes where nothing drew, frames that did not change | a black or one-colour frame, pure-black holes; WARN for dark, flat, featureless or still frames |
58
+ | `ui <phone>` | waits for active play, holds the thumb on the stick, hides the world, paints the page black then white, and counts what stays: the share of the screen the DOM UI covers and what is opaque in the middle third. Both frames are saved with the game's state at each | over 12% covered, or anything opaque in the middle, during active play. N/A when the screen was the results card, a spectator view, the loading cover or a browser cut off from its room (measured, labelled, not judged by that bar). BLOCKED when the game changed state between the two frames. WARN when it would pass and a DOM HUD element sits under one of the play page's own controls (the room button, the server or chat pill, the banner): the row names the element and the control |
59
+ | `sound` | copies what the game sends to its speaker while it is played (no autoplay flag: the real first-touch rule); loudness overall and through a phone speaker, true peak, clipping, gaps, whether each action press answers with a sound, whether music started | silence, clipping; WARN for quiet, gaps, actions without sound, what a phone loses |
60
+ | `play` | a computer plays (direction holds and the game's primary action) and a phone does nothing, in the same public room, for a round that starts after both are in: places, scores, round length against `game.json`, lead changes, and which inputs the script really pressed | no round finishes; WARN when doing nothing scores as well as playing, everyone ties, or a bot is far ahead (a measured gap in one scripted round, never "people cannot win") |
61
+ | `controls` | the owner tests from `homie-studio port check`: hold a direction 5 s (one straight line, the camera's yaw moves under 10°), alternate directions 10 s (every press goes the pressed way), real touch on Android Chrome (and iPhone WebKit when Playwright's WebKit is installed; otherwise that row says skip), the first browser closing (server continuity; host election only for older games), a late joiner, the big screen, audio unlock, errors | any of them (`port` skill: `references/CHECKS.md` says what each failure usually means) |
62
+ | `round` | `homie-studio check`: two fresh browsers press Play, meet in one room and both see a round finish with both of them in the results; reconnects are reported beside completion | they do not. WARN when the round finished and a browser reconnected on the way |
63
+ | `errors` | uncaught errors and failed requests seen along the way | any uncaught error |
64
+
65
+ **BLOCKED is never PASS.** A browser rendering at a few frames a second (a software renderer, a
66
+ machine under heavy load) makes any game look stuck: the rows say BLOCKED and judge nothing. Run
67
+ again on a quieter machine. "Could not test" and "tested and fine" must never sound the same.
68
+ The same goes for the instrument's own faults: a screenshot decoder that does not finish in 20 s
69
+ is stopped, that device's rows say BLOCKED, the browser is closed and the run exits non-zero; a
70
+ site address this computer's Node.js cannot look up (a browser may still open it) stops the run as
71
+ BLOCKED, "network preflight failed, before any page or game was opened" (the words `check`, `perf`
72
+ and `port check` use for it too), and offers the local site. **N/A** means the row did not apply to what
73
+ was on screen. One scripted run is one run: report a gap as measured once, and repeat before
74
+ concluding.
75
+
76
+ ### What the game tells the instruments
77
+
78
+ Every press and every picture is recorded with the game's state, read from its port probe
79
+ (`exposePort`, the `port` skill) and the play page: round phase and time left, whether the body is
80
+ the player's to steer (`busy`), where it is. A game with no probe still runs, and its rows say the
81
+ phase was unknown. Three optional things make the rows sharper; none is required:
82
+
83
+ - In `exposePort(net, { extra: { ... } })`: `alive: () => boolean` (false: spectating until the
84
+ next round), `mode: () => string` (the control mode on screen), `loadout: () => string` (what
85
+ the player holds, when it changes which controls show), `touchHeld: () => boolean`, and
86
+ `drawCalls` / `triangles` (`renderer.info.render.calls` / `.triangles`: the look row then
87
+ reports measured runtime scene cost, which is not the asset inventory's estimate).
88
+ - In `game.json`, the primary action, so a shooter is played by firing and not by the space bar:
89
+
90
+ ```json
91
+ "playtest": { "primary": { "label": "fire",
92
+ "computer": { "mouse": "left" },
93
+ "phone": { "selector": "[data-action=fire]" } } }
94
+ ```
95
+
96
+ `computer` is `{ "key": "<KeyboardEvent.code>" }` or `{ "mouse": "left|right|middle", "at": [x, y] }`;
97
+ `phone` is `{ "selector": "<css, in the game's frame>" }` or `{ "region": [x, y, w, h] }`
98
+ (fractions of the screen). Undeclared, the script presses the space bar and taps a fixed spot,
99
+ says that was a guess, and qualifies anything it concludes from the scores. It does not aim.
100
+ - In `game.json`, `"scoring": "together"`: every result row carries the room's one shared total,
101
+ so the play row marks the active-against-idle comparison not applicable.
102
+
103
+ The UI row measures DOM coverage only. A HUD, labels or hints drawn inside the canvas are hidden
104
+ with the world and are not checked for coverage or overlap; REPORT.md says so beside the result.
105
+
106
+ The UI row also lays the game's visible DOM HUD against the play page's own controls, which the
107
+ page draws over the game's frame and reports as `window.__shell.rects` (the game reads the same as
108
+ `net.shell` / `net.on('shell')`). A HUD element under a control that stays is a WARN with both
109
+ named; under the "N playing" chip, which fades, it is a note. Fix it by laying the HUD out around
110
+ `net.shell`'s rectangles, or by moving the page's controls (`game.json` `screen.share`,
111
+ `screen.chat`). A page that reports no layout (an older studio) is said to be unchecked.
112
+
113
+ A reading taken while the browser was not in its room is labelled, never judged as play: the
114
+ helper's link (`window.__homieNet.link`, and `window.__shell.link.state`) says `reconnecting`,
115
+ `alone` (the room never answered; an older browser-hosted game may play locally), `offline` or `closed`, and a
116
+ press, a picture or a UI frame sampled then is BLOCKED or N/A with "CUT OFF from its room" in its
117
+ state. The round row says a browser "was seen cut off" beside completion.
118
+
119
+ The run writes `.playtest/<game>/<time>/` in the studio (added to `.gitignore`): `REPORT.md`,
120
+ `report.json`, contact sheets (`sheet-desk.png`, `sheet-phone.png`, `sheet-phone-landscape.png`),
121
+ every screenshot (the UI row's pair as `*-ui-on-black.png` and `*-ui-on-white.png`), `sound-capture.wav` and its spectrogram, and the owner-test receipts.
122
+ **Open the contact sheets and look at every picture before believing any number.**
123
+
124
+ ## 2. The blind review
125
+
126
+ For a rules game with maxSpeed 0 on every player kind, the toolkit marks the
127
+ movement probe stationary. First-move rows say N/A and directional owner tests
128
+ say not applicable, never PASS. Exercise its actual choices/taps/turns in two
129
+ browsers and measure local feedback separately from authoritative acceptance.
130
+ Do not add movement, fake pose samples or a board-direction probe to silence a check.
131
+ A hold entirely inside a hiding countdown, death or stun is blocked evidence.
132
+ Wait for an active, steerable phase and repeat the real controls in both browsers;
133
+ record that directed result beside the generic row. Keep the mechanic and honest
134
+ busy/alive probes; never remove a freeze or disguise it to turn a row green.
135
+
136
+ ```sh
137
+ node <playtest.mjs> review .playtest/<game>/<time>
138
+ ```
139
+
140
+ It writes `REVIEW.md` in that folder: a brief for a FRESH reviewer, with the pictures, the numbers and
141
+ a rubric.
142
+
143
+ **An outside review is optional, only when requested.** Finish local checks and your own visual review without an extra question. When an outside review is requested, it hands screenshots outside this session: use the authorization already given, or ask once if the destination was not specified.
144
+ The command prints `approval`: one self-contained question naming every file it sends (REVIEW.md,
145
+ report.json and the listed pictures), the report folder they come from, and the destination (name
146
+ it with `--to "<the reviewer>"`: a subagent in this session, or an external command such as a
147
+ second coding agent). For a requested outside review without prior destination consent, ask that question once. One yes covers that report
148
+ folder and that reviewer; another folder or reviewer is a new question. `review-request.json` in
149
+ the folder keeps the same list. A host that reviews what an agent sends may still refuse the
150
+ transfer: that is the host's decision. Do not rephrase and retry it, and do not send the pictures
151
+ another way. Then one of:
152
+
153
+ - `node <playtest.mjs> review <folder> --local` writes `REVIEW-LOCAL.md`: the same rubric for
154
+ this session to score the contact sheets itself. Nothing leaves the session. It is the builder
155
+ grading its own build, so it is labelled **not independent** everywhere, and you say so in the
156
+ first sentence when you report it.
157
+ Read that brief and every listed picture, follow its JSON rubric and write
158
+ `VERDICT.json` with `"review": "local"` in this exact run folder BEFORE calling
159
+ `reviewed --kind local`. The `reviewed` command records a verdict; it does not create one.
160
+ Rubric parts use 0–10; the overall JSON `score` and `--score` use **0–100**
161
+ (for example, an overall seven out of ten is `--score 70`, not `7`).
162
+ - Record what happened, always: `node <playtest.mjs> reviewed <folder> --kind independent --by
163
+ "<who>"`, `--kind local --reason "<why>"`, or `--kind none --reason "<why>"`. REPORT.md and
164
+ `report <folder>` then say which review this run had. With no record, or `none`, the review line
165
+ is BLOCKED: a missing review never reads as a passed one. Hand its WHOLE TEXT, unchanged, to a reviewer that has not seen the code, the plan or your
166
+ summary (Claude Code: the Agent tool with the file's full contents as the prompt, never just its path
167
+ or your summary of it; Codex: a new session). Add nothing about what you built or changed. It plays the game itself, scores it
168
+ 0-100 in eight parts, and returns gaps ranked by impact, each with evidence it saw and one concrete
169
+ fix. Its verdict outranks yours. Put it in the run folder as `VERDICT.json`.
170
+
171
+ Comparing against other games (the person's references, the studio's last version, a game they
172
+ admire): give the reviewer frames of each under shuffled labels, scored before the labels are revealed.
173
+ A reference is a bar ("is it as good as that"), never a template ("make it look like that").
174
+
175
+ A game with models: give the reviewer the style board, the golden images and the lineup pictures
176
+ (`homie-studio assets lineup <id>`, in `.studio/art/<id>/`) beside the frames, and add one question: "does every
177
+ frame look like ONE game?" (1-10, naming outliers). Record it with `homie-studio assets review <id> --score <n>
178
+ --outliers <ids>`.
179
+
180
+ ## 3. Say what is weak
181
+
182
+ Lead with the numbers and the review, most important first: what fails, then what the reviewer would
183
+ fix first. Group findings by cause before proposing work (five complaints about flat shapes are one
184
+ missing lighting model, not five jobs). Name the one change that would matter most. Then, if the
185
+ person wants, fix it and run the same instruments again; a change that does not measure better than
186
+ the version before it is a regression, not progress. When the weak thing is speed (a low frame rate in
187
+ `first`, a slow first ten seconds, a phone that struggles), the `perf` skill measures it properly: frame
188
+ times and CPU per frame for player browsers (server tick work is separate), alternating runs,
189
+ and only changes that beat the noise.
190
+ When the weak thing is how a move feels (a hit that does not land, a floaty jump), the `lab` skill compares the
191
+ change with the last commit frame by frame, with the person.
192
+
193
+ `references/METHOD.md` has the method behind the rows: playing like a person, the five numbers a
194
+ round owes you, the scenarios, seats with different strategies, the do-nothing test, the ten-second
195
+ test, and the traps that make an instrument lie.
196
+
197
+ ## Never
198
+
199
+ - Never call a game good, fixed or finished from the code, a build that passed, or your own look at it.
200
+ - Never report a local review as a blind one, and never read a BLOCKED or N/A row, or a missing review, as a pass.
201
+ - Never turn one scripted round into a rule ("people cannot win", "input does not matter"): say what was measured, and run it again.
202
+ - Never call the builder's local verdict an independent review: an independent reviewer is fresh every time.
203
+ - Never script a player to make a number look good; the instruments play like a person on purpose.
204
+ - Never leave a room open on a live site: the playtest's browsers leave when it ends; do not start
205
+ more than two browsers of your own beside it.
@@ -0,0 +1,124 @@
1
+ # The method behind the rows
2
+
3
+ How a game gets judged honestly, and the ways a test lies. Everything here was measured on games that
4
+ passed their tests and still failed people.
5
+
6
+ ## Play it like a person
7
+
8
+ Hold a direction for most of a second, not one frame. Press the thing whose label sounds fun. Try the
9
+ control you would try first if nobody had told you anything. A script that presses every button in
10
+ order is a conformance test, and conformance is not what is wrong with a boring game.
11
+
12
+ ## The five numbers a round owes you
13
+
14
+ | | say | why |
15
+ | --- | --- | --- |
16
+ | how long | seconds from start to finish, against the clock the game set | a 150-second round that ends at 40 means nobody has seen most of the game |
17
+ | how it ended | who won and by how much | if you cannot tell, a stranger cannot either |
18
+ | the tense moment | one sentence about the best five seconds, or "there wasn't one" | the answer that unsticks a stalled game |
19
+ | what did nothing | any control that produced no visible change | a dead control is the first thing people notice |
20
+ | what you wanted | the one thing you reached for and could not do | the next change |
21
+
22
+ Add **lead changes** (the number that separates a race from a procession) and **whether doing nothing
23
+ scores as well as playing** (the `play` row measures both humans).
24
+
25
+ ## Different strategies in different seats
26
+
27
+ Seats that all play the same way can only prove the game runs. Give simulated seats different
28
+ standing strategies, one per side of the decision the game claims to be about: the greedy one that
29
+ takes the risk the design says should pay, the safe one, the one that goes near other players on
30
+ purpose, the middle. Then read which one scored zero. On one game whose whole idea was "carrying more
31
+ makes you a target", the greedy runner finished on 0, 0 and 0 in three rounds because at a full carry
32
+ it was slower than the hunter and could never escape: the exciting strategy was arithmetic, and the
33
+ game everyone described was never actually played. No screenshot and no reading of the code found it;
34
+ three rounds with four strategies did, in two minutes.
35
+
36
+ ## The do-nothing test
37
+
38
+ Write it first: a player who stops touching anything, at every place a player plausibly stops, for
39
+ longer than anyone's patience. The game must come to get them (a nudge, a timer, the round moving on),
40
+ never freeze, never go black, never soft-lock the room. Continuous worlds must also be tuned in real
41
+ seconds: a decay tuned on a fast test clock empties a real room in a minute. Simulate a long idle
42
+ stretch and read the state before and after.
43
+
44
+ ## Every scenario before "finished"
45
+
46
+ 1. One person alone, with bots, on a computer.
47
+ 2. One person on a phone, portrait and landscape (a game that needs landscape says so and survives the
48
+ turn).
49
+ 3. Two strangers who pressed Play at different times, including one who arrives mid-round and takes a
50
+ bot's place (and its score: a joiner inherits the body, so only a round that starts after both are in
51
+ says how they played).
52
+ 4. The host leaving mid-round: someone else continues the same round, clock where it was.
53
+ 5. The big screen (`/<id>/tv`): a spectator with no body, a join QR, the game moving.
54
+ 6. Nobody touching anything.
55
+
56
+ Say which you tried. "We could not test it" and "we tested it and it was fine" must never sound alike.
57
+
58
+ ## The ten-second test
59
+
60
+ Ten seconds of silent video, no context, on a phone someone is scrolling past: do they stop? Every
61
+ change should leave one moment worth clipping (a knockout that reads from across a room, a comeback you
62
+ can see coming, something that happens once a round and is unmistakable). A change that produced
63
+ nothing you would cut fifteen seconds around produced polish; polish compounds slowly and nobody shares
64
+ it. Take a swing (a hazard, a rule that changes the last ten seconds) rather than tuning a number.
65
+ Readable at a distance: whose is that, what just happened, who is winning. Punctuate: silence, then a
66
+ hit. A thing that happens constantly is wallpaper; once a round it is an event.
67
+
68
+ ## Judging, not building
69
+
70
+ - **The builder never grades its own work.** A reviewer gets the goal, the bar and the running game,
71
+ never the builder's reasons.
72
+ - **Measure, don't assert.** "I differenced the two frames: nothing moved more than 8 of 255" ends an
73
+ argument that prose cannot.
74
+ - **Two instruments on the same build.** A reviewer given only pictures grades the picture: one scored
75
+ a game 34/100 for a hunted player drawn at 1.14:1 contrast against the searchlight meant to mark
76
+ him, while a play measurement of the same build found the round always ended at 40 s of 150 and the
77
+ risky strategy scored zero. Neither could see what the other saw. When they disagree, the fight
78
+ outranks the frame.
79
+ - **The whole frame and a real round, regularly.** A reviewer shown a crop can only ask about the crop:
80
+ thirty-five changes in a row once went into one decorative prop, each winning its own comparison,
81
+ while the game as a whole did not improve.
82
+ - **Judge the set.** Twenty screens that each passed review were one layout repeated fourteen times.
83
+ Put the whole set in front of one reviewer: squint, how many distinct shapes are there?
84
+ - **A reference is a bar, not a template.** Ask "is it as good as that", never "is it shaped like that".
85
+ - **Group findings by cause** before assigning work; five findings, one cause, one fix in the shared layer.
86
+ - **A finding that names a relation has two sides**: "the brightest thing is not a player" can be fixed
87
+ by darkening the floor or brightening the player; move one side on purpose and check the other.
88
+ - **Stop on a regression.** A change that is not clearly better than the one before is worse. Scores
89
+ like 48, 54, 58, 50, 42 mean the peak was 58: go back to it.
90
+
91
+ ## Measured bars
92
+
93
+ | what | bar |
94
+ | --- | --- |
95
+ | seated and moving | under 10 s from pressing Play, first press moves you within about a second |
96
+ | hold a direction 5 s | one straight line the pressed way on screen, camera yaw change under 10° |
97
+ | alternate directions 10 s | every press goes the pressed way within 600 ms |
98
+ | UI during play | at most about 12% of the screen, nothing opaque in the middle third |
99
+ | look | no black or one-colour frame; detail at several scales (edge share of a finished-looking frame is several times a placeholder's) |
100
+ | sound | starts on the first touch; every action answers; -16 to -12 LUFS while played; nothing clips; little lost on a phone |
101
+ | rounds | two strangers meet in one room and both see a round finish; a killed host hands over in 5 s; a late joiner takes a bot's place |
102
+
103
+ ## How instruments lie
104
+
105
+ - **A software renderer fakes a stuck game.** Headless Chrome without the GPU draws a WebGL game at 1-2
106
+ fps; count animation frames first and say BLOCKED under ~20 fps. With the GPU flags a headless shot
107
+ matches the real screen within a fraction of a luma point.
108
+ - **Reading a WebGL canvas from a script returns black** unless it is read inside a late animation frame;
109
+ use a screenshot of the page (the compositor's picture) instead.
110
+ - **A full-page screenshot stretches the viewport**, so phone-landscape media queries stop matching; take
111
+ the viewport only.
112
+ - **Emulated phones pass while real phones fail.** Real touch on iPhone Safari differs (coalesced
113
+ moves, passive listeners, pointer ids, 120 Hz); the iOS Simulator is the closest thing to an iPhone
114
+ on a Mac (`xcrun simctl openurl booted <url>`, mouse drags become real touches). Say when a result
115
+ is emulation only.
116
+ - **A delivered press is not a correct press.** Only the body's movement on screen proves direction.
117
+ - **Gate on the thing itself.** "A sound file plays" and "a texture is bound", not a number a hiss and a
118
+ flat cube would pass. Ask what the cheapest thing that passes the check would be.
119
+ - **A still is not the game.** A title card or the moment before a round starts is a picture of
120
+ nothing happening; shoot while it is being played.
121
+ - **Another run can replace what you are judging.** Note the build you judged (a digest, a commit) before
122
+ and after.
123
+ - A reviewer that shares your working folder can "tidy" your half-written file: commit before you hand
124
+ work to one.
@@ -0,0 +1,78 @@
1
+ # Blind review: {{GAME}}
2
+
3
+ You are reviewing a multiplayer web game you did not make. You have not seen its code, its plan or its
4
+ maker's notes, and you must not read them: judge only what a player gets. You are demanding and fair,
5
+ you have played the best browser party games, and your job is to find what would make a stranger
6
+ leave, not to encourage the maker.
7
+
8
+ The game: {{URL}} (strangers who press Play land in the same public room; bots fill empty seats; rounds
9
+ end and restart by themselves). The playtest instruments already ran; their folder is `{{FOLDER}}`.
10
+
11
+ ## Look at these yourself, every one, before you read any number
12
+
13
+ {{PICTURES}}
14
+
15
+ ## What the instruments measured
16
+
17
+ {{NUMBERS}}
18
+
19
+ `report.json` in the folder has every number. A BLOCKED row measured nothing; never read it as a pass.
20
+
21
+ ## Play it
22
+
23
+ Play at least one full round yourself in a headless Chrome on the GPU
24
+ (`--use-angle=metal --enable-gpu-rasterization --ignore-gpu-blocklist` on a Mac): once as a computer
25
+ with the keyboard, and once as a phone (390x844, touch events: a thumb that lands, slides and holds).
26
+ Take your own screenshots at moments you choose. Close every browser you open, at most two at a time.
27
+ If you cannot play it, say so and score only what you saw.
28
+
29
+ ## Score it
30
+
31
+ Each part 0-10, then an overall 0-100 that is your honest judgement, not the average:
32
+
33
+ 1. **First ten seconds**: in, seated and moving, knowing what to do, with no instructions read.
34
+ 2. **Controls**: every press means the same thing every second; the camera never turns on its own; a
35
+ phone thumb is as good as a keyboard.
36
+ 3. **The round**: a goal you understand, tension that rises, a finish you can see coming, results
37
+ that say who won and why. Does playing well beat doing nothing?
38
+ 4. **Look**: does it look finished at phone size, readable at arm's length, with a clear subject, detail
39
+ at more than one scale, and nothing flat or placeholder-like?
40
+ 5. **Sound**: does every action answer, does the music carry the round and change with it, is it
41
+ balanced and clean on a phone speaker?
42
+ 6. **Phone**: one-thumb play, UI out of the way (the middle of the screen clear), portrait and landscape.
43
+ 7. **Strangers**: with bots and other people, is it fun together, and does a late joiner fit in?
44
+ 8. **Clip test**: ten seconds of silent video on a phone someone is scrolling past. Do they stop?
45
+
46
+ Then answer plainly: would you play another round? Would you send it to a friend? Would you pick it
47
+ over the best browser party game you know? "No, and here is why" is a useful answer.
48
+
49
+ ## Rules for your findings
50
+
51
+ - **Measure, don't assert.** "The score text is 11 px on a 390 px screen" beats "the HUD is small".
52
+ Every gap names its evidence: a picture and what is in it, or a number from the report or your own play.
53
+ - **The fight outranks the frame.** If the pictures look fine and the round is boring, say the round is
54
+ boring; if doing nothing scores as well as playing, that is the biggest finding there is.
55
+ - **Group by cause.** Five findings with one cause (no lighting, one missing sound bus) are one gap.
56
+ - **One concrete fix per gap**, the smallest change that would move it, and which side of a relation
57
+ it moves ("make the player brighter than the floor" can be done from either side: say which).
58
+ - **Impact 1-10**: how much fixing it would raise your overall score.
59
+ - Say what you verified yourself and what you could not test.
60
+
61
+ ## Return exactly this JSON (save it as `{{FOLDER}}/VERDICT.json`)
62
+
63
+ ```json
64
+ {
65
+ "score": 0,
66
+ "parts": { "first10": 0, "controls": 0, "round": 0, "look": 0, "sound": 0, "phone": 0, "strangers": 0, "clip": 0 },
67
+ "wouldPlayAgain": false,
68
+ "wouldSendToAFriend": false,
69
+ "summary": "two or three sentences a person could act on",
70
+ "verified": ["what you checked yourself, with how"],
71
+ "couldNotTest": ["what you could not test, and why"],
72
+ "gaps": [
73
+ { "title": "", "evidence": "", "fix": "", "impact": 0 }
74
+ ]
75
+ }
76
+ ```
77
+
78
+ Gaps sorted by impact, the largest first. No praise padding; no gap without evidence.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Ending a controller: after its report is written and its browsers are closed, the process leaves, with the exit
3
+ * code its result earned.
4
+ *
5
+ * Why this exists: a full run wrote its report, its sheets and the reviewer command, every Chrome and ffmpeg it
6
+ * owned had closed, and the Node process stayed. Node only leaves by itself when nothing is left on its event loop,
7
+ * and a controller that drives browsers collects things that sit there: a socket puppeteer kept, a pipe a child's
8
+ * helper inherited, a race's losing timer, an AbortSignal's timer. Which one it was that time was never isolated,
9
+ * so the end does not depend on finding it: run the cleanup (bounded), flush what was printed, then exit.
10
+ *
11
+ * The exit code is the one already decided (`process.exitCode`, or `code`): a failed run still exits non-zero.
12
+ */
13
+
14
+ /** Race `p` against `ms`; the losing timer is cleared, so the race itself leaves nothing on the event loop. */
15
+ export function within(p, ms, fallback = null) {
16
+ let timer = null;
17
+ const late = new Promise((r) => { timer = setTimeout(() => r(fallback), ms); });
18
+ return Promise.race([Promise.resolve(p).catch(() => fallback), late]).finally(() => clearTimeout(timer));
19
+ }
20
+
21
+ /**
22
+ * Run `cleanup` (at most `graceMs`), wait for stdout and stderr to drain (at most 2 s), then exit with `code`
23
+ * (default: process.exitCode, else 0). `exit` is process.exit (a test passes its own).
24
+ */
25
+ export async function finish({ code = null, cleanup = null, graceMs = 15_000, exit = (c) => process.exit(c) } = {}) {
26
+ if (cleanup) await within(Promise.resolve().then(cleanup), graceMs);
27
+ // A pipe (a caller reading our JSON) takes writes asynchronously: an exit before the drain would cut the report.
28
+ const drained = (s) => new Promise((r) => { if (!s?.writableLength) r(); else s.write('', () => r()); });
29
+ await within(Promise.all([drained(process.stdout), drained(process.stderr)]), 2000);
30
+ exit(code ?? process.exitCode ?? 0);
31
+ }