Neon Snake
by NovaFox99592 lines19.1 KB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neon Snake</title>
<style>
:root{
--bg:#0b0f1a;
--text:#e8eefc;
--muted:#9fb0d6;
--accent:#7cf7ff;
--accent2:#8b7bff;
--danger:#ff4d6d;
}
html,body{height:100%;margin:0;background:radial-gradient(1200px 800px at 50% 30%, #121b33 0%, var(--bg) 55%, #070a12 100%); color:var(--text); font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;}
body{display:grid; place-items:center; padding:16px;}
#app{width:min(900px, 100%); display:grid; gap:14px; grid-template-columns: 1fr; align-items:start;}
@media (min-width: 860px){ #app{grid-template-columns: 1fr 290px;} }
.card{
background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
border:1px solid rgba(255,255,255,0.10);
border-radius:18px;
box-shadow: 0 12px 40px rgba(0,0,0,0.35);
overflow:hidden;
}
.topbar{
display:flex; gap:10px; align-items:center; justify-content:space-between;
padding:12px 14px; background:rgba(0,0,0,0.20); border-bottom:1px solid rgba(255,255,255,0.08);
}
.brand{display:flex; gap:10px; align-items:center;}
.dot{width:10px;height:10px;border-radius:999px;background:var(--accent); box-shadow:0 0 14px rgba(124,247,255,0.9);}
.title{font-weight:700; letter-spacing:0.2px;}
.stats{display:flex; gap:14px; font-size:13px; color:var(--muted);}
.stats b{color:var(--text); font-weight:700;}
.main{padding:14px; display:grid; gap:12px; justify-items:center;}
canvas{
width: min(640px, 100%);
aspect-ratio: 1 / 1;
border-radius:16px;
background: linear-gradient(180deg, rgba(10,15,28,0.95), rgba(9,12,20,0.95));
border:1px solid rgba(255,255,255,0.10);
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.35), 0 18px 60px rgba(0,0,0,0.40);
touch-action: none;
}
.side{padding:14px; display:grid; gap:12px;}
.section{background:rgba(0,0,0,0.16); border:1px solid rgba(255,255,255,0.08); border-radius:16px; padding:12px;}
.section h3{margin:0 0 8px 0; font-size:13px; color:var(--muted); font-weight:700; letter-spacing:0.3px; text-transform:uppercase;}
.row{display:flex; gap:10px; align-items:center; flex-wrap:wrap; justify-content:center;}
button{
font: inherit;
border-radius:12px;
border:1px solid rgba(255,255,255,0.12);
color: var(--text);
padding:9px 10px;
outline:none;
cursor:pointer;
background: linear-gradient(180deg, rgba(124,247,255,0.22), rgba(124,247,255,0.06));
border-color: rgba(124,247,255,0.28);
}
button.secondary{
background: rgba(0,0,0,0.22);
border-color: rgba(255,255,255,0.12);
}
button:active{transform: translateY(1px);}
.pill{
display:inline-flex; gap:8px; align-items:center;
border-radius:999px; padding:8px 10px;
border:1px solid rgba(255,255,255,0.10);
background: rgba(0,0,0,0.20);
font-size:13px; color:var(--muted);
}
.pill b{color:var(--text);}
.hint{font-size:12.5px; color:var(--muted); line-height:1.35;}
.kbd{display:inline-block; padding:2px 6px; border-radius:8px; border:1px solid rgba(255,255,255,0.18); background:rgba(0,0,0,0.20); color:var(--text); font-size:12px;}
#dpad{
display:grid; grid-template-columns: 58px 58px 58px; grid-template-rows: 58px 58px 58px;
gap:10px; justify-content:center; align-content:center; margin-top:6px;
user-select:none;
}
.padbtn{
border-radius:16px;
border:1px solid rgba(255,255,255,0.12);
background: rgba(0,0,0,0.22);
display:grid; place-items:center;
font-size:18px;
cursor:pointer;
touch-action: manipulation;
}
.padbtn:active{transform: translateY(1px); border-color: rgba(124,247,255,0.35);}
.padbtn.blank{opacity:0; pointer-events:none;}
#overlay{
position:absolute; inset:0;
display:grid; place-items:center;
background: radial-gradient(900px 600px at 50% 40%, rgba(17,26,46,0.78), rgba(5,7,12,0.88));
backdrop-filter: blur(6px);
}
#overlay.hidden{display:none;}
.modal{
width:min(520px, calc(100% - 28px));
border-radius:20px;
background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
border:1px solid rgba(255,255,255,0.14);
box-shadow: 0 22px 70px rgba(0,0,0,0.55);
padding:16px;
display:grid; gap:12px;
}
.modal h2{margin:0; font-size:20px;}
.modal p{margin:0; color:var(--muted); line-height:1.35;}
.modal .actions{display:flex; gap:10px; flex-wrap:wrap; justify-content:flex-end;}
.tag{
display:inline-flex; align-items:center; gap:8px;
border-radius:999px;
padding:6px 10px;
border:1px solid rgba(255,255,255,0.10);
background: rgba(0,0,0,0.18);
color: var(--muted);
font-size:12px;
}
.tag .warn{color:#ffd1d8;}
.tag .dot2{
width:8px;height:8px;border-radius:999px;background:var(--danger);
box-shadow:0 0 14px rgba(255,77,109,0.7);
}
</style>
</head>
<body>
<div id="app">
<div class="card" style="position:relative;">
<div class="topbar">
<div class="brand">
<div class="dot"></div>
<div class="title">Neon Snake</div>
</div>
<div class="stats">
<div><b>Score</b> <span id="score">0</span></div>
<div><b>Best</b> <span id="best">0</span></div>
<div><b>Level</b> <span id="level">1</span></div>
</div>
</div>
<div class="main">
<canvas id="c" width="600" height="600"></canvas>
<div class="row">
<span class="pill"><b>Controls:</b> <span class="kbd">Arrows</span> / <span class="kbd">WASD</span> • <span class="kbd">Space</span> pause</span>
<button id="btnPause" class="secondary">Pause</button>
<button id="btnRestart">Restart</button>
</div>
</div>
<div id="overlay">
<div class="modal">
<h2 id="overlayTitle">Neon Snake</h2>
<p id="overlayText">Eat the glowing food, grow longer, and don’t crash into yourself.</p>
<div class="row" style="justify-content:flex-start;">
<span class="tag"><span class="dot2"></span><span class="warn"><b>Classic walls:</b> hitting the edge ends the game</span></span>
<span class="tag"><b>Sound:</b> on</span>
<span class="tag"><b>Difficulty:</b> medium</span>
</div>
<div class="actions">
<button id="btnStart">Start</button>
</div>
<p class="hint">On phones/tablets: swipe on the board or use the D-pad on the right.</p>
</div>
</div>
</div>
<div class="card">
<div class="side">
<div class="section">
<h3>Touch controls</h3>
<div class="hint">Use the D-pad below, or swipe on the board.</div>
<div id="dpad" aria-label="Touch controls">
<div class="padbtn blank"></div>
<div class="padbtn" data-dir="up">▲</div>
<div class="padbtn blank"></div>
<div class="padbtn" data-dir="left">◀</div>
<div class="padbtn" data-dir="pause">⏸</div>
<div class="padbtn" data-dir="right">▶</div>
<div class="padbtn blank"></div>
<div class="padbtn" data-dir="down">▼</div>
<div class="padbtn blank"></div>
</div>
</div>
<div class="section">
<h3>Shortcuts</h3>
<div class="hint">
<span class="kbd">R</span> restart • <span class="kbd">Space</span> pause
</div>
</div>
</div>
</div>
</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const levelEl = document.getElementById('level');
const btnPause = document.getElementById('btnPause');
const btnRestart = document.getElementById('btnRestart');
const overlay = document.getElementById('overlay');
const overlayTitle = document.getElementById('overlayTitle');
const overlayText = document.getElementById('overlayText');
const btnStart = document.getElementById('btnStart');
const GRID = 24;
const W = canvas.width;
const CELL = W / GRID;
const key = (x,y)=> `${x},${y}`;
const clamp = (n,min,max)=> Math.max(min, Math.min(max,n));
// Defaults requested:
const difficulty = 'normal'; // medium
const walls = true; // classic walls
let soundOn = true; // sound on
const BASE_SPEED = { easy: 140, normal: 115, hard: 92 };
// Best score
let best = Number(localStorage.getItem('neon_snake_best') || 0);
bestEl.textContent = best;
// Game state
let snake, dir, nextDir, food, score, level, tickMs, timer;
let paused = true;
let gameOver = false;
// ===== Audio (WebAudio, no external files) =====
let audioCtx = null;
function ensureAudio() {
if (!soundOn) return;
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume().catch(()=>{});
}
function beep({freq=440, dur=0.08, type='sine', gain=0.035}={}) {
if (!soundOn) return;
ensureAudio();
if (!audioCtx) return;
const t0 = audioCtx.currentTime;
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.type = type;
o.frequency.setValueAtTime(freq, t0);
g.gain.setValueAtTime(0, t0);
g.gain.linearRampToValueAtTime(gain, t0 + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
o.connect(g).connect(audioCtx.destination);
o.start(t0);
o.stop(t0 + dur + 0.02);
}
function sfxEat(){ beep({freq: 640, dur:0.07, type:'triangle', gain:0.04}); }
function sfxTurn(){ beep({freq: 300, dur:0.03, type:'sine', gain:0.02}); }
function sfxLose(){
if (!soundOn) return;
ensureAudio();
beep({freq: 210, dur:0.10, type:'sawtooth', gain:0.05});
setTimeout(()=> beep({freq: 160, dur:0.14, type:'sawtooth', gain:0.05}), 90);
}
function sfxLevel(){
beep({freq: 780, dur:0.06, type:'square', gain:0.03});
setTimeout(()=> beep({freq: 980, dur:0.06, type:'square', gain:0.03}), 70);
}
// ===== Game init/reset =====
function resetGame() {
const cx = Math.floor(GRID/2);
const cy = Math.floor(GRID/2);
snake = [
{ x: cx, y: cy+2 },
{ x: cx, y: cy+3 },
{ x: cx, y: cy+4 }
];
dir = { x: 0, y: -1 };
nextDir = { ...dir };
score = 0;
level = 1;
gameOver = false;
tickMs = BASE_SPEED[difficulty];
scoreEl.textContent = score;
levelEl.textContent = level;
spawnFood();
startLoop();
draw();
}
function startLoop(){
if (timer) clearInterval(timer);
timer = setInterval(step, tickMs);
}
function setSpeed(ms){
tickMs = clamp(ms, 55, 220);
startLoop();
}
function spawnFood(){
const occupied = new Set(snake.map(s => key(s.x,s.y)));
const head = snake[0];
for (let tries=0; tries<999; tries++){
const x = Math.floor(Math.random()*GRID);
const y = Math.floor(Math.random()*GRID);
if (occupied.has(key(x,y))) continue;
const d = Math.abs(x-head.x) + Math.abs(y-head.y);
if (d < 4 && tries < 80) continue;
food = { x, y, pulse: 0 };
return;
}
for (let y=0; y<GRID; y++){
for (let x=0; x<GRID; x++){
if (!occupied.has(key(x,y))) { food = { x,y,pulse:0 }; return; }
}
}
}
// ===== Core game step =====
function step(){
if (paused || gameOver) return;
// Commit buffered direction (disallow instant reversal)
if (!(nextDir.x === -dir.x && nextDir.y === -dir.y)){
if (nextDir.x !== dir.x || nextDir.y !== dir.y) sfxTurn();
dir = nextDir;
}
const head = snake[0];
const nx = head.x + dir.x;
const ny = head.y + dir.y;
// Walls: edge collision ends game
if (nx < 0 || nx >= GRID || ny < 0 || ny >= GRID){
endGame();
return;
}
// Collision with self
for (let i=0; i<snake.length; i++){
if (snake[i].x === nx && snake[i].y === ny){
endGame();
return;
}
}
snake.unshift({ x:nx, y:ny });
// Eat
if (nx === food.x && ny === food.y){
score++;
scoreEl.textContent = score;
sfxEat();
if (score > best){
best = score;
localStorage.setItem('neon_snake_best', String(best));
bestEl.textContent = best;
}
// Level up every 5 points, speed increases slightly
const newLevel = 1 + Math.floor(score / 5);
if (newLevel !== level){
level = newLevel;
levelEl.textContent = level;
sfxLevel();
setSpeed(tickMs - 8);
}
spawnFood();
} else {
snake.pop();
}
food.pulse += 0.22;
draw();
}
function endGame(){
gameOver = true;
paused = true;
sfxLose();
draw();
showOverlay("Game Over", `Score ${score}. Press Start to play again.`);
btnStart.textContent = "Play again";
}
// ===== Rendering =====
function roundRect(x, y, w, h, r){
const rr = Math.min(r, w/2, h/2);
ctx.beginPath();
ctx.moveTo(x+rr, y);
ctx.arcTo(x+w, y, x+w, y+h, rr);
ctx.arcTo(x+w, y+h, x, y+h, rr);
ctx.arcTo(x, y+h, x, y, rr);
ctx.arcTo(x, y, x+w, y, rr);
ctx.closePath();
}
function draw(){
ctx.clearRect(0,0,W,W);
// Background gradient
const g = ctx.createLinearGradient(0,0,0,W);
g.addColorStop(0, 'rgba(18,26,46,0.55)');
g.addColorStop(1, 'rgba(6,8,14,0.65)');
ctx.fillStyle = g;
ctx.fillRect(0,0,W,W);
// Grid lines
ctx.globalAlpha = 0.10;
ctx.strokeStyle = '#ffffff';
ctx.beginPath();
for (let i=1;i<GRID;i++){
ctx.moveTo(i*CELL, 0); ctx.lineTo(i*CELL, W);
ctx.moveTo(0, i*CELL); ctx.lineTo(W, i*CELL);
}
ctx.stroke();
ctx.globalAlpha = 1;
// Food glow
const fx = food.x * CELL;
const fy = food.y * CELL;
const pulse = 0.45 + 0.15*Math.sin(food.pulse || 0);
const pad = CELL*0.16;
const r = CELL*0.28;
ctx.save();
ctx.shadowColor = 'rgba(124,247,255,0.95)';
ctx.shadowBlur = 18;
ctx.fillStyle = `rgba(124,247,255,${0.75*pulse})`;
roundRect(fx+pad, fy+pad, CELL-2*pad, CELL-2*pad, r);
ctx.fill();
ctx.restore();
// Snake
for (let i=snake.length-1; i>=0; i--){
const s = snake[i];
const x = s.x * CELL;
const y = s.y * CELL;
const inset = CELL*0.10;
const rr = CELL*0.30;
const t = i / Math.max(1, snake.length-1);
const alpha = i === 0 ? 0.95 : (0.70 - 0.30*t);
ctx.save();
ctx.shadowColor = i === 0 ? 'rgba(139,123,255,0.70)' : 'rgba(139,123,255,0.25)';
ctx.shadowBlur = i === 0 ? 22 : 10;
ctx.fillStyle = i === 0
? `rgba(139,123,255,${alpha})`
: `rgba(124,247,255,${alpha})`;
roundRect(x+inset, y+inset, CELL-2*inset, CELL-2*inset, rr);
ctx.fill();
ctx.restore();
}
// Walls border (visual)
ctx.save();
ctx.globalAlpha = 0.45;
ctx.strokeStyle = 'rgba(255,77,109,0.55)';
ctx.lineWidth = 6;
ctx.strokeRect(3,3,W-6,W-6);
ctx.restore();
// Small pause toast (only when not overlay)
if (paused && overlay.classList.contains('hidden')){
drawCenterToast("Paused");
}
}
function drawCenterToast(text){
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,0.45)';
const w = 260, h = 56;
const x = (W-w)/2, y = (W-h)/2;
roundRect(x,y,w,h,16);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.90)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '700 18px system-ui, -apple-system, Segoe UI, Roboto, Arial';
ctx.fillText(text, W/2, W/2);
ctx.restore();
}
// ===== Overlay =====
function showOverlay(title, text) {
overlayTitle.textContent = title;
overlayText.textContent = text;
overlay.classList.remove('hidden');
paused = true;
draw();
}
function hideOverlay() {
overlay.classList.add('hidden');
paused = false;
}
// ===== Input handling =====
function setDir(dx,dy){ nextDir = { x:dx, y:dy }; }
function togglePause(){
if (!overlay.classList.contains('hidden')) return;
paused = !paused;
btnPause.textContent = paused ? "Resume" : "Pause";
draw();
}
function onKey(e){
const k = e.key.toLowerCase();
if (k === ' ' || k === 'spacebar'){ togglePause(); e.preventDefault(); return; }
if (k === 'r'){ ensureAudio(); hideOverlay(); resetGame(); return; }
if (k === 'arrowup' || k === 'w') setDir(0,-1);
else if (k === 'arrowdown' || k === 's') setDir(0,1);
else if (k === 'arrowleft' || k === 'a') setDir(-1,0);
else if (k === 'arrowright' || k === 'd') setDir(1,0);
}
window.addEventListener('keydown', onKey, { passive:false });
btnPause.addEventListener('click', ()=>{ ensureAudio(); togglePause(); });
btnRestart.addEventListener('click', ()=>{ ensureAudio(); hideOverlay(); resetGame(); btnStart.textContent="Start"; btnPause.textContent="Pause"; });
// Touch D-pad
document.getElementById('dpad').addEventListener('click', (e)=>{
const btn = e.target.closest('.padbtn');
if (!btn || btn.classList.contains('blank')) return;
ensureAudio();
const d = btn.dataset.dir;
if (d === 'up') setDir(0,-1);
else if (d === 'down') setDir(0,1);
else if (d === 'left') setDir(-1,0);
else if (d === 'right') setDir(1,0);
else if (d === 'pause') togglePause();
});
// Swipe on canvas
let touchStart = null;
canvas.addEventListener('pointerdown', (e)=>{
ensureAudio();
canvas.setPointerCapture(e.pointerId);
touchStart = { x:e.clientX, y:e.clientY, t:performance.now() };
});
canvas.addEventListener('pointerup', (e)=>{
if (!touchStart) return;
const dx = e.clientX - touchStart.x;
const dy = e.clientY - touchStart.y;
const adx = Math.abs(dx), ady = Math.abs(dy);
const dist = Math.hypot(dx,dy);
const dt = performance.now() - touchStart.t;
touchStart = null;
// tap toggles pause
if (dist < 14 && dt < 300){ togglePause(); return; }
if (adx > ady){
setDir(dx > 0 ? 1 : -1, 0);
} else {
setDir(0, dy > 0 ? 1 : -1);
}
});
btnStart.addEventListener('click', ()=>{
ensureAudio(); // required user gesture for audio
hideOverlay();
btnPause.textContent = "Pause";
resetGame();
});
// Start in overlay (paused)
showOverlay("Neon Snake", "Eat the glowing food, grow longer, and don’t crash into yourself.");
</script>
</body>
</html>
Game Source: Neon Snake
Creator: NovaFox99
Libraries: none
Complexity: complex (592 lines, 19.1 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 KIDHUBB header at the top with "remix_of: snake-novafox99" to link back to the original. Then publish at kidhubb.com/publish.