| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>COLLAPSE — the quantum game where you shape the odds, then gamble the measurement</title> |
| <meta name="description" content="A real quantum game. Weave interference to pile amplitude onto the star, then measure and gamble the Born rule. Skill sets the odds; it never sets the outcome. That's the whole of quantum, made playable." /> |
| <meta name="theme-color" content="#06060d" /> |
| <style> |
| /* COLLAPSE — committed to the quantum vacuum. Phase is hue (the honest way to |
| draw a complex amplitude); the target is gold; danger reads coral. Mono is |
| the instrument voice. Single dark world by choice. */ |
| :root{ |
| --void:#06060d; --void-2:#0a0a15; --panel:#0f0f1e; --panel-2:#15152a; |
| --line:#20203c; --line-2:#2f2f4e; |
| --ink:#e9e9ff; --ink-2:#a4a4cf; --ink-3:#66668f; |
| --gold:#ffd98a; --gold-hot:#fff4d6; --cyan:#46e8ff; --coral:#ff6b7d; --violet:#8a6bff; |
| --mono:"JetBrains Mono","Berkeley Mono",ui-monospace,"SF Mono",Menlo,monospace; |
| --sans:"Inter",system-ui,-apple-system,"Segoe UI",sans-serif; |
| } |
| *{box-sizing:border-box;margin:0} |
| body{background:radial-gradient(1000px 560px at 80% -10%,rgba(70,232,255,.05),transparent 60%), |
| radial-gradient(820px 620px at 5% 110%,rgba(138,107,255,.06),transparent 55%),var(--void); |
| color:var(--ink);font-family:var(--sans);min-height:100vh;-webkit-font-smoothing:antialiased} |
| .wrap{max-width:940px;margin:0 auto;padding:24px 18px 70px} |
| header{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:6px} |
| h1{font-family:var(--mono);font-weight:700;font-size:22px;letter-spacing:.2em;color:var(--ink)} |
| .sub{color:var(--ink-3);font-family:var(--mono);font-size:12px;margin-left:auto;letter-spacing:.03em} |
| .lede{color:var(--ink-2);font-size:14.5px;max-width:64ch;margin:10px 0 18px} |
| .lede b{color:var(--ink)} |
| a{color:var(--cyan);text-decoration:none} |
|
|
| .hud{display:flex;gap:20px;align-items:baseline;font-family:var(--mono);flex-wrap:wrap;margin-bottom:14px} |
| .hud .s{font-size:11px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.1em} |
| .hud .s b{display:block;font-size:22px;color:var(--ink);font-variant-numeric:tabular-nums;letter-spacing:normal} |
| .hud .s.gold b{color:var(--gold)} .hud .s.coral b{color:var(--coral)} |
| .lives{letter-spacing:2px} |
|
|
| .stage{display:grid;gap:18px;grid-template-columns:1fr} |
| @media(min-width:800px){.stage{grid-template-columns:minmax(0,1fr) 288px}} |
| .board{background:linear-gradient(180deg,var(--void-2),var(--void));border:1px solid var(--line);border-radius:16px;padding:16px;position:relative} |
| canvas{display:block;width:100%;height:auto;border-radius:10px} |
| .pbar{margin-top:14px} |
| .pbar .lab{display:flex;justify-content:space-between;font-family:var(--mono);font-size:11px;color:var(--ink-3);margin-bottom:5px} |
| .pbar .lab b{color:var(--gold);font-variant-numeric:tabular-nums} |
| .track{height:9px;border-radius:6px;background:var(--panel-2);overflow:hidden;border:1px solid var(--line)} |
| .track>span{display:block;height:100%;background:linear-gradient(90deg,var(--gold),var(--gold-hot));border-radius:6px;transition:width .22s cubic-bezier(.16,1,.3,1)} |
|
|
| .panel{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:15px} |
| .panel h3{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.16em;color:var(--ink-3);margin-bottom:11px;font-weight:600} |
| .qrow{display:flex;align-items:center;gap:7px;margin-bottom:8px} |
| .qrow .ql{font-family:var(--mono);font-size:12px;color:var(--ink-3);width:24px} |
| .g{font-family:var(--mono);font-size:12.5px;font-weight:600;color:var(--ink);background:var(--panel-2);border:1px solid var(--line-2);border-radius:7px;width:31px;height:31px;cursor:pointer;transition:.12s} |
| .g:hover:not(:disabled){border-color:var(--cyan);color:var(--cyan)} .g:active{transform:translateY(1px)} |
| .g:disabled{opacity:.3;cursor:not-allowed} |
| .budget{font-family:var(--mono);font-size:12px;color:var(--ink-2);margin:6px 0 12px} |
| .budget b{color:var(--cyan);font-size:15px} |
| .btn{font-family:var(--mono);font-weight:600;font-size:14px;border:none;border-radius:11px;cursor:pointer;padding:13px;width:100%;letter-spacing:.04em} |
| .measure{background:linear-gradient(180deg,var(--gold),#e9b85f);color:#2a1e05;box-shadow:0 0 26px rgba(255,217,138,.25)} |
| .measure:hover{filter:brightness(1.06)} .measure:disabled{filter:grayscale(.6);opacity:.6;cursor:not-allowed} |
| .msg{font-family:var(--mono);font-size:12.5px;color:var(--ink-3);margin-top:12px;min-height:34px;line-height:1.5} |
| .msg b.win{color:var(--gold)} .msg b.lose{color:var(--coral)} |
| .again{margin-top:8px;background:var(--panel-2);color:var(--ink-2);border:1px solid var(--line-2)} |
| .again:hover{color:var(--ink)} |
| footer{margin-top:34px;padding-top:16px;border-top:1px solid var(--line);font-family:var(--mono);font-size:12px;color:var(--ink-3);display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap} |
| @media(prefers-reduced-motion:reduce){*{animation:none!important}} |
| </style> |
| </head> |
| <body> |
| <div class="wrap"> |
| <header> |
| <h1>COLLAPSE</h1> |
| <span class="sub">shape the odds · then gamble the measurement</span> |
| </header> |
| <p class="lede"> |
| A <b>gold star</b> marks the state you want. Weave <b>interference</b> — split with H, twist phase |
| with Z/S/T, entangle with CNOT — to pile amplitude onto the star and cancel it everywhere else. |
| Then <b>measure</b>. Your probability is your odds, not your outcome: 90% still loses one time in ten. |
| <b>That's the game, and it's the real physics.</b> |
| </p> |
|
|
| <div class="hud"> |
| <span class="s gold"><b id="score">0</b>score</span> |
| <span class="s"><b id="round">1</b>round</span> |
| <span class="s"><b id="streak">0</b>streak</span> |
| <span class="s coral" style="margin-left:auto"><b class="lives" id="lives">♥♥♥</b>lives</span> |
| </div> |
|
|
| <div class="stage"> |
| <div class="board"> |
| <canvas id="board" width="560" height="560" aria-label="Quantum board — 16 basis states"></canvas> |
| <div class="pbar"> |
| <div class="lab"><span>P(collapse onto the star)</span><b id="pstar">0%</b></div> |
| <div class="track"><span id="pfill" style="width:0%"></span></div> |
| </div> |
| </div> |
| <div class="panel"> |
| <h3>Apply gates</h3> |
| <div id="gates"></div> |
| <div class="budget">gates left: <b id="budget">7</b></div> |
| <button class="btn measure" id="measureBtn">⟢ Measure</button> |
| <div class="msg" id="msg">Build the star's amplitude, then gamble the collapse.</div> |
| <button class="btn again" id="againBtn" hidden>new game</button> |
| </div> |
| </div> |
|
|
| <footer> |
| <span>real statevector · 4 qubits · 16 amplitudes · exact quantum, in your browser</span> |
| <span>🜂 ahogy a dolgok vannak</span> |
| </footer> |
| </div> |
|
|
| <script> |
| const SQ=Math.SQRT1_2; |
| const GATES={ H:{a:[SQ,0],b:[SQ,0],c:[SQ,0],d:[-SQ,0]}, Z:{a:[1,0],b:[0,0],c:[0,0],d:[-1,0]}, |
| S:{a:[1,0],b:[0,0],c:[0,0],d:[0,1]}, T:{a:[1,0],b:[0,0],c:[0,0],d:[SQ,SQ]}, X:{a:[0,0],b:[1,0],c:[1,0],d:[0,0]} }; |
| class QState{ |
| constructor(n){this.n=n;this.N=1<<n;this.re=new Float64Array(this.N);this.im=new Float64Array(this.N);this.re[0]=1;} |
| reset(){this.re.fill(0);this.im.fill(0);this.re[0]=1;} |
| apply1(g,q){const bit=1<<q,re=this.re,im=this.im;const[ar,ai]=g.a,[br,bi]=g.b,[cr,ci]=g.c,[dr,di]=g.d; |
| for(let i=0;i<this.N;i++){if(i&bit)continue;const j=i|bit;const xr=re[i],xi=im[i],yr=re[j],yi=im[j]; |
| re[i]=ar*xr-ai*xi+br*yr-bi*yi; im[i]=ar*xi+ai*xr+br*yi+bi*yr; |
| re[j]=cr*xr-ci*xi+dr*yr-di*yi; im[j]=cr*xi+ci*xr+dr*yi+di*yr;}} |
| cnot(cq,tq){const cb=1<<cq,tb=1<<tq;for(let i=0;i<this.N;i++){if((i&cb)&&!(i&tb)){const j=i|tb; |
| let t=this.re[i];this.re[i]=this.re[j];this.re[j]=t;t=this.im[i];this.im[i]=this.im[j];this.im[j]=t;}}} |
| prob(k){return this.re[k]*this.re[k]+this.im[k]*this.im[k];} |
| measure(){let r=Math.random(),acc=0,out=this.N-1;for(let k=0;k<this.N;k++){acc+=this.prob(k);if(r<=acc){out=k;break;}} |
| this.re.fill(0);this.im.fill(0);this.re[out]=1;return out;} |
| } |
| const NQ=4, q=new QState(NQ), reduce=matchMedia('(prefers-reduced-motion:reduce)').matches; |
| const cv=document.getElementById('board'), cx=cv.getContext('2d'); |
| let star=0, budget=7, score=0, round=1, streak=0, lives=3, phase='build', collapsed=-1, flash=0, over=false; |
| const $=id=>document.getElementById(id); |
| |
| function newRound(keepScore){ |
| q.reset(); collapsed=-1; phase='build'; |
| budget = Math.max(4, 8 - Math.floor(round/3)); |
| do{ star=Math.floor(Math.random()*16); }while(star===0); |
| $('budget').textContent=budget; $('round').textContent=round; updateHUD(); enableGates(true); |
| $('msg').textContent='Pile amplitude on the star, then measure.'; $('measureBtn').disabled=false; |
| } |
| function buildGates(){ |
| const w=$('gates'); |
| for(let qb=0; qb<NQ; qb++){ |
| const row=document.createElement('div'); row.className='qrow'; |
| row.innerHTML=`<span class="ql">q${qb}</span>`; |
| ['H','Z','S','T'].forEach(g=>{const b=document.createElement('button');b.className='g';b.textContent=g;b.dataset.q=qb;b.dataset.g=g; |
| b.onclick=()=>useGate(()=>q.apply1(GATES[g],qb)); row.appendChild(b);}); |
| const cn=document.createElement('button');cn.className='g';cn.textContent='⊕';cn.title='CNOT q'+qb+'→q'+((qb+1)%NQ);cn.dataset.q=qb; |
| cn.onclick=()=>useGate(()=>q.cnot(qb,(qb+1)%NQ)); row.appendChild(cn); |
| w.appendChild(row); |
| } |
| } |
| function useGate(fn){ if(phase!=='build'||budget<=0||over) return; fn(); budget--; $('budget').textContent=budget; |
| updateP(); if(budget<=0){ enableGates(false); $('msg').innerHTML='Out of gates — <b>measure</b> and take your odds.'; } } |
| function enableGates(on){ document.querySelectorAll('#gates .g').forEach(b=>b.disabled=!on); } |
| function updateP(){ const p=q.prob(star); $('pstar').textContent=Math.round(p*100)+'%'; $('pfill').style.width=(p*100)+'%'; } |
| function updateHUD(){ $('score').textContent=score; $('streak').textContent=streak; $('lives').textContent='♥'.repeat(Math.max(0,lives))+'·'.repeat(3-Math.max(0,lives)); updateP(); } |
| |
| $('measureBtn').onclick=()=>{ |
| if(phase!=='build'||over) return; |
| const p=q.prob(star); const out=q.measure(); collapsed=out; flash=1; phase='collapsed'; enableGates(false); $('measureBtn').disabled=true; |
| if(out===star){ const gain=Math.round(p*100)*(streak+1); score+=gain; streak++; |
| $('msg').innerHTML=`Collapsed onto the star. <b class="win">+${gain}</b> (P was ${Math.round(p*100)}% · streak ×${streak})`; |
| round++; setTimeout(()=>{ if(!over) newRound(); }, 1100); |
| } else { streak=0; lives--; |
| const bits=out.toString(2).padStart(NQ,'0'), sb=star.toString(2).padStart(NQ,'0'); |
| if(lives<=0){ over=true; $('msg').innerHTML=`Collapsed to |${bits}⟩, not |${sb}⟩. <b class="lose">Game over.</b> Score ${score}.`; $('againBtn').hidden=false; } |
| else { $('msg').innerHTML=`Collapsed to |${bits}⟩, wanted |${sb}⟩ — the ${100-Math.round(p*100)}% bit you. <b class="lose">−1 ♥</b>`; |
| setTimeout(()=>{ if(!over) newRound(); }, 1300); } |
| } |
| updateHUD(); |
| }; |
| $('againBtn').onclick=()=>{ score=0;round=1;streak=0;lives=3;over=false; $('againBtn').hidden=true; newRound(); }; |
| |
| function draw(t){ |
| const W=cv.width,H=cv.height, G=4, M=54, S=(Math.min(W,H)-M*2)/G; |
| cx.clearRect(0,0,W,H); |
| for(let k=0;k<16;k++){ const gx=k%G, gy=(k>>2)|0; const px=M+gx*S+S/2, py=M+gy*S+S/2; |
| const isStar=(k===star), p=q.prob(k), mag=Math.sqrt(p); |
| |
| if(isStar && phase==='build'){ cx.strokeStyle='rgba(255,217,138,'+(0.5+0.3*Math.sin(t/380))+')'; cx.lineWidth=2; |
| cx.setLineDash([5,5]); cx.lineDashOffset=reduce?0:-t/40; cx.beginPath(); cx.arc(px,py,S*0.42,0,7); cx.stroke(); cx.setLineDash([]); } |
| if(phase==='collapsed'){ |
| if(k===collapsed){ const fl=Math.max(0,flash); const hot=(k===star); |
| cx.shadowColor=hot?'#ffd98a':'#ff6b7d'; cx.shadowBlur=30+fl*36; cx.fillStyle=hot?'rgba(255,217,138,.95)':'rgba(255,107,125,.95)'; |
| cx.beginPath(); cx.arc(px,py,S*0.22+fl*8,0,7); cx.fill(); cx.shadowBlur=0; } |
| else { cx.fillStyle='rgba(102,102,143,.14)'; cx.beginPath(); cx.arc(px,py,3,0,7); cx.fill(); } |
| continue; |
| } |
| |
| const hue=(Math.atan2(q.im[k],q.re[k])*180/Math.PI+360)%360; |
| if(mag>0.02){ const r=(S*0.10+mag*S*0.34)*(reduce?1:(0.9+0.1*Math.sin(t/500+k))); |
| cx.shadowColor=`hsl(${hue} 90% 62%)`; cx.shadowBlur=18*mag; cx.fillStyle=`hsla(${hue} 92% ${isStar?70:60}% / ${0.34+0.6*mag})`; |
| cx.beginPath(); cx.arc(px,py,r,0,7); cx.fill(); cx.shadowBlur=0; } |
| else { cx.fillStyle='rgba(102,102,143,.16)'; cx.beginPath(); cx.arc(px,py,2,0,7); cx.fill(); } |
| if(isStar){ cx.fillStyle='rgba(255,217,138,.9)'; cx.font='16px '+getComputedStyle(document.body).getPropertyValue('--mono'); cx.textAlign='center'; cx.textBaseline='middle'; cx.fillText('★',px,py+S*0.42+2); } |
| } |
| if(flash>0) flash-=0.03; |
| requestAnimationFrame(draw); |
| } |
| buildGates(); newRound(); requestAnimationFrame(draw); |
| </script> |
| </body> |
| </html> |
|
|