← back to Games Agentabrams
games/gems/NeonGems.aa
756 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>NEON GEMS</title>
<style>
:root{
--bg:#0a0e14;
--panel:#0f1622;
--line:#1c2735;
--ink:#e8f0ff;
--muted:#7f93ad;
--accent:#3df2ff;
}
*{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html,body{ margin:0; padding:0; height:100%; }
body{
background:
radial-gradient(1200px 700px at 50% -10%, #12213a 0%, rgba(18,33,58,0) 60%),
var(--bg);
color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
touch-action:none;
overflow:hidden;
display:flex;
align-items:center;
justify-content:center;
min-height:100%;
user-select:none;
-webkit-user-select:none;
}
#wrap{
display:flex;
flex-direction:column;
align-items:center;
gap:14px;
padding:16px;
width:100%;
max-width:560px;
}
#title{
margin:0;
font-size:clamp(20px,6vw,34px);
font-weight:800;
letter-spacing:.42em;
text-transform:uppercase;
text-indent:.42em;
background:linear-gradient(90deg,#3df2ff,#8a7dff 45%,#ff5db8);
-webkit-background-clip:text;
background-clip:text;
color:transparent;
filter:drop-shadow(0 0 12px rgba(61,242,255,.35));
text-align:center;
}
#hud{
width:100%;
display:flex;
gap:10px;
justify-content:center;
}
.stat{
flex:1;
background:linear-gradient(180deg,var(--panel),#0b1220);
border:1px solid var(--line);
border-radius:12px;
padding:8px 10px;
text-align:center;
box-shadow:inset 0 1px 0 rgba(255,255,255,.03);
}
.stat .lbl{
font-size:10px;
letter-spacing:.22em;
text-transform:uppercase;
color:var(--muted);
}
.stat .val{
font-size:clamp(16px,5vw,24px);
font-weight:800;
margin-top:2px;
font-variant-numeric:tabular-nums;
color:var(--ink);
}
#boardWrap{
position:relative;
width:100%;
border-radius:16px;
background:linear-gradient(180deg,#0c1420,#080c13);
border:1px solid var(--line);
box-shadow:0 20px 60px rgba(0,0,0,.55), inset 0 0 60px rgba(61,242,255,.04);
padding:10px;
touch-action:none;
}
canvas{ display:block; width:100%; height:auto; border-radius:10px; touch-action:none; }
#foot{
display:flex;
gap:10px;
align-items:center;
justify-content:center;
width:100%;
}
button{
font-family:inherit;
font-weight:700;
letter-spacing:.16em;
text-transform:uppercase;
font-size:11px;
color:var(--ink);
background:linear-gradient(180deg,#132033,#0c1524);
border:1px solid var(--line);
border-radius:10px;
padding:9px 14px;
cursor:pointer;
transition:transform .08s ease, border-color .15s ease, box-shadow .15s ease;
}
button:hover{ border-color:var(--accent); box-shadow:0 0 14px rgba(61,242,255,.25); }
button:active{ transform:translateY(1px); }
#msg{
min-height:16px;
font-size:11px;
letter-spacing:.16em;
text-transform:uppercase;
color:var(--muted);
text-align:center;
}
</style>
</head>
<body>
<div id="wrap">
<h1 id="title">Neon Gems</h1>
<div id="hud">
<div class="stat"><div class="lbl">Score</div><div class="val" id="scoreEl">0</div></div>
<div class="stat"><div class="lbl">Moves</div><div class="val" id="movesEl">30</div></div>
<div class="stat"><div class="lbl">Best</div><div class="val" id="bestEl">0</div></div>
</div>
<div id="boardWrap">
<canvas id="game"></canvas>
</div>
<div id="msg">Swap gems to match 3 or more</div>
<div id="foot">
<button id="soundBtn" type="button">Sound: On</button>
<button id="newBtn" type="button">New Game</button>
</div>
</div>
<script>
(function(){
"use strict";
function boot(){
var canvas = document.getElementById('game');
if(!canvas) return;
var ctx = canvas.getContext('2d');
if(!ctx) return;
var scoreEl = document.getElementById('scoreEl');
var movesEl = document.getElementById('movesEl');
var bestEl = document.getElementById('bestEl');
var msgEl = document.getElementById('msg');
var soundBtn= document.getElementById('soundBtn');
var newBtn = document.getElementById('newBtn');
var boardWrap = document.getElementById('boardWrap');
var N = 8; // grid size
var START_MOVES = 30;
var BEST_KEY = "abrams-gems-best";
// Gem palette: distinct saturated neon hues
var GEMS = [
{ core:"#ff3d6e", glow:"#ff3d6e" }, // pink/red
{ core:"#ffb43d", glow:"#ffb43d" }, // amber
{ core:"#ffe93d", glow:"#ffe93d" }, // yellow
{ core:"#4dff88", glow:"#4dff88" }, // green
{ core:"#3df2ff", glow:"#3df2ff" }, // cyan
{ core:"#7d8bff", glow:"#7d8bff" }, // indigo
{ core:"#c96dff", glow:"#c96dff" } // violet
];
var NC = GEMS.length;
// DPR-aware sizing
var cell = 48; // logical px per cell (set in resize)
var dpr = 1;
// Board state: grid[r][c] = color index (0..NC-1) or -1 empty
var grid = [];
var score = 0, moves = START_MOVES, best = 0;
var soundOn = true;
// Per-cell render offset (for fall/swap animation)
// offset[r][c] = {dx, dy} in logical pixels, animated to 0
var offX = [], offY = [];
// pop animation: pop[r][c] = scale factor while clearing (1 -> 0)
var popScale = [];
var spawnScale = []; // 0 -> 1 for newly spawned/fallen gems
var sel = null; // {r,c} currently selected
var animating = false; // block input during resolve animations
var gameOver = false;
// ---------- storage ----------
function loadBest(){
try{
var v = localStorage.getItem(BEST_KEY);
best = v ? (parseInt(v,10)||0) : 0;
}catch(e){ best = 0; }
}
function saveBest(){
try{ localStorage.setItem(BEST_KEY, String(best)); }catch(e){}
}
// ---------- audio ----------
var audioCtx = null;
function ensureAudio(){
if(audioCtx) return;
try{
var AC = window.AudioContext || window.webkitAudioContext;
if(AC) audioCtx = new AC();
}catch(e){ audioCtx = null; }
}
function blip(freq, dur, type, vol){
if(!soundOn || !audioCtx) return;
try{
var t = audioCtx.currentTime;
var o = audioCtx.createOscillator();
var g = audioCtx.createGain();
o.type = type || "sine";
o.frequency.setValueAtTime(freq, t);
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(vol||0.14, t+0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t+(dur||0.12));
o.connect(g); g.connect(audioCtx.destination);
o.start(t);
o.stop(t+(dur||0.12)+0.02);
}catch(e){}
}
function sfxSwap(){ blip(420,0.09,"triangle",0.10); }
function sfxBad(){ blip(160,0.14,"sawtooth",0.08); }
function sfxMatch(chain){ blip(520 + chain*90, 0.14, "square", 0.12); }
function sfxDrop(){ blip(300,0.06,"sine",0.06); }
// ---------- grid helpers ----------
function rndColor(){ return Math.floor(Math.random()*NC); }
function inBounds(r,c){ return r>=0 && r<N && c>=0 && c<N; }
// Build initial board with NO pre-existing match-3
function fillInitial(){
grid = [];
for(var r=0;r<N;r++){
grid[r] = [];
for(var c=0;c<N;c++){
var col;
var guard = 0;
do{
col = rndColor();
guard++;
// reject if it completes a horizontal or vertical run of 3
}while(guard<50 && (
(c>=2 && grid[r][c-1]===col && grid[r][c-2]===col) ||
(r>=2 && grid[r-1][c]===col && grid[r-2][c]===col)
));
grid[r][c] = col;
}
}
// guarantee at least one valid move
if(!hasValidMove()){ shuffleBoard(); }
}
// find all matched cells (>=3 in row/col). returns 2D boolean.
function findMatches(){
var mark = [];
for(var r=0;r<N;r++){ mark[r]=[]; for(var c=0;c<N;c++) mark[r][c]=false; }
var r,c;
// horizontal
for(r=0;r<N;r++){
var run=1;
for(c=1;c<N;c++){
if(grid[r][c]!==-1 && grid[r][c]===grid[r][c-1]) run++;
else {
if(run>=3){ for(var k=0;k<run;k++) mark[r][c-1-k]=true; }
run=1;
}
}
if(run>=3){ for(var k2=0;k2<run;k2++) mark[r][N-1-k2]=true; }
}
// vertical
for(c=0;c<N;c++){
var run2=1;
for(r=1;r<N;r++){
if(grid[r][c]!==-1 && grid[r][c]===grid[r-1][c]) run2++;
else {
if(run2>=3){ for(var k3=0;k3<run2;k3++) mark[r-1-k3][c]=true; }
run2=1;
}
}
if(run2>=3){ for(var k4=0;k4<run2;k4++) mark[N-1-k4][c]=true; }
}
return mark;
}
function countMarked(mark){
var n=0;
for(var r=0;r<N;r++) for(var c=0;c<N;c++) if(mark[r][c]) n++;
return n;
}
// would swapping (r1,c1)<->(r2,c2) produce a match?
function swapProducesMatch(r1,c1,r2,c2){
var t = grid[r1][c1]; grid[r1][c1]=grid[r2][c2]; grid[r2][c2]=t;
var m = findMatches();
var ok = countMarked(m) > 0;
var t2 = grid[r1][c1]; grid[r1][c1]=grid[r2][c2]; grid[r2][c2]=t2;
return ok;
}
function hasValidMove(){
for(var r=0;r<N;r++){
for(var c=0;c<N;c++){
if(c<N-1 && swapProducesMatch(r,c,r,c+1)) return true;
if(r<N-1 && swapProducesMatch(r,c,r+1,c)) return true;
}
}
return false;
}
function shuffleBoard(){
var guard=0;
do{
// Fisher-Yates over flat colors
var flat=[];
for(var r=0;r<N;r++) for(var c=0;c<N;c++) flat.push(grid[r][c]);
for(var i=flat.length-1;i>0;i--){
var j=Math.floor(Math.random()*(i+1));
var tmp=flat[i]; flat[i]=flat[j]; flat[j]=tmp;
}
var idx=0;
for(var r2=0;r2<N;r2++) for(var c2=0;c2<N;c2++) grid[r2][c2]=flat[idx++];
guard++;
}while(guard<80 && (countMarked(findMatches())>0 || !hasValidMove()));
}
// ---------- animation arrays ----------
function resetAnimArrays(){
offX=[]; offY=[]; popScale=[]; spawnScale=[];
for(var r=0;r<N;r++){
offX[r]=[]; offY[r]=[]; popScale[r]=[]; spawnScale[r]=[];
for(var c=0;c<N;c++){ offX[r][c]=0; offY[r][c]=0; popScale[r][c]=1; spawnScale[r][c]=1; }
}
}
// ---------- new game ----------
function newGame(){
score=0; moves=START_MOVES; gameOver=false; sel=null; animating=false;
fillInitial();
resetAnimArrays();
updateHUD();
setMsg("Swap gems to match 3 or more");
draw();
}
// ---------- HUD ----------
function updateHUD(){
scoreEl.textContent = String(score);
movesEl.textContent = String(moves);
bestEl.textContent = String(best);
}
function setMsg(t){ msgEl.textContent = t; }
// ---------- sizing ----------
function resize(){
// available width from wrapper (minus padding)
var wrapW = boardWrap.clientWidth - 20; // 10px padding each side
if(wrapW < 120) wrapW = 120;
// keep board square, cap so it fits typical iframe heights too
var maxByH = (window.innerHeight || 600) - 230;
var size = Math.min(wrapW, maxByH > 200 ? maxByH : wrapW);
size = Math.max(200, Math.min(size, 520));
cell = size / N;
dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 3));
canvas.width = Math.round(size * dpr);
canvas.height = Math.round(size * dpr);
canvas.style.width = size + "px";
canvas.style.height = size + "px";
draw();
}
// ---------- drawing ----------
function drawGem(cx, cy, rad, gi, alpha, scale){
if(gi<0) return;
var g = GEMS[gi];
var R = rad*scale;
if(R<=0.5) return;
ctx.save();
ctx.globalAlpha = alpha;
// outer glow
ctx.shadowColor = g.glow;
ctx.shadowBlur = R*0.9;
// body gradient
var grad = ctx.createRadialGradient(cx - R*0.3, cy - R*0.35, R*0.15, cx, cy, R);
grad.addColorStop(0, "#ffffff");
grad.addColorStop(0.18, g.core);
grad.addColorStop(1, shade(g.core, -0.45));
ctx.fillStyle = grad;
roundRectPath(cx-R, cy-R, R*2, R*2, R*0.42);
ctx.fill();
// inner rim
ctx.shadowBlur = 0;
ctx.lineWidth = Math.max(1, R*0.10);
ctx.strokeStyle = shade(g.core, 0.35);
ctx.globalAlpha = alpha*0.8;
roundRectPath(cx-R*0.92, cy-R*0.92, R*1.84, R*1.84, R*0.36);
ctx.stroke();
// sparkle highlight
ctx.globalAlpha = alpha*0.85;
ctx.fillStyle = "rgba(255,255,255,0.85)";
ctx.beginPath();
ctx.ellipse(cx - R*0.32, cy - R*0.38, R*0.22, R*0.13, -0.6, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
function roundRectPath(x,y,w,h,r){
if(r>w/2) r=w/2; if(r>h/2) r=h/2;
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r);
ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r);
ctx.arcTo(x,y,x+w,y,r);
ctx.closePath();
}
// shade a hex color by amt (-1..1)
function shade(hex, amt){
var h = hex.replace('#','');
var r = parseInt(h.substring(0,2),16);
var g = parseInt(h.substring(2,4),16);
var b = parseInt(h.substring(4,6),16);
if(amt<0){
r = Math.round(r*(1+amt)); g = Math.round(g*(1+amt)); b = Math.round(b*(1+amt));
} else {
r = Math.round(r+(255-r)*amt); g = Math.round(g+(255-g)*amt); b = Math.round(b+(255-b)*amt);
}
r=Math.max(0,Math.min(255,r)); g=Math.max(0,Math.min(255,g)); b=Math.max(0,Math.min(255,b));
return "rgb("+r+","+g+","+b+")";
}
function draw(){
var W = canvas.width, H = canvas.height;
ctx.setTransform(dpr,0,0,dpr,0,0);
var size = W/dpr;
// bg
ctx.clearRect(0,0,size,size);
var bg = ctx.createLinearGradient(0,0,0,size);
bg.addColorStop(0,"#0b1220");
bg.addColorStop(1,"#070b11");
ctx.fillStyle = bg;
ctx.fillRect(0,0,size,size);
// grid cells backdrop
for(var r=0;r<N;r++){
for(var c=0;c<N;c++){
var x = c*cell, y = r*cell;
ctx.fillStyle = ((r+c)%2===0) ? "rgba(255,255,255,0.018)" : "rgba(255,255,255,0.006)";
ctx.fillRect(x,y,cell,cell);
}
}
// selection highlight
if(sel){
var sx = sel.c*cell, sy = sel.r*cell;
ctx.save();
ctx.strokeStyle = "#3df2ff";
ctx.shadowColor = "#3df2ff";
ctx.shadowBlur = 14;
ctx.lineWidth = 3;
roundRectPath(sx+3, sy+3, cell-6, cell-6, cell*0.2);
ctx.stroke();
ctx.restore();
}
// gems
var rad = cell*0.40;
for(var r2=0;r2<N;r2++){
for(var c2=0;c2<N;c2++){
var gi = grid[r2][c2];
if(gi<0) continue;
var cx = c2*cell + cell/2 + offX[r2][c2];
var cy = r2*cell + cell/2 + offY[r2][c2];
var ps = popScale[r2][c2];
var ss = spawnScale[r2][c2];
var sc = ps * ss;
if(sc<=0.02) continue;
drawGem(cx, cy, rad, gi, Math.min(1, sc<1?sc:1)*(ps<1?ps:1), sc);
}
}
}
// ---------- animation loop ----------
var rafId = null;
function animateOffsets(cb){
animating = true;
var speed = 0.22; // easing factor per frame
function step(){
var moving = false;
for(var r=0;r<N;r++){
for(var c=0;c<N;c++){
// ease offsets to 0
if(Math.abs(offX[r][c])>0.5){ offX[r][c]*= (1-speed); moving=true; } else offX[r][c]=0;
if(Math.abs(offY[r][c])>0.5){ offY[r][c]*= (1-speed); moving=true; } else offY[r][c]=0;
// spawn scale ease to 1
if(spawnScale[r][c] < 0.999){ spawnScale[r][c] += (1-spawnScale[r][c])*0.28; if(spawnScale[r][c]>0.995) spawnScale[r][c]=1; moving=true; }
}
}
draw();
if(moving){ rafId = requestAnimationFrame(step); }
else { animating=false; cb && cb(); }
}
rafId = requestAnimationFrame(step);
}
function animatePops(marks, cb){
animating = true;
function step(){
var moving=false;
for(var r=0;r<N;r++){
for(var c=0;c<N;c++){
if(marks[r][c] && popScale[r][c]>0.02){
popScale[r][c] -= 0.14;
if(popScale[r][c]<0) popScale[r][c]=0;
moving=true;
}
}
}
draw();
if(moving) rafId=requestAnimationFrame(step);
else { animating=false; cb && cb(); }
}
rafId=requestAnimationFrame(step);
}
// ---------- resolve cascades ----------
function resolveBoard(chain){
var marks = findMatches();
var n = countMarked(marks);
if(n===0){
// done cascading
animating=false;
checkEndAndShuffle();
draw();
return;
}
sfxMatch(chain||0);
// score: base 10 per gem, cascade multiplier
var mult = 1 + (chain||0)*0.5;
var gained = Math.round(n * 10 * mult);
score += gained;
if(score>best){ best=score; saveBest(); }
updateHUD();
if((chain||0)>0) setMsg("Combo x"+(chain+1)+" +"+gained);
else setMsg("+"+gained);
// animate pop, then collapse + refill
animatePops(marks, function(){
// clear matched
for(var r=0;r<N;r++) for(var c=0;c<N;c++){ if(marks[r][c]){ grid[r][c]=-1; popScale[r][c]=1; } }
collapseAndRefill();
// animate the falls then resolve again (cascade)
animateOffsets(function(){
resolveBoard((chain||0)+1);
});
});
}
// gravity: pull down, spawn new at top; set offY so they animate falling
function collapseAndRefill(){
for(var c=0;c<N;c++){
// gather existing from bottom up
var writeR = N-1;
for(var r=N-1;r>=0;r--){
if(grid[r][c]!==-1){
if(writeR!==r){
grid[writeR][c]=grid[r][c];
grid[r][c]=-1;
// offset so it visually starts where it was, then eases to 0
offY[writeR][c] = (r-writeR)*cell;
spawnScale[writeR][c]=1;
}
writeR--;
}
}
// fill remaining top with new gems, dropping in from above
var spawnIdx = 0;
for(var rr=writeR; rr>=0; rr--){
grid[rr][c]=rndColor();
offY[rr][c] = -(cell*(spawnIdx+1)) - cell; // start above the board
spawnScale[rr][c]=0.35;
spawnIdx++;
}
}
sfxDrop();
}
function checkEndAndShuffle(){
if(!hasValidMove()){
setMsg("No moves — reshuffling…");
shuffleBoard();
resetAnimArrays();
draw();
// after shuffle there should be no immediate matches (guarded)
}
if(moves<=0){
gameOver=true;
setMsg("Out of moves — Score "+score+". Tap New Game.");
}
}
// ---------- input ----------
function cellFromEvent(clientX, clientY){
var rect = canvas.getBoundingClientRect();
var x = clientX - rect.left;
var y = clientY - rect.top;
// canvas CSS size maps to N*cell logical (cell already logical)
var px = x * (canvas.width/dpr) / rect.width;
var py = y * (canvas.height/dpr) / rect.height;
var c = Math.floor(px / cell);
var r = Math.floor(py / cell);
if(!inBounds(r,c)) return null;
return {r:r,c:c};
}
function tryMove(a, b){
// must be adjacent
var dr = Math.abs(a.r-b.r), dc = Math.abs(a.c-b.c);
if(dr+dc!==1) return false;
if(gameOver) return false;
sfxSwap();
// do swap
var t=grid[a.r][a.c]; grid[a.r][a.c]=grid[b.r][b.c]; grid[b.r][b.c]=t;
// set offsets for swap animation (each moves toward its new home)
offX[a.r][a.c] = (b.c-a.c)*cell; offY[a.r][a.c] = (b.r-a.r)*cell;
offX[b.r][b.c] = (a.c-b.c)*cell; offY[b.r][b.c] = (a.r-b.r)*cell;
var m = findMatches();
if(countMarked(m)===0){
// invalid: animate swap in, then swap back
sfxBad();
animateOffsets(function(){
var t2=grid[a.r][a.c]; grid[a.r][a.c]=grid[b.r][b.c]; grid[b.r][b.c]=t2;
offX[a.r][a.c]=(b.c-a.c)*cell; offY[a.r][a.c]=(b.r-a.r)*cell;
offX[b.r][b.c]=(a.c-b.c)*cell; offY[b.r][b.c]=(a.r-b.r)*cell;
animateOffsets(function(){ setMsg("No match there"); });
});
return true; // was a valid adjacent attempt (but no match)
}
// valid move
moves--;
updateHUD();
animateOffsets(function(){
resolveBoard(0);
});
return true;
}
var pointerDown = null; // {r,c,x,y}
function onDown(clientX, clientY){
ensureAudio();
if(animating || gameOver) return;
var cellPos = cellFromEvent(clientX, clientY);
if(!cellPos) return;
pointerDown = {r:cellPos.r, c:cellPos.c, x:clientX, y:clientY};
if(sel){
// second click: attempt swap or reselect
if(sel.r===cellPos.r && sel.c===cellPos.c){
sel=null; draw(); return;
}
var dr=Math.abs(sel.r-cellPos.r), dc=Math.abs(sel.c-cellPos.c);
if(dr+dc===1){
var a={r:sel.r,c:sel.c}; sel=null;
tryMove(a, cellPos);
draw();
} else {
sel={r:cellPos.r,c:cellPos.c}; draw();
}
} else {
sel={r:cellPos.r,c:cellPos.c}; draw();
}
}
function onUp(clientX, clientY){
if(!pointerDown) return;
if(animating || gameOver){ pointerDown=null; return; }
var dx = clientX - pointerDown.x;
var dy = clientY - pointerDown.y;
var absx=Math.abs(dx), absy=Math.abs(dy);
var threshold = cell*0.3;
if(Math.max(absx,absy) > threshold){
// treat as swipe from the down cell
var a={r:pointerDown.r, c:pointerDown.c};
var b;
if(absx>absy) b={r:a.r, c:a.c + (dx>0?1:-1)};
else b={r:a.r + (dy>0?1:-1), c:a.c};
if(inBounds(b.r,b.c)){
sel=null;
tryMove(a,b);
draw();
}
}
pointerDown=null;
}
// mouse
canvas.addEventListener('mousedown', function(e){ e.preventDefault(); onDown(e.clientX,e.clientY); });
window.addEventListener('mouseup', function(e){ onUp(e.clientX,e.clientY); });
// touch
canvas.addEventListener('touchstart', function(e){
if(e.touches.length){ e.preventDefault(); var t=e.touches[0]; onDown(t.clientX,t.clientY); }
}, {passive:false});
canvas.addEventListener('touchmove', function(e){ e.preventDefault(); }, {passive:false});
canvas.addEventListener('touchend', function(e){
e.preventDefault();
var t = (e.changedTouches && e.changedTouches.length) ? e.changedTouches[0] : null;
if(t) onUp(t.clientX,t.clientY);
}, {passive:false});
// buttons
soundBtn.addEventListener('click', function(){
soundOn = !soundOn;
soundBtn.textContent = "Sound: " + (soundOn?"On":"Off");
if(soundOn){ ensureAudio(); blip(660,0.08,"sine",0.1); }
});
newBtn.addEventListener('click', function(){ ensureAudio(); newGame(); });
// resize handling
var resizeTimer=null;
window.addEventListener('resize', function(){
clearTimeout(resizeTimer);
resizeTimer=setTimeout(resize, 60);
});
// ---------- init ----------
loadBest();
newGame();
resize();
}
if(document.readyState === "loading"){
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
})();
</script>
</body>
</html>