Adafruit_Learning_System_Gu.../Custom_LED_Animations/snake/snake.py
2024-10-31 15:28:02 -05:00

171 lines
5.8 KiB
Python

# SPDX-FileCopyrightText: 2024 Tim Cocks
#
# SPDX-License-Identifier: MIT
"""
SnakeAnimation helper class
"""
import random
from micropython import const
from adafruit_led_animation.animation import Animation
from adafruit_led_animation.grid import PixelGrid, HORIZONTAL
class SnakeAnimation(Animation):
UP = const(0x00)
DOWN = const(0x01)
LEFT = const(0x02)
RIGHT = const(0x03)
ALL_DIRECTIONS = [UP, DOWN, LEFT, RIGHT]
DIRECTION_OFFSETS = {
DOWN: (0, 1),
UP: (0, -1),
RIGHT: (1, 0),
LEFT: (-1, 0)
}
def __init__(self, pixel_object, speed, color, width, height, snake_length=3):
"""
Renders a snake that slithers around the 2D grid of pixels
"""
super().__init__(pixel_object, speed, color)
# how many segments the snake will have
self.snake_length = snake_length
# create a PixelGrid helper to access our strand as a 2D grid
self.pixel_grid = PixelGrid(pixel_object, width, height,
orientation=HORIZONTAL, alternating=False)
# size variables
self.width = width
self.height = height
# list that will hold locations of snake segments
self.snake_pixels = []
self.direction = None
# initialize the snake
self._new_snake()
def _clear_snake(self):
"""
Clear the snake segments and turn off all pixels
"""
while len(self.snake_pixels) > 0:
self.pixel_grid[self.snake_pixels.pop()] = 0x000000
def _new_snake(self):
"""
Create a new single segment snake. The snake has a random
direction and location. Turn on the pixel representing the snake.
"""
# choose a random direction and store it
self.direction = random.choice(SnakeAnimation.ALL_DIRECTIONS)
# choose a random starting tile
starting_tile = (random.randint(0, self.width - 1), random.randint(0, self.height - 1))
# add the starting tile to the list of segments
self.snake_pixels.append(starting_tile)
# turn on the pixel at the chosen location
self.pixel_grid[self.snake_pixels[0]] = self.color
def _can_move(self, direction):
"""
returns true if the snake can move in the given direction
"""
# location of the next tile if we would move that direction
next_tile = tuple(map(sum, zip(
SnakeAnimation.DIRECTION_OFFSETS[direction], self.snake_pixels[0])))
# if the tile is one of the snake segments
if next_tile in self.snake_pixels:
# can't move there
return False
# if the tile is within the bounds of the grid
if 0 <= next_tile[0] < self.width and 0 <= next_tile[1] < self.height:
# can move there
return True
# return false if any other conditions not met
return False
def _choose_direction(self):
"""
Choose a direction to go in. Could continue in same direction
as it's already going, or decide to turn to a dirction that
will allow movement.
"""
# copy of all directions in a list
directions_to_check = list(SnakeAnimation.ALL_DIRECTIONS)
# if we can move the direction we're currently going
if self._can_move(self.direction):
# "flip a coin"
if random.random() < 0.5:
# on "heads" we stay going the same direction
return self.direction
# loop over the copied list of directions to check
while len(directions_to_check) > 0:
# choose a random one from the list and pop it out of the list
possible_direction = directions_to_check.pop(
random.randint(0, len(directions_to_check)-1))
# if we can move the chosen direction
if self._can_move(possible_direction):
# return the chosen direction
return possible_direction
# if we made it through all directions and couldn't move in any of them
# then raise the SnakeStuckException
raise SnakeAnimation.SnakeStuckException
def draw(self):
"""
Draw the current frame of the animation
"""
# if the snake is currently the desired length
if len(self.snake_pixels) == self.snake_length:
# remove the last segment from the list and turn it's LED off
self.pixel_grid[self.snake_pixels.pop()] = 0x000000
# if the snake is less than the desired length
# e.g. because we removed one in the previous step
if len(self.snake_pixels) < self.snake_length:
# wrap with try to catch the SnakeStuckException
try:
# update the direction, could continue straight, or could change
self.direction = self._choose_direction()
# the location of the next tile where the head of the snake will move to
next_tile = tuple(map(sum, zip(
SnakeAnimation.DIRECTION_OFFSETS[self.direction], self.snake_pixels[0])))
# insert the next tile at list index 0
self.snake_pixels.insert(0, next_tile)
# turn on the LED for the tile
self.pixel_grid[next_tile] = self.color
# if the snake exception is caught
except SnakeAnimation.SnakeStuckException:
# clear the snake to get rid of the old one
self._clear_snake()
# make a new snake
self._new_snake()
class SnakeStuckException(RuntimeError):
"""
Exception indicating the snake is stuck and can't move in any direction
"""
def __init__(self):
super().__init__("SnakeStuckException")