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

Jumper-Kabel

BUY

LED

BUY

Taste

BUY

Summer

BUY

OLED Display Module

-

Fusion HAT+

-

Raspberry Pi

-


Verdrahtungsdiagramm

Orientieren Sie sich am folgenden Verdrahtungsdiagramm, um die Komponenten zu verbinden:

../_images/4.15_whack_a_mole_bb.png

Einrichtungsschritte

  1. Installieren Sie die benötigten Bibliotheken:

    sudo pip3 install adafruit-circuitpython-ssd1306 --break
    
  2. Starten Sie das Spiel aus dem ai-lab-kit-Verzeichnis:

    cd ~/ai-lab-kit/python/
    sudo python3 4.15_WhackAMole.py
    
  3. 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

  1. 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]]):
       ...
    
  2. 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]
    
  3. Buzzer-Rückmeldung

    • A5 kurzer hoher Ton = Erfolg

    • E4 lä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()
    
  4. 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)
    
       ...
    
  5. 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_DOWN verwendet 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

  1. Modus mit variabler Geschwindigkeit

    Verkürzen Sie die Fade-Dauer schrittweise, um das Spiel schwieriger zu machen.

  2. Multi-LED-Modus

    Lassen Sie zwei LEDs gleichzeitig aufleuchten – der Spieler muss eine von beiden treffen.

  3. Lebenssystem

    Erlauben Sie drei Fehler, bevor das Spiel endet.

  4. Highscore-Speicherung

    Speichern Sie den höchsten Punktestand in einer Datei und zeigen Sie ihn beim Start an.

  5. 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!