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