#168Средний

Игра «Перетягивание каната» со светодиодами на Raspberry Pi Pico (лента NeoPixel + 2 кнопки + зуммер)

Кнопочная дуэль на двоих! Raspberry Pi Pico превращает ленту WS2812B NeoPixel из 30 светодиодов в канат: красный игрок слева, синий справа. Каждое нажатие продвигает твой цвет на один светодиод; заполни всю ленту, чтобы выиграть раунд, и возьми 3 раунда, чтобы выиграть матч. Нажмёшь раньше времени во время отсчёта — потеряешь позиции; пассивный зуммер даёт каждому игроку свой щелчок, а матч заканчивается радугой. neopixel, подавление дребезга и неблокирующий звук на MicroPython.

0.0(0)
1 собрано
Игра «Перетягивание каната» со светодиодами на Raspberry Pi Pico (лента NeoPixel + 2 кнопки + зуммер)
Светящаяся игра в перетягивание каната, где ты соревнуешься с другом, чьи пальцы быстрее. Одна сторона светодиодной ленты из 30 огоньков красная, другая синяя; кто быстрее жмёт свою кнопку, тот тянет узел посередине на свою сторону, а кто первым закрасит всю ленту своим цветом, выигрывает раунд. Фальстарт наказывается, первый, кто выиграет три раунда, берёт матч, а следом идёт радужное шоу с победной мелодией.

Видео

Схема

Игра «Перетягивание каната» со светодиодами на Raspberry Pi Pico (лента NeoPixel + 2 кнопки + зуммер) - Circuit

Исходный код

MicroPython
1# ─────────────────────────────────────────
2#   BlueGrays · bluegrays.com
3#   Игра «Перетягивание каната» со светодиодами на Raspberry Pi Pico (лента NeoPixel + 2 кнопки + зуммер)
4# ─────────────────────────────────────────
5# Дуэль двух игроков на кнопках. Каждое нажатие толкает твой цвет на шаг
6# вдоль ленты; кто первым дойдёт до дальнего конца, выигрывает раунд, кто
7# первым выиграет 3 раунда — матч. Раннее нажатие во время обратного
8# отсчёта стоит 2 шагов.
9#
10# Пины:
11#   GP2  -> DIN ленты
12#   GP14 -> Кнопка игрока 1 (RED), другая ножка к GND
13#   GP16 -> Кнопка игрока 2 (BLUE), другая ножка к GND
14#   GP15 -> Зуммер (+)
15
16from machine import Pin, PWM
17from neopixel import NeoPixel
18import time
19import math
20
21# ============ НАСТРОЙКИ ============
22NUM_LEDS = 30          # Светодиодов на ленте
23BRIGHTNESS = 0.6       # ~0,4 A с цветами, для USB нормально
24WIN_ROUNDS = 3         # Раундов для победы в матче
25FALSE_START_STEPS = 2  # Штраф за фальстарт
26SOUND_ON = True        # Звуковые эффекты
27
28RED = (255, 0, 0)      # Игрок 1
29BLUE = (0, 60, 255)    # Игрок 2
30
31# Узел = число красных светодиодов (граница между цветами). Обоим игрокам нужно одинаковое число
32# шагов.
33START = NUM_LEDS // 2                   # Начальная граница
34GOAL = min(START, NUM_LEDS - START)     # Шагов для победы
35
36# ============ ЖЕЛЕЗО ============
37strip = NeoPixel(Pin(2), NUM_LEDS)
38btn1 = Pin(14, Pin.IN, Pin.PULL_UP)   # Игрок 1
39btn2 = Pin(16, Pin.IN, Pin.PULL_UP)   # Игрок 2
40buzzer = PWM(Pin(15))
41buzzer.duty_u16(0)
42
43
44# ============ ЗВУК ============
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 = [(частота, мс), ...], частота 0 = пауза
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# ============ КНОПКА ============
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        # Забыть старые нажатия (кнопку, удержанную между раундами, не штрафуют)
109        self.state = self.raw = self.pin.value()
110        self.changed = time.ticks_ms()
111
112
113b1 = Button(btn1)
114b2 = Button(btn2)
115
116
117# ============ ПОМОЩНИКИ ДЛЯ РИСОВАНИЯ ============
118def dim(color, k=1.0):
119    # Масштабировать цвет по общей яркости и k
120    s = BRIGHTNESS * k
121    return (int(color[0] * s), int(color[1] * s), int(color[2] * s))
122
123
124def fill(color, k=1.0):
125    c = dim(color, k)
126    for i in range(NUM_LEDS):
127        strip[i] = c
128    strip.write()
129
130
131def draw_rope(knot):
132    # `knot` светодиодов красные (слева), остальные синие (справа); по 2 светодиода с каждой стороны
133    # границы образуют яркий «узел»
134    for i in range(NUM_LEDS):
135        if i < knot:
136            k = 1.0 if knot - i <= 2 else 0.45   # Светодиоды у узла ярче
137            strip[i] = dim(RED, k)
138        else:
139            k = 1.0 if i - knot < 2 else 0.45
140            strip[i] = dim(BLUE, k)
141    strip.write()
142
143
144def wheel(pos):
145    # Перевести 0-255 в цвет радуги
146    pos %= 256
147    if pos < 85:
148        return (255 - pos * 3, pos * 3, 0)
149    if pos < 170:
150        pos -= 85
151        return (0, 255 - pos * 3, pos * 3)
152    pos -= 170
153    return (pos * 3, 0, 255 - pos * 3)
154
155
156def wait_ms(ms):
157    # Ждать, не останавливая звук
158    end = time.ticks_add(time.ticks_ms(), ms)
159    while time.ticks_diff(end, time.ticks_ms()) > 0:
160        sound.update()
161        time.sleep_ms(5)
162
163
164# ============ ФАЗЫ ИГРЫ ============
165def attract():
166    # Ожидание: оба цвета «дышат», любая кнопка запускает игру
167    print("Baslamak icin bir butona bas / Press a button to start")
168    b1.sync()
169    b2.sync()
170    t0 = time.ticks_ms()
171    while True:
172        sound.update()
173        t = time.ticks_diff(time.ticks_ms(), t0)
174        k = 0.25 + 0.75 * (math.sin(t / 400) + 1) / 2
175        for i in range(NUM_LEDS):
176            if i < START:
177                strip[i] = dim(RED, k)
178            else:
179                strip[i] = dim(BLUE, 1.25 - k)   # «Дышит» в противофазе
180        strip.write()
181        if b1.pressed() or b2.pressed():
182            return
183        time.sleep_ms(15)
184
185
186def countdown():
187    # Обратный отсчёт 3-2-1; ранние нажатия штрафуются. Возвращает начальный узел
188    knot = START
189    b1.sync()
190    b2.sync()
191    colors = [(255, 0, 0), (255, 150, 0), (0, 255, 0)]   # Красный, жёлтый, зелёный
192    for step in range(3):
193        fill(colors[step], 0.6)
194        if step == 2:
195            sound.play([(1200, 200)])   # Старт!
196            break
197        sound.play([(600, 150)])
198        end = time.ticks_add(time.ticks_ms(), 800)
199        while time.ticks_diff(end, time.ticks_ms()) > 0:
200            sound.update()
201            if b1.pressed():
202                knot -= FALSE_START_STEPS
203                print("Oyuncu 1 erken basti! / Player 1 false start!")
204                sound.play([(150, 200)])
205            if b2.pressed():
206                knot += FALSE_START_STEPS
207                print("Oyuncu 2 erken basti! / Player 2 false start!")
208                sound.play([(150, 200)])
209            time.sleep_ms(5)
210    # Штрафы не должны доводить узел до края
211    return max(START - GOAL + 2, min(START + GOAL - 2, knot))
212
213
214def play_round(knot):
215    # Дуэль на кнопках; возвращает победителя (1 или 2), когда узел доходит до края
216    draw_rope(knot)
217    while True:
218        sound.update()
219        moved = False
220        if b1.pressed():
221            knot += 1
222            sound.play([(440, 25)])
223            moved = True
224        if b2.pressed():
225            knot -= 1
226            sound.play([(660, 25)])
227            moved = True
228        if moved:
229            if knot >= START + GOAL:   # Лента полностью красная
230                return 1
231            if knot <= START - GOAL:   # Лента полностью синяя
232                return 2
233            draw_rope(knot)
234        time.sleep_ms(1)
235
236
237def celebrate_round(winner, scores):
238    # Цвет победителя мигает 3 раза, затем показываются точки счёта
239    color = RED if winner == 1 else BLUE
240    print("Turu kazanan / Round winner: Oyuncu / Player", winner, "| Skor / Score:", scores[0], "-", scores[1])
241    sound.play([(523, 120), (659, 120), (784, 120), (1047, 300)])
242    for _ in range(3):
243        fill(color)
244        wait_ms(180)
245        fill((0, 0, 0))
246        wait_ms(120)
247    # Счёт: красные точки слева, синие справа
248    for i in range(NUM_LEDS):
249        strip[i] = (0, 0, 0)
250    for s in range(scores[0]):
251        strip[s * 2] = dim(RED)
252    for s in range(scores[1]):
253        strip[NUM_LEDS - 1 - s * 2] = dim(BLUE)
254    strip.write()
255    wait_ms(1800)
256
257
258def celebrate_match(winner):
259    # Конец матча: радужная волна и победная мелодия
260    print("MACI KAZANAN / MATCH WINNER: Oyuncu / Player", winner)
261    sound.play([(784, 150), (784, 150), (784, 150), (1047, 450), (0, 80), (988, 150), (1047, 500)])
262    t0 = time.ticks_ms()
263    while time.ticks_diff(time.ticks_ms(), t0) < 3500:
264        sound.update()
265        offset = time.ticks_diff(time.ticks_ms(), t0) // 8
266        for i in range(NUM_LEDS):
267            strip[i] = dim(wheel(i * 256 // NUM_LEDS + offset))
268        strip.write()
269        time.sleep_ms(15)
270    fill(RED if winner == 1 else BLUE)
271    wait_ms(1000)
272
273
274# ============ ГЛАВНЫЙ ЦИКЛ ============
275print("== BlueGrays · bluegrays.com ==")
276print("Pico LED Halat Cekme / LED Tug of War")
277
278scores = [0, 0]
279while True:
280    attract()
281    while max(scores) < WIN_ROUNDS:
282        start_knot = countdown()
283        winner = play_round(start_knot)
284        scores[winner - 1] += 1
285        celebrate_round(winner, scores)
286    celebrate_match(1 if scores[0] > scores[1] else 2)
287    scores = [0, 0]
288

Частые вопросы

На моей ленте не 30 светодиодов. Можно ли использовать другую длину?

Да. Укажи в `NUM_LEDS` в начале кода число светодиодов на твоей ленте; стартовая точка (`START`) и число шагов до победы (`GOAL`) считаются автоматически. На ленте из 60 светодиодов каждому игроку нужно 30 нажатий, поэтому раунды длятся дольше.

Действительно ли нужны резистор 330 Ω и конденсатор 470 µF?

В Wokwi нет, но для реальной схемы они рекомендуются. Поставь резистор 330 Ω последовательно между GP2 и входом DIN ленты, а конденсатор 470 µF — между 5V и GND ленты (длинная ножка к 5V). Они защищают первый светодиод и предотвращают мерцание и случайные цвета.

Хватит ли USB у Pico для питания ленты?

С 30 светодиодами и `BRIGHTNESS = 0.6` лента потребляет не больше примерно 0,4 A, и USB с этим справляется. На VBUS есть 5V, только пока подключён USB. Для более длинной ленты или большей яркости используй отдельный адаптер 5V/2A и соедини земли.

Как изменить число раундов и штраф за фальстарт?

`WIN_ROUNDS` — число раундов, нужных для победы в матче (по умолчанию 3). `FALSE_START_STEPS` задаёт, на сколько шагов отбрасывается нажавший раньше времени (по умолчанию 2; поставь 0, чтобы отключить штраф). Для беззвучной игры установи `SOUND_ON = False`.