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