Add RLCD animations and screensavers

This commit is contained in:
aeroreyna
2026-06-21 21:19:51 -04:00
parent edf3da1a30
commit 8dbc5f4c7b
24 changed files with 3097 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
# Retro Video Game Screensavers for Reyna little32 ESP32-S3
A collection of 5 standalone, retro-game-inspired MicroPython screensavers designed for high-contrast, low-resolution reflective LCD screens (specifically the Waveshare ESP32-S3-RLCD-4.2).
## Screensavers List
1. **Maze Chomper (`maze_chomper.py`)**
* **Inspired By:** *maze-chase arcade classic*
* **Description:** A wedge-shaped mouth chomps its way through a symmetric maze grid of dots. A ghost follows and chases the chomper. If the ghost catches the chomper, a collision is registered, and the board resets.
2. **Block Puzzle (`block_puzzle.py`)**
* **Inspired By:** *falling-block puzzle classic*
* **Description:** Tetromino pieces fall from the top of the board. A simple self-playing heuristic automatically shifts and rotates each piece to target a column. When a horizontal row is fully occupied, it is cleared. The game resets once blocks reach the ceiling.
3. **Alien Invaders (`alien_invaders.py`)**
* **Inspired By:** *alien-wave shooter classic*
* **Description:** Crawling leg-animated invaders march side-to-side and descend down the screen. An automated defender ship moves underneath to aim and destroy invaders. Both invaders and ship shoot bullets/bombs, causing pixel-spark explosions.
4. **Asteroid Drift (`asteroid_drift.py`)**
* **Inspired By:** *vector-rock space classic*
* **Description:** A wireframe, vector-style ship sits in the center of the screen, automatically aiming its nose towards the closest drifting space rock. The ship fires lasers to blow up and split larger rocks into medium and small fragments.
5. **Paddle Bounce (`paddle_bounce.py`)**
* **Inspired By:** *paddle-ball arcade classic*
* **Description:** Two vertical paddles play tennis against each other. The ball bounces off the top/bottom boundaries and the paddles, increasing speed with each paddle contact. Simulated reaction times and position offsets introduce realistic mistakes, allowing scores to accrue.
## How It Works
* **Standalone Execution:** Each script is designed to run completely standalone and can be executed via REPL or boot routines.
* **Hardware Fallback:** If `board_config` or `display_instance` is not available (such as running under test environments or without initialized hardware), the script gracefully prints a warning message and exits without throwing a crash exception.
* **Touch Stop Detection:** Every script contains a robust `check_touch` function that intercepts touch interrupts/coordinates across various formats (`board_config.touch_instance`, `board_config.touch`, `display.get_touch()`, or `display.touch()`). If a touch is registered, the loop terminates immediately.
* **Frame Control:** Every script exposes a `run(text="", frames=None)` function. If `frames` is set to `1` (useful for syntax checks or single-frame rendering), the screensaver renders exactly one frame and exits. If `frames` is `None` or `<=0`, it runs indefinitely.
## Example Execution Commands
Upload the desired screensaver to your board using `mpremote`:
```bash
mpremote connect /dev/cu.usbmodem101 cp screensavers/maze_chomper.py :
```
Run the screensaver directly from the MicroPython REPL:
```python
# Execute the entire file to run automatically
exec(open("maze_chomper.py").read())
```
Or import the module and invoke with custom parameters:
```python
# Import and run with frame limits (e.g. exit after 300 frames)
import maze_chomper
maze_chomper.run(text="BREAKROOM SCREEN", frames=300)
```
+322
View File
@@ -0,0 +1,322 @@
# Standalone MicroPython Screensaver: Alien Invaders
# Inspired by alien-wave shooter classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
def draw_invader(d, ix, iy, frame_state):
# Alternating animation frames for crawling legs
d.fill_rect(ix - 7, iy - 4, 14, 3, 1)
d.fill_rect(ix - 5, iy - 1, 10, 3, 1)
if frame_state:
# Legs out
d.fill_rect(ix - 7, iy + 2, 2, 3, 1)
d.fill_rect(ix + 5, iy + 2, 2, 3, 1)
else:
# Legs in
d.fill_rect(ix - 4, iy + 2, 2, 3, 1)
d.fill_rect(ix + 2, iy + 2, 2, 3, 1)
# Eyes
d.fill_rect(ix - 3, iy - 3, 2, 2, 0)
d.fill_rect(ix + 1, iy - 3, 2, 2, 0)
def draw_ship(d, sx, sy):
d.fill_rect(sx - 9, sy - 4, 18, 5, 1)
d.fill_rect(sx - 3, sy - 8, 6, 4, 1)
d.fill_rect(sx - 1, sy - 12, 2, 4, 1)
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
# Game parameters
invader_w, invader_h = 14, 9
col_spacing = 28
row_spacing = 18
grid_cols = 6
grid_rows = 3
# Initialize game objects
invaders = [] # entries: [col, row, alive_bool]
def reset_invaders():
invaders.clear()
for r in range(grid_rows):
for c in range(grid_cols):
invaders.append([c, r, True])
reset_invaders()
inv_grid_x = (width - (grid_cols * col_spacing)) // 2
inv_grid_y = 40
inv_speed = 1.5
inv_dir = 1
ship_x = width // 2
ship_y = height - 15
ship_speed = 2.0
bullets = [] # player bullets: [x, y]
bombs = [] # alien bombs: [x, y]
explosions = [] # particle list: [x, y, vx, vy, life]
score = 0
frame = 0
cooldown = 0
death_timer = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
# If player was hit, run a death screen-shake / reset sequence
if death_timer > 0:
death_timer -= 1
display.clear(0)
# Simple screen shake by offsetting lines slightly
shake = random.randint(-4, 4)
display.text("SYSTEM ERROR", width // 2 - 48 + shake, height // 2 - 4 + shake, 1)
display.show()
if death_timer == 0:
reset_invaders()
bullets.clear()
bombs.clear()
explosions.clear()
inv_grid_x = (width - (grid_cols * col_spacing)) // 2
inv_grid_y = 40
ship_x = width // 2
frame += 1
sleep_ms(30)
continue
# AI: Move player ship toward closest invader horizontally
alive_invaders = [inv for inv in invaders if inv[2]]
if not alive_invaders:
score += 500
reset_invaders()
inv_grid_y = 40
bullets.clear()
bombs.clear()
continue
# Find the lowest/closest invader to follow
target_inv = None
max_r = -1
for inv in alive_invaders:
if inv[1] > max_r:
max_r = inv[1]
target_inv = inv
if target_inv:
inv_col, inv_row = target_inv[0], target_inv[1]
t_x = inv_grid_x + inv_col * col_spacing + col_spacing // 2
if ship_x < t_x:
ship_x = min(width - 15, ship_x + ship_speed)
elif ship_x > t_x:
ship_x = max(15, ship_x - ship_speed)
# Auto shooting
if cooldown > 0:
cooldown -= 1
elif random.random() < 0.15:
bullets.append([ship_x, ship_y - 12])
cooldown = 12
# Invader movements
# Calculate grid bounds based on active invaders
left_col = min(inv[0] for inv in alive_invaders)
right_col = max(inv[0] for inv in alive_invaders)
grid_left = inv_grid_x + left_col * col_spacing
grid_right = inv_grid_x + right_col * col_spacing + col_spacing
# Check boundary collision
if inv_dir == 1 and grid_right >= width - 15:
inv_dir = -1
inv_grid_y += row_spacing
elif inv_dir == -1 and grid_left <= 15:
inv_dir = 1
inv_grid_y += row_spacing
else:
inv_grid_x += inv_dir * inv_speed
# Invader shooting (bombs)
if random.random() < 0.03:
shooter = random.choice(alive_invaders)
bx = inv_grid_x + shooter[0] * col_spacing + col_spacing // 2
by = inv_grid_y + shooter[1] * row_spacing + row_spacing
bombs.append([bx, by])
# Update bullets (going up)
next_bullets = []
for b in bullets:
b[1] -= 4
hit = False
# Collision check with invaders
for inv in invaders:
if inv[2]:
ix = inv_grid_x + inv[0] * col_spacing + col_spacing // 2
iy = inv_grid_y + inv[1] * row_spacing + row_spacing // 2
if abs(b[0] - ix) < inv_col * 0 + 8 and abs(b[1] - iy) < 6:
inv[2] = False
hit = True
score += 30
# Spawn explosion particles
for _ in range(8):
explosions.append([ix, iy, random.uniform(-2, 2), random.uniform(-2, 2), 10])
break
if not hit and b[1] > 28:
next_bullets.append(b)
bullets = next_bullets
# Update bombs (going down)
next_bombs = []
for b in bombs:
b[1] += 3
# Collision check with player ship
if abs(b[0] - ship_x) < 10 and abs(b[1] - ship_y) < 6:
death_timer = 50 # Start system error state
# Large explosion
for _ in range(20):
explosions.append([ship_x, ship_y, random.uniform(-4, 4), random.uniform(-4, 4), 20])
break
elif b[1] < height:
next_bombs.append(b)
bombs = next_bombs
# Update explosions
next_explosions = []
for p in explosions:
p[0] += p[2]
p[1] += p[3]
p[4] -= 1
if p[4] > 0:
next_explosions.append(p)
explosions = next_explosions
# Check game over by invaders reaching bottom
max_y = max(inv_grid_y + inv[1] * row_spacing for inv in alive_invaders)
if max_y >= ship_y - 8:
death_timer = 50
for _ in range(20):
explosions.append([ship_x, ship_y, random.uniform(-4, 4), random.uniform(-4, 4), 20])
continue
# Render game objects
display.clear(0)
# Draw header
display.text(text if text else "INVADER DRIFT", 10, 10, 1)
display.text(f"SCORE: {score:05d}", width - 120, 10, 1)
display.line(0, 26, width, 26, 1)
# Draw invaders
inv_anim_state = (frame // 10) % 2 == 0
for inv in invaders:
if inv[2]:
ix = inv_grid_x + inv[0] * col_spacing + col_spacing // 2
iy = inv_grid_y + inv[1] * row_spacing + row_spacing // 2
draw_invader(display, ix, iy, inv_anim_state)
# Draw player ship
draw_ship(display, ship_x, ship_y)
# Draw player bullets
for b in bullets:
display.fill_rect(b[0] - 1, b[1] - 3, 2, 6, 1)
# Draw alien bombs
for b in bombs:
display.line(b[0], b[1] - 3, b[0], b[1] + 3, 1)
display.line(b[0] - 1, b[1], b[0] + 1, b[1], 1)
# Draw explosion particles
for p in explosions:
display.fill_rect(int(p[0]), int(p[1]), 2, 2, 1)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()
+319
View File
@@ -0,0 +1,319 @@
# Standalone MicroPython Screensaver: Asteroid Drift
# Inspired by vector-rock space classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
def make_rock(x, y, radius):
num_verts = 8
vertices = []
for i in range(num_verts):
angle = i * (2 * math.pi / num_verts)
vert_r = radius + random.uniform(-radius / 3.0, radius / 3.0)
dx = vert_r * math.cos(angle)
dy = vert_r * math.sin(angle)
vertices.append((dx, dy))
# Velocity
speed = random.uniform(0.5, 1.5)
v_angle = random.uniform(0, 2 * math.pi)
vx = speed * math.cos(v_angle)
vy = speed * math.sin(v_angle)
return {
'x': x,
'y': y,
'vx': vx,
'vy': vy,
'radius': radius,
'vertices': vertices
}
def draw_rock(d, rock):
rx, ry = rock['x'], rock['y']
verts = rock['vertices']
n = len(verts)
for i in range(n):
x1, y1 = rx + verts[i][0], ry + verts[i][1]
x2, y2 = rx + verts[(i+1)%n][0], ry + verts[(i+1)%n][1]
d.line(int(x1), int(y1), int(x2), int(y2), 1)
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
px, py = width // 2, height // 2
theta = 0.0
bullets = [] # elements: {'x', 'y', 'vx', 'vy', 'life'}
rocks = [] # elements: rock dicts
explosions = [] # elements: {'x', 'y', 'vx', 'vy', 'life'}
score = 0
cooldown = 0
frame = 0
# Spawn initial rocks
def spawn_initial_rocks():
rocks.clear()
for _ in range(4):
# Spawn rocks away from center
rx = random.choice([random.uniform(20, px - 50), random.uniform(px + 50, width - 20)])
ry = random.choice([random.uniform(20, py - 50), random.uniform(py + 50, height - 20)])
rocks.append(make_rock(rx, ry, 24))
spawn_initial_rocks()
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
# If no rocks are left, spawn more
if not rocks:
score += 1000
spawn_initial_rocks()
bullets.clear()
continue
# AI: Target the nearest rock
nearest_rock = None
min_dist = 999999
for r in rocks:
dx = r['x'] - px
dy = r['y'] - py
dist = dx*dx + dy*dy
if dist < min_dist:
min_dist = dist
nearest_rock = r
if nearest_rock:
tx = nearest_rock['x']
ty = nearest_rock['y']
target_theta = math.atan2(ty - py, tx - px)
# Rotate toward target
angle_diff = target_theta - theta
# Normalize angle to -pi..pi
while angle_diff > math.pi:
angle_diff -= 2 * math.pi
while angle_diff < -math.pi:
angle_diff += 2 * math.pi
if abs(angle_diff) < 0.08:
theta = target_theta
else:
theta += 0.08 if angle_diff > 0 else -0.08
# Shoot if aligned and off cooldown
if abs(angle_diff) < 0.25:
if cooldown == 0:
bx = px + 12 * math.cos(theta)
by = py + 12 * math.sin(theta)
bvx = 5.0 * math.cos(theta)
bvy = 5.0 * math.sin(theta)
bullets.append({'x': bx, 'y': by, 'vx': bvx, 'vy': bvy, 'life': 45})
cooldown = 15
if cooldown > 0:
cooldown -= 1
# Update rocks
for r in rocks:
r['x'] += r['vx']
r['y'] += r['vy']
# Wrap boundaries
rad = r['radius']
if r['x'] < -rad:
r['x'] = width + rad
elif r['x'] > width + rad:
r['x'] = -rad
if r['y'] < -rad:
r['y'] = height + rad
elif r['y'] > height + rad:
r['y'] = -rad
# Update bullets
next_bullets = []
for b in bullets:
b['x'] += b['vx']
b['y'] += b['vy']
b['life'] -= 1
# Wrap
if b['x'] < 0:
b['x'] = width
elif b['x'] > width:
b['x'] = 0
if b['y'] < 0:
b['y'] = height
elif b['y'] > height:
b['y'] = 0
# Collision with rocks
hit = False
for r in rocks:
rdx = b['x'] - r['x']
rdy = b['y'] - r['y']
if rdx*rdx + rdy*rdy < r['radius']*r['radius']:
hit = True
rocks.remove(r)
score += int(100 / r['radius'] * 10)
# Split rock
if r['radius'] > 8:
new_r = r['radius'] // 2
rocks.append(make_rock(r['x'], r['y'], new_r))
rocks.append(make_rock(r['x'], r['y'], new_r))
# Spawn particle sparks
for _ in range(6):
explosions.append({
'x': r['x'],
'y': r['y'],
'vx': random.uniform(-2, 2),
'vy': random.uniform(-2, 2),
'life': 12
})
break
if not hit and b['life'] > 0:
next_bullets.append(b)
bullets = next_bullets
# Update explosions
next_explosions = []
for p in explosions:
p['x'] += p['vx']
p['y'] += p['vy']
p['life'] -= 1
if p['life'] > 0:
next_explosions.append(p)
explosions = next_explosions
# Render frame
display.clear(0)
# Draw header
display.text(text if text else "ASTEROID DRIFT", 10, 10, 1)
display.text(f"SCORE: {score:05d}", width - 120, 10, 1)
display.line(0, 26, width, 26, 1)
# Draw ship (triangle)
fx = px + 12 * math.cos(theta)
fy = py + 12 * math.sin(theta)
blx = px + 8 * math.cos(theta + 2.5)
bly = py + 8 * math.sin(theta + 2.5)
brx = px + 8 * math.cos(theta - 2.5)
bry = py + 8 * math.sin(theta - 2.5)
display.line(int(fx), int(fy), int(blx), int(bly), 1)
display.line(int(blx), int(bly), int(brx), int(bry), 1)
display.line(int(brx), int(bry), int(fx), int(fy), 1)
# Draw small engine flame when firing
if cooldown > 10:
flame_tip_x = px - 12 * math.cos(theta) + random.uniform(-2, 2)
flame_tip_y = py - 12 * math.sin(theta) + random.uniform(-2, 2)
display.line(int(blx), int(bly), int(flame_tip_x), int(flame_tip_y), 1)
display.line(int(brx), int(bry), int(flame_tip_x), int(flame_tip_y), 1)
# Draw rocks
for r in rocks:
draw_rock(display, r)
# Draw bullets
for b in bullets:
display.fill_rect(int(b['x']) - 1, int(b['y']) - 1, 2, 2, 1)
# Draw particles
for p in explosions:
display.pixel(int(p['x']), int(p['y']), 1) if hasattr(display, 'pixel') else display.fill_rect(int(p['x']), int(p['y']), 1, 1, 1)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()
+297
View File
@@ -0,0 +1,297 @@
# Standalone MicroPython Screensaver: Block Puzzle
# Inspired by falling-block puzzle classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
# Shapes relative coordinates
SHAPES = [
[(0, 0), (1, 0), (2, 0), (3, 0)], # I
[(0, 0), (1, 0), (0, 1), (1, 1)], # O
[(1, 0), (0, 1), (1, 1), (2, 1)], # T
[(0, 0), (0, 1), (1, 1), (2, 1)], # L
[(2, 0), (0, 1), (1, 1), (2, 1)], # J
[(1, 0), (2, 0), (0, 1), (1, 1)], # S
[(0, 0), (1, 0), (1, 1), (2, 1)] # Z
]
def rotate_coords(coords, rotation):
# Rotate coords clockwise
res = coords
for _ in range(rotation % 4):
res = [(-y, x) for x, y in res]
# Normalize coordinates so min x and min y are at least 0 (easier bounding)
min_x = min(x for x, y in res)
min_y = min(y for x, y in res)
return [(x - min_x, y - min_y) for x, y in res]
def get_piece_dims(coords):
w = max(x for x, y in coords) - min(x for x, y in coords) + 1
h = max(y for x, y in coords) - min(y for x, y in coords) + 1
return w, h
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
# Calculate grid sizing
cell_size = (height - 40) // 20
board_w = 10 * cell_size
board_h = 20 * cell_size
board_x = (width - board_w) // 2
board_y = (height - board_h) // 2
grid = [[0 for _ in range(10)] for _ in range(20)]
score = 0
lines_cleared = 0
# Game states
current_shape_idx = random.randint(0, len(SHAPES) - 1)
next_shape_idx = random.randint(0, len(SHAPES) - 1)
# Piece rotation & coords
piece_rot = 0
piece_coords = rotate_coords(SHAPES[current_shape_idx], piece_rot)
# Initial piece placement
pw, ph = get_piece_dims(piece_coords)
px = (10 - pw) // 2
py = 0
# Self-playing AI target
target_x = random.randint(0, 10 - pw)
target_rot = random.randint(0, 3)
frame = 0
game_over_delay = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
# If in game over state, delay and restart
if game_over_delay > 0:
game_over_delay -= 1
if game_over_delay == 0:
grid = [[0 for _ in range(10)] for _ in range(20)]
score = 0
lines_cleared = 0
display.clear(0)
display.text("GAME OVER", width // 2 - 36, height // 2 - 4, 1)
display.show()
frame += 1
sleep_ms(30)
continue
# AI logic: adjust rotation and column position as block falls
# Try to rotate toward target rotation
if piece_rot != target_rot:
new_rot = (piece_rot + 1) % 4
new_coords = rotate_coords(SHAPES[current_shape_idx], new_rot)
npw, nph = get_piece_dims(new_coords)
# Keep position valid when rotating
npx = max(0, min(px, 10 - npw))
# Validate
valid = True
for dx, dy in new_coords:
tx, ty = npx + dx, py + dy
if ty >= 20 or tx < 0 or tx >= 10 or grid[ty][tx]:
valid = False
break
if valid:
piece_rot = new_rot
piece_coords = new_coords
px = npx
# Try to move toward target column
if px != target_x:
dx_step = 1 if target_x > px else -1
npx = px + dx_step
valid = True
for dx, dy in piece_coords:
tx, ty = npx + dx, py + dy
if ty >= 20 or tx < 0 or tx >= 10 or grid[ty][tx]:
valid = False
break
if valid:
px = npx
# Apply gravity every 4 frames (speeds up self-play nicely)
if frame % 4 == 0:
npy = py + 1
can_drop = True
for dx, dy in piece_coords:
tx, ty = px + dx, npy + dy
if ty >= 20 or grid[ty][tx]:
can_drop = False
break
if can_drop:
py = npy
else:
# Lock piece
for dx, dy in piece_coords:
grid[py + dy][px + dx] = 1
# Check lines
lines_this_turn = 0
for r in range(20):
if all(grid[r]):
del grid[r]
grid.insert(0, [0 for _ in range(10)])
lines_this_turn += 1
if lines_this_turn > 0:
lines_cleared += lines_this_turn
score += [0, 40, 100, 300, 1200][min(lines_this_turn, 4)]
# Spawn next piece
current_shape_idx = next_shape_idx
next_shape_idx = random.randint(0, len(SHAPES) - 1)
piece_rot = 0
piece_coords = rotate_coords(SHAPES[current_shape_idx], piece_rot)
pw, ph = get_piece_dims(piece_coords)
px = (10 - pw) // 2
py = 0
# Set next targets
target_x = random.randint(0, 10 - pw)
target_rot = random.randint(0, 3)
# Check game over (spawning block collides immediately)
for dx, dy in piece_coords:
if grid[py + dy][px + dx]:
game_over_delay = 60 # 2 seconds of game over display
break
# Draw game state
display.clear(0)
# Draw board border
display.rect(board_x - 2, board_y - 2, board_w + 4, board_h + 4, 1)
# Draw grid contents
for r in range(20):
for c in range(10):
cx_px = board_x + c * cell_size
cy_px = board_y + r * cell_size
if grid[r][c] == 1:
display.fill_rect(cx_px + 1, cy_px + 1, cell_size - 1, cell_size - 1, 1)
# Draw active piece
for dx, dy in piece_coords:
cx_px = board_x + (px + dx) * cell_size
cy_px = board_y + (py + dy) * cell_size
if 0 <= py + dy < 20:
display.fill_rect(cx_px + 1, cy_px + 1, cell_size - 1, cell_size - 1, 1)
# Add a small white dot in falling piece for visual difference
display.fill_rect(cx_px + cell_size//2, cy_px + cell_size//2, 2, 2, 0)
# Side panel (Left) - Title & Info
display.text(text if text else "FALLING", 10, 20, 1)
display.text("PUZZLE", 10, 32, 1)
display.text(f"SCORE:", 10, 80, 1)
display.text(f"{score:05d}", 10, 92, 1)
display.text(f"LINES:", 10, 130, 1)
display.text(f"{lines_cleared}", 10, 142, 1)
# Side panel (Right) - Next piece preview
preview_x = board_x + board_w + 20
display.text("NEXT:", preview_x, 20, 1)
display.rect(preview_x, 35, 50, 50, 1)
next_coords = rotate_coords(SHAPES[next_shape_idx], 0)
npw, nph = get_piece_dims(next_coords)
off_x = preview_x + (50 - npw * cell_size) // 2
off_y = 35 + (50 - nph * cell_size) // 2
for dx, dy in next_coords:
display.fill_rect(off_x + dx * cell_size + 1, off_y + dy * cell_size + 1, cell_size - 1, cell_size - 1, 1)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()
+29
View File
@@ -0,0 +1,29 @@
{
"screensavers": [
{
"filename": "maze_chomper.py",
"inspired_by_style": "maze-chase arcade classic",
"purpose": "A wedge-shaped chomper navigates a grid maze eating dots while being chased by a ghost. If they collide, they reset after showing a collision warning."
},
{
"filename": "block_puzzle.py",
"inspired_by_style": "falling-block puzzle classic",
"purpose": "Falling tetromino blocks are auto-arranged by a self-playing heuristic, aligning and locking into a grid. Full lines are cleared, and a game over triggers a reset when the grid fills to the top."
},
{
"filename": "alien_invaders.py",
"inspired_by_style": "alien-wave shooter classic",
"purpose": "A grid of crawling invaders crawls across and down the screen. An automated player tank tracks the closest invaders and shoots them down, dodging falling enemy bombs."
},
{
"filename": "asteroid_drift.py",
"inspired_by_style": "vector-rock space classic",
"purpose": "A central vector-style triangular ship rotates to automatically track and shoot laser bullets at drifting wireframe rocks. Shot rocks split into smaller rocks and wrap around borders."
},
{
"filename": "paddle_bounce.py",
"inspired_by_style": "paddle-ball arcade classic",
"purpose": "Two automated paddles play table tennis with a bouncing ball that speeds up with each hit. Paddles track the ball with minor errors to allow points to be scored."
}
]
}
+289
View File
@@ -0,0 +1,289 @@
# Standalone MicroPython Screensaver: Maze Chomper
# Inspired by maze-chase arcade classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
def random_choice(seq):
if not seq:
return None
return seq[random.randint(0, len(seq) - 1)]
def draw_chomper(d, cx, cy, rad, direction, mouth_open):
# Draw body
for dy in range(-rad, rad + 1):
dx = int(math.sqrt(rad*rad - dy*dy))
d.line(cx - dx, cy + dy, cx + dx, cy + dy, 1)
# Draw open mouth
if mouth_open:
dx_dir, dy_dir = direction
if dx_dir == 0 and dy_dir == 0:
dx_dir = 1
if dx_dir > 0:
for dy in range(-rad//2, rad//2 + 1):
mx = rad - abs(dy) * 2
d.line(cx, cy + dy, cx + mx, cy + dy, 0)
elif dx_dir < 0:
for dy in range(-rad//2, rad//2 + 1):
mx = rad - abs(dy) * 2
d.line(cx - mx, cy + dy, cx, cy + dy, 0)
elif dy_dir > 0:
for dx in range(-rad//2, rad//2 + 1):
my = rad - abs(dx) * 2
d.line(cx + dx, cy, cx + dx, cy + my, 0)
elif dy_dir < 0:
for dx in range(-rad//2, rad//2 + 1):
my = rad - abs(dx) * 2
d.line(cx + dx, cy - my, cx + dx, cy, 0)
def draw_ghost(d, gcx, gcy, rad):
for dy in range(-rad, 0):
dx = int(math.sqrt(rad*rad - dy*dy))
d.line(gcx - dx, gcy + dy, gcx + dx, gcy + dy, 1)
d.fill_rect(gcx - rad, gcy, rad * 2 + 1, rad, 1)
eye_offset = rad // 3
d.fill_rect(gcx - eye_offset - 1, gcy - eye_offset, 2, 2, 0)
d.fill_rect(gcx + eye_offset - 1, gcy - eye_offset, 2, 2, 0)
for i in range(gcx - rad, gcx + rad + 1, 4):
d.fill_rect(i, gcy + rad - 2, 2, 3, 0)
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
header_h = 30
grid_y_offset = header_h
cell_size = 40
cols = width // cell_size
rows = (height - header_h) // cell_size
grid = []
for r in range(rows):
row_data = []
for c in range(cols):
is_wall = False
if r == 0 or r == rows - 1 or c == 0 or c == cols - 1:
is_wall = True
elif (r == 2 or r == rows - 3) and (2 <= c <= 3 or cols - 4 <= c <= cols - 3):
is_wall = True
elif r == rows // 2 and (cols // 2 - 1 <= c <= cols // 2 + 1):
is_wall = True
row_data.append(2 if is_wall else 1)
grid.append(row_data)
score = 0
chomper_x, chomper_y = cell_size, grid_y_offset + cell_size
chomper_dx, chomper_dy = 1, 0
ghost_x, ghost_y = (cols - 2) * cell_size, grid_y_offset + (rows - 2) * cell_size
ghost_dx, ghost_dy = -1, 0
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
chomper_rel_y = chomper_y - grid_y_offset
if chomper_x % cell_size == 0 and chomper_rel_y % cell_size == 0:
cx, cy = chomper_x // cell_size, chomper_rel_y // cell_size
if grid[cy][cx] == 1:
grid[cy][cx] = 0
score += 10
has_dots = False
for r_data in grid:
if 1 in r_data:
has_dots = True
break
if not has_dots:
for r in range(rows):
for c in range(cols):
if grid[r][c] == 0:
grid[r][c] = 1
valid_dirs = []
dot_dirs = []
for ndx, ndy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cx + ndx, cy + ndy
if 0 <= nx < cols and 0 <= ny < rows:
if grid[ny][nx] != 2:
valid_dirs.append((ndx, ndy))
if grid[ny][nx] == 1:
dot_dirs.append((ndx, ndy))
if dot_dirs:
if (chomper_dx, chomper_dy) in dot_dirs:
pass
else:
chomper_dx, chomper_dy = random_choice(dot_dirs)
elif valid_dirs:
if (chomper_dx, chomper_dy) in valid_dirs and random.random() > 0.3:
pass
else:
chomper_dx, chomper_dy = random_choice(valid_dirs)
else:
chomper_dx, chomper_dy = 0, 0
ghost_rel_y = ghost_y - grid_y_offset
if ghost_x % cell_size == 0 and ghost_rel_y % cell_size == 0:
gcx, gcy = ghost_x // cell_size, ghost_rel_y // cell_size
valid_gdirs = []
for ndx, ndy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
if ndx == -ghost_dx and ndy == -ghost_dy:
continue
nx, ny = gcx + ndx, gcy + ndy
if 0 <= nx < cols and 0 <= ny < rows:
if grid[ny][nx] != 2:
valid_gdirs.append((ndx, ndy))
if not valid_gdirs:
valid_gdirs = [(-ghost_dx, -ghost_dy)]
best_dir = valid_gdirs[0]
min_dist = 999999
ccx, ccy = chomper_x // cell_size, chomper_rel_y // cell_size
for ndx, ndy in valid_gdirs:
nx, ny = gcx + ndx, gcy + ndy
dist = (nx - ccx)**2 + (ny - ccy)**2
if dist < min_dist:
min_dist = dist
best_dir = (ndx, ndy)
if random.random() < 0.25:
ghost_dx, ghost_dy = random_choice(valid_gdirs)
else:
ghost_dx, ghost_dy = best_dir
chomper_x += chomper_dx * 2
chomper_y += chomper_dy * 2
ghost_x += ghost_dx * 2
ghost_y += ghost_dy * 2
dist_sq = (chomper_x - ghost_x)**2 + (chomper_y - ghost_y)**2
if dist_sq < 256:
display.clear(0)
display.text("COLLISION!", width // 2 - 40, height // 2 - 10, 1)
display.show()
sleep_ms(800)
chomper_x, chomper_y = cell_size, grid_y_offset + cell_size
chomper_dx, chomper_dy = 1, 0
ghost_x, ghost_y = (cols - 2) * cell_size, grid_y_offset + (rows - 2) * cell_size
ghost_dx, ghost_dy = -1, 0
frame += 1
continue
display.clear(0)
# Header
display.text(text if text else "MAZE CHOMPER", 10, 10, 1)
display.text(f"SCORE: {score:05d}", width - 120, 10, 1)
display.line(0, 28, width, 28, 1)
# Grid / Walls / Dots
for r in range(rows):
for c in range(cols):
cell_val = grid[r][c]
cx_px = c * cell_size
cy_px = grid_y_offset + r * cell_size
if cell_val == 2:
display.fill_rect(cx_px + 2, cy_px + 2, cell_size - 4, cell_size - 4, 1)
elif cell_val == 1:
display.fill_rect(cx_px + cell_size//2 - 1, cy_px + cell_size//2 - 1, 3, 3, 1)
# Sprites
mouth_open = (frame % 4) < 2
draw_chomper(display, chomper_x + cell_size//2, chomper_y + cell_size//2, 12, (chomper_dx, chomper_dy), mouth_open)
draw_ghost(display, ghost_x + cell_size//2, ghost_y + cell_size//2, 12)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()
+223
View File
@@ -0,0 +1,223 @@
# Standalone MicroPython Screensaver: Paddle Bounce
# Inspired by paddle-ball arcade classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
# Paddle parameters
pad_w = 6
pad_h = 36
left_x = 15
right_x = width - 15 - pad_w
# Initial states
ly = (height - pad_h) // 2
ry = (height - pad_h) // 2
bx, by = width // 2, height // 2
vx, vy = 3.5, 1.8
score_l = 0
score_r = 0
paddle_speed = 3.0
# Simulated tracking error offset
target_offset_l = 0
target_offset_r = 0
def reset_ball(serve_to_left):
nonlocal bx, by, vx, vy, target_offset_l, target_offset_r
bx = width // 2
by = height // 2
vx = -3.5 if serve_to_left else 3.5
vy = random.choice([-2.0, -1.0, 1.0, 2.0])
target_offset_l = random.randint(-12, 12)
target_offset_r = random.randint(-12, 12)
frame = 0
serve_delay = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
if serve_delay > 0:
serve_delay -= 1
else:
# Move ball
bx += vx
by += vy
# Ceiling/floor bounce
if by <= 32:
by = 32
vy = -vy
elif by >= height - 8:
by = height - 8
vy = -vy
# AI tracking
if vx < 0: # Ball heading Left
# Track ball center with error offset
target_y = by + 3 - (pad_h // 2) + target_offset_l
if ly < target_y:
ly = min(height - pad_h - 6, ly + paddle_speed)
elif ly > target_y:
ly = max(32, ly - paddle_speed)
else: # Ball heading Right
target_y = by + 3 - (pad_h // 2) + target_offset_r
if ry < target_y:
ry = min(height - pad_h - 6, ry + paddle_speed)
elif ry > target_y:
ry = max(32, ry - paddle_speed)
# Check Left boundary
if bx <= left_x + pad_w:
if left_x <= bx:
# Ball within x-range of paddle, check y-collision
if ly - 3 <= by <= ly + pad_h + 3:
bx = left_x + pad_w
# Bounce & increase speed
vx = -vx * 1.05
# Cap horizontal speed to prevent teleporting
if abs(vx) > 8.0:
vx = 8.0 if vx > 0 else -8.0
vy = vy * 1.05 + random.uniform(-0.6, 0.6)
# Assign new tracking error for right paddle
target_offset_r = random.randint(-15, 15)
else:
# Miss! Point for Right
score_r += 1
reset_ball(serve_to_left=False)
serve_delay = 40
# Check Right boundary
if bx + 6 >= right_x:
if bx <= right_x + pad_w:
# Ball within x-range of paddle, check y-collision
if ry - 3 <= by <= ry + pad_h + 3:
bx = right_x - 6
# Bounce & increase speed
vx = -vx * 1.05
if abs(vx) > 8.0:
vx = 8.0 if vx > 0 else -8.0
vy = vy * 1.05 + random.uniform(-0.6, 0.6)
# Assign new tracking error for left paddle
target_offset_l = random.randint(-15, 15)
else:
# Miss! Point for Left
score_l += 1
reset_ball(serve_to_left=True)
serve_delay = 40
# Draw frame
display.clear(0)
# Header score line
display.text(text if text else "PADDLE BOUNCE", 10, 10, 1)
display.text(f"{score_l:02d} {score_r:02d}", width // 2 - 24, 10, 1)
display.line(0, 26, width, 26, 1)
# Draw center dotted line
for y in range(30, height, 16):
display.fill_rect(width // 2 - 1, y, 2, 8, 1)
# Draw paddles
display.fill_rect(left_x, int(ly), pad_w, pad_h, 1)
display.fill_rect(right_x, int(ry), pad_w, pad_h, 1)
# Draw ball (6x6 square)
if serve_delay == 0 or (serve_delay // 4) % 2 == 0:
display.fill_rect(int(bx), int(by), 6, 6, 1)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()