Skip to content

Instantly share code, notes, and snippets.

@cfreshman
Created May 8, 2024 13:37
Show Gist options
  • Save cfreshman/715c15667a25849df5b6444db85de77b to your computer and use it in GitHub Desktop.
Save cfreshman/715c15667a25849df5b6444db85de77b to your computer and use it in GitHub Desktop.
the most basic web implementation of snake i could come up with. play at https://freshman.dev/snake_ultra_hydra.html (desktop only)
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>snake ultra hydra</title>
<link rel="icon" href="https://i.guim.co.uk/img/media/993cc4a2107b870f78d1228874906ad9646fb204/0_144_2160_1296/master/2160.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=708569290282f1d206da9d72d5c2046c">
<style>body{display:flex;flex-direction:column;margin:.5em;font-family:system-ui}</style>
<style>
#game-canvas {
image-rendering: pixelated;
/* 30rem by default, but shrink to window height w/ .5em margin */
width: min(30rem, calc(100vh - 1em));
height: min(30rem, calc(100vh - 1em));
}
</style>
<script>
// COMMON CODE
// truthy: interpret the value as a boolean
const truthy = x => Boolean(x)
// V2: a two-dimensional vector value
// - add: add to another vector
// - equals: check if this vector equals another vector (e.g. same position)
class V2 {
constructor(x_or_vector, y) {
let x, vector
if (typeof x_or_vector === 'number') {
x = x_or_vector
} else {
vector = x_or_vector
}
if (vector) {
// allow copying another vector
this.x = vector.x
this.y = vector.y
} else {
this.x = x
this.y = y
}
}
add(other) {
return new V2(this.x + other.x, this.y + other.y)
}
equals(other) {
return this.x === other.x && this.y === other.y
}
}
// ListNode: linked list implementation
// store reference to first node to access entire list
class ListNode {
constructor(value, next) {
this.value = value
this.next = next
}
map(func) {
let curr = this
const results = []
while (curr) {
results.push(func(curr.value))
curr = curr.next
}
return results
}
}
</script>
</head>
<body>
<div>
WASD or ARROW KEYS to move. any other key to reset
</div>
<canvas id="game-canvas"></canvas>
<div>
(DESKTOP ONLY)
</div>
<script>
// board size 20x20 pixels
const SIZE = 20
// cardinal directions the snake can move in
const DIRS = {
RIGHT: new V2(1, 0),
UP: new V2(0, -1),
LEFT: new V2(-1, 0),
DOWN: new V2(0, 1),
}
// set the canvas to SIZE width and height
const canvas = document.querySelector('#game-canvas')
canvas.width = canvas.height = SIZE
// get the canvas graphics context for drawing the game pixels
const ctx = canvas.getContext('2d')
// game colors
const COLORS = {
BACKGROUND: '#000000',
SNAKE: '#34eb7d',
APPLE: '#ff0000',
}
// Pixel: a drawable position
// - draw: draw a colored square at this position
// - randomize: move pixel to random position
class Pixel extends V2 {
constructor(x, y, color) {
super(x, y)
this.color = color
}
draw(ctx, color) {
ctx.fillStyle = color
ctx.fillRect(this.x, this.y, 1, 1)
return this
}
randomize() {
this.x = Math.floor(Math.random() * SIZE)
this.y = Math.floor(Math.random() * SIZE)
return this
}
}
// snake behaviors/functions
function snake_move(head, dir, do_grow=false) {
// moving the snake:
// body nodes don't actually change
// a new head is added to the front (in input direction)
// if the snake isn't growing, the last node is removed
const new_head = new ListNode(new Pixel(head.value.x + dir.x, head.value.y + dir.y, head.color), head)
if (!do_grow) {
let curr = head
while (curr.next.next) curr = curr.next
curr.next = undefined
}
return new_head
}
function snake_intersects(head, position) {
// true if any body parts are placed at the position
return head ? head.map(part => part.equals(position)).some(truthy) : false
}
// === GAME STATE ===
// start the snake heading towards the right
let head = new ListNode(new Pixel(SIZE/4, SIZE/2, COLORS.SNAKE))
let direction = DIRS.RIGHT
// how many new segments to grow
let growth = 2
// place the apple at a random position
const apple = new Pixel(0, 0, COLORS.APPLE).randomize()
// is the game over
let gameover = false
// last is used to avoid letting the snake do a 180
let last = direction
// === GAME LOOP ===
function loop() {
if (gameover) return
// we check against 'last' in the input handler to prevent a 180
// 'last' allows us to update direction immediately but always check the new direction against the actual last direction
last = direction
// move the snake
// gameover if snake intersects itself or went out of bounds
head = snake_move(head, direction, growth)
if (growth > 0) growth--
if (snake_intersects(head.next, head.value)
|| head.value.x < 0 || head.value.x > SIZE-1
|| head.value.y < 0 || head.value.y > SIZE-1) {
gameover = true
return
}
// check if the head is at the same position as the apple
// if so, grow the body and move the apple to a new location
if (head.value.equals(apple)) {
growth += 1
do {
apple.randomize()
} while (snake_intersects(head, apple))
}
// draw the scene:
// 1) cover the entire scene with black
// 2) fill each snake segment with green
// 3) fill the apple position with red
ctx.fillStyle = COLORS.BACKGROUND
ctx.fillRect(0, 0, SIZE, SIZE)
head.map(part => part.draw(ctx, COLORS.SNAKE))
apple.draw(ctx, COLORS.APPLE)
}
// === GAME INPUT ===
// listen for pressed keys to re-direct the snake
window.addEventListener('keydown', e => {
switch (e.key) {
case 'w': case 'ArrowUp':
if (last !== DIRS.DOWN) direction = DIRS.UP
break
case 'a': case 'ArrowLeft':
if (last !== DIRS.RIGHT) direction = DIRS.LEFT
break
case 's': case 'ArrowDown':
if (last !== DIRS.UP) direction = DIRS.DOWN
break
case 'd': case 'ArrowRight':
if (last !== DIRS.LEFT) direction = DIRS.RIGHT
break
default:
if (gameover) location.reload()
break
}
})
// run loop() 10 times per second (every 100ms) (snake moves 10 cells/sec)
setInterval(loop, 100)
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment