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">
<title>THURSDAY POOL</title>
<style>
:root{--bg:#0b0e13;--panel:#141a23;--panel2:#0f141c;--line:#26303f;--ink:#eef2f6;--dim:#8b98a9;
--gold:#f5c542;--green:#3ddc84;--red:#ff5c5c;--blue:#4da3ff;--amber:#ffb454}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{background:var(--bg);color:var(--ink);font:16px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif;
-webkit-user-select:none;user-select:none}
button{font:inherit;color:inherit;background:#1c2430;border:1px solid var(--line);border-radius:10px;
padding:.55em .9em;cursor:pointer;touch-action:manipulation}
button:active{transform:translateY(1px)}
button:disabled{opacity:.35;cursor:default}
button.big{font-size:1.35rem;font-weight:800;padding:.7em 1.1em}
button.danger{border-color:#7a2f2f;color:#ffb3b3}
button.gold{background:#3a2f10;border-color:#8a6d1f;color:var(--gold)}
#hdr{display:flex;align-items:center;gap:1.2rem;padding:.8rem 1.2rem;border-bottom:1px solid var(--line);flex-wrap:wrap}
#hdr h1{font-size:1.5rem;letter-spacing:.12em;color:var(--gold);white-space:nowrap}
#hdr .grow{flex:1}
#hdr .stat{font-size:1.05rem;color:var(--dim)}
#hdr .stat b{color:var(--ink);font-size:1.2em}
#hdr .util{display:flex;gap:.4rem;flex-wrap:wrap}
#hdr .util button{font-size:.8rem;padding:.35em .6em}
#main{display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:1rem;padding:1rem;align-items:start}
@media(max-width:900px){#main{grid-template-columns:1fr}}
#turncard{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:1.2rem;margin-bottom:1rem}
#tc-top{display:flex;align-items:center;gap:1.2rem;flex-wrap:wrap}
#tc-name{font-size:clamp(2.4rem,6vw,4.2rem);font-weight:900;letter-spacing:.02em}
#tc-meta{color:var(--dim);font-size:1.1rem}
#tc-meta b{color:var(--ink)}
#tc-clock{margin-left:auto;font-size:clamp(2.6rem,7vw,4.6rem);font-weight:900;font-variant-numeric:tabular-nums;color:var(--green)}
#tc-clock.warn{color:var(--amber)}
#tc-clock.hot{color:var(--red);animation:pulse 1s infinite}
@keyframes pulse{50%{opacity:.45}}
#tc-btns{display:flex;gap:.6rem;flex-wrap:wrap;margin-top:1rem}
#tc-log{margin-top:1rem;font-size:1.05rem;color:var(--dim);border-top:1px solid var(--line);padding-top:.7rem;min-height:1.6em}
#tc-log b{color:var(--ink)}
#board-wrap{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:1rem;overflow-x:auto}
#board-wrap h2{font-size:.85rem;letter-spacing:.2em;color:var(--dim);margin-bottom:.7rem}
table{border-collapse:collapse;width:100%;min-width:560px}
th,td{padding:.45em .6em;text-align:center;border-bottom:1px solid var(--line);font-size:1.05rem;white-space:nowrap}
th{color:var(--dim);font-size:.75rem;letter-spacing:.15em;text-align:center}
td.nm{text-align:left;font-weight:700}
tr.up{background:#1b2331}
tr.up td.nm::before{content:"▸ ";color:var(--gold)}
tr.out td{color:#5a6472}
tr.out td.nm{text-decoration:line-through}
td.chipcell{letter-spacing:.15em}
.on{color:var(--gold)} .off{color:#333c49}
td.best{color:var(--green);font-weight:800}
td.pot{color:var(--gold);font-weight:800}
#side{display:flex;flex-direction:column;gap:1rem}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:1rem}
.panel h2{font-size:.8rem;letter-spacing:.2em;color:var(--dim);margin-bottom:.7rem}
#shotgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;margin-bottom:.8rem}
#shotgrid button{font-size:1.6rem;font-weight:900;padding:.55em 0}
#shotgrid button.five{color:var(--red)}
.mini{display:flex;gap:.5rem;flex-wrap:wrap}
.mini button{flex:1;font-size:.95rem}
#log{max-height:340px;overflow-y:auto;font-size:.95rem}
#log div{padding:.4em 0;border-bottom:1px solid #1a2230;color:var(--dim)}
#log div b{color:var(--ink)}
#log div:first-child{color:var(--ink)}
#ov{position:fixed;inset:0;background:rgba(5,7,10,.92);display:flex;align-items:center;justify-content:center;z-index:50;padding:1rem}
#ov[hidden]{display:none}
#ovbox{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:2rem;max-width:720px;width:100%;max-height:92vh;overflow-y:auto}
#ovbox h2{font-size:clamp(1.8rem,4vw,2.8rem);color:var(--gold);margin-bottom:1rem;letter-spacing:.05em}
#ovbox table{min-width:0;width:100%;margin-bottom:1rem}
#ovbox .row{display:flex;gap:.6rem;flex-wrap:wrap;margin-top:1rem}
#ovbox .note{color:var(--dim);margin-top:.8rem;font-size:.95rem}
#ovbox input{font:inherit;background:#0d1119;border:1px solid var(--line);border-radius:8px;color:var(--ink);padding:.5em .7em;width:100%}
.hidden{display:none!important}
</style>
</head>
<body>
<header id="hdr">
<h1>THURSDAY POOL</h1>
<span class="stat">ROUND <b id="h-round">1</b></span>
<span class="stat">POT <b id="h-pot">$0</b></span>
<span class="stat">IN <b id="h-in">0</b></span>
<span class="grow"></span>
<span class="util">
<button id="btn-setup">SET UP</button>
<button id="btn-backup">BACKUP</button>
<button id="btn-restore">RESTORE</button>
<button id="btn-reset" class="danger">NEW NIGHT</button>
</span>
</header>
<div id="main">
<section>
<div id="turncard">
<div id="tc-top">
<div>
<div id="tc-name">—</div>
<div id="tc-meta"></div>
</div>
<div id="tc-clock">1:30</div>
</div>
<div id="tc-btns">
<button id="btn-clear" class="big gold">CLEAR ✓</button>
<button id="btn-miss" class="big">MISS</button>
<button id="btn-viol" class="danger">CLOCK ⏱ −1 CHIP</button>
<button id="btn-skip">SKIP TURN</button>
</div>
<div id="tc-log"></div>
</div>
<div id="board-wrap">
<h2>SCOREBOARD</h2>
<table id="board"></table>
</div>
</section>
<aside id="side">
<div class="panel">
<h2>RECORD THIS CLEAR</h2>
<div id="shotgrid"></div>
<div class="mini">
<button id="btn-undo">UNDO LAST</button>
<button id="btn-endround">END ROUND →</button>
</div>
</div>
<div class="panel">
<h2>WHAT JUST HAPPENED</h2>
<div id="log"></div>
</div>
</aside>
</div>
<div id="ov" hidden><div id="ovbox"></div></div>
<input type="file" id="filein" accept=".json,application/json" style="display:none">
<script>
"use strict";
/* ============ CONFIG — edit these, nothing else ============ */
const CFG = {
entry: 20, startChips: 5, shotLimit: 4, turnSec: 90,
bonusCut: 20, secondCut: 20, bonusRounds: 3,
names: ["Dana","Rick","Tommy","Jess","Al","Marcus","Priya","Nate","Colleen","Ray","Sofia","Deke"]
};
/* ============ RULES — shared verbatim with the playback ============ */
function newGame(names){
return { players: names.map(n=>({name:n, chips:CFG.startChips, out:false, clears:[]})),
round:1, turn:0, shots:0, pot:names.length*CFG.entry, log:[], over:false, seq:0 };
}
const cur = g => g.players[g.turn];
const alive = g => g.players.filter(p=>!p.out);
function nextTurn(g){
for(let k=1;k<=g.players.length;k++){ const i=(g.turn+k)%g.players.length;
if(!g.players[i].out){ g.turn=i; return; } }
}
function loseChip(g,p,why){
p.chips--; g.log.push({t:"chip",p:p.name,why});
if(p.chips<=0){ p.out=true; g.log.push({t:"out",p:p.name}); }
}
function recordClear(g,n){
const p=cur(g); p.clears[g.round]=n; g.log.push({t:"clear",p:p.name,n,round:g.round});
if(n>CFG.shotLimit) loseChip(g,p,">"+CFG.shotLimit+" shots");
g.shots=0; if(alive(g).length<=1) return endGame(g); nextTurn(g);
}
function clockViolation(g){ loseChip(g,cur(g),"clock"); g.shots=0;
if(alive(g).length<=1) return endGame(g); nextTurn(g); }
function skipTurn(g){ loseChip(g,cur(g),"forfeit"); g.shots=0;
if(alive(g).length<=1) return endGame(g); nextTurn(g); }
function endRound(g){
g.round++; g.turn=g.players.findIndex(p=>!p.out); g.shots=0;
g.log.push({t:"round",round:g.round});
}
function bonusWinners(g){
const res=[]; let best=Infinity;
g.players.forEach((p,i)=>{ const c=p.clears.slice(1,CFG.bonusRounds+1).filter(x=>x!=null);
if(!c.length) return; const t=c.reduce((a,b)=>a+b,0);
res.push({name:p.name,total:t,clears:c}); if(t<best) best=t; });
return { best, list: res.filter(r=>r.total===best) };
}
function endGame(g){
g.over=true;
const w=alive(g); const b=bonusWinners(g);
const bEach=b.list.length?Math.floor(CFG.bonusCut/b.list.length):0;
g.payout={ winner:w[0]?w[0].name:null, winAmt:g.pot-CFG.bonusCut,
second:null, secondAmt:0, bonus:b.list.map(x=>x.name), bonusAmt:bEach };
let others=g.players.filter(p=>!p.out||p!==w[0]);
let bestAvg=Infinity;
others.forEach(p=>{ const c=p.clears.filter(x=>x!=null);
if(c.length){ const a=c.reduce((s,x)=>s+x,0)/c.length; if(a<bestAvg) bestAvg=a; } });
const secs=others.filter(p=>{ const c=p.clears.filter(x=>x!=null);
return c.length && c.reduce((s,x)=>s+x,0)/c.length===bestAvg; });
if(w.length===0){ g.payout.winner=null; g.payout.winAmt=0;
g.payout.second=secs[0]?secs[0].name:null; g.payout.secondAmt=g.pot; }
else if(w.length===1){ g.payout.second=secs[0]?secs[0].name:null; g.payout.secondAmt=CFG.secondCut; }
else { g.payout.winAmt=Math.floor(g.pot/2); g.payout.second=w[1].name;
g.payout.secondAmt=g.pot-g.payout.winAmt; }
g.log.push({t:"over"});
}
function reduce(g,e){
g.seq++;
switch(e.t){
case "miss": g.shots++; g.log.push({t:"miss",p:cur(g).name,n:g.shots}); break;
case "clear": recordClear(g,e.n); break;
case "clock": clockViolation(g); break;
case "skip": skipTurn(g); break;
case "round": endRound(g); break;
case "over": if(!g.over) endGame(g); break;
}
return g;
}
/* ============ CONSOLE ============ */
let G=newGame(CFG.names);
let clockLeft=CFG.turnSec, clockTimer=null, lastTick=0, pendingClear=null;
const $=id=>document.getElementById(id);
const fmt=s=>Math.floor(s/60)+":"+String(Math.max(0,s)%60).padStart(2,"0");
function startClock(){ stopClock(); clockLeft=CFG.turnSec; lastTick=Date.now();
clockTimer=setInterval(tick,250); paintClock(); }
function stopClock(){ if(clockTimer){clearInterval(clockTimer);clockTimer=null;} }
function tick(){ const now=Date.now(); clockLeft-=Math.floor((now-lastTick)/1000);
lastTick=now; if(clockLeft<0)clockLeft=0; paintClock(); }
function paintClock(){ const el=$("tc-clock"); el.textContent=fmt(clockLeft);
el.className=clockLeft<=10?"hot":clockLeft<=30?"warn":""; }
function logLine(e){
switch(e.t){
case "miss": return "<b>"+e.p+"</b> missed — shot "+e.n;
case "clear": return "<b>"+e.p+"</b> cleared in <b>"+e.n+"</b>"+(e.n>CFG.shotLimit?" · −1 chip":"");
case "chip": return "<b>"+e.p+"</b> −1 chip ("+e.why+")";
case "out": return "<b>"+e.p+"</b> is OUT";
case "round": return "— ROUND "+e.round+" —";
case "over": return "— NIGHT OVER —";
} return "";
}
function paint(){
$("h-round").textContent=G.round;
$("h-pot").textContent="$"+G.pot;
$("h-in").textContent=alive(G).length+" / "+G.players.length;
const p=cur(G);
$("tc-name").textContent=G.over?"—":p.name;
const done=p.clears.filter(x=>x!=null).length;
$("tc-meta").innerHTML=G.over?"night over":
"round <b>"+G.round+"</b> · <b>"+p.chips+"</b> chip"+(p.chips===1?"":"s")+
" · <b>"+done+"</b> clear"+(done===1?"":"s")+" on the board";
$("tc-log").innerHTML=G.log.length?("last: "+logLine(G.log[G.log.length-1])):"";
const dis=G.over;
["btn-clear","btn-miss","btn-viol","btn-skip","btn-undo","btn-endround"].forEach(id=>$(id).disabled=dis);
const maxR=Math.max(4,...G.players.map(q=>q.clears.length));
let h="<tr><th></th>"; for(let r=1;r<=maxR;r++) h+="<th>R"+r+"</th>";
h+="<th>CHIPS</th><th>AVG</th></tr>";
G.players.forEach((q,i)=>{
const cs=q.clears.filter(x=>x!=null);
const avg=cs.length?(cs.reduce((a,b)=>a+b,0)/cs.length).toFixed(1):"—";
const best=cs.length?Math.min.apply(null,cs):null;
let cells=""; for(let r=1;r<=maxR;r++){ const v=q.clears[r];
cells+="<td"+(v===best&&v!=null?' class="best"':"")+">"+(v!=null?v:"·")+"</td>"; }
let chips=""; for(let c=0;c<CFG.startChips;c++)
chips+='<span class="'+(c<q.chips?"on":"off")+'">●</span>';
h+='<tr class="'+(i===G.turn&&!G.over?"up":"")+(q.out?" out":"")+'"><td class="nm">'+q.name+
"</td>"+cells+'<td class="chipcell">'+chips+'</td><td class="'+(avg!=="—"&&avg<=CFG.shotLimit?"pot":"")+'">'+avg+"</td></tr>";
});
$("board").innerHTML=h;
const lg=$("log"); lg.innerHTML=G.log.slice(-40).reverse().map(logLine).map(s=>"<div>"+s+"</div>").join("");
if(G.over) showPayout();
}
function afterEvent(){ paint(); if(!G.over) startClock(); else stopClock(); }
function doMiss(){ reduce(G,{t:"miss"}); afterEvent(); }
function askClear(){
if(G.over) return;
pendingClear=true;
$("ovbox").innerHTML="<h2>CLEARED IN…</h2><div id='ovshots'></div>"+
"<div class='row'><button id='ov-cancel'>CANCEL</button></div>";
const grid=$("ovshots"); grid.style.cssText="display:grid;grid-template-columns:repeat(3,1fr);gap:.7rem";
for(let n=1;n<=6;n++){ const b=document.createElement("button");
b.textContent=n+(n>CFG.shotLimit?" ⚠":""); b.className="big"+(n>CFG.shotLimit?" danger":"");
b.onclick=()=>{ pendingClear=false; hideOv(); reduce(G,{t:"clear",n}); afterEvent(); };
grid.appendChild(b); }
$("ov-cancel").onclick=()=>{ pendingClear=false; hideOv(); };
$("ov").hidden=false;
}
function doViol(){ reduce(G,{t:"clock"}); afterEvent(); }
function doSkip(){ reduce(G,{t:"skip"}); afterEvent(); }
function doEndRound(){ reduce(G,{t:"round"}); afterEvent(); }
function doUndo(){
if(G.log.length<2) return;
const evts=G.log.slice(0,-1); const n=G.players.map(p=>p.name);
G=newGame(n); evts.forEach(e=>reduce(G,e));
paint(); if(!G.over) startClock();
}
function showPayout(){
stopClock();
const py=G.payout, b=bonusWinners(G);
let rows="";
if(py.winner) rows+="<tr><td class='nm'>🏆 "+py.winner+"</td><td class='pot'>$"+py.winAmt+"</td><td>winner</td></tr>";
if(py.second) rows+="<tr><td class='nm'>"+py.second+"</td><td>$"+py.secondAmt+"</td><td>second</td></tr>";
if(py.bonus.length) rows+="<tr><td class='nm'>"+py.bonus.join(" + ")+"</td><td>$"+py.bonusAmt+
" each</td><td>hot hand · R1–"+CFG.bonusRounds+" ("+b.list.map(x=>x.total).join("/")+")</td></tr>";
let board="<tr><th></th><th>AVG</th><th>CLEARS</th><th>CHIPS</th></tr>";
G.players.forEach(p=>{ const cs=p.clears.filter(x=>x!=null);
const avg=cs.length?(cs.reduce((a,b)=>a+b,0)/cs.length).toFixed(1):"—";
board+="<tr"+(p.out?" class='out'":"")+"><td class='nm'>"+p.name+"</td><td>"+avg+
"</td><td>"+cs.length+"</td><td>"+p.chips+"</td></tr>"; });
$("ovbox").innerHTML="<h2>PAY OUT</h2><table>"+rows+"</table><table>"+board+"</table>"+
"<div class='note'>Pot was $"+G.pot+" ("+G.players.length+" × $"+CFG.entry+"). "+
"Bonus = best total over rounds 1–"+CFG.bonusRounds+". Second = best avg among non-winners.</div>"+
"<div class='row'><button id='ov-close'>CLOSE</button><button id='ov-backup' class='gold'>SAVE BACKUP FILE</button></div>";
$("ov-close").onclick=hideOv; $("ov-backup").onclick=backup;
$("ov").hidden=false;
}
function hideOv(){ $("ov").hidden=true; }
function setupOv(){
$("ovbox").innerHTML="<h2>SET UP THE NIGHT</h2>"+
"<p style='color:var(--dim);margin-bottom:.8rem'>One name per line, top of the list shoots first. $"+CFG.entry+" a head, "+CFG.startChips+" chips each.</p>"+
"<textarea id='ov-names' style='width:100%;height:260px;font:inherit;background:#0d1119;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:.6em'></textarea>"+
"<div class='row'><button id='ov-start' class='big gold'>RACK ’EM</button><button id='ov-cancel'>CANCEL</button></div>";
$("ov-names").value=G.players.map(p=>p.name).join("\n");
$("ov-start").onclick=()=>{
const names=$("ov-names").value.split("\n").map(s=>s.trim()).filter(Boolean).slice(0,24);
if(names.length<2){ alert("Need at least two names."); return; }
G=newGame(names); hideOv(); paint(); startClock();
};
$("ov-cancel").onclick=hideOv;
$("ov").hidden=false;
}
function backup(){
const blob=new Blob([JSON.stringify({v:1,cfg:CFG,g:G})],{type:"application/json"});
const a=document.createElement("a"); a.href=URL.createObjectURL(blob);
a.download="thursday-pool-round"+G.round+".json"; a.click();
setTimeout(()=>URL.revokeObjectURL(a.href),5000);
}
function restore(file){
const r=new FileReader();
r.onload=()=>{ try{
const d=JSON.parse(r.result);
if(!d.g||!d.g.players) throw 0;
G=d.g; hideOv(); paint(); if(!G.over) startClock();
}catch(e){ alert("That file doesn't look like a Thursday backup."); } };
r.readAsText(file);
}
$("btn-miss").onclick=doMiss;
$("btn-clear").onclick=askClear;
$("btn-viol").onclick=doViol;
$("btn-skip").onclick=doSkip;
$("btn-endround").onclick=doEndRound;
$("btn-undo").onclick=doUndo;
$("btn-setup").onclick=setupOv;
$("btn-backup").onclick=backup;
$("btn-restore").onclick=()=>$("filein").click();
$("filein").onchange=e=>{ if(e.target.files[0]) restore(e.target.files[0]); e.target.value=""; };
$("btn-reset").onclick=()=>{ if(confirm("Wipe this night and start fresh?")){ G=newGame(CFG.names); hideOv(); paint(); startClock(); } };
(function buildShotGrid(){
const g=$("shotgrid");
for(let n=1;n<=4;n++){ const b=document.createElement("button");
b.textContent=n; b.onclick=()=>{ reduce(G,{t:"clear",n}); afterEvent(); }; g.appendChild(b); }
const b=document.createElement("button"); b.textContent="5+"; b.className="five";
b.onclick=askClear; g.appendChild(b);
})();
document.addEventListener("keydown",e=>{
if(!$("ov").hidden){ if(e.key==="Escape"&&!pendingClear) hideOv(); return; }
if(e.key>="1"&&e.key<="4"){ reduce(G,{t:"clear",n:+e.key}); afterEvent(); }
else if(e.key==="m"||e.key==="M") doMiss();
else if(e.key==="v"||e.key==="V") doViol();
});
paint(); startClock();
</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 POOL — playback</title>
<style>
:root{--bg:#0b0e13;--panel:#141a23;--line:#26303f;--ink:#eef2f6;--dim:#8b98a9;
--gold:#f5c542;--green:#3ddc84;--red:#ff5c5c;--amber:#ffb454}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--ink);font:15px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif}
button{font:inherit;color:inherit;background:#1c2430;border:1px solid var(--line);border-radius:8px;
padding:.4em .8em;cursor:pointer}
button:disabled{opacity:.35}
header{display:flex;align-items:baseline;gap:1rem;padding:.9rem 1.2rem;border-bottom:1px solid var(--line)}
header h1{font-size:1.2rem;letter-spacing:.12em;color:var(--gold)}
header span{color:var(--dim);font-size:.9rem}
#wrap{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:1rem;padding:1rem;align-items:start}
@media(max-width:900px){#wrap{grid-template-columns:1fr}}
#stepcard{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:1rem 1.2rem;margin-bottom:1rem;min-height:96px}
#step-n{font-size:.75rem;letter-spacing:.2em;color:var(--dim)}
#step-txt{font-size:1.5rem;font-weight:800;margin-top:.2rem}
#step-txt small{display:block;font-weight:400;font-size:1rem;color:var(--dim);margin-top:.2rem}
#controls{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-bottom:1rem}
#speed{width:200px;accent-color:var(--gold)}
#spd-lab{color:var(--dim);font-size:.9rem;min-width:5em}
#play{font-weight:800;min-width:6em}
#board-wrap{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:1rem;overflow-x:auto}
#board-wrap h2{font-size:.75rem;letter-spacing:.2em;color:var(--dim);margin-bottom:.6rem}
table{border-collapse:collapse;width:100%;min-width:520px}
th,td{padding:.4em .55em;text-align:center;border-bottom:1px solid var(--line);font-size:.95rem;white-space:nowrap}
th{color:var(--dim);font-size:.7rem;letter-spacing:.12em}
td.nm{text-align:left;font-weight:700}
tr.up{background:#1b2331}
tr.up td.nm::before{content:"▸ ";color:var(--gold)}
tr.out td{color:#5a6472}
tr.out td.nm{text-decoration:line-through}
.on{color:var(--gold)} .off{color:#333c49}
td.best{color:var(--green);font-weight:800}
td.pot{color:var(--gold);font-weight:800}
td.flash{animation:fl 1.2s ease-out}
@keyframes fl{0%{background:#3a2f10}100%{background:transparent}}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:1rem;margin-bottom:1rem}
.panel h2{font-size:.75rem;letter-spacing:.2em;color:var(--dim);margin-bottom:.6rem}
#tl{max-height:420px;overflow-y:auto;font-size:.9rem}
#tl div{padding:.35em .4em;border-bottom:1px solid #1a2230;color:var(--dim);cursor:pointer;border-radius:4px}
#tl div:hover{background:#1a2230}
#tl div.cur{color:var(--ink);background:#1b2331;font-weight:700}
#rules{color:var(--dim);font-size:.85rem;line-height:1.7}
#rules b{color:var(--ink)}
#payout{background:#1b2331;border:1px solid #8a6d1f;border-radius:12px;padding:1rem;margin-bottom:1rem}
#payout h2{color:var(--gold);font-size:.8rem;letter-spacing:.2em;margin-bottom:.6rem}
#payout div{padding:.25em 0;font-size:1rem}
#payout b{color:var(--gold)}
</style>
</head>
<body>
<header><h1>THURSDAY POOL</h1><span>playback — same rules code as the console</span></header>
<div id="wrap">
<section>
<div id="stepcard">
<div id="step-n">STEP 0 / 0</div>
<div id="step-txt">Rack ’em.</div>
</div>
<div id="controls">
<button id="back">◀ BACK</button>
<button id="play">PLAY ▶</button>
<button id="fwd">STEP ▶</button>
<input type="range" id="speed" min="0" max="100" value="40">
<span id="spd-lab">1.0×</span>
</div>
<div id="board-wrap"><h2>SCOREBOARD</h2><table id="board"></table></div>
</section>
<aside>
<div id="payout" hidden><h2>PAYOUT</h2><div id="payout-body"></div></div>
<div class="panel"><h2>THE NIGHT, ONE EVENT AT A TIME</h2><div id="tl"></div></div>
<div class="panel"><h2>RULES AS RUN</h2><div id="rules"></div></div>
</aside>
</div>
<script>
"use strict";
/* ============ CONFIG — identical to the console ============ */
const CFG = {
entry: 20, startChips: 5, shotLimit: 4, turnSec: 90,
bonusCut: 20, secondCut: 20, bonusRounds: 3,
names: ["Dana","Rick","Tommy","Jess","Al","Marcus","Priya","Nate","Colleen","Ray","Sofia","Deke"]
};
/* ============ RULES — shared verbatim with the console ============ */
function newGame(names){
return { players: names.map(n=>({name:n, chips:CFG.startChips, out:false, clears:[]})),
round:1, turn:0, shots:0, pot:names.length*CFG.entry, log:[], over:false, seq:0 };
}
const cur = g => g.players[g.turn];
const alive = g => g.players.filter(p=>!p.out);
function nextTurn(g){
for(let k=1;k<=g.players.length;k++){ const i=(g.turn+k)%g.players.length;
if(!g.players[i].out){ g.turn=i; return; } }
}
function loseChip(g,p,why){
p.chips--; g.log.push({t:"chip",p:p.name,why});
if(p.chips<=0){ p.out=true; g.log.push({t:"out",p:p.name}); }
}
function recordClear(g,n){
const p=cur(g); p.clears[g.round]=n; g.log.push({t:"clear",p:p.name,n,round:g.round});
if(n>CFG.shotLimit) loseChip(g,p,">"+CFG.shotLimit+" shots");
g.shots=0; if(alive(g).length<=1) return endGame(g); nextTurn(g);
}
function clockViolation(g){ loseChip(g,cur(g),"clock"); g.shots=0;
if(alive(g).length<=1) return endGame(g); nextTurn(g); }
function skipTurn(g){ loseChip(g,cur(g),"forfeit"); g.shots=0;
if(alive(g).length<=1) return endGame(g); nextTurn(g); }
function endRound(g){
g.round++; g.turn=g.players.findIndex(p=>!p.out); g.shots=0;
g.log.push({t:"round",round:g.round});
}
function bonusWinners(g){
const res=[]; let best=Infinity;
g.players.forEach((p,i)=>{ const c=p.clears.slice(1,CFG.bonusRounds+1).filter(x=>x!=null);
if(!c.length) return; const t=c.reduce((a,b)=>a+b,0);
res.push({name:p.name,total:t,clears:c}); if(t<best) best=t; });
return { best, list: res.filter(r=>r.total===best) };
}
function endGame(g){
g.over=true;
const w=alive(g); const b=bonusWinners(g);
const bEach=b.list.length?Math.floor(CFG.bonusCut/b.list.length):0;
g.payout={ winner:w[0]?w[0].name:null, winAmt:g.pot-CFG.bonusCut,
second:null, secondAmt:0, bonus:b.list.map(x=>x.name), bonusAmt:bEach };
let others=g.players.filter(p=>!p.out||p!==w[0]);
let bestAvg=Infinity;
others.forEach(p=>{ const c=p.clears.filter(x=>x!=null);
if(c.length){ const a=c.reduce((s,x)=>s+x,0)/c.length; if(a<bestAvg) bestAvg=a; } });
const secs=others.filter(p=>{ const c=p.clears.filter(x=>x!=null);
return c.length && c.reduce((s,x)=>s+x,0)/c.length===bestAvg; });
if(w.length===0){ g.payout.winner=null; g.payout.winAmt=0;
g.payout.second=secs[0]?secs[0].name:null; g.payout.secondAmt=g.pot; }
else if(w.length===1){ g.payout.second=secs[0]?secs[0].name:null; g.payout.secondAmt=CFG.secondCut; }
else { g.payout.winAmt=Math.floor(g.pot/2); g.payout.second=w[1].name;
g.payout.secondAmt=g.pot-g.payout.winAmt; }
g.log.push({t:"over"});
}
function reduce(g,e){
g.seq++;
switch(e.t){
case "miss": g.shots++; g.log.push({t:"miss",p:cur(g).name,n:g.shots}); break;
case "clear": recordClear(g,e.n); break;
case "clock": clockViolation(g); break;
case "skip": skipTurn(g); break;
case "round": endRound(g); break;
case "over": if(!g.over) endGame(g); break;
}
return g;
}
/* ============ PLAYBACK ============ */
/* A scripted night. Each line is one event: one thing that happens at the table. */
const NIGHT=[
["miss","Dana"],
["clear","Dana",2],
["clear","Rick",3],
["miss","Tommy"],["miss","Tommy"],
["clear","Tommy",4],
["clear","Jess",1],
["clear","Al",3],
["miss","Marcus"],["miss","Marcus"],
["clear","Marcus",5],
["clear","Priya",2],
["clear","Nate",2],
["clear","Colleen",3],
["clear","Ray",2],
["clear","Sofia",4],
["clear","Deke",2],
["round","Round 2"],
["clear","Dana",2],
["clear","Rick",2],
["clear","Tommy",3],
["clear","Jess",2],
["clear","Al",2],
["clear","Marcus",3],
["clear","Priya",1],
["clear","Nate",3],
["clear","Colleen",2],
["clear","Ray",3],
["clear","Sofia",2],
["miss","Deke"],["miss","Deke"],["miss","Deke"],
["clear","Deke",6],
["round","Round 3"],
["clear","Dana",3],
["clear","Rick",2],
["miss","Tommy"],["miss","Tommy"],
["clear","Tommy",5],
["clear","Jess",2],
["clear","Al",3],
["clear","Marcus",2],
["clear","Priya",2],
["clear","Nate",2],
["clear","Colleen",3],
["clear","Ray",2],
["clear","Sofia",3],
["clock","Deke"],
["round","Round 4"],
["clear","Dana",2],
["clear","Rick",3],
["clear","Tommy",3],
["clear","Jess",2],
["clear","Al",2],
["clear","Marcus",3],
["clear","Priya",2],
["clear","Nate",3],
["clear","Colleen",2],
["clear","Ray",2],
["clear","Sofia",2],
["skip","Deke"],
["round","Round 5"],
["clear","Dana",2],
["clear","Rick",2],
["miss","Tommy"],["miss","Tommy"],
["clear","Tommy",5],
["clear","Jess",3],
["clear","Al",2],
["clear","Marcus",2],
["clear","Priya",3],
["clear","Nate",2],
["clear","Colleen",3],
["clear","Ray",2],
["clear","Sofia",3],
["over",""]
];
let states=[], step=0, playing=false, timer=null, changed=[];
function buildStates(){
states=[newGame(CFG.names)];
NIGHT.forEach(e=>{
const g=JSON.parse(JSON.stringify(states[states.length-1]));
const before=JSON.stringify(g.players);
reduce(g,{t:e[0],n:e[2]});
changed.push(JSON.stringify(g.players)!==before);
states.push(g);
});
}
function evtText(i){
if(i===0) return ["Rack ’em.", CFG.names.length+" players · "+CFG.startChips+" chips each · $"+(CFG.names.length*CFG.entry)+" in the pot"];
const e=NIGHT[i-1], g=states[i];
switch(e[0]){
case "miss": return [e[1]+" missed.", "shot "+g.shots+" of the turn"];
case "clear":{ const over=e[2]>CFG.shotLimit;
return [e[1]+" cleared in "+e[2]+".", over?"over "+CFG.shotLimit+" — costs a chip":"table cleared"]; }
case "clock": return [e[1]+" ran out the clock.", "−1 chip"];
case "skip": return [e[1]+" skipped the turn.", "forfeit — −1 chip"];
case "round": return ["— ROUND "+g.round+" —",""];
case "over": return ["Night over.","one player left standing"];
}
}
function fmtClock(s){return Math.floor(s/60)+":"+String(s%60).padStart(2,"0");}
function paint(){
const g=states[step];
$("step-n").textContent="STEP "+step+" / "+states.length-1;
const [a,b]=evtText(step);
$("step-txt").innerHTML=a+(b?"<small>"+b+"</small>":"");
const maxR=Math.max(4,...g.players.map(p=>p.clears.length));
let h="<tr><th></th>"; for(let r=1;r<=maxR;r++) h+="<th>R"+r+"</th>";
h+="<th>CHIPS</th><th>AVG</th></tr>";
g.players.forEach((p,i)=>{
const cs=p.clears.filter(x=>x!=null);
const avg=cs.length?(cs.reduce((x,y)=>x+y,0)/cs.length).toFixed(1):"—";
const best=cs.length?Math.min.apply(null,cs):null;
let cells=""; for(let r=1;r<=maxR;r++){ const v=p.clears[r];
cells+="<td"+(v===best&&v!=null?' class="best"':"")+">"+(v!=null?v:"·")+"</td>"; }
let chips=""; for(let c=0;c<CFG.startChips;c++)
chips+='<span class="'+(c<p.chips?"on":"off")+'">●</span>';
h+='<tr class="'+(i===g.turn&&!g.over?"up":"")+(p.out?" out":"")+'"><td class="nm">'+p.name+
"</td>"+cells+'<td>'+chips+'</td><td class="'+(avg!=="—"&&avg<=CFG.shotLimit?"pot":"")+'">'+avg+"</td></tr>";
});
$("board").innerHTML=h;
if(step>0&&changed[step-1]){
document.querySelectorAll("#board tr").forEach(tr=>{
if(tr.className.indexOf("up")>=0||tr.className.indexOf("out")>=0) tr.querySelectorAll("td").forEach(td=>td.classList.add("flash"));
});
}
const tl=$("tl");
tl.innerHTML="";
for(let i=states.length-1;i>=0;i--){
const d=document.createElement("div");
d.textContent=(i===0?"● ":(""+i+". "))+evtText(i)[0];
if(i===step) d.className="cur";
d.onclick=(()=>{const k=i;return()=>{seek(k);};})();
tl.appendChild(d);
}
const curEl=tl.querySelector(".cur"); if(curEl) curEl.scrollIntoView({block:"nearest"});
const po=$("payout");
if(g.over&&g.payout){
po.hidden=false;
let s="";
if(g.payout.winner) s+="<div>🏆 <b>"+g.payout.winner+"</b> — $"+g.payout.winAmt+" (winner)</div>";
if(g.payout.second) s+="<div>"+g.payout.second+" — $"+g.payout.secondAmt+" (second)</div>";
if(g.payout.bonus.length) s+="<div>"+g.payout.bonus.join(" + ")+" — $"+g.payout.bonusAmt+
" each (hot hand, rounds 1–"+CFG.bonusRounds+")</div>";
$("payout-body").innerHTML=s;
} else po.hidden=true;
$("back").disabled=step===0;
$("fwd").disabled=step===states.length-1;
}
function seek(k){ step=Math.max(0,Math.min(states.length-1,k)); paint(); }
function delay(){ const v=+$("speed").value; return 3200*Math.pow(0.945,v); }
function loop(){
if(!playing) return;
if(step>=states.length-1){ stop(); return; }
seek(step+1);
timer=setTimeout(loop,delay());
}
function play(){ if(step>=states.length-1) seek(0); playing=true; $("play").textContent="PAUSE"; loop(); }
function stop(){ playing=false; $("play").textContent="PLAY ▶"; if(timer){clearTimeout(timer);timer=null;} }
$("play").onclick=()=>playing?stop():play();
$("fwd").onclick=()=>{stop();seek(step+1);};
$("back").onclick=()=>{stop();seek(step-1);};
$("speed").oninput=()=>{
const x=+$("speed").value;
$("spd-lab").textContent=(3200/delay()).toFixed(1)+"×";
if(playing){ clearTimeout(timer); timer=setTimeout(loop,delay()); }
};
document.addEventListener("keydown",e=>{
if(e.key==="ArrowRight"){stop();seek(step+1);}
else if(e.key==="ArrowLeft"){stop();seek(step-1);}
else if(e.key===" "){e.preventDefault();playing?stop():play();}
});
$("rules").innerHTML=
"$<b>"+CFG.entry+"</b> in · <b>"+CFG.startChips+"</b> chips · clear the table, low shots win · "+
"over <b>"+CFG.shotLimit+"</b> shots costs a chip · clock is <b>"+fmtClock(CFG.turnSec)+"</b> · "+
"clock or skip costs a chip · zero chips, you're out · last one standing takes the pot minus "+
"$<b>"+CFG.bonusCut+"</b> hot hand (rounds 1–"+CFG.bonusRounds+") · second takes $<b>"+CFG.secondCut+"</b>.";
buildStates();
$("spd-lab").textContent=(3200/delay()).toFixed(1)+"×";
paint();
</script>
</body>
</html>