Binomial_Game / index.html
eaglelandsonce's picture
Update index.html
9632ac5 verified
Raw
History Blame Contribute Delete
10.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Binomial Free-Throw Challenge (Discrete Distributions)</title>
<style>
:root{
--blue:#0b6ef6; --bg:#0f172a; --card:#111827; --muted:#94a3b8; --ok:#10b981; --bad:#ef4444; --accent:#f59e0b;
}
*{box-sizing:border-box}
body{
margin:0; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;
background:linear-gradient(180deg,#0b132b,#1b2433 55%,#0f172a);
color:#e5e7eb; line-height:1.45;
padding:24px;
}
header{
display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between;
}
h1{font-size:1.4rem; margin:0}
.tagline{color:#cbd5e1; font-size:.95rem}
.card{
background:rgba(17,24,39,.7); border:1px solid #1f2937; border-radius:16px; padding:16px; box-shadow:0 10px 30px rgba(0,0,0,.25);
}
.grid{
display:grid; grid-template-columns:repeat(2,minmax(260px,1fr)); gap:16px; margin-top:16px;
}
label{display:block; font-size:.9rem; color:#cbd5e1; margin:6px 0 4px}
input[type="number"], input[type="range"]{
width:100%;
}
.controls{display:grid; grid-template-columns:1fr 1fr; gap:12px}
.row{display:flex; gap:12px; align-items:center}
.hint{font-size:.85rem; color:#94a3b8}
.buttons{display:flex; gap:8px; flex-wrap:wrap; margin-top:10px}
button{
background:var(--blue); color:#fff; border:none; border-radius:999px; padding:10px 14px; font-weight:600; cursor:pointer;
transition:transform .07s ease, filter .15s ease;
}
button:hover{filter:brightness(1.08)}
button:active{transform:scale(.98)}
.secondary{background:#334155}
.danger{background:#b91c1c}
.success{background:var(--ok)}
.pill{padding:.15rem .6rem; border:1px solid #334155; border-radius:999px; color:#cbd5e1; font-size:.8rem}
#shots{
display:grid; grid-template-columns:repeat(auto-fill,minmax(24px,1fr)); gap:6px;
min-height:120px; align-content:start;
}
.shot{
width:100%; aspect-ratio:1/1; border-radius:6px; border:1px solid #1f2937;
display:grid; place-items:center; font-size:.7rem; font-weight:700;
}
.hit{background:rgba(16,185,129,.15); color:#34d399; border-color:#065f46}
.miss{background:rgba(239,68,68,.12); color:#f87171; border-color:#7f1d1d}
.panel{display:grid; gap:8px}
.stat{display:flex; gap:12px; align-items:center; justify-content:space-between; font-variant-numeric:tabular-nums}
.big{font-size:1.35rem; font-weight:800}
#hist{height:220px; display:flex; align-items:flex-end; gap:6px; border-left:2px solid #334155; border-bottom:2px solid #334155; padding:8px}
.bar{flex:1; display:flex; flex-direction:column; justify-content:flex-end; align-items:center}
.bar .block{width:100%; background:linear-gradient(180deg,#60a5fa,#2563eb); border-radius:6px 6px 0 0}
.bar .theory{width:100%; opacity:.55; background:linear-gradient(180deg,#fbbf24,#f59e0b)}
.bar label{font-size:.75rem; color:#94a3b8}
.footer{margin-top:14px; color:#9ca3af; font-size:.85rem}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
.highlight{color:#fde68a}
</style>
</head>
<body>
<header>
<div>
<h1>Binomial Free-Throw Challenge</h1>
<div class="tagline">Choose <span class="pill">n</span> shots and success chance <span class="pill">p</span>, guess the makes, then simulate. (A binomial model of <em>n</em> Bernoulli trials.)</div>
</div>
<div class="pill mono">Discrete Distribution: Binomial(n, p)</div>
</header>
<section class="grid">
<!-- Left: Controls + Results -->
<div class="card">
<div class="controls">
<div>
<label for="n">Number of shots (n)</label>
<input id="n" type="number" min="1" max="100" value="20" />
</div>
<div>
<label for="p">Make probability p = <span id="pVal" class="highlight">0.60</span></label>
<input id="p" type="range" min="0" max="1" step="0.01" value="0.60" oninput="pVal.textContent=this.value" />
</div>
</div>
<div class="row" style="margin-top:8px">
<div style="flex:1">
<label for="guess">Your guess: makes this round</label>
<input id="guess" type="number" min="0" value="12" />
<div class="hint">Score = <span class="mono">100 − 10×|guess − actual|</span> (min 0)</div>
</div>
<div class="panel" style="min-width:220px">
<div class="stat"><span>Expected makes E[X]=np</span><span id="exp" class="mono"></span></div>
<div class="stat"><span>Variance Var[X]=np(1−p)</span><span id="var" class="mono"></span></div>
<div class="stat"><span>St.dev</span><span id="sd" class="mono"></span></div>
</div>
</div>
<div class="buttons">
<button id="play">Shoot this round</button>
<button class="secondary" id="multi">Run 200 rounds → histogram</button>
<button class="danger" id="reset">Reset all</button>
</div>
<div class="panel" style="margin-top:12px">
<div class="stat">
<span class="big">Round Result</span>
<span id="roundSummary" class="mono"></span>
</div>
<div id="shots"></div>
<div class="stat">
<span>Total Score</span>
<span id="score" class="big mono">0</span>
</div>
</div>
</div>
<!-- Right: Histogram -->
<div class="card">
<div class="stat">
<span class="big">Empirical Distribution (Simulated) vs Theoretical PMF</span>
<span id="simCount" class="mono">0 sims</span>
</div>
<div id="hist"></div>
<div class="footer">
Bars show relative frequency of total makes <span class="mono">X~Binomial(n,p)</span>. Blue = simulation; amber = exact binomial <span class="mono">P(X=k)=C(n,k)p^k(1−p)^(n−k)</span>.
</div>
</div>
</section>
<section class="card" style="margin-top:16px">
<strong>What you’re modeling:</strong> When outcomes are <em>countable</em>, the binomial distribution counts successes across a fixed number of independent trials with constant success chance <span class="mono">p</span>. Try changing <span class="mono">n</span> and <span class="mono">p</span> and watch the histogram shift: bigger <span class="mono">n</span> narrows the spread (relative), while <span class="mono">p</span> moves the center.
</section>
<script>
// ===== Utilities =====
const byId = id => document.getElementById(id);
const nEl = byId('n'), pEl = byId('p'), guessEl = byId('guess');
const expEl = byId('exp'), varEl = byId('var'), sdEl = byId('sd');
const shotsDiv = byId('shots'), roundSummary = byId('roundSummary'), scoreEl = byId('score');
const histDiv = byId('hist'), simCountEl = byId('simCount');
let totalScore = 0, sims = [], n=Number(nEl.value), p=Number(pEl.value);
function updateStats(){
n = Number(nEl.value); p = Number(pEl.value);
guessEl.max = n;
const EX = n*p;
const Var = n*p*(1-p);
const SD = Math.sqrt(Var);
expEl.textContent = EX.toFixed(2);
varEl.textContent = Var.toFixed(2);
sdEl.textContent = SD.toFixed(2);
renderHistogram(); // refresh theoretical overlay
}
updateStats();
pEl.addEventListener('input', updateStats);
nEl.addEventListener('change', ()=>{ if(guessEl.value>nEl.value) guessEl.value=nEl.value; updateStats(); });
// Single Bernoulli(p)
function bernoulli(prob){ return Math.random() < prob ? 1 : 0; }
// Run one Binomial round of n trials
function runRound(){
shotsDiv.innerHTML='';
const results = [];
for(let i=0;i<n;i++){
const r = bernoulli(p);
results.push(r);
const cell = document.createElement('div');
cell.className = 'shot ' + (r? 'hit':'miss');
cell.textContent = r? '✓':'×';
shotsDiv.appendChild(cell);
}
const makes = results.reduce((a,b)=>a+b,0);
const guess = Math.max(0, Math.min(n, Number(guessEl.value)||0));
const roundScore = Math.max(0, 100 - 10*Math.abs(guess - makes));
totalScore += roundScore;
roundSummary.textContent = `Made ${makes} / ${n} (you guessed ${guess}) → +${roundScore}`;
scoreEl.textContent = totalScore;
// record for histogram
sims.push(makes);
renderHistogram();
}
// Run many simulations quickly
function runMany(count=200){
for(let i=0;i<count;i++){
const makes = Array.from({length:n}, ()=>bernoulli(p)).reduce((a,b)=>a+b,0);
sims.push(makes);
}
renderHistogram();
}
// Binomial coefficient and PMF
function choose(N,K){
if(K<0||K>N) return 0;
if(K===0||K===N) return 1;
// multiplicative for stability
let c=1;
for(let i=1;i<=K;i++){
c = c * (N - (K - i)) / i;
}
return c;
}
function binomPMF(k, N, P){
return choose(N,k) * Math.pow(P,k) * Math.pow(1-P, N-k);
}
// Histogram render
function renderHistogram(){
const counts = Array.from({length:n+1}, ()=>0);
sims.forEach(v=>{ if(v>=0 && v<=n) counts[v]++; });
const total = sims.length || 1;
const freqs = counts.map(c=>c/total); // 0..1
const theory = Array.from({length:n+1}, (_,k)=>binomPMF(k,n,p));
const maxY = Math.max(0.05, ...freqs, ...theory); // avoid zero
histDiv.innerHTML='';
freqs.forEach((f,k)=>{
const bar = document.createElement('div'); bar.className='bar';
const block = document.createElement('div'); block.className='block';
block.style.height = `${(f/maxY)*200}px`;
const t = document.createElement('div'); t.className='block theory';
t.style.height = `${(theory[k]/maxY)*200}px`;
// stack: theoretical at back, simulated in front
const stack = document.createElement('div'); stack.style.width='100%'; stack.style.display='grid';
stack.style.alignItems='end';
stack.appendChild(t); stack.appendChild(block);
bar.appendChild(stack);
const lab = document.createElement('label'); lab.textContent = k;
bar.appendChild(lab);
histDiv.appendChild(bar);
});
simCountEl.textContent = `${sims.length} sims`;
}
// Buttons
byId('play').addEventListener('click', runRound);
byId('multi').addEventListener('click', ()=>runMany(200));
byId('reset').addEventListener('click', ()=>{
sims = []; totalScore = 0; shotsDiv.innerHTML=''; roundSummary.textContent='';
scoreEl.textContent='0'; renderHistogram();
});
// First paint
renderHistogram();
</script>
</body>
</html>