The adafruit_led_animation library contains a built-in helper class for dealing with 2D Grids of pixels, PixelGrid. We can use this class inside of our custom Animation to be able to refer to the pixels with x, y coordinates in a grid rather than just an index within a strand like normal. This will only make sense to use if your pixels are also physically arranged in a grid, the Neopixel FeatherWing or Dotstar FeatherWing provide an easy to use grid to run them on.
Snake Animation
The SnakeAnimation will make a snake of the specified size and color that will slither around the grid in random directions. If the snake gets stuck in a corner a new one will spawn and carry on slithering around. The current snake segment locations are stored in a list variable. During each animation step the snake will decide randomly whether to continue the direction it's going, or to change to a different random direction. Much of the movement logic is delegated into helper functions _can_move() and _choose_direction().
# 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")
Conways Game Of Life Animation
The ConwaysLifeAnimation is an implementation of Conway's Game of Life. It is a basic simulation of cells that live and die based on simple rules about how many neighbors they have. You get to set the location of a set of live cells at the start of the simulation and then it will step through simulation ticks applying the population rules. During each step the code will iterate over every pixel in the grid and count the number of live and dead neighbor cells, deciding based on the giving rules the fate of the current cell and storing it in a list. After it's made it through the whole grid then it works it's way through the lists created above doing the actual turning on and off of pixels to signify cells spawning and dying.Â
# SPDX-FileCopyrightText: 2024 Tim Cocks
#
# SPDX-License-Identifier: MIT
"""
ConwaysLifeAnimation helper class
"""
from micropython import const
from adafruit_led_animation.animation import Animation
from adafruit_led_animation.grid import PixelGrid, HORIZONTAL
def _is_pixel_off(pixel):
return pixel[0] == 0 and pixel[1] == 0 and pixel[2] == 0
class ConwaysLifeAnimation(Animation):
# Constants
DIRECTION_OFFSETS = [
(0, 1),
(0, -1),
(1, 0),
(-1, 0),
(1, 1),
(-1, 1),
(1, -1),
(-1, -1),
]
LIVE = const(0x01)
DEAD = const(0x00)
def __init__(
self,
pixel_object,
speed,
color,
width,
height,
initial_cells,
equilibrium_restart=True,
):
"""
Conway's Game of Life implementation. Watch the cells
live and die based on the classic rules.
:param pixel_object: The initialised LED object.
:param float speed: Animation refresh rate in seconds, e.g. ``0.1``.
:param color: the color to use for live cells
:param width: the width of the grid
:param height: the height of the grid
:param initial_cells: list of initial cells to be live
:param equilibrium_restart: whether to restart when the simulation gets stuck unchanging
"""
super().__init__(pixel_object, speed, color)
# list to hold which cells are live
self.drawn_pixels = []
# store the initial cells
self.initial_cells = initial_cells
# PixelGrid helper to access the strand as a 2D grid
self.pixel_grid = PixelGrid(
pixel_object, width, height, orientation=HORIZONTAL, alternating=False
)
# size of the grid
self.width = width
self.height = height
# equilibrium restart boolean
self.equilibrium_restart = equilibrium_restart
# counter to store how many turns since the last change
self.equilibrium_turns = 0
# self._init_cells()
def _is_grid_empty(self):
"""
Checks if the grid is empty.
:return: True if there are no live cells, False otherwise
"""
for y in range(self.height):
for x in range(self.width):
if not _is_pixel_off(self.pixel_grid[x][y]):
return False
return True
def _init_cells(self):
"""
Turn off all LEDs then turn on ones cooresponding to the initial_cells
:return: None
"""
self.pixel_grid.fill(0x000000)
for cell in self.initial_cells:
self.pixel_grid[cell] = self.color
def _count_neighbors(self, cell):
"""
Check how many live cell neighbors are found at the given location
:param cell: the location to check
:return: the number of live cell neighbors
"""
neighbors = 0
for direction in ConwaysLifeAnimation.DIRECTION_OFFSETS:
try:
if not _is_pixel_off(
self.pixel_grid[cell[0] + direction[0]][cell[1] + direction[1]]
):
neighbors += 1
except IndexError:
pass
return neighbors
def draw(self):
# pylint: disable=too-many-branches
"""
draw the current frame of the animation
:return: None
"""
# if there are no live cells
if self._is_grid_empty():
# spawn the inital_cells and return
self._init_cells()
return
# list to hold locations to despawn live cells
despawning_cells = []
# list to hold locations spawn new live cells
spawning_cells = []
# loop over the grid
for y in range(self.height):
for x in range(self.width):
# check and set the current cell type, live or dead
if _is_pixel_off(self.pixel_grid[x][y]):
cur_cell_type = ConwaysLifeAnimation.DEAD
else:
cur_cell_type = ConwaysLifeAnimation.LIVE
# get a count of the neigbors
neighbors = self._count_neighbors((x, y))
# if the current cell is alive
if cur_cell_type == ConwaysLifeAnimation.LIVE:
# if it has fewer than 2 neighbors
if neighbors < 2:
# add its location to the despawn list
despawning_cells.append((x, y))
# if it has more than 3 neighbors
if neighbors > 3:
# add its location to the despawn list
despawning_cells.append((x, y))
# if the current location is not a living cell
elif cur_cell_type == ConwaysLifeAnimation.DEAD:
# if it has exactly 3 neighbors
if neighbors == 3:
# add the current location to the spawn list
spawning_cells.append((x, y))
# loop over the despawn locations
for cell in despawning_cells:
# turn off LEDs at each location
self.pixel_grid[cell] = 0x000000
# loop over the spawn list
for cell in spawning_cells:
# turn on LEDs at each location
self.pixel_grid[cell] = self.color
# if equilibrium restart mode is enabled
if self.equilibrium_restart:
# if there were no cells spawned or despaned this round
if len(despawning_cells) == 0 and len(spawning_cells) == 0:
# increment equilibrium turns counter
self.equilibrium_turns += 1
# if the counter is 3 or higher
if self.equilibrium_turns >= 3:
# go back to the initial_cells
self._init_cells()
# reset the turns counter to zero
self.equilibrium_turns = 0
Both Together
The following code.py will run both animations at the same time. By default it expects to find one Neopixel Featherwing, and one Dotstar Featherwing. You can update the code to use a different configuration of featherwings, or some other compatible grid. Remember to update the pins referenced and initialization as needed.Â
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Uses NeoPixel Featherwing connected to D10 and
Dotstar Featherwing connected to D13, and D11.
Update pins as needed for your connections.
"""
import board
import neopixel
import adafruit_dotstar as dotstar
from conways import ConwaysLifeAnimation
from snake import SnakeAnimation
# Update to match the pin connected to your NeoPixels
pixel_pin = board.D10
# Update to match the number of NeoPixels you have connected
pixel_num = 32
# initialize the neopixels featherwing
pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=0.02, auto_write=False)
# initialize the dotstar featherwing
dots = dotstar.DotStar(board.D13, board.D11, 72, brightness=0.02)
# initial live cells for conways
initial_cells = [
(2, 1),
(3, 1),
(4, 1),
(5, 1),
(6, 1),
]
# initialize the animations
conways = ConwaysLifeAnimation(dots, 0.1, 0xff00ff, 12, 6, initial_cells)
snake = SnakeAnimation(pixels, speed=0.1, color=0xff00ff, width=8, height=4)
while True:
# call animate to show the next animation frames
conways.animate()
snake.animate()
Page last edited January 22, 2025
Text editor powered by tinymce.