{{ message }}
Repository navigation
Expand file tree
/
Copy pathsnake_game.py
More file actions
289 lines (238 loc) · 9.76 KB
/
Copy pathsnake_game.py
File metadata and controls
289 lines (238 loc) · 9.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
"""A complete Snake game built with Python's :mod:`turtle` module.
Run this file directly to start the game. Use the arrow keys to steer the
snake, ``P`` to pause or resume, ``R`` after a game over to restart, and
``Q`` to quit.
"""
from __future__ import annotations
import random
import turtle
from enum import Enum, auto
from typing import Final
# Window and game-play settings. Coordinates are centered on the screen.
WINDOW_WIDTH: Final = 700
WINDOW_HEIGHT: Final = 700
CELL_SIZE: Final = 20
BOUNDARY_X: Final = WINDOW_WIDTH // 2 - CELL_SIZE
BOUNDARY_Y: Final = WINDOW_HEIGHT // 2 - CELL_SIZE
INITIAL_DELAY: Final = 110 # Milliseconds between movement updates.
MIN_DELAY: Final = 55
SPEED_INCREMENT: Final = 2
class GameState(Enum):
"""Represent the three states in which the game can exist."""
PLAYING = auto()
PAUSED = auto()
GAME_OVER = auto()
class Snake:
"""Represent the player-controlled snake and its movement rules."""
def __init__(self) -> None:
self.segments: list[turtle.Turtle] = []
self.direction = "stop"
self._create_initial_snake()
def _create_initial_snake(self) -> None:
"""Create a three-segment snake centered in the play area."""
for x_position in (0, -CELL_SIZE, -2 * CELL_SIZE):
self._add_segment(x_position, 0)
def _add_segment(self, x_position: int, y_position: int) -> None:
"""Append one square segment at the supplied board position."""
segment = turtle.Turtle("square")
segment.color("#66e3a1")
segment.penup()
segment.goto(x_position, y_position)
self.segments.append(segment)
@property
def head(self) -> turtle.Turtle:
"""Return the snake's leading segment."""
return self.segments[0]
def change_direction(self, new_direction: str) -> None:
"""Update direction unless it would immediately reverse the snake."""
opposite_directions = {
"up": "down",
"down": "up",
"left": "right",
"right": "left",
}
if opposite_directions.get(new_direction) != self.direction:
self.direction = new_direction
def move(self) -> None:
"""Advance the snake by exactly one board cell in its current direction."""
# Do not shift the body before the player chooses an initial direction.
# Otherwise a body segment is moved onto the stationary head, causing an
# immediate self-collision on the following game tick.
if self.direction == "stop":
return
for index in range(len(self.segments) - 1, 0, -1):
previous = self.segments[index - 1]
self.segments[index].goto(previous.position())
x_position, y_position = self.head.position()
movement = {
"up": (0, CELL_SIZE),
"down": (0, -CELL_SIZE),
"left": (-CELL_SIZE, 0),
"right": (CELL_SIZE, 0),
"stop": (0, 0),
}
delta_x, delta_y = movement[self.direction]
self.head.goto(x_position + delta_x, y_position + delta_y)
def grow(self) -> None:
"""Add a segment at the tail's current location after food is eaten."""
tail = self.segments[-1]
self._add_segment(int(tail.xcor()), int(tail.ycor()))
def collided_with_self(self) -> bool:
"""Return whether the head overlaps any non-head segment."""
return any(self.head.distance(segment) < CELL_SIZE / 2 for segment in self.segments[1:])
def destroy(self) -> None:
"""Hide all snake graphics before a new game begins."""
for segment in self.segments:
segment.hideturtle()
self.segments.clear()
class Food(turtle.Turtle):
"""A food item that appears on an unoccupied grid cell."""
def __init__(self) -> None:
super().__init__("circle")
self.color("#ff6b6b")
self.shapesize(0.65, 0.65)
self.penup()
def relocate(self, snake: Snake) -> None:
"""Move food to a random grid position outside the snake's body."""
occupied = {(int(part.xcor()), int(part.ycor())) for part in snake.segments}
possible_positions = [
(x_position, y_position)
for x_position in range(-BOUNDARY_X, BOUNDARY_X + 1, CELL_SIZE)
for y_position in range(-BOUNDARY_Y, BOUNDARY_Y + 1, CELL_SIZE)
if (x_position, y_position) not in occupied
]
self.goto(random.choice(possible_positions))
class Scoreboard(turtle.Turtle):
"""Draw the current score and game-over instructions."""
def __init__(self) -> None:
super().__init__(visible=False)
self.color("white")
self.penup()
self.score = 0
self.high_score = 0
self.update()
def update(self) -> None:
"""Refresh the score text at the top of the game window."""
self.clear()
self.goto(0, WINDOW_HEIGHT // 2 - 42)
self.write(
f"Score: {self.score} High Score: {self.high_score}",
align="center",
font=("Arial", 16, "bold"),
)
def increase(self) -> None:
"""Increase the score for one eaten food item."""
self.score += 10
self.high_score = max(self.score, self.high_score)
self.update()
def show_game_over(self) -> None:
"""Display restart and quit instructions in the center of the board."""
self.goto(0, 0)
self.write(
"GAME OVER\nPress R to restart or Q to quit",
align="center",
font=("Arial", 18, "bold"),
)
def show_paused(self) -> None:
"""Display instructions while gameplay is paused."""
self.goto(0, 0)
self.write(
"PAUSED\nPress P to continue",
align="center",
font=("Arial", 18, "bold"),
)
def reset(self) -> None:
"""Clear a completed game and reset the visible score."""
self.score = 0
self.update()
class SnakeGame:
"""Coordinate graphics, user input, collisions, scoring, and game timing."""
def __init__(self) -> None:
self.screen = turtle.Screen()
self.screen.title("Snake Game")
self.screen.setup(WINDOW_WIDTH, WINDOW_HEIGHT)
self.screen.bgcolor("#17202a")
self.screen.tracer(0)
self.scoreboard = Scoreboard()
self.snake = Snake()
self.food = Food()
self.delay = INITIAL_DELAY
self.state = GameState.PLAYING
self._draw_border()
self.food.relocate(self.snake)
self._bind_keys()
def _draw_border(self) -> None:
"""Draw a visible boundary that matches the collision area."""
border = turtle.Turtle(visible=False)
border.color("#4f6675")
border.penup()
border.goto(-WINDOW_WIDTH // 2 + 5, -WINDOW_HEIGHT // 2 + 5)
border.pendown()
border.pensize(3)
for _ in range(4):
border.forward(WINDOW_WIDTH - 10)
border.left(90)
def _bind_keys(self) -> None:
"""Connect keyboard events to movement and game-control methods."""
self.screen.listen()
self.screen.onkey(lambda: self.snake.change_direction("up"), "Up")
self.screen.onkey(lambda: self.snake.change_direction("down"), "Down")
self.screen.onkey(lambda: self.snake.change_direction("left"), "Left")
self.screen.onkey(lambda: self.snake.change_direction("right"), "Right")
self.screen.onkey(self.restart, "r")
self.screen.onkey(self.restart, "R")
self.screen.onkey(self.toggle_pause, "p")
self.screen.onkey(self.toggle_pause, "P")
self.screen.onkey(self.quit, "q")
self.screen.onkey(self.quit, "Q")
def _hit_wall(self) -> bool:
"""Return whether the head has reached the edge of the playable area."""
return abs(self.snake.head.xcor()) > BOUNDARY_X or abs(self.snake.head.ycor()) > BOUNDARY_Y
def _end_game(self) -> None:
"""Stop movement and show the game-over message."""
self.state = GameState.GAME_OVER
self.scoreboard.show_game_over()
def toggle_pause(self) -> None:
"""Pause a running game or resume a paused game with the P key."""
if self.state is GameState.PLAYING:
self.state = GameState.PAUSED
self.scoreboard.show_paused()
elif self.state is GameState.PAUSED:
self.state = GameState.PLAYING
self.scoreboard.update()
self._game_loop()
def restart(self) -> None:
"""Create a fresh snake and food placement after a game over."""
if self.state is not GameState.GAME_OVER:
return
self.snake.destroy()
self.snake = Snake()
self.scoreboard.reset()
self.delay = INITIAL_DELAY
self.food.relocate(self.snake)
self.state = GameState.PLAYING
self._game_loop()
def quit(self) -> None:
"""Close the Turtle window safely."""
self.screen.bye()
def _game_loop(self) -> None:
"""Run one non-blocking game tick, then schedule the next one."""
if self.state is not GameState.PLAYING:
return
self.snake.move()
if self._hit_wall() or self.snake.collided_with_self():
self._end_game()
elif self.snake.head.distance(self.food) < CELL_SIZE:
self.snake.grow()
self.food.relocate(self.snake)
self.scoreboard.increase()
self.delay = max(MIN_DELAY, self.delay - SPEED_INCREMENT)
self.screen.update()
if self.state is GameState.PLAYING:
self.screen.ontimer(self._game_loop, self.delay)
def run(self) -> None:
"""Start the game loop and hand control to Turtle's event system."""
self._game_loop()
self.screen.mainloop()
if __name__ == "__main__":
SnakeGame().run()
You can’t perform that action at this time.
