diff --git a/src/useGame/useFood.ts b/src/useGame/useFood.ts index da85d09..b9166c3 100755 --- a/src/useGame/useFood.ts +++ b/src/useGame/useFood.ts @@ -1,13 +1,13 @@ import { useState, useRef } from "react"; import type { Position, SnakeBody } from "../types"; -import type { UseRendererType } from "./useRenderer"; +import type { UseBoardType } from "../useBoard"; export function useFood( cols: number, rows: number, - rendererManager: UseRendererType + boardManager: UseBoardType ) { - const { containerRef } = rendererManager; + const { containerRef } = boardManager; const [foodPositions, setFoodPositions] = useState([]); const foodPositionsRef = useRef([]); @@ -38,22 +38,22 @@ export function useFood( renderFood(newFood); }; - const clearFood = () => { - setFoodPositions([]); - foodPositionsRef.current = []; - const container = containerRef.current; - container?.querySelectorAll(".food").forEach((el) => { - el.classList.remove("food"); - }); - }; + const clearFood = () => { + setFoodPositions([]); + foodPositionsRef.current = []; + const container = containerRef.current; + container?.querySelectorAll(".food").forEach((el: Element) => { + (el as HTMLElement).classList.remove("food"); + }); + }; - const renderFood = (positions: Position[]) => { - const container = containerRef.current; - if (!container) return; + const renderFood = (positions: Position[]) => { + const container = containerRef.current; + if (!container) return; - container.querySelectorAll(".food").forEach((el) => { - el.classList.remove("food"); - }); + container.querySelectorAll(".food").forEach((el: Element) => { + (el as HTMLElement).classList.remove("food"); + }); positions.forEach((pos) => { const cell = container.querySelector( diff --git a/src/useGame/useGameStatus.ts b/src/useGame/useGameStatus.ts deleted file mode 100755 index 26bd181..0000000 --- a/src/useGame/useGameStatus.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import type { GameStatus } from "../types"; -import type { UseRendererType } from "./useRenderer"; -import type { UseSnakeType } from "./useSnake"; -import type { UseFoodType } from "./useFood"; - -const INITIAL_FPS = 12; -const FOODS_PER_LEVEL = 2; - -export function useGameStatus( - rendererManager: UseRendererType, - snakeManager: UseSnakeType, - foodManager: UseFoodType -) { - const { containerRef } = rendererManager; - const { initSnake, clearSnake, snakeBodyRef } = snakeManager; - const { spawnFood, clearFood } = foodManager; - const [gameStatus, setGameStatus] = useState("waiting"); - const [score, setScore] = useState(0); - const [level, setLevel] = useState(1); - const [paused, setPaused] = useState(false); - const levelRef = useRef(1); - const foodsEatenRef = useRef(0); - const gameLoopRef = useRef(null); - const lastUpdateRef = useRef(0); - const snakeManagerRef = useRef(snakeManager); - const foodManagerRef = useRef(foodManager); - const currentFpsRef = useRef(INITIAL_FPS); - const collisionGracePeriodRef = useRef(false); - - snakeManagerRef.current = snakeManager; - foodManagerRef.current = foodManager; - - const startGame = () => { - initSnake(); - spawnFood(snakeBodyRef.current, 3); - setScore(0); - setLevel(1); - levelRef.current = 1; - foodsEatenRef.current = 0; - currentFpsRef.current = INITIAL_FPS; - collisionGracePeriodRef.current = false; - setGameStatus("started"); - }; - - const stopGame = () => { - if (gameLoopRef.current) { - cancelAnimationFrame(gameLoopRef.current); - gameLoopRef.current = null; - } - clearSnake(); - clearFood(); - setGameStatus("waiting"); - const container = containerRef.current; - container?.querySelectorAll(".snake-head, .snake-body, .snake-tail, .collision").forEach((el) => { - el.classList.remove("snake-head", "snake-body", "snake-tail", "collision"); - }); - }; - - useEffect(() => { - if (gameStatus === "started") { - if (paused) return; - let shouldGrow = false; - lastUpdateRef.current = performance.now(); - - const gameLoop = (timestamp: number) => { - const msPerFrame = 1000 / currentFpsRef.current; - const elapsed = timestamp - lastUpdateRef.current; - - if (elapsed >= msPerFrame) { - const result = snakeManagerRef.current.moveSnake(shouldGrow); - - shouldGrow = false; - - if (result === "wall-collision" || result === "self-collision") { - if (collisionGracePeriodRef.current) { - const container = containerRef.current; - container?.querySelectorAll(".snake-head, .snake-body, .snake-tail").forEach((el) => { - el.classList.add("collision"); - }); - setGameStatus("game-over"); - return; - } else { - collisionGracePeriodRef.current = true; - lastUpdateRef.current = timestamp; - gameLoopRef.current = requestAnimationFrame(gameLoop); - return; - } - } - - collisionGracePeriodRef.current = false; - - const head = snakeManagerRef.current.snakeBodyRef.current[0]; - if (head && foodManagerRef.current.checkFoodCollision(head)) { - foodManagerRef.current.removeFood(head); - shouldGrow = true; - - foodsEatenRef.current += 1; - - setScore((prev) => { - let newScore = prev + levelRef.current; - if (foodsEatenRef.current >= FOODS_PER_LEVEL) { - newScore--; - const newLevel = levelRef.current + 1; - levelRef.current = newLevel; - setLevel(newLevel); - currentFpsRef.current = INITIAL_FPS + (newLevel - 1); - foodsEatenRef.current = 0; - } - - foodManagerRef.current.spawnFood( - snakeManagerRef.current.snakeBodyRef.current, - 1 - ); - return newScore; - }); - - } - - lastUpdateRef.current = timestamp; - } - - gameLoopRef.current = requestAnimationFrame(gameLoop); - }; - - gameLoopRef.current = requestAnimationFrame(gameLoop); - - return () => { - if (gameLoopRef.current) { - cancelAnimationFrame(gameLoopRef.current); - gameLoopRef.current = null; - } - }; - } - }, [gameStatus, paused]); - - useEffect(() => { - if (gameStatus === "game-over") { - if (gameLoopRef.current) { - cancelAnimationFrame(gameLoopRef.current); - gameLoopRef.current = null; - } - } - }, [gameStatus]); - - return { - gameStatus, - setGameStatus, - startGame, - stopGame, - paused, - togglePause: () => setPaused(curr => !curr), - score, - level, - }; -} - -export type UseGameStatusType = ReturnType; -export type { GameStatus } from "../types"; diff --git a/src/useGame/useRenderer.ts b/src/useGame/useRenderer.ts deleted file mode 100755 index 3fe7949..0000000 --- a/src/useGame/useRenderer.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useRef } from "react"; - -export type BoardData = { - cols: number; - rows: number; -}; - -export function useRenderer() { - const containerRef = useRef(null); - - const renderBoard = (cols: number, rows: number) => { - const container = containerRef.current; - if (!container) return; - - container.innerHTML = ""; - - const board = document.createElement("div"); - board.id = "snake-board"; - board.className = "snake-board"; - - for (let r = 0; r < rows; r++) { - const rowDiv = document.createElement("div"); - rowDiv.className = "snake-row"; - rowDiv.setAttribute("data-r", String(r)); - - for (let c = 0; c < cols; c++) { - const cellDiv = document.createElement("div"); - cellDiv.className = "snake-cell"; - cellDiv.setAttribute("data-r", String(r)); - cellDiv.setAttribute("data-c", String(c)); - rowDiv.appendChild(cellDiv); - } - - board.appendChild(rowDiv); - } - - container.appendChild(board); - }; - - return { - containerRef, - renderBoard, - }; -} - -export type UseRendererType = ReturnType; diff --git a/src/useGame/useSnake.ts b/src/useGame/useSnake.ts deleted file mode 100755 index 76d9b46..0000000 --- a/src/useGame/useSnake.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { useRef, useState } from "react"; -import type { Direction, SnakeBody } from "../types"; -import type { UseRendererType } from "./useRenderer"; - -export function useSnake( - cols: number, - rows: number, - rendererManager: UseRendererType -) { - const { containerRef } = rendererManager; - const [snakeBody, setSnakeBody] = useState([]); - const snakeBodyRef = useRef([]); - const [direction, setDirection] = useState("up"); - const directionRef = useRef("up"); - const lastMovedDirectionRef = useRef("up"); - - const initSnake = () => { - const centerR = Math.floor(rows / 2); - const centerC = Math.floor(cols / 2); - - const initialBody: SnakeBody = [ - { r: centerR, c: centerC }, - { r: centerR + 1, c: centerC }, - { r: centerR + 2, c: centerC }, - { r: centerR + 3, c: centerC }, - { r: centerR + 4, c: centerC }, - { r: centerR + 5, c: centerC }, - ]; - - setSnakeBody(initialBody); - snakeBodyRef.current = initialBody; - setDirection("up"); - directionRef.current = "up"; - lastMovedDirectionRef.current = "up"; - renderSnake(initialBody); - }; - - const clearSnake = () => { - setSnakeBody([]); - snakeBodyRef.current = []; - setDirection("up"); - directionRef.current = "up"; - lastMovedDirectionRef.current = "up"; - }; - - const renderSnake = (body: SnakeBody) => { - const container = containerRef.current; - if (!container) return; - - const dir = directionRef.current; - const newPositions = new Set(); - - // First, apply new positions and track them - body.forEach((segment, idx) => { - const posKey = `${segment.r},${segment.c}`; - newPositions.add(posKey); - - const cell = container.querySelector( - `.snake-cell[data-r="${segment.r}"][data-c="${segment.c}"]` - ); - if (cell) { - cell.className = "snake-cell"; - if (idx === 0) { - cell.classList.add("snake-head", `dir-${dir}`); - } else if (idx === body.length - 1) { - const tailDir = getTailDirection(body); - cell.classList.add("snake-tail", `dir-${tailDir}`); - } else { - cell.classList.add("snake-body"); - } - } - }); - - // Then clear old positions not in the new body - const allSnakeCells = container.querySelectorAll(".snake-head, .snake-body, .snake-tail"); - allSnakeCells.forEach((el) => { - const r = el.getAttribute("data-r"); - const c = el.getAttribute("data-c"); - const posKey = `${r},${c}`; - if (!newPositions.has(posKey)) { - el.className = "snake-cell"; - } - }); - }; - - const getTailDirection = (body: SnakeBody): Direction => { - if (body.length < 2) return directionRef.current; - - const tail = body[body.length - 1]; - const beforeTail = body[body.length - 2]; - - // Tail points away from beforeTail - if (tail.r < beforeTail.r) return "up"; - if (tail.r > beforeTail.r) return "down"; - if (tail.c < beforeTail.c) return "left"; - if (tail.c > beforeTail.c) return "right"; - - return directionRef.current; - }; - - const changeDirection = (newDirection: Direction) => { - const opposites: Record = { - up: "down", - down: "up", - left: "right", - right: "left", - }; - - if (opposites[lastMovedDirectionRef.current] !== newDirection) { - setDirection(newDirection); - directionRef.current = newDirection; - } - }; - - const moveSnake = (grow: boolean = false): "continue" | "wall-collision" | "self-collision" => { - const body = snakeBodyRef.current; - if (body.length === 0) return "continue"; - - const head = body[0]; - const dir = directionRef.current; - - let newR = head.r; - let newC = head.c; - - switch (dir) { - case "up": - newR -= 1; - break; - case "down": - newR += 1; - break; - case "left": - newC -= 1; - break; - case "right": - newC += 1; - break; - } - - if (newR < 0 || newR >= rows || newC < 0 || newC >= cols) { - return "wall-collision"; - } - - const newHead = { r: newR, c: newC }; - - // Check self-collision (excluding tail if not growing, as tail will move) - const bodyToCheck = grow ? body : body.slice(0, -1); - const collision = bodyToCheck.some( - (segment) => segment.r === newHead.r && segment.c === newHead.c - ); - - if (collision) { - return "self-collision"; - } - - const newBody = grow - ? [newHead, ...body] - : [newHead, ...body.slice(0, -1)]; - - lastMovedDirectionRef.current = dir; - - setSnakeBody(newBody); - snakeBodyRef.current = newBody; - renderSnake(newBody); - - return "continue"; - }; - - return { - snakeBody, - snakeBodyRef, - direction, - directionRef, - initSnake, - clearSnake, - changeDirection, - moveSnake, - renderSnake, - }; -} - -export type UseSnakeType = ReturnType;