Skip to content

Instantly share code, notes, and snippets.

@cdeil
Created February 13, 2025 14:33
Show Gist options
  • Save cdeil/2a6d4bb69353f6e22c6e1827d135eaf5 to your computer and use it in GitHub Desktop.
Save cdeil/2a6d4bb69353f6e22c6e1827d135eaf5 to your computer and use it in GitHub Desktop.
"""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