Files
mcp_screen/screensavers/asteroid_drift.py
T
2026-06-21 21:19:51 -04:00

320 lines
11 KiB
Python

# 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()