Lane F R34 (v7.0 close #1): GODBAY — the offline seeded auction, one hammer per item, ever
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, so there is no retry farm anywhere in the economy (the keeper's offer is a formula, the auction is one hammer per item per life). Fixed 4-draw stream. Rolls: 15% passed in (comes home free) · hammer uniform across the item's band (grail's open 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× band midpoint — UNDER the asking-price EV of in-band stock: random flipping bleeds, only FINDS profit (charter law #2 at the auction house); the keeper's half-of-low stays the instant floor, the gavel the bounded ceiling (2×high×0.88). Band table imported from C's bible.js and PRNG from core/prng.js — one authority each, no mirrors. listings: optional on procity-save/1 (entry shape + listDay ≥ 1, the pulls/wants back-compat law). listFind (identity — the entry LEAVES the crate, it cannot also sell at a counter) · resolveAuctions() runs in adopt() AND sleep(), so 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).
This commit is contained in:
parent
776b10b599
commit
b048f51640
@ -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