Fable (integrator): ROUND 33 CLOSED — THE HUNT lands, all green; the beta's last system is in

qa.sh --strict --matrix on the final tree: 7 passed · 0 failed · 0 warn · 0 skipped, MATRIX 10
towns green. New STRICT smoke_wantlist_travel: ☆→★ through the real card (the save carried
{THE VERANDAHS — 'Servo at Midnight'}) · a keeper muttered the hunted artist within 2 shops
('saw one in Castlemaine just last week') · buying the wanted sleeve cleared the hunt · the
synthetic→melbourne ride cost EXACTLY one day with $179 and the find riding along · the
same-town reboot was free (the control discriminates) · the ride home charged again (day 3) ·
?game=0 built zero want DOM and muttered nothing · 0 console errors. Matrix note: town_matrix
shares one browser across towns so travel drifts the day as it walks — safe because
determinism double-boots the SAME town (free by the same-town rule); reasoned before landing.
Money shots: docs/shots/v7_beta/rumor_and_wantlist.png (the keeper's mutter over the toy
shop's 70s wallpaper + THE WANTLIST open in the crate) + want_card.png. README manual row.
v7.0-beta systems are now ALL BUILT — what remains of v7: Esc-in-person (John's hands), then
the v7.0 close (GodBay offline auctions, THE FIRST FIVE MINUTES, the tour, the tag).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 19:01:40 +10:00
parent 7b26e45fa0
commit 776b10b599
7 changed files with 216 additions and 0 deletions

View File

@ -4,6 +4,19 @@
--- ---
## Round 33 (v7.0-beta wave 2) — THE HUNT: wantlist · rumor · travel (Fable, solo, JING5)
☆ WANT on the pull card → `wants` on procity-save/1 (optional, capped, deduped); buying a
matching item clears the hunt; THE WANTLIST section in the crate panel. The keeper gossips:
seeded 45% banner mutter naming a hunted want + a real cached town. TRAVEL COSTS A DAY:
adopting a save from another townKey (town OR seed) charges +1 day in save.js adopt() — load
and import both pay, same-town stays free (byte-equality gates untouched); arrival toast names
the fare. New STRICT `smoke_wantlist_travel` (real-card want · rumor scan · auto-clear ·
+1-day with intact cash/collection · same-town control · return fare · ?game=0 zero-DOM).
Full record: LANE_F_NOTES §33.
---
## Round 32 (v7.0-beta wave 1) — THE BIBLE + scarcity + the §31.6 queue (Fable, solo, JING5) ## Round 32 (v7.0-beta wave 1) — THE BIBLE + scarcity + the §31.6 queue (Fable, solo, JING5)
Guide bands derived in `js/interiors/bible.js` (E's thresholds mirrored, no pack re-emit) → Guide bands derived in `js/interiors/bible.js` (E's thresholds mirrored, no pack re-emit) →

View File

@ -25,6 +25,7 @@ Click **START** to lock the mouse. Then:
| **E** (inside a shop) | open the crate at a record bin — then it's cursor-driven: click sleeves to riffle, click BUY | | **E** (inside a shop) | open the crate at a record bin — then it's cursor-driven: click sleeves to riffle, click BUY |
| **E** (at the counter) · **C** (anywhere) · **SLEEP** (bottom bar) | sell a find to the keeper (he pays under the **guide band**, never over) · open THE CRATE — your collection, in shops too now · sleep = save, the day advances, stock rotates | | **E** (at the counter) · **C** (anywhere) · **SLEEP** (bottom bar) | sell a find to the keeper (he pays under the **guide band**, never over) · open THE CRATE — your collection, in shops too now · sleep = save, the day advances, stock rotates |
| read the **guide** stamp on every price card | the 90s price-guide band ("guide $824"). An item **asking under its band is a gem** — buy it, sell it to a keeper, pocket the margin. Bought slots stay gone for the day (the bin remembers) | | read the **guide** stamp on every price card | the 90s price-guide band ("guide $824"). An item **asking under its band is a gem** — buy it, sell it to a keeper, pocket the margin. Bought slots stay gone for the day (the bin remembers) |
| **☆ WANT IT** on a pull card | start hunting it — keepers gossip ("saw one in Castlemaine"), and buying one clears the hunt. **Switching towns costs a day** — the 23 towns are a map now, not a dropdown |
| click a book spine / toy box | pull-and-buy card (real price stickers in godverse shops) | | click a book spine / toy box | pull-and-buy card (real price stickers in godverse shops) |
| walk out the door / **Esc** | leave the shop (Esc also frees the mouse anywhere) | | walk out the door / **Esc** | leave the shop (Esc also frees the mouse anywhere) |
| **M** | 2D town map (street only) | | **M** | 2D town map (street only) |

View File

@ -1813,3 +1813,35 @@ band, which is what charter law #2 always said.
`web/assets/models/` is GITIGNORED build output — a fresh checkout runs stock=real/book-toy/ `web/assets/models/` is GITIGNORED build output — a fresh checkout runs stock=real/book-toy/
no-pump gates RED with 404s (the R31-green tree lives on m3ultra). Fixed here by rsync from no-pump gates RED with 404s (the R31-green tree lives on m3ultra). Fixed here by rsync from
m3ultra. If a third machine joins the rotation, sync that dir (49 M) or re-run E's pipeline. m3ultra. If a third machine joins the rotation, sync that dir (49 M) or re-run E's pipeline.
## §33 — R33: v7.0-beta wave 2 — THE HUNT (wantlist · rumor · travel) (Fable, solo, JING5)
- **save.js:** optional `wants: [{artist?|title?|sku?}]` on procity-save/1 (validated
exact-and-loud when present; ≥1 string field per entry; cap 40; absent-when-empty keeps the
alpha byte shape). `recordWant` (case-insensitive dedupe) / `findWant` (a want covers an item
when every field the WANT names matches — sku exact, artist/title by value) / `removeWant`
(identity). **TRAVEL in `adopt()`:** adopted save's `town !== townKey``day += 1`,
`traveled = true`, prunePulls — load AND import both pay the fare; same-town adoption free
(save-determinism byte-equality untouched). `game.traveled` getter → the shell's arrival toast.
- **dig.js:** the pull card's ☆ WANT line — construction-gated on `opts.onWant` (game absent ⇒
zero want DOM, the classic-pure pattern). ☆ click → ★ ON THE WANTLIST (a stamp, not a button,
once wanted).
- **interior_mode.js:** onWant/isWanted seams into dig.open; buy of a matching item removes the
want (+ '✓ off the wantlist' toast). **THE RUMOR** in the enter() banner: seeded
`xmur3('rumor:<citySeed>:<shopId>:<day>')` → 45% roll → hunted want + a town from
RUMOR_TOWNS (the repo's real cached towns, current town excluded). Runtime-only stream —
A's boundary holds; no game / empty wants ⇒ the line never exists.
- **hud.js:** THE WANTLIST section in the crate panel (non-empty only): artist/title rows, ✕
stops hunting; refresh watches wants length so an open panel follows adds/clears.
- **Gate:** `smoke_wantlist_travel` STRICT (see ROUND33_INSTRUCTIONS for the green run) — real
card path, seeded rumor scan, auto-clear, +1-day travel with same-town negative control and
the return fare, `?game=0` zero-DOM control, 0 console errors.
- **Matrix note:** town_matrix shares one browser across towns, so travel drifts the day as it
walks the matrix — safe because determinism double-boots the SAME town (free by the
same-town rule) and no matrix gate reads absolute day. Reasoned before landing, not after.
- **The shot found what the gate didn't (harness lesson #5 candidate):** R32's crate-in-shops
surface BUILT only in street update() frames — a door entered fast after boot had no game
surface inside at all, and the interior tickGame() happily refreshed a UI that never existed.
Green gates missed it because every gate's boot idles on the street long enough to build.
Fixed: tickGame() now builds AND refreshes (any mode); found by taking the money shot, which
entered the shop faster than any gate does. Shots are a detector, not decoration.

View File

@ -0,0 +1,52 @@
# PROCITY — Round 33 instructions (from Fable, integrator)
Date: 2026-07-20 · **R32 closed all green** (7/0/0, matrix 10 towns — THE BIBLE + scarcity live).
Round 33 is **v7.0-beta wave 2 — THE HUNT**: the wantlist, the rumor, and travel that costs a
day ([V7_THE_GAME.md](../V7_THE_GAME.md) system #6, the last unbuilt beta system). Solo round
again (John's "keep going"), same treaty, lane ownership honoured in the commits.
## The design rulings this round encodes
1. **A want is something you SAW.** The wantlist grows from the pull card (☆ WANT IT — you
pulled it, it's dear, you'll hunt one cheaper) — not from an abstract search box the 90s
didn't have. Buying a matching item clears its want automatically (the hunt ends when
you're holding it). Wants ride the save (`wants: [{artist?|title?|sku?}]`, optional field,
same procity-save/1 back-compat law as `pulls`).
2. **The keeper finally gossips** (charter: "D's citizens finally gossip for a reason").
Seeded per (citySeed, shop, day): ~45% of shop entries mutter a hunted want + a real cached
town in the banner — flavor and a travel nudge, honest about being neither promise nor
stock lookup (town-wide packs mean any town CAN surface the record eventually). Runtime
stream, freshly keyed — no plan/golden moves. Full persona lines remain D's when the v6
clip work lands; the banner is the keeper's voice until then.
3. **Travel = the townKey changing hands.** Booting a save in a different town (different
plansrc/town/**seed** — a new seed is a new town) costs exactly one day, applied in
save.js `adopt()` so load and import both pay it, and pulls prune on the way. Same-town
reloads and export→import round trips stay free — the save-determinism gate's byte
equality survives untouched. The town selector IS the bus; the arrival toast names the
fare. The 23 towns are now a map you spend days moving across, not a dropdown.
## Gate (F)
`smoke_wantlist_travel` (STRICT): ☆→★ through the real card → the save carries the want ·
a keeper names the hunted artist within the open shops (seeded scan) · buying the wanted
sleeve auto-clears it · synthetic→melbourne costs exactly one day with cash/collection intact
· same-town reboot is FREE (the control) · the ride home costs a day again · `?game=0` builds
zero want DOM and mutters nothing.
## ROUND CLOSED (Fable) — ALL GREEN
`qa.sh --strict --matrix`: **7 passed · 0 failed · 0 warn · 0 skipped**, MATRIX 10 towns green
(the shared-context day drift travel introduces across matrix towns was reasoned through
before landing: determinism double-boots the SAME town, so no gate sees it). The new gate's
run: want carried {THE VERANDAHS — "Servo at Midnight"} · rumor after 2 shops ("🗣 THE
VERANDAHS? saw one in Castlemaine just last week") · buy cleared the want · travel day 1→2
with $179 and the find riding along · same-town free · return trip day 3 · game=0 clean ·
0 console errors. Full record: LANE_F_NOTES §33.
Also caught and fixed here: the R32 crate-in-shops surface only BUILT in street frames — a
door entered fast after boot had no game surface inside (the money-shot script entered faster
than any gate does and exposed it; tickGame() now builds-or-refreshes in any mode). Money
shots: `docs/shots/v7_beta/rumor_and_wantlist.png` + `want_card.png`.
**What remains of v7:** §31.6-5 (dig Esc in person — John's hands) · then the v7.0 close:
GodBay offline auctions (G's lane), THE FIRST FIVE MINUTES (onboarding), the tour, the tag.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

View File

@ -2136,6 +2136,123 @@ def smoke_bible(p):
b.close() b.close()
def smoke_wantlist_travel(p):
"""R33 gate — THE WANTLIST + THE RUMOR + TRAVEL COSTS A DAY (v7.0-beta wave 2). Legs:
(a) WANT on the pull card (real path) the save carries the want; (b) a keeper rumor
naming the hunted artist surfaces in some shop's banner (seeded per shop×day — scan);
(c) buying the wanted item auto-clears the want; (d) booting ANOTHER town on the same save
costs exactly one day (cash/collection intact), (e) re-booting the SAME town costs nothing
(the control discriminates), (f) the return trip costs a day again; (g) ?game=0 builds ZERO
want DOM and mutters no rumor (construction-gating holds)."""
head('GATE R33: wantlist · rumor · travel-costs-a-day (+ same-town and ?game=0 controls)')
b, pg, errs = new_page(p)
try:
boot(pg, '')
r0 = _enter_record_shop(pg)
if r0.get('err'): FAIL(f"wantlist: {r0['err']}"); return
# (a) pull the front sleeve, click ☆ WANT through the real card
_at_bin(pg)
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(400)
pg.evaluate("""() => { const c = window.PROCITY.renderer.domElement;
c.dispatchEvent(new PointerEvent('pointerdown', { clientX: 640, clientY: 360, bubbles: true }));
c.dispatchEvent(new PointerEvent('pointerup', { clientX: 640, clientY: 360, bubbles: true })); }""")
pg.wait_for_timeout(300)
rw = pg.evaluate("""() => {
const w = document.querySelector('.pcdg-panel .want');
if (!w) return { err: 'no WANT line on the card' };
const before = w.textContent; w.click();
const g = window.PROCITY.game, ex = JSON.parse(g.export());
return { before, after: w.textContent, wants: ex.wants || [], live: g.wants.length }; }""")
if rw.get('err'): FAIL(f"wantlist: {rw['err']}"); return
if 'WANT IT' in rw['before'] and 'ON THE WANTLIST' in rw['after'] and len(rw['wants']) == 1 and rw['live'] == 1:
OK(f"wantlist: ☆→★ through the real card — the save carries {rw['wants'][0]}")
else:
FAIL(f"wantlist: want did not take — {rw}")
artist = (rw['wants'][0].get('artist') or rw['wants'][0].get('title') or '').upper()
pg.evaluate("""() => { const p = document.querySelector('.pcdg-panel');
const x = p && p.querySelector('.x'); if (x) x.click();
const o = document.querySelector('.pcdg-out'); if (o) o.click(); }""")
pg.wait_for_timeout(200)
pg.evaluate("() => window.DBG.exitShop()")
pg.wait_for_timeout(300)
# (b) the rumor: seeded ~45% per (shop, day) — walk the town's open shops until a keeper mutters
rr = pg.evaluate("""() => {
const P = window.PROCITY, D = window.DBG;
const open = (P.plan.shops || []).filter(s => P.isOpen(s)).slice(0, 10);
const seen = [];
for (const s of open) {
D.enterShop(s.id);
const t = [...document.querySelectorAll('div')].map(e => e.textContent)
.find(t => t && t.includes('🚪') && t.includes('🗣'));
D.exitShop();
if (t) return { rumor: t.slice(t.indexOf('🗣')), tried: seen.length + 1 };
seen.push(s.id);
}
return { rumor: null, tried: seen.length }; }""")
if rr.get('rumor') and artist and artist in rr['rumor']:
OK(f"rumor: a keeper muttered after {rr['tried']} shop(s) — {rr['rumor'][:80]}")
else:
FAIL(f"rumor: no keeper named {artist!r} in {rr.get('tried')} open shops — {rr}")
# (c) buy the wanted sleeve → the want clears itself
r1 = _enter_record_shop(pg)
if r1.get('err'): FAIL(f"wantlist/buy: {r1['err']}"); return
_at_bin(pg)
rb = _dig_buy(pg)
rc = pg.evaluate("() => ({ wants: window.PROCITY.game.wants.length, n: window.PROCITY.game.collection.length })")
if not rb.get('err') and not rb.get('disabled') and rc['wants'] == 0 and rc['n'] >= 1:
OK('wantlist: bought the hunted sleeve — the want cleared itself, the find is in the crate')
else:
FAIL(f"wantlist/buy: {rb} · after {rc}")
base = pg.evaluate("() => ({ day: window.PROCITY.game.day, cash: window.PROCITY.game.cash, n: window.PROCITY.game.collection.length })")
# (d) travel: another town on the SAME save — the ride costs exactly one day
boot(pg, 'plansrc=osm&town=melbourne')
t1 = pg.evaluate("() => ({ day: window.PROCITY.game.day, cash: window.PROCITY.game.cash, n: window.PROCITY.game.collection.length, trav: window.PROCITY.game.traveled })")
if t1['day'] == base['day'] + 1 and t1['trav'] and t1['cash'] == base['cash'] and t1['n'] == base['n']:
OK(f"travel: synthetic → melbourne cost exactly one day (day {base['day']}{t1['day']}), "
f"cash ${t1['cash']} and {t1['n']} find(s) rode along")
else:
FAIL(f"travel: {base}{t1} (want day+1, traveled, cash/collection intact)")
# (e) same-town control: re-boot melbourne — no day moves, traveled stays false
boot(pg, 'plansrc=osm&town=melbourne')
t2 = pg.evaluate("() => ({ day: window.PROCITY.game.day, trav: window.PROCITY.game.traveled })")
if t2['day'] == t1['day'] and not t2['trav']:
OK(f"travel/control: re-booting the SAME town is free (day {t2['day']}, traveled false)")
else:
FAIL(f"travel/control: same-town reboot moved the day — {t2}")
# (f) the return trip costs a day again
boot(pg, '')
t3 = pg.evaluate("() => ({ day: window.PROCITY.game.day, trav: window.PROCITY.game.traveled })")
if t3['day'] == t1['day'] + 1 and t3['trav']:
OK(f"travel: the ride home cost a day too (day {t3['day']})")
else:
FAIL(f"travel: return trip wrong — {t3}")
# (g) ?game=0: zero want DOM on the card, zero rumor in any banner
boot(pg, 'game=0')
r2 = _enter_record_shop(pg)
if r2.get('err'): FAIL(f"wantlist/g0: {r2['err']}")
else:
_at_bin(pg)
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(350)
pg.evaluate("""() => { const c = window.PROCITY.renderer.domElement;
c.dispatchEvent(new PointerEvent('pointerdown', { clientX: 640, clientY: 360, bubbles: true }));
c.dispatchEvent(new PointerEvent('pointerup', { clientX: 640, clientY: 360, bubbles: true })); }""")
pg.wait_for_timeout(300)
g0 = pg.evaluate("""() => ({
want: !!document.querySelector('.pcdg-panel .want'),
panel: !!document.querySelector('.pcdg-panel'),
rumor: [...document.querySelectorAll('div')].some(e => e.textContent && e.textContent.includes('🗣')) })""")
if g0['panel'] and not g0['want'] and not g0['rumor']:
OK('wantlist/control: ?game=0 card has ZERO want DOM and no keeper mutters (construction-gated)')
else:
FAIL(f"wantlist/control: game=0 leaked want/rumor DOM — {g0}")
if errs: FAIL(f"wantlist: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('wantlist: 0 console errors')
finally:
b.close()
def smoke_classic_purity(p): def smoke_classic_purity(p):
"""R30 gate (c) — ?classic=1 and ?game=0: ZERO Storage-prototype calls (instrumented before any """R30 gate (c) — ?classic=1 and ?game=0: ZERO Storage-prototype calls (instrumented before any
page script), zero game DOM (B's ids), game null. Default boot is the POSITIVE control (the page script), zero game DOM (B's ids), game null. Default boot is the POSITIVE control (the
@ -2290,6 +2407,7 @@ def main():
smoke_classic_purity(p) # R30 gate c — instrumented Storage zero + zero game DOM + positive control smoke_classic_purity(p) # R30 gate c — instrumented Storage zero + zero game DOM + positive control
smoke_rotation(p) # R30 gate d — vertex-data rotation determinism (the wave-3 binding) smoke_rotation(p) # R30 gate d — vertex-data rotation determinism (the wave-3 binding)
smoke_bible(p) # R32 — THE BIBLE (bands · gems · ledger) + per-day scarcity + ?game=0 control smoke_bible(p) # R32 — THE BIBLE (bands · gems · ledger) + per-day scarcity + ?game=0 control
smoke_wantlist_travel(p) # R33 — wantlist · keeper rumor · travel-costs-a-day (+ controls)
smoke_tier2(p) # R27 — the kill-the-server gate (charter law #1) smoke_tier2(p) # R27 — the kill-the-server gate (charter law #1)
smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path) smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path)
finally: finally: