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.

Video
Circuit Diagram

Source Code
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]
286FAQ
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`.
