PROCITY/docs/SALE_RED_TICKET_DAY.md

20 KiB
Raw Blame History

FABLE'S NOTE (2026-07-26). John's pitch; designed by a 3-framing panel (time-attack / market-day / daily-challenge), each adversarially judged, then synthesised. I verified the load-bearing finding myself before circulating this: kind:'bin' is minted at exactly fittings.js:60 and :75 (recordBin, crate), and those appear in exactly two recipes — record (theme.js:38-39) and market stall (theme.js:196). Confirmed: only record shops and market stalls have a diggable crate. The op shop's recipe (theme.js) is clothes racks, metal shelves, a bookshelf, a trestle table and crateStack — which its own comment labels "back-room dressing", i.e. scenery, not a bin.

That is a finding about the MAIN GAME, not just this mode, and it is filed to the v8 debt ledger: PROCITY is built on thriftgod's op-shop DNA and you cannot dig in an op shop. Whether that is a gap to close (give the op shop a bric-a-brac bin) or a fact to design around (records are the deep dig; op shops are pull-and-buy) is a John-level taste call, and RED TICKET DAY does not depend on the answer.

Superseded in part by John's later reframe — "the city being procedural and nebulous… you have to even know where to find the store" — which is running as its own design pass. Read that charter alongside this one: this document owns the ROUND and the LAWS (the Band Floor Law is the best idea in it); the reframe owns the HUNT.

RED TICKET DAY

synthesis of three designs and three adversarial passes — decision-grade, for John


1. WHATAYARECKON

Yes. Build it. Not because the game needs another system — it needs fewer — but because this is the first thing anyone has proposed that spends the clock.

Read our own frontier doc back: "hours exist! nothing makes you race them" and "the clock is scenery." That is not a missing feature, it is a missing cost. Every verb in PROCITY is currently free: riffle a crate, walk a block, dig all day, nothing is consumed except cash. The shops shut and it costs you nothing because there is nothing you were going to do anyway. The sale fixes that from the other end — it makes today the only day the $110 grail is $66, and once today has a thing in it that tomorrow doesn't, the hours that already exist start biting without a single new clock.

And it answers "what is a session?" — which has been open since v4. A session is one town, one trading day, one docket: wake, read the paper, walk, choose which crates you can afford to open, get shut out of the last shop, sleep. Ten to twenty minutes with a beginning, a middle and an end.

Two things it is not. It is not a stopwatch — all three judges landed on that from different directions and the code agrees (details in §8). And it is not free money: the round is a net cash loss by construction, and that is the point. You are not trading, you are buying the day you finally got the thing.


2. WHAT IT IS

RED TICKET DAY. One day in seven, seeded per town, the shops that have crates in them mark stock down. The paper tells you which ones. The markdown never crosses the guide band's floor, so a red ticket is never a gem and a gem is never red — the skill you already have gets harder, not easier. You cannot buy them all. The shops shut. What you carried out is the score, and it lives in the crate forever with "day 14, Bendigo" under it.

Structure and scoring come from TIMEATTACK (bounded round, forced-flag gates, DOM-only, the Band Floor Law). Diegesis and rhythm come from MARKETDAY (a day, not a stopwatch; the morning paper; the market's own FIRST IN BEST DRESSED fiction, already painted on the wall at theme.js:201). The time model comes from DAILY's one great idea — time passes on the street, not in the shopcorrected: seconds don't cost you indoors, but crates do.


3. THE ROUND

Wake. SLEEP rolls the day (save.js, index.html:281lighting.setSegment(0)). If today is the town's red-ticket day, the wake toast carries the paper:

🌅 day 14 · RED TICKET DAY — Vinyl Solution ⅓ off · The Turnstyle ¼ off · Sadie's half price

The sale module then sets the segment to the first roster shop's opening, so a sale day always starts with a door about to open.

The roster: what the town actually has. Only two shop types carry crates (measured, §4). The roster is the biggest cluster of ≤3 crate-shops all within 400 m of each other, seeded. Across the 26 shipped town keys at seed 20261990:

roster towns tour sprint @ 8.8 m/s
3 shops 5 (fitzroy, adelaide, brunswick, katoomba_real, melbourne) 74369 m 842 s
2 shops 7 6390 m 144 s
1 shop 11
0 — no sale day 3 (bowral, darwin, daylesford)

Synthetic towns (plansrc default, ~480 shops, 103151 crate-shops): roster of 3, tour 827 m. So the mode is a route problem in the default town and in five real ones, and a depth problem in the other seventeen. Both are rounds. Neither is a lie.

Inside. A record shop holds 38 crates × 16 sleeves = 48128 records, of which (modelled off the shipped band table at bible.js:26-30) 513 carry a red ticket. A ticket is 25 / 35 / 50% off asking, seeded per shop-day, floored at the band's low edge. Mean marked price ≈ $21; mean saving ≈ $10; a grail asking $90 comes down to $60 and no further.

The purse. Day-1 wallet is 60 + (r()*140|0)$130 (wallet.js:10). Six red tickets and you're out. There are 1230 on a three-shop roster. You take a fifth to a half of what's marked. That is the whole game of the round: not "grab everything", but "which of these do I want enough."

The clock — the one real change. Time does not pass while you riffle. It passes when you walk (the street clock, exactly as shipped) and when you open a crate: one crate ≈ ⅓ of a segment. A trading day gives you roughly nine crate-openings. Fitzroy's 369 m tour eats three of them. So three shops = two crates each, out of the 38 each shop has. You will never see the bottom of a town, and the shutter comes down mid-crate.

The end. The last roster shop shuts (isOpen, already enforced at index.html:493). The docket totals what you carried out:

🔴 the day's take — 5 red tickets, $84 saved, $103 spent · you left 9 marked in the crates you opened · you never made it to Sadie's

Then SLEEP. Six ordinary days until the next one, or travel (which costs a day anyway).


4. THE THREE FACTS THAT DECIDED THE DESIGN

Measured this session against the tree, not asserted:

(a) Only two shop types have crates. userData.kind:'bin' is minted at exactly fittings.js:60 (recordBin) and :75 (crate), and those appear in exactly two recipes: record (theme.js:38-39) and market stall (theme.js:196). Op shops, pawnbrokers, book barns, toy shops have no diggable bin at all. Two of the three submitted designs set their flagship scene in an op shop. That scene cannot happen.

(b) The real towns are big and thin. Generated all 26 town keys: crate-shops per town run 0 to 12, and they can be 2.3 km apart (hobart, newtown). Three towns have none. The "three shops, three depths, pick a route" premise is a Fitzroy feature. Hence the 400 m clique rule and the honest "the round is however many shops this town has."

(c) The day is six values wide. HOUR_NUM = [6.5, 9, 12.5, 15.5, 18.5, 22] (lighting.js:22); record hours are 1018 (registry.js:40), stalls 814. So records are open at 12:30 and 15:30 only; stalls at 9:00 and 12:30 only — two segments each, 80 seconds of street clock. MIDDAY is the only segment where the whole diggable town trades. That is not a bug to design around, it is the trading day, already shipped: stalls in the morning, records after lunch, everything at midday. Use it.


5. MECHANISM, LANE BY LANE

Lane C — dig.js, ~8 lines, one new optional opt.

  • dig.js:421 — after offers = …, before buildStack(offers) at :424: if (opts.sale) offers = offers.map(opts.sale); A post-draw transform. proceduralSleeves (:36-56) and pickRealOffers (:379-402) are complete; zero stream drift, zero re-order, no golden moves. Never in-place on pack.itemsstockpack.js:30-31 caches by (type, base) and an in-place write leaks town-wide and permanent. {...o} copy, always.
  • dig.js:110-115 — red fillStyle on the price when o.was is present. Rides the existing per-open CanvasTexture. +0 draws.
  • dig.js:266-275 — a 🔴 SALE · was $50 chip in the stamps row (present-fields-only, so absent ⇒ byte-identical card). The guide $2559 line stays exactly where it is: that is the discount theatre. ~~$22~~ $17 inside guide $824 and ~~$50~~ $31 inside guide $2559 are the same red ticket and opposite verdicts.
  • dig.js:480-487 — widen currentOffers() with was and sale. Without this the gates cannot see their subject (vacuous-gate law).
  • bible.js, sell.js: no change. Not one line.

Lane F — one new module + three small edits.

  • web/js/world/sale.js (~180 lines): the seeded calendar, the roster, the ticket transform, the crate accumulator, the docket DOM. Constructed only under flagOn('sale') && game; never constructed under ?classic=1, ?sale=0 or ?game=0, so absence is by non-existence, not by branch (the washing.js / magpie.js precedent).
  • index.html: one construction line, one PROCITY.flags entry, one ?sale=all force. No per-frame update — the module is event-driven off procity:segment and procity:digClose. Zero frame cost.
  • save.js: recordFind carries was and sale:1 on the entry (exactly the band precedent at :294); validEntry (:52-60) validates both when present; listFind (:366) refuses a sale:1 entry.
  • hud.js: the docket strip gets its own body-mounted root, not a child of #pc-gamehud.js:283-285 sets #pc-game to display:none for the entire dig, and with no countdown to watch that is now fine, but the strip needs its own construction gate and its own DOM assertions.

Lane B — one optional four-line API: lighting.advance(frac) so the crate cost can move the day by a third of a segment without setSegment() stomping sub-segment position (lighting.js:113 sets clock = i + 0.001). Fallback if B is busy: sale.js keeps its own accumulator and calls setSegment only on a boundary crossing. Nothing else from B, A, D or E. Zero assets, zero fetches, zero draws.


6. THE LAWS

1 — THE BAND FLOOR LAW. price = max(bandLow, floor(ask × (1d))), and no band ⇒ no ticket (fail-closed). One Math.max buys both fences at once: the keeper's counter offer is min(lo1, ⌊lo/2⌋) off bandLow (sell.js:28-32 + bible.js:52-58), so every marked buy costs ≥ lo and sells for < lothe no-pump gate's all(d <= -1) holds with zero changes to sell.js. And nothing under a band floor is ever a sale, so under-band still means "nobody noticed" — the gem survives intact.

2 — THE PROVENANCE LAW. A sale:1 entry can never be consigned. "Can't put that through GodBay, mate — half the district was at the same sale." One guard at save.js:366, one hidden on the crate row.

This replaces the "drop the band" fence that two of the three designs proposed, and it is not a preference — that fence is broken. With no band, godbayHammer sets lo = hi = pricePaid (save.js:36-37) — but the 12% bidding war multiplies afterwards by 1.52.0 (:43), so ~12% of sold listings profit and the design's own gate arm would go red. It also prints your own purchase price back at you in the morning paper, and blanks the guide line on the sell card (sell.js:104). A refusal is one line, hard-assertable, and diegetic.

3 — THE GEM COVENANT. The gem is never ticketed. On a sale day the eye is trained on red for ten minutes and the money is still in the plain yellow sticker at $6 in a guide $2559 band. Free, and it makes the core skill harder on the day the game is loudest.

4 — DETERMINISM & THE DELTA LAW. Four new kind labels — 'saleday', 'saleroster', 'saleticket', 'salethin' — independent streams by construction, so nothing existing shifts. Tickets are keyed to item identity, never array position (${shopId}:${day}:${binKey}:${sku ?? i}): dig.js:422 filters procedural offers by gone, so a position-keyed ticket would move to a different record the moment you bought one. Save side: zero new top-level fields. The score is a pure function of the collection (Σ was pricePaid over today's sale:1 entries); only was and sale are added, both optional, both on the entry, both validated exact-and-loud. Old saves load untouched.


7. THE GATES (Lane F)

  1. Non-vacuity. ?sale=all marks every eligible shop at rate 1.0, so smoke_nopump's shop picker (flags_check.py:2537-2559) is guaranteed on the roster. Re-run the 5 real UI round trips; assert all(d <= -1); FAIL — not skip — if fewer than 3 of 5 bought offers carried was.
  2. Band floor. Over ≥5 bins: every offer with was has price ≥ bandRange(band)[0] and price < was.
  3. Provenance. Buy marked → listFind === false. Positive control: buy unmarked in the same shop → listFind === true.
  4. Negative control. ?sale=0 / ?classic=1 / ?game=0 → zero [id^=pc-sale] DOM, zero was anywhere, and the offer tuples byte-identical to a pre-sale boot. Mint that offer-price golden this round — it does not exist today (flags_check.py pins plan fingerprints only).
  5. Identity-keyed tickets. Buy one marked slot, re-open the bin, assert the surviving marked set is unchanged (the R27 zero-collateral law, applied to the ticket).
  6. Calendar determinism. Same (seed, town, day) ⇒ identical roster and identical marked set across two boots.
  7. (full version) PROCITY.sale.debug.crates(n) — every existing tool pins midday (dbg.js:325 + 20 call sites), so the crate clock needs an explicit hook or its gate is vacuous by construction.

8. CUT LIST

cut why verified at
The 120-second stopwatch dt is clamped to 0.05 s — a 10 fps machine gets a 2× longer round and best is framerate-denominated; procity:segment can't tell a real transition from a bracket press; DBG pauses the clock in every gate we own; and the strip would be invisible for the whole dig index.html:575, lighting.js:109-113, dbg.js:325, hud.js:283-285
Op shops, pawn, book, toy from the pool no bins, at all fittings.js:60,75 vs theme.js:56-63,134-141
"Drop the band" as the anti-farm fence 12% of sold listings profit via the war multiplier; prints a legible inversion in the paper; blanks the sell card's guide line save.js:36-43, sell.js:104
The Wordle bell, share string, ?run=today needs createGame({startDay}), which doesn't exist (let day = 1, single writer adopt()); the board is a query string, so the leaderboard is decorative save.js:110-113,167
🎟 N marked bin hover forces exporting proceduralSleeves or mirroring it (~1,100 PRNG draws per room enter), and points the player away from the gems dig.js:36
The sandwich-board InstancedMesh +1 draw is affordable; the frontEdge/ry orientation math, texture, chunk refresh and gate arm are not, when the docket already names the shops reserve for v9
The 23-town calendar strip would advertise sale days in the 3 towns with nothing to dig measured §4(b)
Dollar-threshold ranks ("$200 = LEGEND OF THE TIP"), best measure wealth, not play — start cash is 60 + (r()*140|0) and grows unbounded wallet.js:10
goesAt ∈ {2,3,4,5} thinning 5075% of that distribution falls in segments the shop is never open in — it resolves to a constant. Replaced by one bit keyed to the shop's own hours: arrive in a shop's last open segment and half the red tickets are gone registry.js:40,128 × lighting.js:22

9. EFFORT, AND THE MINIMUM SHIPPABLE THING

MINIMUM — one round. This is playable this week. The sale exists and nothing is scored: ?sale flag, seeded calendar, roster, band-floor tickets, red sticker + SALE chip, was/sale:1 on the entry, the listFind refusal, the paper line on the wake toast. Gate arms 16. Cost: one Lane C afternoon (dig.js ~8 lines), one Lane F round (sale.js ~120 lines, three save.js edits, two index.html lines), and the offer-price golden. No lane conflicts — nobody is currently in dig.js:421 or save.js:366, and v8 wave 1 is entirely elsewhere.

FULL — two more rounds.

  • the crate clock and the Lane B advance() API, + thinning at room-enter, + the docket strip and the end-of-day total, + the keeper's rumour instead of a calendar UI, + gate arm 7 and the DBG hook. The second of those two rounds is mostly QA, because the crate clock touches what time means and every existing tool pins midday.

Ship the minimum first as a field test: it tells you whether a red ticket is fun before you rebuild the day around it.


10. WHERE IT SITS AGAINST v8

The minimum version is a genuine v8 sidebar. One flag, zero draws, zero assets, zero fetches, DOM-only, construction-gated, no lane collisions with ambient audio, per-town character, the magpie or backyard props. It is content wearing a very thin system, which is exactly what v8 is.

The crate clock is not a v8 sidebar — it is the head of v9. Making a crate cost time is the first time in this game's history that dwelling costs anything, and it is the direct, literal answer to "the clock is scenery." That deserves an epoch, its own gates and its own retro, not a wave slot.

One flagged follow-up, not this round: give the op shop a record crate. It is one line in theme.js:56-63 and it would take the diggable pool from 012 per real town to something like 325 — which fixes the roster histogram, fixes the 3 dead towns, and delivers the actual op-shop fantasy the pitch is written in. It re-rolls every op shop interior, so it is a Lane C spike with a golden re-mint behind it. Worth costing properly in v9.


11. WHAT NEEDS JOHN

  1. Stopwatch: in or out? The recommendation is out — the pressure is the shutter, the purse, and the crate cost. You said "limited times". Is a trading day limited enough for you, or do you want a number counting down?
  2. Does opening a crate cost time? This is the one real design change in the document, and the one that makes hours bite. It rations breadth, never the riffle itself — you can take as long as you like inside a crate; you just can't open the next one for free. Yes or no.
  3. Are red-ticket finds locked out of GodBay forever? It is the clean fence and it is diegetic, but it is a real restriction on your auction toy.
  4. Is the band floor sacred? i.e. a red ticket can never be a gem. This buys the no-pump law and the gem signal with one clamp. Breaking it means red tickets and grails start looking alike.
  5. Does the sale run in one-shop towns? Eleven of twenty-six get a roster of one. Recommendation: yes — a one-shop round is a depth round. Three towns get no sale day at all, honestly.
  6. Is the crate the only trophy, or do you want a score that persists? Recommendation: crate only, no personal best, no leaderboard. The number is for the day, the record is forever.
  7. The name. RED TICKET DAY is the working title. Given three designs independently reached for "red ticket", it is probably right — but it is your word to pick.