Neon Slither
by LunarGalaxy87491 lines16.7 KB
<!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.