#168Intermediário

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.

0.0(0)
Jogo de cabo de guerra com LEDs e Raspberry Pi Pico (fita NeoPixel + 2 botões + buzzer)
Um jogo luminoso de cabo de guerra em que você e um amigo disputam quem tem os dedos mais rápidos. Um lado da fita de 30 LEDs é vermelho e o outro é azul; quem aperta o botão mais rápido puxa o nó do meio para o seu lado, e quem preencher toda a fita com sua cor primeiro vence a rodada. Quem começa antes da hora recebe uma penalidade, o primeiro a ganhar três rodadas vence a partida, e depois vem um show de arco-íris com uma melodia de vitória.

Vídeo

Esquema do circuito

Jogo de cabo de guerra com LEDs e Raspberry Pi Pico (fita NeoPixel + 2 botões + buzzer) - Circuit

Código-fonte

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

Perguntas 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`.