🎮ArcadeLab

Neon Slither

by LunarGalaxy87
491 lines16.7 KB
▶ Play
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Neon Slither</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { width:100%; height:100%; overflow:hidden; background:#0a0e17; font-family:Arial, sans-serif; touch-action:none; }
  canvas { display:block; background:#0a0e17; }
  #hud { position:fixed; top:10px; left:10px; color:#fff; font-size:14px; z-index:10; text-shadow:0 0 4px #000; }
  #hud div { margin-bottom:4px; }
  #minimap { position:fixed; top:10px; right:10px; width:120px; height:120px; background:rgba(0,0,0,0.4); border:2px solid #0ff; border-radius:50%; z-index:10; }
  #startScreen, #gameOverScreen {
    position:fixed; top:0; left:0; width:100%; height:100%;
    background:rgba(5,8,15,0.92); color:#fff; display:flex; flex-direction:column;
    align-items:center; justify-content:center; z-index:100; text-align:center; padding:20px;
  }
  #startScreen h1, #gameOverScreen h1 {
    font-size:42px; color:#0ff; text-shadow:0 0 20px #0ff, 0 0 40px #0ff; margin-bottom:10px;
    letter-spacing:2px;
  }
  #startScreen p, #gameOverScreen p { color:#aaa; margin-bottom:20px; font-size:15px; }
  input[type=text] {
    padding:10px 14px; font-size:16px; border-radius:8px; border:2px solid #0ff;
    background:#101725; color:#fff; text-align:center; margin-bottom:16px; width:220px;
  }
  button {
    padding:12px 30px; font-size:18px; border:none; border-radius:30px;
    background:linear-gradient(45deg,#0ff,#08f); color:#001; font-weight:bold; cursor:pointer;
    box-shadow:0 0 20px rgba(0,255,255,0.6);
  }
  button:active { transform:scale(0.96); }
  #gameOverScreen { display:none; }
  #score-final { font-size:26px; color:#0ff; margin-bottom:20px; }
  #joystick {
    position:fixed; bottom:30px; left:30px; width:110px; height:110px;
    border-radius:50%; background:rgba(255,255,255,0.08); border:2px solid rgba(0,255,255,0.4);
    z-index:20; touch-action:none;
  }
  #joystickKnob {
    position:absolute; width:50px; height:50px; border-radius:50%;
    background:rgba(0,255,255,0.5); top:30px; left:30px; pointer-events:none;
  }
  #boostBtn {
    position:fixed; bottom:45px; right:30px; width:90px; height:90px; border-radius:50%;
    background:rgba(255,80,80,0.25); border:2px solid rgba(255,80,80,0.6); color:#fff;
    font-size:14px; z-index:20; box-shadow:none;
  }
  #boostBtn:active { background:rgba(255,80,80,0.5); }
</style>
</head>
<body>

<div id="startScreen">
  <h1>NEON SLITHER</h1>
  <p>Grow your snake, avoid other snakes, boost with the red button.</p>
  <input type="text" id="nameInput" placeholder="Enter your name" maxlength="12" value="Player">
  <button id="startBtn">PLAY</button>
</div>

<div id="gameOverScreen">
  <h1>GAME OVER</h1>
  <div id="score-final">Score: 0</div>
  <button id="restartBtn">PLAY AGAIN</button>
</div>

<div id="hud" style="display:none;">
  <div>Score: <span id="scoreVal">0</span></div>
  <div>Length: <span id="lengthVal">10</span></div>
  <div id="rankVal">Rank: 1 / 1</div>
</div>
<canvas id="minimap" style="display:none;"></canvas>

<div id="joystick" style="display:none;"><div id="joystickKnob"></div></div>
<button id="boostBtn" style="display:none;">BOOST</button>

<canvas id="game"></canvas>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const miniCanvas = document.getElementById('minimap');
const miniCtx = miniCanvas.getContext('2d');
miniCanvas.width = 120; miniCanvas.height = 120;

function resize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();

const WORLD_SIZE = 4000;
const FOOD_COUNT = 400;

let world, player, bots, foods, gameRunning = false;
let camera = { x: 0, y: 0 };
let mouse = { x: 0, y: 0, active: false };
let boosting = false;
let joystickVec = { x: 0, y: 0 };

function rand(min, max) { return Math.random() * (max - min) + min; }
function dist(x1,y1,x2,y2){ return Math.hypot(x2-x1, y2-y1); }

const colorPalettes = [
  ['#00f5ff','#0080ff'], ['#ff2d78','#ff7b00'], ['#7CFC00','#2e8b00'],
  ['#ffea00','#ff9900'], ['#c471ff','#7b2fff'], ['#ff5e5e','#b30000'],
  ['#5effc6','#00b386'], ['#ff9de2','#ff2fa1']
];

class Snake {
  constructor(x, y, isPlayer, name) {
    this.isPlayer = isPlayer;
    this.name = name || 'Bot';
    this.alive = true;
    this.angle = rand(0, Math.PI*2);
    this.targetAngle = this.angle;
    this.speed = 2.2;
    this.baseSpeed = 2.2;
    this.boostSpeed = 4.2;
    this.segSpacing = 6;
    this.segments = [];
    this.length = 10;
    this.radius = 6;
    this.score = 0;
    this.colors = colorPalettes[Math.floor(rand(0, colorPalettes.length))];
    this.boosting = false;
    this.boostCooldown = 0;
    for (let i=0;i<this.length;i++){
      this.segments.push({ x: x - Math.cos(this.angle)*i*this.segSpacing, y: y - Math.sin(this.angle)*i*this.segSpacing });
    }
  }
  head() { return this.segments[0]; }
  update(foods, allSnakes) {
    if (!this.alive) return;
    let diff = ((this.targetAngle - this.angle + Math.PI*3) % (Math.PI*2)) - Math.PI;
    this.angle += diff * 0.15;

    let curSpeed = this.baseSpeed;
    if (this.boosting && this.length > 12) {
      curSpeed = this.boostSpeed;
      this.length -= 0.05;
      if (Math.random() < 0.3) {
        foods.push({ x: this.head().x, y: this.head().y, r: 3, color: this.colors[0], value: 1 });
      }
    }
    const head = this.head();
    const nx = head.x + Math.cos(this.angle) * curSpeed;
    const ny = head.y + Math.sin(this.angle) * curSpeed;

    const margin = 30;
    if (nx < margin || nx > WORLD_SIZE-margin || ny < margin || ny > WORLD_SIZE-margin) {
      this.alive = false;
      return;
    }

    this.segments.unshift({ x: nx, y: ny });
    const targetLen = Math.max(10, Math.floor(this.length));
    while (this.segments.length > targetLen) this.segments.pop();

    // eat food
    for (let i = foods.length - 1; i >= 0; i--) {
      const f = foods[i];
      if (dist(nx, ny, f.x, f.y) < this.radius + f.r) {
        this.length += f.value * 1.5;
        this.score += f.value * 10;
        foods.splice(i, 1);
      }
    }

    // collisions with other snakes
    for (const other of allSnakes) {
      if (other === this || !other.alive) continue;
      for (let i = 3; i < other.segments.length; i += 2) {
        const s = other.segments[i];
        if (dist(nx, ny, s.x, s.y) < this.radius + other.radius - 1) {
          this.alive = false;
          for (let k=0;k<Math.floor(this.length/2);k+=2){
            const seg = this.segments[k];
            if (seg) foods.push({ x: seg.x + rand(-10,10), y: seg.y + rand(-10,10), r: rand(2,4), color: this.colors[0], value: 2 });
          }
          return;
        }
      }
    }

    this.radius = 6 + Math.min(14, this.length * 0.05);
  }
  aiThink(foods, allSnakes) {
    if (!this.alive) return;
    const head = this.head();
    // find nearest food
    let nearestFood = null, nd = Infinity;
    for (const f of foods) {
      const d = dist(head.x, head.y, f.x, f.y);
      if (d < nd && d < 500) { nd = d; nearestFood = f; }
    }
    // danger avoidance: other snake heads/bodies nearby
    let danger = null, dd = Infinity;
    for (const other of allSnakes) {
      if (other === this || !other.alive) continue;
      for (let i=0;i<other.segments.length;i+=4){
        const s = other.segments[i];
        const d = dist(head.x, head.y, s.x, s.y);
        if (d < 80 && d < dd) { dd = d; danger = s; }
      }
    }
    if (danger) {
      const angleAway = Math.atan2(head.y - danger.y, head.x - danger.x);
      this.targetAngle = angleAway;
    } else if (nearestFood) {
      this.targetAngle = Math.atan2(nearestFood.y - head.y, nearestFood.x - head.x);
    } else {
      if (Math.random() < 0.02) this.targetAngle += rand(-0.5, 0.5);
    }
    // boundary avoidance
    const margin = 200;
    if (head.x < margin) this.targetAngle = 0;
    else if (head.x > WORLD_SIZE - margin) this.targetAngle = Math.PI;
    if (head.y < margin) this.targetAngle = Math.PI/2;
    else if (head.y > WORLD_SIZE - margin) this.targetAngle = -Math.PI/2;

    this.boosting = Math.random() < 0.002 && this.length > 20;
  }
}

function initWorld() {
  foods = [];
  for (let i=0;i<FOOD_COUNT;i++){
    foods.push({
      x: rand(40, WORLD_SIZE-40), y: rand(40, WORLD_SIZE-40),
      r: rand(2,4), color: colorPalettes[Math.floor(rand(0,colorPalettes.length))][0],
      value: 1
    });
  }
  const name = document.getElementById('nameInput').value || 'Player';
  player = new Snake(WORLD_SIZE/2, WORLD_SIZE/2, true, name);
  bots = [];
  for (let i=0;i<16;i++){
    bots.push(new Snake(rand(200, WORLD_SIZE-200), rand(200, WORLD_SIZE-200), false, 'Bot'+(i+1)));
  }
}

function allSnakesArr() { return [player, ...bots]; }

function gameLoop() {
  if (!gameRunning) return;

  if (player.alive) {
    if (mouse.active) {
      player.targetAngle = Math.atan2(mouse.y - canvas.height/2, mouse.x - canvas.width/2);
    } else if (joystickVec.x !== 0 || joystickVec.y !== 0) {
      player.targetAngle = Math.atan2(joystickVec.y, joystickVec.x);
    }
    player.boosting = boosting;
  }

  const all = allSnakesArr();
  for (const b of bots) b.aiThink(foods, all);
  for (const s of all) s.update(foods, all);

  bots = bots.filter(b => {
    if (!b.alive) {
      return false;
    }
    return true;
  });
  while (bots.length < 16) {
    bots.push(new Snake(rand(200, WORLD_SIZE-200), rand(200, WORLD_SIZE-200), false, 'Bot'+Math.floor(rand(100,999))));
  }
  while (foods.length < FOOD_COUNT) {
    foods.push({ x: rand(40, WORLD_SIZE-40), y: rand(40, WORLD_SIZE-40), r: rand(2,4), color: colorPalettes[Math.floor(rand(0,colorPalettes.length))][0], value: 1 });
  }

  if (!player.alive) {
    endGame();
    return;
  }

  camera.x = player.head().x - canvas.width/2;
  camera.y = player.head().y - canvas.height/2;

  render();
  updateHUD();
  requestAnimationFrame(gameLoop);
}

function render() {
  ctx.fillStyle = '#0a0e17';
  ctx.fillRect(0,0,canvas.width, canvas.height);

  // grid
  ctx.strokeStyle = 'rgba(0,255,255,0.05)';
  ctx.lineWidth = 1;
  const gridSize = 50;
  const startX = -camera.x % gridSize;
  const startY = -camera.y % gridSize;
  for (let x = startX; x < canvas.width; x += gridSize) {
    ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,canvas.height); ctx.stroke();
  }
  for (let y = startY; y < canvas.height; y += gridSize) {
    ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(canvas.width,y); ctx.stroke();
  }

  // border
  ctx.strokeStyle = '#ff2d78';
  ctx.lineWidth = 4;
  ctx.shadowColor = '#ff2d78';
  ctx.shadowBlur = 20;
  ctx.strokeRect(-camera.x, -camera.y, WORLD_SIZE, WORLD_SIZE);
  ctx.shadowBlur = 0;

  // food
  for (const f of foods) {
    const sx = f.x - camera.x, sy = f.y - camera.y;
    if (sx < -20 || sx > canvas.width+20 || sy < -20 || sy > canvas.height+20) continue;
    ctx.beginPath();
    ctx.fillStyle = f.color;
    ctx.shadowColor = f.color;
    ctx.shadowBlur = 8;
    ctx.arc(sx, sy, f.r, 0, Math.PI*2);
    ctx.fill();
  }
  ctx.shadowBlur = 0;

  // snakes
  for (const s of allSnakesArr()) {
    if (!s.alive) continue;
    drawSnake(s);
  }

  // name labels
  for (const s of allSnakesArr()) {
    if (!s.alive) continue;
    const h = s.head();
    const sx = h.x - camera.x, sy = h.y - camera.y - s.radius - 10;
    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    ctx.font = '12px Arial';
    ctx.textAlign = 'center';
    ctx.fillText(s.name, sx, sy);
  }

  renderMinimap();
}

function drawSnake(s) {
  const [c1, c2] = s.colors;
  for (let i = s.segments.length - 1; i >= 0; i--) {
    const seg = s.segments[i];
    const sx = seg.x - camera.x, sy = seg.y - camera.y;
    if (sx < -30 || sx > canvas.width+30 || sy < -30 || sy > canvas.height+30) continue;
    const t = i / s.segments.length;
    ctx.beginPath();
    ctx.fillStyle = i === 0 ? c1 : (i % 2 === 0 ? c1 : c2);
    ctx.shadowColor = c1;
    ctx.shadowBlur = i === 0 ? 15 : 6;
    ctx.arc(sx, sy, s.radius * (1 - t*0.15), 0, Math.PI*2);
    ctx.fill();
  }
  ctx.shadowBlur = 0;
  // eyes on head
  const h = s.head();
  const hx = h.x - camera.x, hy = h.y - camera.y;
  const eyeOffset = s.radius * 0.5;
  const ex1 = hx + Math.cos(s.angle + 0.5) * eyeOffset;
  const ey1 = hy + Math.sin(s.angle + 0.5) * eyeOffset;
  const ex2 = hx + Math.cos(s.angle - 0.5) * eyeOffset;
  const ey2 = hy + Math.sin(s.angle - 0.5) * eyeOffset;
  ctx.fillStyle = '#fff';
  ctx.beginPath(); ctx.arc(ex1, ey1, 2.5, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(ex2, ey2, 2.5, 0, Math.PI*2); ctx.fill();
}

function renderMinimap() {
  miniCtx.clearRect(0,0,120,120);
  miniCtx.fillStyle = 'rgba(10,14,23,0.6)';
  miniCtx.beginPath();
  miniCtx.arc(60,60,58,0,Math.PI*2);
  miniCtx.fill();
  const scale = 110 / WORLD_SIZE;
  for (const b of bots) {
    if (!b.alive) continue;
    const h = b.head();
    miniCtx.fillStyle = b.colors[0];
    miniCtx.beginPath();
    miniCtx.arc(5 + h.x*scale, 5 + h.y*scale, 2, 0, Math.PI*2);
    miniCtx.fill();
  }
  if (player.alive) {
    const h = player.head();
    miniCtx.fillStyle = '#fff';
    miniCtx.beginPath();
    miniCtx.arc(5 + h.x*scale, 5 + h.y*scale, 3, 0, Math.PI*2);
    miniCtx.fill();
  }
}

function updateHUD() {
  document.getElementById('scoreVal').textContent = Math.floor(player.score);
  document.getElementById('lengthVal').textContent = Math.floor(player.length);
  const all = allSnakesArr().filter(s => s.alive).sort((a,b) => b.length - a.length);
  const rank = all.indexOf(player) + 1;
  document.getElementById('rankVal').textContent = `Rank: ${rank} / ${all.length}`;
}

function startGame() {
  initWorld();
  gameRunning = true;
  document.getElementById('startScreen').style.display = 'none';
  document.getElementById('gameOverScreen').style.display = 'none';
  document.getElementById('hud').style.display = 'block';
  miniCanvas.style.display = 'block';
  document.getElementById('joystick').style.display = 'block';
  document.getElementById('boostBtn').style.display = 'block';
  gameLoop();
}

function endGame() {
  gameRunning = false;
  document.getElementById('score-final').textContent = 'Score: ' + Math.floor(player.score);
  document.getElementById('gameOverScreen').style.display = 'flex';
  document.getElementById('hud').style.display = 'none';
  miniCanvas.style.display = 'none';
  document.getElementById('joystick').style.display = 'none';
  document.getElementById('boostBtn').style.display = 'none';
}

document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('restartBtn').addEventListener('click', startGame);

// mouse control (desktop / Safari trackpad-ish)
canvas.addEventListener('mousemove', (e) => {
  mouse.x = e.clientX; mouse.y = e.clientY; mouse.active = true;
});
canvas.addEventListener('mousedown', () => { boosting = true; });
window.addEventListener('mouseup', () => { boosting = false; });

// touch drag on canvas as steering fallback
canvas.addEventListener('touchmove', (e) => {
  const t = e.touches[0];
  mouse.x = t.clientX; mouse.y = t.clientY; mouse.active = true;
}, {passive:true});

// joystick control
const joystick = document.getElementById('joystick');
const knob = document.getElementById('joystickKnob');
let joyActive = false, joyCenter = {x:0,y:0};

function joyStart(e) {
  joyActive = true;
  const rect = joystick.getBoundingClientRect();
  joyCenter = { x: rect.left + rect.width/2, y: rect.top + rect.height/2 };
  mouse.active = false;
}
function joyMove(x, y) {
  if (!joyActive) return;
  let dx = x - joyCenter.x, dy = y - joyCenter.y;
  const maxDist = 40;
  const d = Math.hypot(dx, dy);
  if (d > maxDist) { dx = dx/d*maxDist; dy = dy/d*maxDist; }
  knob.style.left = (30 + dx) + 'px';
  knob.style.top = (30 + dy) + 'px';
  if (d > 5) joystickVec = { x: dx, y: dy };
}
function joyEnd() {
  joyActive = false;
  joystickVec = { x: 0, y: 0 };
  knob.style.left = '30px'; knob.style.top = '30px';
}
joystick.addEventListener('touchstart', (e) => { joyStart(e.touches[0]); }, {passive:true});
joystick.addEventListener('touchmove', (e) => { joyMove(e.touches[0].clientX, e.touches[0].clientY); }, {passive:true});
joystick.addEventListener('touchend', joyEnd);
joystick.addEventListener('mousedown', (e) => { joyStart(e); 
  const mm = (ev) => joyMove(ev.clientX, ev.clientY);
  const mu = () => { joyEnd(); window.removeEventListener('mousemove', mm); window.removeEventListener('mouseup', mu); };
  window.addEventListener('mousemove', mm);
  window.addEventListener('mouseup', mu);
});

// boost button
const boostBtn = document.getElementById('boostBtn');
boostBtn.addEventListener('touchstart', (e) => { e.preventDefault(); boosting = true; }, {passive:false});
boostBtn.addEventListener('touchend', (e) => { e.preventDefault(); boosting = false; }, {passive:false});
boostBtn.addEventListener('mousedown', () => { boosting = true; });
boostBtn.addEventListener('mouseup', () => { boosting = false; });

</script>
</body>
</html>

Game Source: Neon Slither

Creator: LunarGalaxy87

Libraries: none

Complexity: complex (491 lines, 16.7 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: neon-slither-lunargalaxy87" to link back to the original. Then publish at arcadelab.ai/publish.