Raspberry Pi Pico LED-Tauziehen (NeoPixel-Streifen + 2 Taster + Summer)
Ein Tasten-Duell für zwei! Der **Raspberry Pi Pico** macht aus einem **WS2812B-NeoPixel-Streifen** mit 30 LEDs ein Seil: links Rot, rechts Blau. Jeder Druck schiebt deine Farbe eine LED weiter; wer den ganzen Streifen einfärbt, gewinnt die Runde, wer zuerst 3 Runden holt, das Match. Frühstarts im Countdown werden bestraft, der **passive Summer** gibt jedem Spieler einen eigenen Klick, und am Ende gibt es einen Regenbogen. `neopixel`, Entprellung und nicht blockierender Ton in MicroPython.

Video
Schaltplan

Quellcode
1# ─────────────────────────────────────────
2# BlueGrays · bluegrays.com
3# Raspberry Pi Pico LED-Tauziehen (NeoPixel-Streifen + 2 Taster + Summer)
4# ─────────────────────────────────────────
5# Tasten-Duell für zwei Spieler. Jeder Druck schiebt deine Farbe einen
6# Schritt am Streifen entlang; wer zuerst das ferne Ende erreicht, gewinnt
7# die Runde, wer zuerst 3 Runden hat, gewinnt das Match. Ein zu früher
8# Druck während des Countdowns kostet dich 2 Schritte.
9#
10# Pins:
11# GP2 -> Streifen DIN
12# GP14 -> Taste Spieler 1 (ROT), anderes Beinchen an GND
13# GP16 -> Taste Spieler 2 (BLAU), anderes Beinchen an GND
14# GP15 -> Buzzer (+)
15
16from machine import Pin, PWM
17from neopixel import NeoPixel
18import time
19import math
20
21# ============ EINSTELLUNGEN ============
22NUM_LEDS = 30 # LEDs am Streifen
23BRIGHTNESS = 0.6 # ~0.4 A mit Farben, für USB unbedenklich
24WIN_ROUNDS = 3 # Runden zum Gewinn des Matches
25FALSE_START_STEPS = 2 # Strafe für Fehlstart
26SOUND_ON = True # Soundeffekte
27
28RED = (255, 0, 0) # Spieler 1
29BLUE = (0, 60, 255) # Spieler 2
30
31# Knoten = Anzahl der roten LEDs (die Grenze zwischen den Farben). Beide Spieler brauchen dieselbe
32# Anzahl Schritte.
33START = NUM_LEDS // 2 # Startrand
34GOAL = min(START, NUM_LEDS - START) # Zum Gewinnen nötige Schritte
35
36# ============ HARDWARE ============
37strip = NeoPixel(Pin(2), NUM_LEDS)
38btn1 = Pin(14, Pin.IN, Pin.PULL_UP) # Spieler 1
39btn2 = Pin(16, Pin.IN, Pin.PULL_UP) # Spieler 2
40buzzer = PWM(Pin(15))
41buzzer.duty_u16(0)
42
43
44# ============ SOUND ============
45class Sound:
46 """
47 TR: Bloklamayan nota calici: oyun ses calarken de butonlari okur
48 EN: Non-blocking note player: the game keeps reading buttons while sound plays
49 """
50
51 def __init__(self, pwm):
52 self.pwm = pwm
53 self.queue = []
54 self.until = 0
55
56 def play(self, notes):
57 # notes = [(Frequenz, ms), ...], Frequenz 0 = Pause
58 if SOUND_ON:
59 self.queue = list(notes)
60 self.until = 0
61
62 def update(self):
63 now = time.ticks_ms()
64 if self.until and time.ticks_diff(now, self.until) < 0:
65 return
66 if not self.queue:
67 if self.until:
68 self.pwm.duty_u16(0)
69 self.until = 0
70 return
71 freq, ms = self.queue.pop(0)
72 if freq:
73 self.pwm.freq(freq)
74 self.pwm.duty_u16(20000)
75 else:
76 self.pwm.duty_u16(0)
77 self.until = time.ticks_add(now, ms)
78
79
80sound = Sound(buzzer)
81
82
83# ============ TASTE ============
84class Button:
85 """
86 TR: 15 ms debounce ile yeni basislari yakalar
87 EN: Catches new presses with a 15 ms debounce
88 """
89
90 def __init__(self, pin):
91 self.pin = pin
92 self.state = 1
93 self.raw = 1
94 self.changed = time.ticks_ms()
95
96 def pressed(self):
97 now = time.ticks_ms()
98 raw = self.pin.value()
99 if raw != self.raw:
100 self.raw = raw
101 self.changed = now
102 if time.ticks_diff(now, self.changed) > 15 and raw != self.state:
103 self.state = raw
104 return raw == 0
105 return False
106
107 def sync(self):
108 # Veraltete Tastendrücke vergessen (eine zwischen den Runden gehaltene Taste wird nicht
109 # bestraft)
110 self.state = self.raw = self.pin.value()
111 self.changed = time.ticks_ms()
112
113
114b1 = Button(btn1)
115b2 = Button(btn2)
116
117
118# ============ ZEICHEN-HILFSFUNKTIONEN ============
119def dim(color, k=1.0):
120 # Eine Farbe mit der globalen Helligkeit und k skalieren
121 s = BRIGHTNESS * k
122 return (int(color[0] * s), int(color[1] * s), int(color[2] * s))
123
124
125def fill(color, k=1.0):
126 c = dim(color, k)
127 for i in range(NUM_LEDS):
128 strip[i] = c
129 strip.write()
130
131
132def draw_rope(knot):
133 # `knot` LEDs sind rot (links), der Rest blau (rechts); je 2 LEDs auf jeder Seite der Grenze
134 # bilden den hellen "Knoten"
135 for i in range(NUM_LEDS):
136 if i < knot:
137 k = 1.0 if knot - i <= 2 else 0.45 # LEDs nahe dem Knoten sind heller
138 strip[i] = dim(RED, k)
139 else:
140 k = 1.0 if i - knot < 2 else 0.45
141 strip[i] = dim(BLUE, k)
142 strip.write()
143
144
145def wheel(pos):
146 # 0-255 in eine Regenbogenfarbe umrechnen
147 pos %= 256
148 if pos < 85:
149 return (255 - pos * 3, pos * 3, 0)
150 if pos < 170:
151 pos -= 85
152 return (0, 255 - pos * 3, pos * 3)
153 pos -= 170
154 return (pos * 3, 0, 255 - pos * 3)
155
156
157def wait_ms(ms):
158 # Warten und dabei den Sound laufen lassen
159 end = time.ticks_add(time.ticks_ms(), ms)
160 while time.ticks_diff(end, time.ticks_ms()) > 0:
161 sound.update()
162 time.sleep_ms(5)
163
164
165# ============ SPIELPHASEN ============
166def attract():
167 # Warten: Beide Farben atmen, jede Taste startet das Spiel
168 print("Baslamak icin bir butona bas / Press a button to start")
169 b1.sync()
170 b2.sync()
171 t0 = time.ticks_ms()
172 while True:
173 sound.update()
174 t = time.ticks_diff(time.ticks_ms(), t0)
175 k = 0.25 + 0.75 * (math.sin(t / 400) + 1) / 2
176 for i in range(NUM_LEDS):
177 if i < START:
178 strip[i] = dim(RED, k)
179 else:
180 strip[i] = dim(BLUE, 1.25 - k) # Atmet in Gegenphase
181 strip.write()
182 if b1.pressed() or b2.pressed():
183 return
184 time.sleep_ms(15)
185
186
187def countdown():
188 # 3-2-1-Countdown; zu frühe Tastendrücke werden bestraft. Gibt den Startknoten zurück
189 knot = START
190 b1.sync()
191 b2.sync()
192 colors = [(255, 0, 0), (255, 150, 0), (0, 255, 0)] # Rot, Gelb, Grün
193 for step in range(3):
194 fill(colors[step], 0.6)
195 if step == 2:
196 sound.play([(1200, 200)]) # GO!
197 break
198 sound.play([(600, 150)])
199 end = time.ticks_add(time.ticks_ms(), 800)
200 while time.ticks_diff(end, time.ticks_ms()) > 0:
201 sound.update()
202 if b1.pressed():
203 knot -= FALSE_START_STEPS
204 print("Oyuncu 1 erken basti! / Player 1 false start!")
205 sound.play([(150, 200)])
206 if b2.pressed():
207 knot += FALSE_START_STEPS
208 print("Oyuncu 2 erken basti! / Player 2 false start!")
209 sound.play([(150, 200)])
210 time.sleep_ms(5)
211 # Strafen dürfen den Knoten nicht an ein Ende schieben
212 return max(START - GOAL + 2, min(START + GOAL - 2, knot))
213
214
215def play_round(knot):
216 # Tastenduell; gibt den Gewinner (1 oder 2) zurück, wenn der Knoten ein Ende erreicht
217 draw_rope(knot)
218 while True:
219 sound.update()
220 moved = False
221 if b1.pressed():
222 knot += 1
223 sound.play([(440, 25)])
224 moved = True
225 if b2.pressed():
226 knot -= 1
227 sound.play([(660, 25)])
228 moved = True
229 if moved:
230 if knot >= START + GOAL: # Streifen ganz rot
231 return 1
232 if knot <= START - GOAL: # Streifen ganz blau
233 return 2
234 draw_rope(knot)
235 time.sleep_ms(1)
236
237
238def celebrate_round(winner, scores):
239 # Die Gewinnerfarbe blinkt 3-mal, dann werden die Punkte des Punktestands gezeigt
240 color = RED if winner == 1 else BLUE
241 print("Turu kazanan / Round winner: Oyuncu / Player", winner, "| Skor / Score:", scores[0], "-", scores[1])
242 sound.play([(523, 120), (659, 120), (784, 120), (1047, 300)])
243 for _ in range(3):
244 fill(color)
245 wait_ms(180)
246 fill((0, 0, 0))
247 wait_ms(120)
248 # Punktestand: rote Punkte von links, blaue Punkte von rechts
249 for i in range(NUM_LEDS):
250 strip[i] = (0, 0, 0)
251 for s in range(scores[0]):
252 strip[s * 2] = dim(RED)
253 for s in range(scores[1]):
254 strip[NUM_LEDS - 1 - s * 2] = dim(BLUE)
255 strip.write()
256 wait_ms(1800)
257
258
259def celebrate_match(winner):
260 # Ende des Matches: Regenbogenwelle und Siegesmelodie
261 print("MACI KAZANAN / MATCH WINNER: Oyuncu / Player", winner)
262 sound.play([(784, 150), (784, 150), (784, 150), (1047, 450), (0, 80), (988, 150), (1047, 500)])
263 t0 = time.ticks_ms()
264 while time.ticks_diff(time.ticks_ms(), t0) < 3500:
265 sound.update()
266 offset = time.ticks_diff(time.ticks_ms(), t0) // 8
267 for i in range(NUM_LEDS):
268 strip[i] = dim(wheel(i * 256 // NUM_LEDS + offset))
269 strip.write()
270 time.sleep_ms(15)
271 fill(RED if winner == 1 else BLUE)
272 wait_ms(1000)
273
274
275# ============ HAUPTSCHLEIFE ============
276print("== BlueGrays · bluegrays.com ==")
277print("Pico LED Halat Cekme / LED Tug of War")
278
279scores = [0, 0]
280while True:
281 attract()
282 while max(scores) < WIN_ROUNDS:
283 start_knot = countdown()
284 winner = play_round(start_knot)
285 scores[winner - 1] += 1
286 celebrate_round(winner, scores)
287 celebrate_match(1 if scores[0] > scores[1] else 2)
288 scores = [0, 0]
289FAQ
Mein Streifen hat nicht 30 LEDs. Kann ich eine andere Länge verwenden?▾
Ja. Setze `NUM_LEDS` am Anfang des Codes auf die LED-Zahl deines Streifens; Startpunkt (`START`) und die nötigen Schritte zum Sieg (`GOAL`) werden automatisch berechnet. Bei 60 LEDs braucht jeder Spieler 30 Drücke, die Runden dauern also länger.
Brauche ich den 330-Ω-Widerstand und den 470-µF-Kondensator wirklich?▾
In Wokwi nicht, in der echten Schaltung werden sie empfohlen. Setze einen 330-Ω-Widerstand in Reihe zwischen GP2 und DIN des Streifens und einen 470-µF-Kondensator zwischen 5V und GND des Streifens (langes Bein an 5V). Das schützt die erste LED und verhindert Flackern und zufällige Farben.
Reicht der USB-Anschluss des Pico, um den Streifen zu versorgen?▾
Mit 30 LEDs und `BRIGHTNESS = 0.6` zieht der Streifen höchstens etwa 0,4 A, das schafft USB. VBUS liefert 5V nur, solange USB eingesteckt ist. Für einen längeren Streifen oder mehr Helligkeit nimm ein separates 5V/2A-Netzteil und verbinde die GNDs.
Wie ändere ich die Rundenzahl und die Strafe für einen Frühstart?▾
`WIN_ROUNDS` ist die Zahl der Runden für den Matchsieg (Standard 3). `FALSE_START_STEPS` legt fest, um wie viele Schritte ein Frühstarter zurückgeschoben wird (Standard 2; mit 0 ist die Strafe aus). Für ein stilles Spiel setze `SOUND_ON = False`.
