collapse / index.html
PeetPedro's picture
COLLAPSE — a real quantum game: shape the odds with interference, gamble the Born rule
225e1d0 verified
Raw
History Blame Contribute Delete
13.2 kB
<!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); // never trivial (|0000> is the start)
$('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);
// star target ring
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;
}
// amplitude blob: hue=phase, size=|amp|
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>