Created
February 13, 2025 14:33
-
-
Save cdeil/2a6d4bb69353f6e22c6e1827d135eaf5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Snake in Arcade. | |
# Learnings | |
1. Key input handling and update time delta is tricky. | |
Creating `snake.direction_commands` to keep track works well. | |
The `time_to_move` solution here works OK. | |
A simpler alternative could be to use `Window.update_rate` which | |
is passed to `on_update` as `delta_time` directly. | |
2. Grid vs pixel coordinates can litter the code. | |
Creating `Grid` helper class and using tuples for coordinates works well. | |
Having a wall is not pretty or useful here if nothing but the grid | |
is displayed, so we set it's width to just 1 pix. | |
# Possible improvements | |
1. TODO: create points and game speedup over time? | |
2. TODO: add background music and sound effects | |
3. TODO: create second snake for human or AI player | |
""" | |
import dataclasses | |
import random | |
import arcade | |
@dataclasses.dataclass | |
class Grid: | |
width: int = 30 | |
height: int = 20 | |
block_size: int = 20 | |
wall_size: int = 1 | |
@property | |
def shape(self): | |
return self.width, self.height | |
@property | |
def window_shape(self): | |
b, w = self.block_size, self.wall_size | |
return self.width * b + 2 * w, self.height * b + 2 * w | |
def is_inside(self, p): | |
return 0 <= p[0] < self.width and 0 <= p[1] < self.height | |
def pix_pos(self, p): | |
b, w = self.block_size, self.wall_size | |
return p[0] * b + w, p[1] * b + w | |
def random_position(self): | |
return random.randrange(self.width), random.randrange(self.height) | |
class Snake: | |
def __init__(self, pos, length): | |
# body[0] is the head, body[-1] the tail | |
self.body = [(pos[0] - dx, pos[1]) for dx in range(length)] | |
self.direction = (1, 0) | |
self.direction_commands = [] | |
self.has_eaten_fruit = False | |
self.color = arcade.color.REDWOOD | |
@property | |
def head_pos(self): | |
return self.body[0] | |
def update(self): | |
# React to user input | |
if self.direction_commands: | |
self.direction = self.direction_commands.pop(0) | |
# Movement | |
x = self.body[0][0] + self.direction[0] | |
y = self.body[0][1] + self.direction[1] | |
self.body.insert(0, (x, y)) | |
if self.has_eaten_fruit: | |
self.has_eaten_fruit = False | |
else: | |
self.body.pop() | |
class SnakeGame(arcade.View): | |
def __init__(self): | |
super().__init__() | |
self.background_color = arcade.color.BABY_BLUE | |
self.grid = Grid() | |
self.window.set_size(*self.grid.window_shape) | |
self.snake = Snake(pos=(3, self.grid.height // 2), length=3) | |
self.fruit_position = self.grid.random_position() | |
self.time_to_move = 0.1 # time interval in seconds | |
self.time_since_last_movement = 0 | |
def on_key_press(self, symbol, modifiers): | |
directions = { | |
arcade.key.LEFT: (-1, 0), | |
arcade.key.RIGHT: (1, 0), | |
arcade.key.UP: (0, 1), | |
arcade.key.DOWN: (0, -1), | |
} | |
if symbol in directions: | |
self.snake.direction_commands.append(directions[symbol]) | |
if symbol == arcade.key.ESCAPE: | |
self.window.show_view(GameOverView()) | |
def on_update(self, delta_time): | |
if self.snake.head_pos == self.fruit_position: | |
self.snake.has_eaten_fruit = True | |
self.spawn_new_fruit() | |
if not self.grid.is_inside(self.snake.head_pos): | |
message = "Snake hit wall. Snake head hurt! Bad player!" | |
self.window.show_view(GameOverView(message=message)) | |
if self.snake.head_pos in self.snake.body[1:]: | |
message = "Snake eat itself. No no no." | |
self.window.show_view(GameOverView(message=message)) | |
self.time_since_last_movement += delta_time | |
if self.time_since_last_movement > self.time_to_move: | |
self.time_since_last_movement -= self.time_to_move | |
self.snake.update() | |
def spawn_new_fruit(self): | |
self.fruit_position = self.grid.random_position() | |
if self.fruit_position in self.snake.body: | |
self.spawn_new_fruit() | |
def on_draw(self): | |
self.clear() | |
self.draw_walls() | |
self.draw_fruit() | |
self.draw_snake() | |
# TODO: make SpriteSheetList for this! | |
def draw_walls(self): | |
w = self.grid.wall_size | |
for lbwh in [ | |
(0, 0, self.width, w), # bottom | |
(0, self.height - w, self.width, w), # top | |
(0, 0, w, self.height), # left | |
(self.width - w, 0, w, self.height), # right | |
]: | |
arcade.draw_lbwh_rectangle_filled(*lbwh, arcade.color.BLACK) | |
def draw_fruit(self): | |
arcade.draw_lbwh_rectangle_filled( | |
*self.grid.pix_pos(self.fruit_position), | |
self.grid.block_size, self.grid.block_size, | |
arcade.color.YELLOW_ORANGE, | |
) | |
def draw_snake(self): | |
b = self.grid.block_size | |
for grid_pos in self.snake.body: | |
pos = self.grid.pix_pos(grid_pos) | |
arcade.draw_lbwh_rectangle_filled(*pos, b, b, self.snake.color) | |
class GameOverView(arcade.View): | |
def __init__(self, heading="Game Over", message="tbd"): | |
super().__init__() | |
self.heading = heading | |
self.message = message | |
self.background_color = arcade.color.BLACK_OLIVE | |
def on_draw(self): | |
self.clear() | |
arcade.draw_text(text=self.heading, x=self.window.width // 2, y=self.window.height // 2, | |
color=arcade.color.WHITE, font_size=30, anchor_x="center") | |
arcade.draw_text(text=self.message, x=self.window.width // 2, y=self.window.height // 4, | |
color=arcade.color.WHITE, font_size=10, anchor_x="center") | |
def on_key_press(self, symbol, modifiers): | |
if symbol == arcade.key.ESCAPE: | |
self.window.close() | |
if symbol == arcade.key.SPACE: | |
self.window.show_view(SnakeGame()) | |
def main(): | |
window = arcade.Window(title="Arcade Snake") | |
game = SnakeGame() | |
window.show_view(game) | |
arcade.run() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment