Spaces:
Sleeping
Sleeping
snake game
Browse files- Dockerfile +34 -0
- app.py +12 -4
- frontend/index.html +12 -0
- frontend/package.json +22 -0
- frontend/src/App.css +3 -0
- frontend/src/App.tsx +100 -0
- frontend/src/global.d.ts +1 -0
- frontend/src/index.css +13 -0
- frontend/src/main.tsx +10 -0
- frontend/tsconfig.json +16 -0
- frontend/vite.config.ts +12 -0
- requirements.txt +2 -1
- snake_game.py +155 -0
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 |
-
|
|
|
4 |
|
5 |
-
|
6 |
-
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
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
|