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,maximum-scale=1,user-scalable=no">
<title>Thursday 3-Ball — Console</title>
<style>
:root{--bg:#0d1013;--panel:#161b20;--line:#2a3138;--ink:#f2f5f7;--dim:#93a1ab;--felt:#2ea36b;--amber:#ffb000;--red:#ff5a4e;--chip:#ffd34d;}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
html,body{height:100%;}
body{background:var(--bg);color:var(--ink);font-family:-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;overflow:hidden;}
#app{display:flex;flex-direction:column;height:100%;}
header{display:flex;align-items:center;gap:2vmin;padding:1.2vmin 2vmin;border-bottom:1px solid var(--line);color:var(--dim);font-size:16px;font-size:clamp(12px,2.4vmin,22px);}
header b{color:var(--ink);}
.sp{flex:1;}
button{font:inherit;color:var(--ink);background:#232b33;border:1px solid var(--line);border-radius:1.2vmin;cursor:pointer;}
button:active{background:#313b45;}
.hbtn{padding:.8vmin 1.8vmin;font-weight:700;}
#demo{background:var(--amber);color:#20180a;text-align:center;font-weight:800;padding:.7vmin;display:none;font-size:14px;font-size:clamp(12px,2.4vmin,20px);}
#now{text-align:center;padding:.5vmin 2vmin 0;}
#nowLabel{color:var(--amber);letter-spacing:.35em;font-weight:700;font-size:14px;font-size:clamp(11px,2.4vmin,22px);}
#nowName{font-weight:800;line-height:1.02;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:60px;font-size:clamp(40px,11vmin,130px);}
#nowSub{color:var(--dim);font-size:16px;font-size:clamp(13px,2.8vmin,26px);}
#clock{font-weight:800;font-variant-numeric:tabular-nums;line-height:1.1;padding:.4vmin 0;font-size:54px;font-size:clamp(40px,9vmin,110px);}
#clock.ok{color:var(--felt);}
#clock.warn{color:var(--amber);}
#clock.exp{color:var(--red);animation:blink .5s steps(2,start) infinite;}
#clock.at{color:var(--felt);font-size:30px;font-size:clamp(22px,5vmin,60px);letter-spacing:.2em;}
@keyframes blink{50%{opacity:.2;}}
#board{flex:1;overflow-y:auto;padding:0 2vmin 1vmin;}
.row{display:flex;align-items:center;gap:1.6vmin;padding:.7vmin 1vmin;border-bottom:1px solid var(--line);font-size:20px;font-size:clamp(15px,3.4vmin,34px);}
.row.cur{background:#16241c;border-left:1.2vmin solid var(--felt);}
.row.out{color:#57616a;}
.row .nm{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:700;}
.row .dots{color:var(--chip);white-space:nowrap;}
.row.out .dots{color:#3c454d;}
.row .stat{color:var(--dim);font-size:.68em;white-space:nowrap;}
.row .badge{color:var(--red);font-weight:800;font-size:.7em;}
.star{color:var(--amber);}
#pad{border-top:1px solid var(--line);background:var(--panel);padding:1.4vmin 2vmin 2vmin;}
#pad .shots{display:flex;gap:1.4vmin;}
#pad .shots button{flex:1;padding:2.4vmin 0;font-weight:800;font-size:26px;font-size:clamp(20px,5vmin,46px);}
#pad .shots button.over{color:var(--amber);}
#pad .row2{display:flex;gap:1.4vmin;margin-top:1.4vmin;}
#pad .row2 button{flex:1;padding:1.8vmin 0;font-weight:700;font-size:18px;font-size:clamp(14px,3.2vmin,28px);}
#btnNo{color:var(--red);}
#pad.exp .shots{opacity:.3;pointer-events:none;}
#padNote{text-align:center;color:var(--red);font-weight:800;margin-top:1vmin;display:none;font-size:16px;font-size:clamp(13px,3vmin,24px);}
#pad.exp #padNote{display:block;animation:blink .5s steps(2,start) infinite;}
#toast{position:fixed;left:50%;bottom:26vmin;transform:translateX(-50%);background:rgba(0,0,0,.85);border:1px solid var(--line);padding:1.2vmin 3vmin;border-radius:2vmin;font-weight:700;max-width:92vw;text-align:center;display:none;z-index:40;font-size:18px;font-size:clamp(14px,3.4vmin,30px);}
.ov{position:fixed;inset:0;background:rgba(4,6,8,.88);display:none;z-index:50;overflow-y:auto;}
.ov.on{display:flex;align-items:flex-start;justify-content:center;}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:2vmin;margin:4vh 3vw;padding:3vmin;width:100%;max-width:740px;}
.panel h2{margin-bottom:1.5vmin;font-size:24px;font-size:clamp(18px,4vmin,34px);}
.panel .big{width:100%;padding:2.2vmin;margin-top:1.6vmin;font-weight:800;font-size:18px;font-size:clamp(16px,3.6vmin,30px);}
.panel .go{background:var(--felt);border-color:var(--felt);color:#06130c;}
.panel .danger{color:var(--red);}
.seg{display:flex;gap:1vmin;margin:.8vmin 0 1vmin;flex-wrap:wrap;}
.seg button{padding:1.4vmin 2.6vmin;font-weight:700;font-size:16px;font-size:clamp(14px,3vmin,24px);}
.seg button.sel{background:var(--felt);border-color:var(--felt);color:#06130c;}
.lbl{color:var(--dim);margin-top:1.6vmin;font-size:14px;font-size:clamp(12px,2.6vmin,20px);}
.rrow{display:flex;align-items:center;gap:2vmin;padding:1vmin;border-bottom:1px solid var(--line);font-size:18px;font-size:clamp(15px,3.2vmin,26px);}
.rrow .x{color:var(--red);font-weight:800;padding:.4vmin 2vmin;}
.rrow .nm{flex:1;}
.addrow{display:flex;gap:1.4vmin;margin-top:1.6vmin;}
.addrow input{flex:1;font:inherit;padding:1.6vmin;background:#0f1418;color:var(--ink);border:1px solid var(--line);border-radius:1.2vmin;font-size:16px;font-size:clamp(14px,3.2vmin,24px);}
#logList div{padding:1vmin;border-bottom:1px solid var(--line);color:var(--dim);font-size:16px;font-size:clamp(14px,3vmin,24px);}
#logList div b{color:var(--ink);}
#payBody>div{padding:1.4vmin 0;border-bottom:1px solid var(--line);font-weight:700;font-size:22px;font-size:clamp(18px,4.4vmin,40px);}
#payBody .amt{float:right;color:var(--chip);}
#payBody .sub{color:var(--dim);font-weight:400;font-size:.55em;}
#backupBox{width:100%;height:18vh;background:#0f1418;color:var(--ink);border:1px solid var(--line);border-radius:1.2vmin;font:12px/1.4 monospace;padding:1.5vmin;}
body.tv #pad,body.tv #demo,body.tv .notv{display:none;}
body.tv #nowName{font-size:clamp(48px,14vmin,170px);}
body.tv .row{font-size:clamp(18px,4.4vmin,46px);}
</style>
</head>
<body>
<div id="app">
<header>
<b>3-BALL</b><span id="hRound">Round 1</span><span id="hPot"></span><span id="hAlive"></span>
<span class="sp"></span>
<button class="hbtn notv" id="btnLog">Log</button>
<button class="hbtn notv" id="btnMenu">Menu</button>
<button class="hbtn" id="btnTV">TV</button>
</header>
<div id="demo">DEMO NIGHT — Menu → New night when it's the real thing</div>
<section id="now">
<div id="nowLabel">NOW SHOOTING</div>
<div id="nowName">—</div>
<div id="nowSub"></div>
<div id="clock"></div>
</section>
<section id="board"></section>
<div id="pad">
<div class="shots" id="shotBtns"></div>
<div class="row2">
<button id="btnHere">He's here</button>
<button id="btnNo">No-show −1</button>
<button id="btnUndo">Undo</button>
</div>
<div id="padNote">TIME! No-show −1, or tap “He's here”</div>
</div>
</div>
<div id="toast"></div>
<div class="ov" id="ovMenu"><div class="panel">
<h2>Menu</h2>
<button class="big" id="mNew">New night</button>
<button class="big" id="mPay">Payouts</button>
<button class="big" id="mSet">Settings</button>
<button class="big" id="mBak">Backup / restore</button>
<button class="big" id="mUndo">Undo last</button>
<button class="big danger" id="mEnd">Call the night</button>
<button class="big" id="mClose">Close</button>
</div></div>
<div class="ov" id="ovRoster"><div class="panel">
<h2>New night</h2>
<div id="rosterList"></div>
<div class="addrow"><input id="newName" placeholder="Add a player…" maxlength="18"><button id="btnAdd">Add</button></div>
<button class="big" id="btnDemoRoster">Load demo roster</button>
<div class="lbl">Buy-in</div><div class="seg" id="segBuy"></div>
<div class="lbl">Clock (seconds to reach the table)</div><div class="seg" id="segClock"></div>
<div class="lbl">Hot-start prize (off the top, best of rounds 1–2)</div><div class="seg" id="segHot"></div>
<div class="lbl">Split (winner / second)</div><div class="seg" id="segSplit"></div>
<button class="big go" id="btnStart">Start night</button>
<button class="big" id="btnRosterClose">Close</button>
</div></div>
<div class="ov" id="ovSet"><div class="panel">
<h2>Settings</h2>
<div class="lbl">Clock (applies from the next turn)</div><div class="seg" id="segClock2"></div>
<div class="lbl">Split (winner / second)</div><div class="seg" id="segSplit2"></div>
<div class="lbl">Buy-in and hot-start are locked once the night starts.</div>
<button class="big" id="btnSetClose">Close</button>
</div></div>
<div class="ov" id="ovLog"><div class="panel">
<h2>Log</h2>
<div id="logList"></div>
<button class="big" id="btnLogClose">Close</button>
</div></div>
<div class="ov" id="ovPay"><div class="panel">
<h2 id="payTitle">PAYOUTS</h2>
<div id="payBody"></div>
<button class="big" id="btnPayClose">Close</button>
</div></div>
<div class="ov" id="ovBak"><div class="panel">
<h2>Backup / restore</h2>
<div class="lbl">No storage allowed in here, so this is manual: tap Fill, then select-all and copy (or photograph it). To restore, paste and hit Restore.</div>
<textarea id="backupBox"></textarea>
<button class="big" id="btnBakFill">Fill from current game</button>
<button class="big" id="btnBakRestore">Restore from box</button>
<button class="big" id="btnBakClose">Close</button>
</div></div>
<div class="ov" id="ovConfirm"><div class="panel">
<h2>Call the night?</h2>
<div class="lbl">Pays out on current standings: most chips wins, ties split. This ends the game.</div>
<button class="big danger" id="btnCallIt">Call it</button>
<button class="big" id="btnKeep">Keep playing</button>
</div></div>
<script>
/* ============================================================
SHARED RULES ENGINE — Thursday 3-ball.
This block is IDENTICAL in console.html and playback.html.
Rules only: no UI, no randomness, no timers. Edit both copies.
============================================================ */
function createNight(names,opts){
opts=opts||{};
var st={names:names.slice(),chips:[],shots:[],turns:[],hist:[],out:[],outSeq:[],
buyIn:opts.buyIn!=null?opts.buyIn:20,
startChips:opts.startChips!=null?opts.startChips:5,
shotLimit:opts.shotLimit!=null?opts.shotLimit:4,
clockSecs:opts.clockSecs!=null?opts.clockSecs:75,
hotAmount:opts.hotAmount!=null?opts.hotAmount:20,
hotAfterRound:2,
splitFirst:opts.splitFirst!=null?opts.splitFirst:0.8,
round:1,turnIdx:0,hot:null,finished:false,calledEarly:false,winner:-1,second:-1};
for(var i=0;i<names.length;i++){st.chips.push(st.startChips);st.shots.push(0);st.turns.push(0);st.hist.push([]);st.out.push(false);}
return st;
}
function nm(st,i){return st.names[i];}
function alive(st){var n=0;for(var i=0;i<st.names.length;i++)if(!st.out[i])n++;return n;}
function cur(st){var n=st.names.length,i=st.turnIdx%n,g=0;while(st.out[i]&&g<n){i=(i+1)%n;g++;}return i;}
function eliminate(st,i,ev){st.out[i]=true;st.outSeq.push(i);ev.push({t:"elim",p:i,text:nm(st,i)+" is out of chips — OUT"});}
function recordShots(st,n){
var ev=[],i=cur(st);
st.turns[i]++;st.shots[i]+=n;st.hist[i].push({round:st.round,shots:n});
if(n>st.shotLimit){
st.chips[i]--;
ev.push({t:"shots",p:i,n:n,chip:true,text:nm(st,i)+" clears in "+n+" — over "+st.shotLimit+", loses a chip ("+st.chips[i]+" left)"});
if(st.chips[i]<=0)eliminate(st,i,ev);
}else{
ev.push({t:"shots",p:i,n:n,chip:false,text:nm(st,i)+" clears in "+n+" — clean, still on "+st.chips[i]+" chip"+(st.chips[i]===1?"":"s")});
}
finishTurn(st,ev);return ev;
}
function recordNoShow(st){
var ev=[],i=cur(st);
st.turns[i]++;st.hist[i].push({round:st.round,shots:null});st.chips[i]--;
ev.push({t:"noshow",p:i,text:"Clock runs out and "+nm(st,i)+" isn't at the table — loses a chip ("+st.chips[i]+" left)"});
if(st.chips[i]<=0)eliminate(st,i,ev);
finishTurn(st,ev);return ev;
}
function finishTurn(st,ev){
if(alive(st)<=1){
st.finished=true;st.winner=cur(st);
st.second=st.outSeq.length?st.outSeq[st.outSeq.length-1]:-1;
ev.push({t:"end",p:st.winner,text:"That's the night — "+nm(st,st.winner)+" is the last one with chips"});
return;
}
var n=st.names.length,prev=st.turnIdx,i=prev,g=0;
do{i=(i+1)%n;g++;}while(st.out[i]&&g<=n);
var wrapped=i<=prev;st.turnIdx=i;
if(wrapped){st.round++;ev.push({t:"round",n:st.round,text:"— Round "+st.round+" —"});maybeHot(st,ev);}
ev.push({t:"turn",p:cur(st),round:st.round,text:nm(st,cur(st))+"'s turn — clock starts"});
}
function maybeHot(st,ev){
if(st.hot||st.hotAmount<=0||st.round<st.hotAfterRound+1)return;
var best=null,bestVal=1e9,i,r,k,rec,s,ok,h;
for(i=0;i<st.names.length;i++){
h=st.hist[i];s=0;ok=true;
for(r=1;r<=st.hotAfterRound;r++){
rec=null;for(k=0;k<h.length;k++)if(h[k].round===r){rec=h[k];break;}
if(!rec||rec.shots==null){ok=false;break;}
s+=rec.shots;
}
if(ok){if(s<bestVal){bestVal=s;best=[i];}else if(s===bestVal)best.push(i);}
}
if(best&&best.length){
var each=Math.floor(st.hotAmount/best.length);
st.hot={winners:best,each:each,val:bestVal};
var who=[];for(k=0;k<best.length;k++)who.push(nm(st,best[k]));
ev.push({t:"hot",winners:best,each:each,text:"Hot start: "+who.join(" & ")+" — "+bestVal+" shots over two rounds, $"+each+(best.length>1?" each":"")+" off the top"});
}
}
function standings(st){
var a=[],i;
for(i=0;i<st.names.length;i++)a.push(i);
var pos={};for(i=0;i<st.outSeq.length;i++)pos[st.outSeq[i]]=i;
a.sort(function(x,y){
if(st.out[x]!==st.out[y])return st.out[x]?1:-1;
if(st.out[x]&&st.out[y])return(pos[y]||0)-(pos[x]||0);
if(st.chips[y]!==st.chips[x])return st.chips[y]-st.chips[x];
if(st.shots[x]!==st.shots[y])return st.shots[x]-st.shots[y];
return x-y;
});
return a;
}
function endEarly(st){
var ev=[],ord=standings(st),i,aliveOrd=[];
for(i=0;i<ord.length;i++)if(!st.out[ord[i]])aliveOrd.push(ord[i]);
st.calledEarly=true;st.finished=true;
st.winner=aliveOrd.length?aliveOrd[0]:-1;
st.second=aliveOrd.length>1?aliveOrd[1]:-1;
ev.push({t:"end",p:st.winner,text:"Night called early — paid on standings"});
return ev;
}
function payouts(st){
var pot=st.names.length*st.buyIn,i,ord,g1,g2;
var hotTotal=st.hot?st.hot.each*st.hot.winners.length:0;
var rem=pot-hotTotal;
var secondShare=Math.floor(rem*(1-st.splitFirst));
var firstShare=rem-secondShare;
var res={pot:pot,hot:st.hot,hotTotal:hotTotal,first:null,second:null,note:""};
function keyOf(j){return st.chips[j]+"/"+st.shots[j];}
function grp(list,idx){var g=[list[idx]],k=keyOf(list[idx]),j=idx+1;while(j<list.length&&keyOf(list[j])===k){g.push(list[j]);j++;}return g;}
if(st.finished&&!st.calledEarly){
res.first={ps:[st.winner],each:firstShare};
if(st.second>=0)res.second={ps:[st.second],each:secondShare};
return res;
}
ord=[];var so=standings(st);for(i=0;i<so.length;i++)if(!st.out[so[i]])ord.push(so[i]);
if(!ord.length){res.note="Nobody left standing.";return res;}
g1=grp(ord,0);
if(g1.length>1){
res.first={ps:g1,each:Math.floor((firstShare+secondShare)/g1.length)};
res.note="Tie at the top — top two prizes combined and split.";
return res;
}
res.first={ps:g1,each:firstShare};
if(ord.length>g1.length){
g2=grp(ord,g1.length);
res.second={ps:g2,each:Math.floor(secondShare/g2.length)};
if(g2.length>1)res.note="Tie for second — that prize is split.";
}
return res;
}
/* ================= end SHARED RULES ENGINE ================= */
/* ---- console-only below ---- */
var S=null,LOG=[],UNDO=[],clock={left:0,mode:"off"};
var DEMO_NAMES=["Dave","Maya","Big Rob","Suzie","Tom","Kev","Dana","Pete","Lou","Frankie","Jess","Marco"];
var TMP={roster:[],buyIn:20,clockSecs:75,hotAmount:20,splitFirst:0.8};
function $(i){return document.getElementById(i);}
function esc(s){return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");}
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
function rollShots(r,skill){var p=r()-skill*0.18;if(p<0.10)return 3;if(p<0.45)return 4;if(p<0.73)return 5;if(p<0.90)return 6;if(p<0.97)return 7;return 8;}
function dots(n,m){var s="";for(var i=0;i<m;i++)s+=i<n?"\u25CF":"\u25CB";return s;}
function shotTurns(st,i){var h=st.hist[i],n=0;for(var k=0;k<h.length;k++)if(h[k].shots!=null)n++;return n;}
function namesOf(st,ps){var a=[];for(var k=0;k<ps.length;k++)a.push(nm(st,ps[k]));return a.join(" & ");}
function pushUndo(){UNDO.push({s:JSON.stringify(S),log:LOG.length});if(UNDO.length>150)UNDO.shift();}
function logEv(ev){for(var k=0;k<ev.length;k++)LOG.push({r:S.round,text:ev[k].text});}
function toast(t){var e=$("toast");e.textContent=t;e.style.display="block";clearTimeout(e._t);e._t=setTimeout(function(){e.style.display="none";},2600);}
function pickNote(ev){for(var k=0;k<ev.length;k++){var t=ev[k].t;if(t==="end"||t==="elim"||t==="hot")return ev[k].text;}return ev.length?ev[0].text:"";}
function demoNight(){
var st=createNight(DEMO_NAMES,{});
var rng=mulberry32(20240912),skills=[],i;
for(i=0;i<DEMO_NAMES.length;i++)skills.push(rng()*2-1);
LOG.push({r:1,text:"Doors — 12 in at $20, pot $240. (Demo night.)"});
LOG.push({r:1,text:nm(st,0)+" up first."});
var g=0;
while(g++<200){
if(st.round>2||(st.round===2&&cur(st)>=5))break;
var p=cur(st),ev;
if(rng()<0.05)ev=recordNoShow(st);else ev=recordShots(st,rollShots(rng,skills[p]));
logEv(ev);
}
return st;
}
function render(){
$("hRound").textContent="Round "+S.round;
$("hPot").textContent="Pot $"+(S.names.length*S.buyIn);
$("hAlive").textContent=alive(S)+" of "+S.names.length+" in";
var i=S.finished?-1:cur(S);
if(S.finished){
$("nowLabel").textContent=S.calledEarly?"CALLED EARLY":"NIGHT OVER";
$("nowName").textContent=S.winner>=0?nm(S,S.winner):"—";
$("nowSub").textContent=S.winner>=0?"last one with chips":"";
clock.mode="off";
}else{
$("nowLabel").textContent="NOW SHOOTING";
$("nowName").textContent=nm(S,i);
$("nowSub").textContent=dots(S.chips[i],S.startChips)+" · "+S.shots[i]+" shots over "+S.turns[i]+" turns";
}
renderClock();renderBoard();renderPad();
}
function renderClock(){
var c=$("clock");c.className="";
if(!S||S.finished||clock.mode==="off"){c.textContent="";return;}
if(clock.mode==="attable"){c.textContent="AT THE TABLE";c.className="at";return;}
if(clock.mode==="expired"){c.textContent="TIME!";c.className="exp";return;}
var s=Math.max(0,Math.ceil(clock.left)),m=Math.floor(s/60),ss=s%60;
c.textContent=m+":"+(ss<10?"0":"")+ss;
c.className=s<=15?"warn":"ok";
}
function renderBoard(){
var ord=standings(S),h="",ci=S.finished?-1:cur(S);
for(var k=0;k<ord.length;k++){
var i=ord[k],cls="row"+(i===ci?" cur":"")+(S.out[i]?" out":"");
h+='<div class="'+cls+'">';
h+='<span class="nm">'+(i===ci?"\u25B6 ":"")+esc(nm(S,i))+(S.hot&&S.hot.winners.indexOf(i)>=0?' <span class="star">\u2605</span>':"")+"</span>";
h+='<span class="dots">'+dots(S.chips[i],S.startChips)+"</span>";
if(S.out[i])h+='<span class="badge">OUT</span>';
else{var st2=shotTurns(S,i);h+='<span class="stat">'+S.shots[i]+" sh · avg "+(st2?(S.shots[i]/st2).toFixed(1):"—")+"</span>";}
h+="</div>";
}
$("board").innerHTML=h;
}
function renderPad(){
var pad=$("pad");
pad.className=(!S.finished&&clock.mode==="expired")?"exp":"";
pad.style.display=S.finished?"none":"";
$("btnUndo").textContent="Undo"+(UNDO.length?" ("+UNDO.length+")":"");
}
function startTurn(){clock.left=S.clockSecs;clock.mode="count";render();}
function doShot(n){
if(S.finished||clock.mode==="expired")return;
pushUndo();var ev=recordShots(S,n);logEv(ev);clock.mode="off";
toast(pickNote(ev));
if(S.finished){render();showPayout();}else startTurn();
}
function doNoShow(){
if(S.finished)return;
pushUndo();var ev=recordNoShow(S);logEv(ev);clock.mode="off";
toast(pickNote(ev));
if(S.finished){render();showPayout();}else startTurn();
}
function doHere(){if(clock.mode==="count"||clock.mode==="expired"){clock.mode="attable";render();}}
function doUndo(){
if(!UNDO.length){toast("Nothing to undo.");return;}
var u=UNDO.pop();S=JSON.parse(u.s);LOG.length=u.log;
if(S.finished){clock.mode="off";render();}else startTurn();
toast("Undone.");
}
setInterval(function(){
if(!S||S.finished||clock.mode!=="count")return;
clock.left-=0.25;
if(clock.left<=0){clock.left=0;clock.mode="expired";renderPad();}
renderClock();
},250);
function open(id){$(id).classList.add("on");}
function closeAll(){var o=document.querySelectorAll(".ov");for(var k=0;k<o.length;k++)o[k].classList.remove("on");}
function showPayout(){
var P=payouts(S),h="";
$("payTitle").textContent=S.finished?"FINAL PAYOUTS":"IF CALLED NOW — PROJECTION";
h+="<div>Pot <span class=\"amt\">$"+P.pot+"</span></div>";
if(P.hot)h+="<div>Hot start — "+esc(namesOf(S,P.hot.winners))+" <span class=\"amt\">$"+P.hot.each+(P.hot.winners.length>1?" ea":"")+"</span><div class=\"sub\">best shooting over rounds 1–2, off the top</div></div>";
else h+="<div><span class=\"sub\">Hot start "+(S.finished?"not awarded (ended before round 3)":"not decided yet — $"+S.hotAmount+" after round 2")+"</span></div>";
if(P.first)h+="<div>1st — "+esc(namesOf(S,P.first.ps))+" <span class=\"amt\">$"+P.first.each+(P.first.ps.length>1?" ea":"")+"</span></div>";
if(P.second)h+="<div>2nd — "+esc(namesOf(S,P.second.ps))+" <span class=\"amt\">$"+P.second.each+(P.second.ps.length>1?" ea":"")+"</span></div>";
if(P.note)h+="<div><span class=\"sub\">"+esc(P.note)+"</span></div>";
$("payBody").innerHTML=h;
open("ovPay");
}
function seg(elId,options,get,set){
var el=$(elId);el.innerHTML="";
for(var k=0;k<options.length;k++){(function(o){
var b=document.createElement("button");b.textContent=o.l;
if(get()===o.v)b.className="sel";
b.onclick=function(){set(o.v);seg(elId,options,get,set);};
el.appendChild(b);
})(options[k]);}
}
var OPT_BUY=[{v:10,l:"$10"},{v:20,l:"$20"},{v:25,l:"$25"}];
var OPT_CLK=[{v:60,l:"60s"},{v:75,l:"75s"},{v:90,l:"90s"}];
var OPT_HOT=[{v:0,l:"Off"},{v:10,l:"$10"},{v:20,l:"$20"}];
var OPT_SPL=[{v:0.9,l:"90/10"},{v:0.8,l:"80/20"},{v:0.75,l:"75/25"}];
function wireRosterSegs(){
seg("segBuy",OPT_BUY,function(){return TMP.buyIn;},function(v){TMP.buyIn=v;});
seg("segClock",OPT_CLK,function(){return TMP.clockSecs;},function(v){TMP.clockSecs=v;});
seg("segHot",OPT_HOT,function(){return TMP.hotAmount;},function(v){TMP.hotAmount=v;});
seg("segSplit",OPT_SPL,function(){return TMP.splitFirst;},function(v){TMP.splitFirst=v;});
}
function wireSetSegs(){
seg("segClock2",OPT_CLK,function(){return S.clockSecs;},function(v){S.clockSecs=v;if(clock.mode==="count"&&clock.left>v)clock.left=v;renderClock();});
seg("segSplit2",OPT_SPL,function(){return S.splitFirst;},function(v){S.splitFirst=v;});
}
function renderRoster(){
var h="";
for(var k=0;k<TMP.roster.length;k++)h+='<div class="rrow"><span class="nm">'+esc(TMP.roster[k])+'</span><button class="x" data-k="'+k+'">✕</button></div>';
$("rosterList").innerHTML=h||'<div class="lbl">Nobody yet.</div>';
var xs=$("rosterList").querySelectorAll(".x");
for(var j=0;j<xs.length;j++)xs[j].onclick=function(){TMP.roster.splice(+this.getAttribute("data-k"),1);renderRoster();};
}
function openRoster(){TMP.roster=S?S.names.slice():DEMO_NAMES.slice();wireRosterSegs();renderRoster();open("ovRoster");}
function startNight(){
if(TMP.roster.length<2){toast("Need at least 2 players.");return;}
S=createNight(TMP.roster,TMP);LOG=[];UNDO=[];
LOG.push({r:1,text:"Doors — "+S.names.length+" in at $"+S.buyIn+", pot $"+(S.names.length*S.buyIn)+". Five chips each."});
LOG.push({r:1,text:nm(S,0)+" up first."});
$("demo").style.display="none";
closeAll();startTurn();
}
(function init(){
var h="";
for(var n=3;n<=7;n++)h+='<button data-n="'+n+'"'+(n>4?' class="over"':"")+">"+n+"</button>";
h+='<button data-n="8" class="over">8+</button>';
$("shotBtns").innerHTML=h;
var bs=$("shotBtns").children;
for(var k=0;k<bs.length;k++)bs[k].onclick=function(){doShot(+this.getAttribute("data-n"));};
$("btnHere").onclick=doHere;
$("btnNo").onclick=doNoShow;
$("btnUndo").onclick=doUndo;
$("btnTV").onclick=function(){document.body.classList.toggle("tv");};
$("demo").onclick=function(){$("demo").style.display="none";};
$("btnLog").onclick=function(){
var h2="";for(var j=LOG.length-1;j>=0;j--)h2+="<div><b>R"+LOG[j].r+"</b> — "+esc(LOG[j].text)+"</div>";
$("logList").innerHTML=h2||"<div>Nothing yet.</div>";open("ovLog");
};
$("btnLogClose").onclick=closeAll;
$("btnMenu").onclick=function(){open("ovMenu");};
$("mClose").onclick=closeAll;
$("mNew").onclick=function(){closeAll();openRoster();};
$("mPay").onclick=function(){closeAll();showPayout();};
$("mSet").onclick=function(){closeAll();wireSetSegs();open("ovSet");};
$("mBak").onclick=function(){closeAll();open("ovBak");};
$("mUndo").onclick=function(){closeAll();doUndo();};
$("mEnd").onclick=function(){closeAll();open("ovConfirm");};
$("btnSetClose").onclick=closeAll;
$("btnPayClose").onclick=closeAll;
$("btnRosterClose").onclick=closeAll;
$("btnDemoRoster").onclick=function(){TMP.roster=DEMO_NAMES.slice();renderRoster();};
$("btnAdd").onclick=function(){
var v=$("newName").value.replace(/^\s+|\s+$/g,"");
if(v){TMP.roster.push(v);$("newName").value="";renderRoster();}
};
$("newName").onkeydown=function(e){if(e.key==="Enter")$("btnAdd").click();};
$("btnStart").onclick=startNight;
$("btnCallIt").onclick=function(){
if(S.finished){closeAll();return;}
pushUndo();var ev=endEarly(S);logEv(ev);clock.mode="off";
closeAll();render();showPayout();
};
$("btnKeep").onclick=closeAll;
$("btnBakFill").onclick=function(){$("backupBox").value=JSON.stringify(S);$("backupBox").select();};
$("btnBakRestore").onclick=function(){
try{
var o=JSON.parse($("backupBox").value);
if(!o||!o.names||!o.chips)throw 0;
pushUndo();S=o;LOG.push({r:S.round,text:"Restored from backup."});
$("demo").style.display="none";closeAll();
if(S.finished){clock.mode="off";render();}else startTurn();
toast("Restored.");
}catch(e){toast("That backup doesn't parse.");}
};
window.onbeforeunload=function(){if(S&&!S.finished)return "Night in progress — leave?";};
S=demoNight();
$("demo").style.display="block";
startTurn();
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Thursday 3-Ball — Playback</title>
<style>
:root{--bg:#0d1013;--panel:#161b20;--line:#2a3138;--ink:#f2f5f7;--dim:#93a1ab;--felt:#2ea36b;--amber:#ffb000;--red:#ff5a4e;--chip:#ffd34d;}
*{box-sizing:border-box;margin:0;padding:0;}
html,body{height:100%;}
body{background:var(--bg);color:var(--ink);font-family:-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;display:flex;flex-direction:column;height:100%;overflow:hidden;}
header{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line);color:var(--dim);flex-wrap:wrap;}
header b{color:var(--ink);}
.sp{flex:1;}
button{font:inherit;color:var(--ink);background:#232b33;border:1px solid var(--line);border-radius:8px;cursor:pointer;padding:8px 14px;font-weight:700;}
button:active{background:#313b45;}
input[type=text]{font:inherit;background:#0f1418;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;width:110px;}
#banner{padding:14px 16px;font-weight:800;border-bottom:1px solid var(--line);background:var(--panel);min-height:2.4em;font-size:22px;font-size:clamp(16px,3.6vmin,34px);}
#meta{color:var(--dim);padding:6px 16px;font-size:14px;}
#wrap{flex:1;display:flex;min-height:0;}
#board{flex:1.2;overflow-y:auto;padding:0 10px 10px;border-right:1px solid var(--line);}
#log{flex:1;overflow-y:auto;padding:0 10px 10px;}
.row{display:flex;align-items:center;gap:10px;padding:6px 8px;border-bottom:1px solid var(--line);font-size:17px;}
.row.cur{background:#16241c;border-left:6px solid var(--felt);}
.row.out{color:#57616a;}
.row .nm{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:700;}
.row .dots{color:var(--chip);white-space:nowrap;}
.row.out .dots{color:#3c454d;}
.row .stat{color:var(--dim);font-size:.72em;white-space:nowrap;}
.row .badge{color:var(--red);font-weight:800;font-size:.72em;}
.star{color:var(--amber);}
.lr{padding:5px 8px;border-bottom:1px solid var(--line);color:#4c565f;cursor:pointer;font-size:14px;}
.lr.done{color:var(--dim);}
.lr.now{background:#16241c;color:var(--ink);font-weight:700;}
#controls{display:flex;align-items:center;gap:10px;padding:10px 14px;border-top:1px solid var(--line);background:var(--panel);flex-wrap:wrap;}
#controls input[type=range]{flex:1;min-width:120px;}
#spdLab{color:var(--dim);min-width:70px;}
@media (max-width:700px){#wrap{flex-direction:column;}#board{border-right:none;border-bottom:1px solid var(--line);}}
</style>
</head>
<body>
<header>
<b>PLAYBACK</b><span>a simulated Thursday</span>
<span class="sp"></span>
<span>seed</span><input type="text" id="seedIn" value="42">
<button id="btnRun">Run</button>
<button id="btnNew">New random</button>
</header>
<div id="banner"> </div>
<div id="meta"></div>
<div id="wrap">
<section id="board"></section>
<section id="log"></section>
</div>
<div id="controls">
<button id="btnFirst">First</button>
<button id="btnBack">Back</button>
<button id="btnPlay">Play</button>
<button id="btnFwd">Next</button>
<span>slow</span><input type="range" id="speed" min="1" max="10" value="4"><span>fast</span><span id="spdLab"></span>
</div>
<script>
/* ============================================================
SHARED RULES ENGINE — Thursday 3-ball.
This block is IDENTICAL in console.html and playback.html.
Rules only: no UI, no randomness, no timers. Edit both copies.
============================================================ */
function createNight(names,opts){
opts=opts||{};
var st={names:names.slice(),chips:[],shots:[],turns:[],hist:[],out:[],outSeq:[],
buyIn:opts.buyIn!=null?opts.buyIn:20,
startChips:opts.startChips!=null?opts.startChips:5,
shotLimit:opts.shotLimit!=null?opts.shotLimit:4,
clockSecs:opts.clockSecs!=null?opts.clockSecs:75,
hotAmount:opts.hotAmount!=null?opts.hotAmount:20,
hotAfterRound:2,
splitFirst:opts.splitFirst!=null?opts.splitFirst:0.8,
round:1,turnIdx:0,hot:null,finished:false,calledEarly:false,winner:-1,second:-1};
for(var i=0;i<names.length;i++){st.chips.push(st.startChips);st.shots.push(0);st.turns.push(0);st.hist.push([]);st.out.push(false);}
return st;
}
function nm(st,i){return st.names[i];}
function alive(st){var n=0;for(var i=0;i<st.names.length;i++)if(!st.out[i])n++;return n;}
function cur(st){var n=st.names.length,i=st.turnIdx%n,g=0;while(st.out[i]&&g<n){i=(i+1)%n;g++;}return i;}
function eliminate(st,i,ev){st.out[i]=true;st.outSeq.push(i);ev.push({t:"elim",p:i,text:nm(st,i)+" is out of chips — OUT"});}
function recordShots(st,n){
var ev=[],i=cur(st);
st.turns[i]++;st.shots[i]+=n;st.hist[i].push({round:st.round,shots:n});
if(n>st.shotLimit){
st.chips[i]--;
ev.push({t:"shots",p:i,n:n,chip:true,text:nm(st,i)+" clears in "+n+" — over "+st.shotLimit+", loses a chip ("+st.chips[i]+" left)"});
if(st.chips[i]<=0)eliminate(st,i,ev);
}else{
ev.push({t:"shots",p:i,n:n,chip:false,text:nm(st,i)+" clears in "+n+" — clean, still on "+st.chips[i]+" chip"+(st.chips[i]===1?"":"s")});
}
finishTurn(st,ev);return ev;
}
function recordNoShow(st){
var ev=[],i=cur(st);
st.turns[i]++;st.hist[i].push({round:st.round,shots:null});st.chips[i]--;
ev.push({t:"noshow",p:i,text:"Clock runs out and "+nm(st,i)+" isn't at the table — loses a chip ("+st.chips[i]+" left)"});
if(st.chips[i]<=0)eliminate(st,i,ev);
finishTurn(st,ev);return ev;
}
function finishTurn(st,ev){
if(alive(st)<=1){
st.finished=true;st.winner=cur(st);
st.second=st.outSeq.length?st.outSeq[st.outSeq.length-1]:-1;
ev.push({t:"end",p:st.winner,text:"That's the night — "+nm(st,st.winner)+" is the last one with chips"});
return;
}
var n=st.names.length,prev=st.turnIdx,i=prev,g=0;
do{i=(i+1)%n;g++;}while(st.out[i]&&g<=n);
var wrapped=i<=prev;st.turnIdx=i;
if(wrapped){st.round++;ev.push({t:"round",n:st.round,text:"— Round "+st.round+" —"});maybeHot(st,ev);}
ev.push({t:"turn",p:cur(st),round:st.round,text:nm(st,cur(st))+"'s turn — clock starts"});
}
function maybeHot(st,ev){
if(st.hot||st.hotAmount<=0||st.round<st.hotAfterRound+1)return;
var best=null,bestVal=1e9,i,r,k,rec,s,ok,h;
for(i=0;i<st.names.length;i++){
h=st.hist[i];s=0;ok=true;
for(r=1;r<=st.hotAfterRound;r++){
rec=null;for(k=0;k<h.length;k++)if(h[k].round===r){rec=h[k];break;}
if(!rec||rec.shots==null){ok=false;break;}
s+=rec.shots;
}
if(ok){if(s<bestVal){bestVal=s;best=[i];}else if(s===bestVal)best.push(i);}
}
if(best&&best.length){
var each=Math.floor(st.hotAmount/best.length);
st.hot={winners:best,each:each,val:bestVal};
var who=[];for(k=0;k<best.length;k++)who.push(nm(st,best[k]));
ev.push({t:"hot",winners:best,each:each,text:"Hot start: "+who.join(" & ")+" — "+bestVal+" shots over two rounds, $"+each+(best.length>1?" each":"")+" off the top"});
}
}
function standings(st){
var a=[],i;
for(i=0;i<st.names.length;i++)a.push(i);
var pos={};for(i=0;i<st.outSeq.length;i++)pos[st.outSeq[i]]=i;
a.sort(function(x,y){
if(st.out[x]!==st.out[y])return st.out[x]?1:-1;
if(st.out[x]&&st.out[y])return(pos[y]||0)-(pos[x]||0);
if(st.chips[y]!==st.chips[x])return st.chips[y]-st.chips[x];
if(st.shots[x]!==st.shots[y])return st.shots[x]-st.shots[y];
return x-y;
});
return a;
}
function endEarly(st){
var ev=[],ord=standings(st),i,aliveOrd=[];
for(i=0;i<ord.length;i++)if(!st.out[ord[i]])aliveOrd.push(ord[i]);
st.calledEarly=true;st.finished=true;
st.winner=aliveOrd.length?aliveOrd[0]:-1;
st.second=aliveOrd.length>1?aliveOrd[1]:-1;
ev.push({t:"end",p:st.winner,text:"Night called early — paid on standings"});
return ev;
}
function payouts(st){
var pot=st.names.length*st.buyIn,i,ord,g1,g2;
var hotTotal=st.hot?st.hot.each*st.hot.winners.length:0;
var rem=pot-hotTotal;
var secondShare=Math.floor(rem*(1-st.splitFirst));
var firstShare=rem-secondShare;
var res={pot:pot,hot:st.hot,hotTotal:hotTotal,first:null,second:null,note:""};
function keyOf(j){return st.chips[j]+"/"+st.shots[j];}
function grp(list,idx){var g=[list[idx]],k=keyOf(list[idx]),j=idx+1;while(j<list.length&&keyOf(list[j])===k){g.push(list[j]);j++;}return g;}
if(st.finished&&!st.calledEarly){
res.first={ps:[st.winner],each:firstShare};
if(st.second>=0)res.second={ps:[st.second],each:secondShare};
return res;
}
ord=[];var so=standings(st);for(i=0;i<so.length;i++)if(!st.out[so[i]])ord.push(so[i]);
if(!ord.length){res.note="Nobody left standing.";return res;}
g1=grp(ord,0);
if(g1.length>1){
res.first={ps:g1,each:Math.floor((firstShare+secondShare)/g1.length)};
res.note="Tie at the top — top two prizes combined and split.";
return res;
}
res.first={ps:g1,each:firstShare};
if(ord.length>g1.length){
g2=grp(ord,g1.length);
res.second={ps:g2,each:Math.floor(secondShare/g2.length)};
if(g2.length>1)res.note="Tie for second — that prize is split.";
}
return res;
}
/* ================= end SHARED RULES ENGINE ================= */
/* ---- playback-only below: simulation policy + navigation ---- */
var NAMES=["Dave","Maya","Big Rob","Suzie","Tom","Kev","Dana","Pete","Lou","Frankie","Jess","Marco","Ali","Ruth","Vic","Sam","Eddie","Priya","Gus","Nina"];
function $(i){return document.getElementById(i);}
function esc(s){return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");}
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
function rollShots(r,skill){var p=r()-skill*0.18;if(p<0.10)return 3;if(p<0.45)return 4;if(p<0.73)return 5;if(p<0.90)return 6;if(p<0.97)return 7;return 8;}
function dots(n,m){var s="";for(var i=0;i<m;i++)s+=i<n?"\u25CF":"\u25CB";return s;}
function shotTurns(st,i){var h=st.hist[i],n=0;for(var k=0;k<h.length;k++)if(h[k].shots!=null)n++;return n;}
function namesOf(st,ps){var a=[];for(var k=0;k<ps.length;k++)a.push(nm(st,ps[k]));return a.join(" & ");}
function simulate(seed){
var rng=mulberry32(seed),i,k;
var pool=NAMES.slice();
for(i=pool.length-1;i>0;i--){var j=Math.floor(rng()*(i+1)),t=pool[i];pool[i]=pool[j];pool[j]=t;}
var n=12+Math.floor(rng()*5);
var names=pool.slice(0,n),skills=[];
for(i=0;i<n;i++)skills.push(rng()*2-1);
var st=createNight(names,{});
var steps=[];
function snap(label){steps.push({label:label,st:JSON.parse(JSON.stringify(st))});}
snap("Doors. "+n+" players in at $"+st.buyIn+" — pot $"+(n*st.buyIn)+". Five chips each, three balls, low score wins.");
snap("Round 1 — "+nm(st,0)+" is up first. Clock set at "+st.clockSecs+"s.");
var guard=0;
while(!st.finished&&guard++<3000){
if(st.round>14){
var ev0=endEarly(st);
snap("Quarter to twelve — the bar calls it. Lights come on eventually.");
for(k=0;k<ev0.length;k++)snap(ev0[k].text);
break;
}
var p=cur(st),ev;
var pns=st.round===1?0.03:Math.min(0.20,0.05+0.012*st.round);
if(rng()<pns)ev=recordNoShow(st);
else ev=recordShots(st,rollShots(rng,skills[p]));
for(k=0;k<ev.length;k++)snap(ev[k].text);
}
var P=payouts(st);
if(P.hot)snap("Side pot settled — hot start: "+namesOf(st,P.hot.winners)+", $"+P.hot.each+(P.hot.winners.length>1?" each":"")+".");
if(P.second)snap("Second place: "+namesOf(st,P.second.ps)+" — $"+P.second.each+(P.second.ps.length>1?" each":"")+".");
if(P.first)snap("Winner: "+namesOf(st,P.first.ps)+" — $"+P.first.each+(P.first.ps.length>1?" each":"")+" of the $"+P.pot+" pot.");
if(P.note)snap("Note: "+P.note);
return steps;
}
var STEPS=[],idx=0,playing=false,tmr=null;
function interval(){return 1500/(+$("speed").value);}
function build(seed){
pause();
STEPS=simulate(seed);idx=0;
var h="";
for(var k=0;k<STEPS.length;k++)h+='<div class="lr" data-k="'+k+'">'+(k+1)+". "+esc(STEPS[k].label)+"</div>";
$("log").innerHTML=h;
renderStep();
}
function renderBoard(st){
var ord=standings(st),h="",ci=st.finished?-1:cur(st);
for(var k=0;k<ord.length;k++){
var i=ord[k],cls="row"+(i===ci?" cur":"")+(st.out[i]?" out":"");
h+='<div class="'+cls+'">';
h+='<span class="nm">'+(i===ci?"\u25B6 ":"")+esc(nm(st,i))+(st.hot&&st.hot.winners.indexOf(i)>=0?' <span class="star">\u2605</span>':"")+"</span>";
h+='<span class="dots">'+dots(st.chips[i],st.startChips)+"</span>";
if(st.out[i])h+='<span class="badge">OUT</span>';
else{var sn=shotTurns(st,i);h+='<span class="stat">'+st.shots[i]+" sh · avg "+(sn?(st.shots[i]/sn).toFixed(1):"—")+"</span>";}
h+="</div>";
}
$("board").innerHTML=h;
}
function renderStep(){
var s=STEPS[idx],st=s.st;
$("banner").textContent=s.label;
$("meta").textContent="Step "+(idx+1)+" of "+STEPS.length+" · Round "+st.round+" · Pot $"+(st.names.length*st.buyIn)+" · "+alive(st)+" of "+st.names.length+" still in";
renderBoard(st);
var rows=$("log").children;
for(var k=0;k<rows.length;k++)rows[k].className="lr"+(k<idx?" done":k===idx?" now":"");
var row=rows[idx];
if(row)$("log").scrollTop=row.offsetTop-$("log").clientHeight/2;
$("btnPlay").textContent=playing?"Pause":"Play";
}
function tick(){
if(idx<STEPS.length-1){idx++;renderStep();tmr=setTimeout(tick,interval());}
else pause();
}
function play(){if(playing)return;if(idx>=STEPS.length-1)idx=0;playing=true;$("btnPlay").textContent="Pause";tmr=setTimeout(tick,interval());}
function pause(){playing=false;clearTimeout(tmr);$("btnPlay").textContent="Play";}
function step(d){pause();idx=Math.max(0,Math.min(STEPS.length-1,idx+d));renderStep();}
$("btnPlay").onclick=function(){playing?pause():play();};
$("btnFwd").onclick=function(){step(1);};
$("btnBack").onclick=function(){step(-1);};
$("btnFirst").onclick=function(){pause();idx=0;renderStep();};
$("speed").oninput=function(){$("spdLab").textContent="speed "+$("speed").value;};
$("btnRun").onclick=function(){var s=parseInt($("seedIn").value,10);if(isNaN(s))s=42;$("seedIn").value=s;build(s);};
$("btnNew").onclick=function(){var s=(Math.random()*1e9)|0;$("seedIn").value=s;build(s);play();};
$("log").onclick=function(e){
var t=e.target;
while(t&&(!t.className||t.className.indexOf("lr")<0))t=t.parentNode;
if(t&&t.getAttribute("data-k")!=null){idx=+t.getAttribute("data-k");pause();renderStep();}
};
document.onkeydown=function(e){
if(e.key==="ArrowRight"){step(1);e.preventDefault();}
else if(e.key==="ArrowLeft"){step(-1);e.preventDefault();}
else if(e.key===" "){playing?pause():play();e.preventDefault();}
};
$("spdLab").textContent="speed 4";
build(42);
</script>
</body>
</html>