eaglelandsonce's picture
Update index.html
f23dc52 verified
Raw
History Blame Contribute Delete
28.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hypothesis Hero — Spin • Solve • Score</title>
<style>
:root{
--bg:#0d1224; --panel:#121937; --panel2:#0f1530; --accent:#6ee7ff;
--accent2:#ffd166; --good:#00e887; --bad:#ff5c75; --text:#e8ecff;
}
*{box-sizing:border-box}
body{
margin:0; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;
color:var(--text); background:
radial-gradient(1200px 800px at 15% 10%, #1a2354 0%, #0b0f20 40%, #050814 100%);
min-height:100vh; display:flex; flex-direction:column; align-items:center;
}
header{width:100%; padding:16px 18px; display:flex; gap:12px; align-items:center; justify-content:space-between; flex-wrap:wrap}
h1{margin:0; font-size:clamp(20px,3.2vw,34px); letter-spacing:.5px}
.sub{opacity:.9; font-size:.9rem}
.hud{display:flex; gap:12px; flex-wrap:wrap; align-items:center; justify-content:flex-end}
.badge{
background:linear-gradient(180deg, #1c2658, #0d1330); border:1px solid #27306b; color:#dfe6ff;
padding:8px 12px; border-radius:12px; box-shadow:0 6px 14px #0007; font-weight:600; min-width:120px; text-align:center;
}
.linkbar a{
text-decoration:none; font-weight:700; color:#8bd9ff; border:1px solid #2a4e7a; padding:6px 10px; border-radius:10px;
background:linear-gradient(180deg,#0f2047,#0b1534);
}
.container{width:min(1200px, 94%); display:grid; grid-template-columns:420px 1fr; gap:18px; margin:8px auto 24px}
@media (max-width: 960px){ .container{grid-template-columns:1fr} }
.card{
background:linear-gradient(180deg, var(--panel), var(--panel2)); border:1px solid #27306b; border-radius:18px;
box-shadow:0 16px 48px #000a; padding:16px;
}
/* Wheel panel */
.wheel-wrap{display:grid; grid-template-rows:auto auto; gap:14px; align-items:center; justify-items:center}
canvas{background:radial-gradient(350px 350px at 50% 50%, #0f1a49, #0b102a); border-radius:50%; width:100%; max-width:360px; aspect-ratio:1; border:2px solid #27306b}
.needle{
width:0; height:0; border-left:16px solid transparent; border-right:16px solid transparent; border-bottom:26px solid var(--accent2);
filter:drop-shadow(0 3px 3px #0007);
margin-top:-6px;
}
.wheel-controls{display:flex; gap:10px; flex-wrap:wrap; justify-content:center}
button{
appearance:none; border:none; cursor:pointer; padding:10px 14px; border-radius:12px; font-weight:700;
background:linear-gradient(180deg,#3bd6ff,#0cb5e7); color:#042234; box-shadow:0 8px 20px #00b6ff44, inset 0 0 0 1px #66e4ff;
}
button.secondary{background:linear-gradient(180deg,#ffd166,#ffb703); color:#3a1f00; box-shadow:0 8px 20px #ffb70355, inset 0 0 0 1px #ffe19c}
button.ghost{background:transparent; color:var(--accent); border:1px dashed #346692}
button:disabled{opacity:.5; cursor:not-allowed}
/* Submit highlight */
#submitBtn.attn{
animation:glow 0.9s ease-in-out infinite alternate;
}
@keyframes glow{
from{ box-shadow:0 0 0 0 rgba(110,231,255,.55), inset 0 0 0 1px #66e4ff; }
to{ box-shadow:0 0 22px 6px rgba(110,231,255,.65), inset 0 0 0 1px #66e4ff; }
}
/* Question panel */
.q-title{font-size:1.1rem; font-weight:700; margin-bottom:8px}
.q-meta{opacity:.8; font-size:.95rem; margin-bottom:4px}
.q-box{
margin-top:8px; display:grid; grid-template-columns:1fr; gap:12px; padding:12px; border-radius:14px;
background:linear-gradient(180deg,#121b43,#0e1538); border:1px solid #27306b;
}
.candidates{display:grid; grid-template-columns:1fr 1fr; gap:12px}
@media (max-width: 700px){ .candidates{grid-template-columns:1fr} }
.tile{
padding:12px; border-radius:12px; background:#0f1742; border:1px solid #23306b; min-height:70px;
display:flex; gap:8px; align-items:center; justify-content:flex-start; box-shadow:0 6px 18px #0006; cursor:grab;
user-select:none;
}
.tile:active{cursor:grabbing}
.tile .label{font-weight:800; background:var(--accent2); color:#2b1600; padding:2px 8px; border-radius:10px; min-width:34px; text-align:center}
.tile.dragging{opacity:.7; transform:scale(1.02)}
.dropzone{
border:2px dashed #4661a3; border-radius:12px; padding:12px; min-height:74px; display:flex; align-items:center; justify-content:flex-start; gap:10px;
}
.dropzone .slot{font-weight:900; color:#9fc5ff; background:#10224f; border:1px solid #2a3b7a; padding:3px 10px; border-radius:10px}
.dropzone.over{background:#0f204f88; outline:2px solid #6ee7ff44}
.explain{font-size:.95rem; opacity:.9}
.hint{opacity:.86; font-size:.9rem}
.correct{color:var(--good)} .wrong{color:var(--bad)}
.actions{display:flex; gap:10px; flex-wrap:wrap; justify-content:flex-end; margin-top:12px}
.status{display:flex; gap:10px; align-items:center; flex-wrap:wrap; justify-content:space-between}
.toast{
position:fixed; left:50%; transform:translateX(-50%); bottom:18px; background:#0e173e; border:1px solid #2a3b7a;
padding:12px 14px; border-radius:12px; color:#e6efff; box-shadow:0 10px 24px #000b; display:none; z-index:10;
}
.leader{margin-top:10px; font-size:.92rem; opacity:.9}
.divider{height:1px; background:#243065; margin:8px 0}
.key{font-weight:800; padding:2px 8px; background:#12255a; border:1px solid #2a3b7a; border-radius:8px}
/* Feedback area */
#feedback{margin-top:8px}
</style>
</head>
<body>
<header>
<div>
<h1>Hypothesis Hero</h1>
<div class="sub">Spin the wheel • Place <span class="key">A/B</span> into <span class="key">H₀/H₁</span> • Beat the clock</div>
<div class="linkbar" style="margin-top:8px">
<a href="https://www.linkedin.com/pulse/understanding-hypothesis-testing-from-theory-practice-michael-lively-bjvoe/" target="_blank" rel="noopener noreferrer">
📖 Read: Understanding Hypothesis Testing — From Theory to Practice (Michael Lively)
</a>
</div>
</div>
<div class="hud">
<div class="badge" id="scoreBox">Score: 0</div>
<div class="badge" id="roundBox">Round: 1 / 10</div>
<div class="badge" id="timerBox">Time: 00:30</div>
</div>
</header>
<main class="container">
<!-- Left: Wheel -->
<section class="card wheel-wrap">
<div class="needle" title="Wheel pointer"></div>
<canvas id="wheel" width="600" height="600" aria-label="Spin wheel"></canvas>
<div class="wheel-controls">
<button id="spinBtn">🎡 Spin</button>
<button id="powerBtn" class="secondary" title="Play a power-up if you have one">⚡ Use Power-Up</button>
<button id="resetBtn" class="ghost">↻ Reset Game</button>
</div>
<div class="leader">
<strong>Wheel Slices:</strong> +100 • +200 • +300 • Double • Speed-Round • Wild-Swap • Shield • Bust (0)
<div class="divider"></div>
<span class="hint">Power-Ups: <b>Shield</b> (ignore one wrong), <b>Wild-Swap</b> (swap A/B before grading), <b>Double</b> (double this round’s points).</span>
</div>
</section>
<!-- Right: Question -->
<section class="card">
<div id="questionArea">
<div class="q-title" id="qTitle">Spin the wheel to start!</div>
<div class="q-meta" id="qMeta"></div>
<div class="q-box" id="qBox" hidden>
<div id="qText" class="explain"></div>
<div class="candidates" id="candidates"></div>
<div class="status">
<div class="dropzone" id="dropH0" aria-label="Drop zone H0">
<span class="slot">H₀</span>
</div>
<div class="dropzone" id="dropH1" aria-label="Drop zone H1">
<span class="slot">H₁</span>
</div>
</div>
<div id="feedback"></div>
<div class="actions">
<button id="submitBtn" disabled>Submit Answer</button>
<button id="hintBtn" class="ghost">Hint</button>
<button id="skipBtn" class="ghost">Skip (-50)</button>
</div>
</div>
</div>
<div class="divider"></div>
<div class="leader" id="leaderboard"></div>
</section>
</main>
<div class="toast" id="toast"></div>
<script>
/* =========================
DATA: canonical H0/H1 texts per question
========================= */
const QUESTIONS = [
{id:1,title:"New drug lowers blood pressure",question:"Does the new drug reduce mean systolic BP vs. the standard?",metric:"Mean systolic BP (mmHg) after 8 weeks",candidates:{A:"μ_new = μ_std",B:"μ_new < μ_std"},test:"two-sample t-test",tails:"left"},
{id:2,title:"Email subject B increases open rate",question:"Is the open rate higher for Subject B than Subject A?",metric:"Proportion opened (p)",candidates:{A:"p_B = p_A",B:"p_B > p_A"},test:"two-proportion z-test",tails:"right"},
{id:3,title:"Process change cuts defect rate",question:"Did the defect rate drop after the process change?",metric:"Proportion defective (p)",candidates:{A:"p_after = p_before",B:"p_after < p_before"},test:"two-proportion z-test",tails:"left"},
{id:4,title:"Flipped classroom vs. lecture",question:"Do mean exam scores differ by method?",metric:"Mean exam score (μ)",candidates:{A:"μ_flipped = μ_lecture",B:"μ_flipped ≠ μ_lecture"},test:"two-sample t-test",tails:"two"},
{id:5,title:"Call center SLA (≤ 2 minutes)",question:"Is the average wait time under 2 minutes?",metric:"Mean wait time (minutes)",candidates:{A:"μ ≥ 2",B:"μ < 2"},test:"one-sample t-test",tails:"left"},
{id:6,title:"New web layout changes time-on-page",question:"Does the new layout change average time-on-page?",metric:"Mean time (seconds)",candidates:{A:"μ_new = μ_old",B:"μ_new ≠ μ_old"},test:"two-sample t-test",tails:"two"},
{id:7,title:"Fertilizer X boosts yield",question:"Is mean yield higher with Fertilizer X?",metric:"Mean yield (kg/plot)",candidates:{A:"μ_X ≤ μ_std",B:"μ_X > μ_std"},test:"two-sample t-test",tails:"right"},
{id:8,title:"Strategy has positive alpha",question:"Is mean monthly alpha greater than zero?",metric:"Mean alpha (μ_α)",candidates:{A:"μ_α ≤ 0",B:"μ_α > 0"},test:"one-sample t-test",tails:"right"},
{id:9,title:"New machine reduces variability",question:"Is variance lower with the new machine?",metric:"Variance of length (σ²)",candidates:{A:"σ_new² = σ_old²",B:"σ_new² < σ_old²"},test:"F-test (or Levene/BF)",tails:"left"},
{id:10,title:"Device remission rate meets target",question:"Is remission rate at least 60%?",metric:"Proportion in remission p",candidates:{A:"p ≤ 0.60",B:"p > 0.60"},test:"one-sample proportion z-test",tails:"right"}
];
/* =========================
GAME STATE
========================= */
const state = {
round: 1, maxRounds: 10, score: 0, timer: 30, ticking: null,
wheelSpun: false, powerups: [], currentPU: null, wheelValue: null,
drawn: [], currentQ: null, placed: { H0:null, H1:null },
speedMode: false, shieldOn: false, doubleMode: false, wildAvailable: false, streak:0,
correctRound: { A:null, B:null } // per-round mapping: which label is H0/H1
};
/* =========================
UTILS
========================= */
const $ = sel => document.querySelector(sel);
const fmtTime = s => "00:" + String(Math.max(0,s)).padStart(2,'0');
const toast = (msg, ms=1600) => { const t=$("#toast"); t.textContent=msg; t.style.display="block"; setTimeout(()=>t.style.display="none", ms); };
function randChoice(arr){ return arr[Math.floor(Math.random()*arr.length)] }
function escapeHtml(s){ return s.replace(/[&<>"']/g, m=>({ "&":"&amp;","<":"&lt;","\"":"&quot;","'":"&#039;" }[m])) }
/* =========================
WHEEL
========================= */
const slices = [
{ label:"+100", type:"points", value:100, color:"#26c6da" },
{ label:"+200", type:"points", value:200, color:"#29b6f6" },
{ label:"Double", type:"double", value:null, color:"#ffd166" },
{ label:"Speed", type:"speed", value:null, color:"#ab7df6" },
{ label:"+300", type:"points", value:300, color:"#26c6da" },
{ label:"Wild", type:"wild", value:null, color:"#4ade80" },
{ label:"Shield", type:"shield", value:null, color:"#a3e635" },
{ label:"Bust 0", type:"bust", value:0, color:"#ef5350" }
];
const wheel = $("#wheel");
const wctx = wheel.getContext("2d");
const center = wheel.width/2;
const radius = center - 6;
function drawWheel(){
const arc = 2*Math.PI / slices.length;
wctx.clearRect(0,0,wheel.width,wheel.height);
wctx.save(); wctx.translate(center, center);
for(let i=0;i<slices.length;i++){
wctx.beginPath(); wctx.moveTo(0,0);
wctx.fillStyle = slices[i].color;
wctx.arc(0,0,radius, i*arc, (i+1)*arc);
wctx.closePath(); wctx.fill();
wctx.save(); wctx.rotate(i*arc + arc/2);
wctx.fillStyle = "#041029";
wctx.font = "bold 28px system-ui";
wctx.textAlign="center";
wctx.fillText(slices[i].label, radius*0.62, 10);
wctx.restore();
}
wctx.beginPath(); wctx.fillStyle="#0f1a49"; wctx.arc(0,0,70,0,2*Math.PI); wctx.fill();
wctx.beginPath(); wctx.strokeStyle="#27306b"; wctx.lineWidth=3; wctx.arc(0,0,radius,0,2*Math.PI); wctx.stroke();
wctx.restore();
}
drawWheel();
let spinRot = 0;
function spinWheel(){
$("#spinBtn").disabled = true;
const rotations = 6 + Math.random()*4;
const targetSlice = Math.floor(Math.random()*slices.length);
const arc = 360 / slices.length;
const targetAngle = 360*rotations + (targetSlice + 0.5)*arc;
const start = performance.now();
const duration = 3200;
function animate(now){
const t = Math.min(1, (now-start)/duration);
const ease = 1 - Math.pow(1-t, 3);
const angle = targetAngle*ease;
spinRot = angle % 360;
wheel.style.transform = `rotate(${spinRot}deg)`;
if(t<1) requestAnimationFrame(animate);
else {
const result = slices[targetSlice];
handleWheelResult(result);
$("#spinBtn").disabled = false;
}
}
requestAnimationFrame(animate);
}
function handleWheelResult(result){
state.wheelSpun = true; state.wheelValue = result;
if(result.type==="points"){ toast(`Wheel: +${result.value} base points this round`); }
if(result.type==="double"){ state.doubleMode=true; state.powerups.push("double"); toast("Wheel: DOUBLE power-up earned"); }
if(result.type==="speed"){ state.speedMode=true; toast("Wheel: SPEED-ROUND! Shorter timer."); }
if(result.type==="wild"){ state.wildAvailable=true; state.powerups.push("wild"); toast("Wheel: WILD-SWAP power-up earned"); }
if(result.type==="shield"){ state.shieldOn=true; state.powerups.push("shield"); toast("Wheel: SHIELD earned"); }
if(result.type==="bust"){ toast("Wheel: BUST! 0 base points this round."); }
startRound();
}
/* =========================
ROUND / TIMER / QUESTION
========================= */
function startRound(){
if(state.round>state.maxRounds){ endGame(); return; }
const startSecs = state.speedMode ? 18 : 30;
state.timer = startSecs; $("#timerBox").textContent = "Time: " + fmtTime(state.timer);
if(state.ticking) clearInterval(state.ticking);
state.ticking = setInterval(()=>{
state.timer--; $("#timerBox").textContent = "Time: " + fmtTime(state.timer);
if(state.timer<=0){ clearInterval(state.ticking); grade(false, "Time up!"); }
}, 1000);
// Draw question (no repeats)
let pool = QUESTIONS.filter(q=>!state.drawn.includes(q.id));
if(pool.length===0) pool = QUESTIONS.slice();
const q = randChoice(pool);
state.drawn.push(q.id);
state.currentQ = JSON.parse(JSON.stringify(q));
// Build UI
$("#qBox").hidden = false;
$("#qTitle").textContent = `Round ${state.round}: ${q.title}`;
$("#qMeta").innerHTML = `<span class="hint">Test:</span> ${q.test} • <span class="hint">Tails:</span> ${q.tails} • <span class="hint">Metric:</span> ${q.metric}`;
$("#qText").textContent = q.question;
// === RANDOMLY MAP H0/H1 TEXTS TO LABELS A/B, BUT RENDER A THEN B VISUALLY ===
const assignH0To = Math.random() < 0.5 ? "A" : "B";
const assignH1To = assignH0To === "A" ? "B" : "A";
state.correctRound = { [assignH0To]: "H0", [assignH1To]: "H1" };
// Build tiles in strict order: A (left) then B (right)
const textForA = assignH0To === "A" ? q.candidates.A : q.candidates.B; // A shows whichever text was assigned to A
const textForB = assignH0To === "A" ? q.candidates.B : q.candidates.A; // B gets the other text
const cand = $("#candidates"); cand.innerHTML = "";
const tileA = document.createElement("div");
tileA.className = "tile"; tileA.draggable = true; tileA.dataset.key = "A";
tileA.innerHTML = `<span class="label">A</span> <span>${textForA}</span>`;
tileA.addEventListener("click", ()=>clickPlace(tileA));
cand.appendChild(tileA);
const tileB = document.createElement("div");
tileB.className = "tile"; tileB.draggable = true; tileB.dataset.key = "B";
tileB.innerHTML = `<span class="label">B</span> <span>${textForB}</span>`;
tileB.addEventListener("click", ()=>clickPlace(tileB));
cand.appendChild(tileB);
// Reset zones & feedback
state.placed = { H0:null, H1:null };
$("#dropH0").innerHTML = `<span class="slot">H₀</span>`;
$("#dropH1").innerHTML = `<span class="slot">H₁</span>`;
$("#feedback").innerHTML = "";
$("#submitBtn").disabled = true;
$("#submitBtn").classList.remove("attn");
$("#roundBox").textContent = `Round: ${state.round} / ${state.maxRounds}`;
enableDnD();
observeZones();
recalcPlacements();
}
function enableDnD(){
const tiles = document.querySelectorAll(".tile");
const zones = [$("#dropH0"), $("#dropH1")];
tiles.forEach(t=>{
t.addEventListener("dragstart", e=>{
t.classList.add("dragging");
e.dataTransfer.setData("text/plain", t.dataset.key);
if(e.dataTransfer.setDragImage){
const ghost = t.cloneNode(true);
ghost.style.position="absolute"; ghost.style.top="-9999px"; document.body.appendChild(ghost);
e.dataTransfer.setDragImage(ghost, 10, 10);
setTimeout(()=>document.body.removeChild(ghost),0);
}
setTimeout(()=>t.style.opacity="0.6",0);
});
t.addEventListener("dragend", ()=>{
t.classList.remove("dragging"); t.style.opacity="1";
recalcPlacements();
});
});
zones.forEach(z=>{
z.addEventListener("dragover", e=>{ e.preventDefault(); z.classList.add("over"); });
z.addEventListener("dragleave", ()=> z.classList.remove("over"));
z.addEventListener("drop", e=>{
e.preventDefault(); z.classList.remove("over");
const key = e.dataTransfer.getData("text/plain");
const tile = [...document.querySelectorAll(".tile")].find(t=>t.dataset.key===key);
if(!tile) return;
z.querySelectorAll(".tile").forEach(n=> n.remove()); // one per zone
z.appendChild(tile);
autoFillOpposite();
recalcPlacements();
});
});
}
// Auto-place the remaining tile into the empty zone if one is set
function autoFillOpposite(){
const h0 = $("#dropH0"), h1 = $("#dropH1");
const tileA = [...document.querySelectorAll(".tile")].find(t=>t.dataset.key==="A");
const tileB = [...document.querySelectorAll(".tile")].find(t=>t.dataset.key==="B");
const h0Has = !!h0.querySelector(".tile");
const h1Has = !!h1.querySelector(".tile");
if(h0Has && !h1Has){
const other = h0.contains(tileA) ? tileB : tileA;
if(other && !h1.contains(other)){ h1.appendChild(other); }
} else if(h1Has && !h0Has){
const other = h1.contains(tileA) ? tileB : tileA;
if(other && !h0.contains(other)){ h0.appendChild(other); }
}
}
// Click-to-place: toggles into next empty slot, with auto opposite fill
function clickPlace(tile){
const h0 = $("#dropH0"), h1 = $("#dropH1"), pool = $("#candidates");
const inH0 = h0.contains(tile), inH1 = h1.contains(tile);
if(inH0 || inH1){ pool.appendChild(tile); }
else if(!h0.querySelector(".tile")){ h0.appendChild(tile); }
else if(!h1.querySelector(".tile")){ h1.appendChild(tile); }
else { pool.appendChild(tile); }
autoFillOpposite();
recalcPlacements();
}
// Observe zone mutations so Submit updates even on odd DnD behaviors
let zoneObserver;
function observeZones(){
if(zoneObserver) zoneObserver.disconnect();
zoneObserver = new MutationObserver(()=>{ autoFillOpposite(); recalcPlacements(); });
zoneObserver.observe($("#dropH0"), { childList:true, subtree:false });
zoneObserver.observe($("#dropH1"), { childList:true, subtree:false });
}
// Recompute placements + enable/disable submit strictly by occupancy (not correctness)
function recalcPlacements(){
const h0Tile = $("#dropH0 .tile");
const h1Tile = $("#dropH1 .tile");
state.placed.H0 = h0Tile ? h0Tile.dataset.key : null;
state.placed.H1 = h1Tile ? h1Tile.dataset.key : null;
const ready = Boolean(state.placed.H0 && state.placed.H1);
const btn = $("#submitBtn");
btn.disabled = !ready;
btn.classList.toggle("attn", ready);
}
/* =========================
GRADING / SCORING
========================= */
function grade(manual = true, reason = ""){
if(state.ticking){ clearInterval(state.ticking); state.ticking = null; }
const corr = state.correctRound;
if(!corr || (!corr.A && !corr.B)){ toast("Spin the wheel to start."); return; }
let correct = false;
const isH0Correct = state.placed.H0 && corr[state.placed.H0]==="H0";
const isH1Correct = state.placed.H1 && corr[state.placed.H1]==="H1";
correct = Boolean(isH0Correct && isH1Correct);
// Wild auto swap
if(state.wildAvailable && !correct){
const swapIsH0 = state.placed.H1 && corr[state.placed.H1]==="H0";
const swapIsH1 = state.placed.H0 && corr[state.placed.H0]==="H1";
if(swapIsH0 && swapIsH1){ correct = true; reason = (reason?reason+" • ":"") + "Wild-SWAP auto-correct!"; state.wildAvailable=false; consumePU("wild"); }
}
let base = (state.wheelValue && state.wheelValue.type==="points") ? state.wheelValue.value : 0;
const speedBonus = state.speedMode && state.timer>=8 ? 50 : 0;
let delta = 0;
if(correct){
state.streak = (state.streak||0)+1;
delta = base + 100 + speedBonus + Math.min(100, state.streak*20);
if(state.doubleMode){ delta *= 2; consumePU("double"); state.doubleMode = false; }
toast(`✅ Correct! +${delta}`, 1800);
} else {
if(state.shieldOn){ state.shieldOn=false; consumePU("shield"); delta = 0; toast("🛡️ Wrong, but SHIELD blocked the penalty.",1600); }
else { delta = -50; state.streak = 0; toast("❌ Incorrect (-50).", 1500); }
}
state.score += delta; $("#scoreBox").textContent = `Score: ${state.score}`;
// FEEDBACK: replace (don’t append) with current round only
const fb = $("#feedback");
const keyLine = `A = ${corr.A || '—'}, B = ${corr.B || '—'}`;
const q = state.currentQ;
fb.innerHTML = `
<div class="${correct ? 'correct':'wrong'}">
${correct ? "You matched H₀/H₁ correctly." : "Mismatch on H₀/H₁."}
${reason ? " — " + reason : ""}
</div>
<div class="hint" style="margin-top:6px">
<b>Answer key:</b> ${keyLine} • <b>Test:</b> ${q.test} (${q.tails}-tailed)
</div>`;
state.round += 1;
$("#roundBox").textContent = `Round: ${Math.min(state.round,state.maxRounds)} / ${state.maxRounds}`;
const btn = $("#submitBtn"); btn.disabled = true; btn.classList.remove("attn");
setTimeout(()=>{
if(state.round>state.maxRounds){ endGame(); }
else {
state.wheelSpun = false; state.wheelValue = null; state.speedMode = false;
startSpinPhaseUI();
}
}, 1400);
}
function consumePU(name){
const idx = state.powerups.indexOf(name);
if(idx>-1) state.powerups.splice(idx,1);
}
/* =========================
UI HOOKS
========================= */
function startSpinPhaseUI(){
$("#qTitle").textContent = `Spin for Round ${state.round}`;
$("#qMeta").textContent = "";
$("#qBox").hidden = true;
$("#submitBtn").classList.remove("attn");
$("#spinBtn").disabled = false;
}
$("#spinBtn").addEventListener("click", spinWheel);
$("#submitBtn").addEventListener("click", ()=>{
const ready = state.placed.H0 && state.placed.H1;
if(!ready){ toast("Place A/B into H₀ and H₁ first."); return; }
grade(true);
});
$("#hintBtn").addEventListener("click", ()=> toast("Hint: H₀ usually contains equality (status quo). H₁ claims change/effect.", 2200));
$("#skipBtn").addEventListener("click", ()=>{
state.score -= 50; $("#scoreBox").textContent = `Score: ${state.score}`;
toast("Skipped (-50). Moving on…", 1200); grade(false, "Skipped");
});
$("#powerBtn").addEventListener("click", ()=>{
if(state.powerups.length===0) { toast("No power-ups available."); return; }
const next = state.powerups[0];
if(next==="shield"){ state.shieldOn = true; toast("🛡️ SHIELD armed for next wrong."); }
if(next==="double"){ state.doubleMode = true; toast("✖️2 DOUBLE will apply this round."); }
if(next==="wild"){ state.wildAvailable = true; toast("🃏 WILD ready: auto-swap if it makes you correct."); }
});
$("#resetBtn").addEventListener("click", ()=>{
localStorage.removeItem("hypohero_highscores");
resetGame();
});
/* =========================
ENDGAME / LEADERBOARD
========================= */
function endGame(){
$("#qTitle").textContent = "Game Over!";
$("#qMeta").textContent = "";
$("#qBox").hidden = true;
if(state.ticking){ clearInterval(state.ticking); }
const name = prompt("Enter name for leaderboard:", "Player");
const rec = { name: name || "Player", score: state.score, ts: Date.now() };
const key = "hypohero_highscores";
const list = JSON.parse(localStorage.getItem(key) || "[]");
list.push(rec); list.sort((a,b)=>b.score-a.score);
localStorage.setItem(key, JSON.stringify(list.slice(0,10)));
renderBoard(); toast("Saved to leaderboard. Reset to play again!");
}
function renderBoard(){
const key = "hypohero_highscores";
const list = JSON.parse(localStorage.getItem(key) || "[]");
if(list.length===0){ $("#leaderboard").innerHTML = "<b>Leaderboard:</b> No scores yet. Be the first!"; return; }
const rows = list.map((r,i)=>`<tr><td>${i+1}</td><td>${escapeHtml(r.name)}</td><td style="text-align:right">${r.score}</td></tr>`).join("");
$("#leaderboard").innerHTML = `
<b>Leaderboard (Top 10)</b>
<div class="divider"></div>
<table style="width:100%; border-collapse:collapse">
<thead><tr><th style="text-align:left">#</th><th style="text-align:left">Player</th><th style="text-align:right">Score</th></tr></thead>
<tbody>${rows}</tbody>
</table>`;
}
/* =========================
STARTUP / RESET
========================= */
function resetGame(){
state.round = 1; state.maxRounds = 10; state.score = 0; state.timer = 30;
state.wheelSpun = false; state.powerups = []; state.currentPU = null; state.wheelValue = null;
state.drawn = []; state.currentQ = null; state.placed = {H0:null,H1:null};
state.speedMode = false; state.shieldOn = false; state.doubleMode = false; state.wildAvailable = false; state.streak = 0;
state.correctRound = {A:null, B:null};
$("#scoreBox").textContent = "Score: 0";
$("#roundBox").textContent = "Round: 1 / 10";
$("#timerBox").textContent = "Time: 00:30";
// CLEAR UI AREAS
$("#qMeta").textContent = "";
$("#qText").textContent = "";
$("#candidates").innerHTML = "";
$("#dropH0").innerHTML = `<span class="slot">H₀</span>`;
$("#dropH1").innerHTML = `<span class="slot">H₁</span>`;
$("#feedback").innerHTML = "";
$("#qBox").hidden = true;
$("#submitBtn").disabled = true;
$("#submitBtn").classList.remove("attn");
startSpinPhaseUI();
renderBoard();
}
resetGame();
/* =========================
Tiny SFX (optional)
========================= */
(function(){
const ctx = new (window.AudioContext||window.webkitAudioContext)();
function beep(freq=880, dur=0.05, type="square", vol=0.05){
const o = ctx.createOscillator(); const g = ctx.createGain();
o.type=type; o.frequency.setValueAtTime(freq, ctx.currentTime);
g.gain.value = vol; o.connect(g); g.connect(ctx.destination);
o.start(); o.stop(ctx.currentTime+dur);
}
["spinBtn","submitBtn","hintBtn","skipBtn","powerBtn","resetBtn"].forEach(id=>{
const btn = document.getElementById(id); if(!btn) return;
btn.addEventListener("click", ()=>beep(600,0.045,"sine",0.03));
});
})();
</script>
</body>
</html>