Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
9,811 changes: 9,811 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

186 changes: 1 addition & 185 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -1,189 +1,5 @@
import dynamic from "next/dynamic";
import { useEffect, useState, useRef } from "react";
import styles from "../styles/Snake.module.css";

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

const CellType = {
Snake: "snake",
Food: "food",
Empty: "empty",
};

const Direction = {
Left: { x: -1, y: 0 },
Right: { x: 1, y: 0 },
Top: { x: 0, y: -1 },
Bottom: { x: 0, y: 1 },
};

const Cell = ({ x, y, type }) => {
const getStyles = () => {
switch (type) {
case CellType.Snake:
return {
backgroundColor: "yellowgreen",
borderRadius: 8,
padding: 2,
};

case CellType.Food:
return {
backgroundColor: "darkorange",
borderRadius: 20,
width: 32,
height: 32,
};

default:
return {};
}
};
return (
<div
className={styles.cellContainer}
style={{
left: x * Config.cellSize,
top: y * Config.cellSize,
width: Config.cellSize,
height: Config.cellSize,
}}
>
<div className={styles.cell} style={getStyles()}></div>
</div>
);
};

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

const Snake = () => {
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 [food, setFood] = useState({ x: 4, y: 10 });
const [score, setScore] = useState(0);

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

// 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();

return newSnake;
});
};

runSingleStep();
const timer = setInterval(runSingleStep, 500);

return () => clearInterval(timer);
}, [direction, food]);

// update score whenever head touches a food
useEffect(() => {
const head = snake[0];
if (isFood(head)) {
setScore((score) => {
return score + 1;
});

let newFood = getRandomCell();
while (isSnake(newFood)) {
newFood = getRandomCell();
}

setFood(newFood);
}
}, [snake]);

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

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

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

case "ArrowRight":
setDirection(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;
if (isFood({ x, y })) {
type = CellType.Food;
} else if (isSnake({ x, y })) {
type = CellType.Snake;
}
cells.push(<Cell key={`${x}-${y}`} x={x} y={y} type={type} />);
}
}

return (
<div className={styles.container}>
<div
className={styles.header}
style={{ width: Config.width * Config.cellSize }}
>
Score: {score}
</div>
<div
className={styles.grid}
style={{
height: Config.height * Config.cellSize,
width: Config.width * Config.cellSize,
}}
>
{cells}
</div>
</div>
);
};
import { Snake } from "../src/components/Snake";

export default dynamic(() => Promise.resolve(Snake), {
ssr: false,
Expand Down
49 changes: 49 additions & 0 deletions src/components/Cell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { CellType, Config } from "../constants";
import styles from "../../styles/Snake.module.css";
export const Cell = ({ x, y, type, remaining }) => {
const getStyles = () => {
switch (type) {
case CellType.Snake:
return {
backgroundColor: "yellowgreen",
borderRadius: 8,
padding: 2,
};

case CellType.Food:
return {
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:
return {};
}
};
return (
<div
className={styles.cellContainer}
style={{
left: x * Config.cellSize,
top: y * Config.cellSize,
width: Config.cellSize,
height: Config.cellSize,
}}
>
<div className={styles.cell} style={getStyles()}>
{remaining}
</div>
</div>
);
};
27 changes: 27 additions & 0 deletions src/components/Snake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Config } from "../constants";

import styles from "../../styles/Snake.module.css";
import { useSnake } from "../hooks/snake";
export const Snake = () => {
const { score, cells } = useSnake();

return (
<div className={styles.container}>
<div
className={styles.header}
style={{ width: Config.width * Config.cellSize }}
>
Score: {score}
</div>
<div
className={styles.grid}
style={{
height: Config.height * Config.cellSize,
width: Config.width * Config.cellSize,
}}
>
{cells}
</div>
</div>
);
};
19 changes: 19 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const Config = {
height: 25,
width: 25,
cellSize: 32,
};

export const CellType = {
Snake: "snake",
Food: "food",
Poison: "poison",
Empty: "empty",
};

export const Direction = {
Left: { x: -1, y: 0 },
Right: { x: 1, y: 0 },
Top: { x: 0, y: -1 },
Bottom: { x: 0, y: 1 },
};
11 changes: 11 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Config } from "./constants";
export const getRandomCell = () => ({
x: Math.floor(Math.random() * Config.width),
y: Math.floor(Math.random() * Config.width),
createdAt: Date.now(),
});
export const getDefaultSnake = () => [
{ x: 8, y: 12 },
{ x: 7, y: 12 },
{ x: 6, y: 12 },
];
14 changes: 14 additions & 0 deletions src/hooks/interval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useCallback, useRef, useEffect } from "react";
export const useInterval = (callback, delay) => {
const time = useRef(0);
const wrappedCallback = useCallback(() => {
if (Date.now() - time.current >= delay) {
time.current = Date.now();
callback();
}
}, [callback, delay]);
useEffect(() => {
const interval = setInterval(wrappedCallback, 1000 / 60);
return () => clearInterval(interval);
}, [wrappedCallback]);
};
Loading