Bemerkung
Hallo, willkommen in der SunFounder Raspberry Pi & Arduino & ESP32 Enthusiasten-Community auf Facebook! Tauchen Sie mit anderen Enthusiasten tiefer in Raspberry Pi, Arduino und ESP32 ein.
Warum beitreten?
Expertenunterstützung: Lösen Sie Probleme nach dem Kauf und technische Herausforderungen mit Hilfe unserer Community und unseres Teams.
Lernen & Teilen: Tauschen Sie Tipps und Tutorials aus, um Ihre Fähigkeiten zu verbessern.
Exklusive Vorschauen: Erhalten Sie frühzeitigen Zugang zu neuen Produktankündigungen und Sneak Peeks.
Sonderrabatte: Genießen Sie exklusive Rabatte auf unsere neuesten Produkte.
Festliche Aktionen und Gewinnspiele: Nehmen Sie an Gewinnspielen und Feiertagsaktionen teil.
👉 Bereit, mit uns zu entdecken und zu gestalten? Klicken Sie auf [here] und treten Sie noch heute bei!
4.15 Whack-a-Mole-Spiel
Einführung
In dieser Lektion erstellen Sie ein Whack-a-Mole-Spiel mit dem Fusion HAT+. Vier PWM-gesteuerte LEDs fungieren als „Maulwürfe“, die in einem 1-Sekunden-Zyklus sanft ein- und ausblenden. Ihr Ziel ist es, den richtigen Button zu drücken, wenn die entsprechende LED aufleuchtet. Ein Summer gibt akustisches Feedback:
Richtiges Loch → kurzer hoher Piepton
Falsches Loch → GAME OVER (tiefer Ton)
Ein SSD1306-OLED-Display zeigt während des Spiels Ihren aktuellen Punktestand und bei Spielende den Endpunktestand an.
Dieses Projekt kombiniert PWM-Helligkeitssteuerung, Flankenerkennung von Tasten, Timing-Schleifen und eine einfache Spielzustandsmaschine.
Was Sie benötigen
Die folgenden Komponenten werden für dieses Projekt benötigt:
KOMPONENTENBESCHREIBUNG |
KAUFLINK |
|---|---|
- |
|
- |
|
Raspberry Pi |
- |
Verdrahtungsdiagramm
Orientieren Sie sich am folgenden Verdrahtungsdiagramm, um die Komponenten zu verbinden:
Einrichtungsschritte
Installieren Sie die benötigten Bibliotheken:
sudo pip3 install adafruit-circuitpython-ssd1306 --break
Starten Sie das Spiel aus dem
ai-lab-kit-Verzeichnis:cd ~/ai-lab-kit/python/ sudo python3 4.15_WhackAMole.py
Wenn das Skript ausgeführt wird:
Eine LED blendet ein und aus (Dreieckswelle).
Drücken Sie nur die Taste der leuchtenden LED innerhalb des 1-Sekunden-Fensters:
Richtig → Punktestand erhöht sich, der nächste „Maulwurf“ erscheint
Falsch → Summer gibt Fehlerton aus → GAME OVER
Das OLED zeigt während des Spiels nur den aktuellen Punktestand an
Bei Spielende zeigt das OLED „GAME OVER“ und den Endpunktestand
Drücken Sie Ctrl+C, um das Programm zu beenden
Code
Hier ist das vollständige Python-Skript für das Whack-a-Mole-Spiel:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Whack-a-Mole using fusion_hat:
- 4 PWM LEDs (smooth fade in/out; whole cycle = 1.0s)
- 4 buttons via fusion_hat.Pin (PULL_DOWN; pressed=1)
- Buzzer via fusion_hat.Buzzer(PWM(...)) for short feedback tone
- SSD1306 OLED shows only the current hit count during game
- If a non-lit button is pressed -> GAME OVER with final score
"""
import time
import random
from typing import List, Optional
# ---- fusion_hat GPIO / PWM / Buzzer ----
from fusion_hat.pwm import PWM
from fusion_hat.pin import Pin, Mode, Pull
from fusion_hat.modules import Buzzer
# ---- OLED ----
from PIL import Image, ImageDraw, ImageFont
import adafruit_ssd1306
import board
# ===================== Port map (EDIT HERE) =====================
# 4 PWM LED ports of Fusion HAT+
LED_PORTS = ['P0', 'P1', 'P2', 'P3']
# 4 button pins (BCM numbering). Example here uses 17/4/27/22.
# Buttons are configured as PULL_DOWN, so pressed -> value==1
BTN_PINS = [17, 4, 27, 22]
# Buzzer on a PWM-capable Fusion HAT+ port
BUZZER_PORT = 'P4'
# ===============================================================
# ===================== OLED setup =====================
WIDTH, HEIGHT = 128, 64
i2c = board.I2C()
oled = adafruit_ssd1306.SSD1306_I2C(WIDTH, HEIGHT, i2c, addr=0x3C)
oled.fill(0)
oled.show()
image = Image.new("1", (WIDTH, HEIGHT))
draw = ImageDraw.Draw(image)
font = ImageFont.load_default()
def draw_score(score: int):
"""Render in-game screen: only the current score."""
draw.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0)
msg = f"Score: {score}"
tw, th = font.getbbox(msg)[2:]
draw.text(((WIDTH - tw) // 2, (HEIGHT - th) // 2), msg, font=font, fill=255)
oled.image(image)
oled.show()
def draw_game_over(score: int):
"""Render GAME OVER screen with final score."""
draw.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0)
title = "GAME OVER"
tw, th = font.getbbox(title)[2:]
draw.text(((WIDTH - tw) // 2, 8), title, font=font, fill=255)
line = f"Score: {score}"
lw, lh = font.getbbox(line)[2:]
draw.text(((WIDTH - lw) // 2, 8 + th + 8), line, font=font, fill=255)
oled.image(image)
oled.show()
# ===================== Hardware objects =====================
# LEDs (PWM)
leds = [PWM(p) for p in LED_PORTS]
def led_set(idx: int, duty_pct: float):
"""Set LED brightness (0..100%)."""
duty = max(0.0, min(100.0, float(duty_pct)))
leds[idx].pulse_width_percent(duty)
def all_leds_off():
for i in range(len(leds)):
leds[i].pulse_width_percent(0)
# Buttons (Pin, PULL_DOWN => pressed=1)
buttons = [Pin(pin, mode=Mode.IN, pull=Pull.DOWN) for pin in BTN_PINS]
def read_buttons() -> List[int]:
"""Read all buttons; return their current logic levels (0/1)."""
return [b.value() if callable(getattr(b, "value", None)) else int(b.value) for b in buttons]
# Buzzer (tonal)
tb = Buzzer(PWM(BUZZER_PORT))
def beep_hit():
"""Short bright tone for a correct hit."""
tb.play('A5', 0.08) # A5 ~ 880 Hz short chirp
tb.off()
def beep_miss():
"""Lower/longer tone for a miss (game over)."""
tb.play('E4', 0.25) # E4 ~ 330 Hz
tb.off()
# ===================== LED fade (1s total) =====================
def triangle01(u: float) -> float:
"""
Triangle wave in [0,1], rising 0->1 over first half, then 1->0.
u in [0,1].
"""
return 2*u if u < 0.5 else 2*(1.0 - u)
def fade_led_with_button_check(active_idx: int,
prev_states: List[int],
duration: float = 1.0,
step: float = 0.01,
gamma: float = 1.8) -> (bool, Optional[List[int]]):
"""
Fade one LED (active_idx) with triangle brightness over 'duration' seconds.
While fading, poll buttons at 'step' interval:
- If matching button pressed (rising edge) -> immediately turn off LED, return (True, new_states).
- If non-matching button pressed (rising edge) -> game over: return (False, new_states) and let caller handle.
If no press -> return (None, new_states) to continue game.
Returns:
(hit, states)
hit=True -> correct hit
hit=False -> wrong button (game over)
hit=None -> no button pressed in this cycle
"""
t0 = time.monotonic()
states = prev_states[:]
while True:
t = time.monotonic() - t0
if t > duration:
break
# Triangle brightness 0..1 then 1..0
u = triangle01(t / duration)
# Gamma correction for perceived linearity
level = u ** gamma
duty = 3 + 97 * level # keep a tiny floor so LED is visible at the start
led_set(active_idx, duty)
# Poll buttons and detect rising edges
curr = read_buttons()
for i, (p, c) in enumerate(zip(states, curr)):
if p == 0 and c == 1: # rising edge = just pressed
if i == active_idx:
# Correct hit
all_leds_off()
beep_hit()
return True, curr
else:
# Wrong hole -> game over
all_leds_off()
beep_miss()
return False, curr
states = curr
time.sleep(step)
# Finished fade with no press
all_leds_off()
return None, states
# ===================== Game loop =====================
def pick_next(prev_idx: Optional[int]) -> int:
"""Pick a random index different from prev_idx."""
choices = list(range(4))
if prev_idx in choices:
choices.remove(prev_idx)
return random.choice(choices)
def main():
random.seed(time.time())
score = 0
last_idx = None
states = read_buttons() # baseline for edge detection
draw_score(score)
while True:
# Choose which LED lights next
idx = pick_next(last_idx)
last_idx = idx
# Run a 1.0s fade cycle, checking button presses
hit, states = fade_led_with_button_check(idx, states, duration=1.0, step=0.01, gamma=1.8)
if hit is False:
# Wrong button pressed -> Game Over
draw_game_over(score)
break
elif hit is True:
# Correct hit -> score++
score += 1
draw_score(score)
else:
# No press during this mole -> no score change; continue
pass
# stop all outputs
all_leds_off()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
all_leds_off()
oled.fill(0)
oled.show()
tb.off()
print("\nExited.")
Code verstehen
LED-Fade-Animation
Jede LED führt eine 1-Sekunden-Dreieckswellen-Fade-Animation aus:
Die Helligkeit steigt von 0 → 100 %
Danach fällt sie von 100 % → 0
Eine Gamma-Korrektur (
u ** gamma) verbessert die visuelle Linearität
def fade_led_with_button_check(active_idx: int, prev_states: List[int], duration: float = 1.0, step: float = 0.01, gamma: float = 1.8) -> (bool, Optional[List[int]]): ...
Tasten-Flankenerkennung
read_buttons()liest alle vier Tasten aus. Eine steigende Flanke (previous=0, current=1) bedeutet „Taste wurde gerade gedrückt“.Wenn die gedrückte Taste zur leuchtenden LED passt → richtiger Treffer
Andernfalls → falscher Treffer → GAME OVER
def read_buttons() -> List[int]: """Liest alle Tasten und gibt deren aktuelle Logikpegel (0/1) zurück.""" return [b.value() if callable(getattr(b, "value", None)) else int(b.value) for b in buttons]
Buzzer-Rückmeldung
A5kurzer hoher Ton = ErfolgE4längerer Ton = Fehlversuch / Spielende
# Buzzer (Tonsteuerung) tb = Buzzer(PWM(BUZZER_PORT)) def beep_hit(): """Kurzer heller Ton für einen korrekten Treffer.""" tb.play('A5', 0.08) # A5 ~ 880 Hz kurzer Piepton tb.off() def beep_miss(): """Tieferer/längerer Ton für einen Fehlversuch (Game Over).""" tb.play('E4', 0.25) # E4 ~ 330 Hz tb.off()
Spielschleife
Wählt zufällig eine von vier LEDs aus (niemals zweimal hintereinander dieselbe)
Führt einen Fade-Zyklus aus und prüft dabei Tastendrücke
Aktualisiert den Punktestand oder beendet das Spiel
while True: # Bestimmen, welche LED als Nächstes aufleuchtet idx = pick_next(last_idx) last_idx = idx # 1,0-s-Fade-Zyklus ausführen und Tastendrücke prüfen hit, states = fade_led_with_button_check(idx, states, duration=1.0, step=0.01, gamma=1.8) ...
OLED-Anzeige
Zeigt während des Spiels nur den aktuellen Punktestand
Zeigt bei Spielende „GAME OVER“ und den Endpunktestand
def draw_score(score: int): """Zeigt während des Spiels nur den aktuellen Punktestand an.""" draw.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0) msg = f"Score: {score}" tw, th = font.getbbox(msg)[2:] draw.text(((WIDTH - tw) // 2, (HEIGHT - th) // 2), msg, font=font, fill=255) oled.image(image) oled.show() ...
Fehlerbehebung
Tastendrücke werden nicht erkannt
Stellen Sie sicher, dass die Tasten korrekt mit BCM-Pinnummerierung verbunden sind
Bestätigen Sie, dass
PULL_DOWNverwendet wird, sodass der gedrückte Zustand = 1 istÜberprüfen Sie die korrekte Masseverbindung
LEDs flackern statt sanft zu dimmen
Verringern Sie die
step-Zeit für flüssigere Updates (z. B. 0.005)Stellen Sie sicher, dass die PWM-Ports ordnungsgemäße Duty-Cycle-Aktualisierungen unterstützen
Das Spiel endet sofort ohne Tastendruck
Ein schwebender Tasten-Pin kann als 1 gelesen werden
Stellen Sie sicher, dass Pull-Down-Widerstände korrekt konfiguriert sind
Der Punktestand erhöht sich nicht
Überprüfen Sie, ob der LED-Index mit dem Tasten-Index übereinstimmt
Stellen Sie sicher, dass LED-Ports und Tasten-Pins in der richtigen Reihenfolge angeordnet sind
Probieren Sie es selbst aus
Modus mit variabler Geschwindigkeit
Verkürzen Sie die Fade-Dauer schrittweise, um das Spiel schwieriger zu machen.
Multi-LED-Modus
Lassen Sie zwei LEDs gleichzeitig aufleuchten – der Spieler muss eine von beiden treffen.
Lebenssystem
Erlauben Sie drei Fehler, bevor das Spiel endet.
Highscore-Speicherung
Speichern Sie den höchsten Punktestand in einer Datei und zeigen Sie ihn beim Start an.
Animierter Startbildschirm
Fügen Sie einen Startbildschirm hinzu, bevor das Spiel beginnt.
Diese Ideen können Whack-a-Mole in ein vollständiges Arcade-Spiel verwandeln!