Compare commits
3 Commits
776b10b599
...
c9e233a7e5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9e233a7e5 | ||
|
|
e528c92fed | ||
|
|
b048f51640 |
@ -4,6 +4,21 @@
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
after, wherever the day moves (sleep, travel, imports — adopt() and sleep() share the
|
||||
resolver). THE ONE-HAMMER LAW: identity-seeded (never day-seeded) — a passed-in item re-lists
|
||||
to the same result forever, no retry farm. 15% pass · uniform-across-band hammer (grail 2×low)
|
||||
· 12% war ×1.5–2 · 12% fees, net floor $1; EV ≈ 0.8×mid so random flips bleed and only finds
|
||||
profit; keeper = floor, auction = bounded ceiling. The morning paper rides the wake/arrival
|
||||
toast ("⚖ SOLD 'Cane Toad Blues' — hammer $22, $19 after fees"). Band table imported from C's
|
||||
bible.js (one authority), PRNG from core. STRICT `smoke_godbay` green (real-DOM consign ·
|
||||
byte-identical double-sleep · bounded honest resolution · travel hammers · game=0 control).
|
||||
Full record: LANE_F_NOTES §34.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
@ -26,6 +26,7 @@ Click **START** to lock the mouse. Then:
|
||||
| **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 $8–24"). 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 |
|
||||
| **⚖ LIST** in the crate | consign a find to **GodBay** — the auction hammers overnight (sleep or ride somewhere). One hammer per item, ever: keeper cash now, or the gavel tomorrow |
|
||||
| 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) |
|
||||
| **M** | 2D town map (street only) |
|
||||
|
||||
@ -1845,3 +1845,28 @@ m3ultra. If a third machine joins the rotation, sync that dir (49 M) or re-run E
|
||||
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.
|
||||
|
||||
## §34 — R34: GODBAY, the offline auction (v7.0 close opens) (Fable, solo, JING5)
|
||||
|
||||
- **save.js:** `godbayHammer(entry, townKey)` — THE ONE-HAMMER LAW: seeded by ITEM IDENTITY
|
||||
(`godbay:<townKey>:<sku|slotId>:<dayFound>:<pricePaid>`), never by resolution day — a
|
||||
passed-in item re-lists to the same result forever, no retry farm. Fixed 4-draw stream. Rolls:
|
||||
15% passed in (comes home free) · uniform hammer across the band (grail top = 2×low; unbanded
|
||||
alpha finds auction around pricePaid — honest, never invented) · 12% bidding war ×1.5–2.0 ·
|
||||
12% commission, net floor $1. EV ≈ 0.8×mid — under in-band asking EV, so random flipping
|
||||
bleeds and only FINDS profit; the keeper's half-of-low stays the instant floor, the auction
|
||||
is the gambler's ceiling (bounded 2×high×0.88). PRNG imported from core/prng.js; the band
|
||||
table imported from C's bible.js (one authority — no mirror after all).
|
||||
- `listings` optional on procity-save/1 (entry shape + listDay, same back-compat law).
|
||||
`listFind` (identity, entry leaves the crate — can't also sell it) · `resolveAuctions()` runs
|
||||
in adopt() AND sleep(): sleep, travel, and day-moving imports all hammer what they pass.
|
||||
`takeAuctionNews()` drains the transient morning paper (NOT saved — the ledger is cash/
|
||||
collection; the paper is a toast).
|
||||
- **hud.js:** ⚖ LIST on crate find rows (mail-in consignment) + AT AUCTION section; rebuild
|
||||
watches listings. **index.html:** the wake toast and the arrival toast fold the paper in.
|
||||
- **Gate `smoke_godbay` (STRICT):** consign through the real crate DOM → save carries it ·
|
||||
ONE-HAMMER determinism (a frozen pre-sleep export slept twice lands byte-identical) · bounded
|
||||
honest resolution (SOLD net $19 on a standard record, ≤ 2×24×0.88) · the wake toast carried
|
||||
"⚖ 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).
|
||||
|
||||
57
docs/LANES/ROUND34_INSTRUCTIONS.md
Normal file
57
docs/LANES/ROUND34_INSTRUCTIONS.md
Normal file
@ -0,0 +1,57 @@
|
||||
# PROCITY — Round 34 instructions (from Fable, integrator)
|
||||
|
||||
Date: 2026-07-20 · **R33 closed all green** — every v7.0-beta system is built. Round 34 opens
|
||||
**the v7.0 close: GODBAY, the offline auction** ([V7_THE_GAME.md](../V7_THE_GAME.md) system #4:
|
||||
"list an item, a SEEDED offline auction resolves NEXT DAY — the come-back-tomorrow hook").
|
||||
Offline tier FIRST per the proven ladder (G3's live rails are v7.x; this round is the seeded
|
||||
brain in the game layer, G's later live tier must match these laws). Solo round (John's "keep
|
||||
rolling").
|
||||
|
||||
## The laws (GODBAY v0 — named now, gated below)
|
||||
|
||||
1. **Mail-in consignment.** You list from THE CRATE (⚖ LIST on a find) — the 90s answer
|
||||
(classifieds + the post). The entry leaves the collection into `listings` (you cannot also
|
||||
sell it at a counter), `listDay` stamped. Resolution happens when the day moves past
|
||||
`listDay` — sleep, travel, or a day-moving import all count (adopt() and sleep() both run
|
||||
the resolver).
|
||||
2. **THE ONE-HAMMER LAW (no retry farming).** The hammer is seeded by ITEM IDENTITY —
|
||||
`godbay:<townKey>:<sku|slotId>:<dayFound>:<pricePaid>` — NOT by the resolution day. A
|
||||
passed-in item re-lists to the SAME result, forever. There is no re-roll anywhere in the
|
||||
economy: the keeper's offer is a formula, the auction is one hammer per item, per life.
|
||||
3. **The house always eats.** Hammer rolls uniform across the band (grail's open top = 2×low);
|
||||
~12% chance of a bidding war (×1.5–2.0); ~15% passed in (returns to the crate, free);
|
||||
12% commission, net floor $1. Expected value ≈ 0.8× band midpoint — UNDER the asking-price
|
||||
EV of in-band stock, so random flipping bleeds and only FINDS profit (charter law #2's
|
||||
spirit at the auction house: growth bounded by finds, the keeper's instant half-of-low
|
||||
remains the coward's floor and the auction the gambler's ceiling — bounded at 2×high×0.88).
|
||||
4. **Delta law / classic purity unchanged.** `listings` is an optional procity-save/1 field
|
||||
(same back-compat law as pulls/wants); no game layer ⇒ no listings, no DOM, no news.
|
||||
5. **The morning report.** Resolution queues transient news ("⚖ SOLD 'Sunburnt' — hammer $38,
|
||||
$33 after fees"); the shell folds it into the wake/arrival toast. News is NOT saved — it's
|
||||
the morning paper, not the ledger (cash/collection already carry the truth).
|
||||
|
||||
## Gate (F): `smoke_godbay` (STRICT)
|
||||
|
||||
List through the real crate DOM (C → ⚖ LIST) → entry moves collection→listings, the save
|
||||
carries it · sleep → resolved: listings empty, cash+net OR item returned, news queued, net
|
||||
within [1, 2.2×bandHigh] when sold · DETERMINISM: a pre-sleep export imported twice sleeps to
|
||||
byte-identical cash/collection both times (the seeded hammer, measured not trusted) · travel
|
||||
resolves too (list → ride to melbourne → resolved on arrival) · `?game=0` builds zero ⚖ DOM ·
|
||||
0 console errors.
|
||||
|
||||
## ROUND CLOSED (Fable) — ALL GREEN
|
||||
|
||||
`qa.sh --strict --matrix` on the final tree: **7 passed · 0 failed · 0 warn · 0 skipped**,
|
||||
MATRIX 10 towns green. `smoke_godbay`: consigned through the real crate DOM (collection 2→1,
|
||||
listDay stamped) · ONE-HAMMER determinism (a frozen pre-sleep export slept twice → byte-
|
||||
identical) · honest bounded resolution (SOLD net $19 on a standard record, ≤ 2×$24×0.88) ·
|
||||
the wake toast carried "⚖ 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. Design note vs the brief: the band table is IMPORTED from C's bible.js and the PRNG
|
||||
from core/prng.js (one authority each — the drafted mirror died in review). Money shots:
|
||||
`docs/shots/v7_beta/godbay_morning_paper.png` (dawn day 2, the paper in the toast, the crate's
|
||||
⚖ row) + `godbay_at_auction.png`. Full record: LANE_F_NOTES §34.
|
||||
|
||||
**What remains of v7.0:** THE FIRST FIVE MINUTES (onboarding — arrive with a want, three
|
||||
toast-beats) · the tour ("day one to first gem") · docs freeze · the tag. Then John's playtest
|
||||
charters what's next.
|
||||
BIN
docs/shots/v7_beta/godbay_at_auction.png
Normal file
BIN
docs/shots/v7_beta/godbay_at_auction.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 982 KiB |
BIN
docs/shots/v7_beta/godbay_morning_paper.png
Normal file
BIN
docs/shots/v7_beta/godbay_morning_paper.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 705 KiB |
@ -2253,6 +2253,110 @@ def smoke_wantlist_travel(p):
|
||||
b.close()
|
||||
|
||||
|
||||
def smoke_godbay(p):
|
||||
"""R34 gate — GODBAY, the offline seeded auction. Legs: (a) ⚖ LIST through the real crate DOM →
|
||||
the entry leaves the collection into save-carried listings; (b) sleep → resolved: listings empty,
|
||||
cash+net (bounded [1, 2×high×0.88 + slack]) XOR the item came home, the wake toast carries the
|
||||
morning paper; (c) THE ONE-HAMMER DETERMINISM: a pre-sleep export imported and slept TWICE lands
|
||||
byte-identical cash/collection both times; (d) travel resolves too (list → ride → hammered on
|
||||
arrival); (e) ?game=0 builds no crate at all (nothing to list from)."""
|
||||
head('GATE R34: GODBAY — the offline auction (one hammer · bounded · morning paper · controls)')
|
||||
BANDHI = {'record': {'bargain': 7, 'standard': 24, 'collector': 59, 'grail': 120},
|
||||
'book': {'bargain': 4, 'standard': 14, 'collector': 39, 'grail': 80},
|
||||
'toy': {'bargain': 5, 'standard': 19, 'collector': 49, 'grail': 100}}
|
||||
b, pg, errs = new_page(p)
|
||||
try:
|
||||
boot(pg, '')
|
||||
r0 = _enter_record_shop(pg)
|
||||
if r0.get('err'): FAIL(f"godbay: {r0['err']}"); return
|
||||
rich = pg.evaluate("""() => { const g = window.PROCITY.game;
|
||||
const ex = JSON.parse(g.export()); ex.cash = 5000; ex.savedAt = Date.now();
|
||||
return g.import(JSON.stringify(ex)); }""")
|
||||
if not rich: FAIL('godbay: bankroll import failed'); return
|
||||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||||
for i in range(2): # two finds: one to list now, one for the travel leg
|
||||
_at_bin(pg)
|
||||
rb = _dig_buy(pg)
|
||||
if rb.get('err') or rb.get('disabled'): FAIL(f"godbay: buy {i + 1} failed — {rb}"); return
|
||||
# (a) ⚖ LIST through the real crate DOM
|
||||
rl = pg.evaluate("""() => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||||
const g = window.PROCITY.game, n0 = g.collection.length;
|
||||
const lb = document.querySelector('#pc-crate .pc-crate-row div[title*="GodBay"]');
|
||||
if (!lb) return { err: 'no ⚖ LIST control in the crate' };
|
||||
lb.click();
|
||||
const ex = JSON.parse(g.export());
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||||
return { n0, n1: g.collection.length, listings: (ex.listings || []).length,
|
||||
entry: (ex.listings || [])[0] || null }; }""")
|
||||
if rl.get('err'): FAIL(f"godbay: {rl['err']}"); return
|
||||
if rl['n1'] == rl['n0'] - 1 and rl['listings'] == 1 and rl['entry'] and rl['entry'].get('listDay'):
|
||||
OK(f"godbay: consigned through the crate — collection {rl['n0']}→{rl['n1']}, "
|
||||
f"the save carries the listing (listDay {rl['entry']['listDay']})")
|
||||
else:
|
||||
FAIL(f"godbay: listing did not take — {rl}"); return
|
||||
# (c) THE ONE-HAMMER DETERMINISM: freeze pre-sleep, sleep twice from the same frozen state
|
||||
pre = pg.evaluate("() => window.PROCITY.game.export()")
|
||||
outcomes = []
|
||||
for _ in range(2):
|
||||
pg.evaluate("(ex) => window.PROCITY.game.import(ex)", pre)
|
||||
pg.evaluate("() => { document.getElementById('pc-sleep-btn').click(); }")
|
||||
pg.wait_for_timeout(250)
|
||||
toast = pg.evaluate("() => (document.getElementById('pc-toast') || {}).textContent || ''")
|
||||
st = pg.evaluate("""() => { const g = window.PROCITY.game;
|
||||
const ex = JSON.parse(g.export()); delete ex.savedAt;
|
||||
return { cash: g.cash, n: g.collection.length, nl: g.listings.length, ex: JSON.stringify(ex) }; }""")
|
||||
st['toast'] = toast
|
||||
outcomes.append(st)
|
||||
a, b2 = outcomes
|
||||
if a['ex'] == b2['ex'] and a['cash'] == b2['cash']:
|
||||
OK('godbay: ONE HAMMER — the same frozen state slept twice lands byte-identical both times')
|
||||
else:
|
||||
FAIL(f"godbay: hammer not deterministic — {a['cash']} vs {b2['cash']}")
|
||||
# (b) resolution honesty + bounds + the morning paper
|
||||
pre_cash = json.loads(pre)['cash']; pre_n = len(json.loads(pre)['collection'])
|
||||
ent = rl['entry']; hi = BANDHI.get(ent.get('type'), BANDHI['record']).get(ent.get('band'), 120)
|
||||
sold = a['cash'] > pre_cash; came_home = a['n'] == pre_n + 1
|
||||
net = a['cash'] - pre_cash
|
||||
if a['nl'] == 0 and (sold != came_home) and (not sold or 1 <= net <= int(2.0 * hi * 0.88) + 2):
|
||||
OK(f"godbay: resolved — {'SOLD net $' + str(net) + f' (≤ 2×${hi}×0.88 bound)' if sold else 'passed in, came home'}"
|
||||
f" · listings drained")
|
||||
else:
|
||||
FAIL(f"godbay: resolution dishonest — nl={a['nl']} sold={sold} home={came_home} net={net} hi={hi}")
|
||||
if '⚖' in a['toast']:
|
||||
OK(f"godbay: the wake toast carried the morning paper — {a['toast'][:90]}")
|
||||
else:
|
||||
FAIL(f"godbay: no ⚖ in the wake toast — {a['toast'][:90]!r}")
|
||||
# (d) travel resolves too: list the second find, ride to melbourne
|
||||
r2 = pg.evaluate("""() => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||||
const g = window.PROCITY.game;
|
||||
const lb = document.querySelector('#pc-crate .pc-crate-row div[title*="GodBay"]');
|
||||
if (!lb) return { err: 'no ⚖ for the travel leg (collection empty?)' };
|
||||
lb.click();
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||||
return { cash: g.cash, nl: g.listings.length, n: g.collection.length }; }""")
|
||||
if r2.get('err'): FAIL(f"godbay/travel: {r2['err']}")
|
||||
else:
|
||||
boot(pg, 'plansrc=osm&town=melbourne')
|
||||
t = pg.evaluate("""() => { const g = window.PROCITY.game;
|
||||
return { nl: g.listings.length, cash: g.cash, n: g.collection.length, trav: g.traveled }; }""")
|
||||
if t['trav'] and t['nl'] == 0 and (t['cash'] > r2['cash'] or t['n'] == r2['n'] + 1):
|
||||
OK(f"godbay/travel: the ride to melbourne hammered the listing (cash ${r2['cash']}→${t['cash']}, "
|
||||
f"collection {r2['n']}→{t['n']})")
|
||||
else:
|
||||
FAIL(f"godbay/travel: {r2} → {t}")
|
||||
# (e) ?game=0 — no crate, nothing to list from
|
||||
boot(pg, 'game=0')
|
||||
g0 = pg.evaluate("() => ({ crate: !!document.getElementById('pc-crate'), game: !!window.PROCITY.game })")
|
||||
if not g0['crate'] and not g0['game']: OK('godbay/control: ?game=0 — no game, no crate, no auction house')
|
||||
else: FAIL(f"godbay/control: game=0 leaked — {g0}")
|
||||
if errs: FAIL(f"godbay: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||||
else: OK('godbay: 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
|
||||
@ -2408,6 +2512,7 @@ def main():
|
||||
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_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_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:
|
||||
|
||||
@ -264,7 +264,9 @@ const game = GAME_ON ? createGame({
|
||||
onDay: (day) => {
|
||||
if (gigState) gigState.setWeekNight(weekNightOf(day)); // (day − 1) % 7 — see the seam note below
|
||||
lighting.setSegment(0);
|
||||
hud.showToast(`🌅 day ${day}`);
|
||||
// [R34] the morning paper: GODBAY resolved before this hook fired — fold the results in
|
||||
const news = game ? game.takeAuctionNews() : [];
|
||||
hud.showToast(`🌅 day ${day}` + (news.length ? ' · ' + news.join(' · ') : ''), news.length ? 6 : 2.6);
|
||||
},
|
||||
}) : null;
|
||||
const wallet = game ? game.wallet : baseWallet;
|
||||
@ -283,7 +285,13 @@ const weekNightOf = (day) => (Math.max(1, day) - 1) % 7;
|
||||
if (game && gigState) gigState.setWeekNight(weekNightOf(game.day));
|
||||
// [R33 — TRAVEL COSTS A DAY] this boot adopted a save from another town: the ride ate a day (save.js
|
||||
// adopt() did the increment + pull-pruning; weekNight above already read the incremented day). Tell John.
|
||||
if (game && game.traveled) hud.showToast(`🚌 ${plan.name || 'a new town'} — the ride cost a day. Day ${game.day}`, 4);
|
||||
// [R34] the ride also resolved any GODBAY listings the day moved past — the arrival toast carries the news.
|
||||
if (game) {
|
||||
const bootNews = game.takeAuctionNews();
|
||||
if (game.traveled) hud.showToast(`🚌 ${plan.name || 'a new town'} — the ride cost a day. Day ${game.day}`
|
||||
+ (bootNews.length ? ' · ' + bootNews.join(' · ') : ''), bootNews.length ? 7 : 4);
|
||||
else if (bootNews.length) hud.showToast(bootNews.join(' · '), 6);
|
||||
}
|
||||
// [Lane F R9] occupancyOf closure defers to `citizens` (declared just below); only invoked at enter()
|
||||
// time (door click), long after citizens is initialized — so the forward reference is safe.
|
||||
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS,
|
||||
|
||||
@ -200,7 +200,34 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
||||
const meta = document.createElement('div'); meta.className = 'pc-crate-meta';
|
||||
meta.textContent = `paid $${e.pricePaid} · ${foundAt(e, game)} · day ${e.dayFound}`;
|
||||
main.append(t, a, meta);
|
||||
row.append(th, main); panel.appendChild(row);
|
||||
row.append(th, main);
|
||||
// [R34 GODBAY] ⚖ LIST — mail-in consignment; the entry leaves the crate, hammers tomorrow.
|
||||
if (typeof game.listFind === 'function') {
|
||||
const lb = document.createElement('div');
|
||||
lb.textContent = '⚖'; lb.title = 'LIST at GodBay — the auction resolves tomorrow';
|
||||
lb.style.cssText = 'cursor:pointer;opacity:.65;padding:0 4px;flex:none;font-size:15px';
|
||||
lb.onclick = () => { if (game.listFind(e)) rebuildList(); };
|
||||
row.appendChild(lb);
|
||||
}
|
||||
panel.appendChild(row);
|
||||
}
|
||||
// [R34] AT AUCTION — consigned finds, hammering overnight. Non-empty only (no header over nothing).
|
||||
const listings = game.listings || [];
|
||||
if (listings.length) {
|
||||
const lh = document.createElement('h3');
|
||||
lh.style.marginTop = '10px';
|
||||
lh.textContent = `AT AUCTION — ${listings.length} consigned · hammers tomorrow`;
|
||||
panel.appendChild(lh);
|
||||
for (const l of listings) {
|
||||
const row = document.createElement('div'); row.className = 'pc-crate-row';
|
||||
const main = document.createElement('div'); main.className = 'pc-crate-main';
|
||||
const t = document.createElement('div'); t.className = 'pc-crate-title';
|
||||
t.textContent = l.title || l.sku || l.slotId || 'item';
|
||||
const meta = document.createElement('div'); meta.className = 'pc-crate-meta';
|
||||
meta.textContent = `paid $${l.pricePaid} · listed day ${l.listDay}`;
|
||||
main.append(t, meta);
|
||||
row.append(main); panel.appendChild(row);
|
||||
}
|
||||
}
|
||||
// [R33] THE WANTLIST — what you're hunting (☆ on a pull card adds; ✕ here removes; buying one
|
||||
// clears it automatically). Section exists only when non-empty — no stray header over nothing.
|
||||
@ -245,8 +272,8 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
||||
shownCount = col.length; btnCrate.textContent = `CRATE ${shownCount}`;
|
||||
if (open) rebuildList();
|
||||
}
|
||||
const nw = (game.wants || []).length; // [R33] want add/clear refreshes an open panel too
|
||||
if (nw !== shownWants) { shownWants = nw; if (open) rebuildList(); }
|
||||
const nw = (game.wants || []).length + (game.listings || []).length * 100; // [R33/R34] wants OR
|
||||
if (nw !== shownWants) { shownWants = nw; if (open) rebuildList(); } // listings change → rebuild
|
||||
// [R32] the surface yields to the map and to the dig/sell cards (they own the screen), and shows
|
||||
// everywhere else — street AND interior. Cheap: a style write only when the answer changes.
|
||||
const P = window.PROCITY, im = P && P.interiorMode;
|
||||
|
||||
@ -18,6 +18,33 @@
|
||||
export const SAVE_SCHEMA = 'procity-save/1';
|
||||
export const SAVE_KEY = 'procity-save';
|
||||
|
||||
// ── GODBAY v0 — the offline seeded auction (R34, charter system #4; laws in ROUND34_INSTRUCTIONS) ──
|
||||
import { xmur3, mulberry32 } from '../core/prng.js';
|
||||
import { BANDS as GODBAY_BANDS } from '../interiors/bible.js'; // C's pure band table — one authority, no mirror
|
||||
|
||||
const GODBAY_FEE = 0.12; // the house always eats
|
||||
const GODBAY_PASS = 0.15; // passed in — no bids, item comes home
|
||||
const GODBAY_WAR = 0.12; // a bidding war: hammer ×1.5–2.0
|
||||
|
||||
// THE ONE-HAMMER LAW: seeded by ITEM IDENTITY, never by the resolution day — a passed-in item
|
||||
// re-lists to the same result forever, so there is no retry farm anywhere in the economy.
|
||||
// → { sold, hammer, net } | { sold:false }. Fixed draw count (4), outcome-independent.
|
||||
export function godbayHammer(entry, townKey) {
|
||||
const id = `godbay:${townKey}:${entry.sku || entry.slotId}:${entry.dayFound}:${entry.pricePaid}`;
|
||||
const r = mulberry32(xmur3(id)());
|
||||
const passRoll = r(), posRoll = r(), warRoll = r(), warSizeRoll = r();
|
||||
const bands = GODBAY_BANDS[entry.type] || GODBAY_BANDS.record;
|
||||
const range = bands[entry.band] || null;
|
||||
// an unbanded entry (alpha-era find) auctions around what was paid — honest, never invented
|
||||
const lo = range ? range[0] : Math.max(1, entry.pricePaid);
|
||||
const hi = range ? (range[1] == null ? lo * 2 : range[1]) : Math.max(1, entry.pricePaid);
|
||||
if (passRoll < GODBAY_PASS) return { sold: false };
|
||||
let hammer = lo + Math.floor(posRoll * (hi - lo + 1));
|
||||
if (warRoll < GODBAY_WAR) hammer = Math.floor(hammer * (1.5 + warSizeRoll * 0.5));
|
||||
const net = Math.max(1, Math.floor(hammer * (1 - GODBAY_FEE)));
|
||||
return { sold: true, hammer, net };
|
||||
}
|
||||
|
||||
// ── validation — exact and loud (the delta law's falsifiable half) ─────────────────────────────
|
||||
// A corrupted/foreign blob is REJECTED WHOLE: schema string must match exactly, every field must
|
||||
// check. Rejecting loudly + starting fresh is the design — the save can cost you your stuff, never
|
||||
@ -50,6 +77,15 @@ export function validateSave(obj) {
|
||||
for (let i = 0; i < obj.pulls.length; i++)
|
||||
if (typeof obj.pulls[i] !== 'string' || !obj.pulls[i]) return { ok: false, why: `pulls[${i}] malformed` };
|
||||
}
|
||||
// [R34 GODBAY] `listings` is OPTIONAL, same law: collection-shaped entries + an int listDay ≥ 1.
|
||||
if (obj.listings != null) {
|
||||
if (!Array.isArray(obj.listings)) return { ok: false, why: 'listings not an array' };
|
||||
for (let i = 0; i < obj.listings.length; i++) {
|
||||
const l = obj.listings[i];
|
||||
if (!validEntry(l) || !Number.isInteger(l.listDay) || l.listDay < 1)
|
||||
return { ok: false, why: `listings[${i}] malformed` };
|
||||
}
|
||||
}
|
||||
// [R33 wantlist] `wants` is OPTIONAL, same law: array of {artist?|title?|sku?} string fields, at
|
||||
// least one present per entry — a want with nothing to hunt is malformed, not empty-but-fine.
|
||||
if (obj.wants != null) {
|
||||
@ -80,6 +116,9 @@ export function createGame({ townKey, startCash = 0, storage = null, onDay = nul
|
||||
// [R33] the wantlist — what you're hunting ({artist?|title?|sku?}); capped, deduped, save-carried.
|
||||
let wants = [];
|
||||
const WANTS_CAP = 40;
|
||||
// [R34 GODBAY] consigned finds ({...entry, listDay}) + this boot's transient morning report.
|
||||
let listings = [];
|
||||
let auctionNews = []; // NOT saved — the morning paper, not the ledger
|
||||
// [R33 travel] true when THIS boot adopted a save from a DIFFERENT town — the ride cost a day.
|
||||
let traveled = false;
|
||||
// [R32 scarcity] pull records — one string per bought dig slot, keyed by the CONSUMER
|
||||
@ -94,8 +133,30 @@ export function createGame({ townKey, startCash = 0, storage = null, onDay = nul
|
||||
const p = { schema: SAVE_SCHEMA, day, cash, town: townKey, collection, savedAt: Date.now() };
|
||||
if (pulls.length) p.pulls = pulls; // absent when empty → alpha-era byte shape preserved
|
||||
if (wants.length) p.wants = wants; // [R33] same convention
|
||||
if (listings.length) p.listings = listings; // [R34] same convention
|
||||
return p;
|
||||
}
|
||||
|
||||
// [R34 GODBAY] resolve every listing the day has moved past — sleep, travel, and day-moving imports
|
||||
// all funnel here. One hammer per item (identity-seeded), so WHEN it resolves never changes WHAT.
|
||||
function resolveAuctions() {
|
||||
if (!listings.length) return;
|
||||
const still = [];
|
||||
for (const l of listings) {
|
||||
if (l.listDay >= day) { still.push(l); continue; } // resolves the day AFTER listing
|
||||
const title = l.title || l.sku || l.slotId || 'the item';
|
||||
const res = godbayHammer(l, l.townKey || townKey);
|
||||
if (res.sold) {
|
||||
cash += res.net;
|
||||
auctionNews.push(`⚖ SOLD “${title}” — hammer $${res.hammer}, $${res.net} after fees`);
|
||||
} else {
|
||||
const { listDay, ...entry } = l; // passed in — comes home unchanged
|
||||
collection.push(entry);
|
||||
auctionNews.push(`⚖ passed in — “${title}” came home`);
|
||||
}
|
||||
}
|
||||
listings = still;
|
||||
}
|
||||
function save() {
|
||||
if (!store) return false;
|
||||
try { store.setItem(SAVE_KEY, JSON.stringify(payload())); return true; }
|
||||
@ -114,6 +175,8 @@ export function createGame({ townKey, startCash = 0, storage = null, onDay = nul
|
||||
// byte-equality survives untouched. Day moves BEFORE prunePulls so yesterday's-town day-tagged
|
||||
// pulls drop here, and the shell reads game.day/traveled at boot (onDay stays un-fired, §30.1).
|
||||
if (state.town !== townKey) { day += 1; traveled = true; }
|
||||
listings = Array.isArray(state.listings) ? state.listings.slice() : [];
|
||||
resolveAuctions(); // [R34] a day-moving adopt (travel/import) resolves what it passed
|
||||
prunePulls();
|
||||
}
|
||||
|
||||
@ -188,6 +251,7 @@ export function createGame({ townKey, startCash = 0, storage = null, onDay = nul
|
||||
// save (noted vs the brief's literal order in §30.1): disk always carries the morning you woke into.
|
||||
sleep() {
|
||||
day += 1;
|
||||
resolveAuctions(); // [R34] GODBAY hammers overnight — before onDay so the toast carries it
|
||||
prunePulls(); // [R32] yesterday's day-tagged pulls are unreachable — drop them
|
||||
if (onDay) { try { onDay(day); } catch (e) { console.warn('[procity save] onDay hook threw:', e); } }
|
||||
save();
|
||||
@ -293,6 +357,27 @@ export function createGame({ townKey, startCash = 0, storage = null, onDay = nul
|
||||
notify();
|
||||
return true;
|
||||
},
|
||||
|
||||
// ── GODBAY seams (R34) ─────────────────────────────────────────────────────────────────────
|
||||
get listings() { return listings; }, // LIVE, READ-ONLY by contract
|
||||
|
||||
// Consign a collection entry (identity, like removeFind): it leaves the crate for the auction
|
||||
// house and resolves the NEXT day. false ⇒ not in the collection, nothing moves.
|
||||
listFind(entry) {
|
||||
const i = collection.indexOf(entry);
|
||||
if (i < 0) return false;
|
||||
collection.splice(i, 1);
|
||||
listings.push({ ...entry, listDay: day });
|
||||
notify();
|
||||
return true;
|
||||
},
|
||||
|
||||
// Drain the morning paper (transient — resolution wrote it, the shell toasts it once).
|
||||
takeAuctionNews() {
|
||||
const n = auctionNews;
|
||||
auctionNews = [];
|
||||
return n;
|
||||
},
|
||||
};
|
||||
|
||||
// boot: adopt an existing save (fresh start on absence/rejection), then arm the unload save-point.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user