diff --git a/F-progress.md b/F-progress.md index cd79106..855142b 100644 --- a/F-progress.md +++ b/F-progress.md @@ -4,6 +4,19 @@ --- +## Round 35 (v7.0 close, wave 2) — THE FIRST FIVE MINUTES (Fable, solo, JING5) + +A fresh game arrives HUNTING: one seeded artist want (`rng(citySeed,'firstwant',0)` × the +parody roster) named on the splash with the pocket money ("You're hunting HILLS HOIST — $191 +in your pocket"), pointing the rumors and the tour at the same target. Three toast-beats walk +the hustle off existing events (walk-in → digOpen → first find → first sale-or-listing, then +the listener removes itself). Returning saves get no tutorial; ?game=0/classic never reach it. +#pc-toast body-mounted so beats and morning papers surface inside shops too. STRICT +`smoke_first_five` green (roster want + splash · real-path beats · returning/?game=0 controls +· cross-browser seed determinism). Full record: LANE_F_NOTES §35. + +--- + ## Round 34 (v7.0 close, wave 1) — GODBAY: the offline auction (Fable, solo, JING5) ⚖ LIST from the crate (mail-in consignment) → `listings` on procity-save/1 → resolves the day diff --git a/docs/LANES/LANE_F_NOTES.md b/docs/LANES/LANE_F_NOTES.md index c12f46a..91e1fd7 100644 --- a/docs/LANES/LANE_F_NOTES.md +++ b/docs/LANES/LANE_F_NOTES.md @@ -1870,3 +1870,21 @@ m3ultra. If a third machine joins the rotation, sync that dir (49 M) or re-run E "⚖ SOLD Cane Toad Blues — hammer $22, $19 after fees" · the ride to melbourne hammered a listing on arrival · ?game=0 has no crate to list from · 0 console errors. - G's later LIVE tier must match these laws (the ladder rule: the offline tier is the contract). + +## §35 — R35: THE FIRST FIVE MINUTES (Fable, solo, JING5) + +Fresh-game detection = day 1 ∧ empty collection ∧ empty listings (no flag in the save — the +delta law stays clean; a returning empty-crate player re-seeing beats is onboarding working, +not a bug). Seeded first want: `rng(citySeed,'firstwant',0)` × dig.js ARTISTS (exported R35); +guarded on empty wants so the persisted want isn't re-added. Splash gains #pc-hunt (fresh+game +only). Beats: ① on the go-click (900ms), ② on first `procity:digOpen`, ③ on first find via +wallet.onChange, send-off on first sale-or-listing (listener self-removes). #pc-toast body- +mounted (z 31) — visible in interiors/map; hud.dispose removes it. Gate `smoke_first_five` +STRICT (see ROUND35_INSTRUCTIONS). All v7.0 SYSTEMS are now built — tour + freeze + tag remain, +tag gated on John's playtest. +- **Cross-round amendment the suite caught (the reason full runs exist):** R35's seeded first + want changed the FRESH-BOOT CONTRACT (wants starts at 1, not 0) and the R33 gate's three + fresh-boot assumptions went red on the full run — green solo, red together. Amended to a + baseline snapshot (base = wants at boot; card adds base+1; the rumor may name ANY live want; + buying clears ITS want back to base). A feature that moves a boot invariant must sweep every + gate that assumed it — grep the harness for the invariant before calling a round closed. diff --git a/docs/LANES/ROUND35_INSTRUCTIONS.md b/docs/LANES/ROUND35_INSTRUCTIONS.md new file mode 100644 index 0000000..f91507c --- /dev/null +++ b/docs/LANES/ROUND35_INSTRUCTIONS.md @@ -0,0 +1,36 @@ +# PROCITY — Round 35 instructions (from Fable, integrator) + +Date: 2026-07-20 · **R34 closed all green** (GODBAY hammers). Round 35 is **THE FIRST FIVE +MINUTES** ([V7_THE_GAME.md](../V7_THE_GAME.md) system #7 — "smallest system, biggest +stranger-facing payoff"), the last unbuilt v7.0 system before the tour/freeze/tag. + +## What ships + +1. **You arrive with a want.** A FRESH game (day 1, empty crate, empty auction house) seeds ONE + artist want off `rng(citySeed,'firstwant',0)` × C's parody roster (ARTISTS now exported) — + so the splash, the keeper rumors, and the tour all point at the same hunt on a given seed. + The splash names it: "You're hunting HILLS HOIST — $191 in your pocket." A returning save + gets its town back, not a tutorial; ?game=0 / classic never reach the block. +2. **Three toast-beats walk the hustle** (fresh games only): ① find a record shop / E at a + crate (on walk-in) → ② riffle · mind the guide · BUY (first digOpen) → ③ sell at a counter + or ⚖ LIST and sleep (first find banked) → the send-off ("that's the whole hustle") on the + first sale-or-listing. Driven off existing events + wallet.onChange — zero new systems. +3. **Toasts surface in every mode** — #pc-toast moved to document.body (the #pc-game body-mount + rule): beats fire INSIDE shops, and a SLEEP taken at a counter now shows its morning paper. + +## Gate (F): `smoke_first_five` (STRICT) + +Fresh game: 1 seeded want from the roster + the splash names hunt AND cash · beats ②/③/send-off +through the REAL paths (E at crate / dig buy / crate ⚖) · a returning save gets NO tutorial +(splash line hidden, no ② on dig) · a second fresh browser hunts the SAME artist (seeded, not +random) · ?game=0 shows nothing · 0 console errors. + +## ROUND CLOSED (Fable) — ALL GREEN + +`qa.sh --strict --matrix`: 7 passed · 0 failed · 0 warn · 0 skipped, MATRIX 10 towns green +(final tree). Gate run: hunts HILLS HOIST with $191, beats walked through the real dig/buy/list +paths, returning-save and ?game=0 controls clean, second fresh browser hunts the same artist, +0 console errors. Money shot: `docs/shots/v7_beta/first_five_splash.png`. + +**v7.0 remaining:** the tour ("day one to first gem") · docs freeze · the tag — and the tag +waits for John's playtest (the alpha precedent: hands before tags). diff --git a/docs/shots/v7_beta/first_five_splash.png b/docs/shots/v7_beta/first_five_splash.png new file mode 100644 index 0000000..d477039 Binary files /dev/null and b/docs/shots/v7_beta/first_five_splash.png differ diff --git a/tools/flags_check.py b/tools/flags_check.py index 3b51f94..884203e 100644 --- a/tools/flags_check.py +++ b/tools/flags_check.py @@ -2158,6 +2158,9 @@ def smoke_wantlist_travel(p): 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) + # [R35 amendment] a FRESH game already hunts ONE seeded want (the first-five system) — this + # gate's fresh-boot baseline is that want, and the card adds a SECOND on top of it. + base = pg.evaluate("() => window.PROCITY.game.wants.length") rw = pg.evaluate("""() => { const w = document.querySelector('.pcdg-panel .want'); if (!w) return { err: 'no WANT line on the card' }; @@ -2165,11 +2168,14 @@ def smoke_wantlist_travel(p): 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]}") + if 'WANT IT' in rw['before'] and 'ON THE WANTLIST' in rw['after'] and len(rw['wants']) == base + 1 and rw['live'] == base + 1: + OK(f"wantlist: ☆→★ through the real card — the save carries {len(rw['wants'])} want(s) " + f"(the seeded hunt + {rw['wants'][-1]})") else: - FAIL(f"wantlist: want did not take — {rw}") - artist = (rw['wants'][0].get('artist') or rw['wants'][0].get('title') or '').upper() + FAIL(f"wantlist: want did not take — base {base}, {rw}") + # the rumor may pick ANY current want (seeded index over the live list) + artists = [str(w.get('artist') or w.get('title') or '').upper() for w in rw['wants']] + artist = artists[-1] 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(); }""") @@ -2190,20 +2196,21 @@ def smoke_wantlist_travel(p): seen.push(s.id); } return { rumor: null, tried: seen.length }; }""") - if rr.get('rumor') and artist and artist in rr['rumor']: + if rr.get('rumor') and any(a and a in rr['rumor'] for a in artists): 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}") + FAIL(f"rumor: no keeper named any of {artists} 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') + if not rb.get('err') and not rb.get('disabled') and rc['wants'] == base and rc['n'] >= 1: + OK('wantlist: bought the hunted sleeve — ITS want cleared itself (the seeded hunt stays), ' + 'the find is in the crate') else: - FAIL(f"wantlist/buy: {rb} · after {rc}") + FAIL(f"wantlist/buy: {rb} · after {rc} (want base {base})") 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') @@ -2357,6 +2364,89 @@ def smoke_godbay(p): b.close() +def smoke_first_five(p): + """R35 gate — THE FIRST FIVE MINUTES (charter #7). Legs: (a) a FRESH game arrives with a seeded + first want (parody roster) and the splash names the hunt + the pocket money; (b) the want is + the SAME artist in a second fresh browser (seeded, not random); (c) the beats walk the hustle: + E at a crate → '②', a buy → '③', a ⚖ LIST → the send-off; (d) a RETURNING save gets no + tutorial (splash line hidden, no '②' on dig); (e) ?game=0 shows no hunt line at all.""" + head('GATE R35: THE FIRST FIVE MINUTES — seeded want · splash · beats (+ returning/?game=0 controls)') + ROSTER = {'THE VERANDAHS', 'DEZ & THE UTES', 'MERINO', 'HILLS HOIST', 'THE ESKIES', 'GOON BAG', + 'STOBIE POLES', 'THE FIBROS', 'WARATAH', 'THONGS', 'THE CLOTHESLINE', 'GALAH'} + b, pg, errs = new_page(p) + try: + boot(pg, '') + fa = pg.evaluate("""() => { const g = window.PROCITY.game, h = document.getElementById('pc-hunt'); + return { wants: g.wants.map(w => w.artist), cash: g.cash, + hunt: h ? { shown: h.style.display !== 'none', text: h.textContent } : null }; }""") + if (len(fa['wants']) == 1 and fa['wants'][0] in ROSTER and fa['hunt'] and fa['hunt']['shown'] + and fa['wants'][0] in fa['hunt']['text'] and f"${fa['cash']}" in fa['hunt']['text']): + OK(f"first-five: fresh game hunts {fa['wants'][0]} with ${fa['cash']} — the splash says so") + else: + FAIL(f"first-five: seeded want/splash wrong — {fa}") + # (c) the beats, through the real paths + r0 = _enter_record_shop(pg) + if r0.get('err'): FAIL(f"first-five: {r0['err']}"); return + pg.evaluate("""() => { const g = window.PROCITY.game; + const ex = JSON.parse(g.export()); ex.cash = 5000; ex.savedAt = Date.now(); g.import(JSON.stringify(ex)); }""") + pg.evaluate("() => window.DBG.setSegment(2)") + _at_bin(pg) + pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))") + pg.wait_for_timeout(350) + t2 = pg.evaluate("() => (document.getElementById('pc-toast') || {}).textContent || ''") + pg.evaluate("() => { const o = document.querySelector('.pcdg-out'); if (o) o.click(); }") + pg.wait_for_timeout(200) + _at_bin(pg) + rb = _dig_buy(pg) + t3 = pg.evaluate("() => (document.getElementById('pc-toast') || {}).textContent || ''") + rl = pg.evaluate("""() => { + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' })); + const lb = document.querySelector('#pc-crate .pc-crate-row div[title*="GodBay"]'); + if (lb) lb.click(); + window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' })); + return (document.getElementById('pc-toast') || {}).textContent || ''; }""") + if '②' in t2 and '③' in t3 and 'whole hustle' in rl: + OK('first-five: the beats walked the hustle — ② at the crate, ③ on the buy, the send-off on the ⚖') + else: + FAIL(f"first-five: beats wrong — ②:{('②' in t2)} ③:{('③' in t3)} send-off:{('whole hustle' in rl)}") + # (d) a returning save gets no tutorial + boot(pg, '') + rd = pg.evaluate("""() => { const h = document.getElementById('pc-hunt'); + return { shown: h && h.style.display !== 'none', n: window.PROCITY.game.collection.length, + nl: window.PROCITY.game.listings.length }; }""") + r1 = _enter_record_shop(pg) + if r1.get('err'): FAIL(f"first-five/return: {r1['err']}") + else: + _at_bin(pg) + pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))") + pg.wait_for_timeout(350) + tr = pg.evaluate("""() => { const t = document.getElementById('pc-toast'); + return { text: t ? t.textContent : '', vis: t ? t.style.opacity : '0' }; }""") + if not rd['shown'] and not ('②' in tr['text'] and tr['vis'] == '1'): + OK(f"first-five/return: a save with {rd['n']} find(s)/{rd['nl']} listing(s) gets no tutorial") + else: + FAIL(f"first-five/return: tutorial leaked to a returning save — {rd} {tr}") + # (b) determinism: a second FRESH browser hunts the same artist + b2, pg2, _ = new_page(p) + try: + boot(pg2, '') + w2 = pg2.evaluate("() => window.PROCITY.game.wants.map(w => w.artist)") + if w2 == fa['wants']: OK(f"first-five: a second fresh browser hunts the same {w2[0]} — seeded, not random") + else: FAIL(f"first-five: fresh want differs across browsers — {fa['wants']} vs {w2}") + finally: + b2.close() + # (e) ?game=0 — no hunt line + boot(pg, 'game=0') + g0 = pg.evaluate("""() => { const h = document.getElementById('pc-hunt'); + return h ? h.style.display !== 'none' : false; }""") + if not g0: OK('first-five/control: ?game=0 — no hunt, no tutorial') + else: FAIL('first-five/control: game=0 shows the hunt line') + if errs: FAIL(f"first-five: {len(errs)} console error(s); first: {errs[0][:140]}") + else: OK('first-five: 0 console errors') + finally: + b.close() + + def smoke_classic_purity(p): """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 @@ -2513,6 +2603,7 @@ def main(): 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_godbay(p) # R34 — GODBAY offline auction (one hammer · bounded · morning paper) + smoke_first_five(p) # R35 — the first five minutes (seeded want · splash · beats + controls) 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) finally: