// snakeContainer.tsx
import { useEffect } from "react";
import SnakeGame from "./snakeGame";
import Screw from "./screw";
const SnakeContainer = () => {
useEffect(() => {
mountAnimation();
}, []);
const removeScrew = () => {
if (isLastScrew) {
dropSnakeGame();
}
if (isSecondToLastScrew) {
swingSnakeGame()
}
};
return (
<div className="absolute top-0 left-0 shrink-0 rounded-lg py-9 px-8 h-fit">
<Screw position="tl" onUnscrewed={removeScrew} />
<Screw position="tr" onUnscrewed={removeScrew} />
<Screw position="bl" onUnscrewed={removeScrew} />
<Screw position="br" onUnscrewed={removeScrew} />
<SnakeGame />
</div>
);
};
export default SnakeContainer;
// screw.tsx
import CloseSvg from "@/app/designed/svgs/icons/closeSvg";
import { useEffect } from "react";
type TParams = {
position: "tl" | "tr" | "bl" | "br";
onUnscrewed: () => void;
};
const Screw = ({ position, onUnscrewed }: TParams) => {
const turn = () => {
turnsLeft--;
if (turnsLeft >= 0) {
rotateScrew()
return;
}
onUnscrewed();
dropScrew();
};
return (
<button
className="rounded-full size-4 absolute flex items-center justify-center"
onClick={turn}
>
<CloseSvg />
</button>
);
};
export default Screw;
// snakeGame.tsx
import { useRef, useState, useEffect } from "react";
import SnakeControls from "./snakeControls";
enum GameState {
UNSTARTED,
PLAYING,
LOST,
WON,
}
const FoodPoint = ({ active = false }) => {
return (
<div
className="rounded-full ring-4 ring-portfolio-green-light-30 bg-portfolio-green-light-70 p-1 size-fit"
>
<div className="rounded-full size-2 bg-portfolio-green-light" />
</div>
);
};
const SnakeGame = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [snake, setSnake] = useState(initSnake);
const [food, setFood] = useState({ x: 15, y: 15 });
const [direction, setDirection] = useState({ x: 0, y: -1 });
const [gameState, setGameState] = useState<GameState>(GameState.UNSTARTED);
const [score, setScore] = useState(0);
const canvasWidth = 240;
const canvasHeight = 400;
const scale = 10;
const startGame = () => {
setSnake(initSnake);
setFood({
x: Math.floor((Math.random() * canvasWidth) / scale),
y: Math.floor((Math.random() * canvasHeight) / scale),
});
setDirection({ x: 0, y: -1 });
setScore(0);
setGameState(GameState.PLAYING);
};
const gameLoop = () => {
// Update snake position
const newSnake = [...snake];
const head = {
x: newSnake[0].x + direction.x,
y: newSnake[0].y + direction.y,
};
newSnake.unshift(head);
// Check for collisions
const collisionedWithWall =
head.x < 0 ||
head.x >= canvasWidth / scale ||
head.y < 0 ||
head.y >= canvasHeight / scale;
const collisionedSelf = newSnake
.slice(1)
.some((part) => part.x === head.x && part.y === head.y);
if (collisionedWithWall || collisionedSelf) {
setGameState(GameState.LOST);
return;
}
// Check if snake eats food
if (head.x === food.x && head.y === food.y) {
setFood({
x: Math.floor((Math.random() * canvasWidth) / scale),
y: Math.floor((Math.random() * canvasHeight) / scale),
});
setScore((curr) => {
const newScore = curr + 1;
if (newScore === 10) {
setGameState(GameState.WON);
}
return newScore;
});
} else {
newSnake.pop(); // Remove tail
}
setSnake(newSnake);
};
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case "ArrowUp":
// Cannot go backwards
if (snake[0].y - 1 === snake[1].y) break;
setDirection({ x: 0, y: -1 });
break;
case "ArrowDown":
// Cannot go backwards
if (snake[0].y + 1 === snake[1].y) break;
setDirection({ x: 0, y: 1 });
break;
case "ArrowLeft":
// Cannot go backwards
if (snake[0].x - 1 === snake[1].x) break;
setDirection({ x: -1, y: 0 });
break;
case "ArrowRight":
// Cannot go backwards
if (snake[0].x + 1 === snake[1].x) break;
setDirection({ x: 1, y: 0 });
break;
default:
break;
}
};
useEffect(() => {
const snakeSpeed = 100; // in miliseconds
const interval = setInterval(() => {
if (gameState === GameState.PLAYING) {
gameLoop();
}
}, snakeSpeed);
window.addEventListener("keydown", handleKeyDown);
return () => {
clearInterval(interval);
window.removeEventListener("keydown", handleKeyDown);
};
}, [snake, direction, gameState]);
useEffect(() => {
const context = canvasRef.current?.getContext("2d");
if (!context) return;
context.clearRect(0, 0, canvasWidth, canvasHeight);
// Draw snake
const percentPerPart = 100 / snake.length;
drawRoundedHead(context, snake[0], scale, direction);
for (let idx = 1; idx < snake.length; idx++) {
const { x, y } = snake[idx];
const percent = (percentPerPart * (snake.length - idx)) / 100;
context.fillStyle = "rgba(67, 217, 173, {percent})";
context.fillRect(x * scale, y * scale, scale, scale);
}
// Draw food
context.fillStyle = "rgba(67, 217, 173)";
context.beginPath();
context.roundRect(food.x * scale, food.y * scale, scale, scale, 100);
context.fill();
// Draw food ring 1
context.fillStyle = "rgba(67, 217, 173, 0.2)";
context.beginPath();
context.roundRect(
food.x * scale - scale / 2,
food.y * scale - scale / 2,
scale * 2,
scale * 2,
100
);
context.fill();
// Draw food ring 2
context.fillStyle = "rgba(67, 217, 173, 0.1)";
context.beginPath();
context.roundRect(
food.x * scale - scale,
food.y * scale - scale,
scale * 3,
scale * 3,
100
);
context.fill();
}, [snake, food, score]);
return (
<div className="flex gap-6">
<div className="flex relative">
<canvas
ref={canvasRef}
width={canvasWidth}
height={canvasHeight}
className="rounded-lg bg-portfolio-dark-100 shadow-[inset_1px_5px_11px_0_rgba(4,60,90,0.71)]"
/>
{gameStateUi[gameState](startGame)}
</div>
<div className="flex flex-col gap-5">
<div className="bg-black/15 space-y-4 p-3 rounded-lg">
<div className="text-white flex flex-col items-start">
<span>// use keyboard</span>
<span>// arrows to play</span>
</div>
<SnakeControls />
</div>
<div className="space-y-3 p-3">
<div>// food left</div>
<div className="grid grid-cols-5 gap-4">
{Array(10)
.fill(0)
.map((_, idx) => (
<FoodPoint active={score >= idx + 1} key={idx} />
))}
</div>
</div>
</div>
</div>
);
};
export default SnakeGame;
revertir