Files
arrow/index.html
T
truetype 67e6514152 Arrow Puzzle: gioco web a frecce/serpenti
- Tabellone grande (fino a 600x600) con frecce-snake orizzontali/verticali
- Punta allineata all'ultimo tratto del corpo, nessun incrocio tra linee
- Livello sempre risolvibile (clearPath garantito in generazione)
- Animazione di uscita step-by-step (crawl) veloce
- Pan, zoom con rotella/pinch/pulsanti, minimappa cliccabile
- HUD italiano: vite, hint, mosse, undo, livelli
- Server Python su porta 1234 (HTTP + LAN)
2026-08-01 17:54:49 +02:00

597 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Arrow Puzzle</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
background:#1a1a1a;color:#ccc;overflow:hidden;
height:100dvh;width:100vw;
-webkit-tap-highlight-color:transparent;
user-select:none;-webkit-user-select:none;
touch-action:none;
}
#board{display:block;position:absolute;inset:0;width:100%;height:100%}
.hud{
position:fixed;top:16px;left:0;right:0;display:flex;
justify-content:space-between;align-items:center;
padding:0 12px;z-index:10;pointer-events:none;gap:6px;
}
.hud>*{pointer-events:auto}
.hud-item{
font-weight:700;font-size:13px;
background:rgba(10,10,20,0.85);color:#ddd;
padding:5px 12px;border-radius:14px;
backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.08);
white-space:nowrap;
}
.hearts{display:flex;gap:1px}
.heart{font-size:15px;transition:all 0.3s}
.heart.lost{opacity:0.15;transform:scale(0.7)}
.zoom-label{color:#888;font-size:11px}
.overlay-back{background:rgba(255,200,50,0.15)!important;border-color:rgba(255,200,50,0.4)!important;color:#ffc832!important}
.controls{
position:fixed;bottom:16px;left:50%;transform:translateX(-50%);
display:flex;gap:8px;z-index:10;flex-wrap:wrap;justify-content:center;
}
.btn{
padding:9px 16px;border-radius:18px;border:1px solid rgba(255,255,255,0.12);
font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;
background:rgba(10,10,20,0.85);color:#bbb;
backdrop-filter:blur(8px);
transition:all 0.15s;display:flex;align-items:center;gap:4px;
}
.btn:active{transform:scale(0.94);background:rgba(255,255,255,0.1)}
.btn-hint{border-color:rgba(255,200,50,0.35);color:#ffc832}
.hint-dot{width:5px;height:5px;border-radius:50%;background:currentColor;opacity:0.5;display:inline-block;margin:0 1px}
.hint-dot.used{opacity:0.08}
.overlay{
position:fixed;inset:0;background:rgba(0,0,0,0.7);
display:flex;align-items:center;justify-content:center;
z-index:100;animation:ovIn 0.3s ease;
}
@keyframes ovIn{from{opacity:0}to{opacity:1}}
.overlay-card{
background:#16162a;border-radius:20px;padding:28px 24px;text-align:center;
max-width:300px;width:90%;box-shadow:0 16px 48px rgba(0,0,0,0.6);
animation:pop 0.35s cubic-bezier(0.175,0.885,0.32,1.275);
border:1px solid rgba(255,255,255,0.06);
}
@keyframes pop{from{transform:scale(0.8);opacity:0}to{transform:scale(1);opacity:1}}
.overlay-emoji{font-size:48px;margin-bottom:8px}
.overlay-title{font-size:20px;font-weight:800;color:#eee;margin-bottom:4px}
.overlay-subtitle{color:#888;font-size:13px;margin-bottom:16px}
.btn-big{padding:12px 28px;font-size:15px;width:100%;justify-content:center;border-radius:22px}
.btn-win{background:#1b4332;color:#b7e4c7;border-color:rgba(64,145,108,0.3)}
.btn-retry{background:#590d22;color:#ffb3c1;border-color:rgba(255,77,109,0.3)}
.toast{
position:fixed;bottom:110px;left:50%;transform:translateX(-50%);
background:rgba(30,30,50,0.9);color:#eee;backdrop-filter:blur(10px);
padding:8px 20px;border-radius:20px;font-size:13px;font-weight:600;
animation:toastIn 0.3s ease,toastOut 0.3s ease 1.5s forwards;
pointer-events:none;z-index:200;border:1px solid rgba(255,255,255,0.1);
}
@keyframes toastIn{from{opacity:0;transform:translateX(-50%) translateY(10px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
@keyframes toastOut{from{opacity:1}to{opacity:0}}
.minimap{
position:fixed;bottom:100px;right:12px;width:100px;height:100px;
background:rgba(10,10,20,0.8);border:1px solid rgba(255,255,255,0.1);
border-radius:8px;z-index:10;overflow:hidden;cursor:pointer;
}
.minimap canvas{width:100%;height:100%}
.zoom-ctrl{
position:fixed;right:16px;top:50%;transform:translateY(-50%);
display:flex;flex-direction:column;gap:6px;z-index:10;
}
.zoom-ctrl button{
width:42px;height:42px;border-radius:12px;border:1px solid rgba(255,255,255,0.12);
background:rgba(10,10,20,0.85);color:#ddd;font-size:22px;font-weight:700;cursor:pointer;
backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;
transition:all 0.15s;
}
.zoom-ctrl button:active{transform:scale(0.92);background:rgba(255,255,255,0.14)}
.zoom-ctrl button:last-child{font-size:16px}
@media(max-width:400px){.btn{padding:7px 12px;font-size:11px}.hud-item{padding:4px 10px;font-size:12px}.minimap{width:70px;height:70px;bottom:90px;right:6px}.zoom-ctrl{right:6px}.zoom-ctrl button{width:36px;height:36px;font-size:18px}}
</style>
</head>
<body>
<canvas id="board"></canvas>
<div class="minimap" id="minimapWrap"><canvas id="minimap"></canvas></div>
<div class="hud">
<span class="hud-item" id="levelBadge">Livello 1</span>
<span class="hud-item" id="arrowCount">Frecce: 0</span>
<span class="hud-item zoom-label" id="zoomLabel">100%</span>
<div class="hearts" id="heartsContainer"></div>
</div>
<div class="controls">
<button class="btn" id="btnUndo">↩ Annulla</button>
<button class="btn" id="btnFit">⊞ Vista completa</button>
<button class="btn btn-hint" id="btnHint">💡 <span id="hintDots"></span></button>
<button class="btn" id="btnReset">🔄 Riprova</button>
</div>
<div class="zoom-ctrl">
<button id="btnZoomIn" title="Zoom avanti">+</button>
<button id="btnZoomOut" title="Zoom indietro">−</button>
<button id="btnZoomFit" title="Vista completa">⤢</button>
</div>
<script>
(function(){
const $=id=>document.getElementById(id);
const canvas=$('board'), ctx=canvas.getContext('2d');
const mini=$('minimap'), mctx=mini.getContext('2d');
const DIR_VEC={U:[0,-1],R:[1,0],D:[0,1],L:[-1,0]};
const OPP={U:'D',R:'L',D:'U',L:'R'};
// Colors per direction - glowing neon on dark bg
const COLORS=[
{main:'#ffffff',glow:'rgba(255,255,255,0.45)'}, // white
{main:'#ff3333',glow:'rgba(255,51,51,0.45)'}, // red
{main:'#33cc33',glow:'rgba(51,204,51,0.45)'}, // green
{main:'#3388ff',glow:'rgba(51,136,255,0.45)'}, // blue
{main:'#ffcc00',glow:'rgba(255,204,0,0.45)'}, // yellow
{main:'#cc33ff',glow:'rgba(204,51,255,0.45)'}, // purple
{main:'#ff6600',glow:'rgba(255,102,0,0.45)'}, // orange
{main:'#00cccc',glow:'rgba(0,204,204,0.45)'}, // cyan
];
let gridW, gridH, arrows, lives=3, maxLives=3, level=1, hints=3, moves=0, solved=false;
let history=[], selected=null, combo=0;
let camX=0, camY=0, zoom=1, tZoom=1;
let cellPx, minZoom, maxZoom;
let animList=[], animId=null;
// ─── Arrow helpers ───────────────────────────────────
function headCell(a){return a.cells[a.cells.length-1]}
function clearPath(a) {
const [hc,hr]=headCell(a); const [dc,dr]=DIR_VEC[a.dir];
let c=hc+dc, r=hr+dr;
while(c>=0&&c<gridW&&r>=0&&r<gridH){
for(const b of arrows){if(b===a)continue;for(const [bc,br] of b.cells)if(bc===c&&br===r)return false}
c+=dc;r+=dr
}
return true
}
// ─── Level gen (snake arrows with turns) ────────────
function gen(lvl) {
gridW=Math.min(50+Math.floor(lvl*3), 600);
gridH=gridW;
const tgt=Math.min(15+lvl*5, 400);
const dirs=['U','R','D','L'];
arrows=[]; const occ=new Set(); const k=(c,r)=>c+','+r;
let att=0;
while(arrows.length<tgt && att<tgt*40){
att++;
const hc=2+Math.floor(Math.random()*(gridW-4));
const hr=2+Math.floor(Math.random()*(gridH-4));
if(occ.has(k(hc,hr)))continue;
const shf=[...dirs].sort(()=>Math.random()-0.5);
for(const d of shf){
const bl=4+Math.floor(Math.random()*Math.min(30,10+Math.floor(lvl/2)));
const cells=[]; let c=hc,r=hr,ok=true;
let backDir=OPP[d];
let[bdc,bdr]=DIR_VEC[backDir];
for(let i=0;i<bl+1;i++){
if(c<0||c>=gridW||r<0||r>=gridH||occ.has(k(c,r))){ok=false;break}
cells.push([c,r]);
if(i<bl){
c+=bdc;r+=bdr;
// Randomly turn 90° (like Snake game)
if(Math.random()<0.25&&i>1&&i<bl-2){
const perp=backDir==='U'||backDir==='D'?['L','R']:['U','D'];
backDir=perp[Math.floor(Math.random()*2)];
[bdc,bdr]=DIR_VEC[backDir];
}
}
}
if(!ok||cells.length<4)continue;
const ci=Math.floor(Math.random()*COLORS.length);
// Arrowhead direction = direction of the LAST body segment (natural continuation of the line)
const pc=cells[cells.length-2],lc=cells[cells.length-1];
const ddx=lc[0]-pc[0],ddy=lc[1]-pc[1];
const dir=ddx===1?'R':ddx===-1?'L':ddy===1?'D':'U';
const a={dir,cells,colorIdx:ci};
arrows.push(a); for(const[cc,rr]of cells)occ.add(k(cc,rr));
// Solvability guarantee: the NEW arrow must have a clear exit path now,
// so the level is always solvable (remove arrows in reverse insertion order).
if(!clearPath(a)){arrows.pop();for(const[cc,rr]of cells)occ.delete(k(cc,rr));continue}
break
}
}
if(arrows.length<3)return gen(lvl);
moves=0;combo=0;solved=false;history=[];selected=null;animList=[];
if(animId){cancelAnimationFrame(animId);animId=null}
updateUI();fit();draw()
}
// ─── Render ──────────────────────────────────────────
function resize() {
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
cellPx=Math.max(1,Math.min(canvas.width*0.9/gridW,canvas.height*0.9/gridH));
minZoom=0.03;
maxZoom=16;
if(zoom<minZoom)zoom=tZoom=minZoom;
if(zoom>maxZoom)zoom=tZoom=maxZoom;
mini.width=100;mini.height=100
}
function world(c,r){return[c*cellPx*zoom+camX,r*cellPx*zoom+camY]}
function draw() {
resize();
const w=canvas.width,h=canvas.height;
ctx.clearRect(0,0,w,h);
// BG
ctx.fillStyle='#1a1a1a';ctx.fillRect(0,0,w,h);
// Board area - dark grey/black
const bx=camX,by=camY,bw=gridW*cellPx*zoom,bh=gridH*cellPx*zoom;
ctx.fillStyle='#1a1a1a';ctx.fillRect(bx,by,bw,bh);
// Grid (only when zoomed in enough)
const cs=cellPx*zoom;
if(cs>8){
ctx.strokeStyle='rgba(255,255,255,0.02)';ctx.lineWidth=0.5;
// Only draw visible grid lines
const c0=Math.max(0,Math.floor(-camX/cs)-1);
const c1=Math.min(gridW,Math.ceil((w-camX)/cs)+1);
const r0=Math.max(0,Math.floor(-camY/cs)-1);
const r1=Math.min(gridH,Math.ceil((h-camY)/cs)+1);
for(let c=c0;c<=c1;c++){const x=bx+c*cs;ctx.beginPath();ctx.moveTo(x,by);ctx.lineTo(x,by+bh);ctx.stroke()}
for(let r=r0;r<=r1;r++){const y=by+r*cs;ctx.beginPath();ctx.moveTo(bx,y);ctx.lineTo(bx+bw,y);ctx.stroke()}
}
// Arrows
for(const a of arrows){
if(animList.some(x=>x.arrow===a))continue;
const isBlocked=a===selected&&!clearPath(a);
drawArrow(a,0,0,1,isBlocked);
}
// Crawling snakes (step-by-step)
for(const anim of animList){
const cs=cellPx*zoom;
const pts=[];
const n=anim.n;
for(let i=0;i<n;i++){
const fi=anim.t+i;
const idx=Math.min(anim.path.length-1,Math.floor(fi));
const nx=Math.min(anim.path.length-1,idx+1);
const frac=fi-idx;
const c0=anim.path[idx],c1=anim.path[nx];
const px=c0[0]+(c1[0]-c0[0])*frac,py=c0[1]+(c1[1]-c0[1])*frac;
const[p,q]=world(px,py);
pts.push([p+cs/2,q+cs/2]);
}
drawSnakePts(pts,anim.dir,anim.colorIdx,1,false);
}
// Minimap
drawMinimap();
}
function drawMinimap() {
const mw=mini.width,mh=mini.height;
mctx.clearRect(0,0,mw,mh);
mctx.fillStyle='rgba(10,10,25,0.9)';mctx.fillRect(0,0,mw,mh);
const sx=mw/gridW,sy=mh/gridH;
for(const a of arrows){
const c=COLORS[a.colorIdx];
const pts=a.cells.map(([c,r])=>[c*sx,r*sy]);
mctx.strokeStyle=c.main;mctx.lineWidth=0.5;
mctx.beginPath();mctx.moveTo(pts[0][0],pts[0][1]);
for(let i=1;i<pts.length;i++)mctx.lineTo(pts[i][0],pts[i][1]);
mctx.stroke();
}
// Viewport rect
const vx=(-camX/(cellPx*zoom))/gridW*mw;
const vy=(-camY/(cellPx*zoom))/gridH*mh;
const vw=canvas.width/(cellPx*zoom*gridW)*mw;
const vh=canvas.height/(cellPx*zoom*gridH)*mh;
mctx.strokeStyle='rgba(255,255,255,0.5)';mctx.lineWidth=1;
mctx.strokeRect(vx,vy,vw,vh);
}
function drawArrow(a,ox,oy,alpha,blocked){
const cs=cellPx*zoom;
const pts=a.cells.map(([c,r])=>{const[p,q]=world(c,r);return[p+cs/2+ox,q+cs/2+oy]});
drawSnakePts(pts,a.dir,a.colorIdx,alpha,blocked);
}
function drawSnakePts(pts,dir,colorIdx,alpha,blocked){
const c=COLORS[colorIdx];const cs=cellPx*zoom;
const head=pts[pts.length-1];
// Tail glow (long range) - subtle
ctx.save();ctx.globalAlpha=alpha*0.15;
ctx.strokeStyle=c.glow;ctx.lineWidth=cs*0.45;ctx.lineCap='round';ctx.lineJoin='round';
ctx.shadowColor=c.main;ctx.shadowBlur=cs*1.5;
ctx.beginPath();ctx.moveTo(pts[0][0],pts[0][1]);
for(let i=1;i<pts.length;i++)ctx.lineTo(pts[i][0],pts[i][1]);
ctx.stroke();ctx.restore();
// Body line - thin and clean
ctx.save();ctx.globalAlpha=alpha;
ctx.strokeStyle=blocked?'#ff4444':c.main;
ctx.lineWidth=cs*(blocked?0.35:0.14);ctx.lineCap='round';ctx.lineJoin='round';
ctx.shadowColor=blocked?'#ff4444':c.main;
ctx.shadowBlur=blocked?15:4;
ctx.beginPath();ctx.moveTo(pts[0][0],pts[0][1]);
for(let i=1;i<pts.length;i++)ctx.lineTo(pts[i][0],pts[i][1]);
ctx.stroke();ctx.restore();
// Head triangle - large and clearly visible
const[dc,dr]=DIR_VEC[dir];
const headSz=cs*0.4;
const tipX=head[0]+dc*headSz,tipY=head[1]+dr*headSz;
const wing=headSz*0.7;
// Bright white core
ctx.save();ctx.globalAlpha=alpha;
ctx.fillStyle='#ffffff';
ctx.shadowColor='#ffffff';
ctx.shadowBlur=headSz*0.8;
ctx.beginPath();
switch(dir){
case'U':ctx.moveTo(tipX,tipY);ctx.lineTo(head[0]-wing,head[1]+headSz*0.3);ctx.lineTo(head[0]+wing,head[1]+headSz*0.3);break;
case'D':ctx.moveTo(tipX,tipY);ctx.lineTo(head[0]-wing,head[1]-headSz*0.3);ctx.lineTo(head[0]+wing,head[1]-headSz*0.3);break;
case'L':ctx.moveTo(tipX,tipY);ctx.lineTo(head[0]+headSz*0.3,head[1]-wing);ctx.lineTo(head[0]+headSz*0.3,head[1]+wing);break;
case'R':ctx.moveTo(tipX,tipY);ctx.lineTo(head[0]-headSz*0.3,head[1]-wing);ctx.lineTo(head[0]-headSz*0.3,head[1]+wing);break;
}
ctx.closePath();ctx.fill();
// Colored outline
ctx.strokeStyle=blocked?'#ff4444':c.main;
ctx.lineWidth=Math.max(2,cs*0.06);
ctx.shadowColor=blocked?'#ff4444':c.main;
ctx.shadowBlur=blocked?15:10;
ctx.stroke();
ctx.restore();
}
// ─── Anim ────────────────────────────────────────────
function animLoop(){
let d=false;
for(const a of animList){
a.t+=0.8;d=true;
// head reaches the end of the path -> whole snake is off-grid
if(a.t>=a.path.length-a.n)a.done=true;
}
animList=animList.filter(a=>!a.done);
if(Math.abs(zoom-tZoom)>0.0005)clump();
if(d||Math.abs(zoom-tZoom)>0.0005)draw();
if(animList.length>0||Math.abs(zoom-tZoom)>0.0005){
animId=requestAnimationFrame(animLoop)
}else{
animId=null;draw();
if(!solved&&arrows.length===0){solved=true;setTimeout(showWin,300)}
}
}
function fly(a){
const[dc,dr]=DIR_VEC[a.dir];
// path = snake cells + straight continuation in the head direction
const path=a.cells.map(c=>[...c]);
const n=a.cells.length;
let off=0;
while(off<n+2){
const last=path[path.length-1];
const nx=[last[0]+dc,last[1]+dr];
path.push(nx);
if(nx[0]<0||nx[0]>=gridW||nx[1]<0||nx[1]>=gridH)off++;
}
animList.push({arrow:a,path,n,colorIdx:a.colorIdx,dir:a.dir,t:0,done:false});
if(!animId)animId=requestAnimationFrame(animLoop)
}
// ─── Hit test ────────────────────────────────────────
function hit(px,py){
const cs=cellPx*zoom;
for(const a of arrows){
for(const [c,r] of a.cells){
const[cx,cy]=world(c,r);
const dx=px-(cx+cs/2),dy=py-(cy+cs/2);
// Larger hit radius for easier tapping
if(dx*dx+dy*dy<(cs*0.55)*(cs*0.55))return a
}
}
return null
}
// ─── Move ────────────────────────────────────────────
function tryMove(a){
if(solved||lives<=0)return;
if(clearPath(a)){
combo++;moves++;
history.push({arrow:a,cells:a.cells.map(c=>[...c]),dir:a.dir,ci:a.colorIdx});
fly(a);arrows=arrows.filter(x=>x!==a);selected=null;updateUI();
if(combo>=5)showCombo(combo)
}else{
combo=0;lives--;selected=a;updateUI();draw();
if(lives<=0)setTimeout(showGameOver,500)
}
}
// ─── Undo ────────────────────────────────────────────
function undo(){
if(!history.length||solved)return;
const r=history.pop();
arrows.push({dir:r.dir,cells:r.cells,colorIdx:r.ci});
moves=Math.max(0,moves-1);
animList=animList.filter(x=>x.arrow!==r.arrow);
updateUI();draw()
}
// ─── Hint ────────────────────────────────────────────
function useHint(){
if(hints<=0||solved||lives<=0)return;
for(const a of arrows){if(clearPath(a)){hints--;selected=a;updateUI();draw();setTimeout(()=>{if(selected===a){selected=null;draw()}},1500);return}}
}
// ─── Camera ──────────────────────────────────────────
function clump(){
zoom+=(tZoom-zoom)*0.18;
const bw=gridW*cellPx*zoom,bh=gridH*cellPx*zoom;
camX=Math.min(canvas.width*0.15,Math.max(canvas.width-bw-canvas.width*0.15,camX));
camY=Math.min(canvas.height*0.15,Math.max(canvas.height-bh-canvas.height*0.15,camY));
$('zoomLabel').textContent=Math.round(zoom*100)+'%';
}
function fit(){
resize();tZoom=1;zoom=1;
camX=(canvas.width-gridW*cellPx)/2;
camY=(canvas.height-gridH*cellPx)/2;
draw()
}
// ─── UI ──────────────────────────────────────────────
function updateUI(){
$('levelBadge').textContent='Livello '+level;
$('arrowCount').textContent='Frecce: '+arrows.length;
let hd='';for(let i=0;i<3;i++)hd+='<span class="hint-dot'+(i>=hints?' used':'')+'"></span>';
$('hintDots').innerHTML=hd;
const hc=$('heartsContainer');hc.innerHTML='';
for(let i=0;i<maxLives;i++){const h=document.createElement('span');h.className='heart'+(i>=lives?' lost':'');h.textContent='❤️';hc.appendChild(h)}
}
function showCombo(n){
const el=document.createElement('div');
el.style.cssText='position:fixed;pointer-events:none;font-weight:800;font-size:18px;color:#ffc832;left:'+(window.innerWidth/2)+'px;top:'+(window.innerHeight/2)+'px;z-index:200;animation:cb 0.9s ease-out forwards;text-shadow:0 0 12px rgba(255,200,50,0.5)';
el.textContent=n+'x';document.body.appendChild(el);
const s=document.createElement('style');s.textContent='@keyframes cb{0%{opacity:1;transform:translate(-50%,0) scale(1)}100%{opacity:0;transform:translate(-50%,-50px) scale(1.3)}}';
document.head.appendChild(s);
el.addEventListener('animationend',()=>{el.remove();s.remove()},{once:true});
}
function showWin(){
const ov=document.createElement('div');ov.className='overlay';
ov.innerHTML='<div class="overlay-card"><div class="overlay-emoji">🎉</div><div class="overlay-title">Livello '+level+' Completato!</div><div class="overlay-subtitle">'+moves+' mosse — '+arrows.length+' frecce rimosse</div><button class="btn btn-big btn-win" id="btnNext">▶ Prossimo Livello</button></div>';
document.body.appendChild(ov);
ov.querySelector('#btnNext').onclick=()=>{ov.remove();level++;lives=maxLives;hints=3;gen(level)};
}
function showGameOver(){
const ov=document.createElement('div');ov.className='overlay';
ov.innerHTML='<div class="overlay-card"><div class="overlay-emoji">💔</div><div class="overlay-title">Game Over</div><div class="overlay-subtitle">Rimangono '+arrows.length+' frecce</div><button class="btn btn-big btn-retry" id="btnRetry">🔄 Riprova</button></div>';
document.body.appendChild(ov);
ov.querySelector('#btnRetry').onclick=()=>{ov.remove();lives=maxLives;hints=3;gen(level)};
}
function showToast(m){
const el=document.createElement('div');el.className='toast';el.textContent=m;document.body.appendChild(el);
el.addEventListener('animationend',()=>el.remove(),{once:true});
}
// ─── Input ───────────────────────────────────────────
let pd=null,ds=null,isDrag=false,lt=0;
canvas.addEventListener('pointerdown',e=>{
if(solved||lives<=0)return;
pd={x:e.clientX,y:e.clientY,t:Date.now()};ds={x:e.clientX,y:e.clientY,cx:camX,cy:camY};isDrag=false
});
canvas.addEventListener('pointermove',e=>{
if(!pd)return;
const dx=e.clientX-pd.x,dy=e.clientY-pd.y;
if(Math.abs(dx)>4||Math.abs(dy)>4){isDrag=true;camX=ds.cx+dx;camY=ds.cy+dy;draw()}
});
canvas.addEventListener('pointerup',e=>{
if(!pd)return;const dt=Date.now()-pd.t;const dx=Math.abs(e.clientX-pd.x);const dy=Math.abs(e.clientY-pd.y);pd=null;
if(!isDrag&&dt<300&&dx<10&&dy<10){
const a=hit(e.clientX,e.clientY);
if(a)tryMove(a);else{selected=null;draw()}
if(dt<250&&Date.now()-lt<300){
if(Math.abs(zoom-1)<0.05)tZoom=Math.min(6,maxZoom);else tZoom=1;
if(!animId)animId=requestAnimationFrame(animLoop)
}
lt=Date.now()
}
});
canvas.addEventListener('wheel',e=>{
e.preventDefault();
const f=e.deltaY>0?0.7:1.4;
const nz=Math.min(maxZoom,Math.max(minZoom,tZoom*f));
const r=nz/tZoom;camX=e.clientX-(e.clientX-camX)*r;camY=e.clientY-(e.clientY-camY)*r;
tZoom=nz;if(!animId)animId=requestAnimationFrame(animLoop);draw()
},{passive:false});
let lpd=null,psz=1;
canvas.addEventListener('touchstart',e=>{
if(e.touches.length===2){lpd=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);psz=zoom;pd=null}
});
canvas.addEventListener('touchmove',e=>{
if(e.touches.length===2&&lpd){e.preventDefault();
const d=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);
tZoom=Math.min(maxZoom,Math.max(minZoom,psz*(d/lpd)));
if(!animId)animId=requestAnimationFrame(animLoop);draw()}
},{passive:false});
canvas.addEventListener('touchend',()=>{lpd=null});
// ─── Buttons & keys ─────────────────────────────────
function zoomCenter(f){
const nz=Math.min(maxZoom,Math.max(minZoom,tZoom*f));
const r=nz/tZoom;
camX=canvas.width/2-(canvas.width/2-camX)*r;
camY=canvas.height/2-(canvas.height/2-camY)*r;
tZoom=nz;if(!animId)animId=requestAnimationFrame(animLoop);draw()
}
$('btnZoomIn').addEventListener('click',()=>zoomCenter(1.25));
$('btnZoomOut').addEventListener('click',()=>zoomCenter(0.8));
$('btnZoomFit').addEventListener('click',fit);
$('btnHint').addEventListener('click',useHint);
$('btnReset').addEventListener('click',()=>{lives=maxLives;hints=3;gen(level)});
$('btnUndo').addEventListener('click',undo);
$('btnFit').addEventListener('click',fit);
document.addEventListener('keydown',e=>{
if(e.key==='h')useHint();
if(e.key==='r'&&!e.ctrlKey&&!e.metaKey)gen(level);
if(e.key==='z'&&(e.ctrlKey||e.metaKey)){e.preventDefault();undo()}
if(e.key==='0')fit();
if(e.key==='+'||e.key==='='){tZoom=Math.min(maxZoom,tZoom*1.25);if(!animId)animId=requestAnimationFrame(animLoop);draw()}
if(e.key==='-'){tZoom=Math.max(minZoom,tZoom*0.8);if(!animId)animId=requestAnimationFrame(animLoop);draw()}
});
// Click minimap -> jump viewport to that area
$('minimapWrap').addEventListener('click',e=>{
const r=$('minimapWrap').getBoundingClientRect();
const mx=(e.clientX-r.left)/r.width,my=(e.clientY-r.top)/r.height;
camX=canvas.width/2-mx*gridW*cellPx*zoom;
camY=canvas.height/2-my*gridH*cellPx*zoom;
draw()
});
window.addEventListener('resize',()=>{resize();draw()});
// ─── Init ────────────────────────────────────────────
gen(level);
setTimeout(()=>{
if(!localStorage.getItem('arrowHelpShown')){
localStorage.setItem('arrowHelpShown','1');
showToast('🖱️ Trascina per muoverti • ⤢ per vista completa • clicca la minimappa');
}
},800);
})();
</script>
</body>
</html>