<!
doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-
fit=cover" />
<title>Snake — Growth Freeze (Mobile Ready)</title>
<style>
:root{
--bg:#071028;
--panel:#071a2b;
--accent:#22c55e;
--muted:#94a3b8;
--glass: rgba(255,255,255,0.03);
}
html,body{height:100%;margin:0;background:linear-
gradient(180deg,#071028,#041226);color:#e6eef8;font-family:system-ui,-apple-
system,Segoe UI,Roboto,Arial}
.wrap{max-width:1000px;margin:18px
auto;padding:12px;display:flex;gap:18px;align-items:flex-start;justify-
content:center;flex-wrap:wrap}
.panel{background:rgba(255,255,255,0.02);padding:12px;border-radius:12px;box-
shadow:0 8px 28px rgba(2,6,23,0.6);display:flex;flex-
direction:column;gap:12px;align-items:center}
canvas{border-radius:10px;background:linear-
gradient(180deg,#081b2a,#052033);display:block;touch-action:none}
.hud{display:flex;gap:8px;align-items:center;flex-wrap:wrap;font-size:14px}
.chip{background:var(--glass);padding:8px 10px;border-radius:8px;color:var(--
muted);}
.chip strong{display:block;color:var(--accent);font-size:16px}
.freeze{background:rgba(138,43,226,0.12);color:violet;padding:8px 10px;border-
radius:8px}
.controls{display:flex;gap:8px}
.btn{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:8px
10px;border-radius:8px;color:#cfe7d3;cursor:pointer}
.meta{min-width:220px;max-width:320px;display:flex;flex-
direction:column;gap:10px}
.tip{font-size:13px;color:var(--muted);padding:10px;border-
radius:8px;background:rgba(255,255,255,0.02)}
.touch-area{display:none;gap:8px;flex-direction:column;align-
items:center;margin-top:6px}
.touch-row{display:flex;gap:8px}
.touch-btn{width:64px;height:64px;border-
radius:12px;background:rgba(255,255,255,0.03);display:flex;align-
items:center;justify-content:center;font-weight:700;color:#fff;user-
select:none;touch-action:none}
input[type=range]{width:100%}
select{width:100%;padding:8px;border-
radius:8px;background:transparent;color:#cfe7d3;border:1px solid
rgba(255,255,255,0.04)}
@media (max-width:760px){
.wrap{padding:8px}
.panel{width:94vw}
canvas{width:92vw;height:92vw;max-width:640px;max-height:640px}
.meta{width:94vw}
.touch-area{display:flex}
}
</style>
</head>
<body>
<div class="wrap">
<div class="panel" aria-label="game panel">
<canvas id="game" width="600" height="600"></canvas>
<div class="hud" role="status" aria-live="polite">
<div class="chip">Score <strong id="score">0</strong></div>
<div class="chip">High <strong id="highscore">0</strong></div>
<div class="freeze">Freeze <strong id="freezeTimer">0.0</strong>s</div>
<div style="width:8px"></div>
<button id="pauseBtn" class="btn">Pause (P)</button>
<button id="restartBtn" class="btn">Restart</button>
</div>
<div class="touch-area" id="touchControls" aria-hidden="true">
<div class="touch-row" style="justify-content:center">
<div class="touch-btn" data-dir="up" aria-label="up">▲</div>
</div>
<div class="touch-row">
<div class="touch-btn" data-dir="left" aria-label="left">◀</div>
<div class="touch-btn" data-dir="down" aria-label="down">▼</div>
<div class="touch-btn" data-dir="right" aria-label="right">▶</div>
</div>
</div>
</div>
<div class="meta">
<div class="tip">
Controls: Arrow keys / WASD on keyboard. On phone use the on-screen
buttons. Eat green apples for points. Eat purple flasks to get <strong>Growth
Freeze</strong> (5s) — you still get points but the snake won't grow.
</div>
<div>
<label style="font-size:13px;color:var(--muted)">Speed</label>
<input id="speedRange" type="range" min="6" max="20" value="12" />
</div>
<div>
<label style="font-size:13px;color:var(--muted)">Mode</label>
<select id="modeSelect">
<option value="classic">Classic (walls kill)</option>
<option value="wrap">Wrap-around</option>
</select>
</div>
<div style="display:flex;gap:8px">
<button id="saveBtn" class="btn">Save Highscore</button>
<button id="clearBtn" class="btn">Clear Highscore</button>
</div>
<div style="font-size:13px;color:var(--muted)">
Tip: rotate to landscape for a wider play area. If controls don’t appear,
reload the page.
</div>
</div>
</div>
<script>
(function(){
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d', { alpha: false });
const scoreEl = document.getElementById('score');
const highEl = document.getElementById('highscore');
const freezeEl = document.getElementById('freezeTimer');
const pauseBtn = document.getElementById('pauseBtn');
const restartBtn = document.getElementById('restartBtn');
const speedRange = document.getElementById('speedRange');
const modeSelect = document.getElementById('modeSelect');
const touchControls = document.getElementById('touchControls');
const saveBtn = document.getElementById('saveBtn');
const clearBtn = document.getElementById('clearBtn');
// grid
const baseGrid = 20;
const baseSize = 600;
const cols = baseSize / baseGrid;
const rows = baseSize / baseGrid;
// game state
let snake, dir, nextDir, food, freezePower;
let score = 0;
let highscore = parseInt(localStorage.getItem('snake_high') || '0', 10) || 0;
let freezeActive = false;
let freezeRemaining = 0.0; // seconds
let running = true;
let speed = parseInt(speedRange.value, 10);
let mode = modeSelect.value;
let tickTimer = null;
highEl.textContent = highscore;
// helper
const randCell = () => ({ x: Math.floor(Math.random() * cols), y:
Math.floor(Math.random() * rows) });
function newGame(){
const cx = Math.floor(cols/2);
const cy = Math.floor(rows/2);
snake = [
{x: cx+1, y: cy},
{x: cx, y: cy},
{x: cx-1, y: cy}
];
dir = {x:1,y:0};
nextDir = {x:1,y:0};
score = 0;
freezeActive = false;
freezeRemaining = 0;
placeFood();
placeFreeze();
running = true;
updateUI();
}
function placeFood(){
let c;
while(true){
c = randCell();
if (!snake.some(s => s.x===c.x && s.y===c.y) && !(freezePower &&
freezePower.x===c.x && freezePower.y===c.y)) break;
}
food = c;
}
function placeFreeze(){
// spawn freeze with modest chance (20%)
if (Math.random() < 0.2){
let c;
while(true){
c = randCell();
if (!snake.some(s => s.x===c.x && s.y===c.y) && !(food && food.x===c.x &&
food.y===c.y)) break;
}
freezePower = c;
} else {
freezePower = null;
}
}
function updateUI(){
scoreEl.textContent = score;
highEl.textContent = highscore;
freezeEl.textContent = freezeRemaining > 0 ? (freezeRemaining < 1 ?
freezeRemaining.toFixed(1) : freezeRemaining.toFixed(1)) : '0.0';
}
function gameOver(){
running = false;
clearInterval(tickTimer);
tickTimer = null;
if (score > highscore){
highscore = score;
localStorage.setItem('snake_high', String(highscore));
}
updateUI();
// draw overlay
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, canvas.height/2 - 24, canvas.width, 48);
ctx.fillStyle = '#fff';
ctx.font = '20px system-ui, Arial';
ctx.textAlign = 'center';
ctx.fillText('Game Over — Press Restart', canvas.width/2, canvas.height/2 +
6);
}
function tick(){
if (!running) return;
// ms for this tick (derived from speed)
const ms = Math.round(1000 / speed);
const dt = ms / 1000;
// apply queued direction
dir = nextDir;
const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
if (mode === 'wrap'){
if (head.x < 0) head.x = cols - 1;
if (head.x >= cols) head.x = 0;
if (head.y < 0) head.y = rows - 1;
if (head.y >= rows) head.y = 0;
} else {
if (head.x < 0 || head.x >= cols || head.y < 0 || head.y >= rows){
return gameOver();
}
}
// self collision
if (snake.some(s => s.x===head.x && s.y===head.y)) return gameOver();
// move
snake.unshift(head);
// eat food?
if (head.x === food.x && head.y === food.y){
score += 1;
placeFood();
placeFreeze();
if (freezeActive){
// negate growth by removing tail immediately (so length stays same)
snake.pop();
} else {
// grow naturally (we already unshifted head and do not pop)
}
} else if (freezePower && head.x === freezePower.x && head.y ===
freezePower.y){
// collect freeze
freezeActive = true;
freezeRemaining = 5.0;
freezePower = null;
} else {
// normal move (no growth)
snake.pop();
}
// decrement freeze timer
if (freezeActive){
freezeRemaining -= dt;
if (freezeRemaining <= 0){
freezeActive = false;
freezeRemaining = 0;
}
}
draw();
updateUI();
}
function draw(){
// background
ctx.fillStyle = '#061226';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// subtle grid lines (optional)
ctx.strokeStyle = 'rgba(255,255,255,0.02)';
ctx.lineWidth = 1;
for (let x=0;x<=cols;x++){
ctx.beginPath();
ctx.moveTo(x*baseGrid+0.5,0);
ctx.lineTo(x*baseGrid+0.5,canvas.height);
ctx.stroke();
}
for (let y=0;y<=rows;y++){
ctx.beginPath();
ctx.moveTo(0,y*baseGrid+0.5);
ctx.lineTo(canvas.width,y*baseGrid+0.5);
ctx.stroke();
}
// food (green)
ctx.fillStyle = '#22c55e';
roundRect(food.x*baseGrid + 3, food.y*baseGrid + 3, baseGrid - 6, baseGrid -
6, 6);
// freeze power (purple glow)
if (freezePower){
ctx.save();
ctx.fillStyle = '#9b59b6';
ctx.shadowColor = 'rgba(155,89,182,0.9)';
ctx.shadowBlur = 12;
roundRect(freezePower.x*baseGrid + 3, freezePower.y*baseGrid + 3, baseGrid
- 6, baseGrid - 6, 6);
ctx.restore();
}
// snake
for (let i=snake.length-1;i>=0;i--){
const s = snake[i];
const x = s.x * baseGrid;
const y = s.y * baseGrid;
if (i === 0){
// head gradient
const g = ctx.createLinearGradient(x,y,x+baseGrid,y+baseGrid);
g.addColorStop(0,'#b7f5c7');
g.addColorStop(1,'#22c55e');
ctx.fillStyle = g;
roundRect(x+2,y+2,baseGrid-4,baseGrid-4,6);
} else {
ctx.fillStyle = '#2aa26b';
roundRect(x+3,y+3,baseGrid-6,baseGrid-6,5);
}
}
}
function roundRect(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();
ctx.fill();
}
// Input handling
function trySetDir(nx, ny){
// prevent reverse
if (dir.x === -nx && dir.y === -ny) return;
nextDir = {x:nx,y:ny};
}
window.addEventListener('keydown', function(e){
const k = e.key.toLowerCase();
// prevent default for arrow keys to avoid scrolling
if
(["arrowup","arrowdown","arrowleft","arrowright"].includes(e.key.toLowerCase()))
e.preventDefault();
if (k === 'arrowup' || k === 'w') trySetDir(0,-1);
if (k === 'arrowdown' || k === 's') trySetDir(0,1);
if (k === 'arrowleft' || k === 'a') trySetDir(-1,0);
if (k === 'arrowright' || k === 'd') trySetDir(1,0);
if (k === 'p') togglePause();
if (k === 'r') restart();
}, {passive:false});
// Touch controls (buttons)
touchControls.querySelectorAll('.touch-btn').forEach(b=>{
b.addEventListener('touchstart', e=>{
e.preventDefault();
const d = b.dataset.dir;
if (d === 'up') trySetDir(0,-1);
if (d === 'down') trySetDir(0,1);
if (d === 'left') trySetDir(-1,0);
if (d === 'right') trySetDir(1,0);
}, {passive:false});
});
// Pause & restart
function togglePause(){
running = !running;
if (running && !tickTimer) startLoop();
if (!running && tickTimer) {
clearInterval(tickTimer);
tickTimer = null;
}
pauseBtn.textContent = running ? 'Pause (P)' : 'Resume (P)';
}
function restart(){
clearInterval(tickTimer);
tickTimer = null;
newGame();
draw();
startLoop();
}
pauseBtn.addEventListener('click', togglePause);
restartBtn.addEventListener('click', restart);
// Speed / Mode
speedRange.addEventListener('input', ()=>{
speed = parseInt(speedRange.value,10);
if (tickTimer){
clearInterval(tickTimer);
startLoop();
}
});
modeSelect.addEventListener('change', ()=> mode = modeSelect.value );
// Highscore buttons
saveBtn.addEventListener('click', ()=>{
if (score > highscore){
highscore = score;
localStorage.setItem('snake_high', String(highscore));
updateUI();
alert('Highscore saved!');
} else {
alert('Score is not higher than current highscore.');
}
});
clearBtn.addEventListener('click', ()=>{
if (confirm('Clear saved highscore?')){
localStorage.removeItem('snake_high');
highscore = 0;
updateUI();
}
});
// Start loop
function startLoop(){
if (tickTimer) clearInterval(tickTimer);
const ms = Math.round(1000 / speed);
tickTimer = setInterval(tick, ms);
}
// Show touch controls on small screens
function refreshTouchVisible(){
const small = window.matchMedia('(max-width:760px)').matches;
touchControls.style.display = small ? 'flex' : 'none';
}
refreshTouchVisible();
window.addEventListener('resize', refreshTouchVisible);
// Initialization
newGame();
draw();
startLoop();
// Prevent overscroll on canvas for iOS when touching controls
['touchstart','touchmove'].forEach(ev=>{
canvas.addEventListener(ev, e => e.preventDefault(), {passive:false});
});
})();
</script>
</body>
</html>