Skip to content

Instantly share code, notes, and snippets.

@m0rjc
Created December 10, 2025 09:47
Show Gist options
  • Select an option

  • Save m0rjc/7b5d0256fb11786057029bd317b00eb0 to your computer and use it in GitHub Desktop.

Select an option

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
# ----------------------------------------------------------------
# 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