quick-swift-tool / components /SnakeGame.jsx
imweijh's picture
Upload components/SnakeGame.jsx with huggingface_hub
b114a4d verified
import { useState, useEffect, useRef, useCallback } from 'react'
const BOARD_SIZE = 20
const INITIAL_SNAKE = [{ x: 10, y: 10 }]
const INITIAL_FOOD = { x: 15, y: 15 }
const INITIAL_DIRECTION = { x: 0, y: -1 }
const GAME_SPEED = 150
const DIRECTIONS = {
UP: { x: 0, y: -1 },
DOWN: { x: 0, y: 1 },
LEFT: { x: -1, y: 0 },
RIGHT: { x: 1, y: 0 }
}
export default function SnakeGame() {
const canvasRef = useRef(null)
const gameLoopRef = useRef(null)
const [snake, setSnake] = useState(INITIAL_SNAKE)
const [food, setFood] = useState(INITIAL_FOOD)
const [direction, setDirection] = useState(INITIAL_DIRECTION)
const [gameOver, setGameOver] = useState(false)
const [score, setScore] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
const [isPaused, setIsPaused] = useState(false)
// Generate random food position
const generateFood = useCallback((currentSnake) => {
let newFood
do {
newFood = {
x: Math.floor(Math.random() * BOARD_SIZE),
y: Math.floor(Math.random() * BOARD_SIZE)
}
} while (currentSnake.some(segment => segment.x === newFood.x && segment.y === newFood.y))
return newFood
}, [])
// Check collision with walls or self
const checkCollision = useCallback((head, body) => {
// Wall collision
if (head.x < 0 || head.x >= BOARD_SIZE || head.y < 0 || head.y >= BOARD_SIZE) {
return true
}
// Self collision
return body.some(segment => segment.x === head.x && segment.y === head.y)
}, [])
// Move snake
const moveSnake = useCallback(() => {
if (gameOver || isPaused || !isPlaying) return
setSnake(currentSnake => {
const newSnake = [...currentSnake]
const head = { ...newSnake[0] }
head.x += direction.x
head.y += direction.y
// Check collision
if (checkCollision(head, newSnake)) {
setGameOver(true)
setIsPlaying(false)
return currentSnake
}
newSnake.unshift(head)
// Check if food is eaten
if (head.x === food.x && head.y === food.y) {
setScore(prev => prev + 10)
setFood(generateFood(newSnake))
} else {
newSnake.pop()
}
return newSnake
})
}, [direction, food, gameOver, isPaused, isPlaying, checkCollision, generateFood])
// Game loop
useEffect(() => {
if (isPlaying && !gameOver && !isPaused) {
gameLoopRef.current = setInterval(moveSnake, GAME_SPEED)
} else {
clearInterval(gameLoopRef.current)
}
return () => clearInterval(gameLoopRef.current)
}, [isPlaying, gameOver, isPaused, moveSnake])
// Draw game
const draw = useCallback(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
const cellSize = canvas.width / BOARD_SIZE
// Clear canvas
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Draw snake
ctx.fillStyle = '#4ade80'
snake.forEach((segment, index) => {
if (index === 0) {
// Head
ctx.fillStyle = '#22c55e'
} else {
ctx.fillStyle = '#4ade80'
}
ctx.fillRect(
segment.x * cellSize,
segment.y * cellSize,
cellSize - 2,
cellSize - 2
)
})
// Draw food
ctx.fillStyle = '#ef4444'
ctx.fillRect(
food.x * cellSize,
food.y * cellSize,
cellSize - 2,
cellSize - 2
)
// Draw grid
ctx.strokeStyle = '#333'
ctx.lineWidth = 1
for (let i = 0; i <= BOARD_SIZE; i++) {
ctx.beginPath()
ctx.moveTo(i * cellSize, 0)
ctx.lineTo(i * cellSize, canvas.height)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(0, i * cellSize)
ctx.lineTo(canvas.width, i * cellSize)
ctx.stroke()
}
}, [snake, food])
// Redraw when snake or food changes
useEffect(() => {
draw()
}, [draw])
// Handle keyboard input
const handleKeyPress = useCallback((e) => {
if (!isPlaying) return
const { key } = e
const currentDirection = direction
switch (key) {
case 'ArrowUp':
case 'w':
case 'W':
if (currentDirection.y !== 1) {
setDirection(DIRECTIONS.UP)
}
break
case 'ArrowDown':
case 's':
case 'S':
if (currentDirection.y !== -1) {
setDirection(DIRECTIONS.DOWN)
}
break
case 'ArrowLeft':
case 'a':
case 'A':
if (currentDirection.x !== 1) {
setDirection(DIRECTIONS.LEFT)
}
break
case 'ArrowRight':
case 'd':
case 'D':
if (currentDirection.x !== -1) {
setDirection(DIRECTIONS.RIGHT)
}
break
case ' ':
e.preventDefault()
setIsPaused(prev => !prev)
break
}
}, [isPlaying, direction])
useEffect(() => {
window.addEventListener('keydown', handleKeyPress)
return () => window.removeEventListener('keydown', handleKeyPress)
}, [handleKeyPress])
// Touch controls
const handleDirection = useCallback((newDirection) => {
if (!isPlaying || isPaused) return
const currentDirection = direction
// Prevent reverse direction
if (
(newDirection === DIRECTIONS.UP && currentDirection.y === 1) ||
(newDirection === DIRECTIONS.DOWN && currentDirection.y === -1) ||
(newDirection === DIRECTIONS.LEFT && currentDirection.x === 1) ||
(newDirection === DIRECTIONS.RIGHT && currentDirection.x === -1)
) {
return
}
setDirection(newDirection)
}, [isPlaying, isPaused, direction])
// Start new game
const startNewGame = () => {
setSnake(INITIAL_SNAKE)
setFood(generateFood(INITIAL_SNAKE))
setDirection(INITIAL_DIRECTION)
setGameOver(false)
setScore(0)
setIsPlaying(true)
setIsPaused(false)
}
return (
<div className="flex flex-col items-center space-y-6">
{/* Score */}
<div className="text-center">
<div className="score-display">
Score: {score}
</div>
<div className="text-gray-400">
Length: {snake.length}
</div>
</div>
{/* Game Canvas */}
<div className="relative">
<canvas
ref={canvasRef}
width={400}
height={400}
className="block bg-gray-900"
/>
{/* Game Over Overlay */}
{gameOver && (
<div className="absolute inset-0 bg-black bg-opacity-80 flex items-center justify-center rounded-lg">
<div className="text-center text-white">
<h2 className="text-3xl font-bold mb-4">Game Over!</h2>
<p className="text-xl mb-4">Final Score: {score}</p>
<button
onClick={startNewGame}
className="game-button"
>
Play Again
</button>
</div>
</div>
)}
{/* Pause Overlay */}
{isPaused && !gameOver && (
<div className="absolute inset-0 bg-black bg-opacity-60 flex items-center justify-center rounded-lg">
<div className="text-center text-white">
<h2 className="text-3xl font-bold mb-4">Paused</h2>
<p className="text-lg">Press SPACE or tap Resume to continue</p>
</div>
</div>
)}
</div>
{/* Controls */}
<div className="flex flex-col items-center space-y-4">
{!isPlaying || gameOver ? (
<button
onClick={startNewGame}
className="game-button text-lg px-8 py-4"
>
{gameOver ? 'Play Again' : 'Start Game'}
</button>
) : (
<div className="flex space-x-4">
<button
onClick={() => setIsPaused(!isPaused)}
className="game-button"
>
{isPaused ? 'Resume' : 'Pause'}
</button>
<button
onClick={startNewGame}
className="game-button bg-red-600 hover:bg-red-700"
>
Restart
</button>
</div>
)}
</div>
{/* Mobile Touch Controls */}
<div className="md:hidden">
<div className="text-center text-white mb-4">Touch Controls</div>
<div className="grid grid-cols-3 gap-2 w-48">
<div></div>
<button
onClick={() => handleDirection(DIRECTIONS.UP)}
className="control-button text-2xl h-16"
disabled={!isPlaying || isPaused}
>
↑
</button>
<div></div>
<button
onClick={() => handleDirection(DIRECTIONS.LEFT)}
className="control-button text-2xl h-16"
disabled={!isPlaying || isPaused}
>
←
</button>
<div></div>
<button
onClick={() => handleDirection(DIRECTIONS.RIGHT)}
className="control-button text-2xl h-16"
disabled={!isPlaying || isPaused}
>
β†’
</button>
<div></div>
<button
onClick={() => handleDirection(DIRECTIONS.DOWN)}
className="control-button text-2xl h-16"
disabled={!isPlaying || isPaused}
>
↓
</button>
<div></div>
</div>
</div>
{/* Instructions */}
<div className="text-center text-gray-400 text-sm max-w-md">
<p className="mb-2">
<strong>Keyboard:</strong> Arrow keys or WASD to move, SPACE to pause
</p>
<p>
<strong>Mobile:</strong> Use touch controls below
</p>
</div>
</div>
)
}