Add RLCD animations and screensavers
This commit is contained in:
@@ -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)
|
||||
```
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user