imweijh commited on
Commit
b114a4d
Β·
verified Β·
1 Parent(s): 3795663

Upload components/SnakeGame.jsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. components/SnakeGame.jsx +354 -0
components/SnakeGame.jsx ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef, useCallback } from 'react'
2
+
3
+ const BOARD_SIZE = 20
4
+ const INITIAL_SNAKE = [{ x: 10, y: 10 }]
5
+ const INITIAL_FOOD = { x: 15, y: 15 }
6
+ const INITIAL_DIRECTION = { x: 0, y: -1 }
7
+ const GAME_SPEED = 150
8
+
9
+ const DIRECTIONS = {
10
+ UP: { x: 0, y: -1 },
11
+ DOWN: { x: 0, y: 1 },
12
+ LEFT: { x: -1, y: 0 },
13
+ RIGHT: { x: 1, y: 0 }
14
+ }
15
+
16
+ export default function SnakeGame() {
17
+ const canvasRef = useRef(null)
18
+ const gameLoopRef = useRef(null)
19
+ const [snake, setSnake] = useState(INITIAL_SNAKE)
20
+ const [food, setFood] = useState(INITIAL_FOOD)
21
+ const [direction, setDirection] = useState(INITIAL_DIRECTION)
22
+ const [gameOver, setGameOver] = useState(false)
23
+ const [score, setScore] = useState(0)
24
+ const [isPlaying, setIsPlaying] = useState(false)
25
+ const [isPaused, setIsPaused] = useState(false)
26
+
27
+ // Generate random food position
28
+ const generateFood = useCallback((currentSnake) => {
29
+ let newFood
30
+ do {
31
+ newFood = {
32
+ x: Math.floor(Math.random() * BOARD_SIZE),
33
+ y: Math.floor(Math.random() * BOARD_SIZE)
34
+ }
35
+ } while (currentSnake.some(segment => segment.x === newFood.x && segment.y === newFood.y))
36
+ return newFood
37
+ }, [])
38
+
39
+ // Check collision with walls or self
40
+ const checkCollision = useCallback((head, body) => {
41
+ // Wall collision
42
+ if (head.x < 0 || head.x >= BOARD_SIZE || head.y < 0 || head.y >= BOARD_SIZE) {
43
+ return true
44
+ }
45
+ // Self collision
46
+ return body.some(segment => segment.x === head.x && segment.y === head.y)
47
+ }, [])
48
+
49
+ // Move snake
50
+ const moveSnake = useCallback(() => {
51
+ if (gameOver || isPaused || !isPlaying) return
52
+
53
+ setSnake(currentSnake => {
54
+ const newSnake = [...currentSnake]
55
+ const head = { ...newSnake[0] }
56
+
57
+ head.x += direction.x
58
+ head.y += direction.y
59
+
60
+ // Check collision
61
+ if (checkCollision(head, newSnake)) {
62
+ setGameOver(true)
63
+ setIsPlaying(false)
64
+ return currentSnake
65
+ }
66
+
67
+ newSnake.unshift(head)
68
+
69
+ // Check if food is eaten
70
+ if (head.x === food.x && head.y === food.y) {
71
+ setScore(prev => prev + 10)
72
+ setFood(generateFood(newSnake))
73
+ } else {
74
+ newSnake.pop()
75
+ }
76
+
77
+ return newSnake
78
+ })
79
+ }, [direction, food, gameOver, isPaused, isPlaying, checkCollision, generateFood])
80
+
81
+ // Game loop
82
+ useEffect(() => {
83
+ if (isPlaying && !gameOver && !isPaused) {
84
+ gameLoopRef.current = setInterval(moveSnake, GAME_SPEED)
85
+ } else {
86
+ clearInterval(gameLoopRef.current)
87
+ }
88
+
89
+ return () => clearInterval(gameLoopRef.current)
90
+ }, [isPlaying, gameOver, isPaused, moveSnake])
91
+
92
+ // Draw game
93
+ const draw = useCallback(() => {
94
+ const canvas = canvasRef.current
95
+ if (!canvas) return
96
+
97
+ const ctx = canvas.getContext('2d')
98
+ const cellSize = canvas.width / BOARD_SIZE
99
+
100
+ // Clear canvas
101
+ ctx.fillStyle = '#000'
102
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
103
+
104
+ // Draw snake
105
+ ctx.fillStyle = '#4ade80'
106
+ snake.forEach((segment, index) => {
107
+ if (index === 0) {
108
+ // Head
109
+ ctx.fillStyle = '#22c55e'
110
+ } else {
111
+ ctx.fillStyle = '#4ade80'
112
+ }
113
+ ctx.fillRect(
114
+ segment.x * cellSize,
115
+ segment.y * cellSize,
116
+ cellSize - 2,
117
+ cellSize - 2
118
+ )
119
+ })
120
+
121
+ // Draw food
122
+ ctx.fillStyle = '#ef4444'
123
+ ctx.fillRect(
124
+ food.x * cellSize,
125
+ food.y * cellSize,
126
+ cellSize - 2,
127
+ cellSize - 2
128
+ )
129
+
130
+ // Draw grid
131
+ ctx.strokeStyle = '#333'
132
+ ctx.lineWidth = 1
133
+ for (let i = 0; i <= BOARD_SIZE; i++) {
134
+ ctx.beginPath()
135
+ ctx.moveTo(i * cellSize, 0)
136
+ ctx.lineTo(i * cellSize, canvas.height)
137
+ ctx.stroke()
138
+
139
+ ctx.beginPath()
140
+ ctx.moveTo(0, i * cellSize)
141
+ ctx.lineTo(canvas.width, i * cellSize)
142
+ ctx.stroke()
143
+ }
144
+ }, [snake, food])
145
+
146
+ // Redraw when snake or food changes
147
+ useEffect(() => {
148
+ draw()
149
+ }, [draw])
150
+
151
+ // Handle keyboard input
152
+ const handleKeyPress = useCallback((e) => {
153
+ if (!isPlaying) return
154
+
155
+ const { key } = e
156
+ const currentDirection = direction
157
+
158
+ switch (key) {
159
+ case 'ArrowUp':
160
+ case 'w':
161
+ case 'W':
162
+ if (currentDirection.y !== 1) {
163
+ setDirection(DIRECTIONS.UP)
164
+ }
165
+ break
166
+ case 'ArrowDown':
167
+ case 's':
168
+ case 'S':
169
+ if (currentDirection.y !== -1) {
170
+ setDirection(DIRECTIONS.DOWN)
171
+ }
172
+ break
173
+ case 'ArrowLeft':
174
+ case 'a':
175
+ case 'A':
176
+ if (currentDirection.x !== 1) {
177
+ setDirection(DIRECTIONS.LEFT)
178
+ }
179
+ break
180
+ case 'ArrowRight':
181
+ case 'd':
182
+ case 'D':
183
+ if (currentDirection.x !== -1) {
184
+ setDirection(DIRECTIONS.RIGHT)
185
+ }
186
+ break
187
+ case ' ':
188
+ e.preventDefault()
189
+ setIsPaused(prev => !prev)
190
+ break
191
+ }
192
+ }, [isPlaying, direction])
193
+
194
+ useEffect(() => {
195
+ window.addEventListener('keydown', handleKeyPress)
196
+ return () => window.removeEventListener('keydown', handleKeyPress)
197
+ }, [handleKeyPress])
198
+
199
+ // Touch controls
200
+ const handleDirection = useCallback((newDirection) => {
201
+ if (!isPlaying || isPaused) return
202
+
203
+ const currentDirection = direction
204
+ // Prevent reverse direction
205
+ if (
206
+ (newDirection === DIRECTIONS.UP && currentDirection.y === 1) ||
207
+ (newDirection === DIRECTIONS.DOWN && currentDirection.y === -1) ||
208
+ (newDirection === DIRECTIONS.LEFT && currentDirection.x === 1) ||
209
+ (newDirection === DIRECTIONS.RIGHT && currentDirection.x === -1)
210
+ ) {
211
+ return
212
+ }
213
+
214
+ setDirection(newDirection)
215
+ }, [isPlaying, isPaused, direction])
216
+
217
+ // Start new game
218
+ const startNewGame = () => {
219
+ setSnake(INITIAL_SNAKE)
220
+ setFood(generateFood(INITIAL_SNAKE))
221
+ setDirection(INITIAL_DIRECTION)
222
+ setGameOver(false)
223
+ setScore(0)
224
+ setIsPlaying(true)
225
+ setIsPaused(false)
226
+ }
227
+
228
+ return (
229
+ <div className="flex flex-col items-center space-y-6">
230
+ {/* Score */}
231
+ <div className="text-center">
232
+ <div className="score-display">
233
+ Score: {score}
234
+ </div>
235
+ <div className="text-gray-400">
236
+ Length: {snake.length}
237
+ </div>
238
+ </div>
239
+
240
+ {/* Game Canvas */}
241
+ <div className="relative">
242
+ <canvas
243
+ ref={canvasRef}
244
+ width={400}
245
+ height={400}
246
+ className="block bg-gray-900"
247
+ />
248
+
249
+ {/* Game Over Overlay */}
250
+ {gameOver && (
251
+ <div className="absolute inset-0 bg-black bg-opacity-80 flex items-center justify-center rounded-lg">
252
+ <div className="text-center text-white">
253
+ <h2 className="text-3xl font-bold mb-4">Game Over!</h2>
254
+ <p className="text-xl mb-4">Final Score: {score}</p>
255
+ <button
256
+ onClick={startNewGame}
257
+ className="game-button"
258
+ >
259
+ Play Again
260
+ </button>
261
+ </div>
262
+ </div>
263
+ )}
264
+
265
+ {/* Pause Overlay */}
266
+ {isPaused && !gameOver && (
267
+ <div className="absolute inset-0 bg-black bg-opacity-60 flex items-center justify-center rounded-lg">
268
+ <div className="text-center text-white">
269
+ <h2 className="text-3xl font-bold mb-4">Paused</h2>
270
+ <p className="text-lg">Press SPACE or tap Resume to continue</p>
271
+ </div>
272
+ </div>
273
+ )}
274
+ </div>
275
+
276
+ {/* Controls */}
277
+ <div className="flex flex-col items-center space-y-4">
278
+ {!isPlaying || gameOver ? (
279
+ <button
280
+ onClick={startNewGame}
281
+ className="game-button text-lg px-8 py-4"
282
+ >
283
+ {gameOver ? 'Play Again' : 'Start Game'}
284
+ </button>
285
+ ) : (
286
+ <div className="flex space-x-4">
287
+ <button
288
+ onClick={() => setIsPaused(!isPaused)}
289
+ className="game-button"
290
+ >
291
+ {isPaused ? 'Resume' : 'Pause'}
292
+ </button>
293
+ <button
294
+ onClick={startNewGame}
295
+ className="game-button bg-red-600 hover:bg-red-700"
296
+ >
297
+ Restart
298
+ </button>
299
+ </div>
300
+ )}
301
+ </div>
302
+
303
+ {/* Mobile Touch Controls */}
304
+ <div className="md:hidden">
305
+ <div className="text-center text-white mb-4">Touch Controls</div>
306
+ <div className="grid grid-cols-3 gap-2 w-48">
307
+ <div></div>
308
+ <button
309
+ onClick={() => handleDirection(DIRECTIONS.UP)}
310
+ className="control-button text-2xl h-16"
311
+ disabled={!isPlaying || isPaused}
312
+ >
313
+ ↑
314
+ </button>
315
+ <div></div>
316
+ <button
317
+ onClick={() => handleDirection(DIRECTIONS.LEFT)}
318
+ className="control-button text-2xl h-16"
319
+ disabled={!isPlaying || isPaused}
320
+ >
321
+ ←
322
+ </button>
323
+ <div></div>
324
+ <button
325
+ onClick={() => handleDirection(DIRECTIONS.RIGHT)}
326
+ className="control-button text-2xl h-16"
327
+ disabled={!isPlaying || isPaused}
328
+ >
329
+ β†’
330
+ </button>
331
+ <div></div>
332
+ <button
333
+ onClick={() => handleDirection(DIRECTIONS.DOWN)}
334
+ className="control-button text-2xl h-16"
335
+ disabled={!isPlaying || isPaused}
336
+ >
337
+ ↓
338
+ </button>
339
+ <div></div>
340
+ </div>
341
+ </div>
342
+
343
+ {/* Instructions */}
344
+ <div className="text-center text-gray-400 text-sm max-w-md">
345
+ <p className="mb-2">
346
+ <strong>Keyboard:</strong> Arrow keys or WASD to move, SPACE to pause
347
+ </p>
348
+ <p>
349
+ <strong>Mobile:</strong> Use touch controls below
350
+ </p>
351
+ </div>
352
+ </div>
353
+ )
354
+ }