320 lines
11 KiB
Python
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()
|