diff --git a/src/components/snake/canvas.ts b/src/components/snake/canvas.ts index e7f921e..04bbf41 100644 --- a/src/components/snake/canvas.ts +++ b/src/components/snake/canvas.ts @@ -1,4 +1,4 @@ -import { Engine, randint, Vec2, vec2 } from './game-engine'; +import { Engine, Keys, randint, UI, Vec2, vec2 } from './game-engine'; const BOARD_SIZE = 600; // px const SQUARE_SIZE = 30; // px @@ -6,21 +6,43 @@ const SQUARE_SIZE = 30; // px const BOARD_SQUARES = BOARD_SIZE / SQUARE_SIZE; export default function runCanvas(canvas: HTMLCanvasElement) { - const engine = new Engine(canvas); + const ui = new UI(canvas); + const keys = new Keys(); + const engine = new Engine({ updateDelay: 200 }); - let dead = false; - const snake = [vec2(1, 1), vec2(2, 1)]; - let apple = vec2(8, 8); + // game logic -------------------------------------------------------------- - function clearCanvas() { - engine.rect(0, 0, canvas.width, canvas.height); - } - function drawSquare(pos: Vec2) { - engine.rect(pos.x * SQUARE_SIZE, pos.y * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE); - } + function getRandApplePos() { + return vec2(randint(0, BOARD_SQUARES), randint(0, BOARD_SQUARES)) + } + + function calcUpdateDelay(snakeLength: number) { + // 5 squares per second base (200ms) + 1 sps on every apple + return 1000 / (5 + snakeLength) + } + + function resetState() { + let dead = false; + const snake = [vec2(1, 1), vec2(2, 1)]; + let apple = getRandApplePos(); + + engine.setUpdateDelay(calcUpdateDelay(snake.length)) + + return { dead, snake, apple }; + } + + let state = resetState(); + + function update() { + const { dead, snake, apple } = state; + + if (dead) { + if (keys.isKeyPressed(' ')) { + state = resetState(); + } + return; + } - async function update() { - if (dead) return; let dir = snake[snake.length - 1]!.sub(snake[snake.length - 2]!); const keyDirMap = { @@ -30,7 +52,7 @@ export default function runCanvas(canvas: HTMLCanvasElement) { d: vec2(+1, 0), }; for (const [key, newDir] of Object.entries(keyDirMap)) { - if (engine.isKeyPressed(key) && Vec2.dot(dir, newDir) === 0) { + if (keys.isKeyPressed(key) && Vec2.dot(dir, newDir) === 0) { dir = newDir; break; } @@ -40,45 +62,58 @@ export default function runCanvas(canvas: HTMLCanvasElement) { // check for snake intersection for (const square of snake.slice(1)) { if (nextHead.eq(square)) { - dead = true; + state.dead = true; } } // check for snake out of bounds if (nextHead.x < 0 || nextHead.x >= BOARD_SQUARES || nextHead.y < 0 || nextHead.y >= BOARD_SQUARES) { - dead = true; + state.dead = true; } // check for snake hitting apple if (nextHead.eq(apple)) { - apple = vec2(randint(0, BOARD_SQUARES), randint(0, BOARD_SQUARES)); + state.apple = getRandApplePos(); + engine.setUpdateDelay(calcUpdateDelay(snake.length)) } else { snake.shift(); } // move the snake snake.push(nextHead); + + keys.update(); } - async function render() { - engine.setFillStyle('#333333'); + // rendering --------------------------------------------------------------- + function clearCanvas() { + ui.rect(0, 0, canvas.width, canvas.height); + } + function drawSquare(pos: Vec2) { + ui.rect(pos.x * SQUARE_SIZE, pos.y * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE); + } + function render() { + const { dead, snake, apple } = state; + + ui.setFillStyle('#333333'); clearCanvas(); - engine.setFillStyle('#277edb'); + ui.setFillStyle('#277edb'); for (const square of snake.slice(0, snake.length - 1)) { drawSquare(square); } - engine.setFillStyle('#29d17a'); + ui.setFillStyle('#29d17a'); drawSquare(snake[snake.length - 1]!); - engine.setFillStyle('#e01851'); + ui.setFillStyle('#e01851'); drawSquare(apple); if (dead) { - engine.setFillStyle('#ff0000'); - engine.text('You Died', 60, 60); + ui.setFillStyle('#ff0000'); + ui.text('You Died', 60, 60); } } + keys.bindKeys() engine.run(update, render); } diff --git a/src/components/snake/game-engine.ts b/src/components/snake/game-engine.ts index 5ef6226..0bd0482 100644 --- a/src/components/snake/game-engine.ts +++ b/src/components/snake/game-engine.ts @@ -73,14 +73,50 @@ export function vec2(x: number, y: number) { return new Vec2(x, y); } -export class Engine { - ctx: CanvasRenderingContext2D; - - camera = vec2(0, 0); +export class Keys { keys = new Set(); keysPressed = new Set(); keysReleased = new Set(); + isKeyDown(key: string) { + return this.keys.has(key); + } + + isKeyUp(key: string) { + // by default, keys are considered up + return !this.keys.has(key); + } + + isKeyPressed(key: string) { + return this.keysPressed.has(key); + } + + isKeyReleased(key: string) { + return this.keysReleased.has(key); + } + + bindKeys() { + document.addEventListener('keydown', (event: KeyboardEvent) => { + this.keys.add(event.key); + this.keysPressed.add(event.key); + }); + document.addEventListener('keyup', (event: KeyboardEvent) => { + this.keys.delete(event.key); + this.keysReleased.add(event.key); + }); + } + + update() { + this.keysPressed.clear(); + this.keysReleased.clear(); + } +} + +export class UI { + ctx: CanvasRenderingContext2D; + + camera = vec2(0, 0); + constructor(canvasElement: HTMLCanvasElement) { const ctx = canvasElement.getContext('2d'); if (ctx === null) throw Error('unable to get 2d canvas context'); @@ -100,49 +136,29 @@ export class Engine { const c = this.camera; this.ctx.fillText(text, x - c.x, y - c.y); } +} - isKeyDown(key: string) { - return this.keys.has(key); - } +export class Engine { + updateDelay: number - isKeyUp(key: string) { - // by default, keys are considered up - return !this.keys.has(key); - } + constructor({ updateDelay }: { updateDelay: number }) { + this.updateDelay = updateDelay; + } - isKeyPressed(key: string) { - return this.keysPressed.has(key); - } - - isKeyReleased(key: string) { - return this.keysReleased.has(key); - } - - private bindKeys() { - document.addEventListener('keydown', (event: KeyboardEvent) => { - this.keys.add(event.key); - this.keysPressed.add(event.key); - }); - document.addEventListener('keyup', (event: KeyboardEvent) => { - this.keys.delete(event.key); - this.keysReleased.add(event.key); - }); - } - - run(update: () => Promise, render: () => Promise) { - this.bindKeys(); + setUpdateDelay(updateDelay: number) { + this.updateDelay = updateDelay; + } + run(update: () => void, render: () => void) { const updateLoop = async () => { // eslint-disable-next-line no-constant-condition while (true) { - await update(); - this.keysPressed.clear(); - await new Promise(resolve => setTimeout(resolve, 100)); - await new Promise(resolve => setTimeout(resolve, 0)); + update(); + await new Promise(resolve => setTimeout(resolve, this.updateDelay)); } }; const renderLoop = async () => { - await render(); + render(); window.requestAnimationFrame(renderLoop); };