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