Skip to content

Commit

Permalink
Finish module #1 + extras
Browse files Browse the repository at this point in the history
  • Loading branch information
Dhananjoy committed Mar 21, 2022
1 parent 691dd08 commit e85ce2a
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 82 deletions.
172 changes: 90 additions & 82 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -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 = {
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,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 (
<div
key={`${x}-${y}`}
className={styles.cellContainer}
style={{
left: x * Config.cellSize,
Expand All @@ -53,7 +56,9 @@ const Cell = ({ x, y, type }) => {
height: Config.cellSize,
}}
>
<div className={styles.cell} style={getStyles()}></div>
<div className={styles.cell} style={getStyles()}>
{remaining}
</div>
</div>
);
};
Expand All @@ -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 },
Expand All @@ -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;

Expand All @@ -88,7 +108,7 @@ const useSnake = () => {

// resets the snake ,foods, direction to initial values
const resetGame = useCallback(() => {
setFoods(getInitialFoods());
setFoods([]);
setDirection(getInitialDirection());
}, []);

Expand All @@ -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)) {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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(<Cell key={`${x}-${y}`} x={x} y={y} type={type} />);
cells.push(
<Cell key={`${x}-${y}`} x={x} y={y} type={type} remaining={remaining} />
);
}
}

Expand Down
4 changes: 4 additions & 0 deletions styles/Snake.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@
.cell {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: white;
}

0 comments on commit e85ce2a

Please sign in to comment.