Game Tarik Tambang LED Raspberry Pi Pico (Strip NeoPixel + 2 Tombol + Buzzer)
Duel tombol untuk dua pemain! Raspberry Pi Pico mengubah strip NeoPixel WS2812B 30 LED menjadi tali: merah di kiri, biru di kanan. Setiap tekanan mendorong warnamu satu LED ke depan; penuhi seluruh strip untuk memenangkan ronde, dan 3 ronde untuk memenangkan pertandingan. Menekan terlalu cepat ketika hitung mundur membuatmu mundur, buzzer pasif memberi setiap pemain bunyi klik sendiri, dan pemenang disambut pelangi. neopixel, debounce, dan suara non-blocking di MicroPython.

Video
Skema rangkaian

Kode sumber
1# ─────────────────────────────────────────
2# BlueGrays · bluegrays.com
3# Game Tarik Tambang LED Raspberry Pi Pico (Strip NeoPixel + 2 Tombol + Buzzer)
4# ─────────────────────────────────────────
5# Duel tombol dua pemain. Setiap penekanan mendorong warnamu satu langkah
6# di sepanjang strip; yang pertama mencapai ujung seberang memenangkan
7# ronde, yang pertama mencapai 3 ronde memenangkan pertandingan. Menekan
8# terlalu cepat selama hitung mundur membuatmu kehilangan 2 langkah.
9#
10# Pin:
11# GP2 -> DIN strip
12# GP14 -> Tombol pemain 1 (RED), kaki lainnya ke GND
13# GP16 -> Tombol pemain 2 (BLUE), kaki lainnya ke GND
14# GP15 -> Buzzer (+)
15
16from machine import Pin, PWM
17from neopixel import NeoPixel
18import time
19import math
20
21# ============ PENGATURAN ============
22NUM_LEDS = 30 # LED di strip
23BRIGHTNESS = 0.6 # ~0.4 A dengan warna, aman untuk USB
24WIN_ROUNDS = 3 # Ronde untuk memenangkan pertandingan
25FALSE_START_STEPS = 2 # Penalti start terlalu cepat
26SOUND_ON = True # Efek suara
27
28RED = (255, 0, 0) # Pemain 1
29BLUE = (0, 60, 255) # Pemain 2
30
31# Knot = jumlah LED merah (batas antar warna). Kedua pemain butuh jumlah langkah yang sama.
32START = NUM_LEDS // 2 # Batas awal
33GOAL = min(START, NUM_LEDS - START) # Langkah yang dibutuhkan untuk menang
34
35# ============ HARDWARE ============
36strip = NeoPixel(Pin(2), NUM_LEDS)
37btn1 = Pin(14, Pin.IN, Pin.PULL_UP) # Pemain 1
38btn2 = Pin(16, Pin.IN, Pin.PULL_UP) # Pemain 2
39buzzer = PWM(Pin(15))
40buzzer.duty_u16(0)
41
42
43# ============ SUARA ============
44class Sound:
45 """
46 TR: Bloklamayan nota calici: oyun ses calarken de butonlari okur
47 EN: Non-blocking note player: the game keeps reading buttons while sound plays
48 """
49
50 def __init__(self, pwm):
51 self.pwm = pwm
52 self.queue = []
53 self.until = 0
54
55 def play(self, notes):
56 # notes = [(frekuensi, ms), ...], frekuensi 0 = jeda
57 if SOUND_ON:
58 self.queue = list(notes)
59 self.until = 0
60
61 def update(self):
62 now = time.ticks_ms()
63 if self.until and time.ticks_diff(now, self.until) < 0:
64 return
65 if not self.queue:
66 if self.until:
67 self.pwm.duty_u16(0)
68 self.until = 0
69 return
70 freq, ms = self.queue.pop(0)
71 if freq:
72 self.pwm.freq(freq)
73 self.pwm.duty_u16(20000)
74 else:
75 self.pwm.duty_u16(0)
76 self.until = time.ticks_add(now, ms)
77
78
79sound = Sound(buzzer)
80
81
82# ============ TOMBOL ============
83class Button:
84 """
85 TR: 15 ms debounce ile yeni basislari yakalar
86 EN: Catches new presses with a 15 ms debounce
87 """
88
89 def __init__(self, pin):
90 self.pin = pin
91 self.state = 1
92 self.raw = 1
93 self.changed = time.ticks_ms()
94
95 def pressed(self):
96 now = time.ticks_ms()
97 raw = self.pin.value()
98 if raw != self.raw:
99 self.raw = raw
100 self.changed = now
101 if time.ticks_diff(now, self.changed) > 15 and raw != self.state:
102 self.state = raw
103 return raw == 0
104 return False
105
106 def sync(self):
107 # Lupakan penekanan lama (tombol yang ditahan di antara ronde tidak dikenai penalti)
108 self.state = self.raw = self.pin.value()
109 self.changed = time.ticks_ms()
110
111
112b1 = Button(btn1)
113b2 = Button(btn2)
114
115
116# ============ HELPER MENGGAMBAR ============
117def dim(color, k=1.0):
118 # Skala warna dengan kecerahan global dan k
119 s = BRIGHTNESS * k
120 return (int(color[0] * s), int(color[1] * s), int(color[2] * s))
121
122
123def fill(color, k=1.0):
124 c = dim(color, k)
125 for i in range(NUM_LEDS):
126 strip[i] = c
127 strip.write()
128
129
130def draw_rope(knot):
131 # `knot` LED berwarna merah (kiri), sisanya biru (kanan); 2 LED di tiap sisi batas membentuk
132 # "knot" yang terang
133 for i in range(NUM_LEDS):
134 if i < knot:
135 k = 1.0 if knot - i <= 2 else 0.45 # LED di dekat knot lebih terang
136 strip[i] = dim(RED, k)
137 else:
138 k = 1.0 if i - knot < 2 else 0.45
139 strip[i] = dim(BLUE, k)
140 strip.write()
141
142
143def wheel(pos):
144 # Ubah 0-255 menjadi warna pelangi
145 pos %= 256
146 if pos < 85:
147 return (255 - pos * 3, pos * 3, 0)
148 if pos < 170:
149 pos -= 85
150 return (0, 255 - pos * 3, pos * 3)
151 pos -= 170
152 return (pos * 3, 0, 255 - pos * 3)
153
154
155def wait_ms(ms):
156 # Tunggu sambil menjaga suara tetap berjalan
157 end = time.ticks_add(time.ticks_ms(), ms)
158 while time.ticks_diff(end, time.ticks_ms()) > 0:
159 sound.update()
160 time.sleep_ms(5)
161
162
163# ============ FASE PERMAINAN ============
164def attract():
165 # Menunggu: kedua warna bernapas, tombol mana pun memulai permainan
166 print("Baslamak icin bir butona bas / Press a button to start")
167 b1.sync()
168 b2.sync()
169 t0 = time.ticks_ms()
170 while True:
171 sound.update()
172 t = time.ticks_diff(time.ticks_ms(), t0)
173 k = 0.25 + 0.75 * (math.sin(t / 400) + 1) / 2
174 for i in range(NUM_LEDS):
175 if i < START:
176 strip[i] = dim(RED, k)
177 else:
178 strip[i] = dim(BLUE, 1.25 - k) # Bernapas dengan fase berlawanan
179 strip.write()
180 if b1.pressed() or b2.pressed():
181 return
182 time.sleep_ms(15)
183
184
185def countdown():
186 # Hitung mundur 3-2-1; penekanan terlalu cepat dikenai penalti. Mengembalikan knot awal
187 knot = START
188 b1.sync()
189 b2.sync()
190 colors = [(255, 0, 0), (255, 150, 0), (0, 255, 0)] # Merah, kuning, hijau
191 for step in range(3):
192 fill(colors[step], 0.6)
193 if step == 2:
194 sound.play([(1200, 200)]) # GO! (mulai!)
195 break
196 sound.play([(600, 150)])
197 end = time.ticks_add(time.ticks_ms(), 800)
198 while time.ticks_diff(end, time.ticks_ms()) > 0:
199 sound.update()
200 if b1.pressed():
201 knot -= FALSE_START_STEPS
202 print("Oyuncu 1 erken basti! / Player 1 false start!")
203 sound.play([(150, 200)])
204 if b2.pressed():
205 knot += FALSE_START_STEPS
206 print("Oyuncu 2 erken basti! / Player 2 false start!")
207 sound.play([(150, 200)])
208 time.sleep_ms(5)
209 # Penalti tidak boleh mendorong knot sampai ke ujung
210 return max(START - GOAL + 2, min(START + GOAL - 2, knot))
211
212
213def play_round(knot):
214 # Duel tombol; mengembalikan pemenang (1 atau 2) ketika knot mencapai ujung
215 draw_rope(knot)
216 while True:
217 sound.update()
218 moved = False
219 if b1.pressed():
220 knot += 1
221 sound.play([(440, 25)])
222 moved = True
223 if b2.pressed():
224 knot -= 1
225 sound.play([(660, 25)])
226 moved = True
227 if moved:
228 if knot >= START + GOAL: # Strip penuh merah
229 return 1
230 if knot <= START - GOAL: # Strip penuh biru
231 return 2
232 draw_rope(knot)
233 time.sleep_ms(1)
234
235
236def celebrate_round(winner, scores):
237 # Warna pemenang berkedip 3 kali, lalu titik skor ditampilkan
238 color = RED if winner == 1 else BLUE
239 print("Turu kazanan / Round winner: Oyuncu / Player", winner, "| Skor / Score:", scores[0], "-", scores[1])
240 sound.play([(523, 120), (659, 120), (784, 120), (1047, 300)])
241 for _ in range(3):
242 fill(color)
243 wait_ms(180)
244 fill((0, 0, 0))
245 wait_ms(120)
246 # Skor: titik merah dari kiri, titik biru dari kanan
247 for i in range(NUM_LEDS):
248 strip[i] = (0, 0, 0)
249 for s in range(scores[0]):
250 strip[s * 2] = dim(RED)
251 for s in range(scores[1]):
252 strip[NUM_LEDS - 1 - s * 2] = dim(BLUE)
253 strip.write()
254 wait_ms(1800)
255
256
257def celebrate_match(winner):
258 # Akhir pertandingan: gelombang pelangi dan lagu kemenangan
259 print("MACI KAZANAN / MATCH WINNER: Oyuncu / Player", winner)
260 sound.play([(784, 150), (784, 150), (784, 150), (1047, 450), (0, 80), (988, 150), (1047, 500)])
261 t0 = time.ticks_ms()
262 while time.ticks_diff(time.ticks_ms(), t0) < 3500:
263 sound.update()
264 offset = time.ticks_diff(time.ticks_ms(), t0) // 8
265 for i in range(NUM_LEDS):
266 strip[i] = dim(wheel(i * 256 // NUM_LEDS + offset))
267 strip.write()
268 time.sleep_ms(15)
269 fill(RED if winner == 1 else BLUE)
270 wait_ms(1000)
271
272
273# ============ LOOP UTAMA ============
274print("== BlueGrays · bluegrays.com ==")
275print("Pico LED Halat Cekme / LED Tug of War")
276
277scores = [0, 0]
278while True:
279 attract()
280 while max(scores) < WIN_ROUNDS:
281 start_knot = countdown()
282 winner = play_round(start_knot)
283 scores[winner - 1] += 1
284 celebrate_round(winner, scores)
285 celebrate_match(1 if scores[0] > scores[1] else 2)
286 scores = [0, 0]
287FAQ
Strip-ku tidak berisi 30 LED. Bisakah aku memakai panjang yang berbeda?
Ya. Atur `NUM_LEDS` di bagian atas kode sesuai jumlah LED di strip-mu; titik awal (`START`) dan langkah yang dibutuhkan untuk menang (`GOAL`) dihitung otomatis. Di strip 60 LED, setiap pemain butuh 30 tekanan, jadi rondenya lebih lama.
Apakah aku benar-benar butuh resistor 330 Ω dan kapasitor 470 µF?
Tidak di Wokwi, tetapi disarankan untuk rangkaian sungguhan. Pasang resistor 330 Ω secara seri di antara GP2 dan DIN strip, dan kapasitor 470 µF di antara 5V dan GND strip (kaki panjang ke 5V). Keduanya melindungi LED pertama serta mencegah kedipan dan warna acak.
Apakah USB Pico cukup untuk memberi daya strip?
Dengan 30 LED dan `BRIGHTNESS = 0.6`, strip menarik paling banyak sekitar 0,4 A, yang masih sanggup ditangani USB. VBUS hanya memberi 5V selama USB tercolok. Untuk strip yang lebih panjang atau kecerahan lebih tinggi, pakai adaptor 5V/2A terpisah dan sambungkan ground-nya.
Bagaimana cara mengubah jumlah ronde dan hukuman mencuri start?
`WIN_ROUNDS` adalah jumlah ronde yang dibutuhkan untuk memenangkan pertandingan (bawaan 3). `FALSE_START_STEPS` mengatur berapa langkah pemain yang menekan terlalu cepat didorong mundur (bawaan 2; atur ke 0 untuk mematikan hukuman). Untuk game tanpa suara, atur `SOUND_ON = False`.
