Claude Opus 5

AnthropicJuly 2026

What it wrote

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>VOID FORGE — an incremental</title>
<style>
:root{
  --bg:#05060a; --panel:#0c1018cc; --line:#1e2a3d;
  --cy:#3df5ff; --mg:#ff4fd8; --gd:#ffd166; --gr:#7bffb2;
  --dim:#7d8ba3;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
  background:var(--bg); color:#e8f0ff; overflow:hidden;
  font:14px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;
  user-select:none; -webkit-tap-highlight-color:transparent;
}
#stars{position:fixed;inset:0;z-index:0}
#glow{position:fixed;inset:-30%;z-index:0;pointer-events:none;
  background:radial-gradient(40% 40% at 30% 30%,#3df5ff18,transparent 70%),
             radial-gradient(35% 35% at 70% 70%,#ff4fd818,transparent 70%);
  animation:drift 24s ease-in-out infinite alternate}
@keyframes drift{to{transform:translate(4%,-4%) scale(1.1)}}

#app{position:relative;z-index:1;height:100%;display:grid;
  grid-template-columns:300px 1fr 340px;gap:14px;padding:14px}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;
  backdrop-filter:blur(8px);overflow:hidden;display:flex;flex-direction:column}
.panel>h2{font-size:11px;letter-spacing:.22em;color:var(--dim);text-transform:uppercase;
  padding:12px 14px;border-bottom:1px solid var(--line);flex:none}
.scroll{overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:8px}
.scroll::-webkit-scrollbar{width:8px}
.scroll::-webkit-scrollbar-thumb{background:#1e2a3d;border-radius:8px}

/* left */
#left{align-items:center;justify-content:center;gap:6px;padding:18px 14px;text-align:center}
.title{font-size:13px;letter-spacing:.4em;color:var(--cy);text-shadow:0 0 18px #3df5ff88}
#flux{font-size:38px;font-weight:700;letter-spacing:-1px;line-height:1.1;
  text-shadow:0 0 26px #3df5ff66;word-break:break-all}
#rate{color:var(--gr);font-size:13px}
#forge{margin:14px 0;width:170px;height:170px;border-radius:50%;cursor:pointer;flex:none;
  border:1px solid #3df5ff55;background:radial-gradient(circle at 50% 40%,#123044,#080d16 70%);
  box-shadow:0 0 40px #3df5ff2e,inset 0 0 40px #3df5ff1a;
  display:grid;place-items:center;font-size:46px;color:var(--cy);
  transition:transform .07s, box-shadow .2s}
#forge:hover{box-shadow:0 0 70px #3df5ff55,inset 0 0 50px #3df5ff33}
#forge:active{transform:scale(.94)}
#forge::after{content:'';position:absolute;width:190px;height:190px;border-radius:50%;
  border:1px solid #3df5ff33;animation:ring 3s linear infinite;pointer-events:none}
@keyframes ring{0%{transform:scale(.9);opacity:.7}100%{transform:scale(1.25);opacity:0}}
.sub{color:var(--dim);font-size:12px}
#prestige{margin-top:auto;width:100%}
button.big{width:100%;padding:11px;border-radius:10px;cursor:pointer;font:inherit;
  border:1px solid var(--mg);background:#ff4fd816;color:var(--mg);letter-spacing:.1em}
button.big:hover:not(:disabled){background:#ff4fd82e;box-shadow:0 0 24px #ff4fd844}
button.big:disabled{opacity:.35;cursor:not-allowed}

/* items */
.item{border:1px solid var(--line);border-radius:11px;padding:10px 12px;cursor:pointer;
  display:grid;grid-template-columns:36px 1fr auto;gap:10px;align-items:center;
  background:#0d1421aa;transition:.12s}
.item:hover{border-color:#3df5ff66;background:#0f1a2baa;transform:translateX(2px)}
.item.off{opacity:.4;cursor:not-allowed;transform:none}
.item.off:hover{border-color:var(--line);background:#0d1421aa}
.ico{font-size:22px;color:var(--cy);text-align:center}
.nm{font-weight:600}
.ds{color:var(--dim);font-size:11.5px}
.rt{text-align:right;font-size:11.5px;color:var(--dim)}
.cost{color:var(--gd);font-weight:600;font-size:13px}
.own{font-size:20px;font-weight:700;color:#fff;text-align:right;line-height:1}
.tabs{display:flex;gap:6px;padding:10px 10px 0}
.tab{flex:1;padding:7px;text-align:center;border:1px solid var(--line);border-radius:9px;
  cursor:pointer;font-size:11px;letter-spacing:.12em;color:var(--dim)}
.tab.on{color:var(--cy);border-color:#3df5ff66;background:#3df5ff12}
#buyrow{display:flex;gap:6px;padding:0 10px 10px}
#buyrow .tab{flex:1}
.ach{display:grid;grid-template-columns:30px 1fr;gap:9px;padding:8px 10px;border-radius:10px;
  border:1px solid var(--line);background:#0d1421aa;opacity:.35}
.ach.got{opacity:1;border-color:#ffd16655;background:#ffd1660d}
.ach .ico{font-size:18px;color:var(--gd)}

/* fx */
.float{position:fixed;pointer-events:none;z-index:50;font-weight:700;color:var(--cy);
  text-shadow:0 0 12px #3df5ff;animation:fl 1s ease-out forwards}
@keyframes fl{to{transform:translateY(-70px);opacity:0}}
#surge{position:fixed;z-index:60;font-size:40px;cursor:pointer;filter:drop-shadow(0 0 18px var(--gd));
  animation:spin 2s linear infinite;display:none}
@keyframes spin{to{transform:rotate(360deg)}}
#toast{position:fixed;left:50%;top:22px;transform:translateX(-50%);z-index:70;
  display:flex;flex-direction:column;gap:8px;align-items:center}
.msg{background:#0c1018f2;border:1px solid var(--cy);border-radius:10px;padding:10px 18px;
  color:var(--cy);box-shadow:0 0 30px #3df5ff33;animation:in .3s ease-out}
@keyframes in{from{opacity:0;transform:translateY(-14px)}}
#banner{position:fixed;top:0;left:0;right:0;z-index:65;text-align:center;padding:6px;
  background:linear-gradient(90deg,transparent,#ffd16633,transparent);color:var(--gd);
  letter-spacing:.2em;display:none}
@media(max-width:980px){
  body{overflow:auto}
  #app{grid-template-columns:1fr;height:auto;min-height:100%}
  .panel{max-height:60vh}
}
</style>
</head>
<body>
<canvas id="stars"></canvas><div id="glow"></div>
<div id="banner"></div><div id="toast"></div><div id="surge">✧</div>

<div id="app">
  <div class="panel" id="left">
    <div class="title">VOID FORGE</div>
    <div id="flux">0</div>
    <div id="rate">0 /s</div>
    <div id="forge">⬢</div>
    <div class="sub" id="clickinfo">+1 per strike</div>
    <div class="sub" id="singinfo"></div>
    <div id="prestige">
      <button class="big" id="collapse" disabled>COLLAPSE</button>
      <div class="sub" id="collapseinfo" style="margin-top:6px"></div>
    </div>
  </div>

  <div class="panel">
    <h2>Generators</h2>
    <div id="buyrow">
      <div class="tab on" data-buy="1">×1</div>
      <div class="tab" data-buy="10">×10</div>
      <div class="tab" data-buy="100">×100</div>
      <div class="tab" data-buy="max">MAX</div>
    </div>
    <div class="scroll" id="gens"></div>
  </div>

  <div class="panel">
    <div class="tabs">
      <div class="tab on" data-tab="ups">UPGRADES</div>
      <div class="tab" data-tab="achs">TROPHIES</div>
      <div class="tab" data-tab="stats">STATS</div>
    </div>
    <div class="scroll" id="right"></div>
  </div>
</div>

<script>
/* ---------- helpers ---------- */
const SUF=['','K','M','B','T','Qa','Qi','Sx','Sp','Oc','No','Dc','Ud','Dd'];
function fmt(n){
  if(!isFinite(n))return'∞';
  if(n<0)return'-'+fmt(-n);
  if(n<1000)return n<10?(Math.round(n*10)/10+''):Math.floor(n)+'';
  const t=Math.floor(Math.log10(n)/3);
  if(t>=SUF.length)return n.toExponential(2).replace('e+','e');
  return (n/Math.pow(1000,t)).toFixed(2)+SUF[t];
}
const $=s=>document.querySelector(s);

/* ---------- content ---------- */
const GENS=[
 {n:'Spark Coil',      i:'✦', c:15,     r:.1},
 {n:'Plasma Node',     i:'◉', c:120,    r:1},
 {n:'Fusion Cell',     i:'❂', c:1.4e3,  r:8},
 {n:'Quantum Loom',    i:'⧉', c:2e4,    r:47},
 {n:'Singularity Tap', i:'◍', c:3.3e5,  r:260},
 {n:'Dyson Sliver',    i:'☀', c:5.1e6,  r:1400},
 {n:'Nebula Engine',   i:'✷', c:7.5e7,  r:7800},
 {n:'Void Forge',      i:'⬢', c:1e9,    r:4.4e4},
 {n:'Chrono Spindle',  i:'⟳', c:1.4e10, r:2.6e5},
 {n:'Reality Anvil',   i:'⚒', c:1.7e11, r:1.6e6},
];
const RATE=1.15, ROMAN=['I','II','III','IV','V'];

const UPS=[];
GENS.forEach((g,i)=>[10,25,50,100,200].forEach((need,t)=>UPS.push({
  id:'g'+i+'_'+need, n:g.n+' '+ROMAN[t], d:'×2 '+g.n+' output', i:g.i,
  cost:g.c*Math.pow(12,t+1), req:()=>S.gens[i]>=need, fn:m=>m.gen[i]*=2
})));
[[1,300],[2,5e4],[3,2e7],[4,4e10],[5,9e14]].forEach(([t,cost])=>UPS.push({
  id:'c'+t, n:'Kinetic Lens '+ROMAN[t-1], d:'×4 strike power', i:'✋',
  cost, req:()=>S.clicks>=t*40, fn:m=>m.click*=4
}));
[[.01,1e6,1],[.05,1e11,2],[.15,1e17,3]].forEach(([sh,cost,t])=>UPS.push({
  id:'s'+t, n:'Resonance '+ROMAN[t-1], d:'Strikes also gain '+(sh*100)+'% of /s', i:'⟡',
  cost, req:()=>S.clicks>=t*150, fn:m=>m.share=Math.max(m.share,sh)
}));
[[2,1e9],[2,1e14],[2,1e20]].forEach(([x,cost],t)=>UPS.push({
  id:'a'+t, n:'Void Harmonic '+ROMAN[t], d:'×2 ALL production', i:'✺',
  cost, req:()=>S.gens.filter(v=>v>0).length>=6+t, fn:m=>m.all*=x
}));

const ACHS=[
 {n:'First Light',   d:'Strike the forge once',        i:'✦', f:()=>S.clicks>=1},
 {n:'Blistered',     d:'500 strikes',                  i:'✋', f:()=>S.clicks>=500},
 {n:'Jackhammer',    d:'5,000 strikes',                i:'⚡', f:()=>S.clicks>=5000},
 {n:'Kindling',      d:'Own 10 generators total',      i:'◉', f:()=>tot()>=10},
 {n:'Industrialist', d:'Own 100 generators total',     i:'⚙', f:()=>tot()>=100},
 {n:'Megastructure', d:'Own 500 generators total',     i:'🏗', f:()=>tot()>=500},
 {n:'Diversified',   d:'Own one of every generator',   i:'⧉', f:()=>S.gens.every(v=>v>0)},
 {n:'Deca-Coil',     d:'50 Spark Coils',               i:'✧', f:()=>S.gens[0]>=50},
 {n:'Star Eater',    d:'Own a Dyson Sliver',           i:'☀', f:()=>S.gens[5]>0},
 {n:'Anvil Bearer',  d:'Own a Reality Anvil',          i:'⚒', f:()=>S.gens[9]>0},
 {n:'Millionaire',   d:'Earn 1M flux',                 i:'◈', f:()=>S.earned>=1e6},
 {n:'Billionaire',   d:'Earn 1B flux',                 i:'◆', f:()=>S.earned>=1e9},
 {n:'Trillionaire',  d:'Earn 1T flux',                 i:'❖', f:()=>S.earned>=1e12},
 {n:'Absurd',        d:'Earn 1Qa flux',                i:'✵', f:()=>S.earned>=1e15},
 {n:'Tinkerer',      d:'Buy 10 upgrades',              i:'⟡', f:()=>S.ups.length>=10},
 {n:'Completionist', d:'Buy 40 upgrades',              i:'⟢', f:()=>S.ups.length>=40},
 {n:'Surge Rider',   d:'Catch a flux surge',           i:'✧', f:()=>S.surges>=1},
 {n:'Storm Chaser',  d:'Catch 25 surges',              i:'🌩', f:()=>S.surges>=25},
 {n:'Collapse',      d:'Prestige once',                i:'◍', f:()=>S.singTotal>=1},
 {n:'Event Horizon', d:'Hold 100 singularities',       i:'⬤', f:()=>S.sing>=100},
];

/* ---------- state ---------- */
const fresh=()=>({flux:0,earned:0,run:0,clicks:0,sing:0,singTotal:0,surges:0,
  gens:GENS.map(()=>0),ups:[],achs:[],last:Date.now(),start:Date.now(),collapses:0});
let S=fresh(), buyAmt=1, tab='ups', surgeEnd=0;
const tot=()=>S.gens.reduce((a,b)=>a+b,0);

function mults(){
  const m={gen:GENS.map(()=>1),click:1,all:1,share:0};
  for(const u of UPS) if(S.ups.includes(u.id)) u.fn(m);
  m.all*=1+.01*S.achs.length;
  m.all*=1+.15*S.sing;
  if(Date.now()<surgeEnd){m.all*=7;m.click*=7;}
  return m;
}
const perSec=m=>GENS.reduce((a,g,i)=>a+S.gens[i]*g.r*m.gen[i],0)*m.all;
const clickPow=m=>(1+perSec(m)*m.share)*m.click*m.all;
const cost=(i,k)=>GENS[i].c*Math.pow(RATE,S.gens[i])*(Math.pow(RATE,k)-1)/(RATE-1);
function maxBuy(i){
  const b=GENS[i].c*Math.pow(RATE,S.gens[i]), x=1+S.flux*(RATE-1)/b;
  return x<=1?0:Math.max(0,Math.floor(Math.log(x)/Math.log(RATE)));
}
const amt=i=>buyAmt==='max'?Math.max(1,maxBuy(i)):buyAmt;
const singGain=()=>Math.floor(Math.sqrt(S.run/1e10));

/* ---------- actions ---------- */
function gain(n){S.flux+=n;S.earned+=n;S.run+=n;}
function strike(e){
  const n=clickPow(mults());
  gain(n); S.clicks++;
  const r=$('#forge').getBoundingClientRect();
  const x=e?e.clientX:r.left+r.width/2, y=e?e.clientY:r.top+r.height/2;
  float('+'+fmt(n),x+(Math.random()*40-20),y);
}
function buy(i){
  const k=amt(i), c=cost(i,k);
  if(k<1||S.flux<c)return;
  S.flux-=c; S.gens[i]+=k; dirty=true;
}
function buyUp(u){
  if(S.ups.includes(u.id)||S.flux<u.cost||!u.req())return;
  S.flux-=u.cost; S.ups.push(u.id); dirty=true; toast('▲ '+u.n);
}
function collapse(){
  const g=singGain(); if(g<1)return;
  if(!confirm('Collapse the universe?\n\nYou gain '+g+' singularities (+'+(g*15)+'% permanent production) and lose everything else.'))return;
  const keep={sing:S.sing+g,singTotal:S.singTotal+g,earned:S.earned,clicks:S.clicks,
    achs:S.achs,surges:S.surges,start:S.start,collapses:S.collapses+1};
  S=Object.assign(fresh(),keep); surgeEnd=0; dirty=true; toast('◍ +'+g+' singularities');
}

/* ---------- fx ---------- */
function float(t,x,y){
  const e=document.createElement('div');
  e.className='float'; e.textContent=t; e.style.left=x+'px'; e.style.top=y+'px';
  document.body.appendChild(e); setTimeout(()=>e.remove(),1000);
}
function toast(t){
  const e=document.createElement('div'); e.className='msg'; e.textContent=t;
  $('#toast').appendChild(e); setTimeout(()=>e.remove(),3200);
}
let surgeT=45+Math.random()*60;
function surgeTick(dt){
  const el=$('#surge');
  if(el.style.display==='block')return;
  surgeT-=dt;
  if(surgeT>0)return;
  surgeT=60+Math.random()*120;
  el.style.display='block';
  el.style.left=(10+Math.random()*75)+'vw'; el.style.top=(15+Math.random()*65)+'vh';
  setTimeout(()=>el.style.display='none',11000);
}
$('#surge').onclick=e=>{
  e.currentTarget.style.display='none'; S.surges++;
  if(Math.random()<.5){
    surgeEnd=Date.now()+30000; banner('⚡ FLUX SURGE — ×7 FOR 30s');
  }else{
    const n=Math.max(S.flux*.15,perSec(mults())*90);
    gain(n); toast('✧ Overflow  +'+fmt(n));
  }
};
function banner(t){
  const b=$('#banner'); b.textContent=t; b.style.display='block';
  clearTimeout(b._t); b._t=setTimeout(()=>b.style.display='none',30000);
}

/* ---------- render ---------- */
let dirty=true;
const genEls=GENS.map((g,i)=>{
  const e=document.createElement('div'); e.className='item';
  e.innerHTML=`<div class="ico">${g.i}</div>
    <div><div class="nm">${g.n}</div><div class="ds"></div></div>
    <div><div class="own">0</div><div class="rt"></div></div>`;
  e.onclick=()=>buy(i); $('#gens').appendChild(e); return e;
});
function renderGens(m){
  GENS.forEach((g,i)=>{
    const e=genEls[i], k=amt(i), c=cost(i,k), can=S.flux>=c&&k>0;
    const locked=i>0&&S.gens[i-1]===0&&S.gens[i]===0&&S.flux<GENS[i].c*.3;
    e.style.display=locked?'none':'';
    e.classList.toggle('off',!can);
    e.children[1].children[1].innerHTML=
      `<span class="cost">${fmt(c)}</span> for ${k} · ${fmt(g.r*m.gen[i]*m.all)}/s each`;
    e.children[2].children[0].textContent=S.gens[i];
    e.children[2].children[1].textContent=S.gens[i]?fmt(S.gens[i]*g.r*m.gen[i]*m.all)+'/s':'';
  });
}
function renderRight(){
  const box=$('#right'); box.innerHTML='';
  if(tab==='ups'){
    const avail=UPS.filter(u=>!S.ups.includes(u.id)&&u.req());
    if(!avail.length)box.innerHTML='<div class="ds" style="padding:14px">Nothing available. Buy more generators.</div>';
    avail.sort((a,b)=>a.cost-b.cost).forEach(u=>{
      const e=document.createElement('div');
      e.className='item'+(S.flux>=u.cost?'':' off');
      e.innerHTML=`<div class="ico">${u.i}</div>
        <div><div class="nm">${u.n}</div><div class="ds">${u.d}</div></div>
        <div class="cost">${fmt(u.cost)}</div>`;
      e.onclick=()=>buyUp(u); box.appendChild(e);
    });
  }else if(tab==='achs'){
    ACHS.forEach(a=>{
      const got=S.achs.includes(a.n);
      const e=document.createElement('div'); e.className='ach'+(got?' got':'');
      e.innerHTML=`<div class="ico">${a.i}</div>
        <div><div class="nm">${got?a.n:'???'}</div><div class="ds">${a.d}</div></div>`;
      box.appendChild(e);
    });
  }else{
    const m=mults(), h=(Date.now()-S.start)/3.6e6;
    box.innerHTML=`<div class="item" style="grid-template-columns:1fr;cursor:default">${[
      ['Flux (this run)',fmt(S.run)],['Flux (all time)',fmt(S.earned)],
      ['Per second',fmt(perSec(m))],['Per strike',fmt(clickPow(m))],
      ['Strikes',S.clicks],['Generators owned',tot()],
      ['Upgrades',S.ups.length+' / '+UPS.length],['Trophies',S.achs.length+' / '+ACHS.length],
      ['Singularities',S.sing],['Collapses',S.collapses],
      ['Surges caught',S.surges],['Global multiplier','×'+fmt(m.all)],
      ['Played',h.toFixed(1)+' h'],
    ].map(([k,v])=>`<div style="display:flex;justify-content:space-between;padding:3px 0">
      <span class="ds">${k}</span><b>${v}</b></div>`).join('')}</div>
    <button class="big" id="wipe" style="border-color:#ff5c5c;color:#ff5c5c;background:#ff5c5c14">ERASE SAVE</button>`;
    $('#wipe').onclick=()=>{if(confirm('Erase everything, permanently?')){localStorage.removeItem(KEY);S=fresh();dirty=true;}};
  }
}
function render(){
  const m=mults();
  $('#flux').textContent=fmt(S.flux);
  $('#rate').textContent=fmt(perSec(m))+' /s';
  $('#clickinfo').textContent='+'+fmt(clickPow(m))+' per strike';
  $('#singinfo').textContent=S.sing?'◍ '+S.sing+' singularities · ×'+(1+.15*S.sing).toFixed(2):'';
  const g=singGain();
  $('#collapse').disabled=g<1;
  $('#collapseinfo').textContent=g<1
    ? 'Collapse at '+fmt(1e10)+' this run'
    : '+'+g+' singularities (+'+(g*15)+'%)';
  renderGens(m);
  if(dirty||tab==='stats'){renderRight();dirty=false;}
  else document.querySelectorAll('#right .item').forEach(e=>{
    const c=e.querySelector('.cost'); if(!c)return;
    const u=UPS.find(u=>u.n===e.querySelector('.nm').textContent);
    if(u)e.classList.toggle('off',S.flux<u.cost);
  });
}

/* ---------- loop ---------- */
function tick(dt){
  const m=mults();
  gain(perSec(m)*dt);
  for(const a of ACHS) if(!S.achs.includes(a.n)&&a.f()){S.achs.push(a.n);toast('★ '+a.n);dirty=true;}
  surgeTick(dt);
}
let last=performance.now();
function loop(t){
  const dt=Math.min((t-last)/1000,.5); last=t;
  tick(dt); render(); requestAnimationFrame(loop);
}

/* ---------- save ---------- */
const KEY='voidforge.v1';
const save=()=>{S.last=Date.now();localStorage.setItem(KEY,JSON.stringify(S));};
function load(){
  const raw=localStorage.getItem(KEY); if(!raw)return;
  try{ S=Object.assign(fresh(),JSON.parse(raw)); }catch(e){ return; }
  const off=Math.min((Date.now()-S.last)/1000,8*3600);
  if(off>60){
    const n=perSec(mults())*off*.5;
    if(n>0){gain(n);toast('⌛ Offline '+(off/3600).toFixed(1)+'h  +'+fmt(n));}
  }
}

/* ---------- wiring ---------- */
$('#forge').onclick=strike;
$('#collapse').onclick=collapse;
document.querySelectorAll('#buyrow .tab').forEach(t=>t.onclick=()=>{
  document.querySelectorAll('#buyrow .tab').forEach(x=>x.classList.remove('on'));
  t.classList.add('on'); buyAmt=t.dataset.buy==='max'?'max':+t.dataset.buy;
});
document.querySelectorAll('.tabs .tab').forEach(t=>t.onclick=()=>{
  document.querySelectorAll('.tabs .tab').forEach(x=>x.classList.remove('on'));
  t.classList.add('on'); tab=t.dataset.tab; dirty=true;
});
addEventListener('keydown',e=>{
  if(e.code==='Space'){e.preventDefault();strike();}
  if(e.key>='1'&&e.key<='9')buy(+e.key-1);
  if(e.key==='0')buy(9);
});
addEventListener('beforeunload',save);
setInterval(save,10000);

/* starfield */
{
  const c=$('#stars'), x=c.getContext('2d');
  let st=[];
  const size=()=>{c.width=innerWidth;c.height=innerHeight;
    st=Array.from({length:140},()=>({x:Math.random()*c.width,y:Math.random()*c.height,
      z:Math.random()*.9+.1,r:Math.random()*1.4+.2}));};
  addEventListener('resize',size); size();
  (function draw(){
    x.clearRect(0,0,c.width,c.height);
    for(const s of st){
      s.y+=s.z*.14; if(s.y>c.height)s.y=0,s.x=Math.random()*c.width;
      x.globalAlpha=s.z*.8;
      x.fillStyle=s.z>.7?'#3df5ff':'#8fa6c9';
      x.fillRect(s.x,s.y,s.r,s.r);
    }
    requestAnimationFrame(draw);
  })();
}

load(); requestAnimationFrame(t=>{last=t;loop(t);});

/* self-check: open index.html?test and read the console */
if(location.search.includes('test')){
  const eq=(a,b,m)=>console.assert(a===b,m,a,b);
  eq(fmt(999),'999','fmt small'); eq(fmt(1500),'1.50K','fmt K'); eq(fmt(1.5e9),'1.50B','fmt B');
  const bak=JSON.stringify(S);
  S=fresh(); eq(Math.round(cost(0,1)),15,'cost 1');
  eq(Math.round(cost(0,2)*100)/100,32.25,'cost 2 = 15+17.25');
  S.flux=32.25; eq(maxBuy(0),2,'maxBuy exact');
  S.flux=32.24; eq(maxBuy(0),1,'maxBuy under');
  S.flux=1e9; const k=maxBuy(0); S.gens[0]=0;
  eq(cost(0,k)<=1e9&&cost(0,k+1)>1e9,true,'maxBuy boundary');
  S=JSON.parse(bak); console.log('self-check done');
}
</script>
</body>
</html>