Add RLCD animations and screensavers
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user