| <!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-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} |
| |
| |
| #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; } |
| } |
| |
| |
| .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{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"> |
| |
| <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> |
|
|
| |
| <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> |
| |
| |
| |
| 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"} |
| ]; |
| |
| |
| |
| |
| 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 } |
| }; |
| |
| |
| |
| |
| 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=>({ "&":"&","<":"<","\"":""","'":"'" }[m])) } |
| |
| |
| |
| |
| 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(); |
| } |
| |
| |
| |
| |
| 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); |
| |
| |
| 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)); |
| |
| |
| $("#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; |
| |
| |
| const assignH0To = Math.random() < 0.5 ? "A" : "B"; |
| const assignH1To = assignH0To === "A" ? "B" : "A"; |
| state.correctRound = { [assignH0To]: "H0", [assignH1To]: "H1" }; |
| |
| |
| const textForA = assignH0To === "A" ? q.candidates.A : q.candidates.B; |
| const textForB = assignH0To === "A" ? q.candidates.B : q.candidates.A; |
| |
| 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); |
| |
| |
| 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()); |
| z.appendChild(tile); |
| autoFillOpposite(); |
| recalcPlacements(); |
| }); |
| }); |
| } |
| |
| |
| 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); } |
| } |
| } |
| |
| |
| 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(); |
| } |
| |
| |
| 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 }); |
| } |
| |
| |
| 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); |
| } |
| |
| |
| |
| |
| 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); |
| |
| |
| 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}`; |
| |
| |
| 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); |
| } |
| |
| |
| |
| |
| 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(); |
| }); |
| |
| |
| |
| |
| 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>`; |
| } |
| |
| |
| |
| |
| 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"; |
| |
| $("#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(); |
| |
| |
| |
| |
| (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> |
|
|