{"slug": "frogger-ai-prompt-and-results", "title": "Frogger AI Prompt and results", "summary": "A developer built a Frogger-style AI game called 'HOME FRONT' that simulates a frog's commute through a modern home. The project uses a custom prompt and results in an interactive browser-based game with a retro cabinet aesthetic.", "body_md": "|\n<!doctype html> |\n|\n<html lang=\"en\"> |\n|\n<head> |\n|\n<meta charset=\"utf-8\" /> |\n|\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover\" /> |\n|\n<title>HOME FRONT — a frog's commute through the modern home</title> |\n|\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"> |\n|\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin> |\n|\n<link href=\"https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,700;12..96,800&family=Sora:wght@400;500;600;700&display=swap\" rel=\"stylesheet\"> |\n|\n<style> |\n|\n:root{ |\n|\n--ink:#20232b; |\n|\n--cream:#efe7d6; |\n|\n--cream-2:#e4d9c2; |\n|\n--paper:#f6f0e2; |\n|\n--stage:#16181d; |\n|\n--stage-2:#23262e; |\n|\n--bezel:#1b1d22; |\n|\n--coral:#ff5a3c; |\n|\n--coral-d:#d8442b; |\n|\n--gold:#f3b13d; |\n|\n--green:#7ed957; |\n|\n--green-d:#4ea632; |\n|\n--blue:#7fc6ff; |\n|\n--muted:#8a8576; |\n|\n--shadow:0 18px 40px -18px rgba(0,0,0,.65); |\n|\n} |\n|\n*{box-sizing:border-box} |\n|\nhtml,body{height:100%} |\n|\nbody{ |\n|\nmargin:0; |\n|\nfont-family:\"Sora\",system-ui,sans-serif; |\n|\ncolor:var(--cream); |\n|\nbackground: |\n|\nradial-gradient(120% 80% at 18% -10%, #2a2e38 0%, rgba(42,46,56,0) 55%), |\n|\nradial-gradient(120% 90% at 110% 110%, #2c1d18 0%, rgba(44,29,24,0) 50%), |\n|\nlinear-gradient(160deg,#15171c 0%, #1d2027 60%, #14161b 100%); |\n|\nmin-height:100dvh; |\n|\ndisplay:flex; |\n|\nalign-items:center; |\n|\njustify-content:center; |\n|\npadding:18px; |\n|\noverflow:hidden; |\n|\n} |\n|\n/* grain */ |\n|\nbody::before{ |\n|\ncontent:\"\";position:fixed;inset:0;pointer-events:none;opacity:.05;mix-blend-mode:overlay; |\n|\nbackground-image:url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.6'/></svg>\"); |\n|\n} |\n|\n|\n|\n.cabinet{ |\n|\nposition:relative; |\n|\nwidth:min(94vw, 560px); |\n|\nbackground:linear-gradient(180deg,var(--cream) 0%, var(--cream-2) 100%); |\n|\nborder-radius:26px; |\n|\npadding:14px 14px 12px; |\n|\nbox-shadow: |\n|\n0 1px 0 rgba(255,255,255,.7) inset, |\n|\n0 -6px 0 rgba(0,0,0,.06) inset, |\n|\nvar(--shadow); |\n|\nborder:1px solid rgba(0,0,0,.18); |\n|\n} |\n|\n/* brass corner screws */ |\n|\n.cabinet::before,.cabinet::after, |\n|\n.screw-bl,.screw-br{ |\n|\ncontent:\"\";position:absolute;width:11px;height:11px;border-radius:50%; |\n|\nbackground:radial-gradient(circle at 35% 30%, #f4d488, #b8862f 60%, #6e4d18); |\n|\nbox-shadow:0 1px 1px rgba(0,0,0,.35); |\n|\n} |\n|\n.cabinet::before{top:9px;left:9px} |\n|\n.cabinet::after{top:9px;right:9px} |\n|\n.screw-bl{bottom:9px;left:9px} |\n|\n.screw-br{bottom:9px;right:9px} |\n|\n|\n|\n.topbar{ |\n|\ndisplay:flex;align-items:center;justify-content:space-between;gap:10px; |\n|\npadding:4px 22px 10px; |\n|\n} |\n|\n.brand{display:flex;flex-direction:column;line-height:1} |\n|\n.brand h1{ |\n|\nfont-family:\"Bricolage Grotesque\",sans-serif; |\n|\nfont-weight:800;font-size:clamp(20px,4.6vw,26px); |\n|\nletter-spacing:.02em;margin:0;color:var(--ink); |\n|\ndisplay:flex;align-items:center;gap:7px; |\n|\n} |\n|\n.brand h1 .dot{ |\n|\nwidth:11px;height:11px;border-radius:50%; |\n|\nbackground:var(--coral);box-shadow:0 0 0 3px rgba(255,90,60,.18); |\n|\n} |\n|\n.brand .sub{ |\n|\nfont-size:10.5px;color:var(--muted);margin-top:5px;font-weight:500; |\n|\nletter-spacing:.06em;text-transform:uppercase; |\n|\n} |\n|\n.stats{display:flex;gap:7px;align-items:stretch} |\n|\n.chip{ |\n|\nbackground:#fff;border:1px solid rgba(0,0,0,.1);border-radius:11px; |\n|\npadding:5px 9px 6px;min-width:54px;text-align:center; |\n|\nbox-shadow:0 1px 0 rgba(255,255,255,.8) inset, 0 2px 6px -3px rgba(0,0,0,.3); |\n|\n} |\n|\n.chip .k{display:block;font-size:8.5px;letter-spacing:.14em;color:var(--muted);font-weight:600;text-transform:uppercase} |\n|\n.chip .v{display:block;font-family:\"Bricolage Grotesque\",sans-serif;font-weight:800;font-size:17px;color:var(--ink);font-variant-numeric:tabular-nums;line-height:1.1} |\n|\n.chip.coral .v{color:var(--coral-d)} |\n|\n|\n|\n.lives{display:flex;gap:3px;align-items:center;justify-content:center;padding-top:2px} |\n|\n.lives .frog{width:16px;height:16px;display:block} |\n|\n|\n|\n.timerbar{ |\n|\nheight:9px;border-radius:6px;margin:0 4px 8px; |\n|\nbackground:rgba(0,0,0,.16);overflow:hidden; |\n|\nbox-shadow:0 1px 2px rgba(0,0,0,.2) inset; |\n|\n} |\n|\n.timerbar > i{ |\n|\ndisplay:block;height:100%;width:100%; |\n|\nbackground:linear-gradient(90deg,var(--gold),var(--coral)); |\n|\nborder-radius:6px;transition:width .12s linear, background .3s; |\n|\n} |\n|\n.timerbar.low > i{background:linear-gradient(90deg,var(--coral),#ff2d2d);animation:pulse .5s ease-in-out infinite} |\n|\n@keyframes pulse{50%{opacity:.55}} |\n|\n|\n|\n.screen-wrap{ |\n|\nposition:relative; |\n|\naspect-ratio:1/1; |\n|\nborder-radius:16px; |\n|\nbackground:var(--bezel); |\n|\npadding:7px; |\n|\nbox-shadow:0 0 0 1px rgba(0,0,0,.2), 0 10px 24px -14px rgba(0,0,0,.8); |\n|\n} |\n|\ncanvas{ |\n|\ndisplay:block;width:100%;height:100%; |\n|\nborder-radius:11px; |\n|\nbackground:#0e0f12; |\n|\nimage-rendering:auto; |\n|\ntouch-action:none; |\n|\n} |\n|\n.vignette{ |\n|\npointer-events:none;position:absolute;inset:7px;border-radius:11px; |\n|\nbox-shadow:0 0 0 1px rgba(0,0,0,.25), 0 0 40px 6px rgba(0,0,0,.45) inset; |\n|\n} |\n|\n|\n|\n/* overlays */ |\n|\n.overlay{ |\n|\nposition:absolute;inset:7px;border-radius:11px; |\n|\ndisplay:flex;align-items:center;justify-content:center; |\n|\nbackground:rgba(14,15,18,.74);backdrop-filter:blur(3px); |\n|\nz-index:5;text-align:center;padding:22px; |\n|\nanimation:fade .25s ease; |\n|\n} |\n|\n.overlay.hide{display:none} |\n|\n@keyframes fade{from{opacity:0}to{opacity:1}} |\n|\n.card{max-width:88%;color:var(--paper)} |\n|\n.card .eyebrow{font-size:10px;letter-spacing:.32em;text-transform:uppercase;color:var(--gold);font-weight:600} |\n|\n.card h2{ |\n|\nfont-family:\"Bricolage Grotesque\",sans-serif;font-weight:800; |\n|\nfont-size:clamp(28px,7vw,42px);margin:8px 0 4px;color:#fff;letter-spacing:.01em; |\n|\n} |\n|\n.card p{margin:8px auto;max-width:330px;font-size:13.5px;line-height:1.55;color:#d6d2c4;font-weight:400} |\n|\n.card .legend{ |\n|\ndisplay:grid;grid-template-columns:1fr 1fr;gap:6px 14px;margin:14px auto;max-width:320px; |\n|\nfont-size:11.5px;color:#c7c2b3;text-align:left; |\n|\n} |\n|\n.card .legend b{color:#fff;font-weight:600} |\n|\n.card .scoreline{font-family:\"Bricolage Grotesque\",sans-serif;font-size:20px;color:var(--green);font-weight:700;margin-top:6px} |\n|\n.card .scoreline small{color:#9b968a;font-weight:500;font-size:12px;font-family:\"Sora\"} |\n|\n.btn{ |\n|\nappearance:none;border:none;cursor:pointer; |\n|\nfont-family:\"Bricolage Grotesque\",sans-serif;font-weight:800; |\n|\nfont-size:16px;letter-spacing:.04em;text-transform:uppercase; |\n|\ncolor:#fff;background:linear-gradient(180deg,var(--coral),var(--coral-d)); |\n|\npadding:13px 26px;border-radius:13px;margin-top:8px; |\n|\nbox-shadow:0 6px 0 #a8331f, 0 10px 18px -8px rgba(255,90,60,.7); |\n|\ntransition:transform .06s, box-shadow .06s; |\n|\n} |\n|\n.btn:active{transform:translateY(4px);box-shadow:0 2px 0 #a8331f, 0 6px 12px -8px rgba(255,90,60,.7)} |\n|\n|\n|\n.footbar{ |\n|\ndisplay:flex;align-items:center;justify-content:space-between;gap:10px; |\n|\npadding:10px 6px 2px;color:var(--muted);font-size:10.5px;font-weight:500; |\n|\n} |\n|\n.footbar .keys{display:flex;gap:6px;align-items:center;flex-wrap:wrap} |\n|\n.kbd{ |\n|\ndisplay:inline-flex;align-items:center;justify-content:center; |\n|\nmin-width:20px;height:20px;padding:0 5px;border-radius:5px; |\n|\nbackground:#fff;color:var(--ink);border:1px solid rgba(0,0,0,.16); |\n|\nfont-family:\"Bricolage Grotesque\";font-weight:700;font-size:11px; |\n|\nbox-shadow:0 1px 0 rgba(0,0,0,.12); |\n|\n} |\n|\n.iconbtn{ |\n|\nappearance:none;border:1px solid rgba(0,0,0,.16);background:#fff;color:var(--ink); |\n|\nborder-radius:9px;padding:6px 9px;font-size:12px;font-weight:600;cursor:pointer; |\n|\ndisplay:inline-flex;align-items:center;gap:5px; |\n|\n} |\n|\n.iconbtn:active{transform:translateY(1px)} |\n|\n|\n|\n/* on-screen dpad */ |\n|\n.dpad{ |\n|\nposition:absolute;left:10px;bottom:10px;z-index:4; |\n|\ndisplay:grid;grid-template-columns:repeat(3,40px);grid-template-rows:repeat(3,40px); |\n|\ngap:4px;opacity:.92; |\n|\n} |\n|\n.dpad button{ |\n|\nborder:none;border-radius:10px;cursor:pointer; |\n|\nbackground:rgba(255,255,255,.12);color:#fff;backdrop-filter:blur(4px); |\n|\nborder:1px solid rgba(255,255,255,.18); |\n|\nfont-size:18px;display:flex;align-items:center;justify-content:center; |\n|\n-webkit-tap-highlight-color:transparent;user-select:none; |\n|\n} |\n|\n.dpad button:active{background:rgba(255,90,60,.55)} |\n|\n.dpad .up{grid-column:2;grid-row:1} |\n|\n.dpad .left{grid-column:1;grid-row:2} |\n|\n.dpad .right{grid-column:3;grid-row:2} |\n|\n.dpad .down{grid-column:2;grid-row:3} |\n|\n@media (hover:hover) and (pointer:fine){ |\n|\n.dpad{display:none} |\n|\n} |\n|\n@media (max-width:430px){ |\n|\n.stats{gap:5px}.chip{min-width:46px;padding:4px 7px} |\n|\n.chip .v{font-size:15px} |\n|\n.brand .sub{display:none} |\n|\n} |\n|\n</style> |\n|\n</head> |\n|\n<body> |\n|\n<main class=\"cabinet\" role=\"application\" aria-label=\"Home Front game\"> |\n|\n<span class=\"screw-bl\"></span><span class=\"screw-br\"></span> |\n|\n|\n|\n<div class=\"topbar\"> |\n|\n<div class=\"brand\"> |\n|\n<h1><span class=\"dot\"></span>HOME FRONT</h1> |\n|\n<span class=\"sub\">a frog's commute through the modern home</span> |\n|\n</div> |\n|\n<div class=\"stats\"> |\n|\n<div class=\"chip\"><span class=\"k\">Score</span><span class=\"v\" id=\"score\">0</span></div> |\n|\n<div class=\"chip coral\"><span class=\"k\">Hi</span><span class=\"v\" id=\"hi\">0</span></div> |\n|\n<div class=\"chip\"><span class=\"k\">Lvl</span><span class=\"v\" id=\"level\">1</span></div> |\n|\n</div> |\n|\n</div> |\n|\n|\n|\n<div class=\"timerbar\" id=\"timerbar\"><i id=\"timerfill\"></i></div> |\n|\n|\n|\n<div class=\"screen-wrap\"> |\n|\n<canvas id=\"game\" width=\"572\" height=\"572\"></canvas> |\n|\n<div class=\"vignette\"></div> |\n|\n|\n|\n<div class=\"dpad\" id=\"dpad\" aria-hidden=\"true\"> |\n|\n<button class=\"up\" data-dir=\"up\" aria-label=\"up\">▲</button> |\n|\n<button class=\"left\" data-dir=\"left\" aria-label=\"left\">◀</button> |\n|\n<button class=\"right\" data-dir=\"right\" aria-label=\"right\">▶</button> |\n|\n<button class=\"down\" data-dir=\"down\" aria-label=\"down\">▼</button> |\n|\n</div> |\n|\n|\n|\n<!-- START --> |\n|\n<div class=\"overlay\" id=\"startScreen\"> |\n|\n<div class=\"card\"> |\n|\n<div class=\"eyebrow\">Arcade · Cozy Mode</div> |\n|\n<h2>HOME FRONT</h2> |\n|\n<p>You're a frog trying to get home across the living room, the driveway, and the office desk. Hop lane by lane — and don't let the everyday objects touch you.</p> |\n|\n<div class=\"legend\"> |\n|\n<div><b>▲ ▼ ◀ ▶ / WASD</b><br>hop one tile</div> |\n|\n<div><b>Reach the nests</b><br>4 cozy alcoves up top</div> |\n|\n<div><b>Dodge</b><br>Tesla, Ferrari, Lambo…</div> |\n|\n<div><b>Avoid</b><br>phones, laptops, monitors</div> |\n|\n</div> |\n|\n<button class=\"btn\" id=\"startBtn\">Play</button> |\n|\n</div> |\n|\n</div> |\n|\n|\n|\n<!-- PAUSE --> |\n|\n<div class=\"overlay hide\" id=\"pauseScreen\"> |\n|\n<div class=\"card\"> |\n|\n<div class=\"eyebrow\">Paused</div> |\n|\n<h2>Take a Breath</h2> |\n|\n<p>Press <span class=\"kbd\">P</span> or tap to resume.</p> |\n|\n<button class=\"btn\" id=\"resumeBtn\">Resume</button> |\n|\n</div> |\n|\n</div> |\n|\n|\n|\n<!-- GAME OVER --> |\n|\n<div class=\"overlay hide\" id=\"overScreen\"> |\n|\n<div class=\"card\"> |\n|\n<div class=\"eyebrow\" id=\"overEyebrow\">Squashed</div> |\n|\n<h2 id=\"overTitle\">Game Over</h2> |\n|\n<div class=\"scoreline\" id=\"overScore\">0 <small>pts</small></div> |\n|\n<p id=\"overMsg\">The household claimed another hopper.</p> |\n|\n<button class=\"btn\" id=\"againBtn\">Play Again</button> |\n|\n</div> |\n|\n</div> |\n|\n</div> |\n|\n|\n|\n<div class=\"footbar\"> |\n|\n<div class=\"keys\"> |\n|\n<span class=\"kbd\">←</span><span class=\"kbd\">↑</span><span class=\"kbd\">↓</span><span class=\"kbd\">→</span> |\n|\n<span style=\"opacity:.7\">move</span> |\n|\n<span style=\"margin-left:8px\" class=\"kbd\">P</span><span style=\"opacity:.7\">pause</span> |\n|\n<span style=\"margin-left:8px\" class=\"kbd\">M</span><span style=\"opacity:.7\">mute</span> |\n|\n</div> |\n|\n<button class=\"iconbtn\" id=\"muteBtn\" aria-label=\"toggle sound\"> |\n|\n<span id=\"muteIcon\">🔊</span><span>Sound</span> |\n|\n</button> |\n|\n</div> |\n|\n</main> |\n|\n|\n|\n<script> |\n|\n(function(){ |\n|\n\"use strict\"; |\n|\n/* ============================================================ HOME FRONT |\n|\nA cozy, top-down Frogger-style game. Frog crosses a modern home: |\n|\na desk (gadgets), a grass median, and a driveway (toy cars). |\n|\nSingle file, vanilla canvas. No external assets. |\n|\n========================================================================= */ |\n|\n|\n|\n/* ---------- Config ---------- */ |\n|\nconst COLS=13, ROWS=13, TILE=44; |\n|\nconst W=COLS*TILE, H=ROWS*TILE; |\n|\nconst HOP_DUR=0.13; |\n|\nconst START_COL=6, START_ROW=ROWS-1; |\n|\nconst GOAL_SLOTS=[[1,2],[4,5],[7,8],[10,11]]; // column ranges in goal row |\n|\nconst BASE_TIME=40, MIN_TIME=22; |\n|\nconst HI_KEY=\"homefront.hiscore.v1\"; |\n|\n|\n|\n/* ---------- DOM ---------- */ |\n|\nconst canvas=document.getElementById(\"game\"); |\n|\nconst ctx=canvas.getContext(\"2d\"); |\n|\nconst elScore=document.getElementById(\"score\"); |\n|\nconst elHi=document.getElementById(\"hi\"); |\n|\nconst elLevel=document.getElementById(\"level\"); |\n|\nconst elTimerBar=document.getElementById(\"timerbar\"); |\n|\nconst elTimerFill=document.getElementById(\"timerfill\"); |\n|\nconst startScreen=document.getElementById(\"startScreen\"); |\n|\nconst pauseScreen=document.getElementById(\"pauseScreen\"); |\n|\nconst overScreen=document.getElementById(\"overScreen\"); |\n|\n|\n|\n/* ---------- Canvas / DPR ---------- */ |\n|\nlet DPR=1, bgCanvas=document.createElement(\"canvas\"), bgctx=bgCanvas.getContext(\"2d\"); |\n|\nfunction resize(){ |\n|\nDPR=Math.min(window.devicePixelRatio||1,2); |\n|\ncanvas.width=W*DPR; canvas.height=H*DPR; |\n|\nctx.setTransform(DPR,0,0,DPR,0,0); |\n|\nctx.imageSmoothingEnabled=true; |\n|\nbuildBackground(); |\n|\n} |\n|\nwindow.addEventListener(\"resize\",resize); |\n|\n|\n|\n/* ---------- Helpers ---------- */ |\n|\nconst clamp=(v,a,b)=>v<a?a:v>b?b:v; |\n|\nconst lerp=(a,b,t)=>a+(b-a)*t; |\n|\nconst rand=(a,b)=>a+Math.random()*(b-a); |\n|\nconst pick=arr=>arr[(Math.random()*arr.length)|0]; |\n|\nfunction rr(x,y,w,h,r){ // rounded rect path |\n|\nr=Math.min(r,w/2,h/2); |\n|\nctx.beginPath(); |\n|\nctx.moveTo(x+r,y); |\n|\nctx.arcTo(x+w,y,x+w,y+h,r); |\n|\nctx.arcTo(x+w,y+h,x,y+h,r); |\n|\nctx.arcTo(x,y+h,x,y,r); |\n|\nctx.arcTo(x,y,x+w,y,r); |\n|\nctx.closePath(); |\n|\n} |\n|\nfunction bg_rr(x,y,w,h,r){ |\n|\nr=Math.min(r,w/2,h/2); |\n|\nbgctx.beginPath(); |\n|\nbgctx.moveTo(x+r,y); |\n|\nbgctx.arcTo(x+w,y,x+w,y+h,r); |\n|\nbgctx.arcTo(x+w,y+h,x,y+h,r); |\n|\nbgctx.arcTo(x,y+h,x,y,r); |\n|\nbgctx.arcTo(x,y,x+w,y,r); |\n|\nbgctx.closePath(); |\n|\n} |\n|\nconst colToX=c=>c*TILE+TILE/2; |\n|\nconst rowToY=r=>r*TILE+TILE/2; |\n|\n|\n|\n/* ---------- Audio (Web Audio synth) ---------- */ |\n|\nlet actx=null, master=null, muted=false; |\n|\nfunction ensureAudio(){ |\n|\nif(actx) return; |\n|\ntry{ |\n|\nactx=new (window.AudioContext||window.webkitAudioContext)(); |\n|\nmaster=actx.createGain(); master.gain.value=0.5; master.connect(actx.destination); |\n|\n}catch(e){actx=null;} |\n|\n} |\n|\nfunction blip(freq,dur,type,vol,slideTo){ |\n|\nif(!actx||muted) return; |\n|\nconst t=actx.currentTime; |\n|\nconst o=actx.createOscillator(), g=actx.createGain(); |\n|\no.type=type||\"square\"; o.frequency.setValueAtTime(freq,t); |\n|\nif(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo,t+dur); |\n|\ng.gain.setValueAtTime(0.0001,t); |\n|\ng.gain.exponentialRampToValueAtTime(vol||0.25,t+0.012); |\n|\ng.gain.exponentialRampToValueAtTime(0.0001,t+dur); |\n|\no.connect(g); g.connect(master); o.start(t); o.stop(t+dur+0.02); |\n|\n} |\n|\nfunction noise(dur,vol){ |\n|\nif(!actx||muted) return; |\n|\nconst t=actx.currentTime; |\n|\nconst n=Math.floor(actx.sampleRate*dur); |\n|\nconst buf=actx.createBuffer(1,n,actx.sampleRate); |\n|\nconst d=buf.getChannelData(0); |\n|\nfor(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*(1-i/n); |\n|\nconst s=actx.createBufferSource(); s.buffer=buf; |\n|\nconst g=actx.createGain(); g.gain.value=vol||0.3; |\n|\nconst f=actx.createBiquadFilter(); f.type=\"lowpass\"; f.frequency.value=900; |\n|\ns.connect(f); f.connect(g); g.connect(master); s.start(t); |\n|\n} |\n|\nconst sfx={ |\n|\nhop(){blip(520,0.07,\"triangle\",0.18,640);}, |\n|\ngoal(){blip(523,0.09,\"square\",0.2);setTimeout(()=>blip(659,0.09,\"square\",0.2),70);setTimeout(()=>blip(784,0.14,\"square\",0.22),140);}, |\n|\ndie(){noise(0.35,0.35);blip(300,0.4,\"sawtooth\",0.18,70);}, |\n|\nwall(){blip(140,0.18,\"square\",0.2,90);noise(0.12,0.18);}, |\n|\nlevel(){[523,659,784,1046].forEach((f,i)=>setTimeout(()=>blip(f,0.16,\"triangle\",0.22),i*90));}, |\n|\nover(){[392,330,262].forEach((f,i)=>setTimeout(()=>blip(f,0.3,\"sawtooth\",0.2),i*150));} |\n|\n}; |\n|\n|\n|\n/* ---------- Variant tables ---------- */ |\n|\nconst CARS=[ |\n|\n{name:\"tesla\", w:1.70, body:\"#e9edf2\", roof:\"#cfd6dd\", glass:\"#a9dcff\", stripe:\"#dfe4ea\", dark:\"#9aa0a8\"}, |\n|\n{name:\"ferrari\", w:1.60, body:\"#e8392b\", roof:\"#b22619\", glass:\"#bfe9ff\", stripe:\"#ffe9c2\", dark:\"#7a1a12\"}, |\n|\n{name:\"lambo\", w:1.66, body:\"#f4c542\", roof:\"#d6a72f\", glass:\"#cfeaff\", stripe:\"#1b1b1b\", dark:\"#9c7a1f\"}, |\n|\n{name:\"cybertruck\",w:1.95, body:\"#b7bcc4\", roof:\"#9aa0a8\", glass:\"#8fc0db\", stripe:\"#8a8f97\", dark:\"#6c7178\"}, |\n|\n{name:\"porsche\", w:1.60, body:\"#27344f\", roof:\"#19212f\", glass:\"#bfe9ff\", stripe:\"#3c4d6b\", dark:\"#10151f\"} |\n|\n]; |\n|\nconst GADGETS=[ |\n|\n{name:\"iphone\", w:0.82, body:\"#15171c\", screen:\"#1d2740\", tile:\"#3a4a6b\"}, |\n|\n{name:\"tablet\", w:1.04, body:\"#1b1e25\", screen:\"#243049\", tile:\"#3c4d72\"}, |\n|\n{name:\"laptop\", w:1.36, body:\"#cbd0d8\", base:\"#aab0ba\", screen:\"#27314a\"}, |\n|\n{name:\"monitor\",w:1.26, body:\"#14161b\", screen:\"#243552\", stand:\"#3b4049\"} |\n|\n]; |\n|\n|\n|\n/* ---------- Lane plan ---------- */ |\n|\nconst LANE_PLAN=[ |\n|\n// row, dir, speed, count, possible variants, kind |\n|\n{row:1, dir:-1, speed:52, count:3, kinds:[\"iphone\",\"tablet\"], kind:\"gadget\"}, |\n|\n{row:2, dir: 1, speed:80, count:2, kinds:[\"laptop\"], kind:\"gadget\"}, |\n|\n{row:3, dir:-1, speed:44, count:3, kinds:[\"monitor\",\"tablet\"], kind:\"gadget\"}, |\n|\n{row:4, dir: 1, speed:94, count:3, kinds:[\"iphone\"], kind:\"gadget\"}, |\n|\n{row:5, dir:-1, speed:62, count:2, kinds:[\"tablet\",\"laptop\"], kind:\"gadget\"}, |\n|\n{row:7, dir: 1, speed:70, count:3, kinds:[\"tesla\",\"porsche\"], kind:\"car\"}, |\n|\n{row:8, dir:-1, speed:98, count:2, kinds:[\"ferrari\"], kind:\"car\"}, |\n|\n{row:9, dir: 1, speed:58, count:2, kinds:[\"cybertruck\"], kind:\"car\"}, |\n|\n{row:10,dir:-1, speed:114, count:3, kinds:[\"lambo\",\"ferrari\"], kind:\"car\"}, |\n|\n{row:11,dir: 1, speed:50, count:3, kinds:[\"tesla\",\"porsche\"], kind:\"car\"} |\n|\n]; |\n|\n|\n|\nfunction buildLanes(){ |\n|\nreturn LANE_PLAN.map(p=>{ |\n|\nconst tbl = p.kind===\"car\"?CARS:GADGETS; |\n|\nconst items=[]; |\n|\nlet maxW=0; |\n|\nfor(let i=0;i<p.count;i++){ |\n|\nconst v=tbl.find(x=>x.name===pick(p.kinds))||tbl[0]; |\n|\nconst wpx=v.w*TILE; |\n|\nitems.push({variant:v, w:wpx}); |\n|\nif(wpx>maxW)maxW=wpx; |\n|\n} |\n|\nconst pad=rand(30,140); |\n|\nconst cycleLen=W+maxW+pad; |\n|\nconst spacing=cycleLen/p.count; |\n|\nitems.forEach((it,i)=>{ it.pos0 = i*spacing - it.w/2; }); |\n|\nreturn {row:p.row, dir:p.dir, speed:p.speed, kind:p.kind, items, cycleLen, phase:rand(0,cycleLen)}; |\n|\n}); |\n|\n} |\n|\n|\n|\n/* ---------- Game state ---------- */ |\n|\nconst game={ |\n|\nstate:\"boot\", // boot | play | dead | levelclear | gameover |\n|\npaused:false, |\n|\nscore:0, hi:0, level:1, lives:3, |\n|\ntimer:BASE_TIME, maxTime:BASE_TIME, speedMul:1, |\n|\ngoals:[false,false,false,false], |\n|\ndeadTimer:0, lcTimer:0, |\n|\ntoast:null, toastLife:0, |\n|\nflash:0 |\n|\n}; |\n|\nconst frog={ |\n|\ncol:START_COL, row:START_ROW, fx:colToX(START_COL), fy:rowToY(START_ROW), |\n|\nfromX:0, fromY:0, toX:0, toY:0, hopT:0, hopping:false, facing:0, alive:true, |\n|\nminRow:START_ROW, hopPulse:0 |\n|\n}; |\n|\nlet lanes=[]; let particles=[]; |\n|\n|\n|\n/* ---------- Flow ---------- */ |\n|\nfunction startGame(){ |\n|\nensureAudio(); |\n|\ngame.score=0; game.level=1; game.lives=3; game.speedMul=1; |\n|\ngame.maxTime=BASE_TIME; game.goals=[false,false,false,false]; |\n|\ngame.paused=false; |\n|\nlanes=buildLanes(); |\n|\nrespawn(); |\n|\nhideOverlay(startScreen); hideOverlay(pauseScreen); hideOverlay(overScreen); |\n|\ngame.state=\"play\"; |\n|\nupdateHUD(); |\n|\n} |\n|\nfunction respawn(){ |\n|\nfrog.col=START_COL; frog.row=START_ROW; |\n|\nfrog.fx=colToX(START_COL); frog.fy=rowToY(START_ROW); |\n|\nfrog.hopping=false; frog.alive=true; frog.facing=0; frog.minRow=START_ROW; |\n|\ngame.timer=game.maxTime; |\n|\n} |\n|\nfunction loseLife(){ |\n|\ngame.lives--; |\n|\nupdateHUD(); |\n|\nif(game.lives<=0){ gameOver(); } |\n|\nelse { respawn(); game.state=\"play\"; } |\n|\n} |\n|\nfunction reachGoal(slot){ |\n|\ngame.goals[slot]=true; |\n|\nconst tb=Math.floor(game.timer)*10; |\n|\naddScore(200+tb); |\n|\nspawnParticles(colToX((GOAL_SLOTS[slot][0]+GOAL_SLOTS[slot][1])/2), rowToY(0)+TILE*0.4, 22, \"goal\"); |\n|\nsfx.goal(); |\n|\nif(game.goals.every(Boolean)){ |\n|\naddScore(1000); |\n|\ngame.state=\"levelclear\"; game.lcTimer=1.9; |\n|\nshowToast(\"LEVEL \"+game.level+\" CLEAR\",\"+1000 bonus\"); |\n|\nsfx.level(); |\n|\n}else{ |\n|\nrespawn(); |\n|\n} |\n|\nupdateHUD(); |\n|\n} |\n|\nfunction nextLevel(){ |\n|\ngame.level++; |\n|\ngame.speedMul=1+(game.level-1)*0.15; |\n|\ngame.maxTime=Math.max(MIN_TIME,BASE_TIME-(game.level-1)*2); |\n|\ngame.goals=[false,false,false,false]; |\n|\nlanes=buildLanes(); |\n|\nrespawn(); |\n|\ngame.state=\"play\"; |\n|\nshowToast(\"LEVEL \"+game.level,\"faster traffic\"); |\n|\nupdateHUD(); |\n|\n} |\n|\nfunction killFrog(reason){ |\n|\nif(!frog.alive) return; |\n|\nfrog.alive=false; |\n|\ngame.state=\"dead\"; game.deadTimer=0.95; |\n|\ngame.flash=0.6; |\n|\nspawnParticles(frog.fx,frog.fy,18,reason===\"time\"?\"drown\":\"splat\"); |\n|\nif(reason===\"wall\") sfx.wall(); else sfx.die(); |\n|\n} |\n|\nfunction gameOver(){ |\n|\ngame.state=\"gameover\"; |\n|\nif(game.score>game.hi){ game.hi=game.score; try{localStorage.setItem(HI_KEY,String(game.hi));}catch(e){} } |\n|\ndocument.getElementById(\"overScore\").innerHTML = game.score+\" <small>pts</small>\"; |\n|\ndocument.getElementById(\"overTitle\").textContent = game.score===0?\"Try Again\":\"Game Over\"; |\n|\ndocument.getElementById(\"overMsg\").textContent = |\n|\ngame.score>=1500?\"A legend of the living room.\":game.score>=600?\"Respectable hopping.\":\"The household claimed another hopper.\"; |\n|\ndocument.getElementById(\"overEyebrow\").textContent = game.lives<=0?\"Out of lives\":\"Time's up\"; |\n|\nshowOverlay(overScreen); |\n|\nupdateHUD(); |\n|\nsfx.over(); |\n|\n} |\n|\nfunction addScore(n){ game.score+=n; updateHUD(); } |\n|\n|\n|\n/* ---------- HUD ---------- */ |\n|\nfunction updateHUD(){ |\n|\nelScore.textContent=game.score; |\n|\nelHi.textContent=Math.max(game.hi,game.score); |\n|\nelLevel.textContent=game.level; |\n|\nelTimerFill.style.width=clamp(game.timer/game.maxTime,0,1)*100+\"%\"; |\n|\nelTimerBar.classList.toggle(\"low\", game.timer/game.maxTime<0.25); |\n|\n} |\n|\nfunction showToast(text,sub){ game.toast={text,sub}; game.toastLife=2.0; } |\n|\nfunction showOverlay(el){ el.classList.remove(\"hide\"); } |\n|\nfunction hideOverlay(el){ el.classList.add(\"hide\"); } |\n|\n|\n|\n/* ---------- Input ---------- */ |\n|\nconst DIR={ up:{dc:0,dr:-1,a:0}, down:{dc:0,dr:1,a:Math.PI}, left:{dc:-1,dr:0,a:-Math.PI/2}, right:{dc:1,dr:0,a:Math.PI/2} }; |\n|\nfunction tryMove(name){ |\n|\nif(game.state===\"boot\"){ startGame(); return; } |\n|\nif(game.state===\"gameover\"){ return; } |\n|\nif(game.paused){ togglePause(); return; } |\n|\nif(game.state!==\"play\"||frog.hopping||!frog.alive) return; |\n|\nconst d=DIR[name]; if(!d) return; |\n|\nconst nc=clamp(frog.col+d.dc,0,COLS-1); |\n|\nconst nr=clamp(frog.row+d.dr,0,ROWS-1); |\n|\nif(nc===frog.col&&nr===frog.row) return; // edge, no-op |\n|\nfrog.facing=d.a; |\n|\nfrog.fromX=frog.fx; frog.fromY=frog.fy; |\n|\nfrog.col=nc; frog.row=nr; |\n|\nfrog.toX=colToX(nc); frog.toY=rowToY(nr); |\n|\nfrog.hopping=true; frog.hopT=0; |\n|\nif(nr<frog.minRow){ game.score+=(frog.minRow-nr)*10; frog.minRow=nr; updateHUD(); } |\n|\nsfx.hop(); |\n|\n} |\n|\nfunction togglePause(){ |\n|\nif(game.state!==\"play\") return; |\n|\ngame.paused=!game.paused; |\n|\nif(game.paused) showOverlay(pauseScreen); else hideOverlay(pauseScreen); |\n|\n} |\n|\nwindow.addEventListener(\"keydown\",e=>{ |\n|\nconst k=e.key; |\n|\nif([\"ArrowUp\",\"ArrowDown\",\"ArrowLeft\",\"ArrowRight\",\" \"].includes(k)) e.preventDefault(); |\n|\nif(k===\"ArrowUp\"||k===\"w\"||k===\"W\") tryMove(\"up\"); |\n|\nelse if(k===\"ArrowDown\"||k===\"s\"||k===\"S\") tryMove(\"down\"); |\n|\nelse if(k===\"ArrowLeft\"||k===\"a\"||k===\"A\") tryMove(\"left\"); |\n|\nelse if(k===\"ArrowRight\"||k===\"d\"||k===\"D\") tryMove(\"right\"); |\n|\nelse if(k===\"p\"||k===\"P\"||k===\"Escape\"){ togglePause(); } |\n|\nelse if(k===\"m\"||k===\"M\"){ toggleMute(); } |\n|\nelse if(k===\"Enter\"){ if(game.state===\"boot\") startGame(); else if(game.state===\"gameover\") startGame(); } |\n|\n},{passive:false}); |\n|\n|\n|\n/* on-screen dpad + swipe */ |\n|\ndocument.querySelectorAll(\"#dpad button\").forEach(b=>{ |\n|\nconst fire=ev=>{ev.preventDefault(); ensureAudio(); tryMove(b.dataset.dir);}; |\n|\nb.addEventListener(\"touchstart\",fire,{passive:false}); |\n|\nb.addEventListener(\"mousedown\",fire); |\n|\n}); |\n|\nlet tsx=0,tsy=0,tracking=false; |\n|\ncanvas.addEventListener(\"touchstart\",e=>{ ensureAudio(); if(e.touches.length){tsx=e.touches[0].clientX;tsy=e.touches[0].clientY;tracking=true;} },{passive:true}); |\n|\ncanvas.addEventListener(\"touchend\",e=>{ |\n|\nif(!tracking) return; tracking=false; |\n|\nconst t=e.changedTouches[0]; const dx=t.clientX-tsx, dy=t.clientY-tsy; |\n|\nif(Math.abs(dx)<22&&Math.abs(dy)<22){ // tap |\n|\nif(game.state===\"boot\") startGame(); |\n|\nreturn; |\n|\n} |\n|\nif(Math.abs(dx)>Math.abs(dy)) tryMove(dx>0?\"right\":\"left\"); |\n|\nelse tryMove(dy>0?\"down\":\"up\"); |\n|\n},{passive:true}); |\n|\n|\n|\n/* buttons */ |\n|\ndocument.getElementById(\"startBtn\").addEventListener(\"click\",()=>{ensureAudio();startGame();}); |\n|\ndocument.getElementById(\"againBtn\").addEventListener(\"click\",()=>{ensureAudio();startGame();}); |\n|\ndocument.getElementById(\"resumeBtn\").addEventListener(\"click\",togglePause); |\n|\nconst muteBtn=document.getElementById(\"muteBtn\"), muteIcon=document.getElementById(\"muteIcon\"); |\n|\nfunction toggleMute(){ |\n|\nmuted=!muted; |\n|\nmuteIcon.textContent = muted?\"🔇\":\"🔊\"; |\n|\nmuteBtn.style.opacity = muted?\".6\":\"1\"; |\n|\n} |\n|\nmuteBtn.addEventListener(\"click\",toggleMute); |\n|\n|\n|\n/* pause when tab hidden */ |\n|\ndocument.addEventListener(\"visibilitychange\",()=>{ |\n|\nif(document.hidden && game.state===\"play\" && !game.paused){ game.paused=true; showOverlay(pauseScreen); } |\n|\n}); |\n|\n|\n|\n/* ---------- Particles ---------- */ |\n|\nfunction spawnParticles(x,y,n,type){ |\n|\nconst palettes={ |\n|\nsplat:[\"#7ed957\",\"#4ea632\",\"#a7e76a\",\"#e8ffce\"], |\n|\ndrown:[\"#7fc6ff\",\"#3f7fd6\",\"#bfe6ff\",\"#e8f5ff\"], |\n|\ngoal:[\"#ff5a3c\",\"#f3b13d\",\"#7ed957\",\"#ffffff\",\"#ff8a5c\"] |\n|\n}; |\n|\nconst pal=palettes[type]||palettes.splat; |\n|\nfor(let i=0;i<n;i++){ |\n|\nconst a=Math.random()*Math.PI*2, sp=rand(40,180); |\n|\nparticles.push({ |\n|\nx,y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp - (type===\"goal\"?60:0), |\n|\nlife:rand(0.5,1.0), max:1.0, size:rand(2.5,5.5), |\n|\ncolor:pick(pal), spin:rand(-6,6), rot:rand(0,6.28), |\n|\nshape: type===\"goal\"? (Math.random()<0.5?\"star\":\"rect\") : \"leaf\" |\n|\n}); |\n|\n} |\n|\n} |\n|\n|\n|\n/* ---------- Update ---------- */ |\n|\nfunction update(dt){ |\n|\n// toast & flash & particles always tick (even overlays) for liveliness |\n|\nif(game.toastLife>0) game.toastLife-=dt; |\n|\nif(game.flash>0) game.flash=Math.max(0,game.flash-dt*1.6); |\n|\nupdateParticles(dt); |\n|\n|\n|\nif(game.paused) return; |\n|\n|\n|\nif(game.state===\"play\"){ |\n|\nupdateFrog(dt); |\n|\nupdateLanes(dt); |\n|\ncheckCollisions(); |\n|\ngame.timer-=dt; |\n|\nif(game.timer<=0){ game.timer=0; killFrog(\"time\"); } |\n|\nupdateHUD(); |\n|\n} else if(game.state===\"dead\"){ |\n|\nupdateLanes(dt); |\n|\ngame.deadTimer-=dt; |\n|\nif(game.deadTimer<=0) loseLife(); |\n|\n} else if(game.state===\"levelclear\"){ |\n|\nupdateLanes(dt); |\n|\ngame.lcTimer-=dt; |\n|\nif(game.lcTimer<=0) nextLevel(); |\n|\n} |\n|\n} |\n|\nfunction updateFrog(dt){ |\n|\nfrog.hopPulse+=dt; |\n|\nif(!frog.hopping) return; |\n|\nfrog.hopT+=dt/HOP_DUR; |\n|\nif(frog.hopT>=1){ |\n|\nfrog.hopT=1; frog.hopping=false; |\n|\nfrog.fx=frog.toX; frog.fy=frog.toY; |\n|\nif(frog.row===0) handleGoalRow(); |\n|\nreturn; |\n|\n} |\n|\nconst e=1-Math.pow(1-frog.hopT,2.2); // ease-out |\n|\nfrog.fx=lerp(frog.fromX,frog.toX,e); |\n|\nfrog.fy=lerp(frog.fromY,frog.toY,e); |\n|\n} |\n|\nfunction handleGoalRow(){ |\n|\nconst col=frog.col; |\n|\nconst slot=GOAL_SLOTS.findIndex(r=>col>=r[0]&&col<=r[1]); |\n|\nif(slot===-1){ killFrog(\"wall\"); return; } |\n|\nif(game.goals[slot]){ killFrog(\"wall\"); return; } |\n|\nreachGoal(slot); |\n|\n} |\n|\nfunction updateLanes(dt){ |\n|\nfor(const lane of lanes){ |\n|\nlane.phase=(lane.phase + lane.dir*lane.speed*game.speedMul*dt)%lane.cycleLen; |\n|\nfor(const it of lane.items){ |\n|\nlet x=(it.pos0+lane.phase)%lane.cycleLen; |\n|\nif(x<0) x+=lane.cycleLen; |\n|\nit.curX=x; |\n|\n} |\n|\n} |\n|\n} |\n|\nfunction checkCollisions(){ |\n|\nif(!frog.alive) return; |\n|\nconst hw=TILE*0.28, hh=TILE*0.30; |\n|\nfor(const lane of lanes){ |\n|\nconst top=lane.row*TILE, bot=top+TILE; |\n|\nif(bot<=frog.fy-hh||top>=frog.fy+hh) continue; |\n|\nfor(const it of lane.items){ |\n|\nfor(const off of [0,-lane.cycleLen,lane.cycleLen]){ |\n|\nconst sx=it.curX+off; |\n|\nif(sx>frog.fx+hw||sx+it.w<frog.fx-hw) continue; |\n|\nkillFrog(\"hit\"); return; |\n|\n} |\n|\n} |\n|\n} |\n|\n} |\n|\nfunction updateParticles(dt){ |\n|\nfor(let i=particles.length-1;i>=0;i--){ |\n|\nconst p=particles[i]; |\n|\np.life-=dt; |\n|\nif(p.life<=0){ particles.splice(i,1); continue; } |\n|\np.x+=p.vx*dt; p.y+=p.vy*dt; |\n|\np.vx*=0.92; p.vy=p.vy*0.92+ (p.shape===\"leaf\"?120:40)*dt; |\n|\np.rot+=p.spin*dt; |\n|\n} |\n|\n} |\n|\n|\n|\n/* ============================================================ RENDER |\n|\n========================================================================= */ |\n|\nfunction render(){ |\n|\nctx.clearRect(0,0,W,H); |\n|\nctx.drawImage(bgCanvas,0,0,W,H); |\n|\ndrawGoalRow(); |\n|\ndrawLanes(); |\n|\nif(frog.alive && (game.state===\"play\"||game.state===\"boot\")) drawFrog(ctx,frog.fx,frog.fy,1,frog.facing,frog.hopping?frog.hopT:0,frog.hopPulse); |\n|\ndrawParticles(); |\n|\nif(game.flash>0){ ctx.fillStyle=\"rgba(255,90,60,\"+game.flash*0.5+\")\"; ctx.fillRect(0,0,W,H); } |\n|\nif(game.toastLife>0&&game.toast) drawToast(); |\n|\n} |\n|\n|\n|\n/* ---------- Background prerender ---------- */ |\n|\nfunction buildBackground(){ |\n|\nbgCanvas.width=W*DPR; bgCanvas.height=H*DPR; |\n|\nbgctx.setTransform(DPR,0,0,DPR,0,0); |\n|\nconst g=bgctx; |\n|\ng.clearRect(0,0,W,H); |\n|\nfor(let r=0;r<ROWS;r++){ |\n|\nconst y=r*TILE; |\n|\nif(r===0) paintGoalWall(g,y); |\n|\nelse if(r>=1&&r<=5) paintDesk(g,y,r); |\n|\nelse if(r===6) paintGrass(g,y); |\n|\nelse if(r>=7&&r<=11) paintAsphalt(g,y,r); |\n|\nelse if(r===12) paintStartFloor(g,y); |\n|\n// separators |\n|\ng.strokeStyle=\"rgba(0,0,0,0.18)\"; g.lineWidth=1; |\n|\ng.beginPath(); g.moveTo(0,y+TILE-0.5); g.lineTo(W,y+TILE-0.5); g.stroke(); |\n|\n} |\n|\n// outer frame |\n|\ng.strokeStyle=\"rgba(0,0,0,0.35)\"; g.lineWidth=2; g.strokeRect(1,1,W-2,H-2); |\n|\n} |\n|\nfunction paintGoalWall(g,y){ |\n|\ng.fillStyle=\"#3a2f26\"; g.fillRect(0,y,W,TILE); |\n|\n// wood grain |\n|\ng.strokeStyle=\"rgba(0,0,0,0.18)\"; g.lineWidth=1; |\n|\nfor(let i=0;i<6;i++){ g.beginPath(); g.moveTo(0,y+6+i*7); g.bezierCurveTo(W*0.3,y+8+i*7,W*0.6,y+4+i*7,W,y+7+i*7); g.stroke(); } |\n|\n// walls between slots (cols 0,3,6,9,12) |\n|\ng.fillStyle=\"#2a211b\"; |\n|\nfor(const c of [0,3,6,9,12]){ |\n|\ng.fillRect(c*TILE,y,TILE,TILE); |\n|\ng.fillStyle=\"rgba(255,255,255,0.04)\"; g.fillRect(c*TILE,y,TILE,3); |\n|\ng.fillStyle=\"#2a211b\"; |\n|\n} |\n|\n} |\n|\nfunction paintDesk(g,y,r){ |\n|\n// birch desk surface |\n|\ng.fillStyle=\"#e7d3a8\"; g.fillRect(0,y,W,TILE); |\n|\ng.fillStyle=\"rgba(255,255,255,0.18)\"; g.fillRect(0,y,W,TILE*0.5); |\n|\ng.strokeStyle=\"rgba(150,120,70,0.22)\"; g.lineWidth=1; |\n|\nfor(let i=0;i<3;i++){ const yy=y+8+i*12; g.beginPath(); g.moveTo(0,yy); g.bezierCurveTo(W*0.3,yy+2,W*0.6,yy-2,W,yy+1); g.stroke(); } |\n|\n// a coffee stain on one lane for character |\n|\nif(r===3){ |\n|\ng.save(); g.globalAlpha=0.10; g.fillStyle=\"#5a3a1a\"; |\n|\ng.beginPath(); g.arc(W*0.78,y+TILE*0.5,TILE*0.42,0,6.29); g.fill(); |\n|\ng.beginPath(); g.arc(W*0.78,y+TILE*0.5,TILE*0.30,0,6.29); g.fillStyle=\"#e7d3a8\"; g.fill(); |\n|\ng.restore(); |\n|\n} |\n|\n// direction chevrons |\n|\nconst dir=LANE_PLAN.find(p=>p.row===r)?.dir||1; |\n|\ndrawChevrons(g,y,dir,\"rgba(120,90,40,0.25)\"); |\n|\n} |\n|\nfunction paintGrass(g,y){ |\n|\ng.fillStyle=\"#7fb15a\"; g.fillRect(0,y,W,TILE); |\n|\ng.fillStyle=\"#6ba247\"; |\n|\nfor(let i=0;i<26;i++){ |\n|\nconst x=(i/26)*W + ((i*53)%18); |\n|\ng.fillRect(x,y+ ( (i*7)%(TILE-8) )+4, 2, 6+ (i%3)*2); |\n|\n} |\n|\n// little flowers |\n|\nconst fc=[\"#f3b13d\",\"#ff5a3c\",\"#ffffff\"]; |\n|\nfor(let i=0;i<8;i++){ |\n|\nconst x=rand(8,W-8), yy=y+rand(8,TILE-8); |\n|\ng.fillStyle=fc[i%3]; |\n|\nfor(let a=0;a<5;a++){ const ang=a/5*6.28; g.beginPath(); g.arc(x+Math.cos(ang)*2.4,yy+Math.sin(ang)*2.4,1.7,0,6.29); g.fill(); } |\n|\ng.fillStyle=\"#fff3c2\"; g.beginPath(); g.arc(x,yy,1.3,0,6.29); g.fill(); |\n|\n} |\n|\n} |\n|\nfunction paintAsphalt(g,y,r){ |\n|\ng.fillStyle=\"#26282d\"; g.fillRect(0,y,W,TILE); |\n|\n// speckle |\n|\ng.fillStyle=\"rgba(255,255,255,0.03)\"; |\n|\nfor(let i=0;i<40;i++){ g.fillRect(rand(0,W),y+rand(0,TILE),1,1); } |\n|\ng.fillStyle=\"rgba(0,0,0,0.25)\"; |\n|\nfor(let i=0;i<14;i++){ g.fillRect(rand(0,W),y+rand(0,TILE),rand(2,5),2); } |\n|\n// dashed lane line at top edge |\n|\nif(r>7){ |\n|\ng.strokeStyle=\"rgba(244,237,220,0.55)\"; g.lineWidth=2; g.setLineDash([12,12]); |\n|\ng.beginPath(); g.moveTo(0,y); g.lineTo(W,y); g.stroke(); g.setLineDash([]); |\n|\n} |\n|\nconst dir=LANE_PLAN.find(p=>p.row===r)?.dir||1; |\n|\ndrawChevrons(g,y,dir,\"rgba(244,237,220,0.10)\"); |\n|\n} |\n|\nfunction paintStartFloor(g,y){ |\n|\n// warm wood planks |\n|\ng.fillStyle=\"#c98a5a\"; g.fillRect(0,y,W,TILE); |\n|\ng.fillStyle=\"#b87642\"; |\n|\nfor(let c=0;c<COLS;c++){ |\n|\nif(c%2===0){ g.fillRect(c*TILE,y,1,TILE); } |\n|\n} |\n|\ng.strokeStyle=\"rgba(0,0,0,0.18)\"; g.lineWidth=1; |\n|\nfor(let c=0;c<=COLS;c++){ g.beginPath(); g.moveTo(c*TILE,y); g.lineTo(c*TILE,y+TILE); g.stroke(); } |\n|\n// a little rug under start |\n|\ng.save(); |\n|\ng.translate(W/2,y+TILE*0.5); |\n|\ng.fillStyle=\"#b3422b\"; bg_rr(-TILE*1.6,-TILE*0.34,TILE*3.2,TILE*0.68,10); g.fill(); |\n|\ng.strokeStyle=\"rgba(255,255,255,0.18)\"; g.lineWidth=2; g.setLineDash([5,5]); |\n|\nbg_rr(-TILE*1.45,-TILE*0.28,TILE*2.9,TILE*0.56,8); g.stroke(); g.setLineDash([]); |\n|\ng.restore(); |\n|\n} |\n|\nfunction drawChevrons(g,y,dir,color){ |\n|\ng.save(); g.globalAlpha=1; g.fillStyle=color; |\n|\nconst n=4; |\n|\nfor(let i=0;i<n;i++){ |\n|\nconst x=(i+0.5)*(W/n); |\n|\nconst cx=x, cy=y+TILE/2; |\n|\ng.beginPath(); |\n|\nif(dir>0){ g.moveTo(cx-6,cy-5); g.lineTo(cx+2,cy); g.lineTo(cx-6,cy+5); } |\n|\nelse { g.moveTo(cx+6,cy-5); g.lineTo(cx-2,cy); g.lineTo(cx+6,cy+5); } |\n|\ng.closePath(); g.fill(); |\n|\n} |\n|\ng.restore(); |\n|\n} |\n|\n|\n|\n/* ---------- Goal row (dynamic) ---------- */ |\n|\nfunction drawGoalRow(){ |\n|\nconst y=0; |\n|\nfor(let s=0;s<GOAL_SLOTS.length;s++){ |\n|\nconst [c0,c1]=GOAL_SLOTS[s]; |\n|\nconst x=c0*TILE, w=(c1-c0+1)*TILE; |\n|\nconst cx=x+w/2, cy=y+TILE/2; |\n|\n// alcove |\n|\nctx.save(); |\n|\nctx.fillStyle=\"#efe2c4\"; rr(x+3,y+3,w-6,TILE-6,8); ctx.fill(); |\n|\n// nest cushion |\n|\nctx.fillStyle = game.goals[s] ? \"#9be36b\" : \"#d9c79a\"; |\n|\nrr(x+8,y+8,w-16,TILE-16,12); ctx.fill(); |\n|\nif(game.goals[s]){ |\n|\n// sleeping frog |\n|\ndrawFrog(ctx,cx,cy+2,0.78,0,0,performance.now()/600,true); |\n|\n}else{ |\n|\n// pulsing invite glow |\n|\nconst p=0.5+0.5*Math.sin(performance.now()/420 + s); |\n|\nctx.globalAlpha=0.25+0.25*p; |\n|\nctx.fillStyle=\"#fff7a8\"; rr(x+12,y+12,w-24,TILE-24,10); ctx.fill(); |\n|\nctx.globalAlpha=1; |\n|\n} |\n|\nctx.restore(); |\n|\n} |\n|\n} |\n|\n|\n|\n/* ---------- Lanes ---------- */ |\n|\nfunction drawLanes(){ |\n|\nfor(const lane of lanes){ |\n|\nconst cy=rowToY(lane.row); |\n|\nfor(const it of lane.items){ |\n|\nfor(const off of [0,-lane.cycleLen,lane.cycleLen]){ |\n|\nconst sx=it.curX+off; |\n|\nif(sx>W+4||sx+it.w<-4) continue; |\n|\nif(lane.kind===\"car\") drawCar(ctx,sx,cy-laneH(it)/2,it.w,laneH(it),it.variant,lane.dir); |\n|\nelse drawGadget(ctx,sx,cy-laneH(it)/2,it.w,laneH(it),it.variant,lane.dir); |\n|\n} |\n|\n} |\n|\n} |\n|\n} |\n|\nfunction laneH(it){ return TILE*0.80; } |\n|\n|\n|\n/* ---------- Sprite: FROG ---------- */ |\n|\nfunction drawFrog(c,cx,cy,scale,angle,hopT,pulse,asleep){ |\n|\nconst lift = hopT>0 ? Math.sin(hopT*Math.PI) : 0; // 0..1 airborne amount |\n|\nconst stretchY = 1 + 0.22*lift, stretchX = 1 - 0.10*lift; |\n|\nconst pop = 1 + 0.10*lift; |\n|\nconst bob = asleep ? Math.sin(pulse)*1.2 : 0; |\n|\nc.save(); |\n|\nc.translate(cx,cy+bob); |\n|\n// shadow |\n|\nc.save(); |\n|\nc.scale(1,0.5); |\n|\nc.fillStyle=\"rgba(0,0,0,\"+(0.32*(1-0.45*lift))+\")\"; |\n|\nc.beginPath(); c.arc(0, TILE*0.5, TILE*0.34*(1-0.18*lift)*scale, 0, 6.29); c.fill(); |\n|\nc.restore(); |\n|\n|\n|\nc.rotate(angle); |\n|\nc.scale(stretchX*pop*scale, stretchY*pop*scale); |\n|\nconst u=TILE; // unit |\n|\n// hind legs |\n|\nc.fillStyle=\"#5fb33a\"; |\n|\nellipse(c,-0.30*u,0.22*u,0.16*u,0.12*u,0.4); |\n|\nellipse(c, 0.30*u,0.22*u,0.16*u,0.12*u,-0.4); |\n|\n// body |\n|\nc.fillStyle=\"#7ed957\"; |\n|\nbodyPath(c,0,0,0.34*u,0.30*u); c.fill(); |\n|\n// belly highlight |\n|\nc.fillStyle=\"rgba(255,255,255,0.18)\"; |\n|\nellipse(c,0,0.04*u,0.20*u,0.14*u,0); |\n|\n// back spots |\n|\nc.fillStyle=\"#57b635\"; |\n|\nellipse(c,-0.12*u,-0.02*u,0.05*u,0.04*u,0); |\n|\nellipse(c, 0.12*u,-0.04*u,0.05*u,0.04*u,0); |\n|\nellipse(c,0,0.10*u,0.04*u,0.03*u,0); |\n|\n// front feet |\n|\nc.fillStyle=\"#5fb33a\"; |\n|\nellipse(c,-0.20*u,-0.20*u,0.09*u,0.06*u,0.5); |\n|\nellipse(c, 0.20*u,-0.20*u,0.09*u,0.06*u,-0.5); |\n|\n// eyes (head is toward -y => up) |\n|\nconst ey=-0.18*u, ex=0.16*u; |\n|\n// eye humps |\n|\nc.fillStyle=\"#7ed957\"; |\n|\nellipse(c,-ex,ey,0.11*u,0.10*u,0); |\n|\nellipse(c, ex,ey,0.11*u,0.10*u,0); |\n|\n// whites |\n|\nc.fillStyle=\"#ffffff\"; |\n|\nellipse(c,-ex,ey-0.01*u,0.075*u,0.075*u,0); |\n|\nellipse(c, ex,ey-0.01*u,0.075*u,0.075*u,0); |\n|\n// pupils |\n|\nc.fillStyle=\"#15240c\"; |\n|\nconst py = asleep ? ey : ey-0.015*u; |\n|\nellipse(c,-ex,py,0.035*u,asleep?0.012*u:0.04*u,0); |\n|\nellipse(c, ex,py,0.035*u,asleep?0.012*u:0.04*u,0); |\n|\n// eye shine |\n|\nc.fillStyle=\"rgba(255,255,255,0.9)\"; |\n|\nellipse(c,-ex+0.02*u,py-0.02*u,0.014*u,0.014*u,0); |\n|\nellipse(c, ex+0.02*u,py-0.02*u,0.014*u,0.014*u,0); |\n|\nc.restore(); |\n|\n} |\n|\nfunction ellipse(c,x,y,rx,ry,rot){ c.save(); c.translate(x,y); c.rotate(rot||0); c.beginPath(); c.ellipse(0,0,rx,ry,0,0,6.2832); c.fill(); c.restore(); } |\n|\nfunction bodyPath(c,x,y,rx,ry){ |\n|\n// rounded blob, slightly pointed at top (head) |\n|\nc.beginPath(); |\n|\nc.moveTo(x, y-ry); |\n|\nc.bezierCurveTo(x+rx,y-ry, x+rx,y+ry*0.9, x+rx*0.5,y+ry); |\n|\nc.bezierCurveTo(x+rx*0.2,y+ry*1.05, x-rx*0.2,y+ry*1.05, x-rx*0.5,y+ry); |\n|\nc.bezierCurveTo(x-rx,y+ry*0.9, x-rx,y-ry, x,y-ry); |\n|\nc.closePath(); |\n|\n} |\n|\n|\n|\n/* ---------- Sprite: CAR ---------- */ |\n|\nfunction drawCar(c,x,y,w,h,v,dir){ |\n|\nc.save(); |\n|\n// shadow |\n|\nc.fillStyle=\"rgba(0,0,0,0.28)\"; |\n|\nrr(x+2,y+h*0.16,w-2,h*0.86,h*0.32); c.fill(); |\n|\n// body |\n|\nconst flip = dir<0; |\n|\nc.save(); |\n|\nif(flip){ c.translate(x+w/2,0); c.scale(-1,1); c.translate(-(x+w/2),0); } |\n|\n// chassis |\n|\nc.fillStyle=v.body; rr(x,y,w,h,h*0.34); c.fill(); |\n|\n// lower skirt |\n|\nc.fillStyle=v.dark; rr(x+w*0.04,y+h*0.62,w*0.92,h*0.3,h*0.2); c.fill(); |\n|\n// cabin/roof |\n|\nc.fillStyle=v.roof; rr(x+w*0.20,y+h*0.16,w*0.5,h*0.5,h*0.22); c.fill(); |\n|\n// windshield (front, toward +x = right since not flipped here in local) |\n|\nc.fillStyle=v.glass; |\n|\nc.beginPath(); |\n|\nc.moveTo(x+w*0.66,y+h*0.20); c.lineTo(x+w*0.78,y+h*0.20); c.lineTo(x+w*0.74,y+h*0.80); c.lineTo(x+w*0.64,y+h*0.80); c.closePath(); c.fill(); |\n|\n// rear window |\n|\nc.beginPath(); |\n|\nc.moveTo(x+w*0.22,y+h*0.20); c.lineTo(x+w*0.34,y+h*0.20); c.lineTo(x+w*0.34,y+h*0.80); c.lineTo(x+w*0.24,y+h*0.80); c.closePath(); c.fill(); |\n|\n// stripe |\n|\nc.fillStyle=v.stripe; c.fillRect(x+w*0.06,y+h*0.47,w*0.88,h*0.06); |\n|\n// headlights (front = right side locally) |\n|\nc.fillStyle=\"#fff6c8\"; |\n|\nc.fillRect(x+w*0.93,y+h*0.18,w*0.05,h*0.12); |\n|\nc.fillRect(x+w*0.93,y+h*0.70,w*0.05,h*0.12); |\n|\n// taillights (left) |\n|\nc.fillStyle=\"#ff4d4d\"; |\n|\nc.fillRect(x+w*0.02,y+h*0.18,w*0.04,h*0.12); |\n|\nc.fillRect(x+w*0.02,y+h*0.70,w*0.04,h*0.12); |\n|\n// wheels |\n|\nc.fillStyle=\"#15171c\"; |\n|\nc.fillRect(x+w*0.10,y+h*0.04,w*0.18,h*0.10); |\n|\nc.fillRect(x+w*0.10,y+h*0.86,w*0.18,h*0.10); |\n|\nc.fillRect(x+w*0.72,y+h*0.04,w*0.18,h*0.10); |\n|\nc.fillRect(x+w*0.72,y+h*0.86,w*0.18,h*0.10); |\n|\n// cybertruck angularity |\n|\nif(v.name===\"cybertruck\"){ |\n|\nc.strokeStyle=\"rgba(0,0,0,0.25)\"; c.lineWidth=1.5; |\n|\nc.beginPath(); c.moveTo(x+w*0.5,y+h*0.05); c.lineTo(x+w*0.96,y+h*0.5); c.lineTo(x+w*0.5,y+h*0.95); c.lineTo(x+w*0.04,y+h*0.5); c.closePath(); c.stroke(); |\n|\n} |\n|\n// tesla screen hint |\n|\nif(v.name===\"tesla\"){ c.fillStyle=\"#0c1218\"; rr(x+w*0.42,y+h*0.30,w*0.10,h*0.20,2); c.fill(); } |\n|\nc.restore(); |\n|\nc.restore(); |\n|\n} |\n|\n|\n|\n/* ---------- Sprite: GADGET ---------- */ |\n|\nfunction drawGadget(c,x,y,w,h,v,dir){ |\n|\nc.save(); |\n|\n// shadow |\n|\nc.fillStyle=\"rgba(0,0,0,0.22)\"; |\n|\nrr(x+2,y+h*0.1,w,h*0.9,h*0.18); c.fill(); |\n|\nif(v.name===\"laptop\"){ |\n|\n// base |\n|\nc.fillStyle=v.base; rr(x,y+h*0.42,w,h*0.58,h*0.14); c.fill(); |\n|\n// trackpad |\n|\nc.fillStyle=\"rgba(0,0,0,0.10)\"; rr(x+w*0.30,y+h*0.66,w*0.40,h*0.26,h*0.06); c.fill(); |\n|\n// keyboard dots |\n|\nc.fillStyle=\"rgba(0,0,0,0.22)\"; |\n|\nconst cols=8,rowsK=3,kw=w/cols*0.6; |\n|\nfor(let r=0;r<rowsK;r++) for(let cc=0;cc<cols;cc++){ |\n|\nc.fillRect(x+w*0.10+cc*(w*0.8/cols), y+h*0.46+r*h*0.06, kw, h*0.035); |\n|\n} |\n|\n// screen (lid) — hinged at top edge |\n|\nc.fillStyle=v.body; rr(x+w*0.06,y,w*0.88,h*0.44,h*0.1); c.fill(); |\n|\nc.fillStyle=v.screen; rr(x+w*0.10,y+h*0.05,w*0.80,h*0.34,h*0.06); c.fill(); |\n|\n// desktop bar |\n|\nc.fillStyle=\"#5a7bbf\"; c.fillRect(x+w*0.10,y+h*0.05,w*0.80,h*0.06); |\n|\nc.fillStyle=\"rgba(255,255,255,0.25)\"; |\n|\nfor(let i=0;i<4;i++) c.fillRect(x+w*0.14+i*w*0.18,y+h*0.16,w*0.10,h*0.10); |\n|\n} else if(v.name===\"monitor\"){ |\n|\n// screen |\n|\nc.fillStyle=v.body; rr(x+w*0.04,y,w*0.92,h*0.74,h*0.1); c.fill(); |\n|\nc.fillStyle=v.screen; rr(x+w*0.10,y+h*0.07,w*0.80,h*0.6,h*0.04); c.fill(); |\n|\n// content |\n|\nc.fillStyle=\"#5a7bbf\"; c.fillRect(x+w*0.10,y+h*0.07,w*0.80,h*0.07); |\n|\nc.fillStyle=\"rgba(255,255,255,0.22)\"; |\n|\nc.fillRect(x+w*0.14,y+h*0.20,w*0.30,h*0.18); |\n|\nc.fillRect(x+w*0.48,y+h*0.20,w*0.38,h*0.08); |\n|\nc.fillRect(x+w*0.48,y+h*0.32,w*0.38,h*0.06); |\n|\n// stand neck + foot |\n|\nc.fillStyle=v.stand; |\n|\nc.fillRect(x+w*0.46,y+h*0.70,w*0.08,h*0.14); |\n|\nrr(x+w*0.32,y+h*0.82,w*0.36,h*0.12,h*0.05); c.fill(); |\n|\n} else { |\n|\n// phone / tablet |\n|\nconst rad = v.name===\"tablet\" ? h*0.1 : h*0.18; |\n|\nc.fillStyle=v.body; rr(x,y,w,h,rad); c.fill(); |\n|\nc.fillStyle=v.screen; rr(x+w*0.12,y+h*0.07,w*0.76,h*0.86,rad*0.6); c.fill(); |\n|\n// notch (phone) |\n|\nif(v.name===\"iphone\"){ |\n|\nc.fillStyle=v.body; rr(x+w*0.38,y+h*0.03,w*0.24,h*0.06,h*0.03); c.fill(); |\n|\n} |\n|\n// app grid |\n|\nc.fillStyle=v.tile; |\n|\nconst cols = v.name===\"tablet\"?5:4; |\n|\nconst rowsG=6, pad=w*0.04, aw=(w*0.76-pad*(cols+1))/cols, ah=(h*0.86-pad*(rowsG+1))/rowsG; |\n|\nfor(let r=0;r<rowsG;r++) for(let cc=0;cc<cols;cc++){ |\n|\nconst ax=x+w*0.12+pad+cc*(aw+pad), ay=y+h*0.07+pad+r*(ah+pad); |\n|\nc.fillStyle = [\"#ff5a3c\",\"#f3b13d\",\"#7ed957\",\"#7fc6ff\",\"#c98a5a\"][(r*cols+cc)%5]; |\n|\nrr(ax,ay,aw,ah,Math.min(aw,ah)*0.25); c.fill(); |\n|\n} |\n|\n// reflection |\n|\nc.fillStyle=\"rgba(255,255,255,0.12)\"; |\n|\nc.beginPath(); c.moveTo(x+w*0.12,y+h*0.07); c.lineTo(x+w*0.12+w*0.3,y+h*0.07); c.lineTo(x+w*0.12,y+h*0.07+h*0.4); c.closePath(); c.fill(); |\n|\n} |\n|\nc.restore(); |\n|\n} |\n|\n|\n|\n/* ---------- Particles ---------- */ |\n|\nfunction drawParticles(){ |\n|\nfor(const p of particles){ |\n|\nconst a=clamp(p.life/p.max,0,1); |\n|\nctx.globalAlpha=a; |\n|\nctx.fillStyle=p.color; |\n|\nif(p.shape===\"star\"){ |\n|\nctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot); |\n|\nctx.beginPath(); |\n|\nfor(let i=0;i<5;i++){ |\n|\nconst ang=i/5*6.28 - 1.57; |\n|\nctx.lineTo(Math.cos(ang)*p.size, Math.sin(ang)*p.size); |\n|\nconst ang2=ang+0.63; |\n|\nctx.lineTo(Math.cos(ang2)*p.size*0.45, Math.sin(ang2)*p.size*0.45); |\n|\n} |\n|\nctx.closePath(); ctx.fill(); ctx.restore(); |\n|\n} else { |\n|\nctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot); |\n|\nctx.fillStyle=p.color; ellipse(ctx,0,0,p.size*0.6,p.size*1.4,0); |\n|\nctx.restore(); |\n|\n} |\n|\n} |\n|\nctx.globalAlpha=1; |\n|\n} |\n|\n|\n|\n/* ---------- Toast ---------- */ |\n|\nfunction drawToast(){ |\n|\nconst t=game.toast; if(!t) return; |\n|\nconst a = clamp(game.toastLife,0,1) * (game.toastLife>1.7?(2-game.toastLife)/0.3:1); |\n|\nctx.save(); |\n|\nctx.globalAlpha=clamp(a,0,1); |\n|\nctx.translate(W/2,H*0.42); |\n|\nconst w=TILE*7.2, h=TILE*2.0; |\n|\nctx.fillStyle=\"rgba(14,15,18,0.9)\"; |\n|\nrr(-w/2,-h/2,w,h,16); ctx.fill(); |\n|\nctx.strokeStyle=\"#f3b13d\"; ctx.lineWidth=2; rr(-w/2,-h/2,w,h,16); ctx.stroke(); |\n|\nctx.fillStyle=\"#fff\"; ctx.textAlign=\"center\"; ctx.textBaseline=\"middle\"; |\n|\nctx.font=\"800 \"+Math.floor(TILE*0.5)+\"px 'Bricolage Grotesque',sans-serif\"; |\n|\nctx.fillText(t.text,0,-TILE*0.18); |\n|\nctx.font=\"500 \"+Math.floor(TILE*0.3)+\"px 'Sora',sans-serif\"; |\n|\nctx.fillStyle=\"#f3b13d\"; ctx.fillText(t.sub,0,TILE*0.32); |\n|\nctx.restore(); |\n|\n} |\n|\n|\n|\n/* ---------- Loop ---------- */ |\n|\nlet last=0; |\n|\nfunction frame(ts){ |\n|\nif(!last) last=ts; |\n|\nlet dt=(ts-last)/1000; last=ts; |\n|\nif(dt>0.05) dt=0.05; |\n|\nupdate(dt); |\n|\nrender(); |\n|\nrequestAnimationFrame(frame); |\n|\n} |\n|\n|\n|\n/* ---------- Init ---------- */ |\n|\nfunction init(){ |\n|\ntry{ game.hi=parseInt(localStorage.getItem(HI_KEY)||\"0\",10)||0; }catch(e){ game.hi=0; } |\n|\nresize(); |\n|\nupdateHUD(); |\n|\n// idle ambient: show a frog on the start rug behind the overlay |\n|\nrequestAnimationFrame(frame); |\n|\n} |\n|\ninit(); |\n|\n})(); |\n|\n</script> |\n|\n</body> |\n|\n</html> |", "url": "https://wpnews.pro/news/frogger-ai-prompt-and-results", "canonical_source": "https://gist.github.com/ivanfioravanti/6042a6b02a7a95648b797fd6ec1c0760", "published_at": "2026-06-14 09:36:08+00:00", "updated_at": "2026-06-14 12:11:15.033373+00:00", "lang": "en", "topics": ["artificial-intelligence", "computer-vision"], "entities": ["HOME FRONT", "Frogger"], "alternates": {"html": "https://wpnews.pro/news/frogger-ai-prompt-and-results", "markdown": "https://wpnews.pro/news/frogger-ai-prompt-and-results.md", "text": "https://wpnews.pro/news/frogger-ai-prompt-and-results.txt", "jsonld": "https://wpnews.pro/news/frogger-ai-prompt-and-results.jsonld"}}