feat(phase1.1): create hook module structure with proper exports
- Create new hook modules: useBoard, useCursor, useGameStatus - Move/copy hook implementations to their own directories - Create index.ts files for clean module exports - Update all imports to use new module locations - Rename useSnake -> useCursor for Unified API alignment - Rename useRenderer -> useBoard for clarity - Update main src/index.ts to export from all hook modules - All TypeScript builds successfully - Example app remains functional
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
export * from './useGame';
|
export * from './useGame';
|
||||||
|
export * from './useCursor';
|
||||||
|
export * from './useGameStatus';
|
||||||
|
export * from './useBoard';
|
||||||
|
export * from './useScore';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './useBoard';
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
export type BoardData = {
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useBoard() {
|
||||||
|
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 UseBoardType = ReturnType<typeof useBoard>;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './useCursor';
|
||||||
Executable
+179
@@ -0,0 +1,179 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import type { Direction, SnakeBody } from "../types";
|
||||||
|
import type { UseBoardType } from "../useBoard";
|
||||||
|
|
||||||
|
export function useCursor(
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
boardManager: UseBoardType
|
||||||
|
) {
|
||||||
|
const { containerRef } = boardManager;
|
||||||
|
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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
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: Element) => {
|
||||||
|
const r = el.getAttribute("data-r");
|
||||||
|
const c = el.getAttribute("data-c");
|
||||||
|
const posKey = `${r},${c}`;
|
||||||
|
if (!newPositions.has(posKey)) {
|
||||||
|
(el as HTMLElement).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 UseCursorType = ReturnType<typeof useCursor>;
|
||||||
@@ -1,2 +1 @@
|
|||||||
export * from './useGame';
|
export * from './useGame';
|
||||||
export * from './useKeyBindings';
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useRenderer } from "./useRenderer";
|
import { useBoard } from "../useBoard";
|
||||||
import { useSnake } from "./useSnake";
|
import { useCursor } from "../useCursor";
|
||||||
import { useFood } from "./useFood";
|
import { useFood } from "./useFood";
|
||||||
import { useGameStatus } from "./useGameStatus";
|
import { useGameStatus } from "../useGameStatus";
|
||||||
import { useKeyBindings, type UseKeyBindingsType } from "./useKeyBindings";
|
import { useKeyBindings, type UseKeyBindingsType } from "./useKeyBindings";
|
||||||
import { useScore } from "../useScore";
|
import { useScore } from "../useScore";
|
||||||
|
|
||||||
export function useGame(cols: number, rows: number, platformHook?: unknown) {
|
export function useGame(cols: number, rows: number, platformHook?: unknown) {
|
||||||
const rendererManager = useRenderer();
|
const boardManager = useBoard();
|
||||||
const { containerRef, renderBoard } = rendererManager;
|
const { containerRef, renderBoard } = boardManager;
|
||||||
|
|
||||||
const snakeManager = useSnake(cols, rows, rendererManager);
|
const snakeManager = useCursor(cols, rows, boardManager);
|
||||||
const { changeDirection } = snakeManager;
|
const { changeDirection } = snakeManager;
|
||||||
|
|
||||||
const foodManager = useFood(cols, rows, rendererManager);
|
const foodManager = useFood(cols, rows, boardManager);
|
||||||
|
|
||||||
const gameManager = useGameStatus(rendererManager, snakeManager, foodManager);
|
const gameManager = useGameStatus(boardManager, snakeManager, foodManager);
|
||||||
const { gameStatus, startGame, stopGame, togglePause, score, level } = gameManager;
|
const { gameStatus, startGame, stopGame, togglePause, score, level } = gameManager;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './useGameStatus';
|
||||||
Executable
+169
@@ -0,0 +1,169 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import type { GameStatus } from "../types";
|
||||||
|
import type { UseBoardType } from "../useBoard";
|
||||||
|
import type { UseCursorType } from "../useCursor";
|
||||||
|
import type { UseFoodType } from "../useGame/useFood";
|
||||||
|
|
||||||
|
const INITIAL_FPS = 5;
|
||||||
|
const FOODS_PER_LEVEL = 2;
|
||||||
|
|
||||||
|
export function useGameStatus(
|
||||||
|
boardManager: UseBoardType,
|
||||||
|
snakeManager: UseCursorType,
|
||||||
|
foodManager: UseFoodType
|
||||||
|
) {
|
||||||
|
const { containerRef } = boardManager;
|
||||||
|
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: Element) => {
|
||||||
|
(el as HTMLElement).classList.remove("snake-head", "snake-body", "snake-tail", "collision");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("useGameStatus effect - gameStatus:", gameStatus);
|
||||||
|
if (gameStatus === "started") {
|
||||||
|
console.log("Starting game loop");
|
||||||
|
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) {
|
||||||
|
console.log("Game loop tick");
|
||||||
|
const result = snakeManagerRef.current.moveSnake(shouldGrow);
|
||||||
|
console.log("Move result:", result);
|
||||||
|
|
||||||
|
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: Element) => {
|
||||||
|
(el as HTMLElement).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)) {
|
||||||
|
console.log("Food eaten!");
|
||||||
|
foodManagerRef.current.removeFood(head);
|
||||||
|
shouldGrow = true;
|
||||||
|
|
||||||
|
foodsEatenRef.current += 1;
|
||||||
|
|
||||||
|
setScore((prev) => {
|
||||||
|
let newScore = prev + levelRef.current;
|
||||||
|
console.log('newscore:', newScore, 'level:');
|
||||||
|
if (foodsEatenRef.current >= FOODS_PER_LEVEL) {
|
||||||
|
newScore--;
|
||||||
|
console.log('newScore after decrement:', newScore);
|
||||||
|
const newLevel = levelRef.current + 1;
|
||||||
|
levelRef.current = newLevel;
|
||||||
|
setLevel(newLevel);
|
||||||
|
currentFpsRef.current = INITIAL_FPS + (newLevel - 1);
|
||||||
|
console.log(`Level up! Level ${newLevel}, FPS: ${currentFpsRef.current}`);
|
||||||
|
foodsEatenRef.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
foodManagerRef.current.spawnFood(
|
||||||
|
snakeManagerRef.current.snakeBodyRef.current,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
console.log('Final newScore to be set:', newScore);
|
||||||
|
return newScore;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdateRef.current = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
};
|
||||||
|
|
||||||
|
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log("Cleaning up game loop");
|
||||||
|
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";
|
||||||
Reference in New Issue
Block a user