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:
2025-10-30 22:52:40 +00:00
parent c5bd51d96b
commit b671c7452c
4 changed files with 17 additions and 404 deletions
+17 -17
View File
@@ -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(
-159
View File
@@ -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";
-46
View File
@@ -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>;
-182
View File
@@ -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>;