This commit is contained in:
parent
6f258bf13d
commit
c3a43148c0
|
|
@ -28,6 +28,7 @@
|
||||||
<li><a href="/scorecard-4p/">Generic Scorecard - 4 Players</a></li>
|
<li><a href="/scorecard-4p/">Generic Scorecard - 4 Players</a></li>
|
||||||
<li><a href="/mahjong/">Mahjong Scorecard</a></li>
|
<li><a href="/mahjong/">Mahjong Scorecard</a></li>
|
||||||
<li><a href="/finska/">Finska Scorecard</a></li>
|
<li><a href="/finska/">Finska Scorecard</a></li>
|
||||||
|
<li><a href="/neon-snake/">Neon Snake</a>: vibe-coded by Google Gemini</li>
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
441
site/neon-snake/index.html
Normal file
441
site/neon-snake/index.html
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>Neon Snake</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--neon-green: #39ff14;
|
||||||
|
--neon-red: #ff0055;
|
||||||
|
--neon-blue: #00ffff;
|
||||||
|
--bg-color: #050510;
|
||||||
|
--grid-line: rgba(0, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--neon-green);
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CRT Scanline Effect Overlay */
|
||||||
|
.scanlines {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(255,255,255,0),
|
||||||
|
rgba(255,255,255,0) 50%,
|
||||||
|
rgba(0,0,0,0.1) 50%,
|
||||||
|
rgba(0,0,0,0.1)
|
||||||
|
);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container for the game */
|
||||||
|
#game-container {
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 0 20px rgba(57, 255, 20, 0.2);
|
||||||
|
border: 2px solid var(--neon-green);
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(var(--grid-line) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui-layer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-shadow: 0 0 5px var(--neon-green);
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#start-screen, #game-over-screen {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(5, 5, 16, 0.9);
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--neon-green);
|
||||||
|
box-shadow: 0 0 15px var(--neon-green);
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 3rem;
|
||||||
|
color: var(--neon-blue);
|
||||||
|
text-shadow: 0 0 10px var(--neon-blue);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--neon-green);
|
||||||
|
border: 2px solid var(--neon-green);
|
||||||
|
padding: 10px 30px;
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
box-shadow: 0 0 10px var(--neon-green);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: var(--neon-green);
|
||||||
|
color: black;
|
||||||
|
box-shadow: 0 0 20px var(--neon-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile specific controls hint */
|
||||||
|
.controls-hint {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #888;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="scanlines"></div>
|
||||||
|
|
||||||
|
<div id="game-container">
|
||||||
|
<canvas id="gameCanvas" width="400" height="400"></canvas>
|
||||||
|
|
||||||
|
<div id="ui-layer">
|
||||||
|
<div class="header">
|
||||||
|
<span id="score-display">SCORE: 000</span>
|
||||||
|
<span id="highscore-display">HI: 000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="start-screen">
|
||||||
|
<h1>NEON SNAKE</h1>
|
||||||
|
<p>USE ARROW KEYS OR SWIPE</p>
|
||||||
|
<button onclick="startGame()">INITIALIZE</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="game-over-screen" class="hidden">
|
||||||
|
<h1 style="color: var(--neon-red); text-shadow: 0 0 10px var(--neon-red);">SYSTEM FAILURE</h1>
|
||||||
|
<p>FINAL SCORE: <span id="final-score">0</span></p>
|
||||||
|
<button onclick="startGame()">REBOOT</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls-hint">Desktop: Arrows/WASD | Mobile: Swipe</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const scoreEl = document.getElementById('score-display');
|
||||||
|
const highScoreEl = document.getElementById('highscore-display');
|
||||||
|
const startScreen = document.getElementById('start-screen');
|
||||||
|
const gameOverScreen = document.getElementById('game-over-screen');
|
||||||
|
const finalScoreEl = document.getElementById('final-score');
|
||||||
|
|
||||||
|
// Game Configuration
|
||||||
|
let tileCount = 20; // 20x20 grid
|
||||||
|
let tileSize = canvas.width / tileCount - 2; // -2 for gap
|
||||||
|
let gameSpeed = 100; // ms per frame
|
||||||
|
|
||||||
|
// Game State
|
||||||
|
let score = 0;
|
||||||
|
let highScore = localStorage.getItem('snake-highscore') || 0;
|
||||||
|
let snake = [];
|
||||||
|
let food = { x: 15, y: 15 };
|
||||||
|
let dx = 0;
|
||||||
|
let dy = 0;
|
||||||
|
let gameInterval;
|
||||||
|
let isGameRunning = false;
|
||||||
|
let lastInputTime = 0;
|
||||||
|
|
||||||
|
// Initialize display
|
||||||
|
highScoreEl.innerText = `HI: ${String(highScore).padStart(3, '0')}`;
|
||||||
|
resizeCanvas();
|
||||||
|
|
||||||
|
// Responsive Canvas
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
// Constrain canvas to window size for mobile, max 600px
|
||||||
|
let size = Math.min(window.innerWidth - 40, window.innerHeight - 100, 600);
|
||||||
|
// Ensure size is a multiple of tileCount for clean rendering
|
||||||
|
size = Math.floor(size / tileCount) * tileCount;
|
||||||
|
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
tileSize = size / tileCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
startScreen.classList.add('hidden');
|
||||||
|
gameOverScreen.classList.add('hidden');
|
||||||
|
|
||||||
|
// Reset State
|
||||||
|
snake = [{x: 10, y: 10}, {x: 9, y: 10}, {x: 8, y: 10}];
|
||||||
|
score = 0;
|
||||||
|
dx = 1; // Start moving right
|
||||||
|
dy = 0;
|
||||||
|
updateScore(0);
|
||||||
|
placeFood();
|
||||||
|
|
||||||
|
if (gameInterval) clearInterval(gameInterval);
|
||||||
|
isGameRunning = true;
|
||||||
|
gameInterval = setInterval(gameLoop, gameSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gameLoop() {
|
||||||
|
if (!isGameRunning) return;
|
||||||
|
|
||||||
|
// Move Snake
|
||||||
|
const head = { x: snake[0].x + dx, y: snake[0].y + dy };
|
||||||
|
|
||||||
|
// Wall Collision Check (Wrap around logic can be used, but let's do hard walls for retro feel)
|
||||||
|
if (head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount) {
|
||||||
|
gameOver();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self Collision Check
|
||||||
|
for (let i = 0; i < snake.length; i++) {
|
||||||
|
if (head.x === snake[i].x && head.y === snake[i].y) {
|
||||||
|
gameOver();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snake.unshift(head);
|
||||||
|
|
||||||
|
// Eat Food
|
||||||
|
if (head.x === food.x && head.y === food.y) {
|
||||||
|
score += 10;
|
||||||
|
updateScore(score);
|
||||||
|
// Speed up slightly every 50 points
|
||||||
|
if (score % 50 === 0 && gameSpeed > 50) {
|
||||||
|
clearInterval(gameInterval);
|
||||||
|
gameSpeed -= 5;
|
||||||
|
gameInterval = setInterval(gameLoop, gameSpeed);
|
||||||
|
}
|
||||||
|
placeFood();
|
||||||
|
} else {
|
||||||
|
snake.pop(); // Remove tail
|
||||||
|
}
|
||||||
|
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
// Clear screen
|
||||||
|
ctx.fillStyle = '#000000'; // Pure black for high contrast
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw Food (Glitchy Red Square)
|
||||||
|
ctx.shadowBlur = 15;
|
||||||
|
ctx.shadowColor = "#ff0055";
|
||||||
|
ctx.fillStyle = "#ff0055";
|
||||||
|
ctx.fillRect(food.x * tileSize + 1, food.y * tileSize + 1, tileSize - 2, tileSize - 2);
|
||||||
|
|
||||||
|
// Draw Snake
|
||||||
|
snake.forEach((part, index) => {
|
||||||
|
// Head is brighter, tail fades slightly
|
||||||
|
if (index === 0) {
|
||||||
|
ctx.fillStyle = "#ffffff"; // White hot head
|
||||||
|
ctx.shadowBlur = 20;
|
||||||
|
ctx.shadowColor = "#39ff14";
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = "#39ff14";
|
||||||
|
ctx.shadowBlur = 10 - Math.min(index, 8); // Fading glow
|
||||||
|
ctx.shadowColor = "#39ff14";
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillRect(part.x * tileSize + 1, part.y * tileSize + 1, tileSize - 2, tileSize - 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset shadow for next frame performance
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeFood() {
|
||||||
|
// Random position not on snake
|
||||||
|
let valid = false;
|
||||||
|
while (!valid) {
|
||||||
|
food.x = Math.floor(Math.random() * tileCount);
|
||||||
|
food.y = Math.floor(Math.random() * tileCount);
|
||||||
|
|
||||||
|
valid = true;
|
||||||
|
for (let part of snake) {
|
||||||
|
if (part.x === food.x && part.y === food.y) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateScore(val) {
|
||||||
|
scoreEl.innerText = `SCORE: ${String(val).padStart(3, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gameOver() {
|
||||||
|
isGameRunning = false;
|
||||||
|
clearInterval(gameInterval);
|
||||||
|
|
||||||
|
if (score > highScore) {
|
||||||
|
highScore = score;
|
||||||
|
localStorage.setItem('snake-highscore', highScore);
|
||||||
|
highScoreEl.innerText = `HI: ${String(highScore).padStart(3, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalScoreEl.innerText = score;
|
||||||
|
gameOverScreen.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input Handling
|
||||||
|
document.addEventListener('keydown', changeDirection);
|
||||||
|
|
||||||
|
function changeDirection(event) {
|
||||||
|
const LEFT_KEY = 37;
|
||||||
|
const RIGHT_KEY = 39;
|
||||||
|
const UP_KEY = 38;
|
||||||
|
const DOWN_KEY = 40;
|
||||||
|
const W_KEY = 87;
|
||||||
|
const A_KEY = 65;
|
||||||
|
const S_KEY = 83;
|
||||||
|
const D_KEY = 68;
|
||||||
|
const I_KEY = 73;
|
||||||
|
const J_KEY = 74;
|
||||||
|
const K_KEY = 75;
|
||||||
|
const L_KEY = 76;
|
||||||
|
|
||||||
|
// Prevent reversing direction immediately
|
||||||
|
const goingUp = dy === -1;
|
||||||
|
const goingDown = dy === 1;
|
||||||
|
const goingRight = dx === 1;
|
||||||
|
const goingLeft = dx === -1;
|
||||||
|
|
||||||
|
const keyPressed = event.keyCode;
|
||||||
|
|
||||||
|
// Prevent default scrolling
|
||||||
|
if([37, 38, 39, 40].indexOf(keyPressed) > -1) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input buffering (simple check to prevent double turn in one frame)
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastInputTime < 50) return;
|
||||||
|
lastInputTime = now;
|
||||||
|
|
||||||
|
if ((keyPressed === LEFT_KEY || keyPressed === A_KEY || keyPressed == J_KEY) && !goingRight) {
|
||||||
|
dx = -1; dy = 0;
|
||||||
|
}
|
||||||
|
if ((keyPressed === UP_KEY || keyPressed === W_KEY || keyPressed == I_KEY) && !goingDown) {
|
||||||
|
dx = 0; dy = -1;
|
||||||
|
}
|
||||||
|
if ((keyPressed === RIGHT_KEY || keyPressed === D_KEY || keyPressed == L_KEY) && !goingLeft) {
|
||||||
|
dx = 1; dy = 0;
|
||||||
|
}
|
||||||
|
if ((keyPressed === DOWN_KEY || keyPressed === S_KEY || keyPressed == K_KEY) && !goingUp) {
|
||||||
|
dx = 0; dy = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch Handling (Swipe)
|
||||||
|
let touchStartX = 0;
|
||||||
|
let touchStartY = 0;
|
||||||
|
|
||||||
|
document.addEventListener('touchstart', function(event) {
|
||||||
|
touchStartX = event.changedTouches[0].screenX;
|
||||||
|
touchStartY = event.changedTouches[0].screenY;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', function(event) {
|
||||||
|
// Prevent scrolling while playing
|
||||||
|
if(isGameRunning) event.preventDefault();
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
document.addEventListener('touchend', function(event) {
|
||||||
|
let touchEndX = event.changedTouches[0].screenX;
|
||||||
|
let touchEndY = event.changedTouches[0].screenY;
|
||||||
|
handleSwipe(touchStartX, touchStartY, touchEndX, touchEndY);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
function handleSwipe(startX, startY, endX, endY) {
|
||||||
|
let diffX = endX - startX;
|
||||||
|
let diffY = endY - startY;
|
||||||
|
|
||||||
|
// Threshold to count as a swipe
|
||||||
|
if (Math.abs(diffX) < 30 && Math.abs(diffY) < 30) return;
|
||||||
|
|
||||||
|
const goingUp = dy === -1;
|
||||||
|
const goingDown = dy === 1;
|
||||||
|
const goingRight = dx === 1;
|
||||||
|
const goingLeft = dx === -1;
|
||||||
|
|
||||||
|
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||||||
|
// Horizontal Swipe
|
||||||
|
if (diffX > 0 && !goingLeft) { dx = 1; dy = 0; }
|
||||||
|
if (diffX < 0 && !goingRight) { dx = -1; dy = 0; }
|
||||||
|
} else {
|
||||||
|
// Vertical Swipe
|
||||||
|
if (diffY > 0 && !goingUp) { dx = 0; dy = 1; }
|
||||||
|
if (diffY < 0 && !goingDown) { dx = 0; dy = -1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial draw
|
||||||
|
draw();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue