Skip to content

Commit ebac795

Browse files
Pins are yours. Share is the sheet.
Thunder is not auto-pinned. Web Share plus X, Reddit, TikTok, Snapchat, Discord.
1 parent 9a0c47d commit ebac795

25 files changed

Lines changed: 504 additions & 183 deletions

HOW-TO-PLAY.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Live booth: [first-bucket-studios.vercel.app](https://first-bucket-studios.verce
1010
1. **Rip the pack.** A franchise, an era, and a luck lane land. New every time.
1111
2. **Tear the foil.** Ten cards flip. Tap five.
1212
3. **Lock five.** Eighty-two nights walk from those names. The number on the card is that walk.
13-
4. **Send the card.** Save the PNG. Post to X. Copy the walk. The URL is the certificate.
13+
4. **Send the card.** Save the PNG. Share it. Pin it if you want it on the rail. The URL is the walk.
1414

1515
![Home](docs/stills/01-home.jpg)
1616

@@ -30,9 +30,11 @@ One pull on tape: [docs/tape/demo.mp4](docs/tape/demo.mp4)
3030
| **Rip pack** / tap the foil | Tears it. The ten turn. |
3131
| **Lock five** | Walks 82 nights. |
3232
| **Save the card** | Downloads `first-bucket-{team}-{wins}.png`. |
33-
| **Post to X** | Opens X with the walk. Attach the PNG if you want the plate in the post. |
34-
| **Threads / Facebook** | In **More ways to send**. Same walk URL. |
35-
| **More ways to send** | Opens the tray: story frame, native share, copy. |
33+
| **Share** | Phone: the system share sheet (Messages, TikTok, Snapchat, Discord). Desktop: the tray. |
34+
| **Pin this walk** | Keeps it on the rail. Unpin anytime. Nothing is pinned until you pin it. |
35+
| **X / Threads / Reddit / Facebook** | Open those apps with the walk. |
36+
| **TikTok / Snapchat** | Saves a 9:16 plate. Open the app and post it. |
37+
| **Discord** | Copies the walk for paste. |
3638
| **Copy the walk** | Copies the public walk URL so you can paste it. |
3739
| **Open the walk** | Same five, same nights, forever. |
3840

@@ -42,13 +44,13 @@ Save needs a top-level tab. Inside a preview iframe the host may swallow the dow
4244

4345
## House job (optional)
4446

45-
Thunder · Positionless · Even is pinned so one walk never moves:
47+
Thunder 51–31 is a staged demo (`?pack=house`). It is not pinned on anyone’s rail. Same five every time, for people who want one walk that never moves:
4648

4749
```
4850
/walk/v1.OKC.positionless.even.51.chet~dort~hartenstein~jalenw~sga
4951
```
5052

51-
51–31. Playoff lock. Same five every time. It hangs on The Wall. The door is the live pack.
53+
The door is still the live pack.
5254

5355
## What you are sending
5456

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Ninety seconds. [HOW-TO-PLAY.md](HOW-TO-PLAY.md) is the sheet.
3030

3131
![The card](docs/stills/03-result.jpg)
3232

33-
**4. Send the card.** Save the PNG. Post to X. Copy the walk. The URL is the certificate.
33+
**4. Send the card.** Save the PNG. Share it. Pin it if you want it on the rail.
3434

3535
![Send](docs/stills/04-tray.jpg)
3636

@@ -41,8 +41,9 @@ Tape: [docs/tape/demo.mp4](docs/tape/demo.mp4)
4141
| **Rip the pack** | New room. New ten. |
4242
| **Lock five** | 82 nights from those names. |
4343
| **Save the card** | `first-bucket-{team}-{wins}.png` |
44-
| **Post to X** | Opens X with the walk. Attach the PNG for the plate. |
45-
| **More ways to send** | Tray: story frame, Threads, Facebook, native share. |
44+
| **Share** | System share sheet, or the tray on desktop. |
45+
| **Pin this walk** | Keeps it on the rail. Nothing is pinned until you pin it. |
46+
| **X / TikTok / Snapchat / Reddit / Discord** | In the share tray. |
4647
| **Copy the walk** | Public `/walk/v1.…` — paste it anywhere. |
4748
| **Open the walk** | Same five, same nights, forever. |
4849

SUPPORT.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ Save is a native file link (`<a download>` with a PNG data URL). A nested previe
2222

2323
## Share on social did nothing
2424

25-
**Post to X** is a real X intent. It posts the walk URL. The PNG is a separate file — Save it, then attach it on X if you want the plate in the post. Instagram has no web share target; pick **9:16 studio** in More ways to send, Save, then upload.
25+
**Share** is the system sheet on a phone (Messages, TikTok, Snapchat, Discord). On a desktop it opens the tray: X, Threads, Reddit, Facebook, TikTok, Snapchat, Discord.
26+
27+
TikTok and Snapchat have no web post button. Those buttons save a 9:16 plate and copy the caption — then you post in the app.
28+
29+
## Thunder is pinned and I never pinned it
30+
31+
That was a bug. The house demo is no longer auto-pinned. **Pin this walk** on a card you actually sent. The X on the rail unpins it.
2632

2733
## Every pack is Thunder 51
2834

docs/TESTING.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ GitHub Action **Pin** runs layers 1 and 3 on every push to `main`. Layer 4 needs
2424
| Live 82-0 pack | Ready | Random franchise. Tear. Ten plates. Not Thunder every time. |
2525
| Player cards | Ready | One 5×7 face. Night plate. Pick five. Court back only when face-down. |
2626
| House pin | Ready | Thunder 51–31, same walk every time. Optional. |
27-
| Send PNG | Ready in a top-level tab | **Save the card** is a real `<a download>`. **Post to X** is `x.com/intent/tweet`. |
28-
| Social | Ready | X on the card. Threads + Facebook + native share in the tray. |
27+
| Send PNG | Ready in a top-level tab | **Save the card** is a real `<a download>`. **Share** is Web Share, then the tray. |
28+
| Social | Ready | X, Threads, Reddit, Facebook, TikTok, Snapchat, Discord. |
29+
| Pins | Ready | Empty until the user pins. Thunder is not auto-pinned. |
2930
| Pin tests | Must stay green | `npm run test:pin` then `npm run test:vl` |
3031
| Public URL | Ready | [first-bucket-studios.vercel.app](https://first-bucket-studios.vercel.app). GitHub stays private. Domain later. |
3132

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"typecheck": "tsc --noEmit",
2020
"check:auth": "node scripts/check-auth-invariant.mjs",
2121
"test": "node --test 'scripts/**/*.test.mjs' && node --experimental-strip-types --test src/lib/app-data/app-data.test.ts src/lib/auth/gate-identity.test.ts",
22-
"test:pin": "node --experimental-strip-types --test src/lib/house-pack.test.ts src/lib/nba.test.ts src/lib/walk.test.ts src/lib/demo-funnel.test.ts src/lib/stream-json.test.ts src/lib/foil-gl.test.ts src/lib/deliver.test.ts",
22+
"test:pin": "node --experimental-strip-types --test src/lib/house-pack.test.ts src/lib/nba.test.ts src/lib/walk.test.ts src/lib/demo-funnel.test.ts src/lib/stream-json.test.ts src/lib/foil-gl.test.ts src/lib/deliver.test.ts src/lib/studio-save.test.ts",
2323
"test:demo": "node scripts/demo-eval.mjs",
2424
"test:vl": "node scripts/vl-bench.mjs",
2525
"capture": "node scripts/capture-demo.mjs",

scripts/capture-demo.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,15 @@ for (let i = 0; i < Math.min(5, n); i++) {
6363
}
6464

6565
await page.getByRole("button", { name: "Lock five" }).click();
66+
const skip = page.getByRole("button", { name: /Skip to the card|The card/ });
67+
if (await skip.first().isVisible().catch(() => false)) await skip.first().click();
6668
await page.waitForSelector("text=Save the card");
67-
await page.waitForSelector("text=Post to X");
69+
await page.waitForSelector("text=Share");
6870
await page.waitForTimeout(400);
6971
await shot("03-result.jpg");
7072

71-
await page.getByRole("button", { name: "More ways to send" }).click();
72-
await page.waitForSelector("text=Save the PNG");
73+
await page.getByRole("button", { name: "Share" }).click();
74+
await page.waitForSelector("text=TikTok");
7375
await page.waitForTimeout(400);
7476
await shot("04-tray.jpg");
7577

scripts/demo-eval.mjs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,11 @@ try {
9191
if ((await card.getAttribute("aria-pressed")) !== "true") await card.click();
9292
}
9393
await page.getByRole("button", { name: "Lock five" }).click();
94+
const skip = page.getByRole("button", { name: /Skip to the card|The card/ });
95+
if (await skip.first().isVisible().catch(() => false)) await skip.first().click();
9496
await page.getByRole("link", { name: "Save the card" }).waitFor();
95-
await page.getByRole("link", { name: "Post to X" }).waitFor();
97+
await page.getByRole("button", { name: "Share" }).waitFor();
98+
await page.getByRole("button", { name: "Pin this walk" }).waitFor();
9699
await flushBeats();
97100
const walk = (await page.locator("text=/\\/walk\\/v1\\./").first().textContent()) ?? "";
98101
const id = walk.replace(/^[\s\S]*\/walk\//, "").trim();
@@ -103,12 +106,14 @@ try {
103106
const href = await save.getAttribute("href");
104107
if (!href?.startsWith("data:image/png")) throw new Error(`save is not a png data url: ${href?.slice(0, 40)}`);
105108
if (!(await save.getAttribute("download"))?.endsWith(".png")) throw new Error("save has no png download name");
106-
const post = page.getByRole("link", { name: "Post to X" });
107-
const tweet = await post.getAttribute("href");
108-
if (!tweet?.includes("x.com/intent/tweet")) throw new Error(`post to x is not an intent: ${tweet}`);
109-
await page.getByRole("button", { name: "More ways to send" }).click();
109+
await page.getByRole("button", { name: "Share" }).click();
110110
await page.getByRole("link", { name: "Save the card" }).nth(1).waitFor();
111-
await page.getByRole("button", { name: "Copy the walk" }).click();
111+
await page.getByRole("link", { name: "X" }).waitFor();
112+
await page.getByRole("link", { name: "Reddit" }).waitFor();
113+
await page.getByRole("button", { name: "TikTok" }).waitFor();
114+
await page.getByRole("button", { name: "Snapchat" }).waitFor();
115+
await page.getByRole("button", { name: "Discord" }).waitFor();
116+
await page.getByRole("button", { name: "Copy walk" }).click();
112117
await page.waitForTimeout(250);
113118
const demo = await flushBeats();
114119
const funnel = demo?.funnel;

scripts/red-team.mjs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/** Red team: 10 live packs, no house pin, real save + share. */
2+
import { chromium } from "playwright";
3+
4+
const BASE = process.env.APP_URL || "http://127.0.0.1:8080";
5+
const HOUSE = "v1.OKC.positionless.even.51.chet~dort~hartenstein~jalenw~sga";
6+
7+
const browser = await chromium.launch({ args: ["--no-sandbox", "--disable-gpu"] });
8+
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
9+
page.setDefaultTimeout(40000);
10+
11+
const teams = [];
12+
const walks = [];
13+
14+
try {
15+
await page.goto(BASE + "/about", { waitUntil: "networkidle" });
16+
const body = await page.locator("main").innerText();
17+
if (/Thunder foil\. Same pack every time/i.test(body)) throw new Error("about still teaches the house pack");
18+
if (/certifcate/i.test(body)) throw new Error("typo: certifcate");
19+
const pinned = await page.getByText("Pinned walk").count();
20+
if (pinned) throw new Error("Thunder house widget is still on the rail");
21+
22+
await page.goto(BASE + "/games/82-0", { waitUntil: "networkidle" });
23+
24+
for (let i = 0; i < 10; i += 1) {
25+
if (i > 0) {
26+
const again = page.getByRole("button", { name: /Rip again|Pull again/ });
27+
if (await again.count()) await again.first().click();
28+
else await page.goto(BASE + "/games/82-0", { waitUntil: "networkidle" });
29+
}
30+
const pack = page.locator(".rip-pack");
31+
await pack.waitFor({ state: "visible" });
32+
await page.waitForFunction(() => {
33+
const el = document.querySelector(".rip-pack");
34+
const label = el?.getAttribute("aria-label") || "";
35+
return Boolean(label) && !/house prospect/i.test(label);
36+
});
37+
const label = (await pack.getAttribute("aria-label")) || "";
38+
const team = /Rip the (.+) prospect pack/.exec(label)?.[1] || "";
39+
if (!team) throw new Error(`pack ${i + 1} has no franchise`);
40+
teams.push(team);
41+
await pack.click();
42+
const cards = page.locator(".pack-grid button");
43+
await cards.first().waitFor({ state: "visible" });
44+
const n = await cards.count();
45+
if (n !== 10) throw new Error(`${team} dealt ${n} cards`);
46+
const stacked = await page.locator(".card-face.is-down").count();
47+
if (stacked > 0) throw new Error(`${team} stacked faces`);
48+
for (let c = 0; c < 5; c += 1) await cards.nth(c).click();
49+
await page.getByRole("button", { name: "Lock five" }).click();
50+
const skip = page.getByRole("button", { name: /Skip to the card|The card/ });
51+
if (await skip.first().isVisible().catch(() => false)) await skip.first().click();
52+
await page.getByRole("link", { name: "Save the card" }).waitFor();
53+
const href = await page.getByRole("link", { name: "Save the card" }).first().getAttribute("href");
54+
if (!href?.startsWith("data:image/png")) throw new Error(`${team} save is not a png`);
55+
await page.getByRole("button", { name: "Share" }).click();
56+
await page.getByRole("link", { name: "X" }).waitFor();
57+
await page.getByRole("button", { name: "TikTok" }).waitFor();
58+
await page.getByRole("button", { name: "Discord" }).waitFor();
59+
const tweet = await page.getByRole("link", { name: "X" }).getAttribute("href");
60+
if (!tweet?.includes("x.com/intent/tweet")) throw new Error(`${team} X is not an intent`);
61+
const walk = (await page.locator("text=/\\/walk\\/v1\\./").first().textContent()) ?? "";
62+
const id = walk.replace(/^[\s\S]*\/walk\//, "").trim();
63+
if (!id.startsWith("v1.")) throw new Error(`${team} has no walk`);
64+
if (id === HOUSE) throw new Error(`${team} collapsed to the house pin`);
65+
walks.push(id);
66+
await page.keyboard.press("Escape");
67+
console.log(JSON.stringify({ i: i + 1, team, walk: id }));
68+
}
69+
70+
const unique = new Set(teams);
71+
if (unique.size < 4) throw new Error(`only ${unique.size} franchises in 10 pulls: ${[...unique].join(", ")}`);
72+
if (teams.every((t) => t === "Thunder")) throw new Error("all 10 packs were Thunder");
73+
if (new Set(walks).size !== walks.length) throw new Error("duplicate walks — mixer stuck");
74+
console.log(JSON.stringify({ ok: true, teams, unique: unique.size, walks: walks.length }));
75+
} catch (err) {
76+
console.error("RED TEAM FAIL", err);
77+
await page.screenshot({ path: "/tmp/fbs-red-fail.png", fullPage: true }).catch(() => {});
78+
process.exitCode = 1;
79+
} finally {
80+
await browser.close();
81+
}

src/components/eighty-two.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { recapOf, type Recap } from "@/lib/recap";
4141
import { deltaVsBest } from "@/lib/ledger";
4242
import { seasonTelemetry } from "@/lib/telemetry";
4343
import { seasonWalk, wnbaWalk, type Night } from "@/lib/sim";
44-
import { bestFrom, justFiled, loadSave, needsExportNag, recordRun, todayKey, yesterdayKey } from "@/lib/studio-save";
44+
import { bestFrom, loadSave, needsExportNag, recordRun, todayKey, yesterdayKey } from "@/lib/studio-save";
4545
import { ripTick } from "@/lib/tick";
4646
import { dailyRoom, encodeChallengeIds, encodeWalk, encodeWnbaWalk, houseWalk, playersOf, publicWalkUrl } from "@/lib/walk";
4747
import { cn } from "@/lib/utils";
@@ -558,21 +558,13 @@ export function EightyTwo({ mode, challenge }: { mode: Mode; challenge?: Challen
558558
/>
559559
</div>
560560
)}
561-
{daily && already && <p className="mt-3 text-sm text-subtle">Replay logged. Streak already counted today.</p>}
561+
{daily && already && <p className="mt-3 text-sm text-subtle">Already counted today.</p>}
562562
{daily && !already && streak > 0 && <p className="mt-3 text-sm text-fg">Streak {streak}.</p>}
563-
{mode !== "82-0" && !cue && justFiled() && <p className="mt-3 text-sm text-fg">Studio file saved. The house is the file.</p>}
564-
{daily && justFiled() && <p className="mt-2 text-sm text-muted">The streak is in the file.</p>}
565563
{daily && (
566564
<div className="mt-4">
567565
<StreakStrip today={stamp} lastDaily={save.lastDaily} streak={streak || save.streak} />
568566
</div>
569567
)}
570-
{mode !== "82-0" && !cue && (
571-
<p className="mt-3 text-sm text-muted">This room, this device, best {bestFrom(save.runs, mode) || wins}.</p>
572-
)}
573-
{mode !== "82-0" && !cue && ((daily && streak >= 2 && !save.exportedAt) || needsExportNag(save)) && (
574-
<p className="mt-3 text-sm text-fg">The streak dies with this browser. Export the file.</p>
575-
)}
576568
{walkId ? (
577569
<p className="mt-4 break-all font-mono text-micro text-subtle">/walk/{walkId}</p>
578570
) : null}

0 commit comments

Comments
 (0)