#168Intermediate

Raspberry Pi Pico LED Tug of War Game (NeoPixel Strip + 2 Buttons + Buzzer)

A two-player button duel! The **Raspberry Pi Pico** turns a 30-LED **WS2812B NeoPixel strip** into a rope: red player on the left, blue player on the right. Every press pushes your color one LED forward; fill the whole strip to win the round, and take 3 rounds to win the match. Press too early in the countdown and you lose ground, the **passive buzzer** gives each player a different click, and the match ends in a rainbow. `neopixel`, debouncing and non-blocking sound in MicroPython.

0.0(0)
Raspberry Pi Pico LED Tug of War Game (NeoPixel Strip + 2 Buttons + Buzzer)
A glowing tug-of-war game where you and a friend race to see whose fingers are faster. One side of the 30-LED light strip is red and the other blue; whoever hits their button faster pulls the knot in the middle toward their side, and the first to paint the whole strip their color wins the round. Jumping the gun costs you, the first to win three rounds takes the match, and a rainbow show with a victory tune follows.

Video

Circuit Diagram

Raspberry Pi Pico LED Tug of War Game (NeoPixel Strip + 2 Buttons + Buzzer) - Circuit

Source Code

MicroPython
1# ─────────────────────────────────────────
2#   BlueGrays · bluegrays.com
3#   Raspberry Pi Pico LED Tug of War Game (NeoPixel Strip + 2 Buttons + Buzzer)
4# ─────────────────────────────────────────
5# Two-player button duel. Every press pushes your color one step along the
6# strip; the first to reach the far end wins the round, first to 3 rounds
7# wins the match. Pressing early during the countdown costs you 2 steps.
8#
9# Pins:
10#   GP2  -> strip DIN
11#   GP14 -> Player 1 button (RED), diger ucu GND
12#   GP16 -> Player 2 button (BLUE), diger ucu GND
13#   GP15 -> Buzzer (+)
14
15from machine import Pin, PWM
16from neopixel import NeoPixel
17import time
18import math
19
20# ============ SETTINGS ============
21NUM_LEDS = 30          # LEDs on the strip
22BRIGHTNESS = 0.6       # ~0.4 A with colours, fine for USB
23WIN_ROUNDS = 3         # Rounds to win the match
24FALSE_START_STEPS = 2  # False start penalty
25SOUND_ON = True        # Sound effects
26
27RED = (255, 0, 0)      # Player 1
28BLUE = (0, 60, 255)    # Player 2
29
30# Knot = number of red LEDs (the border between colors). Both players need the same number of steps.
31START = NUM_LEDS // 2                   # Starting border
32GOAL = min(START, NUM_LEDS - START)     # Steps needed to win
33
34# ============ HARDWARE ============
35strip = NeoPixel(Pin(2), NUM_LEDS)
36btn1 = Pin(14, Pin.IN, Pin.PULL_UP)   # Player 1
37btn2 = Pin(16, Pin.IN, Pin.PULL_UP)   # Player 2
38buzzer = PWM(Pin(15))
39buzzer.duty_u16(0)
40
41
42# ============ SOUND ============
43class Sound:
44    """
45    TR: Bloklamayan nota calici: oyun ses calarken de butonlari okur
46    EN: Non-blocking note player: the game keeps reading buttons while sound plays
47    """
48
49    def __init__(self, pwm):
50        self.pwm = pwm
51        self.queue = []
52        self.until = 0
53
54    def play(self, notes):
55        # notes = [(frequency, ms), ...], frequency 0 = rest
56        if SOUND_ON:
57            self.queue = list(notes)
58            self.until = 0
59
60    def update(self):
61        now = time.ticks_ms()
62        if self.until and time.ticks_diff(now, self.until) < 0:
63            return
64        if not self.queue:
65            if self.until:
66                self.pwm.duty_u16(0)
67                self.until = 0
68            return
69        freq, ms = self.queue.pop(0)
70        if freq:
71            self.pwm.freq(freq)
72            self.pwm.duty_u16(20000)
73        else:
74            self.pwm.duty_u16(0)
75        self.until = time.ticks_add(now, ms)
76
77
78sound = Sound(buzzer)
79
80
81# ============ BUTTON ============
82class Button:
83    """
84    TR: 15 ms debounce ile yeni basislari yakalar
85    EN: Catches new presses with a 15 ms debounce
86    """
87
88    def __init__(self, pin):
89        self.pin = pin
90        self.state = 1
91        self.raw = 1
92        self.changed = time.ticks_ms()
93
94    def pressed(self):
95        now = time.ticks_ms()
96        raw = self.pin.value()
97        if raw != self.raw:
98            self.raw = raw
99            self.changed = now
100        if time.ticks_diff(now, self.changed) > 15 and raw != self.state:
101            self.state = raw
102            return raw == 0
103        return False
104
105    def sync(self):
106        # Forget stale presses (a button held between rounds is not penalized)
107        self.state = self.raw = self.pin.value()
108        self.changed = time.ticks_ms()
109
110
111b1 = Button(btn1)
112b2 = Button(btn2)
113
114
115# ============ DRAWING HELPERS ============
116def dim(color, k=1.0):
117    # Scale a color by global brightness and k
118    s = BRIGHTNESS * k
119    return (int(color[0] * s), int(color[1] * s), int(color[2] * s))
120
121
122def fill(color, k=1.0):
123    c = dim(color, k)
124    for i in range(NUM_LEDS):
125        strip[i] = c
126    strip.write()
127
128
129def draw_rope(knot):
130    # `knot` LEDs are red (left), the rest blue (right); 2 LEDs on each side of the border form the
131    # bright "knot"
132    for i in range(NUM_LEDS):
133        if i < knot:
134            k = 1.0 if knot - i <= 2 else 0.45   # LEDs near the knot are brighter
135            strip[i] = dim(RED, k)
136        else:
137            k = 1.0 if i - knot < 2 else 0.45
138            strip[i] = dim(BLUE, k)
139    strip.write()
140
141
142def wheel(pos):
143    # Convert 0-255 to a rainbow color
144    pos %= 256
145    if pos < 85:
146        return (255 - pos * 3, pos * 3, 0)
147    if pos < 170:
148        pos -= 85
149        return (0, 255 - pos * 3, pos * 3)
150    pos -= 170
151    return (pos * 3, 0, 255 - pos * 3)
152
153
154def wait_ms(ms):
155    # Wait while keeping the sound running
156    end = time.ticks_add(time.ticks_ms(), ms)
157    while time.ticks_diff(end, time.ticks_ms()) > 0:
158        sound.update()
159        time.sleep_ms(5)
160
161
162# ============ GAME PHASES ============
163def attract():
164    # Waiting: both colors breathe, any button starts the game
165    print("Baslamak icin bir butona bas / Press a button to start")
166    b1.sync()
167    b2.sync()
168    t0 = time.ticks_ms()
169    while True:
170        sound.update()
171        t = time.ticks_diff(time.ticks_ms(), t0)
172        k = 0.25 + 0.75 * (math.sin(t / 400) + 1) / 2
173        for i in range(NUM_LEDS):
174            if i < START:
175                strip[i] = dim(RED, k)
176            else:
177                strip[i] = dim(BLUE, 1.25 - k)   # Breathes in opposite phase
178        strip.write()
179        if b1.pressed() or b2.pressed():
180            return
181        time.sleep_ms(15)
182
183
184def countdown():
185    # 3-2-1 countdown; early presses are penalized. Returns the starting knot
186    knot = START
187    b1.sync()
188    b2.sync()
189    colors = [(255, 0, 0), (255, 150, 0), (0, 255, 0)]   # Red, yellow, green
190    for step in range(3):
191        fill(colors[step], 0.6)
192        if step == 2:
193            sound.play([(1200, 200)])   # GO!
194            break
195        sound.play([(600, 150)])
196        end = time.ticks_add(time.ticks_ms(), 800)
197        while time.ticks_diff(end, time.ticks_ms()) > 0:
198            sound.update()
199            if b1.pressed():
200                knot -= FALSE_START_STEPS
201                print("Oyuncu 1 erken basti! / Player 1 false start!")
202                sound.play([(150, 200)])
203            if b2.pressed():
204                knot += FALSE_START_STEPS
205                print("Oyuncu 2 erken basti! / Player 2 false start!")
206                sound.play([(150, 200)])
207            time.sleep_ms(5)
208    # Penalties must not push the knot to an end
209    return max(START - GOAL + 2, min(START + GOAL - 2, knot))
210
211
212def play_round(knot):
213    # Button duel; returns the winner (1 or 2) when the knot reaches an end
214    draw_rope(knot)
215    while True:
216        sound.update()
217        moved = False
218        if b1.pressed():
219            knot += 1
220            sound.play([(440, 25)])
221            moved = True
222        if b2.pressed():
223            knot -= 1
224            sound.play([(660, 25)])
225            moved = True
226        if moved:
227            if knot >= START + GOAL:   # Strip fully red
228                return 1
229            if knot <= START - GOAL:   # Strip fully blue
230                return 2
231            draw_rope(knot)
232        time.sleep_ms(1)
233
234
235def celebrate_round(winner, scores):
236    # Winner color flashes 3 times, then the score dots are shown
237    color = RED if winner == 1 else BLUE
238    print("Turu kazanan / Round winner: Oyuncu / Player", winner, "| Skor / Score:", scores[0], "-", scores[1])
239    sound.play([(523, 120), (659, 120), (784, 120), (1047, 300)])
240    for _ in range(3):
241        fill(color)
242        wait_ms(180)
243        fill((0, 0, 0))
244        wait_ms(120)
245    # Score: red dots from the left, blue dots from the right
246    for i in range(NUM_LEDS):
247        strip[i] = (0, 0, 0)
248    for s in range(scores[0]):
249        strip[s * 2] = dim(RED)
250    for s in range(scores[1]):
251        strip[NUM_LEDS - 1 - s * 2] = dim(BLUE)
252    strip.write()
253    wait_ms(1800)
254
255
256def celebrate_match(winner):
257    # End of match: rainbow wave and victory tune
258    print("MACI KAZANAN / MATCH WINNER: Oyuncu / Player", winner)
259    sound.play([(784, 150), (784, 150), (784, 150), (1047, 450), (0, 80), (988, 150), (1047, 500)])
260    t0 = time.ticks_ms()
261    while time.ticks_diff(time.ticks_ms(), t0) < 3500:
262        sound.update()
263        offset = time.ticks_diff(time.ticks_ms(), t0) // 8
264        for i in range(NUM_LEDS):
265            strip[i] = dim(wheel(i * 256 // NUM_LEDS + offset))
266        strip.write()
267        time.sleep_ms(15)
268    fill(RED if winner == 1 else BLUE)
269    wait_ms(1000)
270
271
272# ============ MAIN LOOP ============
273print("== BlueGrays · bluegrays.com ==")
274print("Pico LED Halat Cekme / LED Tug of War")
275
276scores = [0, 0]
277while True:
278    attract()
279    while max(scores) < WIN_ROUNDS:
280        start_knot = countdown()
281        winner = play_round(start_knot)
282        scores[winner - 1] += 1
283        celebrate_round(winner, scores)
284    celebrate_match(1 if scores[0] > scores[1] else 2)
285    scores = [0, 0]
286

FAQ

My strip doesn't have 30 LEDs. Can I use a different length?▾

Yes. Set `NUM_LEDS` at the top of the code to the number of LEDs on your strip; the starting point (`START`) and the steps needed to win (`GOAL`) are calculated automatically. On a 60-LED strip each player needs 30 presses, so rounds last longer.

Do I really need the 330 Ω resistor and the 470 µF capacitor?▾

Not in Wokwi, but they are recommended for the real circuit. Put a 330 Ω resistor in series between GP2 and the strip's DIN, and a 470 µF capacitor across the strip's 5V and GND (long leg to 5V). They protect the first LED and prevent flicker and random colors.

Is the Pico's USB enough to power the strip?▾

With 30 LEDs and `BRIGHTNESS = 0.6` the strip draws about 0.4 A at most, which USB can handle. VBUS only provides 5V while USB is plugged in. For a longer strip or higher brightness, use a separate 5V/2A adapter and connect the grounds.

How do I change the number of rounds and the false start penalty?▾

`WIN_ROUNDS` is the number of rounds needed to win the match (3 by default). `FALSE_START_STEPS` sets how many steps an early presser is pushed back (2 by default; set it to 0 to turn the penalty off). For a silent game, set `SOUND_ON = False`.