import time import random import adafruit_trellism4 COLORS = [0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0xFF00FF] trellis = adafruit_trellism4.TrellisM4Express(rotation=0) trellis.pixels.brightness = 0.1 trellis.pixels.fill(0) pixel_colors = [None] * 32 found_pairs = 0 previously_pressed = set([]) first_pixel = None
COLORS
contains the possible colors for the pairs. Feel free to add to or change these as you desire, but keep in mind that they should be clearly differentiated.
pixel_colors
holds the assigned color of each pixel on the NeoTrellis, found_pairs
tracks how many pairs have been found in the current game, first_pixel
contains the coordinates of the first pixel of a potential pair (and currently lit in its color), None if there isn't one yet. previously_pressed
keeps track of what buttons were pressed the last time we checked (so we can notice new presses). We'll see how these are used below.
Helper
There's an index_of
helper function that converts the (x, y)
coordinate tuple of a pixel to it's NeoPixel index.
def index_of(coord): x, y = coord return y * 8 + x
Completion Splash
Next is a group of functions and generators that together generate a rainbow color splash that happens when all pairs are found. A page in the Hacking Ikea Lamps with Circuit Playground Express guide describes how this code works.
def wheel(pos): # Input a value 0 to 255 to get a color value. # The colours are a transition r - g - b - back to r. if pos < 0 or pos > 255: return 0, 0, 0 if pos < 85: return int(255 - pos*3), int(pos*3), 0 if pos < 170: pos -= 85 return 0, int(255 - pos*3), int(pos*3) pos -= 170 return int(pos * 3), 0, int(255 - (pos*3)) def cycle_sequence(seq): while True: for elem in seq: yield elem def rainbow_lamp(seq): g = cycle_sequence(seq) while True: trellis.pixels.fill(wheel(next(g))) yield def splash(): rainbow = rainbow_lamp(range(0, 256, 8)) for _ in range(64): next(rainbow) time.sleep(0.005)
Generating Pairs to Find
Now we get into the components of the game itself. The assign_colors
function assigns random color pairs. It does this by constructing a list of all possible pixel coordinates using a list comprehension. This is the list of pixels remaining unassigned. While it's not empty, we grab a random coordinate and remove it. Then we grab another random coordinate from what's left, and remove it. A random color is picked and applied to the two pixels. This continues until all random pairs of pixels have been assigned.
def assign_colors(): remaining = [(x, y) for x in range(8) for y in range(4)] while remaining: first = random.choice(remaining) remaining.remove(first) second = random.choice(remaining) remaining.remove(second) c = random.choice(COLORS) pixel_colors[index_of(first)] = c pixel_colors[index_of(second)] = c
Handling Input
The crux of the game happens in response to button presses by the player (or the demo mode). The handle_key
function handles them and implements the game mechanics. It takes the coordinate tuple of the pixel button that was pressed (key
), and the current value of thefound_pairs
and first_pixel
variables we created earlier. The function returns new values for those variables.
The first thing that happens is that the key is checked to see if it's an actual press. If key
is None
, then it isn't which causes an immediate return, changing nothing.
Since we now know it's a legitimate key press, its color is checked. None
means it's part of a pair that has been found already, and so it's ignored.
If we get to this point we know that a valid unmatched key has been pressed. The color of the corresponding NeoTrellis NeoPixel is set to the pixel's color for half a second.
Now we check to see if this is the first pixel of a pair, or a possibly second one. Note that we handle the case of pressing the same key twice.
If this is the second in a possible pair and the colors match, it is noted that the pair of pixels has been matched and both are flashed white 5 times before being set to white. Values are returned indicating that another pair was found, and resetting first_key
to None
.
If the colors didn't match, both NeoPixels are turned off and the return values clear first_pixel
.
Finally if this is the first key of a potential pair, it is assigned to first_pixel
via the function's return values. The final return handles the case where a pixel from a previously matched pair is pressed: first_pixel
is cleared.
def handle_key(key, _found_pairs, _first_pixel): if key is None: return _found_pairs, _first_pixel key_color = pixel_colors[index_of(key)] if key_color is not None: trellis.pixels[key] = pixel_colors[index_of(key)] time.sleep(0.5) if _first_pixel and _first_pixel != key: if key_color == pixel_colors[index_of(_first_pixel)]: pixel_colors[index_of(_first_pixel)] = None pixel_colors[index_of(key)] = None for _ in range(5): trellis.pixels[_first_pixel] = 0xFFFFFF trellis.pixels[key] = 0xFFFFFF time.sleep(0.1) trellis.pixels[_first_pixel] = 0x000000 trellis.pixels[key] = 0x000000 time.sleep(0.1) trellis.pixels[_first_pixel] = 0x444444 trellis.pixels[key] = 0x444444 return _found_pairs + 1, None else: trellis.pixels[_first_pixel] = 0x000000 trellis.pixels[key] = 0x000000 return _found_pairs, None else: return _found_pairs, key return _found_pairs, None
Later, in the main game loop, we'll see lines like
found_pairs, first_pixel = handle_key(..., found_pairs, first_pixel)
Note that both found_pairs
and first_pixel
are global variables. Why not just change them directly in the handle_key
function? That would be considered somewhat bad form. Globals are best avoided in general, and by default Python assumes you want to create a local variable the first time you assign to something inside a function. You can use the global
statement to tell the compiler that what you really want to do is to assign to the global variable instead of creating a new local:
global first_pixel
Even though this is available, it's use is bad form and pylint will complain about it. This technique of returning new values for globals can be a handy way to avoid changing globals from inside a function. Additionally, you can see we've taken the next step of passing the values of those globals into function to avoid directly referencing the globals and completely decoupling handle_key
from them.
Detecting Keypresses
The next function compares the currently pressed keys with those that were pressed last time it checked, returning the one that was new since last time. "One that was new" means an arbitrary one that wasn't pressed before but is now. Because we're using Sets for the comparison, the comparison result (set difference) is converted to a List, and the first one in that list is returned, if there is more than one now pressed the one that is returned depends on the Set to List conversion.
Similarly to handle_key
, check_for_key
takes the set of keys previously pressed as an argument and returns its new value along with a key pressed (or None
)
def check_for_key(last_pressed): now_pressed = set(trellis.pressed_keys) new_presses = now_pressed - last_pressed if new_presses: return now_pressed, list(new_presses)[0] return now_pressed, None
Main Game Loop
This is actually a loop within a loop. Each pass through is a game: either demo or playable. The game starts in demo mode. The display is cleared, color pairs assigned, and game variables initialized.
Then the inner loop starts. if left to run to completion it continues until all pairs have been found. What happens in this loop depends on whether it's in demo or play mode.
Play mode is simple, if a button was pressed, it's handled (as detailed above).
Demo mode is a bit more involved. First it picks a random unmatched pixel, handles it, then picks a random pixel of the same color as the first and handles it. That will take care of matching pairs and the NeoPixel manipulation involved. Before each random pick, a keypress is checked for and if there is any, the demo stops and play mode is entered. If the inner loop completes (all 16 pairs matched) a color splash is played. The value of found_pairs
is checked to see if all were found because the loop can be exited early when a button is pressed in demo mode (to switch into play mode).
demo_mode_enabled = True while True: trellis.pixels.fill(0x000000) assign_colors() found_pairs = 0 first_pixel = None remaining = [(x, y) for x in range(8) for y in range(4)] while found_pairs < 16: if demo_mode_enabled: previously_pressed, key_pressed = check_for_key(previously_pressed) if key_pressed: demo_mode_enabled = False break first = random.choice(remaining) remaining.remove(first) found_pairs, first_pixel = handle_key(first, found_pairs, first_pixel) previously_pressed, key_pressed = check_for_key(previously_pressed) if key_pressed: demo_mode_enabled = False break c = pixel_colors[index_of(first)] match = random.choice([x for x in remaining if pixel_colors[index_of(x)] == c]) found_pairs, first_pixel = handle_key(match, found_pairs, first_pixel) remaining.remove(match) else: previously_pressed, key_pressed = check_for_key(previously_pressed) found_pairs, first_pixel = handle_key(key_pressed, found_pairs, first_pixel) if found_pairs == 16: splash()
That's it. Quite simple code when you look at the individual pieces, but a fairly complex and fun game. One interesting bit is how the same underlying code is used for bot play and demo, differing only by how "pressed" pixels are supplied.
Text editor powered by tinymce.