refactor(phase1.3): remove old duplicate hook files
- Delete src/useGame/useSnake.ts (replaced by src/useCursor/useCursor.ts) - Delete src/useGame/useRenderer.ts (replaced by src/useBoard/useBoard.ts) - Delete src/useGame/useGameStatus.ts (replaced by src/useGameStatus/useGameStatus.ts) - Update useFood.ts to import from useBoard instead of useRenderer - Fix TypeScript type annotations in useFood.ts - Cleaner codebase with no duplicate implementations - Build and example app still work correctly
This commit is contained in:
+17
-17
@@ -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<Position[]>([]);
|
||||
const foodPositionsRef = useRef<Position[]>([]);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<GameStatus>("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<number | null>(null);
|
||||
const lastUpdateRef = useRef<number>(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<typeof useGameStatus>;
|
||||
export type { GameStatus } from "../types";
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
export type BoardData = {
|
||||
cols: number;
|
||||
rows: number;
|
||||
};
|
||||
|
||||
export function useRenderer() {
|
||||
const containerRef = useRef<HTMLDivElement | null>(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<typeof useRenderer>;
|
||||
@@ -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<SnakeBody>([]);
|
||||
const snakeBodyRef = useRef<SnakeBody>([]);
|
||||
const [direction, setDirection] = useState<Direction>("up");
|
||||
const directionRef = useRef<Direction>("up");
|
||||
const lastMovedDirectionRef = useRef<Direction>("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<string>();
|
||||
|
||||
// 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<Direction, Direction> = {
|
||||
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<typeof useSnake>;
|
||||
Reference in New Issue
Block a user