Untitled

 avatar
unknown
plain_text
6 days ago
11 kB
3
Indexable
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Google‑style Snake — Single File</title>
  <style>
    :root{--bg:#0f1724;--panel:#0b1220;--accent:#26de81;--muted:#94a3b8;--danger:#ff3b30}
    *{box-sizing:border-box;font-family:Inter,ui-sans-serif,system-ui,Segoe UI,Roboto,"Helvetica Neue",Arial}
    html,body{height:100%;margin:0;background:linear-gradient(180deg,var(--bg),#071024);color:#e6eef8}
    .wrap{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    .card{background:rgba(255,255,255,0.03);border-radius:12px;padding:18px;max-width:900px;width:100%;display:grid;grid-template-columns:1fr 320px;gap:16px;box-shadow:0 10px 30px rgba(2,6,23,0.6)}
    header{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between}
    h1{font-size:18px;margin:0}
    .board-wrap{display:flex;flex-direction:column;gap:12px;align-items:center}
    canvas{background:linear-gradient(180deg,#071428 0%, #052033 100%);image-rendering:pixelated;border-radius:8px}
    .controls{display:flex;gap:8px;flex-wrap:wrap}
    .btn{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:8px 10px;border-radius:8px;cursor:pointer;color:var(--muted)}
    .btn.primary{border-color:var(--accent);color:var(--accent)}
    .stats{background:rgba(255,255,255,0.02);padding:12px;border-radius:8px}
    .row{display:flex;justify-content:space-between;gap:12px}
    label{font-size:12px;color:var(--muted)}
    input[type=range]{width:100%}
    .small{font-size:13px;color:var(--muted)}
    .footer{font-size:12px;color:var(--muted);grid-column:1/-1;text-align:center;margin-top:4px}
    @media(max-width:880px){.card{grid-template-columns:1fr;}}
  </style>
</head>
<body>
  <div class="wrap">
    <div class="card">
      <header>
        <h1>Google‑style Snake — single file</h1>
        <div class="small">Use arrow keys / WASD. Touch: swipe. Space to pause.</div>
      </header>

      <div class="board-wrap">
        <canvas id="game" width="480" height="480"></canvas>

        <div class="controls">
          <button id="startBtn" class="btn primary">Start</button>
          <button id="pauseBtn" class="btn">Pause</button>
          <button id="resetBtn" class="btn">Reset</button>

          <div style="min-width:180px;">
            <label>Speed <span id="spdLabel">8</span></label>
            <input id="speed" type="range" min="4" max="18" value="8">
          </div>

          <div style="min-width:120px;">
            <label>Grid size</label>
            <select id="gridSz" class="btn" style="padding:6px 8px">
              <option value="12">12x12</option>
              <option value="16" selected>16x16</option>
              <option value="20">20x20</option>
              <option value="24">24x24</option>
            </select>
          </div>
        </div>
      </div>

      <aside class="stats">
        <div class="row"><div>Score</div><div id="score">0</div></div>
        <div class="row"><div>Highscore</div><div id="highscore">0</div></div>
        <div style="height:12px"></div>
        <div class="small">Controls: Arrow keys / WASD. Press space to pause. Tap and swipe on mobile.</div>
      </aside>

      <div class="footer">Built for you — open this file in any modern browser to play. Want features added? Ask!</div>
    </div>
  </div>

  <script>
// Simple single-file Snake game
(() => {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  let grid = parseInt(document.getElementById('gridSz').value); // number of cells per row
  let cellSize = Math.floor(canvas.width / grid);
  let speed = Number(document.getElementById('speed').value);
  const spdLabel = document.getElementById('spdLabel');
  const startBtn = document.getElementById('startBtn');
  const pauseBtn = document.getElementById('pauseBtn');
  const resetBtn = document.getElementById('resetBtn');
  const scoreEl = document.getElementById('score');
  const highEl = document.getElementById('highscore');
  const speedInput = document.getElementById('speed');
  const gridSelect = document.getElementById('gridSz');

  let frameInterval = 1000 / speed; // ms per move
  let lastTime = 0;
  let running = false;
  let paused = false;

  // Game state
  let snake, dir, nextDir, food, score;

  // Touch handling
  let touchStart = null;

  function resetState() {
    grid = parseInt(gridSelect.value);
    cellSize = Math.floor(canvas.width / grid);
    speed = Number(speedInput.value);
    frameInterval = 1000 / speed;

    const startX = Math.floor(grid/2);
    const startY = Math.floor(grid/2);
    snake = [ {x: startX, y: startY}, {x: startX-1, y: startY}, {x: startX-2, y: startY} ];
    dir = {x:1,y:0};
    nextDir = {x:1,y:0};
    placeFood();
    score = 0;
    scoreEl.textContent = score;
    highEl.textContent = localStorage.getItem('snake_high') || 0;
  }

  function placeFood(){
    // choose random empty cell
    const taken = new Set(snake.map(s=>s.x+','+s.y));
    let tries = 0;
    do{
      food = {x: Math.floor(Math.random()*grid), y: Math.floor(Math.random()*grid)};
      tries++;
      if(tries>1000) break;
    } while(taken.has(food.x+','+food.y));
  }

  function step(){
    // move snake
    dir = nextDir; // apply queued direction
    const head = {x: snake[0].x + dir.x, y: snake[0].y + dir.y};

    // wrap around edges (classic Google snake behavior uses walls optionally; we wrap here)
    head.x = (head.x + grid) % grid;
    head.y = (head.y + grid) % grid;

    // collision with self?
    if(snake.some(s => s.x===head.x && s.y===head.y)){
      gameOver();
      return;
    }

    snake.unshift(head);

    // eat food?
    if(head.x===food.x && head.y===food.y){
      score += 1;
      scoreEl.textContent = score;
      placeFood();
    } else {
      snake.pop();
    }
  }

  function gameOver(){
    running = false;
    paused = false;
    startBtn.textContent = 'Start';
    // update highscore
    const prev = Number(localStorage.getItem('snake_high') || 0);
    if(score>prev) localStorage.setItem('snake_high', score);
    highEl.textContent = localStorage.getItem('snake_high');
    // flash border
    canvas.animate([{boxShadow:'0 0 0 0 rgba(255,0,0,0.0)'},{boxShadow:'0 0 0 8px rgba(255,0,0,0.18)'}],{duration:400,iterations:1});
  }

  function draw(){
    // clear
    ctx.clearRect(0,0,canvas.width,canvas.height);

    // background grid
    ctx.fillStyle = '#041025';
    ctx.fillRect(0,0,canvas.width,canvas.height);

    // draw food
    drawCell(food.x, food.y, '#ff3b30');

    // draw snake
    for(let i=snake.length-1;i>=0;i--){
      const s = snake[i];
      const pct = i/snake.length;
      // head brighter
      drawCell(s.x, s.y, i===0? '#26de81' : `rgba(38,222,129,${0.5 + pct*0.5})`);
    }

    // score overlay (nice brushed look)
    ctx.fillStyle='rgba(255,255,255,0.03)';
    ctx.fillRect(6,6,120,34);
    ctx.fillStyle='#cfe9d7';
    ctx.font='14px Inter, Arial';
    ctx.fillText('Score: '+score, 14, 28);
  }

  function drawCell(x,y,fill){
    const pad = Math.max(1, Math.floor(cellSize*0.06));
    const px = x*cellSize + pad;
    const py = y*cellSize + pad;
    const size = Math.max(2, cellSize - pad*2);
    ctx.fillStyle = fill;
    roundRect(ctx,px,py,size,size,Math.max(2,cellSize*0.12));
    ctx.fill();
  }

  function roundRect(ctx,x,y,w,h,r){
    ctx.beginPath();
    ctx.moveTo(x+r,y);
    ctx.arcTo(x+w,y,x+w,y+h,r);
    ctx.arcTo(x+w,y+h,x,y+h,r);
    ctx.arcTo(x,y+h,x,y,r);
    ctx.arcTo(x,y,x+w,y,r);
    ctx.closePath();
  }

  function loop(ts){
    if(!running) return;
    if(paused){ lastTime = ts; requestAnimationFrame(loop); return; }
    if(ts - lastTime >= frameInterval){
      step();
      draw();
      lastTime = ts;
    }
    requestAnimationFrame(loop);
  }

  // Input handling
  const keyMap = {
    ArrowUp: {x:0,y:-1}, ArrowDown:{x:0,y:1}, ArrowLeft:{x:-1,y:0}, ArrowRight:{x:1,y:0},
    w:{x:0,y:-1}, s:{x:0,y:1}, a:{x:-1,y:0}, d:{x:1,y:0}
  };

  window.addEventListener('keydown', e =>{
    if(e.code==='Space'){
      togglePause(); e.preventDefault(); return;
    }
    const k = e.key;
    if(keyMap[k]){
      const nd = keyMap[k];
      // prevent reversing
      if(nd.x === -dir.x && nd.y === -dir.y) return;
      nextDir = nd;
    }
  });

  // Touch swipe support
  canvas.addEventListener('touchstart', (ev)=>{
    if(ev.touches.length>1) return;
    const t = ev.touches[0];
    touchStart = {x:t.clientX, y:t.clientY};
  });
  canvas.addEventListener('touchmove',(ev)=>{ ev.preventDefault(); }, {passive:false});
  canvas.addEventListener('touchend',(ev)=>{
    if(!touchStart) return;
    const t = ev.changedTouches[0];
    const dx = t.clientX - touchStart.x;
    const dy = t.clientY - touchStart.y;
    if(Math.abs(dx)>30 || Math.abs(dy)>30){
      if(Math.abs(dx)>Math.abs(dy)){
        // left/right
        nextDir = dx>0? {x:1,y:0} : {x:-1,y:0};
      } else {
        nextDir = dy>0? {x:0,y:1} : {x:0,y:-1};
      }
    }
    touchStart = null;
  });

  // Buttons
  startBtn.addEventListener('click', ()=>{
    if(!running){
      running = true; paused = false; startBtn.textContent = 'Running';
      lastTime = performance.now();
      requestAnimationFrame(loop);
    }
  });
  pauseBtn.addEventListener('click', togglePause);
  resetBtn.addEventListener('click', ()=>{ resetState(); draw(); });

  function togglePause(){
    if(!running) return;
    paused = !paused;
    pauseBtn.textContent = paused? 'Resume' : 'Pause';
  }

  // speed and grid controls
  speedInput.addEventListener('input', ()=>{
    speed = Number(speedInput.value);
    frameInterval = 1000 / speed;
    spdLabel.textContent = speed;
  });
  gridSelect.addEventListener('change', ()=>{
    resetState(); draw();
  });

  // resizing canvas on device pixel ratio to make it sharp
  function adjustCanvas(){
    const ratio = window.devicePixelRatio || 1;
    canvas.width = 480 * ratio;
    canvas.height = 480 * ratio;
    canvas.style.width = '480px';
    canvas.style.height = '480px';
    ctx.setTransform(ratio,0,0,ratio,0,0);
    cellSize = Math.floor(480 / grid);
    draw();
  }
  window.addEventListener('resize', adjustCanvas);

  // init
  resetState();
  adjustCanvas();
  draw();

})();
  </script>
</body>
</html>
Editor is loading...
Leave a Comment