elyor-ml commited on
Commit
285d2af
·
1 Parent(s): 0ceeacd

snake game

Browse files
Dockerfile CHANGED
@@ -1,6 +1,23 @@
1
  # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
  # you will also find guides on how best to write your Dockerfile
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  FROM python:3.9
5
 
6
  RUN useradd -m -u 1000 user
@@ -13,4 +30,21 @@ COPY --chown=user ./requirements.txt requirements.txt
13
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
 
15
  COPY --chown=user . /app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
  # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
  # you will also find guides on how best to write your Dockerfile
3
 
4
+ ############################
5
+ # Stage 1 – Build frontend #
6
+ ############################
7
+
8
+ FROM node:20 AS frontend-builder
9
+
10
+ WORKDIR /frontend
11
+
12
+ # Copy and install deps
13
+ COPY ./frontend/package.json ./frontend/tsconfig.json ./frontend/vite.config.ts ./frontend/index.html ./
14
+ COPY ./frontend/src ./src
15
+ RUN npm install --legacy-peer-deps && npm run build
16
+
17
+ ###########################
18
+ # Stage 2 – Backend image #
19
+ ###########################
20
+
21
  FROM python:3.9
22
 
23
  RUN useradd -m -u 1000 user
 
30
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
31
 
32
  COPY --chown=user . /app
33
+
34
+ # Copy built static files from frontend stage
35
+ COPY --from=frontend-builder /frontend/dist ./static
36
+
37
+ # ------------------------------------------------------------------------------
38
+ # NOTE: Hugging Face Spaces run inside a headless container. The Snake game
39
+ # itself requires an X11 display and therefore cannot run on the Space. The
40
+ # FastAPI app in `app.py` just serves a landing page and a health-check.
41
+ # ------------------------------------------------------------------------------
42
+
43
+ # Install lightweight additional libraries to satisfy pygame at import time.
44
+ # These X11/SDL libs are small and safe even for headless use.
45
+ RUN apt-get update && apt-get install -y --no-install-recommends \
46
+ libsdl2-2.0-0 libsdl2-image-2.0-0 libsdl2-ttf-2.0-0 \
47
+ && rm -rf /var/lib/apt/lists/*
48
+
49
+ # Default command that Hugging Face will execute
50
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -1,7 +1,15 @@
1
  from fastapi import FastAPI
 
 
2
 
3
- app = FastAPI()
 
4
 
5
- @app.get("/")
6
- def greet_json():
7
- return {"Hello": "World!"}
 
 
 
 
 
 
1
  from fastapi import FastAPI
2
+ from fastapi.responses import RedirectResponse
3
+ from fastapi.staticfiles import StaticFiles
4
 
5
+ # FastAPI application
6
+ app = FastAPI(title="Snake Game React Space")
7
 
8
+ # Serve compiled React app (generated by Vite) as static files
9
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
10
+
11
+
12
+ # Health-check endpoint for the Space
13
+ @app.get("/ping")
14
+ def ping():
15
+ return {"status": "ok"}
frontend/index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Snake Game</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
frontend/package.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "snake-react",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/react": "^18.2.0",
17
+ "@types/react-dom": "^18.2.0",
18
+ "@vitejs/plugin-react": "^4.1.0",
19
+ "typescript": "^5.4.4",
20
+ "vite": "^5.2.0"
21
+ }
22
+ }
frontend/src/App.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ canvas {
2
+ border: 1px solid #333;
3
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import './App.css';
3
+
4
+ type Point = { x: number; y: number };
5
+
6
+ const CELL = 20;
7
+ const COLS = 30;
8
+ const ROWS = 20;
9
+ const WIDTH = COLS * CELL;
10
+ const HEIGHT = ROWS * CELL;
11
+
12
+ const DIRECTIONS: Record<string, Point> = {
13
+ ArrowUp: { x: 0, y: -1 },
14
+ ArrowDown: { x: 0, y: 1 },
15
+ ArrowLeft: { x: -1, y: 0 },
16
+ ArrowRight: { x: 1, y: 0 },
17
+ };
18
+
19
+ function App() {
20
+ const canvasRef = useRef<HTMLCanvasElement>(null);
21
+ const [snake, setSnake] = useState<Point[]>([
22
+ { x: 5, y: 5 },
23
+ ]);
24
+ const [food, setFood] = useState<Point>({ x: 10, y: 10 });
25
+ const [dir, setDir] = useState<Point>({ x: 1, y: 0 });
26
+ const [gameOver, setGameOver] = useState(false);
27
+
28
+ const randomFood = () => {
29
+ return {
30
+ x: Math.floor(Math.random() * COLS),
31
+ y: Math.floor(Math.random() * ROWS),
32
+ };
33
+ };
34
+
35
+ useEffect(() => {
36
+ const handleKey = (e: KeyboardEvent) => {
37
+ if (DIRECTIONS[e.key]) {
38
+ setDir(DIRECTIONS[e.key]);
39
+ }
40
+ };
41
+ window.addEventListener('keydown', handleKey);
42
+ return () => window.removeEventListener('keydown', handleKey);
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ const ctx = canvasRef.current?.getContext('2d');
47
+ if (!ctx) return;
48
+
49
+ const interval = setInterval(() => {
50
+ setSnake((prev: Point[]) => {
51
+ const head = { x: prev[0].x + dir.x, y: prev[0].y + dir.y };
52
+ // check wall
53
+ if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) {
54
+ setGameOver(true);
55
+ clearInterval(interval);
56
+ return prev;
57
+ }
58
+ // check self
59
+ if (prev.some((p: Point) => p.x === head.x && p.y === head.y)) {
60
+ setGameOver(true);
61
+ clearInterval(interval);
62
+ return prev;
63
+ }
64
+ const newSnake = [head, ...prev];
65
+ if (head.x === food.x && head.y === food.y) {
66
+ setFood(randomFood());
67
+ } else {
68
+ newSnake.pop();
69
+ }
70
+ return newSnake;
71
+ });
72
+ }, 100);
73
+ return () => clearInterval(interval);
74
+ }, [dir, food]);
75
+
76
+ useEffect(() => {
77
+ const ctx = canvasRef.current?.getContext('2d');
78
+ if (!ctx) return;
79
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
80
+
81
+ // draw food
82
+ ctx.fillStyle = 'red';
83
+ ctx.fillRect(food.x * CELL, food.y * CELL, CELL, CELL);
84
+
85
+ // draw snake
86
+ ctx.fillStyle = 'green';
87
+ snake.forEach((s: Point) => {
88
+ ctx.fillRect(s.x * CELL, s.y * CELL, CELL, CELL);
89
+ });
90
+ }, [snake, food]);
91
+
92
+ return (
93
+ <div className="wrapper">
94
+ {gameOver && <h2>Game Over</h2>}
95
+ <canvas ref={canvasRef} width={WIDTH} height={HEIGHT} />
96
+ </div>
97
+ );
98
+ }
99
+
100
+ export default App;
frontend/src/global.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ declare module '*.css';
frontend/src/index.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ margin: 0;
3
+ font-family: system-ui, sans-serif;
4
+ display: flex;
5
+ justify-content: center;
6
+ align-items: center;
7
+ height: 100vh;
8
+ background-color: #f5f5f5;
9
+ }
10
+
11
+ .wrapper {
12
+ text-align: center;
13
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
frontend/tsconfig.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "jsx": "react-jsx",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "skipLibCheck": true,
10
+ "moduleResolution": "bundler",
11
+ "allowJs": false,
12
+ "resolveJsonModule": true
13
+ },
14
+ "types": ["vite/client"],
15
+ "include": ["src", "src/**/*.css", "src/global.d.ts"]
16
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ },
9
+ build: {
10
+ outDir: 'dist',
11
+ },
12
+ });
requirements.txt CHANGED
@@ -1,2 +1,3 @@
1
  fastapi
2
- uvicorn[standard]
 
 
1
  fastapi
2
+ uvicorn[standard]
3
+ pygame
snake_game.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pygame
2
+ import random
3
+ import sys
4
+
5
+ # Initialize pygame
6
+ pygame.init()
7
+
8
+ # Screen dimensions
9
+ WIDTH, HEIGHT = 600, 400
10
+ CELL_SIZE = 20 # Size of a grid cell
11
+ GRID_WIDTH = WIDTH // CELL_SIZE
12
+ GRID_HEIGHT = HEIGHT // CELL_SIZE
13
+
14
+ # Colors (R, G, B)
15
+ WHITE = (255, 255, 255)
16
+ BLACK = (0, 0, 0)
17
+ RED = (200, 0, 0)
18
+ GREEN = (0, 200, 0)
19
+ DARK_GREEN = (0, 155, 0)
20
+
21
+ # Frames per second
22
+ FPS = 10
23
+
24
+ # Directions
25
+ UP = (0, -1)
26
+ DOWN = (0, 1)
27
+ LEFT = (-1, 0)
28
+ RIGHT = (1, 0)
29
+
30
+ # Create the screen
31
+ screen = pygame.display.set_mode((WIDTH, HEIGHT))
32
+ pygame.display.set_caption("Snake Game")
33
+ clock = pygame.time.Clock()
34
+ font = pygame.font.SysFont("arial", 24)
35
+
36
+
37
+ def draw_grid():
38
+ """Draw grid lines (optional)."""
39
+ for x in range(0, WIDTH, CELL_SIZE):
40
+ pygame.draw.line(screen, BLACK, (x, 0), (x, HEIGHT))
41
+ for y in range(0, HEIGHT, CELL_SIZE):
42
+ pygame.draw.line(screen, BLACK, (0, y), (WIDTH, y))
43
+
44
+
45
+ def random_food_position(snake_body):
46
+ """Return a random position not occupied by the snake."""
47
+ while True:
48
+ pos = (random.randint(0, GRID_WIDTH - 1), random.randint(0, GRID_HEIGHT - 1))
49
+ if pos not in snake_body:
50
+ return pos
51
+
52
+
53
+ def draw_snake(snake_body):
54
+ for segment in snake_body:
55
+ rect = pygame.Rect(segment[0] * CELL_SIZE, segment[1] * CELL_SIZE, CELL_SIZE, CELL_SIZE)
56
+ pygame.draw.rect(screen, DARK_GREEN, rect)
57
+ pygame.draw.rect(screen, GREEN, rect.inflate(-4, -4))
58
+
59
+
60
+ def draw_food(food_pos):
61
+ rect = pygame.Rect(food_pos[0] * CELL_SIZE, food_pos[1] * CELL_SIZE, CELL_SIZE, CELL_SIZE)
62
+ pygame.draw.rect(screen, RED, rect)
63
+
64
+
65
+ def show_text(text, center):
66
+ surface = font.render(text, True, BLACK)
67
+ rect = surface.get_rect(center=center)
68
+ screen.blit(surface, rect)
69
+
70
+
71
+ def game_over_screen(score):
72
+ screen.fill(WHITE)
73
+ show_text(f"Game Over! Score: {score}", (WIDTH // 2, HEIGHT // 2 - 20))
74
+ show_text("Press Enter to play again or Esc to quit", (WIDTH // 2, HEIGHT // 2 + 20))
75
+ pygame.display.flip()
76
+
77
+ waiting = True
78
+ while waiting:
79
+ for event in pygame.event.get():
80
+ if event.type == pygame.QUIT:
81
+ pygame.quit()
82
+ sys.exit()
83
+ elif event.type == pygame.KEYDOWN:
84
+ if event.key == pygame.K_RETURN:
85
+ waiting = False
86
+ elif event.key == pygame.K_ESCAPE:
87
+ pygame.quit()
88
+ sys.exit()
89
+ clock.tick(60)
90
+
91
+
92
+ def main():
93
+ # Initial snake setup
94
+ snake_body = [(GRID_WIDTH // 2, GRID_HEIGHT // 2)]
95
+ direction = RIGHT
96
+ food_pos = random_food_position(snake_body)
97
+ score = 0
98
+
99
+ running = True
100
+ while running:
101
+ clock.tick(FPS)
102
+ for event in pygame.event.get():
103
+ if event.type == pygame.QUIT:
104
+ pygame.quit()
105
+ sys.exit()
106
+ elif event.type == pygame.KEYDOWN:
107
+ if event.key == pygame.K_UP and direction != DOWN:
108
+ direction = UP
109
+ elif event.key == pygame.K_DOWN and direction != UP:
110
+ direction = DOWN
111
+ elif event.key == pygame.K_LEFT and direction != RIGHT:
112
+ direction = LEFT
113
+ elif event.key == pygame.K_RIGHT and direction != LEFT:
114
+ direction = RIGHT
115
+
116
+ # Calculate new head position
117
+ new_head = (snake_body[0][0] + direction[0], snake_body[0][1] + direction[1])
118
+
119
+ # Check collisions with walls
120
+ if (
121
+ new_head[0] < 0
122
+ or new_head[0] >= GRID_WIDTH
123
+ or new_head[1] < 0
124
+ or new_head[1] >= GRID_HEIGHT
125
+ ):
126
+ game_over_screen(score)
127
+ return # Restart whole game
128
+
129
+ # Check collision with self
130
+ if new_head in snake_body:
131
+ game_over_screen(score)
132
+ return
133
+
134
+ # Move snake
135
+ snake_body.insert(0, new_head)
136
+
137
+ # Check if food eaten
138
+ if new_head == food_pos:
139
+ score += 1
140
+ food_pos = random_food_position(snake_body)
141
+ else:
142
+ snake_body.pop() # Remove tail segment if no food eaten
143
+
144
+ # Draw everything
145
+ screen.fill(WHITE)
146
+ draw_grid()
147
+ draw_snake(snake_body)
148
+ draw_food(food_pos)
149
+ show_text(f"Score: {score}", (60, 20))
150
+ pygame.display.flip()
151
+
152
+
153
+ if __name__ == "__main__":
154
+ while True:
155
+ main() # Restartable game loop