add page for in-browser Snake game

Hunter
Wednesday, July 23rd, 2025 at 4:45 PM

pages/snake/index.htmlnew

pages/snake/index.html
  1 +<html>
  2 +<head>
  3 +	<title>SNAKE</title>
  4 +	<style>
  5 +		body {
  6 +			background-color: white;
  7 +			font-family: Arial, sans-serif;
  8 +			text-align: center;
  9 +			margin: 0;
 10 +			padding: 20px;
 11 +		}
 12 +		
 13 +		h1 {
 14 +			margin: 20px 0;
 15 +		}
 16 +		
 17 +		#gameBoard {
 18 +			display: inline-block;
 19 +			border: 2px solid #333;
 20 +			background-color: white;
 21 +			font-size: 16px;
 22 +			line-height: 1;
 23 +			font-family: monospace;
 24 +		}
 25 +		
 26 +		.row {
 27 +			display: block;
 28 +			margin: 0;
 29 +			padding: 0;
 30 +		}
 31 +		
 32 +		#score, #highScore {
 33 +			font-size: 20px;
 34 +			margin: 10px 0;
 35 +		}
 36 +		
 37 +		#gameOver, #startHint {
 38 +			font-size: 24px;
 39 +			color: red;
 40 +			margin: 20px 0;
 41 +		}
 42 +		
 43 +		#startHint {
 44 +			color: #333;
 45 +		}
 46 +		
 47 +		#mobileControls {
 48 +			display: none;
 49 +			margin: 20px 0;
 50 +		}
 51 +		
 52 +		.control-pad {
 53 +			display: inline-block;
 54 +			position: relative;
 55 +			width: 144px;
 56 +			height: 144px;
 57 +		}
 58 +		
 59 +		.control-btn {
 60 +			position: absolute;
 61 +			width: 48px;
 62 +			height: 48px;
 63 +			font-size: 48px;
 64 +			cursor: pointer;
 65 +			user-select: none;
 66 +			touch-action: manipulation;
 67 +			line-height: 48px;
 68 +			text-align: center;
 69 +			display: flex;
 70 +			align-items: center;
 71 +			justify-content: center;
 72 +		}
 73 +		
 74 +		.control-btn:active {
 75 +			opacity: 0.6;
 76 +		}
 77 +		
 78 +		#upBtn { top: 0; left: 48px; }
 79 +		#downBtn { bottom: 0; left: 48px; }
 80 +		#leftBtn { top: 48px; left: 0; }
 81 +		#rightBtn { top: 48px; right: 0; }
 82 +		#centerBtn { top: 48px; left: 48px; display: none; }
 83 +		
 84 +		#mobileGameOver, #mobileStartHint {
 85 +			font-size: 20px;
 86 +			color: red;
 87 +			margin-top: 20px;
 88 +			display: none;
 89 +		}
 90 +		
 91 +		#mobileStartHint {
 92 +			color: #333;
 93 +		}
 94 +		
 95 +		@media (pointer: none), (pointer: coarse) {
 96 +			#mobileControls {
 97 +				display: block;
 98 +			}
 99 +			
100 +			#gameOver {
101 +				display: none !important;
102 +			}
103 +			
104 +			#startHint {
105 +				display: none !important;
106 +			}
107 +		}
108 +	</style>
109 +</head>
110 +<body>
111 +	<h1>SNAKE</h1>
112 +	<div id="gameBoard"></div>
113 +	<div id="score">Score: 0</div>
114 +	<div id="highScore">High Score: 0</div>
115 +	<div id="gameOver"></div>
116 +	<div id="startHint">Press Space to start!</div>
117 +	
118 +	<div id="mobileControls">
119 +		<div class="control-pad">
120 +			<div class="control-btn" id="upBtn">🔼</div>
121 +			<div class="control-btn" id="leftBtn">◀️</div>
122 +			<div class="control-btn" id="rightBtn">▶️</div>
123 +			<div class="control-btn" id="downBtn">🔽</div>
124 +			<div class="control-btn" id="centerBtn">❇️</div>
125 +		</div>
126 +		<div id="mobileGameOver"></div>
127 +		<div id="mobileStartHint">Press ❇️ to start!</div>
128 +	</div>
129 +
130 +	<script>
131 +		const BOARD_SIZE = 20;
132 +		const EMPTY = '⬜';
133 +		const SNAKE = '🟩';
134 +		const APPLE = '🟥';
135 +		
136 +		let board = [];
137 +		let snake = [{x: 10, y: 10}];
138 +		let direction = {x: 0, y: -1};
139 +		let nextDirection = {x: 0, y: -1};
140 +		let apple = {x: 5, y: 5};
141 +		let score = 0;
142 +		let highScore = 0;
143 +		let gameRunning = false;
144 +		let gameStarted = false;
145 +		let gameSpeed = 200;
146 +		let gameInterval;
147 +		
148 +		function initBoard() {
149 +			for (let y = 0; y < BOARD_SIZE; y++) {
150 +				board[y] = [];
151 +				for (let x = 0; x < BOARD_SIZE; x++) {
152 +					board[y][x] = EMPTY;
153 +				}
154 +			}
155 +		}
156 +		
157 +		function placeApple() {
158 +			let newApple;
159 +			do {
160 +				newApple = {
161 +					x: Math.floor(Math.random() * BOARD_SIZE),
162 +					y: Math.floor(Math.random() * BOARD_SIZE)
163 +				};
164 +			} while (snake.some(segment => segment.x === newApple.x && segment.y === newApple.y));
165 +			
166 +			apple = newApple;
167 +		}
168 +		
169 +		function updateBoard() {
170 +			// Clear board
171 +			for (let y = 0; y < BOARD_SIZE; y++) {
172 +				for (let x = 0; x < BOARD_SIZE; x++) {
173 +					board[y][x] = EMPTY;
174 +				}
175 +			}
176 +			
177 +			// Place snake
178 +			snake.forEach(segment => {
179 +				if (segment.x >= 0 && segment.x < BOARD_SIZE && segment.y >= 0 && segment.y < BOARD_SIZE) {
180 +					board[segment.y][segment.x] = SNAKE;
181 +				}
182 +			});
183 +			
184 +			// Place apple
185 +			board[apple.y][apple.x] = APPLE;
186 +		}
187 +		
188 +		function renderBoard() {
189 +			const gameBoard = document.getElementById('gameBoard');
190 +			gameBoard.innerHTML = '';
191 +			
192 +			for (let y = 0; y < BOARD_SIZE; y++) {
193 +				const row = document.createElement('div');
194 +				row.className = 'row';
195 +				for (let x = 0; x < BOARD_SIZE; x++) {
196 +					row.innerHTML += board[y][x];
197 +				}
198 +				gameBoard.appendChild(row);
199 +			}
200 +		}
201 +		
202 +		function moveSnake() {
203 +			if (!gameRunning) return;
204 +			
205 +			// Use the next direction for movement
206 +			direction = nextDirection;
207 +			
208 +			const head = {
209 +				x: snake[0].x + direction.x,
210 +				y: snake[0].y + direction.y
211 +			};
212 +			
213 +			// Check wall collision
214 +			if (head.x < 0 || head.x >= BOARD_SIZE || head.y < 0 || head.y >= BOARD_SIZE) {
215 +				gameOver();
216 +				return;
217 +			}
218 +			
219 +			// Check self collision
220 +			if (snake.some(segment => segment.x === head.x && segment.y === head.y)) {
221 +				gameOver();
222 +				return;
223 +			}
224 +			
225 +			snake.unshift(head);
226 +			
227 +			// Check apple collision
228 +			if (head.x === apple.x && head.y === apple.y) {
229 +				score += 10;
230 +				document.getElementById('score').textContent = `Score: ${score}`;
231 +				updateHighScore();
232 +				
233 +				// Increase speed
234 +				gameSpeed = Math.max(50, 200 - Math.floor(score / 1));
235 +				clearInterval(gameInterval);
236 +				gameInterval = setInterval(gameLoop, gameSpeed);
237 +				
238 +				placeApple();
239 +			} else {
240 +				snake.pop();
241 +			}
242 +		}
243 +		
244 +		function gameOver() {
245 +			gameRunning = false;
246 +			document.getElementById('gameOver').innerHTML = 'Game Over!<br>Press Space to restart';
247 +			
248 +			// Show restart button and mobile message
249 +			const centerBtn = document.getElementById('centerBtn');
250 +			const mobileGameOver = document.getElementById('mobileGameOver');
251 +			if (centerBtn) {
252 +				centerBtn.textContent = '🔄';
253 +				centerBtn.style.display = 'block';
254 +			}
255 +			if (mobileGameOver) {
256 +				mobileGameOver.innerHTML = 'Game Over!<br>Press 🔄 to restart';
257 +				mobileGameOver.style.display = 'block';
258 +			}
259 +			
260 +			clearInterval(gameInterval);
261 +		}
262 +		
263 +		function startGame() {
264 +			// Reset game state first
265 +			snake = [{x: 10, y: 10}];
266 +			direction = {x: 0, y: -1};
267 +			nextDirection = {x: 0, y: -1};
268 +			score = 0;
269 +			gameSpeed = 200;
270 +			document.getElementById('score').textContent = `Score: ${score}`;
271 +			
272 +			// Only place new apple if restarting (not first time)
273 +			if (score > 0 || gameStarted) {
274 +				placeApple();
275 +			}
276 +
277 +			gameRunning = true;
278 +			gameStarted = true;
279 +			
280 +			// Hide all hint messages
281 +			document.getElementById('startHint').style.display = 'none';
282 +			document.getElementById('mobileStartHint').style.display = 'none';
283 +			document.getElementById('gameOver').textContent = '';
284 +			
285 +			const mobileGameOver = document.getElementById('mobileGameOver');
286 +			if (mobileGameOver) {
287 +				mobileGameOver.style.display = 'none';
288 +			}
289 +			
290 +			// Hide center button
291 +			const centerBtn = document.getElementById('centerBtn');
292 +			if (centerBtn) {
293 +				centerBtn.style.display = 'none';
294 +			}
295 +			
296 +			updateBoard();
297 +			renderBoard();
298 +			
299 +			// Start game loop
300 +			clearInterval(gameInterval);
301 +			gameInterval = setInterval(gameLoop, gameSpeed);
302 +		}
303 +		
304 +		function loadHighScore() {
305 +			const saved = localStorage.getItem('snakeHighScore');
306 +			highScore = saved ? parseInt(saved) : 0;
307 +			document.getElementById('highScore').textContent = `High Score: ${highScore}`;
308 +		}
309 +		
310 +		function saveHighScore() {
311 +			localStorage.setItem('snakeHighScore', highScore.toString());
312 +		}
313 +		
314 +		function updateHighScore() {
315 +			if (score > highScore) {
316 +				highScore = score;
317 +				document.getElementById('highScore').textContent = `High Score: ${highScore}`;
318 +				saveHighScore();
319 +			}
320 +		}
321 +		
322 +		function gameLoop() {
323 +			moveSnake();
324 +			updateBoard();
325 +			renderBoard();
326 +		}
327 +		
328 +		function setDirection(newDirection) {
329 +			if (!gameRunning) return;
330 +			
331 +			// Allow any direction when snake is only 1 segment long
332 +			if (snake.length === 1) {
333 +				nextDirection = newDirection;
334 +				return;
335 +			}
336 +			
337 +			// Prevent reversing into self by checking current direction
338 +			if (newDirection.x !== 0 && direction.x !== -newDirection.x) {
339 +				nextDirection = newDirection;
340 +			} else if (newDirection.y !== 0 && direction.y !== -newDirection.y) {
341 +				nextDirection = newDirection;
342 +			}
343 +		}
344 +		
345 +		document.addEventListener('keydown', (event) => {
346 +			if (event.code === 'Space') {
347 +				if (!gameRunning) {
348 +					startGame();
349 +				}
350 +				return;
351 +			}
352 +			
353 +			if (!gameRunning) return;
354 +			
355 +			switch(event.code) {
356 +				case 'ArrowUp':
357 +					setDirection({x: 0, y: -1});
358 +					break;
359 +				case 'ArrowDown':
360 +					setDirection({x: 0, y: 1});
361 +					break;
362 +				case 'ArrowLeft':
363 +					setDirection({x: -1, y: 0});
364 +					break;
365 +				case 'ArrowRight':
366 +					setDirection({x: 1, y: 0});
367 +					break;
368 +			}
369 +		});
370 +		
371 +		// Mobile controls
372 +		document.getElementById('upBtn').addEventListener('click', () => {
373 +			if (gameRunning) setDirection({x: 0, y: -1});
374 +		});
375 +		document.getElementById('downBtn').addEventListener('click', () => {
376 +			if (gameRunning) setDirection({x: 0, y: 1});
377 +		});
378 +		document.getElementById('leftBtn').addEventListener('click', () => {
379 +			if (gameRunning) setDirection({x: -1, y: 0});
380 +		});
381 +		document.getElementById('rightBtn').addEventListener('click', () => {
382 +			if (gameRunning) setDirection({x: 1, y: 0});
383 +		});
384 +		document.getElementById('centerBtn').addEventListener('click', () => {
385 +			if (!gameRunning) {
386 +				startGame();
387 +			}
388 +		});
389 +		
390 +		// Initialize game
391 +		loadHighScore();
392 +		initBoard();
393 +		updateBoard();
394 +		renderBoard();
395 +		
396 +		// Show initial start hints and button
397 +		document.getElementById('startHint').style.display = 'block';
398 +		document.getElementById('mobileStartHint').style.display = 'block';
399 +		document.getElementById('centerBtn').style.display = 'block';
400 +		document.getElementById('centerBtn').textContent = '❇️';
401 +	</script>
402 +</body>
403 +</html>