Created
December 10, 2025 09:47
-
-
Save m0rjc/7b5d0256fb11786057029bd317b00eb0 to your computer and use it in GitHub Desktop.
Code for the Raspberry Pi Pico as part of a Defuse The Bomb puzzle I made for Scouts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ---------------------------------------------------------------- | |
| # D E F U S E T H E B O M B P U Z Z L E | |
| # | |
| # Richard Corfield | |
| # Third Harrogate Scouts. December 2025 | |
| # ---------------------------------------------------------------- | |
| # The Scouts were presented with a bomb with coloured wires. They | |
| # had to complete tasks over the evening to earn clues in order to | |
| # work out the correct order to cut the wires. | |
| # | |
| # This device is based on the Raspberry Pi PICO. | |
| # It features blinking lights, which also can be used to tell the | |
| # leader the state of the wires when connecting and aid in connecting | |
| # the correct colours into the correct sockets. | |
| # The Pico's own LED blinks a message in Morse Code. | |
| # | |
| # The blinking lights go out as the wires are cut. Once all wires | |
| # are cut in the correct order all lights are out and the device | |
| # appears completely disarmed. If an incorrect wire is cut then the | |
| # buzzer sounds. | |
| # | |
| # The buzzwer will stop as soon as any change to input is made. | |
| # This means it can be silenced easily and made ready for the next | |
| # team to attempt. | |
| # | |
| # See: | |
| # https://corfelectronics.me.uk/defuse-the-bomb-puzzle-game-with-the-scouts/ | |
| # ---------------------------------------------------------------- | |
| from picozero import Button, pico_led | |
| from machine import Pin, PWM | |
| from time import sleep, ticks_ms, ticks_diff, ticks_add | |
| from random import randint | |
| import math | |
| # Physical GPIO numbers for accessories | |
| # These are the puzzle wires which tge Scouts had to cut or unplug in the correct order. | |
| RED = 15 | |
| BLACK = 14 | |
| WHITE = 13 | |
| GREEN = 12 | |
| YELLOW = 11 | |
| GREEN_LED = 3 | |
| RED_LED = 9 | |
| ORANGE_LED = 28 | |
| BLUE_LED = 21 | |
| WHITE_LED = 27 | |
| BUZZER = 0 | |
| # This is the answer - the wire GPIOs in order that they must be cut | |
| BUTTON_GPIOS = [WHITE,GREEN,RED,YELLOW,BLACK] | |
| # If you align the blinking lights then the LED corresponding to the wire will go out | |
| # This is useful when re-wiring as you can plug wires into the socket that causes their LED | |
| # to stop blinking. | |
| BLINKENLICHT = [WHITE_LED,GREEN_LED,RED_LED,ORANGE_LED,BLUE_LED] | |
| # This clue will be flashed on an Pico's own LED. | |
| MORSE_CLUE = "WHITE COMES BEFORE GREEN" | |
| # Using wire cutters resulted in bounce. We don't need to be too responsive with this. | |
| # 0.1 was not high enough to be reliable. I've increased it to 0.2 and also changed | |
| # the state machine to allow moving back and forth between valid states in case it | |
| # bounces. | |
| DEBOUNCE=0.2 | |
| # These are the Morse Code time delays. | |
| # I found that my Scouts needed more than the usual 3:1 ratio to be able to read the Morse reliably. | |
| MORSE_DIT_LENGTH = 180 | |
| MORSE_DAH_LENGTH = 4 * MORSE_DIT_LENGTH # dits | |
| MORSE_INTER_CHARACTER_LENGTH = 4 * MORSE_DIT_LENGTH # dits | |
| MORSE_INTER_WORD_LENGTH = 7 * MORSE_DIT_LENGTH | |
| MORSE_INTER_MESSAGE_LENGTH = 21 * MORSE_DIT_LENGTH | |
| RESET_STATE = 0 | |
| WINNING_STATE = 2 ** len(BUTTON_GPIOS) - 1 | |
| wires = [Button(pin, bounce_time=DEBOUNCE) for pin in BUTTON_GPIOS] | |
| buzzer = Pin(BUZZER, Pin.OUT) | |
| # ---------------------------------------------------------------- | |
| # Flashing Lights | |
| # ---------------------------------------------------------------- | |
| # PWM is used to manage the current draw. We aim for 50mA Max Total | |
| # across all LEDs in the system. | |
| # The dimmer light is also less blinding when the user is trying to | |
| # copy the Morse clue. | |
| LED_ON_PWM_U16 = 3000 | |
| class RandomlyBlinkingLed: | |
| def __init__(self, gpio): | |
| self.gpio = gpio | |
| self.pin = Pin(gpio, Pin.OUT) | |
| self.pwm = PWM(self.pin) | |
| self.pwm.init(freq=5000, duty_u16=0) | |
| self.value = 0 | |
| self.period = randint(200,1200) | |
| self.next_poll = ticks_ms() | |
| self.enabled = True | |
| def _calc_delay(self): | |
| sign = 1 | |
| factor = randint(0,200) | |
| if factor < 100: | |
| sign = -1 | |
| factor = 200 - factor # 100 to 200 | |
| factor = math.sqrt(factor) / 10 # 1 to 1.4 | |
| delay = self.period * factor | |
| return int(delay) | |
| def _output(self): | |
| if self.value == 1 and self.enabled: | |
| self.pwm.duty_u16(LED_ON_PWM_U16) | |
| else: | |
| self.pwm.duty_u16(0) | |
| def enable(self, value): | |
| if(value != self.enabled): | |
| self.enabled = value | |
| self._output() | |
| def blink(self): | |
| self.value = 1 - self.value | |
| if(self.enabled): | |
| self._output() | |
| def poll(self): | |
| now = ticks_ms() | |
| tmp = ticks_diff(now, self.next_poll) | |
| if ticks_diff(now, self.next_poll) > 0: | |
| self.value = 1 - self.value | |
| if(self.enabled): | |
| self._output() | |
| self.next_poll = ticks_add(now, self._calc_delay()) | |
| leds = [RandomlyBlinkingLed(pin) for pin in BLINKENLICHT] | |
| # This is a polling loop function. It takes the current state as argument. | |
| def blinkenlicht_poll(state): | |
| for index, led in enumerate(leds): | |
| flag = 2 ** index | |
| led.enable(state & flag != flag) | |
| led.poll() | |
| # We blink the LEDs corresponding to disconnected wires to help the user wire | |
| # the LEDs correctly before the game. | |
| show_disconnected_last_poll = ticks_ms() | |
| def show_disconnected_wires(state): | |
| global show_disconnected_last_poll | |
| if ticks_diff(ticks_ms(), show_disconnected_last_poll) > 250: | |
| show_disconnected_last_poll = ticks_ms() | |
| for index, led in enumerate(leds): | |
| flag = 2 ** index | |
| if state & flag == flag: | |
| led.enable(True) | |
| led.blink() | |
| else: | |
| led.enable(False) | |
| # ---------------------------------------------------------------- | |
| # MORSE Routines | |
| # ---------------------------------------------------------------- | |
| MORSE = [0b00000010, # A | |
| 0b11110001, # B | |
| 0b11110101, # C | |
| 0b11111001, # D | |
| 0b11111110, # E | |
| 0b11110100, # F | |
| 0b11111011, # G | |
| 0b11110000, # H | |
| 0b11111100, # I | |
| 0b00001110, # J | |
| 0b00000101, # K | |
| 0b11110010, # L | |
| 0b00000011, # M | |
| 0b11111101, # N | |
| 0b00000111, # O | |
| 0b11110110, # P | |
| 0b00001011, # Q | |
| 0b11111010, # R | |
| 0b11111000, # S | |
| 0b00000001, # T | |
| 0b00000100, # U | |
| 0b00001000, # V | |
| 0b00000110, # W | |
| 0b00001001, # X | |
| 0b00001101, # Y | |
| 0b11110011] # Z | |
| MORSE_STATE_INTER_CHARACTER = 0 | |
| MORSE_STATE_SYMBOL = 1 | |
| MORSE_STATE_INTER_SYMBOL = 2 | |
| morseState = MORSE_STATE_INTER_CHARACTER | |
| morseCursor = -1 | |
| morseCurrentCharacter = -1 | |
| morseNextAlarm = ticks_ms() | |
| def morse_led_off(): | |
| pico_led.off() | |
| def morse_led_on(): | |
| pico_led.on() | |
| def morse_sleep(interval): | |
| global morseNextAlarm | |
| morseNextAlarm = ticks_add(ticks_ms(), interval) | |
| def morse_play_current_symbol(): | |
| global morseState | |
| interval = MORSE_DIT_LENGTH if morseCurrentCharacter & 1 == 0 else MORSE_DAH_LENGTH | |
| morseState = MORSE_STATE_SYMBOL | |
| morse_led_on() | |
| morse_sleep(interval) | |
| def morse_shift_bits(): | |
| global morseCurrentCharacter | |
| morseCurrentCharacter = morseCurrentCharacter >> 1 | |
| if morseCurrentCharacter & 0x40 == 0x40: | |
| morseCurrentCharacter = morseCurrentCharacter | 0x80 | |
| def morse_step(): | |
| global morseState | |
| global morseCursor | |
| global morseCurrentCharacter | |
| if ticks_diff(ticks_ms(), morseNextAlarm) < 0: | |
| return | |
| if morseState == MORSE_STATE_INTER_CHARACTER: | |
| morseCursor = morseCursor + 1 | |
| if morseCursor >= len(MORSE_CLUE): | |
| morseCursor = -1 | |
| morse_sleep(MORSE_INTER_MESSAGE_LENGTH) | |
| return | |
| currentLetter = MORSE_CLUE[morseCursor] | |
| if currentLetter == ' ': | |
| morse_led_off() | |
| morse_sleep(MORSE_INTER_WORD_LENGTH) | |
| return | |
| else: | |
| ordinal = ord(currentLetter.upper()) - ord('A') | |
| morseCurrentCharacter = MORSE[ordinal] | |
| morse_play_current_symbol() | |
| return | |
| elif morseState == MORSE_STATE_INTER_SYMBOL: | |
| morse_shift_bits() | |
| if morseCurrentCharacter == 0 or morseCurrentCharacter == 0xFF: | |
| morseState = MORSE_STATE_INTER_CHARACTER | |
| morse_sleep(MORSE_INTER_CHARACTER_LENGTH) # On top of after symbol | |
| return | |
| else: | |
| morse_play_current_symbol() | |
| return | |
| else: | |
| morseState = MORSE_STATE_INTER_SYMBOL | |
| morse_led_off() | |
| morse_sleep(MORSE_DIT_LENGTH) | |
| # ---------------------------------------------------------------- | |
| # WIRE PUZZLE ROUTINES | |
| # ---------------------------------------------------------------- | |
| # Return the button state in terms of the actual logic levels on the | |
| # pins as a bit field. The least significant bit is the first wire to | |
| # be cut. When all wires are connected this will be 0. When all are | |
| # cut it will be WINNING_STATE which is 31 for our 5 wire system. | |
| def get_button_state(): | |
| raw = sum(wire.value * 2**index for (index,wire) in enumerate(wires)) | |
| return raw ^ WINNING_STATE | |
| # Calculate the nex expected state given a valid state. | |
| def next_state(state): | |
| return (state << 1) | 1 | |
| def prior_state(state): | |
| return (state >> 1) | |
| def is_winning_state(state): | |
| return state & WINNING_STATE == WINNING_STATE | |
| def is_reset_state(state): | |
| return state == 0 | |
| def wait_for_change(state): | |
| new_state = state | |
| print("wait_for_change") | |
| while new_state == state: | |
| new_state = get_button_state() | |
| blinkenlicht_poll(new_state) | |
| if(state & WINNING_STATE != WINNING_STATE): | |
| morse_step() | |
| return new_state | |
| def wait_for_reset(state): | |
| while state != RESET_STATE: | |
| state = get_button_state() | |
| show_disconnected_wires(state) | |
| return state | |
| # ---------------------------------------------------------------- | |
| # GAME LOGIC | |
| # ---------------------------------------------------------------- | |
| def on_win(state): | |
| print("Bomb defused!") | |
| pico_led.off() | |
| sleep(1) # Added more pause for debounce | |
| state = wait_for_change(state) | |
| state = wait_for_reset(state) | |
| return state | |
| def on_lose(state): | |
| buzzer.value(1) | |
| print("BOOOOOOM") | |
| pico_led.on() | |
| sleep(1) # Added more pause for debounce | |
| state = wait_for_change(state) | |
| buzzer.value(0) | |
| state = wait_for_reset(state) | |
| return state | |
| state = 0xFF | |
| while True: | |
| buzzer.value(0) | |
| print("Initialsing....") | |
| state = wait_for_reset(state) | |
| print("Initialised") | |
| expected = next_state(RESET_STATE) | |
| state = wait_for_change(RESET_STATE) | |
| backward_state = RESET_STATE # Added ability to step backwards to improve debounce | |
| pico_led.blink() | |
| while state != RESET_STATE: | |
| if is_winning_state(state): | |
| state = on_win(state) | |
| elif state == backward_state or state == expected: | |
| expected = next_state(state) | |
| backward_state = prior_state(state) | |
| state = wait_for_change(state) | |
| else: | |
| state = on_lose(state) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment