8000 Create a new game by shanekoster65 · Pull Request #22 · Shopify/.github · GitHub
[go: up one dir, main page]

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions game/game.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
(() => {
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const restartBtn = document.getElementById('restartBtn');

const GRID = 24; // 24x24 cells
const CELL = canvas.width / GRID;
const BASE_SPEED_MS = 120;

const DIR = {
UP: { x: 0, y: -1 },
DOWN: { x: 0, y: 1 },
LEFT: { x: -1, y: 0 },
RIGHT: { x: 1, y: 0 },
};

let state = {
snake: [{ x: 8, y: 12 }],
dir: DIR.RIGHT,
pendingDir: DIR.RIGHT,
food: spawnFood([{ x: 8, y: 12 }]),
score: 0,
best: Number(localStorage.getItem('snake_best') || '0'),
running: false,
gameOver: false,
tickMs: BASE_SPEED_MS,
lastTickAt: 0,
};

bestEl.textContent = String(state.best);

function spawnFood(occupied) {
while (true) {
const x = Math.floor(Math.random() * GRID);
const y = Math.floor(Math.random() * GRID);
if (!occupied.some(p => p.x === x && p.y === y)) return { x, y };
}
}

function reset() {
state.snake = [{ x: 8, y: 12 }];
state.dir = DIR.RIGHT;
state.pendingDir = DIR.RIGHT;
state.food = spawnFood(state.snake);
state.score = 0;
state.running = false;
state.gameOver = false;
state.tickMs = BASE_SPEED_MS;
scoreEl.textContent = '0';
draw();
}

function start() {
if (state.gameOver) reset();
state.running = true;
}

function pause() { state.running = false; }

function restart() { reset(); start(); }

startBtn.addEventListener('click', start);
pauseBtn.addEventListener('click', pause);
restartBtn.addEventListener('click', restart);

window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (key === 'p') { state.running = !state.running; return; }
if (key === ' ' || key === 'enter') { if (!state.running) start(); return; }
const map = {
arrowup: DIR.UP, w: DIR.UP,
arrowdown: DIR.DOWN, s: DIR.DOWN,
arrowleft: DIR.LEFT, a: DIR.LEFT,
arrowright: DIR.RIGHT, d: DIR.RIGHT,
};
const next = map[key];
if (!next) return;
// Prevent reversing directly
const isOpposite = (a, b) => a.x + b.x === 0 && a.y + b.y === 0;
if (!isOpposite(next, state.dir)) state.pendingDir = next;
});

function tick(now) {
if (!state.lastTickAt) state.lastTickAt = now;
const elapsed = now - state.lastTickAt;
if (state.running && elapsed >= state.tickMs) {
state.lastTickAt = now;
advance();
}
draw();
requestAnimationFrame(tick);
}

function advance() {
state.dir = state.pendingDir;
const head = state.snake[0];
const next = { x: head.x + state.dir.x, y: head.y + state.dir.y };

// Wrap around edges
next.x = (next.x + GRID) % GRID;
next.y = (next.y + GRID) % GRID;

// Self collision
if (state.snake.some((p, i) => i !== 0 && p.x === next.x && p.y === next.y)) {
state.running = false; state.gameOver = true; return;
}

// Move
state.snake.unshift(next);

// Eat
if (next.x === state.food.x && next.y === state.food.y) {
state.score += 1;
scoreEl.textContent = String(state.score);
state.food = spawnFood(state.snake);
if (state.score % 5 === 0 && state.tickMs > 60) state.tickMs -= 5;
} else {
state.snake.pop();
}

// Best
if (state.score > state.best) {
state.best = state.score;
localStorage.setItem('snake_best', String(state.best));
bestEl.textContent = String(state.best);
}
}

function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

// Grid backdrop
ctx.fillStyle = '#0b1a2b';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
for (let i = 0; i <= GRID; i++) {
ctx.beginPath();
ctx.moveTo(i * CELL, 0); ctx.lineTo(i * CELL, canvas.height); ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * CELL); ctx.lineTo(canvas.width, i * CELL); ctx.stroke();
}

// Food
drawCell(state.food.x, state.food.y, '#ff5d73');

// Snake
state.snake.forEach((p, idx) => {
const tone = 180 + Math.min(60, idx * 2);
drawCell(p.x, p.y, `hsl(${tone} 90% 60%)`);
});

if (!state.running) {
ctx.fillStyle = 'rgba(5,11,18,0.6)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#cfe6ff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 28px Inter, system-ui';
ctx.fillText(state.gameOver ? 'Game Over' : 'Paused', canvas.width/2, canvas.height/2 - 14);
ctx.font = '14px Inter, system-ui';
ctx.fillText('Press Start or Space to play', canvas.width/2, canvas.height/2 + 14);
}
}

function drawCell(x, y, color) {
const px = x * CELL; const py = y * CELL;
const r = 6;
ctx.fillStyle = color;
roundRect(ctx, px + 1, py + 1, CELL - 2, CELL - 2, r);
ctx.fill();
}

function roundRect(ctx, 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();
}

reset();
requestAnimationFrame(tick);
})();
28 changes: 28 additions & 0 deletions game/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Snake</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main class="container">
<h1>Snake</h1>
<canvas id="board" width="480" height="480"></canvas>
<div class="hud">
<div class="score">Score: <span id="score">0</span> · Best: <span id="best">0</span></div>
<div class="controls">
<button id="startBtn">Start</button>
<button id="pauseBtn">Pause</button>
<button id="restartBtn">Restart</button>
</div>
<p class="hint">Use arrow keys or WASD. P to pause.</p>
</div>
</main>
<script src="./game.js"></script>
</body>
</html>
23 changes: 23 additions & 0 deletions game/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
* { box-sizing: border-box; }
:root { color-scheme: dark light; }
html, body { height: 100%; }
body {
margin: 0;
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, "Apple Color Emoji", "Segoe UI Emoji";
display: grid; place-items: center;
background: radial-gradient(1000px 600px at 20% 0%, #0e1f2f 0%, #0a1623 40%, #050b12 100%);
color: #e6edf3;
}
.container { width: min(92vw, 640px); padding: 24px 16px 40px; }
h1 { margin: 8px 0 16px; font-weight: 800; letter-spacing: 0.3px; }
canvas { width: 100%; height: auto; image-rendering: pixelated; background: #081320; border-radius: 12px; border: 1px solid #0b2136; box-shadow: 0 20px 60px rgba(0,0,0,0.45), inset 0 0 0 1px rgba(255,255,255,0.03); }
.hud { display: grid; gap: 12px; margin-top: 16px; }
.score { font-weight: 600; color: #9cc4ff; }
.controls { display: flex; gap: 8px; }
button {
appearance: none; border: 1px solid #21476f; background: linear-gradient(180deg, #15314d, #0f2337);
color: #cfe6ff; padding: 8px 12px; border-radius: 8px; font-weight: 600; cursor: pointer;
}
button:hover { filter: brightness(1.1); }
button:active { transform: translateY(1px); }
.hint { color: #7a8aa6; margin: 8px 0 0; }
0