this project is inspired by dan's streaming,but implement my way.
data structure and variables
const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")
const width = 400
const height = 400
const cellLength = 20
let foodPosition
let initSnake = [
[0, 0],
[1, 0],
[2, 0],
]
let snake = [...initSnake]
let direction = "right"
let canChangeDirection = true
canvas
// background
function drawBackground() {
ctx.strokeStyle = "#bfbfbf"
for (let i = 0; i <= height / cellLength; i++) {
ctx.beginPath()
ctx.moveTo(0, cellLength * i)
ctx.lineTo(width, cellLength * i)
ctx.stroke()
}
for (let i = 0; i <= width / cellLength; i++) {
ctx.beginPath()
ctx.moveTo(cellLength * i, 0)
ctx.lineTo(cellLength * i, height)
ctx.stroke()
}
}
// snake
function drawSnake() {
let step = 100 / (snake.length - 1)
for (let i = 0; i < snake.length; i++) {
// gradient color
const percent = Math.min(100 - step * i, 90)
ctx.fillStyle = `hsl(0,0%,${percent}%)`
ctx.fillRect(
snake[i][0] * cellLength,
snake[i][1] * cellLength,
cellLength,
cellLength
)
}
}
// draw food
// random food position
function generateRandomFood() {
// if no place to generate
if (snake.length > width * height) {
return alert("you win")
}
const randomX = Math.floor(Math.random() * (width / cellLength))
const randomY = Math.floor(Math.random() * (height / cellLength))
// if the position comflict with snake, then re-generate
for (let i = 0; i < snake.length; i++) {
if (snake[i][0] === randomX && snake[i][1] === randomY) {
return generateRandomFood()
}
}
foodPosition = [randomX, randomY]
}
// draw
function drawFood() {
ctx.fillStyle = "#ff7875"
ctx.fillRect(
foodPosition[0] * cellLength,
foodPosition[1] * cellLength,
cellLength,
cellLength
)
}
snake movement
function snakeMove() {
let next
let last = snake[snake.length - 1]
// set new snake head by direction
switch (direction) {
case "up": {
next = [last[0], last[1] - 1]
break
}
case "down": {
next = [last[0], last[1] + 1]
break
}
case "left": {
next = [last[0] - 1, last[1]]
break
}
case "right": {
next = [last[0] + 1, last[1]]
break
}
}
// boundary collision
const boundary =
next[0] < 0 ||
next[0] >= width / cellLength ||
next[1] < 0 ||
next[1] >= height / cellLength
// self collision
const selfCollision = snake.some(([x, y]) => next[0] === x && next[1] === y)
// if collision, restart
if (boundary || selfCollision) {
return restart()
}
snake.push(next)
// if next movement is food, push head, do not shift
if (next[0] === foodPosition[0] && next[1] === foodPosition[1]) {
generateRandomFood()
return
}
snake.shift()
canChangeDirection = true
}
event listener
document.addEventListener("keydown", (e) => {
switch (e.key) {
case "ArrowUp":
if (direction === "down" || !canChangeDirection) return
direction = "up"
canChangeDirection = false
break
case "ArrowDown":
if (direction === "up" || !canChangeDirection) return
direction = "down"
canChangeDirection = false
break
case "ArrowLeft":
if (direction === "right" || !canChangeDirection) return
direction = "left"
canChangeDirection = false
break
case "ArrowRight":
if (direction === "left" || !canChangeDirection) return
direction = "right"
canChangeDirection = false
break
}
})
requestAnimationFrame for animate
// its too fast for this game by default, make it slow down
function animate() {
let count = 0
function loop() {
if (++count > 5) {
draw()
count = 0
}
requestAnimationFrame(loop)
}
requestAnimationFrame(loop)
}
fix Bug
because requestAnimationFrame is async, as if snake's direction is right, i can change it to top,and then left before snake movement.
so i add canChangeDirection
, direction can change only after snake moved
// event callback
case "ArrowUp":
if (direction === "down" |!canChangeDirection) return
direction = "up"
canChangeDirection = false
break
Top comments (0)