feat(phase1.2b): implement Nokia-style key buffering system
- Add nextDirectionRef to buffer player input - Update changeDirection() to queue input instead of immediate execution - Add applyBufferedDirection() to safely apply buffered input - Buffer prevents 180° reversals for safe turns - Call applyBufferedDirection() after each successful move in game loop - Remove collision grace period - no longer needed with key buffering - Remove collisionGracePeriodRef and related logic - Clean up all console.log statements from useGameStatus - Game now feels responsive at high speeds - Allows pre-emptive turning on board edges (Nokia Snake style) - Build and example app work correctly
This commit is contained in:
@@ -13,6 +13,7 @@ export function useCursor(
|
|||||||
const [direction, setDirection] = useState<Direction>("up");
|
const [direction, setDirection] = useState<Direction>("up");
|
||||||
const directionRef = useRef<Direction>("up");
|
const directionRef = useRef<Direction>("up");
|
||||||
const lastMovedDirectionRef = useRef<Direction>("up");
|
const lastMovedDirectionRef = useRef<Direction>("up");
|
||||||
|
const nextDirectionRef = useRef<Direction | null>(null);
|
||||||
|
|
||||||
const initSnake = () => {
|
const initSnake = () => {
|
||||||
const centerR = Math.floor(rows / 2);
|
const centerR = Math.floor(rows / 2);
|
||||||
@@ -103,9 +104,29 @@ export function useCursor(
|
|||||||
right: "left",
|
right: "left",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Buffer the direction instead of applying immediately
|
||||||
|
// Only buffer if it's not a 180° reversal of the last MOVED direction
|
||||||
|
if (opposites[lastMovedDirectionRef.current] !== newDirection) {
|
||||||
|
nextDirectionRef.current = newDirection;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyBufferedDirection = () => {
|
||||||
|
if (nextDirectionRef.current === null) return;
|
||||||
|
|
||||||
|
const newDirection = nextDirectionRef.current;
|
||||||
|
const opposites: Record<Direction, Direction> = {
|
||||||
|
up: "down",
|
||||||
|
down: "up",
|
||||||
|
left: "right",
|
||||||
|
right: "left",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply buffer only if still valid (not 180° reversal)
|
||||||
if (opposites[lastMovedDirectionRef.current] !== newDirection) {
|
if (opposites[lastMovedDirectionRef.current] !== newDirection) {
|
||||||
setDirection(newDirection);
|
setDirection(newDirection);
|
||||||
directionRef.current = newDirection;
|
directionRef.current = newDirection;
|
||||||
|
nextDirectionRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,6 +258,7 @@ export function useCursor(
|
|||||||
initSnake,
|
initSnake,
|
||||||
clearSnake,
|
clearSnake,
|
||||||
changeDirection,
|
changeDirection,
|
||||||
|
applyBufferedDirection,
|
||||||
moveSnake,
|
moveSnake,
|
||||||
renderSnake,
|
renderSnake,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ export function useGameStatus(
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("useGameStatus effect - gameStatus:", gameStatus);
|
|
||||||
if (gameStatus === "started") {
|
if (gameStatus === "started") {
|
||||||
console.log("Starting game loop");
|
|
||||||
if (paused) return;
|
if (paused) return;
|
||||||
let shouldGrow = false;
|
let shouldGrow = false;
|
||||||
lastUpdateRef.current = performance.now();
|
lastUpdateRef.current = performance.now();
|
||||||
@@ -70,9 +68,7 @@ export function useGameStatus(
|
|||||||
const elapsed = timestamp - lastUpdateRef.current;
|
const elapsed = timestamp - lastUpdateRef.current;
|
||||||
|
|
||||||
if (elapsed >= msPerFrame) {
|
if (elapsed >= msPerFrame) {
|
||||||
console.log("Game loop tick");
|
|
||||||
const result = snakeManagerRef.current.moveSnake(shouldGrow);
|
const result = snakeManagerRef.current.moveSnake(shouldGrow);
|
||||||
console.log("Move result:", result);
|
|
||||||
|
|
||||||
shouldGrow = false;
|
shouldGrow = false;
|
||||||
|
|
||||||
@@ -96,7 +92,6 @@ export function useGameStatus(
|
|||||||
|
|
||||||
const head = snakeManagerRef.current.snakeBodyRef.current[0];
|
const head = snakeManagerRef.current.snakeBodyRef.current[0];
|
||||||
if (head && foodManagerRef.current.checkFoodCollision(head)) {
|
if (head && foodManagerRef.current.checkFoodCollision(head)) {
|
||||||
console.log("Food eaten!");
|
|
||||||
foodManagerRef.current.removeFood(head);
|
foodManagerRef.current.removeFood(head);
|
||||||
shouldGrow = true;
|
shouldGrow = true;
|
||||||
|
|
||||||
@@ -104,15 +99,12 @@ export function useGameStatus(
|
|||||||
|
|
||||||
setScore((prev) => {
|
setScore((prev) => {
|
||||||
let newScore = prev + levelRef.current;
|
let newScore = prev + levelRef.current;
|
||||||
console.log('newscore:', newScore, 'level:');
|
|
||||||
if (foodsEatenRef.current >= FOODS_PER_LEVEL) {
|
if (foodsEatenRef.current >= FOODS_PER_LEVEL) {
|
||||||
newScore--;
|
newScore--;
|
||||||
console.log('newScore after decrement:', newScore);
|
|
||||||
const newLevel = levelRef.current + 1;
|
const newLevel = levelRef.current + 1;
|
||||||
levelRef.current = newLevel;
|
levelRef.current = newLevel;
|
||||||
setLevel(newLevel);
|
setLevel(newLevel);
|
||||||
currentFpsRef.current = INITIAL_FPS + (newLevel - 1);
|
currentFpsRef.current = INITIAL_FPS + (newLevel - 1);
|
||||||
console.log(`Level up! Level ${newLevel}, FPS: ${currentFpsRef.current}`);
|
|
||||||
foodsEatenRef.current = 0;
|
foodsEatenRef.current = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +112,6 @@ export function useGameStatus(
|
|||||||
snakeManagerRef.current.snakeBodyRef.current,
|
snakeManagerRef.current.snakeBodyRef.current,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
console.log('Final newScore to be set:', newScore);
|
|
||||||
return newScore;
|
return newScore;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,7 +126,6 @@ export function useGameStatus(
|
|||||||
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log("Cleaning up game loop");
|
|
||||||
if (gameLoopRef.current) {
|
if (gameLoopRef.current) {
|
||||||
cancelAnimationFrame(gameLoopRef.current);
|
cancelAnimationFrame(gameLoopRef.current);
|
||||||
gameLoopRef.current = null;
|
gameLoopRef.current = null;
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export function useGameStatus(
|
|||||||
const snakeManagerRef = useRef(snakeManager);
|
const snakeManagerRef = useRef(snakeManager);
|
||||||
const foodManagerRef = useRef(foodManager);
|
const foodManagerRef = useRef(foodManager);
|
||||||
const currentFpsRef = useRef(INITIAL_FPS);
|
const currentFpsRef = useRef(INITIAL_FPS);
|
||||||
const collisionGracePeriodRef = useRef(false);
|
|
||||||
|
|
||||||
snakeManagerRef.current = snakeManager;
|
snakeManagerRef.current = snakeManager;
|
||||||
foodManagerRef.current = foodManager;
|
foodManagerRef.current = foodManager;
|
||||||
@@ -39,7 +38,6 @@ export function useGameStatus(
|
|||||||
levelRef.current = 1;
|
levelRef.current = 1;
|
||||||
foodsEatenRef.current = 0;
|
foodsEatenRef.current = 0;
|
||||||
currentFpsRef.current = INITIAL_FPS;
|
currentFpsRef.current = INITIAL_FPS;
|
||||||
collisionGracePeriodRef.current = false;
|
|
||||||
setGameStatus("started");
|
setGameStatus("started");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,9 +56,7 @@ export function useGameStatus(
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("useGameStatus effect - gameStatus:", gameStatus);
|
|
||||||
if (gameStatus === "started") {
|
if (gameStatus === "started") {
|
||||||
console.log("Starting game loop");
|
|
||||||
if (paused) return;
|
if (paused) return;
|
||||||
let shouldGrow = false;
|
let shouldGrow = false;
|
||||||
lastUpdateRef.current = performance.now();
|
lastUpdateRef.current = performance.now();
|
||||||
@@ -70,33 +66,24 @@ export function useGameStatus(
|
|||||||
const elapsed = timestamp - lastUpdateRef.current;
|
const elapsed = timestamp - lastUpdateRef.current;
|
||||||
|
|
||||||
if (elapsed >= msPerFrame) {
|
if (elapsed >= msPerFrame) {
|
||||||
console.log("Game loop tick");
|
|
||||||
const result = snakeManagerRef.current.moveSnake(shouldGrow);
|
const result = snakeManagerRef.current.moveSnake(shouldGrow);
|
||||||
console.log("Move result:", result);
|
|
||||||
|
|
||||||
shouldGrow = false;
|
shouldGrow = false;
|
||||||
|
|
||||||
if (result === "wall-collision" || result === "self-collision") {
|
if (result === "wall-collision" || result === "self-collision") {
|
||||||
if (collisionGracePeriodRef.current) {
|
const container = containerRef.current;
|
||||||
const container = containerRef.current;
|
container?.querySelectorAll(".snake-head, .snake-body, .snake-tail").forEach((el: Element) => {
|
||||||
container?.querySelectorAll(".snake-head, .snake-body, .snake-tail").forEach((el: Element) => {
|
(el as HTMLElement).classList.add("collision");
|
||||||
(el as HTMLElement).classList.add("collision");
|
});
|
||||||
});
|
setGameStatus("game-over");
|
||||||
setGameStatus("game-over");
|
return;
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
collisionGracePeriodRef.current = true;
|
|
||||||
lastUpdateRef.current = timestamp;
|
|
||||||
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
collisionGracePeriodRef.current = false;
|
// Apply any buffered direction input for next frame
|
||||||
|
snakeManagerRef.current.applyBufferedDirection();
|
||||||
|
|
||||||
const head = snakeManagerRef.current.snakeBodyRef.current[0];
|
const head = snakeManagerRef.current.snakeBodyRef.current[0];
|
||||||
if (head && foodManagerRef.current.checkFoodCollision(head)) {
|
if (head && foodManagerRef.current.checkFoodCollision(head)) {
|
||||||
console.log("Food eaten!");
|
|
||||||
foodManagerRef.current.removeFood(head);
|
foodManagerRef.current.removeFood(head);
|
||||||
shouldGrow = true;
|
shouldGrow = true;
|
||||||
|
|
||||||
@@ -104,15 +91,12 @@ export function useGameStatus(
|
|||||||
|
|
||||||
setScore((prev) => {
|
setScore((prev) => {
|
||||||
let newScore = prev + levelRef.current;
|
let newScore = prev + levelRef.current;
|
||||||
console.log('newscore:', newScore, 'level:');
|
|
||||||
if (foodsEatenRef.current >= FOODS_PER_LEVEL) {
|
if (foodsEatenRef.current >= FOODS_PER_LEVEL) {
|
||||||
newScore--;
|
newScore--;
|
||||||
console.log('newScore after decrement:', newScore);
|
|
||||||
const newLevel = levelRef.current + 1;
|
const newLevel = levelRef.current + 1;
|
||||||
levelRef.current = newLevel;
|
levelRef.current = newLevel;
|
||||||
setLevel(newLevel);
|
setLevel(newLevel);
|
||||||
currentFpsRef.current = INITIAL_FPS + (newLevel - 1);
|
currentFpsRef.current = INITIAL_FPS + (newLevel - 1);
|
||||||
console.log(`Level up! Level ${newLevel}, FPS: ${currentFpsRef.current}`);
|
|
||||||
foodsEatenRef.current = 0;
|
foodsEatenRef.current = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +104,6 @@ export function useGameStatus(
|
|||||||
snakeManagerRef.current.snakeBodyRef.current,
|
snakeManagerRef.current.snakeBodyRef.current,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
console.log('Final newScore to be set:', newScore);
|
|
||||||
return newScore;
|
return newScore;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,7 +118,6 @@ export function useGameStatus(
|
|||||||
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log("Cleaning up game loop");
|
|
||||||
if (gameLoopRef.current) {
|
if (gameLoopRef.current) {
|
||||||
cancelAnimationFrame(gameLoopRef.current);
|
cancelAnimationFrame(gameLoopRef.current);
|
||||||
gameLoopRef.current = null;
|
gameLoopRef.current = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user