fix
This commit is contained in:
1 parent
3a473ec51e
commit
0ae74cd3fd
1 file changed
+838
-8
@@ -43,12 +43,25 @@
|
||||
# Windows voraus (Startmenue-Suche) und dass der Pico gerade am PC steckt,
|
||||
# dessen Fokus auf dem Desktop liegt (kein Textfeld o.ae. offen), sonst
|
||||
# landet der getippte Text an der falschen Stelle.
|
||||
#
|
||||
# Eigene Profile direkt am Geraet: letztes Profil "Neu" (per Encoder
|
||||
# erreichbar) -> Taste1 = neues Profil anlegen (Name + 4 Tastenaktionen
|
||||
# ueber Encoder/Taster zusammenklicken), Taste2 = eigenes Profil wieder
|
||||
# loeschen. Landet persistent in CUSTOM_PROFILES_FILE im Flash-Speicher des
|
||||
# Pico und uebersteht einen Neustart - kein PC/Thonny noetig. Siehe die
|
||||
# Kommentare beim "Profil-Editor" weiter unten im Code fuer die genaue
|
||||
# Bedienung.
|
||||
|
||||
import time
|
||||
import random
|
||||
from array import array
|
||||
from machine import Pin, I2C, PWM
|
||||
|
||||
try:
|
||||
import ujson as json # aeltere MicroPython-Firmware kennt nur "ujson"
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
import usb.device
|
||||
from usb.device.keyboard import KeyboardInterface, KeyCode
|
||||
from usb.device.hid import HIDInterface
|
||||
@@ -128,7 +141,33 @@ SCREENSAVER_ENABLED = True
|
||||
SCREENSAVER_TIMEOUT_MS = 30000 # 30s Inaktivitaet bis der Screensaver startet
|
||||
SCREENSAVER_FRAME_MS = 80 # Bildwiederholrate der Animation
|
||||
SCREENSAVER_ANIMATION = "stars" # Startwert - "stars"|"pacman"|"matrix"|"bounce"
|
||||
PACMAN_SPEED_PX = 1 # Pac-Man Bewegung pro Bild in Pixel - kleiner = langsamer
|
||||
PACMAN_SPEED_PX = 3 # Pac-Man Bewegung pro Bild in Pixel - kleiner = langsamer
|
||||
|
||||
# --- Mini-Spiele ("Games"-Profil, Taste 1-4 startet direkt ein Spiel) ---
|
||||
# Verlassen jederzeit moeglich: Encoder-Taster druecken.
|
||||
GAME_FRAME_MS = 60 # Bildrate fuer Dino/Flappy (ms zwischen Bildern)
|
||||
GAME_DINO_GRAVITY = 1 # Fallbeschleunigung pro Bild
|
||||
GAME_DINO_JUMP_VELOCITY = -7 # Sprungimpuls (negativ = nach oben)
|
||||
GAME_DINO_SPEED_START = 3 # Anfangsgeschwindigkeit der Hindernisse (px/Bild)
|
||||
GAME_FLAPPY_GRAVITY = 1 # Fallbeschleunigung pro Bild
|
||||
GAME_FLAPPY_FLAP_VELOCITY = -3 # Aufwaerts-Impuls bei Tastendruck
|
||||
GAME_FLAPPY_GAP_H = 13 # Luecke zwischen oberem/unterem Rohr (px)
|
||||
GAME_FLAPPY_SPEED = 2 # Rohr-Geschwindigkeit (px/Bild)
|
||||
GAME_TETRIS_COLS = 10 # Spielfeldbreite in Zellen
|
||||
GAME_TETRIS_ROWS = 16 # Spielfeldhoehe in Zellen
|
||||
GAME_TETRIS_CELL_PX = 2 # Zellengroesse in Pixel
|
||||
GAME_TETRIS_DROP_MS_START = 500 # Anfangs-Falldauer pro Reihe (ms)
|
||||
GAME_PACMAN_COLS = 32 # Labyrinth-Breite in Zellen
|
||||
GAME_PACMAN_ROWS = 8 # Labyrinth-Hoehe in Zellen
|
||||
GAME_PACMAN_CELL_PX = 4 # Zellengroesse in Pixel
|
||||
GAME_PACMAN_MOVE_MS = 160 # Bewegungstakt Spieler (ms pro Feld)
|
||||
GAME_PACMAN_GHOST_MOVE_EVERY = 2 # Geist bewegt sich nur bei jedem n-ten Takt
|
||||
|
||||
# --- Eigene Profile (direkt am Geraet anlegen, siehe "Neu"-Profil) ---
|
||||
# Landen persistent (ueberleben einen Neustart) in dieser Datei im Flash-
|
||||
# Speicher des Pico. Die festen Profile oben in PROFILES bleiben davon
|
||||
# unberuehrt - geloescht werden kann nur, was man selbst angelegt hat.
|
||||
CUSTOM_PROFILES_FILE = "/custom_profiles.json"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -305,6 +344,12 @@ def do_action(action):
|
||||
set_led_mode(action["mode"])
|
||||
elif kind == "screensaver":
|
||||
set_screensaver_animation(action["animation"])
|
||||
elif kind == "game":
|
||||
run_game(action["game"])
|
||||
elif kind == "new_profile":
|
||||
wizard_create_profile()
|
||||
elif kind == "delete_profile":
|
||||
wizard_delete_profile()
|
||||
# "none" -> nichts tun
|
||||
|
||||
|
||||
@@ -355,7 +400,7 @@ PROFILES = [
|
||||
{"type": "open", "query": "Opera GX"},
|
||||
{"type": "open", "query": "Visual Studio Code"},
|
||||
{"type": "open", "query": "Thunderbird"},
|
||||
{"type": "open", "query": "Bitwarden"},
|
||||
{"type": "open", "query": "Vaultwarden"},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -392,8 +437,72 @@ PROFILES = [
|
||||
{"type": "screensaver", "animation": "bounce"},
|
||||
],
|
||||
},
|
||||
{
|
||||
# Mini-Spiele: Taste druecken startet direkt das jeweilige Spiel.
|
||||
# Steuerung siehe Kommentar am Anfang des jeweiligen Spiels weiter
|
||||
# unten im Code. Encoder-Taster druecken beendet ein Spiel jederzeit.
|
||||
"name": "Games",
|
||||
"labels": ["Dino", "Flpy", "Tetrs", "Pacmn"],
|
||||
"actions": [
|
||||
{"type": "game", "game": "dino"},
|
||||
{"type": "game", "game": "flappy"},
|
||||
{"type": "game", "game": "tetris"},
|
||||
{"type": "game", "game": "pacman"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Eigene Profile: persistentes Laden von Flash + "Neu"-Menuepunkt anhaengen.
|
||||
# BUILTIN_PROFILE_COUNT merkt sich, wo die festen Profile aufhoeren, damit
|
||||
# der Loeschen-Dialog nur selbst angelegte Profile zur Auswahl anbietet.
|
||||
# ---------------------------------------------------------------------------
|
||||
BUILTIN_PROFILE_COUNT = len(PROFILES)
|
||||
|
||||
|
||||
def _load_custom_profiles():
|
||||
try:
|
||||
with open(CUSTOM_PROFILES_FILE) as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def _save_custom_profiles(profiles):
|
||||
try:
|
||||
with open(CUSTOM_PROFILES_FILE, "w") as f:
|
||||
json.dump(profiles, f)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _custom_profiles_list():
|
||||
"""Aktuell selbst angelegte Profile (ohne feste Profile, ohne den
|
||||
'Neu'-Menuepunkt) - das wird auf Flash gespeichert."""
|
||||
return PROFILES[BUILTIN_PROFILE_COUNT:-1]
|
||||
|
||||
|
||||
NEW_PROFILE_MENU_ENTRY = {
|
||||
# Bleibt immer der letzte Eintrag in PROFILES (siehe wizard_create_profile,
|
||||
# die neue Profile davor einfuegt).
|
||||
"name": "Neu",
|
||||
"labels": ["Erstl", "Losch", "-", "-"],
|
||||
"actions": [
|
||||
{"type": "new_profile"},
|
||||
{"type": "delete_profile"},
|
||||
NONE_ACTION,
|
||||
NONE_ACTION,
|
||||
],
|
||||
}
|
||||
|
||||
for _cp in _load_custom_profiles():
|
||||
PROFILES.append(_cp)
|
||||
PROFILES.append(NEW_PROFILE_MENU_ENTRY)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hardware: Taster
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -550,10 +659,17 @@ _encoder_last_change_ms = 0
|
||||
_encoder_step_accum = 0
|
||||
profile_dirty = True # erzwingt initiales Display-Zeichnen
|
||||
|
||||
# Waehrend der Profil-Editor ("Neu"-Menue) laeuft, soll das Drehen des
|
||||
# Encoders NICHT durch die Profile blaettern, sondern Listen im Editor
|
||||
# scrollen. _encoder_mode steuert das um, _wiz_encoder_delta sammelt die
|
||||
# Schritte fuer den Editor (siehe Mini-Spiele/Profil-Editor weiter unten).
|
||||
_encoder_mode = "profile" # "profile" | "wizard"
|
||||
_wiz_encoder_delta = 0
|
||||
|
||||
|
||||
def _encoder_irq(pin):
|
||||
global profile_index, _encoder_last_change_ms, profile_dirty, _encoder_step_accum
|
||||
global _last_activity_ms, _screensaver_active
|
||||
global _last_activity_ms, _screensaver_active, _wiz_encoder_delta
|
||||
now = time.ticks_ms()
|
||||
if time.ticks_diff(now, _encoder_last_change_ms) < ENCODER_DEBOUNCE_MS:
|
||||
return
|
||||
@@ -573,13 +689,19 @@ def _encoder_irq(pin):
|
||||
_encoder_step_accum += 1 if forward else -1
|
||||
|
||||
if _encoder_step_accum >= ENCODER_STEPS_PER_CLICK:
|
||||
profile_index = (profile_index + 1) % len(PROFILES)
|
||||
_encoder_step_accum = 0
|
||||
profile_dirty = True
|
||||
if _encoder_mode == "wizard":
|
||||
_wiz_encoder_delta += 1
|
||||
else:
|
||||
profile_index = (profile_index + 1) % len(PROFILES)
|
||||
profile_dirty = True
|
||||
elif _encoder_step_accum <= -ENCODER_STEPS_PER_CLICK:
|
||||
profile_index = (profile_index - 1) % len(PROFILES)
|
||||
_encoder_step_accum = 0
|
||||
profile_dirty = True
|
||||
if _encoder_mode == "wizard":
|
||||
_wiz_encoder_delta -= 1
|
||||
else:
|
||||
profile_index = (profile_index - 1) % len(PROFILES)
|
||||
profile_dirty = True
|
||||
|
||||
|
||||
encoder_clk.irq(trigger=Pin.IRQ_FALLING, handler=_encoder_irq)
|
||||
@@ -781,6 +903,714 @@ def _wake_screensaver():
|
||||
draw_profile()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mini-Spiele (ueber das "Games"-Profil startbar)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Alle vier Spiele teilen sich zwei Grundregeln:
|
||||
# - Encoder-Taster druecken beendet das laufende Spiel sofort.
|
||||
# - Nach "Game Over" (oder Sieg) einmal eine beliebige Taste druecken, um
|
||||
# zurueck ins "Games"-Profil zu kommen.
|
||||
# Waehrend ein Spiel laeuft, pausieren LED-Animation und Bildschirmschoner -
|
||||
# die Hauptschleife wartet ja auf die Rueckkehr aus run_game().
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _game_exit_requested():
|
||||
"""True, sobald der Encoder-Taster gedrueckt wird (jederzeit abbrechen)."""
|
||||
return encoder_sw.value() == 0
|
||||
|
||||
|
||||
def _game_any_button():
|
||||
"""True, wenn irgendeine der 4 Tasten gerade gedrueckt ist."""
|
||||
for btn in buttons:
|
||||
if btn.value() == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _game_over_screen(title, score):
|
||||
"""Zeigt Titel + Punktestand und wartet auf Tastendruck oder Encoder."""
|
||||
oled.fill(0)
|
||||
oled.text(title, 0, 4)
|
||||
oled.text("Score: " + str(score), 0, 18)
|
||||
oled.show()
|
||||
# Erst warten, bis alles losgelassen ist (sonst fuehrt der Tastendruck,
|
||||
# der zum Game Over gefuehrt hat, sofort wieder aus dem Bildschirm raus)
|
||||
while _game_any_button() or _game_exit_requested():
|
||||
time.sleep_ms(20)
|
||||
while not (_game_any_button() or _game_exit_requested()):
|
||||
time.sleep_ms(20)
|
||||
|
||||
|
||||
# --- Spiel 1: Dino-Runner - Taste 1 = Springen ------------------------------
|
||||
def _game_dino():
|
||||
ground_y = 30
|
||||
dino_x = 8
|
||||
dino_w = 6
|
||||
dino_h = 8
|
||||
dino_y = ground_y - dino_h
|
||||
vel_y = 0
|
||||
on_ground = True
|
||||
|
||||
obst_x = 127
|
||||
obst_w = 3
|
||||
obst_h = random.randint(6, 10)
|
||||
|
||||
speed = GAME_DINO_SPEED_START
|
||||
score = 0
|
||||
|
||||
while True:
|
||||
if _game_exit_requested():
|
||||
return
|
||||
|
||||
if buttons[0].value() == 0 and on_ground:
|
||||
vel_y = GAME_DINO_JUMP_VELOCITY
|
||||
on_ground = False
|
||||
|
||||
vel_y += GAME_DINO_GRAVITY
|
||||
dino_y += vel_y
|
||||
if dino_y >= ground_y - dino_h:
|
||||
dino_y = ground_y - dino_h
|
||||
vel_y = 0
|
||||
on_ground = True
|
||||
|
||||
obst_x -= speed
|
||||
if obst_x < -obst_w:
|
||||
obst_x = 127
|
||||
obst_h = random.randint(6, 10)
|
||||
score += 1
|
||||
if score % 5 == 0:
|
||||
speed += 1
|
||||
|
||||
if dino_x < obst_x + obst_w and dino_x + dino_w > obst_x:
|
||||
if dino_y + dino_h > ground_y - obst_h:
|
||||
_game_over_screen("GAME OVER", score)
|
||||
return
|
||||
|
||||
oled.fill(0)
|
||||
oled.hline(0, ground_y, 128, 1)
|
||||
oled.fill_rect(dino_x, dino_y, dino_w, dino_h, 1)
|
||||
oled.fill_rect(obst_x, ground_y - obst_h, obst_w, obst_h, 1)
|
||||
oled.text(str(score), 100, 0)
|
||||
oled.show()
|
||||
|
||||
time.sleep_ms(GAME_FRAME_MS)
|
||||
|
||||
|
||||
# --- Spiel 2: Floppy Fly - Taste 1 = Fluegelschlag --------------------------
|
||||
def _game_flappy():
|
||||
bird_x = 20
|
||||
bird_y = 16
|
||||
vel_y = 0
|
||||
|
||||
pipe_x = 127
|
||||
pipe_w = 4
|
||||
gap_h = GAME_FLAPPY_GAP_H
|
||||
gap_y = random.randint(2, 32 - gap_h - 2)
|
||||
|
||||
score = 0
|
||||
scored_this_pipe = False
|
||||
|
||||
while True:
|
||||
if _game_exit_requested():
|
||||
return
|
||||
|
||||
if buttons[0].value() == 0:
|
||||
vel_y = GAME_FLAPPY_FLAP_VELOCITY
|
||||
|
||||
vel_y += GAME_FLAPPY_GRAVITY
|
||||
bird_y += vel_y
|
||||
|
||||
pipe_x -= GAME_FLAPPY_SPEED
|
||||
if pipe_x < -pipe_w:
|
||||
pipe_x = 127
|
||||
gap_y = random.randint(2, 32 - gap_h - 2)
|
||||
scored_this_pipe = False
|
||||
|
||||
if not scored_this_pipe and pipe_x + pipe_w < bird_x:
|
||||
score += 1
|
||||
scored_this_pipe = True
|
||||
|
||||
crashed = bird_y < 0 or bird_y > 31
|
||||
if pipe_x < bird_x + 2 and pipe_x + pipe_w > bird_x - 2:
|
||||
if bird_y < gap_y or bird_y > gap_y + gap_h:
|
||||
crashed = True
|
||||
|
||||
if crashed:
|
||||
_game_over_screen("GAME OVER", score)
|
||||
return
|
||||
|
||||
oled.fill(0)
|
||||
oled.fill_rect(pipe_x, 0, pipe_w, gap_y, 1)
|
||||
oled.fill_rect(pipe_x, gap_y + gap_h, pipe_w, 32 - (gap_y + gap_h), 1)
|
||||
oled.fill_rect(bird_x - 2, int(bird_y) - 2, 4, 4, 1)
|
||||
oled.text(str(score), 100, 0)
|
||||
oled.show()
|
||||
|
||||
time.sleep_ms(GAME_FRAME_MS)
|
||||
|
||||
|
||||
# --- Spiel 3: Mini-Tetris ----------------------------------------------------
|
||||
# Taste1=Links, Taste2=Rechts, Taste3=Drehen, Taste4=Runter (Soft-Drop)
|
||||
_TETRIS_SHAPES = {
|
||||
"I": [[(0, 1), (1, 1), (2, 1), (3, 1)], [(2, 0), (2, 1), (2, 2), (2, 3)]],
|
||||
"O": [[(1, 0), (2, 0), (1, 1), (2, 1)]],
|
||||
"T": [
|
||||
[(1, 0), (0, 1), (1, 1), (2, 1)],
|
||||
[(1, 0), (1, 1), (2, 1), (1, 2)],
|
||||
[(0, 1), (1, 1), (2, 1), (1, 2)],
|
||||
[(1, 0), (0, 1), (1, 1), (1, 2)],
|
||||
],
|
||||
"S": [[(1, 0), (2, 0), (0, 1), (1, 1)], [(1, 0), (1, 1), (2, 1), (2, 2)]],
|
||||
"Z": [[(0, 0), (1, 0), (1, 1), (2, 1)], [(2, 0), (1, 1), (2, 1), (1, 2)]],
|
||||
"L": [
|
||||
[(0, 0), (0, 1), (1, 1), (2, 1)],
|
||||
[(1, 0), (2, 0), (1, 1), (1, 2)],
|
||||
[(0, 1), (1, 1), (2, 1), (2, 2)],
|
||||
[(1, 0), (1, 1), (0, 2), (1, 2)],
|
||||
],
|
||||
"J": [
|
||||
[(2, 0), (0, 1), (1, 1), (2, 1)],
|
||||
[(1, 0), (1, 1), (1, 2), (2, 2)],
|
||||
[(0, 1), (1, 1), (2, 1), (0, 2)],
|
||||
[(0, 0), (1, 0), (1, 1), (1, 2)],
|
||||
],
|
||||
}
|
||||
_TETRIS_NAMES = list(_TETRIS_SHAPES.keys())
|
||||
|
||||
|
||||
def _tetris_cells(shape, rot, px, py):
|
||||
rots = _TETRIS_SHAPES[shape]
|
||||
dxdy = rots[rot % len(rots)]
|
||||
return [(px + dx, py + dy) for dx, dy in dxdy]
|
||||
|
||||
|
||||
def _tetris_valid(cells, grid, cols, rows):
|
||||
for cx, cy in cells:
|
||||
if cx < 0 or cx >= cols or cy < 0 or cy >= rows:
|
||||
return False
|
||||
if grid[cy][cx]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _game_tetris():
|
||||
cols = GAME_TETRIS_COLS
|
||||
rows = GAME_TETRIS_ROWS
|
||||
cell = GAME_TETRIS_CELL_PX
|
||||
grid = [[0] * cols for _ in range(rows)]
|
||||
|
||||
def spawn():
|
||||
s = random.choice(_TETRIS_NAMES)
|
||||
return s, 0, cols // 2 - 2, 0
|
||||
|
||||
shape, rot, px, py = spawn()
|
||||
score = 0
|
||||
drop_ms = GAME_TETRIS_DROP_MS_START
|
||||
last_drop = time.ticks_ms()
|
||||
btn_last = [1, 1, 1, 1]
|
||||
|
||||
while True:
|
||||
if _game_exit_requested():
|
||||
return
|
||||
|
||||
now = time.ticks_ms()
|
||||
for i in range(4):
|
||||
state = buttons[i].value()
|
||||
if state == 0 and btn_last[i] == 1:
|
||||
if i == 0:
|
||||
cells = _tetris_cells(shape, rot, px - 1, py)
|
||||
if _tetris_valid(cells, grid, cols, rows):
|
||||
px -= 1
|
||||
elif i == 1:
|
||||
cells = _tetris_cells(shape, rot, px + 1, py)
|
||||
if _tetris_valid(cells, grid, cols, rows):
|
||||
px += 1
|
||||
elif i == 2:
|
||||
new_rot = rot + 1
|
||||
if _tetris_valid(_tetris_cells(shape, new_rot, px, py), grid, cols, rows):
|
||||
rot = new_rot
|
||||
elif _tetris_valid(_tetris_cells(shape, new_rot, px - 1, py), grid, cols, rows):
|
||||
rot = new_rot
|
||||
px -= 1
|
||||
elif _tetris_valid(_tetris_cells(shape, new_rot, px + 1, py), grid, cols, rows):
|
||||
rot = new_rot
|
||||
px += 1
|
||||
elif i == 3:
|
||||
cells = _tetris_cells(shape, rot, px, py + 1)
|
||||
if _tetris_valid(cells, grid, cols, rows):
|
||||
py += 1
|
||||
btn_last[i] = state
|
||||
|
||||
if time.ticks_diff(now, last_drop) >= drop_ms:
|
||||
last_drop = now
|
||||
cells = _tetris_cells(shape, rot, px, py + 1)
|
||||
if _tetris_valid(cells, grid, cols, rows):
|
||||
py += 1
|
||||
else:
|
||||
for cx, cy in _tetris_cells(shape, rot, px, py):
|
||||
if 0 <= cy < rows and 0 <= cx < cols:
|
||||
grid[cy][cx] = 1
|
||||
cleared = 0
|
||||
new_grid = []
|
||||
for r in range(rows):
|
||||
if all(grid[r]):
|
||||
cleared += 1
|
||||
else:
|
||||
new_grid.append(grid[r])
|
||||
for _ in range(cleared):
|
||||
new_grid.insert(0, [0] * cols)
|
||||
grid = new_grid
|
||||
if cleared:
|
||||
score += cleared * 10
|
||||
drop_ms = max(120, drop_ms - cleared * 15)
|
||||
shape, rot, px, py = spawn()
|
||||
if not _tetris_valid(_tetris_cells(shape, rot, px, py), grid, cols, rows):
|
||||
_game_over_screen("GAME OVER", score)
|
||||
return
|
||||
|
||||
oled.fill(0)
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if grid[r][c]:
|
||||
oled.fill_rect(c * cell, r * cell, cell, cell, 1)
|
||||
for cx, cy in _tetris_cells(shape, rot, px, py):
|
||||
if 0 <= cy < rows and 0 <= cx < cols:
|
||||
oled.fill_rect(cx * cell, cy * cell, cell, cell, 1)
|
||||
oled.text("SCR", cols * cell + 4, 2)
|
||||
oled.text(str(score), cols * cell + 4, 12)
|
||||
oled.show()
|
||||
time.sleep_ms(30)
|
||||
|
||||
|
||||
# --- Spiel 4: Mini-Pacman -----------------------------------------------------
|
||||
# Taste1=Hoch, Taste2=Runter, Taste3=Links, Taste4=Rechts. Ein Geist jagt dich,
|
||||
# links/rechts gibt es einen Tunnel-Wrap wie im Original.
|
||||
def _game_pacman_maze(cols, rows, tunnel_row):
|
||||
grid = [[0] * cols for _ in range(rows)]
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if r == 0 or r == rows - 1:
|
||||
grid[r][c] = 1
|
||||
elif c == 0 or c == cols - 1:
|
||||
grid[r][c] = 0 if r == tunnel_row else 1
|
||||
elif r in (2, rows - 3) and c % 4 in (1, 2):
|
||||
grid[r][c] = 1
|
||||
return grid
|
||||
|
||||
|
||||
def _game_pacman():
|
||||
cols = GAME_PACMAN_COLS
|
||||
rows = GAME_PACMAN_ROWS
|
||||
cell = GAME_PACMAN_CELL_PX
|
||||
tunnel_row = rows // 2
|
||||
|
||||
walls = _game_pacman_maze(cols, rows, tunnel_row)
|
||||
dots = set()
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if not walls[r][c]:
|
||||
dots.add((c, r))
|
||||
|
||||
px, py = 1, 1
|
||||
dots.discard((px, py))
|
||||
pdx, pdy = 1, 0
|
||||
|
||||
gx, gy = cols - 2, rows - 2
|
||||
gdx, gdy = -1, 0
|
||||
|
||||
score = 0
|
||||
tick = 0
|
||||
last_move = time.ticks_ms()
|
||||
btn_last = [1, 1, 1, 1]
|
||||
|
||||
def can_move(x, y, dx, dy):
|
||||
ny = y + dy
|
||||
if ny < 0 or ny >= rows:
|
||||
return False
|
||||
nx = (x + dx) % cols
|
||||
return not walls[ny][nx]
|
||||
|
||||
while True:
|
||||
if _game_exit_requested():
|
||||
return
|
||||
|
||||
for i in range(4):
|
||||
state = buttons[i].value()
|
||||
if state == 0 and btn_last[i] == 1:
|
||||
if i == 0:
|
||||
pdx, pdy = 0, -1
|
||||
elif i == 1:
|
||||
pdx, pdy = 0, 1
|
||||
elif i == 2:
|
||||
pdx, pdy = -1, 0
|
||||
elif i == 3:
|
||||
pdx, pdy = 1, 0
|
||||
btn_last[i] = state
|
||||
|
||||
now = time.ticks_ms()
|
||||
if time.ticks_diff(now, last_move) >= GAME_PACMAN_MOVE_MS:
|
||||
last_move = now
|
||||
tick += 1
|
||||
|
||||
if can_move(px, py, pdx, pdy):
|
||||
px = (px + pdx) % cols
|
||||
py += pdy
|
||||
if (px, py) in dots:
|
||||
dots.discard((px, py))
|
||||
score += 1
|
||||
|
||||
if tick % GAME_PACMAN_GHOST_MOVE_EVERY == 0:
|
||||
best = None
|
||||
best_dist = None
|
||||
for ddx, ddy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
|
||||
if can_move(gx, gy, ddx, ddy):
|
||||
nx = (gx + ddx) % cols
|
||||
ny = gy + ddy
|
||||
dist = abs(nx - px) + abs(ny - py)
|
||||
if best_dist is None or dist < best_dist:
|
||||
best_dist = dist
|
||||
best = (ddx, ddy)
|
||||
if best is None:
|
||||
best = (0, 0)
|
||||
gdx, gdy = best
|
||||
gx = (gx + gdx) % cols
|
||||
gy += gdy
|
||||
|
||||
if (px, py) == (gx, gy):
|
||||
_game_over_screen("GAME OVER", score)
|
||||
return
|
||||
|
||||
if not dots:
|
||||
_game_over_screen("CLEAR!", score)
|
||||
return
|
||||
|
||||
oled.fill(0)
|
||||
for (dx_, dy_) in dots:
|
||||
oled.pixel(dx_ * cell + cell // 2, dy_ * cell + cell // 2, 1)
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if walls[r][c]:
|
||||
oled.fill_rect(c * cell, r * cell, cell, cell, 1)
|
||||
oled.fill_rect(px * cell, py * cell, cell, cell, 1)
|
||||
oled.rect(gx * cell, gy * cell, cell, cell, 1)
|
||||
oled.show()
|
||||
time.sleep_ms(20)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Profil-Editor: eigene Profile direkt am Geraet anlegen und wieder loeschen
|
||||
# ---------------------------------------------------------------------------
|
||||
# Erreichbar ueber das feste "Neu"-Profil (letzter Eintrag, per Encoder
|
||||
# erreichbar): Taste 1 = neues Profil erstellen, Taste 2 = eigenes Profil
|
||||
# loeschen. Neue Profile landen in CUSTOM_PROFILES_FILE im Flash-Speicher
|
||||
# des Pico und ueberleben damit einen Neustart; die festen Profile weiter
|
||||
# oben im Code sind davon nicht betroffen.
|
||||
#
|
||||
# Bedienung beim Erstellen:
|
||||
# 1) Name eingeben: Encoder dreht durch die Zeichen, Taste1 uebernimmt das
|
||||
# angezeigte Zeichen, Taste2 loescht das letzte Zeichen, Taste3 = fertig
|
||||
# (Name abschliessen), Taste4 fuegt direkt ein Leerzeichen ein.
|
||||
# 2) Fuer jede der 4 Tasten des neuen Profils: erst Art waehlen (Tasten-
|
||||
# Kombi / Medien-Taste / Keine), bei Tasten-Kombi zuerst die Modifier
|
||||
# (Strg/Shift/Alt/Win) mit Taste 1-4 an/ausschalten und mit kurzem
|
||||
# Encoder-Druck bestaetigen, danach die Basistaste aus einer Liste
|
||||
# waehlen (Encoder dreht, Taste1 bestaetigt).
|
||||
# In jeder Liste bricht ein Druck auf den Encoder-Taster den kompletten
|
||||
# Vorgang ab (bei den Modifiern: erst ein LANGER Druck).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _wiz_read_delta():
|
||||
"""Liest die seit dem letzten Aufruf aufgelaufenen Encoder-Schritte im
|
||||
Wizard-Modus und setzt den Zaehler zurueck."""
|
||||
global _wiz_encoder_delta
|
||||
d = _wiz_encoder_delta
|
||||
_wiz_encoder_delta = 0
|
||||
return d
|
||||
|
||||
|
||||
def _wiz_wait_release():
|
||||
"""Wartet, bis alle 4 Taster UND der Encoder-Taster losgelassen sind."""
|
||||
while _game_any_button() or encoder_sw.value() == 0:
|
||||
time.sleep_ms(20)
|
||||
|
||||
|
||||
def _wiz_message(line1, line2="", wait_ms=900):
|
||||
oled.fill(0)
|
||||
oled.text(line1[:16], 0, 8)
|
||||
if line2:
|
||||
oled.text(line2[:16], 0, 18)
|
||||
oled.show()
|
||||
time.sleep_ms(wait_ms)
|
||||
|
||||
|
||||
def _wiz_select(title, options, start=0):
|
||||
"""Scrollbare Liste: Encoder dreht, Taste1 bestaetigt (gibt den Index
|
||||
zurueck), Encoder-Taster druecken bricht ab (gibt None zurueck)."""
|
||||
idx = start % len(options)
|
||||
_wiz_read_delta()
|
||||
last_draw = None
|
||||
while True:
|
||||
if idx != last_draw:
|
||||
oled.fill(0)
|
||||
oled.text(title[:16], 0, 0)
|
||||
oled.text("> " + options[idx][:14], 0, 12)
|
||||
nxt = options[(idx + 1) % len(options)]
|
||||
oled.text(" " + nxt[:14], 0, 22)
|
||||
oled.show()
|
||||
last_draw = idx
|
||||
if encoder_sw.value() == 0:
|
||||
_wiz_wait_release()
|
||||
return None
|
||||
if buttons[0].value() == 0:
|
||||
_wiz_wait_release()
|
||||
return idx
|
||||
d = _wiz_read_delta()
|
||||
if d:
|
||||
idx = (idx + d) % len(options)
|
||||
time.sleep_ms(20)
|
||||
|
||||
|
||||
_WIZ_NAME_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
|
||||
def _wiz_enter_name():
|
||||
"""Zeichen-fuer-Zeichen-Namenseingabe (max. 10 Zeichen). Gibt den Namen
|
||||
zurueck, oder None bei Abbruch (Encoder-Taster)."""
|
||||
chars = _WIZ_NAME_CHARS
|
||||
ci = 0
|
||||
name = ""
|
||||
_wiz_read_delta()
|
||||
last_draw = None
|
||||
while True:
|
||||
state = (ci, name)
|
||||
if state != last_draw:
|
||||
oled.fill(0)
|
||||
oled.text("Name:" + name, 0, 0)
|
||||
oled.text("<" + chars[ci] + ">", 0, 12)
|
||||
oled.text("1:OK 2:Del 3:Fin", 0, 22)
|
||||
oled.show()
|
||||
last_draw = state
|
||||
if encoder_sw.value() == 0:
|
||||
_wiz_wait_release()
|
||||
return None
|
||||
if buttons[0].value() == 0:
|
||||
if len(name) < 10:
|
||||
name += chars[ci]
|
||||
_wiz_wait_release()
|
||||
last_draw = None
|
||||
elif buttons[1].value() == 0:
|
||||
name = name[:-1]
|
||||
_wiz_wait_release()
|
||||
last_draw = None
|
||||
elif buttons[2].value() == 0:
|
||||
if name:
|
||||
_wiz_wait_release()
|
||||
return name
|
||||
_wiz_wait_release()
|
||||
elif buttons[3].value() == 0:
|
||||
if len(name) < 10:
|
||||
name += " "
|
||||
_wiz_wait_release()
|
||||
last_draw = None
|
||||
d = _wiz_read_delta()
|
||||
if d:
|
||||
ci = (ci + d) % len(chars)
|
||||
time.sleep_ms(20)
|
||||
|
||||
|
||||
def _wiz_pick_modifiers(title):
|
||||
"""Taste 1-4 schalten Strg/Shift/Alt/Win an/aus. Kurzer Encoder-Druck
|
||||
uebernimmt die Auswahl, ein LANGER Encoder-Druck bricht ab."""
|
||||
mods = [False, False, False, False]
|
||||
names = ["C", "S", "A", "W"]
|
||||
btn_last = [1, 1, 1, 1]
|
||||
sw_last = 1
|
||||
sw_down_start = None
|
||||
last_draw = None
|
||||
while True:
|
||||
state_key = tuple(mods)
|
||||
if state_key != last_draw:
|
||||
line = " ".join(names[i] + ("*" if mods[i] else "_") for i in range(4))
|
||||
oled.fill(0)
|
||||
oled.text(title[:16], 0, 0)
|
||||
oled.text(line[:16], 0, 12)
|
||||
oled.text("1-4=Mod Enc=weit", 0, 22)
|
||||
oled.show()
|
||||
last_draw = state_key
|
||||
for i in range(4):
|
||||
s = buttons[i].value()
|
||||
if s == 0 and btn_last[i] == 1:
|
||||
mods[i] = not mods[i]
|
||||
last_draw = None
|
||||
btn_last[i] = s
|
||||
sw = encoder_sw.value()
|
||||
if sw == 0 and sw_last == 1:
|
||||
sw_down_start = time.ticks_ms()
|
||||
elif sw == 1 and sw_last == 0 and sw_down_start is not None:
|
||||
held = time.ticks_diff(time.ticks_ms(), sw_down_start)
|
||||
sw_down_start = None
|
||||
if held >= ENCODER_SW_LONG_PRESS_MS:
|
||||
return None
|
||||
return mods
|
||||
sw_last = sw
|
||||
time.sleep_ms(20)
|
||||
|
||||
|
||||
_WIZ_BASE_KEYS = (
|
||||
[(c, c) for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
|
||||
+ [(str(d), "N" + str(d)) for d in range(10)]
|
||||
+ [("F" + str(n), "F" + str(n)) for n in range(1, 13)]
|
||||
+ [
|
||||
("ENTER", "ENTER"),
|
||||
("SPACE", "SPACE"),
|
||||
("TAB", "TAB"),
|
||||
("ESC", "ESCAPE"),
|
||||
("BKSP", "BACKSPACE"),
|
||||
("DEL", "DELETE"),
|
||||
("UP", "UP"),
|
||||
("DOWN", "DOWN"),
|
||||
("LEFT", "LEFT"),
|
||||
("RIGHT", "RIGHT"),
|
||||
("HOME", "HOME"),
|
||||
("END", "END"),
|
||||
]
|
||||
)
|
||||
|
||||
_WIZ_MEDIA_KEYS = [
|
||||
("Vol+", "VOLUME_UP"),
|
||||
("Vol-", "VOLUME_DOWN"),
|
||||
("Mute", "MUTE"),
|
||||
("Play", "PLAY_PAUSE"),
|
||||
("Stop", "STOP"),
|
||||
("Next", "NEXT_TRACK"),
|
||||
("Prev", "PREV_TRACK"),
|
||||
]
|
||||
|
||||
_WIZ_ACTION_CATEGORIES = ["Tasten-Kombi", "Medien-Taste", "Keine"]
|
||||
|
||||
_WIZ_MOD_ATTRS = ["LEFT_CTRL", "LEFT_SHIFT", "LEFT_ALT", "LEFT_UI"]
|
||||
_WIZ_MOD_SYMS = ["^", "+", "~", "#"]
|
||||
|
||||
|
||||
def _wiz_pick_keys_action(title):
|
||||
mods = _wiz_pick_modifiers(title)
|
||||
if mods is None:
|
||||
return None, None
|
||||
key_labels = [lbl for lbl, _ in _WIZ_BASE_KEYS]
|
||||
ki = _wiz_select(title, key_labels)
|
||||
if ki is None:
|
||||
return None, None
|
||||
key_label, key_attr = _WIZ_BASE_KEYS[ki]
|
||||
keys = []
|
||||
mod_label = ""
|
||||
for i in range(4):
|
||||
if mods[i]:
|
||||
keys.append(getattr(K, _WIZ_MOD_ATTRS[i]))
|
||||
mod_label += _WIZ_MOD_SYMS[i]
|
||||
keys.append(getattr(K, key_attr))
|
||||
label = (mod_label + key_label)[:5] or "-"
|
||||
return label, {"type": "keys", "keys": keys}
|
||||
|
||||
|
||||
def _wiz_pick_action(slot):
|
||||
"""Fragt die Aktion fuer eine der 4 Tasten des neuen Profils ab. Gibt
|
||||
(label, action) zurueck, oder (None, None) bei Abbruch."""
|
||||
title = "Taste %d/4" % (slot + 1)
|
||||
cat_idx = _wiz_select(title, _WIZ_ACTION_CATEGORIES)
|
||||
if cat_idx is None:
|
||||
return None, None
|
||||
cat = _WIZ_ACTION_CATEGORIES[cat_idx]
|
||||
if cat == "Keine":
|
||||
return "-", {"type": "none"}
|
||||
if cat == "Medien-Taste":
|
||||
names = [n for n, _ in _WIZ_MEDIA_KEYS]
|
||||
mi = _wiz_select(title, names)
|
||||
if mi is None:
|
||||
return None, None
|
||||
label, attr = _WIZ_MEDIA_KEYS[mi]
|
||||
return label[:5], {"type": "media", "code": getattr(M, attr)}
|
||||
return _wiz_pick_keys_action(title)
|
||||
|
||||
|
||||
def wizard_create_profile():
|
||||
"""Legt ueber die Geraete-Bedienung ein neues Profil an und speichert es
|
||||
persistent (siehe CUSTOM_PROFILES_FILE)."""
|
||||
global PROFILES, profile_index, profile_dirty, _encoder_mode
|
||||
_encoder_mode = "wizard"
|
||||
_wiz_wait_release() # die Taste, die "Erstl" ausgeloest hat, nicht mitzaehlen
|
||||
try:
|
||||
name = _wiz_enter_name()
|
||||
if name is None:
|
||||
return
|
||||
labels = []
|
||||
actions = []
|
||||
for slot in range(4):
|
||||
label, action = _wiz_pick_action(slot)
|
||||
if label is None:
|
||||
_wiz_message("Abgebrochen")
|
||||
return
|
||||
labels.append(label)
|
||||
actions.append(action)
|
||||
new_profile = {"name": name, "labels": labels, "actions": actions}
|
||||
insert_at = len(PROFILES) - 1 # vor dem "Neu"-Menuepunkt einfuegen
|
||||
PROFILES.insert(insert_at, new_profile)
|
||||
profile_index = insert_at
|
||||
profile_dirty = True
|
||||
if _save_custom_profiles(_custom_profiles_list()):
|
||||
_wiz_message("Gespeichert!", name)
|
||||
else:
|
||||
_wiz_message("Nur temporaer!", "Speichern fehlg.")
|
||||
finally:
|
||||
_encoder_mode = "profile"
|
||||
|
||||
|
||||
def wizard_delete_profile():
|
||||
"""Loescht ein selbst erstelltes Profil wieder (feste Profile stehen
|
||||
nicht zur Auswahl)."""
|
||||
global PROFILES, profile_index, profile_dirty, _encoder_mode
|
||||
_encoder_mode = "wizard"
|
||||
_wiz_wait_release()
|
||||
try:
|
||||
customs = _custom_profiles_list()
|
||||
if not customs:
|
||||
_wiz_message("Keine eigenen", "Profile vorhanden")
|
||||
return
|
||||
names = [p["name"] for p in customs]
|
||||
idx = _wiz_select("Loeschen?", names)
|
||||
if idx is None:
|
||||
return
|
||||
target = customs[idx]
|
||||
real_index = PROFILES.index(target)
|
||||
del PROFILES[real_index]
|
||||
if profile_index >= len(PROFILES):
|
||||
profile_index = len(PROFILES) - 1
|
||||
profile_dirty = True
|
||||
if _save_custom_profiles(_custom_profiles_list()):
|
||||
_wiz_message("Geloescht:", target["name"])
|
||||
else:
|
||||
_wiz_message("Geloescht,", "Speichern fehlg.")
|
||||
finally:
|
||||
_encoder_mode = "profile"
|
||||
def run_game(name):
|
||||
"""Startet eines der 4 Mini-Spiele (siehe "Games"-Profil)."""
|
||||
oled.fill(0)
|
||||
oled.text("Los geht's!", 10, 12)
|
||||
oled.show()
|
||||
time.sleep_ms(400)
|
||||
# Sicherstellen, dass die Taste, die das Spiel gestartet hat, nicht noch
|
||||
# gedrueckt ist - sonst wuerde z.B. Dino sofort beim Start hochspringen.
|
||||
while _game_any_button():
|
||||
time.sleep_ms(20)
|
||||
if name == "dino":
|
||||
_game_dino()
|
||||
elif name == "flappy":
|
||||
_game_flappy()
|
||||
elif name == "tetris":
|
||||
_game_tetris()
|
||||
elif name == "pacman":
|
||||
_game_pacman()
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hauptschleife
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -847,4 +1677,4 @@ while True:
|
||||
else:
|
||||
_do_encoder_sw_action(ENCODER_SW_SHORT_ACTION)
|
||||
|
||||
time.sleep_ms(5)
|
||||
time.sleep_ms(5)
|
||||
Reference in new issue
Block a user