/* ShinyPoker — LIVE lobby. Design look (lobby.css) driven by on-chain tables via window.SP. Cash tab is live; other tabs are flagged coming-soon. */ const { useState: uS, useEffect: uE, useRef: uR } = React; const Ln = (wei) => Number(SP.fmt(wei, 6)); const lshort = (a) => (a && a !== "0x0000000000000000000000000000000000000000" ? a.slice(0, 6) + "…" + a.slice(-4) : ""); const stakeOf = (bb) => (bb <= 0.05 ? "micro" : bb <= 1 ? "low" : bb <= 5 ? "mid" : "high"); function LobbyApp() { const [tab, setTab] = uS("cash"); const [rows, setRows] = uS([]); const [loaded, setLoaded] = uS(false); const [connected, setConnected] = uS(false); const [addr, setAddr] = uS(null); const [bal, setBal] = uS(0); const [stake, setStake] = uS("all"); const [size, setSize] = uS("all"); const [trnCount, setTrnCount] = uS(0); const [showCreate, setShowCreate] = uS(false); const [theme] = uS(() => localStorage.getItem("sp_theme") || "b"); const canvasRef = uR(null); // ambient grid uE(() => { if (!canvasRef.current || !window.GridField) return; const f = new GridField(canvasRef.current, { cell: 20, gap: 6, speed: 0.4, density: 0.5, accent: "#6E6EED", accent2: "#9B9BF4", maxAlpha: 0.7, minBright: 0.01, shape: "square" }); f.start(); const r = setTimeout(() => f._resize(), 300); return () => { clearTimeout(r); f.destroy(); }; }, []); // restore Privy session uE(() => { const restore = () => SP.sdk.tryRestorePrivy().then((a) => { if (a) { setAddr(a); setConnected(true); refreshBal(); } }).catch(() => {}); restore(); const on = (ev) => { if (ev.detail && ev.detail.authenticated && ev.detail.address) restore(); }; document.addEventListener("shinyluck:auth-state", on); return () => document.removeEventListener("shinyluck:auth-state", on); }, []); async function refreshBal() { if (SP.sdk.address) { try { setBal(Ln(await SP.sdk.balanceOf(SP.sdk.address))); } catch {} } } // poll live tables uE(() => { let stop = false; async function load() { try { const n = await SP.sdk.tableCount(); const out = []; for (let t = 0; t < n; t++) { const [cfg, hand, seats] = await Promise.all([SP.sdk.getTable(t), SP.sdk.getHand(t), SP.sdk.getSeats(t)]); out.push({ id: t, sb: Ln(cfg.smallBlind), bb: Ln(cfg.bigBlind), size: cfg.maxSeats, seated: seats.filter((s) => !s.empty).length, pot: Ln(hand.pot), inHand: hand.inProgress, rake: (cfg.rakeBps / 100), cap: Ln(cfg.rakeCap), stake: stakeOf(Ln(cfg.bigBlind)), }); } if (!stop) { setRows(out); setLoaded(true); } if (SP.sdk.hasTournaments()) { try { const ts = await SP.sdk.tournaments(); if (!stop) setTrnCount(ts.filter((t) => t.status <= 1).length); } catch {} } } catch (e) { if (!stop) setLoaded(true); } } load(); const iv = setInterval(load, 4000); return () => { stop = true; clearInterval(iv); }; }, []); async function connect() { try { const a = await SP.sdk.connect(); setAddr(a); setConnected(true); refreshBal(); } catch (e) { if (e && e.message !== "cancelled") alert(e.message || "connect failed"); } } let cash = rows; if (stake !== "all") cash = cash.filter((r) => r.stake === stake); if (size !== "all") cash = cash.filter((r) => String(r.size) === size); const totalSeated = rows.reduce((a, r) => a + r.seated, 0); const running = rows.filter((r) => r.inHand).length; const biggest = rows.reduce((m, r) => Math.max(m, r.pot), 0); const sym = SP.NETWORK.currency.symbol; const COLS = "1.1fr 0.7fr 0.9fr 1.2fr 0.8fr 1.1fr"; return (
shinypoker
RoomLOBBY
{connected ? (
{bal.toFixed(1)}{lshort(addr)}
) : ( )}
{[["cash", "Cash"], ["tournaments", "Tournaments"], ["sng", "Sit & Go"], ["clubs", "Clubs"]].map(([k, l]) => ( ))}
Players seated{totalSeated}
Tables{rows.length}
Hands running{running}
Biggest pot now{biggest.toFixed(2)}{sym}
{tab === "cash" ? ( Stakes
{[["all", "All"], ["micro", "Micro"], ["low", "Low"], ["mid", "Mid"], ["high", "High"]].map(([k, l]) => )}
Size
{[["all", "All"], ["6", "6-max"], ["9", "9-max"], ["2", "Heads-up"]].map(([k, l]) => )}
Provably fair · commit-reveal ) : tab === "tournaments" && SP.sdk.hasTournaments() ? ( Single-table tournaments · buy-in or sponsored pools
) : {tab === "tournaments" ? "Scheduled tournaments" : tab === "sng" ? "Sit & Go / Spin" : "Clubs & private tables"}}
{tab === "tournaments" && SP.sdk.hasTournaments() ? ( setShowCreate(false)} /> ) : tab === "sng" && SP.sdk.hasTournaments() ? ( ) : tab !== "cash" ? (
{tab === "tournaments" ? "Tournaments" : tab === "sng" ? "Sit & Go" : "Clubs"} — coming soon
Cash NLHE is live now. Scheduled tournaments, Sit&Go and clubs land next.
) : !loaded ?
Loading tables…
: cash.length === 0 ?
No tables match your filters.
: (
Stakes · RakeGameSizeSeatsPot nowAction
{cash.map((r) => { const full = r.seated >= r.size; const sizeLabel = r.size === 2 ? "Heads-up" : r.size + "-max"; return (
{r.sb} / {r.bb}{sym}rake {r.rake}% · cap {r.cap} · no flop no drop
NLHE
{sizeLabel}
{Array.from({ length: r.size }).map((_, i) => )}{r.seated}/{r.size}
{r.pot > 0 ? r.pot.toFixed(2) : "—"}{r.pot > 0 ? sym : ""}
Observe {full ? Full : Join}
); })}
)}
); } /* ---------------- Tournaments (live) ---------------- */ const TRN_COLS = "1.5fr 1.2fr 1.2fr 0.9fr 0.9fr 1.2fr"; function TournamentsTab({ connected, connect, addr, onCount, showCreate, closeCreate }) { const [list, setList] = uS(null); const [mine, setMine] = uS({}); // id -> isRegistered const [busy, setBusy] = uS(false); const [msg, setMsg] = uS(null); const sym = SP.NETWORK.currency.symbol; function flash(m) { setMsg(m); setTimeout(() => setMsg(null), 4000); } async function load() { try { const ts = await SP.sdk.tournaments(); ts.reverse(); // newest first setList(ts); onCount(ts.filter((t) => t.status <= 1).length); if (SP.sdk.address) { const m = {}; for (const t of ts) m[t.id] = await SP.sdk.isRegisteredIn(t.id); setMine(m); } } catch (e) { console.warn("trn load:", e.message); setList([]); } } uE(() => { load(); const iv = setInterval(load, 4000); return () => clearInterval(iv); }, [connected]); async function act(label, fn) { setBusy(true); try { await fn(); flash(label + " ✓"); await load(); } catch (e) { flash(label + " ✗ " + (e?.shortMessage || e?.reason || e?.message || "").replace(/execution reverted:?/i, "").slice(0, 70)); console.error(e); } finally { setBusy(false); } } const fmtSplit = (bps) => bps.map((b) => (b / 100) + "%").join(" / "); return ( {msg &&
{msg}
} {showCreate && { closeCreate(); load(); }} act={act} busy={busy} />} {list == null ? (
Loading tournaments…
) : list.length === 0 ? (
No tournaments yet
Be the first — create one with your own buy-in, prize pool and payout split.
) : (
TournamentBuy-in · splitPrize pool EntrantsStatusAction
{list.map((t) => { const cost = t.buyIn + t.fee; const statusCls = ["registering", "running", "finished", "finished"][t.status]; const reg = mine[t.id]; const isCreator = addr && t.creator.toLowerCase() === addr.toLowerCase(); return (
SNG #{t.id} · {t.maxPlayers}-max {Number(t.startStack)} chipsby {t.creator.slice(0, 6)}…{t.pool > t.buyIn * BigInt(t.registered) && ◆ sponsored}
{Number(SP.fmt(cost, 4))}{sym} payout {fmtSplit(t.payoutBps)}
{Number(SP.fmt(t.pool, 4))}{sym} ✓ secured on-chain
{t.registered}/{t.maxPlayers}{t.status === 1 && {t.remaining} left}
{SP.TRN_STATUS[t.status]}
{t.status === 0 && !connected && } {t.status === 0 && connected && !reg && } {t.status === 0 && connected && reg && } {t.status === 0 && connected && isCreator && t.registered >= 2 && } {t.status === 1 && {reg ? "Play →" : "Observe"}} {t.status >= 2 && }
); })}
)}
); } /* ---------------- Sit & Go (live, one-click presets on the tournament engine) ---------------- */ const SNG_PRESETS = [ { name: "Heads-Up Duel", structure: "turbo", players: 2, buyIn: "0.5", fee: "0.05", stack: 1500, sb: 10, bb: 20, levelMin: 4, split: [10000] }, { name: "Hyper 6-Max", structure: "hyper", players: 6, buyIn: "0.5", fee: "0.05", stack: 1000, sb: 25, bb: 50, levelMin: 3, split: [6500, 3500] }, { name: "9-Max Standard", structure: "regular", players: 9, buyIn: "0.3", fee: "0.03", stack: 1500, sb: 10, bb: 20, levelMin: 5, split: [5000, 3000, 2000] }, ]; const SPIN_MULTIS = [{ x: 2 }, { x: 3 }, { x: 5 }, { x: 10 }, { x: 25 }, { x: 120 }, { x: 1000 }]; function SngTab({ connected, connect, addr }) { const [open, setOpen] = uS(null); // registering tournaments const [mine, setMine] = uS({}); const [busy, setBusy] = uS(false); const [msg, setMsg] = uS(null); const [display, setDisplay] = uS(SPIN_MULTIS[2]); const sym = SP.NETWORK.currency.symbol; function flash(m) { setMsg(m); setTimeout(() => setMsg(null), 4000); } async function load() { try { const ts = (await SP.sdk.tournaments()).filter((t) => t.status === 0).reverse(); setOpen(ts); if (SP.sdk.address) { const m = {}; for (const t of ts) m[t.id] = await SP.sdk.isRegisteredIn(t.id); setMine(m); } } catch (e) { setOpen([]); } } uE(() => { load(); const iv = setInterval(load, 4000); return () => clearInterval(iv); }, [connected]); // spin reel — pure eye-candy teaser while Spin SNG awaits on-chain randomness uE(() => { const iv = setInterval(() => setDisplay(SPIN_MULTIS[Math.floor(Math.random() * SPIN_MULTIS.length)]), 1400); return () => clearInterval(iv); }, []); async function act(label, fn) { setBusy(true); try { await fn(); flash(label + " ✓"); await load(); } catch (e) { flash(label + " ✗ " + (e?.shortMessage || e?.reason || e?.message || "").replace(/execution reverted:?/i, "").slice(0, 70)); console.error(e); } finally { setBusy(false); } } /// One click: create the SNG from the preset AND take the first seat. const quickStart = (p) => act("Create & join " + p.name, async () => { await SP.sdk.createTournament({ buyInEth: p.buyIn, feeEth: p.fee, maxPlayers: p.players, startStack: p.stack, sbStart: p.sb, bbStart: p.bb, levelDur: p.levelMin * 60, payoutBps: p.split, sponsorEth: 0, }); const id = (await SP.sdk.tournamentCount()) - 1; const t = await SP.sdk.tournamentInfo(id); await SP.sdk.registerTournament(id, t.buyIn + t.fee); }); return (
Quick Sit & GoBuy-inFormatAction
{SNG_PRESETS.map((p) => (
{p.name} {p.structure}{p.stack} chipspayout {p.split.map((b) => b / 100 + "%").join("/")}
{(parseFloat(p.buyIn) + parseFloat(p.fee)).toFixed(2)}{sym} {p.buyIn}+{p.fee} fee
{p.players === 2 ? "Heads-up" : p.players + "-max"}
))}
Open seats — join nowBuy-inSeatsAction
{open == null ?
Loading…
: open.length === 0 ?
No open Sit & Go right now — start one above.
: open.map((t) => { const cost = t.buyIn + t.fee; return (
SNG #{t.id} {Number(t.startStack)} chipspool {Number(SP.fmt(t.pool, 4))} {sym}
{Number(SP.fmt(cost, 4))}{sym}
{Array.from({ length: t.maxPlayers }).map((_, i) => )} {t.registered}/{t.maxPlayers}
{!connected ? : mine[t.id] ? : }
); })}
◆ Spin & Go
3-handed hyper turbo with a random prize multiplier revealed at seating — powered by the same provably-fair on-chain randomness as the deck. Coming soon.
×{display.x}
{msg &&
{msg}
}
); } function CreateTournamentModal({ close, onDone, act, busy }) { const sym = SP.NETWORK.currency.symbol; const [f, setF] = uS({ buyIn: "0.5", fee: "0.05", maxPlayers: "6", startStack: "1500", sb: "10", bb: "20", levelMin: "5", split: "65/35", sponsor: "0" }); const set = (k) => (e) => setF((s) => ({ ...s, [k]: e.target.value })); const splitBps = f.split.split("/").map((s) => Math.round(parseFloat(s.trim()) * 100)).filter((n) => !isNaN(n)); const splitOk = splitBps.length > 0 && splitBps.reduce((a, b) => a + b, 0) === 10000; const stop = (e) => e.stopPropagation(); return (

Create tournament

Players' buy-ins build the prize pool — or sponsor the pool yourself (or both). Winners split it exactly how you set below.

{!splitOk &&

Percentages must add up to exactly 100.

}
); } function mountScaleLobby() { const scaler = document.getElementById("scaler"); if (!scaler) return; const app = scaler.querySelector(".app"); if (!app) return; const fit = () => { const s = Math.min((window.innerWidth - 24) / 1600, (window.innerHeight - 84) / 1000); app.style.transform = `scale(${s})`; app.style.transformOrigin = "top left"; app.style.position = "absolute"; app.style.top = "0"; app.style.left = "0"; scaler.style.width = 1600 * s + "px"; scaler.style.height = 1000 * s + "px"; }; fit(); window.addEventListener("resize", fit); } function bootLobby() { ReactDOM.createRoot(document.getElementById("root")).render(); setInterval(mountScaleLobby, 400); setTimeout(mountScaleLobby, 80); } if (window.SP) bootLobby(); else window.addEventListener("sp:ready", bootLobby, { once: true });