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>
*{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:#0b0f14;color:#f2f5f7}header{padding:12px 16px;background:#111a24;display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center}h1{font-size:clamp(20px,4vw,34px);margin:0}.meta{font-size:clamp(16px,3vw,24px)}.wrap{padding:12px;max-width:1100px;margin:auto}.card{background:#16222e;border:2px solid #2a3d4f;border-radius:14px;padding:14px;margin-bottom:12px}#curName{font-size:clamp(42px,10vw,84px);font-weight:900;line-height:1}.big{font-size:clamp(20px,4vw,30px)}.timer{font-size:clamp(36px,9vw,64px);font-weight:900}.timer.warn{color:#ffb020}.timer.zero{color:#ff5252}button{font:inherit;min-height:56px;border-radius:12px;border:2px solid #3a556e;background:#1e3247;color:#fff;padding:10px 14px;font-size:20px;touch-action:manipulation;cursor:pointer}button:active{background:#2c4a66}button:disabled{opacity:.4}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.shot{font-size:clamp(26px,6vw,40px);font-weight:900;min-height:72px}#timeoutBtn{background:#7a1f1f;border-color:#b03a3a;grid-column:span 4}.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px;align-items:center}table{width:100%;border-collapse:collapse;font-size:clamp(16px,3.2vw,26px)}th,td{padding:8px 6px;border-bottom:1px solid #2a3d4f;text-align:left}tr.out{opacity:.45;text-decoration:line-through}tr.cur{background:#243a4f}.log{font-size:clamp(15px,3vw,22px);line-height:1.4}.log div{padding:4px 0;border-bottom:1px solid #223}input{font:inherit;font-size:20px;padding:10px;border-radius:10px;border:2px solid #3a556e;background:#0f1a26;color:#fff;width:100%}.rosterRow{display:flex;gap:8px;margin-bottom:8px}.rosterRow button{min-width:56px}.hidden{display:none}body.tv .no-tv{display:none!important}body.tv table{font-size:clamp(24px,4vw,40px)}body.tv #curName{font-size:clamp(60px,12vw,120px)}#settlementPanel{border-color:#ffb020}
</style>
</head>
<body>
<header><h1>Thursday 3-Ball</h1><div class="meta">Round <span id="roundEl">1</span> · Pot <span id="potEl">$240</span> · In <span id="inEl">12 / 12</span></div></header>
<div class="wrap">
<div class="card"><div class="big">Now shooting</div><div id="curName">—</div><div class="big">Chips <span id="curChips">●●●●●</span> · Total <span id="curTotal">0</span> · Last <span id="curLast">—</span></div><div class="row no-tv"><div id="timerEl" class="timer">1:30</div><button id="timerStartBtn">Start clock</button><button id="timerPauseBtn">Pause</button><button id="timerResetBtn">Reset clock</button></div></div>
<div class="card no-tv"><div class="big">Shots to clear (3 balls)</div><div class="grid"><button class="shot" data-shots="3">3</button><button class="shot" data-shots="4">4</button><button class="shot" data-shots="5">5</button><button class="shot" data-shots="6">6</button><button class="shot" data-shots="7">7</button><button class="shot" data-shots="8">8</button><button class="shot" data-shots="9">9+</button><button class="shot" data-shots="4" style="visibility:hidden">.</button><button id="timeoutBtn">Timeout / No-show — chip</button></div><div class="row"><button id="undoBtn">Undo last turn</button><button id="endBtn">End night & settle</button><button id="resetBtn">Reset game</button></div></div>
<div class="card" id="settlementPanel" style="display:none"><div class="big">Settlement — operator only</div><div id="settleBody" class="big"></div><div class="row"><button id="revealBtn">Tap to reveal amounts</button></div></div>
<div class="card"><div class="big">Standings — big board</div><table><thead><tr><th>#</th><th>Player</th><th>Chips</th><th>Total</th><th>Last</th><th>Status</th></tr></thead><tbody id="standingsBody"></tbody></table></div>
<div class="card"><div class="big">Last plays</div><div id="logEl" class="log"></div></div>
<div class="card no-tv"><div class="row"><button id="rosterToggleBtn">Edit roster</button><button id="tvBtn">TV mode</button></div><div id="rosterPanel" class="hidden"><div id="rosterList"></div><div class="row"><button id="addPlayerBtn">Add player</button></div><div id="rosterNote" class="log"></div></div></div>
</div>
<script>
const ENTRY=20,START_CHIPS=5,CHIP_SHOT_LIMIT=4,CLOCK_SECONDS=90,EARLY_BONUS=20;
const DEFAULT_NAMES=["Marty","Denise","Ray","Tina","Sal","Jo","Pete","Lou","Carmen","Eddie","Fran","Gus"];
function createState(names){return{players:names.map(function(n,i){return{name:n||("Player "+(i+1)),order:i,chips:START_CHIPS,out:false,totalShots:0,turns:0,lastShots:null,eliminatedAt:0};}),currentIdx:0,round:1,gameOver:false,earlyBonusWinner:null,earlyBonusAmount:0,turnCount:0};}
function rankedPlayers(s){return s.players.slice().sort(function(a,b){if(a.out!==b.out)return a.out?1:-1;if(b.chips!==a.chips)return b.chips-a.chips;if(a.totalShots!==b.totalShots)return a.totalShots-b.totalShots;if((b.eliminatedAt||0)!==(a.eliminatedAt||0))return(b.eliminatedAt||0)-(a.eliminatedAt||0);return a.order-b.order;});}
function awardEarlyBonus(s){var elig=s.players.filter(function(p){return!p.out;});if(!elig.length)return null;elig.sort(function(a,b){return a.totalShots-b.totalShots||b.chips-a.chips||a.order-b.order;});var w=elig[0];s.earlyBonusWinner=w.name;s.earlyBonusAmount=EARLY_BONUS;return w.name;}
function applyTurn(s,shots){var p=s.players[s.currentIdx];if(!p||s.gameOver)return{player:p?p.name:"",shots:shots,chipLost:false,eliminated:false,roundEnded:false,bonus:null,gameOver:true,round:s.round};s.turnCount++;p.turns++;var chipLost=false;if(shots===null){p.lastShots="TO";p.totalShots+=9;chipLost=true;}else{p.lastShots=shots;p.totalShots+=shots;if(shots>CHIP_SHOT_LIMIT)chipLost=true;}if(chipLost)p.chips=Math.max(0,p.chips-1);var eliminated=false;if(p.chips<=0&&!p.out){p.out=true;p.eliminatedAt=s.turnCount;eliminated=true;}var roundEnded=false,bonus=null,next=-1,i;for(i=s.currentIdx+1;i<s.players.length;i++){if(!s.players[i].out){next=i;break;}}if(next!==-1){s.currentIdx=next;}else{var first=-1;for(var j=0;j<s.players.length;j++){if(!s.players[j].out){first=j;break;}}if(first!==-1){s.currentIdx=first;s.round++;roundEnded=true;if(s.round===3&&!s.earlyBonusWinner)bonus=awardEarlyBonus(s);}}var inCount=s.players.filter(function(x){return!x.out;}).length;if(inCount<=1)s.gameOver=true;return{player:p.name,shots:shots,chipLost:chipLost,eliminated:eliminated,roundEnded:roundEnded,bonus:bonus,gameOver:s.gameOver,round:s.round};}
function calcPayouts(s){var pot=s.players.length*ENTRY,bonus=s.earlyBonusWinner?s.earlyBonusAmount:0,remaining=pot-bonus,ranked=rankedPlayers(s),winner=ranked[0]||null,second=ranked[1]||null,winnerShare=0,secondShare=0;if(winner){if(second){winnerShare=Math.round(remaining*0.8);secondShare=remaining-winnerShare;}else winnerShare=remaining;}return{pot:pot,bonus:bonus,bonusWinner:s.earlyBonusWinner,remaining:remaining,winner:winner?winner.name:null,winnerShare:winnerShare,second:second?second.name:null,secondShare:secondShare,ranked:ranked};}
function chipDots(n){var s="";for(var i=0;i<START_CHIPS;i++)s+=i<n?"●":"○";return s;}
function escapeHtml(t){return String(t).replace(/[&<>"']/g,function(c){return{"&":"&","<":"<",">":">",'"':""","'":"'"}[c];});}
let state=createState(DEFAULT_NAMES),history=[],log=["Game ready — "+state.players.length+" players, pot $"+(state.players.length*ENTRY)+". First player up."],endedEarly=false,revealed=false,lockout=false,timeLeft=CLOCK_SECONDS,timerId=null;
const $=function(id){return document.getElementById(id);};
const roundEl=$("roundEl"),potEl=$("potEl"),inEl=$("inEl"),curName=$("curName"),curChips=$("curChips"),curTotal=$("curTotal"),curLast=$("curLast"),timerEl=$("timerEl"),standingsBody=$("standingsBody"),logEl=$("logEl"),undoBtn=$("undoBtn"),settlementPanel=$("settlementPanel"),settleBody=$("settleBody"),revealBtn=$("revealBtn"),endBtn=$("endBtn"),resetBtn=$("resetBtn");
function updateTimer(){timerEl.textContent=Math.floor(timeLeft/60)+":"+String(timeLeft%60).padStart(2,"0");timerEl.className="timer"+(timeLeft===0?" zero":timeLeft<=30?" warn":"");}
function startTimer(){if(timerId)return;timerId=setInterval(function(){if(timeLeft>0){timeLeft--;updateTimer();}},1000);updateTimer();}
function pauseTimer(){if(timerId){clearInterval(timerId);timerId=null;}}
function resetTimer(auto){timeLeft=CLOCK_SECONDS;updateTimer();if(auto)startTimer();else pauseTimer();}
function showSettlement(){var pay=calcPayouts(state),amt=function(v){return revealed?"$"+v:"$•••";};settlementPanel.style.display="block";settleBody.innerHTML="<div>Pot: "+amt(pay.pot)+"</div>"+(pay.bonus?"<div>Early bonus: "+escapeHtml(pay.bonusWinner)+" — "+amt(pay.bonus)+"</div>":"<div>Early bonus: none</div>")+"<div>Winner: "+escapeHtml(pay.winner||"—")+" — "+amt(pay.winnerShare)+"</div><div>Second: "+escapeHtml(pay.second||"—")+" — "+amt(pay.secondShare)+"</div><div style='font-size:.7em'>Check: "+(pay.bonus+pay.winnerShare+pay.secondShare)+" of "+pay.pot+"</div>";revealBtn.textContent=revealed?"Hide amounts":"Tap to reveal amounts";}
function render(){roundEl.textContent=state.round;potEl.textContent="$"+(state.players.length*ENTRY);var inCount=state.players.filter(function(p){return!p.out;}).length;inEl.textContent=inCount+" / "+state.players.length;if(state.gameOver||endedEarly){curName.textContent="Night over";curChips.textContent="—";curTotal.textContent="—";curLast.textContent="—";}else{var p=state.players[state.currentIdx];curName.textContent=p.name;curChips.textContent=chipDots(p.chips);curTotal.textContent=p.totalShots;curLast.textContent=p.lastShots===null?"—":p.lastShots;}var ranked=rankedPlayers(state);standingsBody.innerHTML=ranked.map(function(p,i){var isCur=!(state.gameOver||endedEarly)&&state.players[state.currentIdx]===p;return"<tr class=\""+(p.out?"out ":"")+(isCur?"cur":"")+"\"><td>"+(i+1)+"</td><td>"+escapeHtml(p.name)+"</td><td>"+chipDots(p.chips)+"</td><td>"+p.totalShots+"</td><td>"+(p.lastShots===null?"—":p.lastShots)+"</td><td>"+(p.out?"OUT":"IN")+"</td></tr>";}).join("");undoBtn.disabled=history.length===0;var dis=lockout||state.gameOver||endedEarly;document.querySelectorAll(".shot").forEach(function(b){b.disabled=dis;});$("timeoutBtn").disabled=dis;logEl.innerHTML=log.slice(-6).reverse().map(function(x){return"<div>"+escapeHtml(x)+"</div>";}).join("");if(state.gameOver||endedEarly)showSettlement();else settlementPanel.style.display="none";}
function doTurn(shots){if(lockout||state.gameOver||endedEarly)return;history.push({state:JSON.parse(JSON.stringify(state)),log:log.slice(),endedEarly:endedEarly});var p=state.players[state.currentIdx],before=p.chips,roundBefore=state.round,res=applyTurn(state,shots),desc;if(shots===null)desc="R"+roundBefore+" "+res.player+": timeout — chip lost ("+before+"→"+p.chips+")";else if(shots>4)desc="R"+roundBefore+" "+res.player+": "+shots+" shots — chip lost ("+before+"→"+p.chips+")";else desc="R"+roundBefore+" "+res.player+": "+shots+" shots — no chip ("+p.chips+" left)";if(res.eliminated)desc+=" — OUT";if(res.bonus)desc+=" — Early bonus $"+EARLY_BONUS+" to "+res.bonus;if(res.gameOver)desc+=" — Game over";log.push(desc);lockout=true;resetTimer(!state.gameOver);render();setTimeout(function(){lockout=false;render();},600);}
document.querySelectorAll(".shot").forEach(function(b){b.addEventListener("click",function(){doTurn(parseInt(b.getAttribute("data-shots"),10));});});
$("timeoutBtn").addEventListener("click",function(){doTurn(null);});
undoBtn.addEventListener("click",function(){if(!history.length)return;var prev=history.pop();state=prev.state;log=prev.log;endedEarly=prev.endedEarly;log.push("Undo — last turn taken back.");resetTimer(false);render();});
$("timerStartBtn").addEventListener("click",startTimer);$("timerPauseBtn").addEventListener("click",pauseTimer);$("timerResetBtn").addEventListener("click",function(){resetTimer(false);});
let endArmed=false,endTimer=null;endBtn.addEventListener("click",function(){if(!endArmed){endArmed=true;endBtn.textContent="Tap again to end night";endTimer=setTimeout(function(){endArmed=false;endBtn.textContent="End night & settle";},3000);return;}clearTimeout(endTimer);endArmed=false;endBtn.textContent="End night & settle";endedEarly=true;pauseTimer();render();});
let resetArmed=false,resetTimerId=null;resetBtn.addEventListener("click",function(){if(!resetArmed){resetArmed=true;resetBtn.textContent="Tap again to reset";resetTimerId=setTimeout(function(){resetArmed=false;resetBtn.textContent="Reset game";},3000);return;}clearTimeout(resetTimerId);resetArmed=false;resetBtn.textContent="Reset game";state=createState(DEFAULT_NAMES);history=[];log=["New game — "+state.players.length+" players, pot $"+(state.players.length*ENTRY)+"."];endedEarly=false;revealed=false;resetTimer(false);renderRoster();render();});
revealBtn.addEventListener("click",function(){revealed=!revealed;showSettlement();});
const rosterPanel=$("rosterPanel"),rosterList=$("rosterList"),rosterNote=$("rosterNote"),addPlayerBtn=$("addPlayerBtn");
$("rosterToggleBtn").addEventListener("click",function(){rosterPanel.classList.toggle("hidden");renderRoster();});
$("tvBtn").addEventListener("click",function(){document.body.classList.toggle("tv");this.textContent=document.body.classList.contains("tv")?"Exit TV mode":"TV mode";});
function renderRoster(){rosterList.innerHTML="";state.players.forEach(function(p,i){var row=document.createElement("div");row.className="rosterRow";var input=document.createElement("input");input.value=p.name;input.disabled=state.turnCount>0;input.addEventListener("input",function(){p.name=input.value;render();});var del=document.createElement("button");del.textContent="✕";del.disabled=state.turnCount>0;del.addEventListener("click",function(){if(state.players.length<=2)return;state.players.splice(i,1);state.players.forEach(function(q,j){q.order=j;});renderRoster();render();});row.appendChild(input);row.appendChild(del);rosterList.appendChild(row);});rosterNote.textContent=state.turnCount>0?"Roster locked after first turn.":"Edit names before first turn. Pot updates with player count.";addPlayerBtn.disabled=state.turnCount>0;}
addPlayerBtn.addEventListener("click",function(){if(state.turnCount>0)return;state.players.push({name:"Player "+(state.players.length+1),order:state.players.length,chips:START_CHIPS,out:false,totalShots:0,turns:0,lastShots:null,eliminatedAt:0});renderRoster();render();});
function tryWake(){try{if(navigator.wakeLock)navigator.wakeLock.request("screen").catch(function(){});}catch(e){}}
document.addEventListener("pointerdown",tryWake);
resetTimer(false);renderRoster();render();
</script>
</body>
</html>
<!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 — Playback</title>
<style>
*{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:#0b0f14;color:#f2f5f7}header{padding:12px 16px;background:#111a24;display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center}h1{font-size:clamp(20px,4vw,34px);margin:0}.meta{font-size:clamp(16px,3vw,24px)}.wrap{padding:12px;max-width:1100px;margin:auto}.card{background:#16222e;border:2px solid #2a3d4f;border-radius:14px;padding:14px;margin-bottom:12px}.big{font-size:clamp(20px,4vw,30px)}.stepDesc{font-size:clamp(24px,5vw,42px);font-weight:800;line-height:1.3;min-height:2.6em;margin-top:8px}button{font:inherit;min-height:56px;border-radius:12px;border:2px solid #3a556e;background:#1e3247;color:#fff;padding:10px 14px;font-size:20px;touch-action:manipulation;cursor:pointer}button:active{background:#2c4a66}.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px;align-items:center}table{width:100%;border-collapse:collapse;font-size:clamp(16px,3.2vw,26px)}th,td{padding:8px 6px;border-bottom:1px solid #2a3d4f;text-align:left}tr.out{opacity:.45;text-decoration:line-through}tr.cur{background:#243a4f}.log{font-size:clamp(15px,3vw,22px);line-height:1.4;max-height:320px;overflow:auto}.log div{padding:4px 0;border-bottom:1px solid #223}.log div.now{background:#243a4f;font-weight:700}input[type=range]{width:100%;min-height:48px}label.big{display:block;width:100%}#payoutPanel{border-color:#ffb020}
</style>
</head>
<body>
<header><h1>Thursday 3-Ball — Playback</h1><div class="meta">Round <span id="roundEl">1</span> · Pot <span id="potEl">$240</span> · In <span id="inEl">12 / 12</span> · Early bonus <span id="bonusEl">—</span></div></header>
<div class="wrap">
<div class="card"><div class="big" id="stepCount">Step 1</div><div id="stepDesc" class="stepDesc"></div></div>
<div class="card"><div class="big">Scorecard</div><table><thead><tr><th>Player</th><th>Chips</th><th>Total</th><th>Last</th><th>Turns</th><th>Status</th></tr></thead><tbody id="scoreBody"></tbody></table></div>
<div class="card" id="payoutPanel" style="display:none"><div class="big">Final payouts</div><div id="payoutBody" class="big"></div></div>
<div class="card"><div class="row"><button id="restartBtn">Restart</button><button id="prevBtn">◀ Prev</button><button id="playBtn">Play</button><button id="nextBtn">Next ▶</button></div><div class="row"><label class="big">Speed <span id="speedLabel">1.00s/step</span></label><input type="range" id="speedSlider" min="0" max="100" value="50"></div><div class="row"><label class="big">Scrub to step</label><input type="range" id="scrubSlider" min="0" max="0" value="0"></div></div>
<div class="card"><div class="big">What happened</div><div id="logEl" class="log"></div></div>
</div>
<script>
const ENTRY=20,START_CHIPS=5,CHIP_SHOT_LIMIT=4,CLOCK_SECONDS=90,EARLY_BONUS=20;
const DEFAULT_NAMES=["Marty","Denise","Ray","Tina","Sal","Jo","Pete","Lou","Carmen","Eddie","Fran","Gus"];
function createState(names){return{players:names.map(function(n,i){return{name:n||("Player "+(i+1)),order:i,chips:START_CHIPS,out:false,totalShots:0,turns:0,lastShots:null,eliminatedAt:0};}),currentIdx:0,round:1,gameOver:false,earlyBonusWinner:null,earlyBonusAmount:0,turnCount:0};}
function rankedPlayers(s){return s.players.slice().sort(function(a,b){if(a.out!==b.out)return a.out?1:-1;if(b.chips!==a.chips)return b.chips-a.chips;if(a.totalShots!==b.totalShots)return a.totalShots-b.totalShots;if((b.eliminatedAt||0)!==(a.eliminatedAt||0))return(b.eliminatedAt||0)-(a.eliminatedAt||0);return a.order-b.order;});}
function awardEarlyBonus(s){var elig=s.players.filter(function(p){return!p.out;});if(!elig.length)return null;elig.sort(function(a,b){return a.totalShots-b.totalShots||b.chips-a.chips||a.order-b.order;});var w=elig[0];s.earlyBonusWinner=w.name;s.earlyBonusAmount=EARLY_BONUS;return w.name;}
function applyTurn(s,shots){var p=s.players[s.currentIdx];if(!p||s.gameOver)return{player:p?p.name:"",shots:shots,chipLost:false,eliminated:false,roundEnded:false,bonus:null,gameOver:true,round:s.round};s.turnCount++;p.turns++;var chipLost=false;if(shots===null){p.lastShots="TO";p.totalShots+=9;chipLost=true;}else{p.lastShots=shots;p.totalShots+=shots;if(shots>CHIP_SHOT_LIMIT)chipLost=true;}if(chipLost)p.chips=Math.max(0,p.chips-1);var eliminated=false;if(p.chips<=0&&!p.out){p.out=true;p.eliminatedAt=s.turnCount;eliminated=true;}var roundEnded=false,bonus=null,next=-1,i;for(i=s.currentIdx+1;i<s.players.length;i++){if(!s.players[i].out){next=i;break;}}if(next!==-1){s.currentIdx=next;}else{var first=-1;for(var j=0;j<s.players.length;j++){if(!s.players[j].out){first=j;break;}}if(first!==-1){s.currentIdx=first;s.round++;roundEnded=true;if(s.round===3&&!s.earlyBonusWinner)bonus=awardEarlyBonus(s);}}var inCount=s.players.filter(function(x){return!x.out;}).length;if(inCount<=1)s.gameOver=true;return{player:p.name,shots:shots,chipLost:chipLost,eliminated:eliminated,roundEnded:roundEnded,bonus:bonus,gameOver:s.gameOver,round:s.round};}
function calcPayouts(s){var pot=s.players.length*ENTRY,bonus=s.earlyBonusWinner?s.earlyBonusAmount:0,remaining=pot-bonus,ranked=rankedPlayers(s),winner=ranked[0]||null,second=ranked[1]||null,winnerShare=0,secondShare=0;if(winner){if(second){winnerShare=Math.round(remaining*0.8);secondShare=remaining-winnerShare;}else winnerShare=remaining;}return{pot:pot,bonus:bonus,bonusWinner:s.earlyBonusWinner,remaining:remaining,winner:winner?winner.name:null,winnerShare:winnerShare,second:second?second.name:null,secondShare:secondShare,ranked:ranked};}
function chipDots(n){var s="";for(var i=0;i<START_CHIPS;i++)s+=i<n?"●":"○";return s;}
function escapeHtml(t){return String(t).replace(/[&<>"']/g,function(c){return{"&":"&","<":"<",">":">",'"':""","'":"'"}[c];});}
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;};}
var rng=mulberry32(20240917);
function pickOutcome(r){var x=r();if(x<0.02)return null;if(x<0.27)return 3;if(x<0.62)return 4;if(x<0.82)return 5;if(x<0.92)return 6;if(x<0.97)return 7;if(x<0.995)return 8;return 9;}
function buildSteps(){var s=createState(DEFAULT_NAMES),steps=[];function snap(desc){steps.push({desc:desc,state:JSON.parse(JSON.stringify(s))});}snap("Night starts — "+s.players.length+" players, $"+ENTRY+" each, pot $"+(s.players.length*ENTRY)+". Everyone starts with "+START_CHIPS+" chips. Clear in 4 or fewer, no chip lost. 5+ or timeout, lose a chip.");var guard=0;while(!s.gameOver&&guard<500){guard++;var p=s.players[s.currentIdx];snap("Round "+s.round+" — "+p.name+" to play. Chips: "+p.chips+" · Total shots: "+p.totalShots+" · Last: "+(p.lastShots===null?"—":p.lastShots));var shots=pickOutcome(rng),beforeChips=p.chips,res=applyTurn(s,shots),desc;if(shots===null)desc=p.name+" — timeout / no-show. Chip lost ("+beforeChips+" → "+p.chips+").";else if(shots>4)desc=p.name+" cleared in "+shots+" shots. Over 4, chip lost ("+beforeChips+" → "+p.chips+").";else desc=p.name+" cleared in "+shots+" shots. No chip lost ("+p.chips+" left).";if(res.eliminated)desc+=" "+p.name+" is OUT.";if(res.roundEnded)desc+=" Round "+(s.round-1)+" ends.";if(res.bonus)desc+=" Early bonus: "+res.bonus+" gets $"+EARLY_BONUS+" for best total after Round 2.";snap(desc);}var pay=calcPayouts(s);snap("Game over — "+pay.winner+" wins. Pot $"+pay.pot+(pay.bonus?" · Early bonus $"+pay.bonus+" to "+pay.bonusWinner:"")+" · Winner $"+pay.winnerShare+" · Second $"+pay.secondShare+" to "+pay.second+".");return steps;}
const steps=buildSteps();let idx=0,playing=false,playTimer=null;
const $=function(id){return document.getElementById(id);};
const stepDesc=$("stepDesc"),stepCount=$("stepCount"),roundEl=$("roundEl"),potEl=$("potEl"),inEl=$("inEl"),bonusEl=$("bonusEl"),scoreBody=$("scoreBody"),logEl=$("logEl"),playBtn=$("playBtn"),prevBtn=$("prevBtn"),nextBtn=$("nextBtn"),restartBtn=$("restartBtn"),speedSlider=$("speedSlider"),speedLabel=$("speedLabel"),scrubSlider=$("scrubSlider"),payoutPanel=$("payoutPanel"),payoutBody=$("payoutBody");
scrubSlider.max=steps.length-1;
function delayFromSlider(){return Math.round(2000-(+speedSlider.value/100)*1850);}
function render(){var step=steps[idx],s=step.state;stepDesc.textContent=step.desc;stepCount.textContent="Step "+(idx+1)+" / "+steps.length;scrubSlider.value=idx;roundEl.textContent=s.round;potEl.textContent="$"+(s.players.length*ENTRY);inEl.textContent=s.players.filter(function(p){return!p.out;}).length+" / "+s.players.length;bonusEl.textContent=s.earlyBonusWinner?s.earlyBonusWinner+" $"+s.earlyBonusAmount:"—";scoreBody.innerHTML=s.players.map(function(p){var isCur=!s.gameOver&&s.players[s.currentIdx]===p;return"<tr class=\""+(p.out?"out ":"")+(isCur?"cur":"")+"\"><td>"+escapeHtml(p.name)+"</td><td>"+chipDots(p.chips)+"</td><td>"+p.totalShots+"</td><td>"+(p.lastShots===null?"—":p.lastShots)+"</td><td>"+p.turns+"</td><td>"+(p.out?"OUT":"IN")+"</td></tr>";}).join("");logEl.innerHTML=steps.slice(0,idx+1).map(function(st,i){return"<div class=\""+(i===idx?"now":"")+"\">"+(i+1)+". "+escapeHtml(st.desc)+"</div>";}).join("");logEl.scrollTop=logEl.scrollHeight;if(s.gameOver){var pay=calcPayouts(s);payoutPanel.style.display="block";payoutBody.innerHTML="<div>Pot: $"+pay.pot+"</div>"+(pay.bonus?"<div>Early bonus: "+escapeHtml(pay.bonusWinner)+" — $"+pay.bonus+"</div>":"<div>Early bonus: none</div>")+"<div>Winner: "+escapeHtml(pay.winner||"—")+" — $"+pay.winnerShare+"</div><div>Second: "+escapeHtml(pay.second||"—")+" — $"+pay.secondShare+"</div>";}else payoutPanel.style.display="none";playBtn.textContent=playing?"Pause":"Play";}
function schedule(){clearTimeout(playTimer);if(!playing)return;playTimer=setTimeout(function(){if(idx<steps.length-1){idx++;render();schedule();}else{playing=false;render();}},delayFromSlider());}
playBtn.addEventListener("click",function(){if(idx>=steps.length-1&&!playing)idx=0;playing=!playing;render();schedule();});
prevBtn.addEventListener("click",function(){playing=false;clearTimeout(playTimer);if(idx>0)idx--;render();});
nextBtn.addEventListener("click",function(){playing=false;clearTimeout(playTimer);if(idx<steps.length-1)idx++;render();});
restartBtn.addEventListener("click",function(){playing=false;clearTimeout(playTimer);idx=0;render();});
speedSlider.addEventListener("input",function(){speedLabel.textContent=(delayFromSlider()/1000).toFixed(2)+"s/step";if(playing)schedule();});
scrubSlider.addEventListener("input",function(){playing=false;clearTimeout(playTimer);idx=+scrubSlider.value;render();});
speedLabel.textContent=(delayFromSlider()/1000).toFixed(2)+"s/step";render();
</script>
</body>
</html>