Flipper auf dem Picopad


Spiel in CircuitPython programmiert



Anleitung getestet mit CircuitPython 10.0.3






Bevor es losgehen kann brauchen Sie einige Dateien. Es sind dies die Bibliotheken für die CircuitPython-Version (10.x), die beiden Hintergrundbilder (320x240 Pixel) und die Textdatei für den Score. Laden Sie einfach die zip-Datei von hier herunter, entpacken sie und kopieren die Ordner 'images', 'lib' und 'txt' ins Hauptverzeichnis des Laufwerks 'CIRCUITPY'.

Los gehts:

Im Unterschied zum oben gezeigten 'Tetris' bietet es sich beim 'Flipper'-Spiel an, das Display im Hochformat zu bezutzen, also so:


Dadurch verändert sich die Bezeichnung (nicht die GPIO's) der Button. Aus 'rechts' wird 'up', aus 'up' wird 'left' u.s.w. Ich zeige dazu den Quellcode zu Definition der Module (1 -21), Initialisierung des Displays (29 - 41) und der Button (43 - 56):

  
  
1   # SPDX-FileCopyrightText : 2026 Detlef Gebhardt, written for CircuitPython 10.0.3
2   # SPDX-FileCopyrightText : Copyright (c) 2026 Detlef Gebhardt
3   # SPDX-Filename          : Flipper-Spiel
4   # SPDX-License-Identifier: https://dgebhardt.de
5   import time
6   import board
7   import busio
8   import displayio
9   import terminalio
10  import digitalio
11  import fourwire
12  import bitmaptools
13  from adafruit_display_text import label
14  from adafruit_display_shapes.circle import Circle
15  from adafruit_display_shapes.roundrect import RoundRect
16  from adafruit_st7789 import ST7789
17  import adafruit_imageload
18  import random
19  import math
20  import pwmio
21  import microcontroller
22
23  # Anzahl Kugeln
24  kugeln = 6
25
26  # speaker aktivieren
27  speak = pwmio.PWMOut(board.GP15, frequency=4000, variable_frequency=True)
28
29  # 2 Zoll 320x240 Waveshare Display
30  cs=board.GP21
31  dc=board.GP17
32  reset=board.GP20
33  bl=board.GP16
34  # Release any resources currently in use for the displays
35  displayio.release_displays()
36  spi = busio.SPI(board.GP18, board.GP19)
37  display_bus = fourwire.FourWire(spi, command=dc, chip_select=cs, reset=reset)
38  display = ST7789(display_bus, rotation=0, width=240, height=320, backlight_pin=bl, rowstart=0, colstart=0)
39  display.brightness = 1
40  main = displayio.Group()
41  display.root_group = main
42
43  key_A = digitalio.DigitalInOut(board.GP7)
44  key_A.switch_to_input(pull=digitalio.Pull.UP)
45  key_B = digitalio.DigitalInOut(board.GP6)
46  key_B.switch_to_input(pull=digitalio.Pull.UP)
47  keyUp = digitalio.DigitalInOut(board.GP2)
48  keyUp.switch_to_input(pull=digitalio.Pull.UP)
49  keyLeft = digitalio.DigitalInOut(board.GP4)
50  keyLeft.switch_to_input(pull=digitalio.Pull.UP)
51  keyRight = digitalio.DigitalInOut(board.GP5)
52  keyRight.switch_to_input(pull=digitalio.Pull.UP)
53  key_X = digitalio.DigitalInOut(board.GP9)
54  key_X.switch_to_input(pull=digitalio.Pull.UP)
55  key_Y = digitalio.DigitalInOut(board.GP8)
56  key_Y.switch_to_input(pull=digitalio.Pull.UP)
57
  

Hinweisen möchte ich auch auf Zeile 24. Hier wird die Variable für die Anzahl der zu spielenden Kugeln festgelegt. In Zeile 27 wird der Lautsprecher aktiviert (GPIO 15). Im Spiel gibt es später die Möglichkeit, diesen über die X- oder Y-Taste ein- bzw. auszuschalten.

Als nächstes werden der in einer Textdatei gespeicherte 'Highscore' gelesen, der Hintergrund, die beweglichen Flipper (als 'Sprites') und die Hindernisse (obstacles) dargestellt.

  
  
58  # read score from file
59  datei = open("txt/score.txt", "r")
60  score = datei.readline()
61  datei.close()
62
63  # Intro for the score
64  intro_0,bg_intro0 = adafruit_imageload.load("images/score.bmp")
65  bg_intro0.make_transparent(0)
66  bg_tile_grid_intr = displayio.TileGrid(intro_0, pixel_shader=bg_intro0, x=0,y=0)
67  main.append(bg_tile_grid_intr)
68  # the label for the score
69  label_1 = label.Label(font=terminalio.FONT, text=str(score), scale=2, color=0xffffff, line_spacing=1)
70  label_1.anchor_point = (0, 0)
71  label_1.anchored_position = (120-6*len(score), 60)
72  main.append(label_1)
73  while True:
74      if key_A.value == False or key_B.value == False or key_X.value == False or key_Y.value == False :
75          main.remove(label_1)
76          main.remove(bg_tile_grid_intr)
77          break
78
79  # Load the sprite sheets for the table and the flippers(bitmap)
80  bitmap, palette = adafruit_imageload.load("/images/background.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette)
81  backgr_group = displayio.TileGrid(bitmap, pixel_shader=palette)
82
83  sprite_sheet, palette = adafruit_imageload.load("/images/flipper.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette)
84  palette.make_transparent(1)
85  flipperl = displayio.TileGrid(sprite_sheet, pixel_shader=palette, width=1, height=1, tile_width=32, tile_height=32, default_tile=0)
86  flipperl_group = displayio.Group(scale=1)
87  flipperl_group.append(flipperl)
88  flipperr = displayio.TileGrid(sprite_sheet, pixel_shader=palette, width=1, height=1, tile_width=32, tile_height=32, default_tile=0)
89  flipperr_group = displayio.Group(scale=1)
90  flipperr_group.append(flipperr)
91  lock = displayio.TileGrid(sprite_sheet, pixel_shader=palette, width=1, height=1, tile_width=32, tile_height=32, default_tile=0)
92  lock_group = displayio.Group(scale=1)
93  lock_group.append(lock)
94
95  ball = [200, 260, 0xffff00]
96  x_ball = ball[0]
97  y_ball = ball[1]
98
99  circle_1 = Circle(120, 60, 10, fill=0x555555)
100 circle_obstacle1 = Circle(120, 60, 6, fill=0xff0000)
101 circle_2 = Circle(65, 100, 10, fill=0x555555)
102 circle_obstacle2 = Circle(65, 100, 6, fill=0x00ff00)
103 circle_3 = Circle(175, 100, 10, fill=0x555555)
104 circle_obstacle3 = Circle(175, 100, 6, fill=0x00ff00)
105 circle_4 = Circle(120, 158, 10, fill=0x555555)
106 circle_obstacle4 = Circle(120, 158, 6, fill=0xff0000)
107 circle_5 = Circle(80, 180, 10, fill=0x555555)
108 circle_obstacle5 = Circle(80, 180, 6, fill=0xffff00)
109 circle_6 = Circle(160, 180, 10, fill=0x555555)
110 circle_obstacle6 = Circle(160, 180, 6, fill=0xffff00)
111 bonus_hole = Circle(120, 210, 10, fill=0xaf3232)
112 circle_ball = Circle(ball[0], ball[1], 6, fill=ball[2])
113
  

Die Zeilen 63 bis 77 bilden ein 'Intro' zur Anzeige des Highscore und die 'while'-Schleife wird erst abgebrochen, wenn eine der Tasten A, B, X oder Y gedrückt wurde. Um den belegten Speicher zu 'entlasten', wird in Zeile 75 und 76 das angezeigte Label und das Hintergrundbild aus dem Speicher gelöscht und erst danach die 'weiteren Teile' geladen. Danach werden noch Label für entsprechende Anzeigen im Spiel definiert und alle 'Komponenten' für das Spiel dem Speicher zugewiesen.

  
  
114 # Draw the label for the score
115 updating_score = label.Label(font=terminalio.FONT, text = "",scale=2, color=0xffffff, line_spacing=1)
116 updating_score.anchor_point = (0, 0)
117 updating_score.anchored_position = (80, 25)
118
119 text_balls = label.Label(terminalio.FONT, text=str(kugeln), color=0xffffff)
120 balls_group = displayio.Group(scale=2, x=170, y=250)
121 balls_group.append(text_balls)
122
123 label = label.Label(terminalio.FONT, text="start with 'up'", color=0xffff00)
124 text_label = displayio.Group(scale=2, x=40, y=295)
125 text_label.append(label)
126
127 main.append(backgr_group)
128 main.append(updating_score)
129 main.append(circle_1)
130 main.append(circle_2)
131 main.append(circle_3)
132 main.append(circle_4)
133 main.append(circle_5)
134 main.append(circle_6)
135 main.append(circle_obstacle1)
136 main.append(circle_obstacle2)
137 main.append(circle_obstacle3)
138 main.append(circle_obstacle4)
139 main.append(circle_obstacle5)
140 main.append(circle_obstacle6)
141 main.append(bonus_hole)
142 main.append(lock_group)
143 main.append(flipperl_group)
144 main.append(flipperr_group)
145 main.append(circle_ball)
146 main.append(balls_group)
147 main.append(text_label)
148
  

Es folgen Funktionen für den Abschuss einer Kugel, Initialisierung der Sprites, das Abspielen eines Tons, das Registrieren einer Kollision und Spielende, sowie die Festlegung von Variablen. Darauf wird hier jetzt nicht im Detail eingegangen.

  
  
149 def ball_launch(launch, ball):    # Start
150     x_ball = ball[0]
151     y_ball = ball[1]
152     lock.x = 185
153     lock.y = 190
154     lock[0] = 7
155     for i in range (0, 9):
156         y_ball -= 20
157         circle_ball.y = y_ball
158         time.sleep(0.02)
159     lock.x = 190
160     lock.y = 205
161     lock[0] = 3
162     launchspeed = random.randint(3, 18)
163     for i in range (0, launchspeed):
164         x_ball -= 10
165         y_ball = int(104 - math.sqrt(9604 - (x_ball-118)*(x_ball-118)))
166         circle_ball.x = x_ball
167         circle_ball.y = y_ball
168         time.sleep(0.02)
169     launch = False
170     ball[0] = x_ball
171     ball[1] = y_ball
172     return launch
173
174 def init():
175     lock.x = 190
176     lock.y = 205
177     lock[0] = 3

....

271 debounce = False
272 launch = True
273 rolldown = False
274 hitback = False
275 direction = [0]
276 points = 0
277 hit = time.monotonic()
278 roll = time.monotonic()
279 flip = time.monotonic()
280 ballspeed = 0
281 init()
282 circle_ball.x = x_ball
283 circle_ball.y = y_ball
284 last_tone = time.monotonic() - 4
285 sound = True
286
  

Den 'Rest' erledigt die 'while'-Schleife ab Zeile 287. Klicken Sie auf 'Code kopieren', um den vollständigen Quellcode in die Thonny-IDE zu übertragen. Die Auslassungen hier sind nur dem großen Umfang und der Übersicht geschuldet. Einen Hinweis möchte ich noch einmal geben. Wenn der Ton eingeschaltet ist, werden vor Spielstart, bei jeder Kollision und bei Spielende Töne abgespielt. Das kann auf Dauer etwas nervig sein. Deshalb kann mit der X-Taste der Ton abgestellt werden. Um dennoch eine gewisse 'Anspannung' zu schaffen, wird bei abgeschaltetem Ton die Bildschirmhelligkeit in sehr schneller Folge verändert. Es handelt sich also nicht um einen 'Wackelkontakt' am Display (siehe z.B. Zeilen 288 bis 296).

  
  
287 while True:
288     # Effekt vor Spielbeginn
289     if kugeln == 6 and (time.monotonic() - last_tone) >= 4 and not sound:
290         last_tone = time.monotonic()
291         for i in range (5):
292             display.brightness = 1
293             time.sleep(0.05)
294             display.brightness = 0.05
295             time.sleep(0.05)
296         display.brightness = 1

.....


318     # rechten Flipper bewegen
319     if keyRight.value == False and not debounce:
320         debounce = True
321         hit = time.monotonic()
322         flipperr[0] = 6
323         if y_ball >= 222 and y_ball <= 233 and x_ball >= 125 and x_ball <= 145:
324             rolldown = False
325             hitback = True
326             ballspeed = -2
327             i = random.randint(-1,1)
328             if i == 1:
329                 direction[0] = 1
330             else:
331                 direction[0] = -1
332             hit = time.monotonic()

.....

452     # Flipper zurücksetzen
453     if time.monotonic() - hit > 0.1:
454         debounce = False
455         flipperl[0] = 1
456         flipperr[0] = 4
  

Viel Spass und Erfolg beim Ausprobieren.