Jogo de cabo de guerra com LEDs e Raspberry Pi Pico (fita NeoPixel + 2 botões + buzzer)
Duelo de botões para dois jogadores! O **Raspberry Pi Pico** transforma uma **fita NeoPixel WS2812B** de 30 LEDs em uma corda: vermelho à esquerda, azul à direita. Cada toque avança sua cor em um LED; preencha a fita para vencer a rodada e ganhe 3 rodadas para vencer a partida. Se apertar cedo demais na contagem regressiva, você perde terreno; o **buzzer passivo** dá um clique diferente a cada jogador, e a partida termina em um arco-íris. `neopixel`, debounce e som sem bloqueio em MicroPython.

Vídeo
Esquema do circuito

Código-fonte
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]
286Perguntas frequentes
Minha fita não tem 30 LEDs. Posso usar outro comprimento?▾
Sim. Defina `NUM_LEDS` no início do código com a quantidade de LEDs da sua fita; o ponto inicial (`START`) e os passos necessários para vencer (`GOAL`) são calculados automaticamente. Em uma fita de 60 LEDs, cada jogador precisa apertar 30 vezes para vencer, então as rodadas duram mais.
Preciso mesmo do resistor de 330 Ω e do capacitor de 470 µF?▾
No Wokwi, não, mas são recomendados no circuito real. Coloque um resistor de 330 Ω em série entre GP2 e DIN da fita, e um capacitor de 470 µF entre 5V e GND da fita (terminal longo no 5V). Eles protegem o primeiro LED e evitam cintilação e cores aleatórias.
O USB do Pico é suficiente para alimentar a fita?▾
Com 30 LEDs e `BRIGHTNESS = 0.6`, a fita consome no máximo cerca de 0,4 A, o que o USB consegue fornecer. VBUS só fornece 5V enquanto o USB está conectado. Para uma fita mais longa ou maior brilho, use uma fonte separada de 5V/2A e conecte os GNDs.
Como mudar a quantidade de rodadas e a penalidade por largada antecipada?▾
`WIN_ROUNDS` é a quantidade de rodadas necessárias para vencer a partida (3 por padrão). `FALSE_START_STEPS` define quantos passos recua quem aperta antes da hora (2 por padrão; defina como 0 para desativar a penalidade). Para jogar sem som, defina `SOUND_ON = False`.
