CircuitPython Treasure Hunt

If you are new to CircuitPython, be sure to check out the Welcome guide for an overview. And if you want to know even more, check out the Essentials guide.

Let's see how we can create our Hunter and Treasures using CircuitPython. This is a little more complex than using MakeCode, so you if you would rather just play the game, skip this and move on to the Playing The Game section.

The Treasure

Once again we'll start with the Treasure code, as it is the most simple. Here it is:

Download: file
import time
import board
import pulseio
import adafruit_irremote
import neopixel

# Configure treasure information
TREASURE_ID = 1
TRANSMIT_DELAY = 15

# Create NeoPixel object to indicate status
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10)

# Create a 'pulseio' output, to send infrared signals on the IR transmitter @ 38KHz
pwm = pulseio.PWMOut(board.IR_TX, frequency=38000, duty_cycle=2 ** 15)
pulseout = pulseio.PulseOut(pwm)

# Create an encoder that will take numbers and turn them into IR pulses
encoder = adafruit_irremote.GenericTransmit(header=[9500, 4500], one=[550, 550], zero=[550, 1700], trail=0)

while True:
    pixels.fill(0xFF0000)
    encoder.transmit(pulseout, [TREASURE_ID]*4)
    time.sleep(0.25)
    pixels.fill(0)
    time.sleep(TRANSMIT_DELAY)

After importing all the goodies we need, we set the ID for the Treasure as well as the time to wait between transmissions.

Download: file
# Configure treasure information
TREASURE_ID = 1
TRANSMIT_DELAY = 15

You'll want to change TREASURE_ID to a unique value for each Treasure. Just use simple numbers like 1, 2, 3, etc. The value of TRANSMIT_DELAY determines how often (in seconds) to send out the ID.

Then we create various items needed for using NeoPixels, the IR receiver, as well as an encoder for creating the transmitted signal.

After all that, we just loop forever sending out the ID:

Download: file
while True:
    pixels.fill(0xFF0000)
    encoder.transmit(pulseout, [TREASURE_ID]*4)
    time.sleep(0.25)
    pixels.fill(0)
    time.sleep(TRANSMIT_DELAY)

Currently, the CircuitPython IR Remote library works with 4 byte NEC codes only. We create this in place with the syntax [TREASURE_ID]*4. If that looks too magical, you can try it out in the REPL to see what it does.

Adafruit CircuitPython 3.0.0 on 2018-07-09; Adafruit CircuitPlayground Express with samd21g18
>>> [1]*4
[1, 1, 1, 1]
>>> ["hello"]*4
['hello', 'hello', 'hello', 'hello']
>>>

It's just a simple syntax for creating a list with all the same content. In this case, 4 of the same thing.

The Hunter

OK, now for the Hunter code. There's a bit more to it, but here it is in its entirety:

Download: file
import time
import board
import pulseio
import adafruit_irremote
import neopixel

# Configure treasure information
#                  ID       PIXEL    COLOR
TREASURE_INFO = { (1,)*4 : (  0  , 0xFF0000) ,
                  (2,)*4 : (  1  , 0x00FF00) ,
                  (3,)*4 : (  2  , 0x0000FF) }
treasures_found = dict.fromkeys(TREASURE_INFO.keys(), False) 

# Create NeoPixel object to indicate status
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10)

# Sanity check setup
if len(TREASURE_INFO) > pixels.n:
    raise ValueError("More treasures than pixels.")

# Create a 'pulseio' input, to listen to infrared signals on the IR receiver
pulsein = pulseio.PulseIn(board.IR_RX, maxlen=120, idle_state=True)

# Create a decoder that will take pulses and turn them into numbers
decoder = adafruit_irremote.GenericDecode()

while True:
    # Listen for incoming IR pulses
    pulses = decoder.read_pulses(pulsein)

    # Try and decode them
    try:
        # Attempt to convert received pulses into numbers
        received_code = tuple(decoder.decode_bits(pulses, debug=False))
    except adafruit_irremote.IRNECRepeatException:
        # We got an unusual short code, probably a 'repeat' signal
        # print("NEC repeat!")
        continue
    except adafruit_irremote.IRDecodeException as e:
        # Something got distorted or maybe its not an NEC-type remote?
        # print("Failed to decode: ", e.args)
        continue

    # See if received code matches any of the treasures
    if received_code in TREASURE_INFO.keys():
        treasures_found[received_code] = True
        p, c = TREASURE_INFO[received_code] 
        pixels[p] = c
        
    # Check to see if all treasures have been found
    if False not in treasures_found.values():
        pixels.auto_write = False
        while True:
            # Round and round we go
            pixels.buf = pixels.buf[-3:] + pixels.buf[:-3]
            pixels.show()
            time.sleep(0.1)

After the necessary import, we configure things using a dictionary.

Download: file
# Configure treasure information
#                  ID      PIXEL    COLOR
TREASURE_INFO = { (1,)*4: (  0  , 0xFF0000) ,
                  (2,)*4: (  1  , 0x00FF00) ,
                  (3,)*4: (  2  , 0x0000FF) }

The key is Treasure ID, in the form of the expected decoded IR signal - we use the same trick as above to create the necessary 4 byte value expected by the irremote library. The values of the dictionary are tuples which contain the location and color of the NeoPixel to use to indicate the Treasure has been found.

If you wanted to increase the number of Treasures in the game, just add an entry to the dictionary. Keep in mind that you'll have to modify a copy of the Treasure code to create a corresponding CPX Treasure to match the new entry.

A second dictionary is created to keep track of whether the Treasures have been found or not:

Download: file
treasures_found = dict.fromkeys(TREASURE_INFO.keys(), False) 

This is created using the same keys as the previous dictionary. The values are just booleans to indicate found state. Initially they are all False, since nothing has been found yet.

After some other setup, we just loop forever getting raw pulses:

Download: file
    # Listen for incoming IR pulses
    pulses = decoder.read_pulses(pulsein)

Which we then try and decode:

Download: file
        # Attempt to convert received pulses into numbers
        received_code = tuple(decoder.decode_bits(pulses, debug=False))

If anything happens, exceptions are thrown, which are currently just silently ignored.

Once we get a valid signal, we check the code against the ones we setup in the dictionary. If it matches an entry, then we update the dictionary of found Treasures and turn on the corresponding NeoPixel.

Download: file
    # See if received code matches any of the treasures
    if received_code in TREASURE_INFO.keys():
        treasures_found[received_code] = True
        p, c = TREASURE_INFO[received_code] 
        pixels[p] = c

Once all of the Treasures have been found, there will no longer be any False entries in the dictionary of found Treasures. So it's a simple check to see if they have all been found. If they have, then we just spin the NeoPixels round and round forever.

Download: file
    # Check to see if all treasures have been found
    if False not in treasures_found.values():
        pixels.auto_write = False
        while True:
            # Round and round we go
            pixels.buf = pixels.buf[-3:] + pixels.buf[:-3]
            pixels.show()
            time.sleep(0.1)
This guide was first published on Jul 26, 2018. It was last updated on Jul 26, 2018. This page (CircuitPython Treasure Hunt) was last updated on Nov 05, 2019.