diff --git a/pages/index.js b/pages/index.js index bffd478..2f8fb7b 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,5 +1,5 @@ import dynamic from "next/dynamic"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import styles from "../styles/Snake.module.css"; const Config = { @@ -21,7 +21,7 @@ const Direction = { Bottom: { x: 0, y: 1 }, }; -const Cell = ({ x, y, type }) => { +const Cell = ({ x, y, type, remaining }) => { const getStyles = () => { switch (type) { case CellType.Snake: @@ -33,18 +33,21 @@ const Cell = ({ x, y, type }) => { case CellType.Food: return { - backgroundColor: "darkorange", + backgroundColor: "tomato", borderRadius: 20, width: 32, height: 32, + transform: `scale(${0.5 + remaining / 20})`, }; default: return {}; } }; + return (
{ height: Config.cellSize, }} > -
+
+ {remaining} +
); }; @@ -64,10 +69,25 @@ const getRandomCell = () => ({ createdAt: Date.now(), }); -const getInitialFoods = () => [{ x: 4, y: 10, createdAt: Date.now() }]; - const getInitialDirection = () => Direction.Right; +const useInterval = (callback, duration) => { + const time = useRef(0); + + const wrappedCallback = useCallback(() => { + // don't call callback() more than once within `duration` + if (Date.now() - time.current >= duration) { + time.current = Date.now(); + callback(); + } + }, [callback, duration]); + + useEffect(() => { + const interval = setInterval(wrappedCallback, 1000 / 60); + return () => clearInterval(interval); + }, [wrappedCallback, duration]); +}; + const useSnake = () => { const getDefaultSnake = () => [ { x: 8, y: 12 }, @@ -79,7 +99,7 @@ const useSnake = () => { const [snake, setSnake] = useState(getDefaultSnake()); const [direction, setDirection] = useState(getInitialDirection()); - const [foods, setFoods] = useState(getInitialFoods()); + const [foods, setFoods] = useState([]); const score = snake.length - 3; @@ -88,7 +108,7 @@ const useSnake = () => { // resets the snake ,foods, direction to initial values const resetGame = useCallback(() => { - setFoods(getInitialFoods()); + setFoods([]); setDirection(getInitialDirection()); }, []); @@ -99,6 +119,19 @@ const useSnake = () => { ); }, []); + // ?. is called optional chaining + // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining + const isFood = useCallback( + ({ x, y }) => foods.some((food) => food.x === x && food.y === y), + [foods] + ); + + const isSnake = useCallback( + ({ x, y }) => + snake.find((position) => position.x === x && position.y === y), + [snake] + ); + const addFood = useCallback(() => { let newFood = getRandomCell(); while (isSnake(newFood) || isFood(newFood)) { @@ -108,68 +141,46 @@ const useSnake = () => { }, [isFood, isSnake]); // move the snake - useEffect(() => { - const runSingleStep = () => { - setSnake((snake) => { - const head = snake[0]; - - // 0 <= a % b < b - // so new x will always be inside the grid - const newHead = { - x: (head.x + direction.x + Config.height) % Config.height, - y: (head.y + direction.y + Config.width) % Config.width, - }; - - // make a new snake by extending head - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax - const newSnake = [newHead, ...snake]; - - // reset the game if the snake hit itself - if (isSnake(newHead)) { - resetGame(); - return getDefaultSnake(); - } - - // remove tail from the increased size snake - // only if the newHead isn't a food - if (!isFood(newHead)) { - newSnake.pop(); - } else { - setFoods((currentFoods) => - currentFoods.filter( - (food) => !(food.x === newHead.x && food.y === newHead.y) - ) - ); - } - - return newSnake; - }); - }; + const runSingleStep = useCallback(() => { + setSnake((snake) => { + const head = snake[0]; + + // 0 <= a % b < b + // so new x will always be inside the grid + const newHead = { + x: (head.x + direction.x + Config.height) % Config.height, + y: (head.y + direction.y + Config.width) % Config.width, + }; + + // make a new snake by extending head + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax + const newSnake = [newHead, ...snake]; + + // reset the game if the snake hit itself + if (isSnake(newHead)) { + resetGame(); + return getDefaultSnake(); + } - runSingleStep(); - const timer = setInterval(runSingleStep, 500); + // remove tail from the increased size snake + // only if the newHead isn't a food + if (!isFood(newHead)) { + newSnake.pop(); + } else { + setFoods((currentFoods) => + currentFoods.filter( + (food) => !(food.x === newHead.x && food.y === newHead.y) + ) + ); + } - return () => clearInterval(timer); - }, [direction, foods, isFood, resetGame, isSnake]); + return newSnake; + }); + }, [direction, isFood, isSnake, resetGame]); - useEffect(() => { - // add a food in a 3s interval - const createFoodIntervalId = setInterval(() => { - addFood(); - }, 3000); - - // run the remove function each second, - // but the function will decide which foods are - // older than 10s and delete them - const removeFoodIntervalId = setInterval(() => { - removeFoods(); - }, 1000); - - return () => { - clearInterval(createFoodIntervalId); - clearInterval(removeFoodIntervalId); - }; - }, [addFood, removeFoods]); + useInterval(runSingleStep, 200); + useInterval(addFood, 3000); + useInterval(removeFoods, 100); useEffect(() => { const handleDirection = (direction, oppositeDirection) => { @@ -204,29 +215,26 @@ const useSnake = () => { return () => window.removeEventListener("keydown", handleNavigation); }, []); - // ?. is called optional chaining - // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining - const isFood = useCallback( - ({ x, y }) => foods.some((food) => food.x === x && food.y === y), - [foods] - ); - - const isSnake = useCallback( - ({ x, y }) => - snake.find((position) => position.x === x && position.y === y), - [snake] - ); - const cells = []; for (let x = 0; x < Config.width; x++) { for (let y = 0; y < Config.height; y++) { - let type = CellType.Empty; + let type = CellType.Empty, + remaining = undefined; if (isFood({ x, y })) { type = CellType.Food; + remaining = + 10 - + Math.round( + (Date.now() - + foods.find((food) => food.x === x && food.y === y).createdAt) / + 1000 + ); } else if (isSnake({ x, y })) { type = CellType.Snake; } - cells.push(); + cells.push( + + ); } } diff --git a/styles/Snake.module.css b/styles/Snake.module.css index b4f0f28..7f8040f 100644 --- a/styles/Snake.module.css +++ b/styles/Snake.module.css @@ -34,4 +34,8 @@ .cell { width: 100%; height: 100%; + display: flex; + justify-content: center; + align-items: center; + color: white; }