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 { useState, useRef } from "react";
|
||||||
import type { Position, SnakeBody } from "../types";
|
import type { Position, SnakeBody } from "../types";
|
||||||
import type { UseRendererType } from "./useRenderer";
|
import type { UseBoardType } from "../useBoard";
|
||||||
|
|
||||||
export function useFood(
|
export function useFood(
|
||||||
cols: number,
|
cols: number,
|
||||||
rows: number,
|
rows: number,
|
||||||
rendererManager: UseRendererType
|
boardManager: UseBoardType
|
||||||
) {
|
) {
|
||||||
const { containerRef } = rendererManager;
|
const { containerRef } = boardManager;
|
||||||
const [foodPositions, setFoodPositions] = useState<Position[]>([]);
|
const [foodPositions, setFoodPositions] = useState<Position[]>([]);
|
||||||
const foodPositionsRef = useRef<Position[]>([]);
|
const foodPositionsRef = useRef<Position[]>([]);
|
||||||
|
|
||||||
@@ -38,22 +38,22 @@ export function useFood(
|
|||||||
renderFood(newFood);
|
renderFood(newFood);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearFood = () => {
|
const clearFood = () => {
|
||||||
setFoodPositions([]);
|
setFoodPositions([]);
|
||||||
foodPositionsRef.current = [];
|
foodPositionsRef.current = [];
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
container?.querySelectorAll(".food").forEach((el) => {
|
container?.querySelectorAll(".food").forEach((el: Element) => {
|
||||||
el.classList.remove("food");
|
(el as HTMLElement).classList.remove("food");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFood = (positions: Position[]) => {
|
const renderFood = (positions: Position[]) => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
container.querySelectorAll(".food").forEach((el) => {
|
container.querySelectorAll(".food").forEach((el: Element) => {
|
||||||
el.classList.remove("food");
|
(el as HTMLElement).classList.remove("food");
|
||||||
});
|
});
|
||||||
|
|
||||||
positions.forEach((pos) => {
|
positions.forEach((pos) => {
|
||||||
const cell = container.querySelector(
|
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