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)
This commit is contained in:
truetype committed 2026-08-01 17:54:49 +02:00
commit 67e6514152
5 files changed
+619

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# Python
.venv/
__pycache__/
*.pyc
# Node
node_modules/
# IDE / tooling
.opencode/
.sisyphus/
.DS_Store
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

+596
View File
@@ -0,0 +1,596 @@
<!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>
+11
View File
@@ -0,0 +1,11 @@
import http.server
import socketserver
PORT = 1234
class Server(socketserver.TCPServer):
allow_reuse_address = True
with Server(('0.0.0.0', PORT), http.server.SimpleHTTPRequestHandler) as httpd:
print(f'Arrow Puzzle server on http://0.0.0.0:{PORT}')
httpd.serve_forever()