47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
16:54Stealth Union Alpha: Is this another cheap GPT model? (better than Luna, cheaper than Terra?)
Sep 16, 2026 · YouTube
16:45DeepSeek V4.1 Flash Is Shockingly Good for the Price
Sep 10, 2026 · YouTube
17:37Tencent Hy4 Preview Tested: Is It Really Better Than Kimi K3 and GLM 5.3?
Aug 28, 2026 · YouTube
23:58Qwen 3.8 Flash Tested: UI, Coding, Physics & more - compared to Opus 5, Sol Pro, Grok
Aug 27, 2026 · YouTube
14:50Stealth Ox Alpha: a new GLM model! Full test (coding & writing) against Grok, Gemini, GPT & others
Aug 21, 2026 · YouTube
My buddy runs a bar. I told him I'd build him something for Thursdays. He's the one talking here — I just write down what he says and hand it to you.
It's not real pool. Three balls on the table, you shoot until all three are down, and we count how many shots it took you. Low is good. Everybody takes a turn, then we go around again — that's a round.
Twenty bucks to play. You start with five chips. Take more than four shots to clear the table and it costs you a chip. Out of chips and you're out, go back to drinking. Last one with chips left takes the pot.
There's a clock on your turn — a minute, minute and a half, we've never really settled it. Clock runs out and you're not at the table, that's a chip.
It's a Thursday. Nine o'clock, everybody's had a few, the jukebox is loud and half the room is outside smoking. Twelve, sixteen people usually, however many show up. It runs two, three hours and nobody's in a hurry.
Last year I ran the whole thing on a legal pad and it got away from me around round four. Two guys arguing with me about what they shot, me trying to remember whose turn it was, somebody's beer on the pad. Never again.
I'm running this off my phone, one-handed, standing at the table with a beer in the other. There's a TV over the bar and that's the only screen anybody in the room can see — from across the room, in the dark, so it's got to be big. People want to know where they stand without walking over and asking me.
My phone's on 15% by ten. The bar's wifi is the bar's wifi.
Twenty a head, all of it in the pot. Winner takes it — mostly. We kick a little to second. And there's a side thing we do for whoever's shooting best early on, I never wrote that part down properly.
The app doesn't hold the money. I do. But at the end of the night it has to tell me what I owe who, and I'm reading that off my phone with sixteen people standing around the table.
Two pages.
The console — the thing I run it from, and the thing that goes up on the TV. Open it and there's already a game going. I'm not typing in twelve names to find out whether it works.
The playback — a separate page that plays a whole night through, start to finish, so I can watch it work before Thursday. One step at a time, a step being one thing happening, with the scorecard filling in as it goes and a slider for how fast it runs. I want to slow it down, back it up, and read what just happened. Show me the way a night actually goes.
The playback runs the same rules the console runs. If the two of them drift apart, the playback is a cartoon and it's no use to me.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — these run in a sandboxed frame where those throw.html blocks: the console first, the playback second. Leave yourself room for the second one.I know I wasn't clear about everything. Everything you had to decide for me, one line each — what you decided and why. Short.
The first fenced html block.
The second fenced html block.
It's Thursday, it's nine o'clock, and I'm running this for real. What comes up that this doesn't handle? Be straight with me. I'd much rather hear it now than find out with sixteen people around a pool table and three hundred dollars sitting on the bar.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Thursday 3-Ball Console</title>
<style>
:root {
--bg: #0b0c10;
--card: #16181f;
--card-border: #262936;
--text: #f0f2f5;
--text-dim: #8b92a5;
--accent: #f59e0b;
--accent-glow: rgba(245, 158, 11, 0.25);
--green: #10b981;
--red: #ef4444;
--blue: #38bdf8;
--chip-gold: #fbbf24;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; -webkit-tap-highlight-color: transparent; }
body { background: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; overflow-x: hidden; }
/* HEADER & TV STATS BAR */
header {
background: #101218;
border-bottom: 2px solid var(--card-border);
padding: 12px 18px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.pot-banner { display: flex; gap: 16px; align-items: baseline; }
.pot-total { font-size: 2rem; font-weight: 900; color: var(--chip-gold); text-shadow: 0 0 12px rgba(251, 191, 36, 0.4); }
.pot-label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); }
.header-badges { display: flex; gap: 8px; align-items: center; }
.badge { background: #202430; padding: 4px 10px; border-radius: 6px; font-size: 0.85rem; font-weight: 700; }
.badge.round { color: var(--accent); border: 1px solid var(--accent); }
/* MAIN LAYOUT: Split for Big Screen / Handheld */
.layout { display: grid; grid-template-columns: 1fr; gap: 14px; padding: 14px; flex: 1; max-width: 1400px; margin: 0 auto; width: 100%; }
@media (min-width: 900px) {
.layout { grid-template-columns: 420px 1fr; align-items: start; }
}
/* CURRENT TURN CONTROLLER (Thumb Optimized) */
.turn-panel {
background: var(--card);
border: 2px solid var(--accent);
border-radius: 14px;
padding: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
gap: 12px;
}
.turn-header { display: flex; justify-content: space-between; align-items: baseline; }
.now-shooting { font-size: 0.8rem; text-transform: uppercase; color: var(--accent); font-weight: 800; letter-spacing: 1.5px; }
.turn-name { font-size: 2.2rem; font-weight: 900; line-height: 1.1; color: #fff; word-break: break-word; }
.chips-display { display: flex; align-items: center; gap: 6px; margin-top: 4px; }
.chip-dot { width: 18px; height: 18px; border-radius: 50%; background: var(--chip-gold); box-shadow: 0 0 6px var(--chip-gold); }
.chip-dot.lost { background: #2f3442; box-shadow: none; }
/* SHOT BUTTONS */
.shot-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 6px; }
.btn-shot {
background: #232734;
border: 2px solid #3b4256;
color: #fff;
font-size: 1.7rem;
font-weight: 900;
padding: 14px 0;
border-radius: 10px;
cursor: pointer;
touch-action: manipulation;
transition: transform 0.05s, background 0.1s;
}
.btn-shot:active { transform: scale(0.95); background: #3b4256; }
.btn-shot.safe { border-color: var(--green); color: var(--green); }
.btn-shot.penalty { border-color: var(--red); color: var(--red); }
.btn-timeout {
grid-column: span 4;
background: #451015;
border: 2px solid var(--red);
color: #fca5a5;
font-size: 1.1rem;
font-weight: 800;
padding: 12px;
border-radius: 10px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-timeout:active { background: var(--red); color: #fff; }
/* TIMER BAR */
.timer-box {
background: #0f1117;
border-radius: 10px;
padding: 10px 14px;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid #232734;
}
.timer-val { font-size: 2.2rem; font-weight: 900; font-variant-numeric: tabular-nums; font-family: monospace; color: var(--blue); }
.timer-val.urgent { color: var(--red); animation: blink 0.8s infinite; }
@keyframes blink { 50% { opacity: 0.4; } }
.timer-controls { display: flex; gap: 6px; }
.btn-time {
background: #232734;
border: 1px solid #475069;
color: #fff;
padding: 8px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 0.85rem;
cursor: pointer;
}
/* ACTION UTILS */
.action-bar { display: flex; gap: 8px; justify-content: space-between; }
.btn-util {
flex: 1;
background: #1c1f2b;
border: 1px solid #363d52;
color: var(--text-dim);
padding: 10px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
}
.btn-util.undo { color: #fca5a5; border-color: #7f1d1d; }
.btn-util:active { background: #2e3547; }
/* SCOREBOARD / TV LEADERBOARD */
.board-panel {
background: var(--card);
border: 1px solid var(--card-border);
border-radius: 14px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.board-title { font-size: 1.1rem; font-weight: 800; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); display: flex; justify-content: space-between; }
.standings-table { width: 100%; border-collapse: collapse; text-align: left; }
.standings-table th { padding: 8px; font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; border-bottom: 2px solid #232734; }
.standings-table td { padding: 12px 8px; border-bottom: 1px solid #1a1d27; font-size: 1.1rem; vertical-align: middle; }
.player-row.active { background: rgba(245, 158, 11, 0.12); border-left: 4px solid var(--accent); }
.player-row.eliminated { opacity: 0.35; filter: grayscale(1); }
.player-row.out-rank td { text-decoration: line-through; }
.p-name { font-weight: 800; font-size: 1.25rem; }
.p-chips { font-weight: 900; color: var(--chip-gold); font-size: 1.3rem; }
.p-history { font-size: 0.85rem; font-family: monospace; color: var(--text-dim); }
.shot-pill { display: inline-block; padding: 2px 6px; border-radius: 4px; margin-right: 4px; font-weight: 700; font-size: 0.8rem; }
.shot-pill.safe { background: rgba(16, 185, 129, 0.2); color: #34d399; }
.shot-pill.cost { background: rgba(239, 68, 68, 0.2); color: #f87171; }
/* PAYOUT MODAL / SCREEN */
.payout-card {
background: linear-gradient(145deg, #18221b, #0e1612);
border: 2px solid var(--green);
border-radius: 12px;
padding: 16px;
margin-top: 10px;
}
.payout-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-top: 10px; }
.payout-item { background: rgba(0,0,0,0.4); padding: 10px; border-radius: 8px; border-left: 3px solid var(--green); }
.payout-val { font-size: 1.6rem; font-weight: 900; color: #fff; }
.payout-who { font-size: 0.9rem; font-weight: 700; color: #a7f3d0; margin-top: 2px; }
/* ROSTER DRAWER / SETUP */
.setup-card {
background: #11131a;
border: 1px dashed #343b4f;
border-radius: 12px;
padding: 14px;
display: none;
margin-bottom: 12px;
}
.setup-card.open { display: block; }
.player-input-row { display: flex; gap: 8px; margin-top: 8px; }
.txt-input { flex: 1; background: #1c202d; border: 1px solid #363e56; color: #fff; padding: 10px; border-radius: 6px; font-size: 1rem; }
.btn-add { background: var(--green); border: none; color: #fff; padding: 0 16px; border-radius: 6px; font-weight: 800; cursor: pointer; }
.tag-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.tag { background: #232838; padding: 4px 8px; border-radius: 4px; font-size: 0.85rem; display: flex; align-items: center; gap: 6px; }
.tag-del { color: var(--red); cursor: pointer; font-weight: 900; }
</style>
</head>
<body>
<header>
<div class="pot-banner">
<div>
<div class="pot-label">Total Pot ($20/head)</div>
<div class="pot-total" id="potDisplay">$240</div>
</div>
<div class="header-badges">
<div class="badge round" id="roundBadge">ROUND 1</div>
<div class="badge" id="aliveBadge">12 In</div>
</div>
</div>
<div style="display:flex; gap:8px;">
<button class="btn-util" onclick="toggleSetup()">⚙️ Roster</button>
<button class="btn-util undo" onclick="undoAction()">↩ Undo</button>
</div>
</header>
<div class="layout">
<!-- LEFT / PHONE CONTROLLER -->
<div>
<!-- ROSTER EDIT DRAWER -->
<div class="setup-card" id="setupDrawer">
<div style="font-weight:800; font-size:0.9rem; color:var(--text-dim); text-transform:uppercase;">Player Roster (Edit Before or Mid-Game)</div>
<div class="player-input-row">
<input type="text" id="newPlayerName" class="txt-input" placeholder="New player name..." maxlength="14" onkeydown="if(event.key==='Enter') addPlayer()">
<button class="btn-add" onclick="addPlayer()">Add</button>
</div>
<div class="tag-list" id="rosterTags"></div>
</div>
<!-- MAIN SHOOTING CONTROLLER -->
<div class="turn-panel" id="turnPanel">
<div class="turn-header">
<div class="now-shooting">At Table</div>
<div id="onDeckText" style="font-size:0.85rem; color:var(--text-dim);">On deck: Sully</div>
</div>
<div class="turn-name" id="activePlayerName">Big Mike</div>
<div class="chips-display" id="activeChipsDots">
<div class="chip-dot"></div><div class="chip-dot"></div><div class="chip-dot"></div><div class="chip-dot"></div><div class="chip-dot"></div>
</div>
<!-- TIMER -->
<div class="timer-box">
<div>
<div style="font-size:0.7rem; color:var(--text-dim); text-transform:uppercase;">Turn Clock</div>
<div class="timer-val" id="timerDisplay">60</div>
</div>
<div class="timer-controls">
<button class="btn-time" onclick="toggleTimer()" id="timerToggleBtn">Pause</button>
<button class="btn-time" onclick="resetTimer(60)">60s</button>
<button class="btn-time" onclick="bumpTimer(15)">+15s</button>
</div>
</div>
<!-- ONE TOUCH SHOT ENTRY -->
<div style="font-size:0.75rem; text-transform:uppercase; color:var(--text-dim); font-weight:700; margin-top:2px;">
Record Shots (1-4 Safe • 5+ Costs 1 Chip)
</div>
<div class="shot-grid">
<button class="btn-shot safe" onclick="recordTurn(1)">1</button>
<button class="btn-shot safe" onclick="recordTurn(2)">2</button>
<button class="btn-shot safe" onclick="recordTurn(3)">3</button>
<button class="btn-shot safe" onclick="recordTurn(4)">4</button>
<button class="btn-shot penalty" onclick="recordTurn(5)">5</button>
<button class="btn-shot penalty" onclick="recordTurn(6)">6</button>
<button class="btn-shot penalty" onclick="recordTurn(7)">7</button>
<button class="btn-shot penalty" onclick="recordTurn(8)">8+</button>
<button class="btn-timeout" onclick="recordTurn('T')">⏱ Turn Expired / Forfeit (-1 Chip)</button>
</div>
</div>
<!-- PAYOUT SUMMARY CARD -->
<div class="payout-card" id="payoutSection">
<div style="display:flex; justify-content:space-between; align-items:baseline;">
<span style="font-weight:900; color:#34d399; font-size:1.1rem; text-transform:uppercase;">💰 Cash Payouts</span>
<span style="font-size:0.8rem; color:var(--text-dim);" id="payoutStatus">Live Estimate</span>
</div>
<div class="payout-grid">
<div class="payout-item">
<div class="pot-label">1st Place (70%)</div>
<div class="payout-val" id="pay1stVal">$168</div>
<div class="payout-who" id="pay1stWho">--</div>
</div>
<div class="payout-item">
<div class="pot-label">2nd Place (20%)</div>
<div class="payout-val" id="pay2ndVal">$48</div>
<div class="payout-who" id="pay2ndWho">--</div>
</div>
<div class="payout-item">
<div class="pot-label">Hot Hand (10%)</div>
<div class="payout-val" id="payHotVal">$24</div>
<div class="payout-who" id="payHotWho">Best R1-3 Score</div>
</div>
</div>
</div>
</div>
<!-- RIGHT / TV LEADERBOARD -->
<div class="board-panel">
<div class="board-title">
<span>Standings & Card</span>
<span style="font-size:0.8rem; color:var(--text-dim);" id="elimSummary">0 Eliminated</span>
</div>
<table class="standings-table">
<thead>
<tr>
<th>Shooter</th>
<th style="text-align:center;">Chips</th>
<th>Round Card</th>
</tr>
</thead>
<tbody id="standingsBody">
<!-- Injected via JS -->
</tbody>
</table>
</div>
</div>
<script>
// INITIAL DATA (12 Bar Regulars)
const DEFAULT_NAMES = ["Big Mike", "Sully", "Dave R.", "Katie", "Mac", "Jenna", "Tommy", "Red", "Preacher", "Cole", "Shorty", "Doc"];
let state = {
players: DEFAULT_NAMES.map((name, id) => ({
id: id + 1,
name: name,
chips: 5,
history: [], // array of shot counts or 'T'
eliminatedRound: null,
active: true
})),
currentRound: 1,
currentTurnIdx: 0,
gameOver: false,
historyStack: [] // For Undo support
};
// TIMER STATE
let timerVal = 60;
let timerInterval = null;
let timerRunning = false;
function startTimer() {
if (timerInterval) clearInterval(timerInterval);
timerRunning = true;
document.getElementById('timerToggleBtn').textContent = 'Pause';
timerInterval = setInterval(() => {
if (timerVal > 0) {
timerVal--;
updateTimerUI();
} else {
clearInterval(timerInterval);
timerRunning = false;
document.getElementById('timerToggleBtn').textContent = 'Start';
document.getElementById('timerDisplay').classList.add('urgent');
}
}, 1000);
}
function pauseTimer() {
clearInterval(timerInterval);
timerRunning = false;
document.getElementById('timerToggleBtn').textContent = 'Start';
}
function toggleTimer() {
if (timerRunning) pauseTimer();
else startTimer();
}
function resetTimer(sec = 60) {
pauseTimer();
timerVal = sec;
updateTimerUI();
startTimer();
}
function bumpTimer(sec = 15) {
timerVal += sec;
updateTimerUI();
}
function updateTimerUI() {
const el = document.getElementById('timerDisplay');
el.textContent = timerVal;
if (timerVal <= 10) el.classList.add('urgent');
else el.classList.remove('urgent');
}
// SAVE HISTORY FOR UNDO
function snapshot() {
state.historyStack.push(JSON.stringify({
players: state.players,
currentRound: state.currentRound,
currentTurnIdx: state.currentTurnIdx,
gameOver: state.gameOver
}));
}
function undoAction() {
if (!state.historyStack.length) return;
const prev = JSON.parse(state.historyStack.pop());
state.players = prev.players;
state.currentRound = prev.currentRound;
state.currentTurnIdx = prev.currentTurnIdx;
state.gameOver = prev.gameOver;
resetTimer(60);
render();
}
// GAME CORE LOGIC
function getActiveShooters() {
return state.players.filter(p => p.chips > 0);
}
function recordTurn(shots) {
if (state.gameOver) return;
const actives = getActiveShooters();
if (actives.length === 0) return;
snapshot();
const player = actives[state.currentTurnIdx % actives.length];
const isPenalty = shots === 'T' || shots >= 5;
player.history.push(shots);
if (isPenalty) {
player.chips = Math.max(0, player.chips - 1);
if (player.chips === 0) {
player.eliminatedRound = state.currentRound;
}
}
// Move to next shooter in round
state.currentTurnIdx++;
// Check if round complete
if (state.currentTurnIdx >= actives.length) {
state.currentRound++;
state.currentTurnIdx = 0;
}
// Check Game Over (1 or 0 players left with chips)
const remaining = getActiveShooters();
if (remaining.length <= 1) {
state.gameOver = true;
pauseTimer();
} else {
resetTimer(60);
}
render();
}
// HOT HAND CALCULATION (Lowest shot in rounds 1-3)
function getHotHandWinners() {
let minScore = 999;
let candidates = [];
state.players.forEach(p => {
p.history.slice(0, 3).forEach(s => {
if (typeof s === 'number') {
if (s < minScore) {
minScore = s;
candidates = [p.name];
} else if (s === minScore && !candidates.includes(p.name)) {
candidates.push(p.name);
}
}
});
});
return { score: minScore === 999 ? null : minScore, names: candidates };
}
// PAYOUTS
function updatePayouts() {
const pot = state.players.length * 20;
const pay1st = Math.round(pot * 0.70);
const pay2nd = Math.round(pot * 0.20);
const payHot = pot - pay1st - pay2nd;
document.getElementById('potDisplay').textContent = `$${pot}`;
document.getElementById('pay1stVal').textContent = `$${pay1st}`;
document.getElementById('pay2ndVal').textContent = `$${pay2nd}`;
document.getElementById('payHotVal').textContent = `$${payHot}`;
const remaining = getActiveShooters();
const hot = getHotHandWinners();
if (hot.names.length > 0) {
document.getElementById('payHotWho').textContent = `${hot.names.join(', ')} (${hot.score} shots)`;
} else {
document.getElementById('payHotWho').textContent = "Pending R1-3";
}
if (state.gameOver) {
document.getElementById('payoutStatus').textContent = "FINAL PAYOUT";
if (remaining.length === 1) {
document.getElementById('pay1stWho').textContent = remaining[0].name + " (Winner)";
// Find 2nd place (eliminated in highest round)
const outPlayers = [...state.players].filter(p => p.chips === 0).sort((a,b) => b.eliminatedRound - a.eliminatedRound);
document.getElementById('pay2ndWho').textContent = outPlayers[0] ? outPlayers[0].name : "Split";
} else {
document.getElementById('pay1stWho').textContent = "Tie - Split 1st/2nd";
document.getElementById('pay2ndWho').textContent = "Tie - Split";
}
} else {
document.getElementById('payoutStatus').textContent = "In Progress";
if (remaining.length === 2) {
document.getElementById('pay1stWho').textContent = remaining[0].name + " / " + remaining[1].name;
document.getElementById('pay2ndWho').textContent = "Final 2 Alive";
} else {
document.getElementById('pay1stWho').textContent = `${remaining.length} shooters alive`;
document.getElementById('pay2ndWho').textContent = "TBD";
}
}
}
// RENDER UI
function render() {
const actives = getActiveShooters();
const totalPlayers = state.players.length;
document.getElementById('roundBadge').textContent = `ROUND ${state.currentRound}`;
document.getElementById('aliveBadge').textContent = `${actives.length} / ${totalPlayers} Alive`;
document.getElementById('elimSummary').textContent = `${totalPlayers - actives.length} Eliminated`;
if (!state.gameOver && actives.length > 0) {
const activePlayer = actives[state.currentTurnIdx % actives.length];
const nextPlayer = actives[(state.currentTurnIdx + 1) % actives.length];
document.getElementById('activePlayerName').textContent = activePlayer.name;
document.getElementById('onDeckText').textContent = `On deck: ${nextPlayer.name}`;
// Render chips pips
let dotsHtml = '';
for (let i = 0; i < 5; i++) {
dotsHtml += `<div class="chip-dot ${i < activePlayer.chips ? '' : 'lost'}"></div>`;
}
document.getElementById('activeChipsDots').innerHTML = dotsHtml;
} else if (state.gameOver) {
document.getElementById('activePlayerName').textContent = actives.length ? `${actives[0].name} Wins!` : 'Tournament Over';
document.getElementById('onDeckText').textContent = 'Final Standings Below';
document.getElementById('activeChipsDots').innerHTML = '';
}
// RENDER STANDINGS
const tbody = document.getElementById('standingsBody');
tbody.innerHTML = '';
state.players.forEach((p, idx) => {
const isShootingNow = !state.gameOver && actives.length > 0 && actives[state.currentTurnIdx % actives.length].id === p.id;
const tr = document.createElement('tr');
tr.className = `player-row ${p.chips === 0 ? 'eliminated out-rank' : ''} ${isShootingNow ? 'active' : ''}`;
let historyHtml = p.history.map(s => {
if (s === 'T') return `<span class="shot-pill cost">T</span>`;
return `<span class="shot-pill ${s <= 4 ? 'safe' : 'cost'}">${s}</span>`;
}).join('');
tr.innerHTML = `
<td>
<div class="p-name">${p.name} ${isShootingNow ? '🎱' : ''}</div>
</td>
<td style="text-align:center;">
<span class="p-chips">${'🪙'.repeat(p.chips)}</span>
<span style="font-size:0.8rem; color:var(--text-dim);"> (${p.chips})</span>
</td>
<td>
<div class="p-history">${historyHtml || '<span style="color:#475069;">--</span>'}</div>
</td>
`;
tbody.appendChild(tr);
});
// Roster tag drawer
const tagsContainer = document.getElementById('rosterTags');
tagsContainer.innerHTML = state.players.map(p => `
<div class="tag">
${p.name} (${p.chips}c)
<span class="tag-del" onclick="removePlayer(${p.id})">×</span>
</div>
`).join('');
updatePayouts();
}
function toggleSetup() {
const el = document.getElementById('setupDrawer');
el.classList.toggle('open');
}
function addPlayer() {
const inp = document.getElementById('newPlayerName');
const name = inp.value.trim();
if (!name) return;
snapshot();
state.players.push({
id: Date.now(),
name: name,
chips: 5,
history: [],
eliminatedRound: null,
active: true
});
inp.value = '';
render();
}
function removePlayer(id) {
if (state.players.length <= 2) return;
snapshot();
state.players = state.players.filter(p => p.id !== id);
render();
}
// Init
resetTimer(60);
render();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thursday 3-Ball Full Playback Simulator</title>
<style>
:root {
--bg: #090a0f;
--card: #13161f;
--border: #232838;
--accent: #f59e0b;
--green: #10b981;
--red: #ef4444;
--blue: #38bdf8;
--gold: #fbbf24;
--text: #f3f4f6;
--text-dim: #9ca3af;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; }
body { background: var(--bg); color: var(--text); padding: 14px; min-height: 100vh; display: flex; flex-direction: column; gap: 14px; }
/* CONTROLLER PANEL */
.playback-bar {
background: #11141e;
border: 2px solid var(--accent);
border-radius: 12px;
padding: 14px 18px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.tape-controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.btn-pb {
background: #1f2433;
border: 1px solid #3b4461;
color: #fff;
padding: 8px 14px;
border-radius: 6px;
font-weight: 800;
cursor: pointer;
font-size: 0.9rem;
}
.btn-pb:hover { background: #2f374e; }
.btn-pb.primary { background: var(--accent); color: #000; border-color: var(--accent); }
.speed-slider { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: var(--text-dim); }
/* LOG & STEP VIEWER */
.main-grid { display: grid; grid-template-columns: 1fr; gap: 14px; flex: 1; }
@media (min-width: 960px) {
.main-grid { grid-template-columns: 460px 1fr; }
}
.event-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.event-badge { font-size: 0.8rem; font-weight: 800; text-transform: uppercase; color: var(--accent); letter-spacing: 1px; }
.event-desc { font-size: 1.4rem; font-weight: 800; line-height: 1.3; color: #fff; }
.event-commentary { background: #0c0e14; border-left: 3px solid var(--blue); padding: 10px; border-radius: 4px; font-size: 0.95rem; color: #cbd5e1; font-style: italic; }
/* STANDINGS & CARD */
.table-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
overflow-x: auto;
}
table { width: 100%; border-collapse: collapse; text-align: left; }
th { padding: 8px; font-size: 0.75rem; text-transform: uppercase; color: var(--text-dim); border-bottom: 2px solid var(--border); }
td { padding: 10px 8px; border-bottom: 1px solid #1c202d; font-size: 0.95rem; }
.row-elim { opacity: 0.35; }
.row-cur { background: rgba(245, 158, 11, 0.15); border-left: 3px solid var(--accent); }
.pill { display: inline-block; padding: 2px 5px; border-radius: 4px; margin-right: 3px; font-weight: 700; font-size: 0.75rem; }
.pill.safe { background: rgba(16, 185, 129, 0.2); color: #34d399; }
.pill.cost { background: rgba(239, 68, 68, 0.2); color: #f87171; }
/* PAYOUT PREVIEW */
.payout-box {
background: #0f1612;
border: 1px solid var(--green);
border-radius: 8px;
padding: 12px;
display: flex;
justify-content: space-around;
text-align: center;
}
.p-val { font-size: 1.3rem; font-weight: 900; color: #fff; }
.p-lbl { font-size: 0.7rem; text-transform: uppercase; color: var(--text-dim); }
</style>
</head>
<body>
<div class="playback-bar">
<div class="tape-controls">
<button class="btn-pb primary" id="playBtn" onclick="togglePlay()">▶ Play</button>
<button class="btn-pb" onclick="stepBack()">◀ Prev Step</button>
<button class="btn-pb" onclick="stepForward()">Next Step ▶</button>
<button class="btn-pb" onclick="resetSim()">↺ Start</button>
<button class="btn-pb" onclick="jumpToEnd()">⏭ Finish</button>
</div>
<div class="speed-slider">
<span>Speed:</span>
<input type="range" id="speedSlider" min="100" max="1800" value="700" step="50" oninput="updateSpeed(this.value)">
<span id="speedVal" style="font-family:monospace; color:#fff;">0.7s</span>
</div>
</div>
<div class="payout-box">
<div>
<div class="p-lbl">1st Place ($168)</div>
<div class="p-val" id="pb1st">--</div>
</div>
<div>
<div class="p-lbl">2nd Place ($48)</div>
<div class="p-val" id="pb2nd">--</div>
</div>
<div>
<div class="p-lbl">Hot Hand ($24)</div>
<div class="p-val" id="pbHot">--</div>
</div>
<div>
<div class="p-lbl">Step Progress</div>
<div class="p-val" id="pbStep">0 / 0</div>
</div>
</div>
<div class="main-grid">
<!-- LEFT: NARRATIVE LOG -->
<div class="event-card">
<div class="event-badge" id="stepBadge">Simulation Ready</div>
<div class="event-desc" id="stepTitle">Press Play or Next Step to start Thursday Night</div>
<div class="event-commentary" id="stepCommentary">12 shooters signed up at $20 a head ($240 pot). 5 chips each. 3 balls on the table.</div>
</div>
<!-- RIGHT: SCORECARD TABLE -->
<div class="table-card">
<table>
<thead>
<tr>
<th>Shooter</th>
<th>Chips</th>
<th>Rounds History</th>
</tr>
</thead>
<tbody id="simTableBody">
</tbody>
</table>
</div>
</div>
<script>
const INITIAL_ROSTER = ["Big Mike", "Sully", "Dave R.", "Katie", "Mac", "Jenna", "Tommy", "Red", "Preacher", "Cole", "Shorty", "Doc"];
// SCRIPTED REALISTIC TOURNAMENT LOG (Exact game rules applied)
const SCRIPTED_STEPS = [
// ROUND 1
{ p: "Big Mike", shots: 3, note: "Big Mike drills 3 clean shots. Safe." },
{ p: "Sully", shots: 4, note: "Sully rattles the 3-ball on the corner pocket, drops it in 4. Safe." },
{ p: "Dave R.", shots: 2, note: "Dave R. banks two in one shot! 2 shots total. Sets early Hot Hand target." },
{ p: "Katie", shots: 3, note: "Katie strokes clean. 3 shots." },
{ p: "Mac", shots: 5, note: "Mac scratches on the second ball. Takes 5 shots (-1 chip, down to 4)." },
{ p: "Jenna", shots: 3, note: "Jenna clears easily in 3." },
{ p: "Tommy", shots: 'T', note: "Tommy was out back smoking when clock ran out. Turn expired (-1 chip, down to 4)." },
{ p: "Red", shots: 4, note: "Red plays it safe. 4 shots." },
{ p: "Preacher", shots: 6, note: "Preacher hangs on the lip. 6 shots (-1 chip, down to 4)." },
{ p: "Cole", shots: 3, note: "Cole runs 3 smooth shots." },
{ p: "Shorty", shots: 5, note: "Shorty misses the cut. 5 shots (-1 chip, down to 4)." },
{ p: "Doc", shots: 4, note: "Doc finishes round 1 with 4 shots safe." },
// ROUND 2
{ p: "Big Mike", shots: 4, note: "Big Mike takes 4. Safe." },
{ p: "Sully", shots: 5, note: "Sully misses cue ball position. 5 shots (-1 chip, down to 4)." },
{ p: "Dave R.", shots: 3, note: "Dave stays solid. 3 shots." },
{ p: "Katie", shots: 2, note: "Katie ties Dave for the Hot Hand bonus with 2 shots!" },
{ p: "Mac", shots: 6, note: "Mac struggles again (-1 chip, down to 3)." },
{ p: "Jenna", shots: 4, note: "Jenna takes 4 shots." },
{ p: "Tommy", shots: 3, note: "Tommy made it to the table this time. Clean 3." },
{ p: "Red", shots: 5, note: "Red takes 5 (-1 chip, down to 4)." },
{ p: "Preacher", shots: 5, note: "Preacher misses again (-1 chip, down to 3)." },
{ p: "Cole", shots: 4, note: "Cole keeps his 5 chips intact." },
{ p: "Shorty", shots: 6, note: "Shorty takes 6 (-1 chip, down to 3)." },
{ p: "Doc", shots: 3, note: "Doc runs 3." },
// ROUND 3 (Hot hand closes after this round)
{ p: "Big Mike", shots: 5, note: "Big Mike drops his first chip (-1 chip, down to 4)." },
{ p: "Sully", shots: 5, note: "Sully takes 5 (-1 chip, down to 3)." },
{ p: "Dave R.", shots: 4, note: "Dave safe." },
{ p: "Katie", shots: 4, note: "Katie safe. Dave & Katie officially lock and split the $24 Hot Hand side pot!" },
{ p: "Mac", shots: 6, note: "Mac takes 6 (-1 chip, down to 2)." },
{ p: "Jenna", shots: 5, note: "Jenna drops a chip (-1 chip, down to 4)." },
{ p: "Tommy", shots: 5, note: "Tommy takes 5 (-1 chip, down to 3)." },
{ p: "Red", shots: 6, note: "Red drops another (-1 chip, down to 3)." },
{ p: "Preacher", shots: 6, note: "Preacher takes 6 (-1 chip, down to 2)." },
{ p: "Cole", shots: 3, note: "Cole is still perfect with 5 chips." },
{ p: "Shorty", shots: 5, note: "Shorty takes 5 (-1 chip, down to 2)." },
{ p: "Doc", shots: 5, note: "Doc drops a chip (-1 chip, down to 4)." },
// ROUND 4
{ p: "Big Mike", shots: 3, note: "Mike safe." },
{ p: "Sully", shots: 5, note: "Sully (-1 chip, down to 2)." },
{ p: "Dave R.", shots: 5, note: "Dave R. drops a chip (-1 chip, down to 4)." },
{ p: "Katie", shots: 3, note: "Katie clean 3." },
{ p: "Mac", shots: 5, note: "Mac on the ropes (-1 chip, down to 1)." },
{ p: "Jenna", shots: 4, note: "Jenna safe." },
{ p: "Tommy", shots: 6, note: "Tommy drops (-1 chip, down to 2)." },
{ p: "Red", shots: 5, note: "Red drops (-1 chip, down to 2)." },
{ p: "Preacher", shots: 5, note: "Preacher on danger watch (-1 chip, down to 1)." },
{ p: "Cole", shots: 5, note: "Cole drops his first chip (-1 chip, down to 4)." },
{ p: "Shorty", shots: 6, note: "Shorty is down to his last chip (-1 chip, down to 1)." },
{ p: "Doc", shots: 4, note: "Doc safe." },
// ROUND 5 (Eliminations begin)
{ p: "Big Mike", shots: 4, note: "Mike safe (4 chips)." },
{ p: "Sully", shots: 6, note: "Sully drops to 1 chip." },
{ p: "Dave R.", shots: 3, note: "Dave clean 3 (4 chips)." },
{ p: "Katie", shots: 4, note: "Katie safe (5 chips)." },
{ p: "Mac", shots: 6, note: "💥 MAC IS ELIMINATED! First out of the tournament." },
{ p: "Jenna", shots: 5, note: "Jenna drops to 3 chips." },
{ p: "Tommy", shots: 4, note: "Tommy safe (2 chips)." },
{ p: "Red", shots: 5, note: "Red drops to 1 chip." },
{ p: "Preacher", shots: 7, note: "💥 PREACHER IS ELIMINATED! Round 5 claims another." },
{ p: "Cole", shots: 4, note: "Cole safe (4 chips)." },
{ p: "Shorty", shots: 5, note: "💥 SHORTY IS ELIMINATED! Back to the bar." },
{ p: "Doc", shots: 4, note: "Doc safe (4 chips)." },
// ROUND 6 (9 Shooters Left)
{ p: "Big Mike", shots: 5, note: "Mike drops to 3 chips." },
{ p: "Sully", shots: 5, note: "💥 SULLY IS ELIMINATED in Round 6!" },
{ p: "Dave R.", shots: 4, note: "Dave safe (4 chips)." },
{ p: "Katie", shots: 3, note: "Katie still in command (5 chips)." },
{ p: "Jenna", shots: 6, note: "Jenna down to 2 chips." },
{ p: "Tommy", shots: 5, note: "Tommy down to 1 chip." },
{ p: "Red", shots: 6, note: "💥 RED IS ELIMINATED!" },
{ p: "Cole", shots: 4, note: "Cole safe (4 chips)." },
{ p: "Doc", shots: 5, note: "Doc drops to 3 chips." },
// ROUND 7 (7 Shooters Left)
{ p: "Big Mike", shots: 4, note: "Mike safe (3 chips)." },
{ p: "Dave R.", shots: 5, note: "Dave drops to 3 chips." },
{ p: "Katie", shots: 5, note: "Katie drops her first chip of the night! (4 chips)." },
{ p: "Jenna", shots: 5, note: "Jenna down to 1 chip." },
{ p: "Tommy", shots: 5, note: "💥 TOMMY IS ELIMINATED in Round 7!" },
{ p: "Cole", shots: 4, note: "Cole safe (4 chips)." },
{ p: "Doc", shots: 5, note: "Doc down to 2 chips." },
// ROUND 8 (6 Shooters Left)
{ p: "Big Mike", shots: 6, note: "Mike down to 2 chips." },
{ p: "Dave R.", shots: 4, note: "Dave safe (3 chips)." },
{ p: "Katie", shots: 4, note: "Katie safe (4 chips)." },
{ p: "Jenna", shots: 5, note: "💥 JENNA IS ELIMINATED!" },
{ p: "Cole", shots: 5, note: "Cole drops to 3 chips." },
{ p: "Doc", shots: 5, note: "Doc down to 1 chip." },
// ROUND 9 (5 Shooters Left)
{ p: "Big Mike", shots: 5, note: "Mike down to 1 chip." },
{ p: "Dave R.", shots: 4, note: "Dave safe (3 chips)." },
{ p: "Katie", shots: 3, note: "Katie clean 3 (4 chips)." },
{ p: "Cole", shots: 5, note: "Cole drops to 2 chips." },
{ p: "Doc", shots: 6, note: "💥 DOC IS ELIMINATED in Round 9!" },
// ROUND 10 (Final 4: Mike 1, Dave 3, Katie 4, Cole 2)
{ p: "Big Mike", shots: 5, note: "💥 BIG MIKE IS ELIMINATED! Finishes 4th." },
{ p: "Dave R.", shots: 5, note: "Dave down to 2 chips." },
{ p: "Katie", shots: 4, note: "Katie safe (4 chips)." },
{ p: "Cole", shots: 5, note: "Cole down to 1 chip." },
// ROUND 11 (Final 3: Dave 2, Katie 4, Cole 1)
{ p: "Dave R.", shots: 4, note: "Dave safe (2 chips)." },
{ p: "Katie", shots: 4, note: "Katie safe (4 chips)." },
{ p: "Cole", shots: 6, note: "💥 COLE IS ELIMINATED! Cole takes 3rd. Dave & Katie are in the money!" },
// ROUND 12 (Championship Heads-up: Dave 2 vs Katie 4)
{ p: "Dave R.", shots: 5, note: "Dave takes 5 shots (-1 chip, down to 1). Katie has match point." },
{ p: "Katie", shots: 3, note: "Katie strokes clean in 3! Katie retains 4 chips." },
// ROUND 13 (Final Showdown)
{ p: "Dave R.", shots: 5, note: "💥 DAVE R. TAKES 5 SHOTS AND BUSTS! Katie wins Thursday 3-Ball!" }
];
let stepIndex = 0;
let playInterval = null;
let playSpeed = 700;
function computeStateAtStep(targetIndex) {
const players = INITIAL_ROSTER.map(name => ({
name: name,
chips: 5,
history: [],
eliminatedAtStep: null
}));
for (let i = 0; i < targetIndex; i++) {
const act = SCRIPTED_STEPS[i];
const p = players.find(x => x.name === act.p);
p.history.push(act.shots);
if (act.shots === 'T' || act.shots >= 5) {
p.chips--;
if (p.chips === 0 && p.eliminatedAtStep === null) {
p.eliminatedAtStep = i;
}
}
}
return players;
}
function renderStep() {
const players = computeStateAtStep(stepIndex);
const currentStep = stepIndex > 0 ? SCRIPTED_STEPS[stepIndex - 1] : null;
document.getElementById('pbStep').textContent = `${stepIndex} / ${SCRIPTED_STEPS.length}`;
if (currentStep) {
document.getElementById('stepBadge').textContent = `Step ${stepIndex} Action`;
document.getElementById('stepTitle').textContent = `${currentStep.p}: ${currentStep.shots === 'T' ? 'Turn Expired' : currentStep.shots + ' Shots'}`;
document.getElementById('stepCommentary').textContent = currentStep.note;
} else {
document.getElementById('stepBadge').textContent = "Pre-Game";
document.getElementById('stepTitle').textContent = "Tournament Ready";
document.getElementById('stepCommentary').textContent = "12 shooters signed up at $20 a head ($240 pot). 5 chips each.";
}
// Hot hand lock check (after step 36 = 3 full rounds)
if (stepIndex >= 36) {
document.getElementById('pbHot').textContent = "Dave R. / Katie ($12 ea)";
} else {
document.getElementById('pbHot').textContent = "In Play (R1-3)";
}
// Alive count & payouts
const alive = players.filter(p => p.chips > 0);
if (alive.length === 1 || stepIndex === SCRIPTED_STEPS.length) {
const winner = alive[0] || players.find(p => p.name === "Katie");
document.getElementById('pb1st').textContent = `${winner.name} ($168)`;
document.getElementById('pb2nd').textContent = "Dave R. ($48)";
} else {
document.getElementById('pb1st').textContent = `${alive.length} Alive`;
document.getElementById('pb2nd').textContent = "TBD ($48)";
}
// Scoreboard Table
const tbody = document.getElementById('simTableBody');
tbody.innerHTML = '';
players.forEach(p => {
const isCur = currentStep && currentStep.p === p.name;
const isElim = p.chips === 0;
const tr = document.createElement('tr');
tr.className = `${isElim ? 'row-elim' : ''} ${isCur ? 'row-cur' : ''}`;
let historyPills = p.history.map(s => {
if (s === 'T') return `<span class="pill cost">T</span>`;
return `<span class="pill ${s <= 4 ? 'safe' : 'cost'}">${s}</span>`;
}).join('');
tr.innerHTML = `
<td style="font-weight:700;">${p.name} ${isCur ? '🎱' : ''}</td>
<td style="color:var(--gold); font-weight:800;">${'🪙'.repeat(p.chips)} <span style="font-size:0.75rem; color:var(--text-dim);">(${p.chips})</span></td>
<td>${historyPills || '<span style="color:#475069;">--</span>'}</td>
`;
tbody.appendChild(tr);
});
}
function stepForward() {
if (stepIndex < SCRIPTED_STEPS.length) {
stepIndex++;
renderStep();
} else {
pausePlay();
}
}
function stepBack() {
if (stepIndex > 0) {
stepIndex--;
renderStep();
}
}
function resetSim() {
pausePlay();
stepIndex = 0;
renderStep();
}
function jumpToEnd() {
pausePlay();
stepIndex = SCRIPTED_STEPS.length;
renderStep();
}
function togglePlay() {
if (playInterval) pausePlay();
else startPlay();
}
function startPlay() {
if (stepIndex >= SCRIPTED_STEPS.length) stepIndex = 0;
document.getElementById('playBtn').textContent = '⏸ Pause';
playInterval = setInterval(stepForward, playSpeed);
}
function pausePlay() {
clearInterval(playInterval);
playInterval = null;
document.getElementById('playBtn').textContent = '▶ Play';
}
function updateSpeed(ms) {
playSpeed = parseInt(ms);
document.getElementById('speedVal').textContent = (ms / 1000).toFixed(1) + 's';
if (playInterval) {
pausePlay();
startPlay();
}
}
// Initial render
renderStep();
</script>
</body>
</html>
⏱ Turn Expired right away or people will argue that friends are getting free grace periods.