From c0989adfb9e90a4c0dd8ee22b3b8ba5f4e75177c Mon Sep 17 00:00:00 2001 From: royantar0311 Date: Fri, 18 Mar 2022 04:11:57 +0600 Subject: [PATCH 1/4] module-1 --- pages/index.js | 135 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 105 insertions(+), 30 deletions(-) diff --git a/pages/index.js b/pages/index.js index de83024..bffd478 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,5 +1,5 @@ import dynamic from "next/dynamic"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState, useCallback } from "react"; import styles from "../styles/Snake.module.css"; const Config = { @@ -61,36 +61,86 @@ const Cell = ({ x, y, type }) => { const getRandomCell = () => ({ x: Math.floor(Math.random() * Config.width), y: Math.floor(Math.random() * Config.width), + createdAt: Date.now(), }); -const Snake = () => { +const getInitialFoods = () => [{ x: 4, y: 10, createdAt: Date.now() }]; + +const getInitialDirection = () => Direction.Right; + +const useSnake = () => { const getDefaultSnake = () => [ { x: 8, y: 12 }, { x: 7, y: 12 }, { x: 6, y: 12 }, ]; - const grid = useRef(); // snake[0] is head and snake[snake.length - 1] is tail const [snake, setSnake] = useState(getDefaultSnake()); - const [direction, setDirection] = useState(Direction.Right); + const [direction, setDirection] = useState(getInitialDirection()); + + const [foods, setFoods] = useState(getInitialFoods()); + + const score = snake.length - 3; + + // useCallback() prevents instantiation of a function on each rerender + // based on the dependency array + + // resets the snake ,foods, direction to initial values + const resetGame = useCallback(() => { + setFoods(getInitialFoods()); + setDirection(getInitialDirection()); + }, []); + + const removeFoods = useCallback(() => { + // only keep those foods which were created within last 10s. + setFoods((currentFoods) => + currentFoods.filter((food) => Date.now() - food.createdAt <= 10 * 1000) + ); + }, []); - const [food, setFood] = useState({ x: 4, y: 10 }); - const [score, setScore] = useState(0); + const addFood = useCallback(() => { + let newFood = getRandomCell(); + while (isSnake(newFood) || isFood(newFood)) { + newFood = getRandomCell(); + } + setFoods((currentFoods) => [...currentFoods, newFood]); + }, [isFood, isSnake]); // move the snake useEffect(() => { const runSingleStep = () => { setSnake((snake) => { const head = snake[0]; - const newHead = { x: head.x + direction.x, y: head.y + direction.y }; + + // 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]; - // remove tail - newSnake.pop(); + // 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; }); @@ -100,42 +150,52 @@ const Snake = () => { const timer = setInterval(runSingleStep, 500); return () => clearInterval(timer); - }, [direction, food]); + }, [direction, foods, isFood, resetGame, isSnake]); - // update score whenever head touches a food useEffect(() => { - const head = snake[0]; - if (isFood(head)) { - setScore((score) => { - return score + 1; - }); + // add a food in a 3s interval + const createFoodIntervalId = setInterval(() => { + addFood(); + }, 3000); - let newFood = getRandomCell(); - while (isSnake(newFood)) { - newFood = getRandomCell(); - } + // 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); - setFood(newFood); - } - }, [snake]); + return () => { + clearInterval(createFoodIntervalId); + clearInterval(removeFoodIntervalId); + }; + }, [addFood, removeFoods]); useEffect(() => { + const handleDirection = (direction, oppositeDirection) => { + setDirection((currentDirection) => { + if (currentDirection === oppositeDirection) { + return currentDirection; + } else return direction; + }); + }; + const handleNavigation = (event) => { switch (event.key) { case "ArrowUp": - setDirection(Direction.Top); + handleDirection(Direction.Top, Direction.Bottom); break; case "ArrowDown": - setDirection(Direction.Bottom); + handleDirection(Direction.Bottom, Direction.Top); break; case "ArrowLeft": - setDirection(Direction.Left); + handleDirection(Direction.Left, Direction.Right); break; case "ArrowRight": - setDirection(Direction.Right); + handleDirection(Direction.Right, Direction.Left); break; } }; @@ -146,10 +206,16 @@ const Snake = () => { // ?. is called optional chaining // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining - const isFood = ({ x, y }) => food?.x === x && food?.y === y; + const isFood = useCallback( + ({ x, y }) => foods.some((food) => food.x === x && food.y === y), + [foods] + ); - const isSnake = ({ x, y }) => - snake.find((position) => position.x === x && position.y === y); + 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++) { @@ -164,6 +230,15 @@ const Snake = () => { } } + return { + snake, + cells, + score, + }; +}; + +const Snake = () => { + const { cells, score } = useSnake(); return (
Date: Mon, 21 Mar 2022 18:54:12 +0600 Subject: [PATCH 2/4] Finish module #1 + extras --- pages/index.js | 172 +++++++++++++++++++++------------------- styles/Snake.module.css | 4 + 2 files changed, 94 insertions(+), 82 deletions(-) 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; } From 86c7be5ae499ec2f72218e928c8c5d8cbe7ae048 Mon Sep 17 00:00:00 2001 From: mozammalrahat Date: Wed, 6 Apr 2022 02:53:10 +0600 Subject: [PATCH 3/4] Poison added --- pages/index.js | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/pages/index.js b/pages/index.js index 2f8fb7b..351023c 100644 --- a/pages/index.js +++ b/pages/index.js @@ -12,6 +12,7 @@ const CellType = { Snake: "snake", Food: "food", Empty: "empty", + Poison: "poison", }; const Direction = { @@ -39,6 +40,14 @@ const Cell = ({ x, y, type, remaining }) => { height: 32, transform: `scale(${0.5 + remaining / 20})`, }; + case CellType.Poison: + return { + backgroundColor: "black", + borderRadius: 20, + width: 32, + height: 32, + transform: `scale(${0.5 + remaining / 20})`, + }; default: return {}; @@ -100,6 +109,7 @@ const useSnake = () => { const [direction, setDirection] = useState(getInitialDirection()); const [foods, setFoods] = useState([]); + const [poison, setPoison] = useState({}); const score = snake.length - 3; @@ -110,6 +120,7 @@ const useSnake = () => { const resetGame = useCallback(() => { setFoods([]); setDirection(getInitialDirection()); + setPoison({}); }, []); const removeFoods = useCallback(() => { @@ -132,13 +143,23 @@ const useSnake = () => { [snake] ); + const isPoison = useCallback( ({x,y}) => { return poison.x === x && poison.y === y}, [poison]) + const addFood = useCallback(() => { let newFood = getRandomCell(); - while (isSnake(newFood) || isFood(newFood)) { + while (isSnake(newFood) || isFood(newFood) || isPoison(newFood)) { newFood = getRandomCell(); } setFoods((currentFoods) => [...currentFoods, newFood]); }, [isFood, isSnake]); + + const addPoison = useCallback(() => { + let newPoison = getRandomCell(); + while (isSnake(newPoison) || isFood(newPoison) || isPoison(newPoison)) { + newPoison = getRandomCell(); + } + setPoison(newPoison); + },[isFood, isSnake]) // move the snake const runSingleStep = useCallback(() => { @@ -162,6 +183,11 @@ const useSnake = () => { return getDefaultSnake(); } + if (isPoison(newHead)) { + resetGame(); + return getDefaultSnake(); + } + // remove tail from the increased size snake // only if the newHead isn't a food if (!isFood(newHead)) { @@ -181,6 +207,7 @@ const useSnake = () => { useInterval(runSingleStep, 200); useInterval(addFood, 3000); useInterval(removeFoods, 100); + useInterval(addPoison, 5000); useEffect(() => { const handleDirection = (direction, oppositeDirection) => { @@ -232,6 +259,16 @@ const useSnake = () => { } else if (isSnake({ x, y })) { type = CellType.Snake; } + else if (isPoison({x,y})) { + type = CellType.Poison; + remaining = + 5 - + Math.round( + (Date.now() - + poison.createdAt) / + 1000 + ); + } cells.push( ); From ded74ef9510e3ce9ec125fbb0bea165bd24ce2c2 Mon Sep 17 00:00:00 2001 From: mozammalrahat Date: Thu, 14 Apr 2022 22:13:12 +0600 Subject: [PATCH 4/4] removed some redundancies --- .eslintrc.json | 3 +++ pages/index.js | 49 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 9671fde..4f75410 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,5 +3,8 @@ "env": { "es6": true, "browser": true + }, + "rules":{ + "react-hooks/exhaustive-deps": "error" } } diff --git a/pages/index.js b/pages/index.js index 351023c..8b0668e 100644 --- a/pages/index.js +++ b/pages/index.js @@ -104,6 +104,7 @@ const useSnake = () => { { x: 6, y: 12 }, ]; + // snake[0] is head and snake[snake.length - 1] is tail const [snake, setSnake] = useState(getDefaultSnake()); const [direction, setDirection] = useState(getInitialDirection()); @@ -116,12 +117,17 @@ const useSnake = () => { // useCallback() prevents instantiation of a function on each rerender // based on the dependency array + + + // resets the snake ,foods, direction to initial values const resetGame = useCallback(() => { - setFoods([]); + + setFoods([getEmptyCell()]); setDirection(getInitialDirection()); setPoison({}); - }, []); + + }, [getEmptyCell]); const removeFoods = useCallback(() => { // only keep those foods which were created within last 10s. @@ -144,22 +150,33 @@ const useSnake = () => { ); const isPoison = useCallback( ({x,y}) => { return poison.x === x && poison.y === y}, [poison]) + const isOccupied = useCallback((cell) => isFood(cell) || isSnake(cell) || isPoison(cell), [isFood, isSnake, isPoison]); - const addFood = useCallback(() => { - let newFood = getRandomCell(); - while (isSnake(newFood) || isFood(newFood) || isPoison(newFood)) { - newFood = getRandomCell(); + const getEmptyCell = useCallback(() =>{ + let newCell = getRandomCell(); + while (isOccupied(newCell)) { + newCell = getRandomCell(); } - setFoods((currentFoods) => [...currentFoods, newFood]); - }, [isFood, isSnake]); + return newCell; + },[isOccupied]); + + const addFood = useCallback(() => { + setFoods((currentFoods) => [...currentFoods, getEmptyCell()]); + }, [getEmptyCell]); const addPoison = useCallback(() => { - let newPoison = getRandomCell(); - while (isSnake(newPoison) || isFood(newPoison) || isPoison(newPoison)) { - newPoison = getRandomCell(); + setPoison(getEmptyCell()); + },[getEmptyCell]) + + const addObject = (typeOfObject = "food")=>{ + if(typeOfObject === "food"){ + addFood() } - setPoison(newPoison); - },[isFood, isSnake]) + else if(typeOfObject === "poison"){ + addPoison() + } + } + // move the snake const runSingleStep = useCallback(() => { @@ -202,12 +219,12 @@ const useSnake = () => { return newSnake; }); - }, [direction, isFood, isSnake, resetGame]); + }, [direction, isFood, isSnake, isPoison, resetGame]); useInterval(runSingleStep, 200); - useInterval(addFood, 3000); + useInterval(()=>addObject("food"), 3000); + useInterval(()=>addObject("poison"), 5000); useInterval(removeFoods, 100); - useInterval(addPoison, 5000); useEffect(() => { const handleDirection = (direction, oppositeDirection) => {