Recovers an interrupted session and finishes its verification honestly. RESTORED (the predecessor was killed mid-mutation-check): - build_sail_post_corroded still had negative control M1 APPLIED — clean galvanised steel, DROOP=0 — so both shipped GLBs were the untelled post. Restored: steel_weathered/rust/rust_deep, DROOP_DEG = 38. - asset_report.json held 1 of 37 assets and contact_sheet.png was a 196 KB truncated write. Both were interrupted artifacts, not decisions; a clean rebuild restores 37 assets and a 5.07 MB 1680x4200 sheet. DETERMINISM: 37/37 GLBs byte-identical across two consecutive full runs; report and contact sheet byte-identical too. Zero churn on the 35 pre-existing GLBs. ASSERT THAT COULD NOT FAIL, KILLED: the sag pin (honestY - corrodedY > 0.05) reads 0.060 at ZERO droop, because the eye hangs off a weld line 160 mm down the post — measured on the M-B build, anchor at (0, 3.920, 0). It measured the weld offset and called it a sag. Replaced with the droop ANGLE derived from the exported anchor and pinned to baked extras (padeye_droop_deg / arm_m / weld_y_threejs / dir_threejs), all stated in three.js coords — the axis trap closed the swing_set way, so note and mesh can only lie together. ASSERT WHOSE REASON WAS BACKWARDS, FIXED: the 20 m pin claimed the corroded shaft read darker. Measured off look.html's canvas at 20 m: honest lum 50, corroded lum 94 — it renders twice as BRIGHT. Base colour is darker but metalness drives the pixels (gal 0.90/0.35 goes near-black with no env map; weathered 0.55/0.60 is diffuse). Test now pins both halves and names the mechanism the player actually sees. Three tells split into three tests so one failure cannot mask the others — pre-split, the M-A mutation went red on rust area and never reached the lightness or droop asserts at all. Mutation-checked in the browser, all three witnessed red: M-A clean steel (1/3 + 2/3 red, 3/3 green, factory GREEN throughout — the trap it cannot see, confirmed); M-B flat eye (3/3 red, others green); M-C baked note lies while mesh is honest (3/3 red, proving the pin reads the extra, not a literal). Selftest 483/0/0 (474 baseline + 9 exactly). Tell verified by looking at 20 m at the game's own camera height, screenshots taken. editor.js untouched — palette hunk filed verbatim in THREADS for A. contracts.js ANCHOR_TYPE widening flagged for A. Lane D flagged: the tier is placeable, gate 3.2 unblocked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| docs | ||
| prototype | ||
| tools | ||
| web/world | ||
| .gitignore | ||
| DESIGN.md | ||
| LANE_PROMPTS.md | ||
| PLAN3D.md | ||
| README.md | ||
| ROADMAP.md | ||
| server.py | ||
| SPRINT2.md | ||
| SPRINT3.md | ||
| SPRINT4.md | ||
| SPRINT5.md | ||
| SPRINT6.md | ||
| SPRINT7.md | ||
| SPRINT8.md | ||
| SPRINT9.md | ||
| SPRINT10.md | ||
| SPRINT11.md | ||
| SPRINT12.md | ||
| SPRINT13.md | ||
| SPRINT14.md | ||
| SPRINT15.md | ||
| SPRINT16.md | ||
| SPRINT17.md | ||
| THREADS.md | ||
HARD YARDS — rig it, then survive the night
(repo/product history: the project was codenamed SHADES through Sprint ~8; the shipped game is HARD YARDS. Internal docs and the gitea repo name still say shades — that's the same thing, not a second game.)
Geometry/tower-defense: you're a small person rigging shade sails against Australian storms. three.js r175 (vendored, no build step) + a zero-dep Python server. Everything the repo needs is in the repo.
Run it
python3 server.py --port 8801 # any free port
# game: http://localhost:8801/web/world/index.html
# selftest: http://localhost:8801/web/world/selftest.html (must be all green)
# 2D proto: http://localhost:8801/prototype/index.html (the original thesis)
No pip, no npm, no build. Python 3.8+ and a browser.
Read in this order
DESIGN.md— the design canon (what the game is)PLAN3D.md— the build canon (stack, contracts, lane model) ·docs/MANUAL.md— the HOW: tool map, asset/site cookbooks, earned gotchas ·ROADMAP.md— the growth canon (the arcs from the week to the business)THREADS.md— the append-only lane log. The project's memory. Read the last few[I]integrator entries to know the current state.SPRINT15.md— the current sprint (highest-numbered fired sprint wins: SPRINT16 is pre-speced TEETH and fires only when 15 integrates — its own header says so)LANE_PROMPTS.md— copy-paste prompts per lane, per sprint (bottom = newest)
The lane model (how this repo is built)
Five parallel Opus agents ("lanes"), each in its OWN clone, on its own branch:
| lane | owns |
|---|---|
| A | main.js, world.js, camera, hud, week, sites, server — shell & integration, merge shepherd |
| B | sail.js, rigging — cloth sim, economy, balance.test pen |
| C | weather, skyfx, debris, storm/site wind data |
| D | player, interact, ladder, broom — and the designated playtester |
| E | Blender asset factory (tools/blender), models/, textures |
Hard rules (they exist because each was earned — see THREADS):
- One clone per lane. Never two agents in one working tree.
- Rebase onto latest
mainbefore starting; small commits; push your branch. - File ownership is the merge strategy; cross-lane needs go in THREADS.md.
- Selftest green after every landing. Skips must be real. An assert that cannot fail is decoration.
- All sim code is deterministic
(dt, t)— no Date.now, no rAF. - Measure, don't reason: the repo's biggest bugs were all "a number gathered from the wrong harness". When two harnesses disagree, find the variable.
Setup on a new machine
Gitea's SSH runs over tailscale (the public host is Cloudflare-proxied, no
direct SSH). Add to ~/.ssh/config:
Host gitea.partly.party
HostName 100.71.119.27
Port 222
User git
Then:
git clone git@gitea.partly.party:monster/hardyards.git ~/Documents/shades
for L in A B C D E; do
git clone git@gitea.partly.party:monster/hardyards.git ~/Documents/shades-lane$L
git -C ~/Documents/shades-lane$L checkout -B lane/$(echo $L | tr 'A-Z' 'a-z') origin/lane/$(echo $L | tr 'A-Z' 'a-z')
done
~/Documents/shades is the integration checkout (main; where merges and
sprint docs happen). The five shades-lane* clones are where lane agents run.
Per clone, once: cp .claude/launch.example.json .claude/launch.json and pick
a port of your own (integration 8823, lanes A–E 8824–8828). The real
launch.json is untracked — six checkouts on one laptop can't share one port.
Sprint cycle
- Fire lanes with the newest prompts at the bottom of
LANE_PROMPTS.md(Lane A usually first if the sprint gives it rulings/blocking items). - Lanes work, push their branches, log in THREADS.md.
- Integrate in the main checkout: merge
origin/lane/{e,c,b,d}(THREADS.md conflicts resolve keep-both-chronological; real code conflicts get read), run selftest, play the result, writeSPRINT(n+1).md+ prompts, push main AND fast-forward all fivelane/*branches to main.