From 762bc86878ec6f26485ece75a9962c6aae64b1cd Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Thu, 30 Oct 2025 22:39:21 +0000 Subject: [PATCH] feat(phase1.2b): implement Nokia-style key buffering system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/useCursor/useCursor.ts | 22 +++++++++++++++++++ src/useGame/useGameStatus.ts | 10 --------- src/useGameStatus/useGameStatus.ts | 34 +++++++----------------------- 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/useCursor/useCursor.ts b/src/useCursor/useCursor.ts index 2601f36..95a3139 100755 --- a/src/useCursor/useCursor.ts +++ b/src/useCursor/useCursor.ts @@ -13,6 +13,7 @@ export function useCursor( const [direction, setDirection] = useState("up"); const directionRef = useRef("up"); const lastMovedDirectionRef = useRef("up"); + const nextDirectionRef = useRef(null); const initSnake = () => { const centerR = Math.floor(rows / 2); @@ -103,9 +104,29 @@ export function useCursor( 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 = { + up: "down", + down: "up", + left: "right", + right: "left", + }; + + // Apply buffer only if still valid (not 180° reversal) if (opposites[lastMovedDirectionRef.current] !== newDirection) { setDirection(newDirection); directionRef.current = newDirection; + nextDirectionRef.current = null; } }; @@ -237,6 +258,7 @@ export function useCursor( initSnake, clearSnake, changeDirection, + applyBufferedDirection, moveSnake, renderSnake, }; diff --git a/src/useGame/useGameStatus.ts b/src/useGame/useGameStatus.ts index 4e954d8..746de5a 100755 --- a/src/useGame/useGameStatus.ts +++ b/src/useGame/useGameStatus.ts @@ -58,9 +58,7 @@ export function useGameStatus( }; 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(); @@ -70,9 +68,7 @@ export function useGameStatus( 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; @@ -96,7 +92,6 @@ export function useGameStatus( const head = snakeManagerRef.current.snakeBodyRef.current[0]; if (head && foodManagerRef.current.checkFoodCollision(head)) { - console.log("Food eaten!"); foodManagerRef.current.removeFood(head); shouldGrow = true; @@ -104,15 +99,12 @@ export function useGameStatus( 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; } @@ -120,7 +112,6 @@ export function useGameStatus( snakeManagerRef.current.snakeBodyRef.current, 1 ); - console.log('Final newScore to be set:', newScore); return newScore; }); @@ -135,7 +126,6 @@ export function useGameStatus( gameLoopRef.current = requestAnimationFrame(gameLoop); return () => { - console.log("Cleaning up game loop"); if (gameLoopRef.current) { cancelAnimationFrame(gameLoopRef.current); gameLoopRef.current = null; diff --git a/src/useGameStatus/useGameStatus.ts b/src/useGameStatus/useGameStatus.ts index 5463aee..6aacd8c 100755 --- a/src/useGameStatus/useGameStatus.ts +++ b/src/useGameStatus/useGameStatus.ts @@ -26,7 +26,6 @@ export function useGameStatus( const snakeManagerRef = useRef(snakeManager); const foodManagerRef = useRef(foodManager); const currentFpsRef = useRef(INITIAL_FPS); - const collisionGracePeriodRef = useRef(false); snakeManagerRef.current = snakeManager; foodManagerRef.current = foodManager; @@ -39,7 +38,6 @@ export function useGameStatus( levelRef.current = 1; foodsEatenRef.current = 0; currentFpsRef.current = INITIAL_FPS; - collisionGracePeriodRef.current = false; setGameStatus("started"); }; @@ -58,9 +56,7 @@ export function useGameStatus( }; 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(); @@ -70,33 +66,24 @@ export function useGameStatus( 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; - } + 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; } - collisionGracePeriodRef.current = false; + // Apply any buffered direction input for next frame + snakeManagerRef.current.applyBufferedDirection(); const head = snakeManagerRef.current.snakeBodyRef.current[0]; if (head && foodManagerRef.current.checkFoodCollision(head)) { - console.log("Food eaten!"); foodManagerRef.current.removeFood(head); shouldGrow = true; @@ -104,15 +91,12 @@ export function useGameStatus( 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; } @@ -120,7 +104,6 @@ export function useGameStatus( snakeManagerRef.current.snakeBodyRef.current, 1 ); - console.log('Final newScore to be set:', newScore); return newScore; }); @@ -135,7 +118,6 @@ export function useGameStatus( gameLoopRef.current = requestAnimationFrame(gameLoop); return () => { - console.log("Cleaning up game loop"); if (gameLoopRef.current) { cancelAnimationFrame(gameLoopRef.current); gameLoopRef.current = null;