Files
2026-06-21 21:19:51 -04:00

323 lines
11 KiB
Python

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