Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module #2 (Nowshin Owishi) #29

Open
wants to merge 11 commits into
base: module-2
Choose a base branch
from
Open
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 163 additions & 52 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import dynamic from "next/dynamic";
import { useEffect, useState, useRef } from "react";
import { useEffect, useState, useRef, useCallback } from "react";
import styles from "../styles/Snake.module.css";

const Config = {
height: 25,
width: 25,
height: 15,
width: 15,
cellSize: 32,
};

Expand All @@ -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:
Expand All @@ -33,10 +33,20 @@ 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})`,
};

case CellType.Poison:
return {
backgroundColor: "red",
borderRadius: 20,
width: 32,
height: 32,
transform: `scale(${0.5 + remaining / 20})`,
};

default:
Expand All @@ -53,17 +63,36 @@ const Cell = ({ x, y, type }) => {
height: Config.cellSize,
}}
>
<div className={styles.cell} style={getStyles()}></div>
<div className={styles.cell} style={getStyles()}>
{remaining}
</div>
</div>
);
};

const getRandomCell = () => ({
x: Math.floor(Math.random() * Config.width),
y: Math.floor(Math.random() * Config.width),
start: Date.now(),
});

const Snake = () => {
//custom hook
//controller
const UseInterval = (func, dir) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

custom hooks name should start with use with a lowercase u and a react components name should start with a capital letter.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

custom hooks name should start with use with a lowercase u and a react components name should start with a capital letter.

@owishiboo

const timer = useRef(Date.now());
const createCallback = useCallback(() => {
if (Date.now() - timer.current > dir) {
timer.current = Date.now();
func();
}
}, [dir, func]);

useEffect(() => {
const interval = setInterval(createCallback, 1000 / 60);
return () => clearInterval(interval);
}, [createCallback]);
};
const UseSnake = () => {
Copy link

@MahdiMurshed MahdiMurshed Apr 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix naming convention

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UseSnake is not a hook here. I'm not sure, should I remove the term 'Use'?

const getDefaultSnake = () => [
{ x: 8, y: 12 },
{ x: 7, y: 12 },
Expand All @@ -74,96 +103,178 @@ const Snake = () => {
// snake[0] is head and snake[snake.length - 1] is tail
const [snake, setSnake] = useState(getDefaultSnake());
const [direction, setDirection] = useState(Direction.Right);
const [foods, setFoods] = useState([]);
const [poisons, setPoison] = useState([]);
const score = snake.length - 3;

const [food, setFood] = useState({ x: 4, y: 10 });
const [score, setScore] = useState(0);
// ?. is called optional chaining
const isFood = useCallback(
({ x, y }) =>
foods.find((position) => position.x === x && position.y === y),
[foods]
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as snake state is different from objects state,it makes sense to have a different function for checking snake.And it also makes the code more readable

  const isSnake = useCallback(
    ({ x, y }) =>
      snake.find((position) => position.x === x && position.y === y),
    [snake]
  );

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I give an upvote to this. @owishiboo your function can work with all three types but loses simplicity. poison and food are similiar and we put them to one object state, but snake is different so we can keep it seperate fully.
well there is no right or wrong here, it comes down to your opinion.

// move the snake
useEffect(() => {
const runSingleStep = () => {
setSnake((snake) => {
const head = snake[0];
const newHead = { x: head.x + direction.x, y: head.y + direction.y };
const isPoison = useCallback(
({ x, y }) =>
poisons.find((position) => position.x === x && position.y === y),
[poisons]
);

// make a new snake by extending head
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
const newSnake = [newHead, ...snake];
const isSnake = useCallback(
({ x, y }) =>
snake.find((position) => position.x === x && position.y === y),
[snake]
);

// remove tail
newSnake.pop();
//restart the game
const resetGame = useCallback(() => {
setSnake(getDefaultSnake());
setDirection(Direction.Right);
setFoods([]);
setPoison([]);
}, []);

return newSnake;
});
};
//moving the snake
const runSingleStep = useCallback(() => {
setSnake((snake) => {
const head = snake[0];
const newHead = {
x: (head.x + direction.x + Config.width) % Config.width,
y: (head.y + direction.y + Config.height) % Config.height,
};

// make a new snake by extending head
const newSnake = [newHead, ...snake];

runSingleStep();
const timer = setInterval(runSingleStep, 500);
// remove tail when head doesnt eat food
if (!isFood(newHead)) {
newSnake.pop();
}
//remove again when eats poison
if (isPoison(newHead)) {
newSnake.pop();
}
if (isSnake(newHead) || score < 0) {
Copy link

@royantar0311 royantar0311 Mar 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how weird would that be to play with a snake of length < 3 ? haha
also one more thing, you are popping the snake here but does the score gets updated here? no, you will get the updated value of score only on the next rerender. so you cannot rely on the score here to get the snake length, use newSnake.length here to avoid potential bugs.

resetGame();
}

return () => clearInterval(timer);
}, [direction, food]);
return newSnake;
});
}, [direction.x, direction.y, isFood, isPoison, isSnake, resetGame, score]);

// update score whenever head touches a food
const isBlocked = useCallback(
(cell) => isSnake(cell) || isFood(cell) || isPoison(cell),
[isFood, isPoison, isSnake]
);
const addObject = useCallback(
(flag) => {
let newObject = getRandomCell();
while (isBlocked(newObject)) {
newObject = getRandomCell();
}
if (flag === "food")
setFoods((currentFoods) => [...currentFoods, newObject]);
else setPoison((currentPoison) => [...currentPoison, newObject]);
},
[isBlocked]
);
const removeObject = useCallback((type) => {
if (type === "food")
setFoods((currentFoods) =>
currentFoods.filter((food) => Date.now() - food.start < 10000)
);
else
setPoison((currentPoison) =>
currentPoison.filter((poison) => Date.now() - poison.start < 10000)
);
}, []);

// update foods and poisons whenever head touches a food or a poison
useEffect(() => {
const head = snake[0];
if (isFood(head)) {
setScore((score) => {
return score + 1;
});
console.log("ate food");
setFoods((currentFoods) =>
currentFoods.filter((food) => !(food.x === head.x && food.y === head.y))
);
}
if (isPoison(head)) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Corei13
Thanks for the previous help.
Whenever the snake eats one poison, other poisons of the same row OR same column get deleted. But I'm checking both row AND column every time. The same happens with the food also. How to fix it? Everything else is working fine.

console.log("ate poison");
setPoison((currentPoison) =>
currentPoison.filter(
(poison) => !(poison.x === head.x && poison.y === head.y)
)
);
}
}, [isFood, isPoison, snake]);

let newFood = getRandomCell();
while (isSnake(newFood)) {
newFood = getRandomCell();
}
UseInterval(() => addObject("food"), 3000);
UseInterval(() => addObject("poison"), 1000);
UseInterval(runSingleStep, 300);
UseInterval(() => removeObject("food"), 50);
UseInterval(() => removeObject("poison"), 100);

setFood(newFood);
}
}, [snake]);
const changeDir = (checkDir, newDir) => {
setDirection((direction) => {
if (direction != checkDir) return newDir;
return direction;
});
};

useEffect(() => {
const handleNavigation = (event) => {
switch (event.key) {
case "ArrowUp":
setDirection(Direction.Top);
changeDir(Direction.Bottom, Direction.Top);
break;

case "ArrowDown":
setDirection(Direction.Bottom);
changeDir(Direction.Top, Direction.Bottom);
break;

case "ArrowLeft":
setDirection(Direction.Left);
changeDir(Direction.Right, Direction.Left);
break;

case "ArrowRight":
setDirection(Direction.Right);
changeDir(Direction.Left, Direction.Right);
break;
}
};
window.addEventListener("keydown", handleNavigation);

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 = ({ x, y }) => food?.x === x && food?.y === y;

const isSnake = ({ x, y }) =>
snake.find((position) => position.x === x && position.y === y);

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 =
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@royantar0311 I forgot to add the remaining time to the food, so I tried it out today. But the time shows undefined. I can't figure out the problem :")

10 -
Math.round(
(Date.now() -
foods.find((food) => food.x === x && food.y === y).createdAt) /
Copy link

@MahdiMurshed MahdiMurshed Apr 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as your cell looks like

{
x: Math.floor(Math.random() * Config.width),
  y: Math.floor(Math.random() * Config.width),
  start: Date.now()
}

.createdAt should be

 poisons.find((poison) => poison.x === x && poison.y === y)
                .start) /
              1000

@owishiboo

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, bro @MahdiMurshed

1000
);
} else if (isSnake({ x, y })) {
type = CellType.Snake;
} else if (isPoison({ x, y })) {
type = CellType.Poison;
}
cells.push(<Cell key={`${x}-${y}`} x={x} y={y} type={type} />);
cells.push(
<Cell key={`${x}-${y}`} x={x} y={y} type={type} remaining={remaining} />
);
}
}
return { score, isFood, isSnake, isPoison, cells };
};

//view
const Snake = () => {
const { score, cells } = UseSnake();
return (
<div className={styles.container}>
<div
Expand Down