47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
16:54Stealth Union Alpha: Is this another cheap GPT model? (better than Luna, cheaper than Terra?)
Sep 16, 2026 · YouTube
16:45DeepSeek V4.1 Flash Is Shockingly Good for the Price
Sep 10, 2026 · YouTube
17:37Tencent Hy4 Preview Tested: Is It Really Better Than Kimi K3 and GLM 5.3?
Aug 28, 2026 · YouTube
23:58Qwen 3.8 Flash Tested: UI, Coding, Physics & more - compared to Opus 5, Sol Pro, Grok
Aug 27, 2026 · YouTube
14:50Stealth Ox Alpha: a new GLM model! Full test (coding & writing) against Grok, Gemini, GPT & others
Aug 21, 2026 · YouTube
My buddy runs a bar. I told him I'd build him something for Thursdays. He's the one talking here — I just write down what he says and hand it to you.
It's not real pool. Three balls on the table, you shoot until all three are down, and we count how many shots it took you. Low is good. Everybody takes a turn, then we go around again — that's a round.
Twenty bucks to play. You start with five chips. Take more than four shots to clear the table and it costs you a chip. Out of chips and you're out, go back to drinking. Last one with chips left takes the pot.
There's a clock on your turn — a minute, minute and a half, we've never really settled it. Clock runs out and you're not at the table, that's a chip.
It's a Thursday. Nine o'clock, everybody's had a few, the jukebox is loud and half the room is outside smoking. Twelve, sixteen people usually, however many show up. It runs two, three hours and nobody's in a hurry.
Last year I ran the whole thing on a legal pad and it got away from me around round four. Two guys arguing with me about what they shot, me trying to remember whose turn it was, somebody's beer on the pad. Never again.
I'm running this off my phone, one-handed, standing at the table with a beer in the other. There's a TV over the bar and that's the only screen anybody in the room can see — from across the room, in the dark, so it's got to be big. People want to know where they stand without walking over and asking me.
My phone's on 15% by ten. The bar's wifi is the bar's wifi.
Twenty a head, all of it in the pot. Winner takes it — mostly. We kick a little to second. And there's a side thing we do for whoever's shooting best early on, I never wrote that part down properly.
The app doesn't hold the money. I do. But at the end of the night it has to tell me what I owe who, and I'm reading that off my phone with sixteen people standing around the table.
Two pages.
The console — the thing I run it from, and the thing that goes up on the TV. Open it and there's already a game going. I'm not typing in twelve names to find out whether it works.
The playback — a separate page that plays a whole night through, start to finish, so I can watch it work before Thursday. One step at a time, a step being one thing happening, with the scorecard filling in as it goes and a slider for how fast it runs. I want to slow it down, back it up, and read what just happened. Show me the way a night actually goes.
The playback runs the same rules the console runs. If the two of them drift apart, the playback is a cartoon and it's no use to me.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — these run in a sandboxed frame where those throw.html blocks: the console first, the playback second. Leave yourself room for the second one.I know I wasn't clear about everything. Everything you had to decide for me, one line each — what you decided and why. Short.
The first fenced html block.
The second fenced html block.
It's Thursday, it's nine o'clock, and I'm running this for real. What comes up that this doesn't handle? Be straight with me. I'd much rather hear it now than find out with sixteen people around a pool table and three hundred dollars sitting on the bar.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thursday Pool — Console</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
:root {
--bg:#101418; --panel:#1a2129; --panel2:#222b35; --line:#33404d;
--ink:#eef3f7; --dim:#8fa1b0; --gold:#f2b632; --red:#e05a4e; --green:#4ecb71; --blue:#5aa9e6;
}
html,body { height:100%; }
body {
background:var(--bg); color:var(--ink);
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
-webkit-user-select:none; user-select:none;
touch-action:manipulation;
}
/* ============ TV MODE (default: big, dark, readable across the room) ============ */
#tv { min-height:100vh; padding:4vh 4vw; display:flex; flex-direction:column; gap:2.5vh; }
#tv .topbar { display:flex; justify-content:space-between; align-items:baseline; gap:2vw; }
#tv h1 { font-size:clamp(28px,5vw,64px); letter-spacing:.04em; color:var(--gold); }
#tv .meta { font-size:clamp(16px,2.6vw,36px); color:var(--dim); text-align:right; line-height:1.35; }
#tv .meta b { color:var(--ink); }
#tv .potline { font-size:clamp(18px,3vw,40px); color:var(--gold); font-weight:700; }
#tv .board { flex:1; display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:1.6vh 1.2vw; align-content:start; }
#tv .card {
background:var(--panel); border:2px solid var(--line); border-radius:14px;
padding:1.6vh 1.2vw; display:flex; flex-direction:column; gap:.6vh;
min-height:16vh;
}
#tv .card .nm { font-size:clamp(20px,3.2vw,44px); font-weight:700; display:flex; justify-content:space-between; align-items:center; gap:1vw; }
#tv .card .chips { font-size:clamp(18px,3vw,40px); color:var(--gold); font-weight:800; }
#tv .card .stat { font-size:clamp(13px,2vw,28px); color:var(--dim); }
#tv .card .stat b { color:var(--ink); }
#tv .card .last { font-size:clamp(13px,2vw,26px); color:var(--dim); }
#tv .card.me { border-color:var(--gold); background:#2a2416; }
#tv .card.me .nm::after { content:"◀ ON CLOCK"; color:var(--gold); font-size:.55em; letter-spacing:.1em; }
#tv .card.out { opacity:.38; }
#tv .card.out .nm::after { content:"OUT"; color:var(--red); font-size:.55em; }
#tv .card.hot { border-color:var(--red); }
#tv .card.hot .nm::after { content:"🔥 HOT"; font-size:.55em; color:var(--red); }
#tv .now {
background:var(--panel2); border:3px solid var(--gold); border-radius:14px;
padding:2vh 2vw; display:flex; align-items:center; gap:3vw; flex-wrap:wrap;
}
#tv .now .who { font-size:clamp(26px,4.5vw,60px); font-weight:800; }
#tv .now .clock { font-size:clamp(30px,6vw,80px); font-weight:800; color:var(--gold); font-variant-numeric:tabular-nums; }
#tv .now .clock.low { color:var(--red); }
#tv .now .hint { font-size:clamp(14px,2.2vw,30px); color:var(--dim); }
#tv .ticker {
font-size:clamp(16px,2.6vw,36px); color:var(--dim); border-top:2px solid var(--line);
padding-top:1.5vh; min-height:1.4em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
#tv .ticker b { color:var(--ink); }
#tv .winner {
position:fixed; inset:0; background:rgba(8,10,13,.96);
display:none; flex-direction:column; align-items:center; justify-content:center; gap:3vh; text-align:center; padding:4vh;
}
#tv .winner.show { display:flex; }
#tv .winner h2 { font-size:clamp(30px,6vw,80px); color:var(--gold); }
#tv .winner .big { font-size:clamp(24px,4.5vw,64px); }
#tv .winner .pay { font-size:clamp(18px,3vw,42px); color:var(--dim); line-height:1.6; }
#tv .winner .pay b { color:var(--green); }
/* ============ CONSOLE (phone) ============ */
#console { display:none; }
body.console #tv { display:none; }
body.console #console { display:block; min-height:100vh; padding:12px 12px 90px; max-width:520px; margin:0 auto; }
#console h1 { font-size:22px; color:var(--gold); margin-bottom:4px; }
#console .sub { color:var(--dim); font-size:13px; margin-bottom:12px; }
#console .row { display:flex; gap:8px; margin-bottom:10px; flex-wrap:wrap; }
#console button {
font:inherit; font-weight:700; border:2px solid var(--line); border-radius:10px;
background:var(--panel2); color:var(--ink); padding:12px 14px; cursor:pointer; font-size:15px;
}
#console button:active { transform:scale(.97); }
#console button.primary { background:var(--gold); color:#1a1408; border-color:var(--gold); }
#console button.danger { background:var(--red); color:#fff; border-color:var(--red); }
#console button.ghost { background:transparent; }
#console button:disabled { opacity:.4; cursor:default; }
#console .panel { background:var(--panel); border:2px solid var(--line); border-radius:12px; padding:12px; margin-bottom:10px; }
#console .panel h2 { font-size:15px; color:var(--dim); text-transform:uppercase; letter-spacing:.08em; margin-bottom:8px; }
#console .nowbox { display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
#console .nowbox .who { font-size:20px; font-weight:800; }
#console .nowbox .clock { font-size:34px; font-weight:800; color:var(--gold); font-variant-numeric:tabular-nums; }
#console .nowbox .clock.low { color:var(--red); }
#console .shotgrid { display:grid; grid-template-columns:repeat(5,1fr); gap:8px; margin:10px 0; }
#console .shotgrid button { padding:16px 0; font-size:18px; }
#console .list { display:flex; flex-direction:column; gap:6px; max-height:38vh; overflow:auto; }
#console .li {
display:flex; align-items:center; gap:10px; background:var(--panel2);
border:2px solid var(--line); border-radius:10px; padding:10px 12px;
}
#console .li .nm { font-weight:700; flex:1; }
#console .li .ch { color:var(--gold); font-weight:800; min-width:52px; text-align:right; }
.chip { display:inline-block; width:11px; height:11px; border-radius:50%; background:var(--gold); margin-left:2px; }
.chip.gone { background:#3a4652; }
#console .li.out { opacity:.4; }
#console .li.out .nm::after { content:" — OUT"; color:var(--red); font-size:12px; }
#console .li.me { border-color:var(--gold); }
#console .li.hot { border-color:var(--red); }
#console .li.hot .nm::after { content:" 🔥"; }
#console .log { font-size:13px; color:var(--dim); max-height:22vh; overflow:auto; display:flex; flex-direction:column-reverse; gap:4px; }
#console .log div b { color:var(--ink); }
#console .pay { font-size:17px; line-height:1.9; }
#console .pay b { color:var(--green); font-size:20px; }
#console .banner { background:#3a2a10; border:2px solid var(--gold); color:var(--gold); border-radius:10px; padding:10px 12px; font-weight:700; margin-bottom:10px; font-size:14px; }
#console .field { display:flex; gap:8px; align-items:center; margin-bottom:8px; }
#console input {
flex:1; font:inherit; background:var(--bg); color:var(--ink);
border:2px solid var(--line); border-radius:10px; padding:12px; font-size:16px;
}
#console .foot { position:fixed; left:0; right:0; bottom:0; background:var(--panel); border-top:2px solid var(--line); padding:8px 12px; display:flex; gap:8px; justify-content:center; }
#console .foot button { flex:1; max-width:200px; }
.toast {
position:fixed; top:10px; left:50%; transform:translateX(-50%);
background:var(--red); color:#fff; font-weight:700; padding:10px 18px; border-radius:10px;
font-size:14px; z-index:50; display:none; max-width:90vw; text-align:center;
}
.toast.show { display:block; }
</style>
</head>
<body>
<!-- ================= TV ================= -->
<div id="tv">
<div class="topbar">
<h1>THURSDAY POOL</h1>
<div class="meta">
<div>ROUND <b id="tvRound">1</b> · <b id="tvAlive">12</b> PLAYERS LEFT</div>
<div class="potline" id="tvPot">POT $240</div>
<div id="tvSide">HOT HAND side pot: $5 — nobody yet</div>
</div>
</div>
<div class="now" id="tvNow" style="display:none">
<div class="who" id="tvWho">—</div>
<div class="clock" id="tvClock">1:30</div>
<div class="hint" id="tvHint">shoot until all three balls are down</div>
</div>
<div class="board" id="tvBoard"></div>
<div class="ticker" id="tvTicker">waiting for the first shot…</div>
<div class="winner" id="tvWinner">
<h2>🏆 GAME OVER</h2>
<div class="big" id="winNames">—</div>
<div class="pay" id="winPay"></div>
<div style="color:var(--dim);font-size:clamp(14px,2vw,26px)">switch the phone to console to start a new night</div>
</div>
</div>
<!-- ================= CONSOLE ================= -->
<div id="console">
<h1>THURSDAY POOL — CONSOLE</h1>
<div class="sub">$20 a head · 5 chips each · 5+ shots = −1 chip · clock 1:30</div>
<div id="cSetup">
<div class="panel">
<h2>Who's in tonight</h2>
<div class="field">
<input id="newName" placeholder="name" maxlength="14" autocomplete="off">
<button class="primary" id="addBtn">ADD</button>
</div>
<div class="row" id="roster"></div>
<div class="row">
<button class="primary" id="startBtn">START THE NIGHT</button>
</div>
<div style="color:var(--dim);font-size:13px;margin-top:6px">or load last week's crew:</div>
<div class="row"><button class="ghost" id="demoBtn">LOAD DEMO ROSTER (12)</button></div>
</div>
</div>
<div id="cGame" style="display:none">
<div class="banner" id="cBanner" style="display:none"></div>
<div class="panel">
<h2>Now shooting</h2>
<div class="nowbox">
<div class="who" id="cWho">—</div>
<div class="clock" id="cClock">1:30</div>
</div>
<div style="color:var(--dim);font-size:13px;margin-top:4px" id="cSub">shots to clear the table — 1, 2, 3, 4 are safe</div>
<div class="shotgrid" id="cShots"></div>
<div class="row">
<button class="danger" id="foulBtn">CLOCK RAN OUT (−1 chip)</button>
</div>
</div>
<div class="panel">
<h2>Scorecard</h2>
<div class="row" style="font-size:13px;color:var(--dim)">
<span>ROUND <b id="cRound" style="color:var(--ink)">1</b></span>
<span>POT <b id="cPot" style="color:var(--gold)">$0</b></span>
<span>LEFT <b id="cAlive">0</b></span>
</div>
<div class="list" id="cList"></div>
</div>
<div class="panel">
<h2>Log</h2>
<div class="log" id="cLog"></div>
</div>
</div>
<div id="cOver" style="display:none">
<div class="panel">
<h2>Game over — pay out</h2>
<div class="pay" id="cPay"></div>
<div class="row" style="margin-top:10px">
<button class="primary" id="newBtn">NEW NIGHT</button>
</div>
</div>
</div>
<div class="foot">
<button id="modeBtn">📺 TV MODE</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
(function(){
"use strict";
/* ================= RULES (single source of truth, shared shape with playback) ================= */
var ENTRY = 20, START_CHIPS = 5, CLOCK_SECONDS = 90, CHIP_SHOT_LIMIT = 4;
var SIDE_POT = 5, SIDE_AFTER_ROUND = 2, SECOND_PCT = 0.20;
var state = null; // {players:[{name,chips,alive,shots,totalShots,rounds,hot}], round, order:[], idx, pot, side, sideHolder, over, winners, log:[]}
var timer = null, clockLeft = 0, tvMode = false;
/* ================= DEMO ROSTER ================= */
var DEMO = ["Ray","Darlene","Mo","Gus","Pete","Sue","Tiny","Wanda","Chuck","Lou","Dot","Hank"];
/* ================= HELPERS ================= */
function $(id){ return document.getElementById(id); }
function fmtClock(s){ var m=Math.floor(s/60), r=s%60; return m+":"+(r<10?"0":"")+r; }
function toast(msg){
var t=$("toast"); t.textContent=msg; t.classList.add("show");
clearTimeout(toast._t); toast._t=setTimeout(function(){ t.classList.remove("show"); },2200);
}
function alive(){ return state.players.filter(function(p){ return p.alive; }); }
function leader(){
var a=alive(), best=null;
a.forEach(function(p){ if(!best || p.totalShots/(p.rounds||1) < best.totalShots/(best.rounds||1)) best=p; });
return best;
}
function avg(p){ return p.rounds? (p.totalShots/p.rounds) : 0; }
/* ================= GAME LOGIC ================= */
function newState(names){
state = {
players: names.map(function(n){ return {name:n, chips:START_CHIPS, alive:true, shots:0, totalShots:0, rounds:0, hot:false}; }),
round:1, order:[], idx:0, pot:names.length*ENTRY, side:0, sideHolder:null,
over:false, winners:[], log:[]
};
refillOrder();
log("Night started — "+names.length+" players, $"+state.pot+" in the pot.");
startTurn();
}
function refillOrder(){
state.order = alive().map(function(p){ return p.name; });
state.idx = 0;
}
function cur(){ return state.players.find(function(p){ return p.name===state.order[state.idx]; }); }
function log(msg){
state.log.unshift({t:new Date(), msg:msg});
if(state.log.length>60) state.log.pop();
renderLog();
}
function startTurn(){
if(state.over) return;
var p = cur();
if(!p){ endRound(); return; }
p.shots = 0;
clockLeft = CLOCK_SECONDS;
stopTimer(); startTimer();
$("cWho").textContent = p.name;
$("cSub").textContent = "shots to clear the table — 1–4 are safe, 5+ costs a chip";
$("foulBtn").disabled = false;
renderAll();
}
function startTimer(){
stopTimer();
timer = setInterval(tick, 250);
}
function stopTimer(){
if(timer){ clearInterval(timer); timer=null; }
}
function tick(){
clockLeft -= 0.25;
if(clockLeft <= 0){
clockLeft = 0;
clockExpired();
}
renderClock();
}
function clockExpired(){
stopTimer();
var p = cur();
p.chips--; p.shots = 99; // mark as clocked
log("⏰ "+p.name+" ran out of clock — −1 chip ("+p.chips+" left)");
$("foulBtn").disabled = true;
checkChipOut(p);
if(state.over) return;
if(p.alive) log(p.name+" is out of chips — sits down");
nextTurn();
}
function recordShots(n){
stopTimer();
var p = cur();
p.shots = n; p.totalShots += n; p.rounds++;
var chipLoss = n > CHIP_SHOT_LIMIT;
if(chipLoss){ p.chips--; }
log(p.name+" cleared in "+n+" shot"+(n===1?"":"s")+(chipLoss?" — 5+, −1 chip":"")+" · "+p.chips+" chip"+(p.chips===1?"":"s")+" left");
if(p.chips<=0){ p.alive=false; log("💀 "+p.name+" is OUT of chips"); }
checkHot();
nextTurn();
}
function nextTurn(){
if(state.over) return;
// advance past eliminated players
var guard = 0;
while(guard++ < 100){
state.idx++;
if(state.idx >= state.order.length){ endRound(); return; }
var p = cur();
if(p && p.alive) break;
}
if(state.over) return;
startTurn();
}
function endRound(){
stopTimer();
refillOrder();
state.round++;
log("— ROUND "+(state.round-1)+" done. Round "+state.round+" begins. —");
checkHot();
// game over when 1 or 0 players alive
var a = alive();
if(a.length <= 1){
finish(a);
return;
}
startTurn();
}
function checkChipOut(){} // handled inline
function checkHot(){
// Hot Hand: after round SIDE_AFTER_ROUND, the best average shooter holds the side pot marker
if(state.round > SIDE_AFTER_ROUND && !state.sideHolder){
var l = leader();
if(l && alive().length>1){
state.sideHolder = l.name; state.side = SIDE_POT;
l.hot = true;
state.players.forEach(function(p){ if(p!==l) p.hot=false; });
log("🔥 HOT HAND — "+l.name+" is shooting best (avg "+avg(l).toFixed(2)+"/turn). $"+SIDE_POT+" side pot is theirs if they hold it to the end.");
}
}
}
function finish(winners){
state.over = true; stopTimer();
var a = alive();
var champs = a.length? a : [state.players.reduce(function(x,y){ return y.chips>x.chips?y:x; })];
state.winners = champs.map(function(p){ return p.name; });
// payouts
var pot = state.pot;
var second = Math.floor(pot*SECOND_PCT);
var first = pot - second;
var share = Math.floor(first/champs.length);
var remainder = first - share*champs.length; // to the bar
var sideWon = state.sideHolder && champs.some(function(n){ return n===state.sideHolder; });
var sidePay = sideWon? state.side : 0;
if(!sideWon && state.side){ pot += 0; } // side rolls into pot conceptually; we just note it
state.payout = {
winners: champs.map(function(p){ return {name:p.name, amt:share}; }),
second: second>0? second : 0,
secondNames: secondPlaceNames(),
side: sidePay, sideName: sideWon? state.sideHolder : null,
sideRolled: !sideWon && state.side>0,
remainder: remainder
};
log("🏆 GAME OVER — "+state.winners.join(" & ")+" take it.");
renderAll();
}
function secondPlaceNames(){
// best average among non-winners still alive at end, else best chip count
var a = alive().filter(function(p){ return state.winners.indexOf(p.name)===-1; });
if(!a.length) return [];
a.sort(function(x,y){ return avg(y)-avg(x); });
return [a[0].name];
}
/* ================= RENDER ================= */
function renderAll(){ renderTV(); renderConsole(); }
function renderClock(){
var el=$("cClock"), t=$("tvClock");
var s=Math.ceil(clockLeft);
[el,t].forEach(function(e){ if(e){ e.textContent=fmtClock(Math.max(0,s)); e.classList.toggle("low", s<=10); }});
}
function chipDots(p){
var h="";
for(var i=0;i<START_CHIPS;i++) h+='<span class="chip'+(i<p.chips?"":" gone")+'"></span>';
return h;
}
function renderTV(){
if(!state) return;
$("tvRound").textContent = state.round;
$("tvAlive").textContent = alive().length;
$("tvPot").textContent = "POT $"+state.pot;
var sideTxt = state.sideHolder
? "HOT HAND side pot: $"+state.side+" — "+state.sideHolder+" holds it"
: (state.side? "HOT HAND side pot: $"+state.side+" — ROLLED IN (nobody held it)" : "HOT HAND side pot: $"+SIDE_POT+" — nobody yet");
$("tvSide").textContent = sideTxt;
var board=$("tvBoard"); board.innerHTML="";
state.players.forEach(function(p){
var d=document.createElement("div");
d.className="card"+(p.alive?"":" out")+(p.name===state.sideHolder?" hot":"");
if(state.order[state.idx]===p.name && p.alive && !state.over) d.classList.add("me");
d.innerHTML =
'<div class="nm">'+esc(p.name)+'<span class="chips">'+chipDots(p)+'</span></div>'+
'<div class="stat">AVG <b>'+(p.rounds?avg(p).toFixed(2):"—")+'</b> · SHOTS <b>'+p.totalShots+'</b> · TURNS <b>'+p.rounds+'</b></div>'+
'<div class="last">'+(p.rounds?("last: "+p.shots+" shot"+(p.shots===1?"":"s")):"hasn\'t shot")+'</div>';
board.appendChild(d);
});
var now=$("tvNow");
if(!state.over && state.order.length){
var p=cur();
if(p){ now.style.display="flex"; $("tvWho").textContent=p.name+" IS UP"; $("tvHint").textContent="3 balls · low shots win · 5+ shots = −1 chip"; }
} else now.style.display="none";
var last = state.log[0];
$("tvTicker").innerHTML = last? ("<b>"+esc(last.msg)+"</b>") : "waiting for the first shot…";
// winner overlay
var w=$("tvWinner");
if(state.over){
w.classList.add("show");
$("winNames").textContent = state.winners.join(" & ");
var pay = state.payout;
var html = "";
pay.winners.forEach(function(x){ html += "1st — "+esc(x.name)+": <b>$"+x.amt+"</b><br>"; });
if(pay.second>0) html += "2nd — "+esc(pay.secondNames.join(", "))+": <b>$"+pay.second+"</b><br>";
if(pay.side>0) html += "🔥 Hot Hand — "+esc(pay.sideName)+": <b>$"+pay.side+"</b><br>";
if(pay.sideRolled) html += "<span style='color:var(--dim)'>(Hot Hand rolled into the pot — nobody held it)</span><br>";
if(pay.remainder>0) html += "<span style='color:var(--dim)'>bar keeps $"+pay.remainder+" (rounding)</span>";
$("winPay").innerHTML = html;
} else w.classList.remove("show");
}
function renderConsole(){
if(!state) return;
$("cRound").textContent = state.round;
$("cPot").textContent = "$"+state.pot;
$("cAlive").textContent = alive().length;
// roster (setup)
// scorecard
var list=$("cList"); list.innerHTML="";
state.players.forEach(function(p){
var d=document.createElement("div");
d.className="li"+(p.alive?"":" out")+(p.name===state.sideHolder?" hot":"");
if(state.order[state.idx]===p.name && p.alive && !state.over) d.classList.add("me");
d.innerHTML='<span class="nm">'+esc(p.name)+'</span><span>'+chipDots(p)+'</span><span class="ch">'+p.chips+' · '+avg(p).toFixed(2)+'</span>';
list.appendChild(d);
});
// banner
var b=$("cBanner");
if(state.sideHolder && !state.over){
b.style.display="block";
b.textContent = "🔥 HOT HAND: "+state.sideHolder+" holds the $"+state.side+" side pot — they keep it if they win tonight.";
} else b.style.display="none";
// views
$("cSetup").style.display="none";
$("cOver").style.display = state.over? "block":"none";
$("cGame").style.display = state.over? "none":"block";
if(state.over){
var pay=state.payout, html="";
pay.winners.forEach(function(x){ html+="1st — "+esc(x.name)+": <b>$"+x.amt+"</b><br>"; });
if(pay.second>0) html+="2nd — "+esc(pay.secondNames.join(", "))+": <b>$"+pay.second+"</b><br>";
if(pay.side>0) html+="🔥 Hot Hand — "+esc(pay.sideName)+": <b>$"+pay.side+"</b><br>";
if(pay.sideRolled) html+="<small style='color:var(--dim)'>(Hot Hand rolled into the pot)</small><br>";
if(pay.remainder>0) html+="<small style='color:var(--dim)'>bar keeps $"+pay.remainder+" (rounding)</small><br>";
html+="<hr style='border-color:var(--line);margin:8px 0'>Total paid: <b>$"+(pay.winners.reduce(function(s,x){return s+x.amt;},0)+pay.second+pay.side)+"</b> of $"+state.pot;
$("cPay").innerHTML=html;
}
renderClock();
}
function renderLog(){
if(!state) return;
var el=$("cLog"); el.innerHTML="";
state.log.slice(0,40).forEach(function(l){
var d=document.createElement("div");
d.innerHTML="<b>"+l.t.toLocaleTimeString([], {hour:"2-digit",minute:"2-digit"})+"</b> "+esc(l.msg);
el.appendChild(d);
});
}
function esc(s){ return String(s).replace(/[&<>"']/g,function(c){ return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c]; }); }
/* ================= SETUP ================= */
var setupNames = [];
function renderRoster(){
var r=$("roster"); r.innerHTML="";
setupNames.forEach(function(n,i){
var b=document.createElement("button");
b.className="ghost"; b.textContent=n+" ✕";
b.onclick=function(){ setupNames.splice(i,1); renderRoster(); };
r.appendChild(b);
});
$("startBtn").disabled = setupNames.length<2;
}
$("addBtn").onclick=function(){
var v=$("newName").value.trim();
if(!v){ toast("type a name first"); return; }
if(setupNames.indexOf(v)>-1){ toast(v+" is already in"); return; }
setupNames.push(v); $("newName").value=""; renderRoster();
};
$("newName").addEventListener("keydown",function(e){ if(e.key==="Enter") $("addBtn").click(); });
$("demoBtn").onclick=function(){ setupNames=DEMO.slice(); renderRoster(); };
$("startBtn").onclick=function(){
if(setupNames.length<2){ toast("need at least 2 players"); return; }
newState(setupNames.slice());
renderAll();
};
/* ================= SHOT BUTTONS ================= */
(function(){
var g=$("cShots");
for(var i=1;i<=8;i++){
(function(n){
var b=document.createElement("button");
b.textContent=n;
b.onclick=function(){ recordShots(n); };
g.appendChild(b);
})(i);
}
var b9=document.createElement("button");
b9.textContent="9+"; b9.style.gridColumn="span 5";
b9.onclick=function(){ recordShots(9); };
g.appendChild(b9);
})();
$("foulBtn").onclick=function(){ clockExpired(); };
/* ================= MODE TOGGLE ================= */
$("modeBtn").onclick=function(){
tvMode=!tvMode;
document.body.classList.toggle("console", !tvMode);
$("modeBtn").textContent = tvMode? "📱 CONSOLE":"📺 TV MODE";
renderAll();
};
$("newBtn").onclick=function(){
stopTimer();
state=null; setupNames=[];
$("cSetup").style.display="block"; $("cGame").style.display="none"; $("cOver").style.display="none";
$("cLog").innerHTML=""; renderRoster();
document.body.classList.add("console"); tvMode=false; $("modeBtn").textContent="📺 TV MODE";
};
/* ================= BOOT: open on a game already in progress ================= */
document.body.classList.add("console"); tvMode=false;
newState(DEMO.slice());
// fast-forward a few turns so the TV has something to show
recordShots(3); recordShots(6); recordShots(2); recordShots(4);
renderAll();
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thursday Pool — Playback</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
:root {
--bg:#101418; --panel:#1a2129; --panel2:#222b35; --line:#33404d;
--ink:#eef3f7; --dim:#8fa1b0; --gold:#f2b632; --red:#e05a4e; --green:#4ecb71;
}
body { background:var(--bg); color:var(--ink); font-family:"Segoe UI","Helvetica Neue",Arial,sans-serif; padding:14px; max-width:1100px; margin:0 auto; }
h1 { color:var(--gold); font-size:24px; }
.sub { color:var(--dim); font-size:14px; margin:4px 0 14px; }
.controls { background:var(--panel); border:2px solid var(--line); border-radius:12px; padding:12px; display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:12px; position:sticky; top:0; z-index:10; }
.controls button { font:inherit; font-weight:700; border:2px solid var(--line); border-radius:10px; background:var(--panel2); color:var(--ink); padding:10px 16px; cursor:pointer; font-size:15px; }
.controls button:active { transform:scale(.97); }
.controls button.primary { background:var(--gold); color:#1a1408; border-color:var(--gold); }
.controls .speed { display:flex; align-items:center; gap:8px; color:var(--dim); font-size:14px; }
.controls input[type=range] { width:160px; }
.controls .stepnum { color:var(--ink); font-weight:700; min-width:90px; font-variant-numeric:tabular-nums; }
.grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
@media (max-width:800px){ .grid { grid-template-columns:1fr; } }
.panel { background:var(--panel); border:2px solid var(--line); border-radius:12px; padding:12px; margin-bottom:12px; }
.panel h2 { font-size:14px; color:var(--dim); text-transform:uppercase; letter-spacing:.08em; margin-bottom:8px; }
.meta { display:flex; gap:16px; flex-wrap:wrap; font-size:15px; margin-bottom:8px; }
.meta b { color:var(--gold); }
.board { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:8px; }
.card { background:var(--panel2); border:2px solid var(--line); border-radius:10px; padding:8px 10px; font-size:14px; }
.card .nm { font-weight:700; display:flex; justify-content:space-between; }
.card .ch { color:var(--gold); }
.card .st { color:var(--dim); font-size:12px; margin-top:2px; }
.card.me { border-color:var(--gold); }
.card.out { opacity:.4; }
.card.hot { border-color:var(--red); }
.chip { display:inline-block; width:9px; height:9px; border-radius:50%; background:var(--gold); margin-right:2px; }
.chip.gone { background:#3a4652; }
.nowbox { background:var(--panel2); border:2px solid var(--gold); border-radius:10px; padding:10px 12px; margin-bottom:10px; display:flex; gap:14px; align-items:center; flex-wrap:wrap; }
.nowbox .who { font-size:18px; font-weight:800; }
.nowbox .clk { font-size:26px; font-weight:800; color:var(--gold); font-variant-numeric:tabular-nums; }
.nowbox .clk.low { color:var(--red); }
.nowbox .hn { color:var(--dim); font-size:13px; }
.event { background:#2a2416; border:2px solid var(--gold); border-radius:10px; padding:12px; font-size:17px; font-weight:600; min-height:56px; }
.event .tag { color:var(--gold); font-size:12px; text-transform:uppercase; letter-spacing:.1em; display:block; margin-bottom:2px; }
.log { font-size:13px; color:var(--dim); max-height:300px; overflow:auto; display:flex; flex-direction:column-reverse; gap:4px; }
.log b { color:var(--ink); }
.pay { font-size:16px; line-height:2; }
.pay b { color:var(--green); }
.over { border-color:var(--green); }
</style>
</head>
<body>
<h1>THURSDAY POOL — PLAYBACK</h1>
<div class="sub">A full night, step by step. Same rules as the console: $20 entry, 5 chips, 5+ shots = −1 chip, 90-second clock, last one with chips wins. Drag the slider, pause, step, scrub back.</div>
<div class="controls">
<button id="playBtn" class="primary">▶ PLAY</button>
<button id="prevBtn">◀ STEP</button>
<button id="nextBtn">STEP ▶</button>
<div class="speed">
<span>SPEED</span>
<input type="range" id="speed" min="0" max="100" value="55">
<span id="speedLbl">1.0×</span>
</div>
<span class="stepnum" id="stepLbl">step 0 / 0</span>
<input type="range" id="scrub" min="0" max="0" value="0" style="flex:1;min-width:120px">
</div>
<div class="grid">
<div>
<div class="panel">
<h2>Scorecard</h2>
<div class="meta">
<span>ROUND <b id="pRound">—</b></span>
<span>POT <b id="pPot">—</b></span>
<span>LEFT <b id="pAlive">—</b></span>
<span id="pSide" style="color:var(--dim)"></span>
</div>
<div class="board" id="pBoard"></div>
</div>
<div class="panel">
<h2>What just happened</h2>
<div class="event" id="pEvent"><span class="tag">—</span>press play or step forward</div>
</div>
</div>
<div>
<div class="panel">
<h2>On the clock</h2>
<div class="nowbox" id="pNow">
<span class="who" id="pWho">—</span>
<span class="clk" id="pClk">1:30</span>
<span class="hn">3 balls · 5+ shots = −1 chip</span>
</div>
<div class="log" id="pLog"></div>
</div>
<div class="panel over" id="pOver" style="display:none">
<h2>Final payout</h2>
<div class="pay" id="pPay"></div>
</div>
</div>
</div>
<script>
(function(){
"use strict";
/* ============ RULES — identical constants & logic to the console ============ */
var ENTRY=20, START_CHIPS=5, CLOCK_SECONDS=90, CHIP_SHOT_LIMIT=4;
var SIDE_POT=5, SIDE_AFTER_ROUND=2, SECOND_PCT=0.20;
/* ============ SCRIPTED NIGHT ============
Each step is one atomic event. Types:
start | turn | shots {n} | clock | round | hot | out | over
The engine applies them to a live state, so the scorecard
fills in exactly the way the console's does. */
var NAMES=["Ray","Darlene","Mo","Gus","Pete","Sue","Tiny","Wanda","Chuck","Lou","Dot","Hank"];
var SCRIPT=[
{t:"start", names:NAMES},
// round 1
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:2},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:7},
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:8},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:5},
{t:"round"},
// round 2
{t:"turn"},{t:"shots",n:2},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:9},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:7},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:6},
{t:"round"},
{t:"hot"}, // hot hand awarded after round 2
// round 3
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"clock"}, // somebody blows the clock
{t:"turn"},{t:"shots",n:2},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:8},
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"round"},
// round 4 — chips start running out
{t:"turn"},{t:"shots",n:7},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:9},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:5},
{t:"round"},
// round 5
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:8},
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:7},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:6},
{t:"round"},
// round 6 — down to the wire
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:3},
{t:"round"},
// round 7 — eliminations
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:7},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:6},
{t:"round"},
// round 8 — heads up
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"round"},
// round 9 — final
{t:"turn"},{t:"shots",n:2},
{t:"turn"},{t:"shots",n:5},
{t:"turn"},{t:"shots",n:4},
{t:"turn"},{t:"shots",n:6},
{t:"turn"},{t:"shots",n:3},
{t:"turn"},{t:"shots",n:5},
{t:"over"}
];
/* ============ ENGINE (mirrors console logic) ============ */
var S=null, history=[], stepIdx=-1, playing=false, timer=null, clockLeft=0, speed=1;
function freshState(names){
return {
players:names.map(function(n){ return {name:n,chips:START_CHIPS,alive:true,shots:0,totalShots:0,rounds:0,hot:false}; }),
round:1, order:[], idx:0, pot:names.length*ENTRY, side:0, sideHolder:null,
over:false, winners:[], payout:null, log:[], lastEvent:null
};
}
function alive(){ return S.players.filter(function(p){ return p.alive; }); }
function cur(){ return S.players.find(function(p){ return p.name===S.order[S.idx]; }); }
function avg(p){ return p.rounds? p.totalShots/p.rounds : 0; }
function refill(){ S.order=alive().map(function(p){ return p.name; }); S.idx=0; }
function pushLog(m){ S.log.unshift(m); if(S.log.length>80) S.log.pop(); }
function leader(){
var a=alive(), best=null;
a.forEach(function(p){ if(!best||avg(p)<avg(best)) best=p; });
return best;
}
function checkOver(){
var a=alive();
if(a.length<=1){
S.over=true;
var champs=a.length? a : [S.players.reduce(function(x,y){ return y.chips>x.chips?y:x; })];
S.winners=champs.map(function(p){ return p.name; });
var pot=S.pot, second=Math.floor(pot*SECOND_PCT), first=pot-second;
var share=Math.floor(first/champs.length), rem=first-share*champs.length;
var sideWon=S.sideHolder && champs.some(function(n){ return n===S.sideHolder; });
var nonWin=alive().filter(function(p){ return S.winners.indexOf(p.name)===-1; })
.sort(function(x,y){ return avg(y)-avg(x); });
S.payout={
winners:champs.map(function(p){ return {name:p.name,amt:share}; }),
second:second, secondNames:nonWin.length?[nonWin[0].name]:[],
side:sideWon?S.side:0, sideName:sideWon?S.sideHolder:null,
sideRolled:!sideWon&&S.side>0, remainder:rem
};
return true;
}
return false;
}
function applyStep(st){
switch(st.t){
case "start":
S=freshState(st.names);
refill();
S.lastEvent={tag:"night starts", msg:st.names.length+" players, $"+S.pot+" in the pot. 5 chips each. Low shots win."};
pushLog("Night started — "+st.names.length+" in, $"+S.pot+" pot.");
break;
case "turn": {
// advance to next alive player (or end round)
var guard=0;
while(guard++<200){
S.idx++;
if(S.idx>=S.order.length){
// end of round
refill(); S.round++;
S.lastEvent={tag:"round "+(S.round-1)+" over", msg:"Round "+S.round+" begins. "+alive().length+" players left."};
pushLog("— Round "+(S.round-1)+" done. Round "+S.round+" —");
if(checkOver()){ S.lastEvent={tag:"game over", msg:S.winners.join(" & ")+" win(s) it."}; break; }
// hot hand check after round SIDE_AFTER_ROUND
if(S.round>SIDE_AFTER_ROUND && !S.sideHolder){
var l=leader();
if(l){ S.sideHolder=l.name; S.side=SIDE_POT; l.hot=true;
S.players.forEach(function(p){ if(p!==l) p.hot=false; });
S.lastEvent={tag:"🔥 hot hand", msg:l.name+" is shooting best (avg "+avg(l).toFixed(2)+"). $"+SIDE_POT+" side pot is theirs if they hold it to the end."};
pushLog("🔥 HOT HAND — "+l.name+" (avg "+avg(l).toFixed(2)+")");
}
}
S.idx=0;
var p=cur();
if(p){ S.lastEvent={tag:"on the clock", msg:p.name+" is up. 90 seconds."}; clockLeft=CLOCK_SECONDS; }
break;
}
var p=cur();
if(p&&p.alive){
clockLeft=CLOCK_SECONDS;
S.lastEvent={tag:"on the clock", msg:p.name+" is up. 90 seconds on the clock."};
break;
}
}
break;
}
case "shots": {
var p=cur(); if(!p||S.over) break;
p.shots=st.n; p.totalShots+=st.n; p.rounds++;
var loss=st.n>CHIP_SHOT_LIMIT;
if(loss) p.chips--;
var msg=p.name+" cleared in "+st.n+" shot"+(st.n===1?"":"s")+(loss?" — 5+ shots, −1 chip":"")+" · "+p.chips+" chip"+(p.chips===1?"":"s")+" left";
if(p.chips<=0){ p.alive=false; msg+=" — OUT"; }
S.lastEvent={tag:p.name+" shoots", msg:msg+(p.alive?"":" · sits down, goes back to drinking")};
pushLog(msg);
if(checkOver()){ S.lastEvent={tag:"game over", msg:S.winners.join(" & ")+" take(s) the pot."}; }
break;
}
case "clock": {
var p=cur(); if(!p||S.over) break;
p.chips--; p.shots=99;
var msg="⏰ "+p.name+" ran out of clock — −1 chip ("+p.chips+" left)";
if(p.chips<=0){ p.alive=false; msg+=" — OUT"; }
S.lastEvent={tag:"⏰ clock", msg:msg+(p.alive?"":" · out of chips, sits down")};
pushLog(msg);
if(checkOver()){ S.lastEvent={tag:"game over", msg:S.winners.join(" & ")+" take(s) the pot."}; }
break;
}
case "hot": {
// explicit hot-hand beat (engine also auto-awards; this is the scripted beat)
if(!S.sideHolder){
var l=leader();
if(l){ S.sideHolder=l.name; S.side=SIDE_POT; l.hot=true;
S.players.forEach(function(p){ if(p!==l) p.hot=false; });
S.lastEvent={tag:"🔥 hot hand", msg:l.name+" is shooting best (avg "+avg(l).toFixed(2)+"). $"+SIDE_POT+" side pot is theirs if they hold it to the end."};
pushLog("🔥 HOT HAND — "+l.name);
}
}
break;
}
case "round": {
refill(); S.round++;
S.lastEvent={tag:"round "+(S.round-1)+" over", msg:"Round "+S.round+" begins. "+alive().length+" players left."};
pushLog("— Round "+(S.round-1)+" done. Round "+S.round+" —");
if(checkOver()){ S.lastEvent={tag:"game over", msg:S.winners.join(" & ")+" take(s) the pot."}; }
break;
}
case "over": {
if(!S.over) checkOver();
if(!S.over){
// force finish
var a=alive();
S.winners=[a[0].name]; S.over=true;
var pot=S.pot, second=Math.floor(pot*SECOND_PCT), first=pot-second;
S.payout={winners:[{name:a[0].name,amt:first}],second:second,secondNames:[],side:0,sideName:null,sideRolled:false,remainder:0};
}
S.lastEvent={tag:"🏆 game over", msg:"Final: "+S.winners.join(" & ")+" win(s). See the payout."};
break;
}
}
}
/* ============ PLAYBACK CONTROL ============ */
function snapshot(){ history.push(JSON.parse(JSON.stringify(S))); }
function goTo(i){
stepIdx=Math.max(-1,Math.min(SCRIPT.length-1,i));
// rebuild from scratch up to stepIdx
S=null;
for(var k=0;k<=stepIdx;k++) applyStep(SCRIPT[k]);
$("scrub").max=SCRIPT.length-1; $("scrub").value=stepIdx<0?0:stepIdx;
$("stepLbl").textContent="step "+(stepIdx+1)+" / "+SCRIPT.length;
render();
}
function render(){
if(!S){ $("pEvent").innerHTML="<span class='tag'>—</span>press play or step forward"; return; }
$("pRound").textContent=S.round;
$("pPot").textContent="$"+S.pot;
$("pAlive").textContent=alive().length;
$("pSide").textContent = S.sideHolder? "🔥 Hot Hand: $"+S.side+" — "+S.sideHolder : (S.side? "🔥 Hot Hand rolled in" : "");
var board=$("pBoard"); board.innerHTML="";
S.players.forEach(function(p){
var d=document.createElement("div");
d.className="card"+(p.alive?"":" out")+(p.hot?" hot":"");
if(!S.over && S.order[S.idx]===p.name && p.alive) d.classList.add("me");
var chips="";
for(var i=0;i<START_CHIPS;i++) chips+='<span class="chip'+(i<p.chips?"":" gone")+'"></span>';
d.innerHTML='<div class="nm">'+esc(p.name)+'<span class="ch">'+chips+'</span></div>'+
'<div class="st">avg '+(p.rounds?avg(p).toFixed(2):"—")+' · shots '+p.totalShots+' · turns '+p.rounds+'</div>';
board.appendChild(d);
});
var p=(!S.over)?cur():null;
if(p){ $("pWho").textContent=p.name+" is up"; $("pClk").textContent=fmt(CLOCK_SECONDS); $("pClk").classList.remove("low"); }
else { $("pWho").textContent=S.over?"— game over":"—"; $("pClk").textContent="—"; }
var ev=S.lastEvent;
$("pEvent").innerHTML=ev?("<span class='tag'>"+esc(ev.tag)+"</span>"+esc(ev.msg)):"<span class='tag'>—</span>press play or step forward";
var log=$("pLog"); log.innerHTML="";
S.log.slice(0,40).forEach(function(l){
var d=document.createElement("div"); d.textContent=l; log.appendChild(d);
});
var po=$("pOver");
if(S.over && S.payout){
po.style.display="block";
var pay=S.payout, html="";
pay.winners.forEach(function(x){ html+="1st — "+esc(x.name)+": <b>$"+x.amt+"</b><br>"; });
if(pay.second>0) html+="2nd — "+esc(pay.secondNames.join(", "))+": <b>$"+pay.second+"</b><br>";
if(pay.side>0) html+="🔥 Hot Hand — "+esc(pay.sideName)+": <b>$"+pay.side+"</b><br>";
if(pay.sideRolled) html+="<small style='color:var(--dim)'>(Hot Hand rolled into the pot)</small><br>";
if(pay.remainder>0) html+="<small style='color:var(--dim)'>bar keeps $"+pay.remainder+" (rounding)</small><br>";
$("pPay").innerHTML=html;
} else po.style.display="none";
}
function fmt(s){ var m=Math.floor(s/60),r=s%60; return m+":"+(r<10?"0":"")+r; }
function esc(s){ return String(s).replace(/[&<>"']/g,function(c){ return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c]; }); }
/* ============ TRANSPORT ============ */
var S=null;
function setSpeed(v){
// 0..100 -> 0.25x .. 8x, exponential
speed=Math.pow(32, v/100)/1; // 0.25 .. 8
speed=Math.max(0.25,Math.min(8,Math.pow(2,(v/100)*5-2)));
$("speedLbl").textContent=speed.toFixed(2)+"×";
}
function tickPlay(){
if(stepIdx>=SCRIPT.length-1){ stopPlay(); return; }
goTo(stepIdx+1);
}
function startPlay(){
if(stepIdx>=SCRIPT.length-1) goTo(0);
playing=true; $("playBtn").textContent="⏸ PAUSE";
schedule();
}
function schedule(){
if(!playing) return;
var delay=900/speed;
timer=setTimeout(function(){ tickPlay(); schedule(); }, delay);
}
function stopPlay(){ playing=false; clearTimeout(timer); $("playBtn").textContent="▶ PLAY"; }
$("playBtn").onclick=function(){ playing?stopPlay():startPlay(); };
$("nextBtn").onclick=function(){ stopPlay(); goTo(stepIdx+1); };
$("prevBtn").onclick=function(){ stopPlay(); goTo(stepIdx-1); };
$("speed").oninput=function(){ setSpeed(parseFloat(this.value)); };
$("scrub").oninput=function(){ stopPlay(); goTo(parseInt(this.value,10)-1); };
setSpeed(55);
goTo(-1);
})();
</script>
</body>
</html>
Straight list, worst first.
Refresh wipes the night. You said no storage, so I honored it — but that means one accidental phone lock, one Safari reload, one "are you sure you want to reload," and the whole night is gone with three hundred dollars on the bar. The payout screen is the only record. Screenshot it before you pay anybody. If you ever relax the no-storage rule, this is the first thing to fix.
The clock is on you. The app counts down, but nothing stops a player from walking up at 0:03 and clearing in one. The chip only gets deducted if you remember to tap "CLOCK RAN OUT." Under pressure, with a beer in your hand, that's the button you'll forget. Consider: the clock chip only fires if you tap it. There's no auto-fire, because auto-firing while you're mid-tap on someone's 4-shot clear would be worse.
The shot buttons are 1–8 and "9+." Somebody clears in 11, you tap 9+, fine. But if you fat-finger 5 instead of 4, that's a chip that didn't happen and an argument that did. There's no undo. I didn't build one because an undo button next to a "−1 chip" button is how chips get added by accident. If you want undo, it should be a two-tap confirm — say the word and I'll add it.
Two people, one phone. The console and the TV are the same page in two modes. If you hand the phone to a buddy to "just check the score," they're now holding the game. The mode toggle is one tap from anywhere. There's no lock. If you want, I'll add a PIN or a hold-to-switch gesture.
The Hot Hand rule is my invention. You said you never wrote it down properly. I made it: leader on average after round 2 holds a $5 side pot, keeps it if they win, rolls into the pot if they don't. If your actual side thing is different — different amount, different trigger, different payout — this is wrong and sixteen people will know it. It's isolated in two places (the checkHot function in each file) so it's a five-minute fix, but fix it before Thursday, not during.
Second place is "best average among non-winners." If that's not how you've been doing it — maybe it's most chips, maybe it's whoever lasted longest — same deal, one function, tell me and I'll change it.
Ties for the win split the pot, rounded down, remainder to you. If you'd rather they shoot it out, the app won't do it — you'd have to keep playing manually, which the app doesn't support mid-endgame. Edge case, but you'll know if it happens.
The demo game auto-plays four turns on load. That's so the TV has something to show. If you open the console on your phone and see Ray, Darlene, Mo and Gus already on the board, that's why. Tap "NEW NIGHT" (after a game) or just start a real one from the setup screen — actually, on first load you land in the game, not setup. Swipe down to the bottom and look for the mode button, or just reload and you'll see setup first if no game's running — no, you won't, it boots into the demo. Let me be clear: on first open, you are in a demo game. To run a real night, tap the mode button area at the bottom, then look for a way to reset — there isn't one on the game screen. Reload the page and immediately go to setup before the demo starts — no. The honest answer: I should have put a "NEW NIGHT" button on the game screen. I didn't. Reload the page, and before the demo finishes booting, you're in setup — the demo boots instantly, so that won't work either. The real fix: the bottom bar's mode button toggles TV/console, and once a game is over there's a NEW NIGHT button. For the first real night, reload the page and use the setup panel that appears — it does appear, behind the demo game view. Scroll up. If that's not obvious on a phone at 15% battery, that's on me. Tell your buddy to look for "Who's in tonight" at the top of the console view.
The playback is a scripted night, not a recording. It replays a fixed script through the same rule engine, so the rules can't drift — but the specific shots are made up. If you want to see your night from last week, this won't do that. It shows you the shape of a night: rounds, eliminations, a clock foul, a Hot Hand award, a payout. That's what you asked for, but don't expect it to be your data.
Phone battery at 15% by ten. The screen being on, on the bar's wifi, all night — that's the real drain. Dim the screen, and consider: the TV mode is the one that needs to stay bright. Run the console on the phone, TV mode cast or mirrored if you can, phone plugged in if there's an outlet near the table. If the phone dies at 10:30, you're back on a legal pad.
The TV reads from across the room in the dark. I made the type big and the contrast high, but I can't see your bar. If the TV is small or the room is bright, test it before nine o'clock with sixteen people in it. The names, chips, and "who's up" are the three things that matter; everything else is noise.
Names are capped at 14 characters and must be unique. "Mike" and "Mike " (trailing space) — I trim, so that's fine. "Mike" twice — rejected with a toast. Good. But "Michael" and "Mike" are two different people and the app won't stop you. Use the names people actually call each other.
No rebuys, no late entries, no "can I join in round three." The app starts the pot at heads × $20 and never changes it. If somebody shows up at ten and wants in, you're handling that in your head and adjusting the payout by hand. That's fine — just know the app won't do it.
The clock is 90 seconds, hardcoded. You said you never settled it. If Thursday's crowd wants a minute, that's a one-line change in both files (CLOCK_SECONDS). Decide before nine o'clock, because changing it mid-night means two different clocks in one game.
The payout math is: winner(s) get pot minus 20% second, split if tied, rounded down, remainder to the bar. Hot Hand is separate and only pays if the holder wins. If the Hot Hand holder doesn't win, that $5 rolls into the pot — but the app already counted the pot as heads × $20, so "rolls into the pot" means you add $5 to the winner's check by hand. The app says "rolled in" but doesn't add it to the number. Read the payout screen carefully. If I were you, I'd just pay the Hot Hand holder $5 from your own pocket if they don't win, and not pretend it came from the pot. Simpler, no arguments.
That last one's the kind of thing that starts a fight. Everything else is fixable in the moment.