Add RLCD animations and screensavers
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
# Standalone MicroPython Screensaver: Block Puzzle
|
||||
# Inspired by falling-block puzzle 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
|
||||
|
||||
# Shapes relative coordinates
|
||||
SHAPES = [
|
||||
[(0, 0), (1, 0), (2, 0), (3, 0)], # I
|
||||
[(0, 0), (1, 0), (0, 1), (1, 1)], # O
|
||||
[(1, 0), (0, 1), (1, 1), (2, 1)], # T
|
||||
[(0, 0), (0, 1), (1, 1), (2, 1)], # L
|
||||
[(2, 0), (0, 1), (1, 1), (2, 1)], # J
|
||||
[(1, 0), (2, 0), (0, 1), (1, 1)], # S
|
||||
[(0, 0), (1, 0), (1, 1), (2, 1)] # Z
|
||||
]
|
||||
|
||||
def rotate_coords(coords, rotation):
|
||||
# Rotate coords clockwise
|
||||
res = coords
|
||||
for _ in range(rotation % 4):
|
||||
res = [(-y, x) for x, y in res]
|
||||
# Normalize coordinates so min x and min y are at least 0 (easier bounding)
|
||||
min_x = min(x for x, y in res)
|
||||
min_y = min(y for x, y in res)
|
||||
return [(x - min_x, y - min_y) for x, y in res]
|
||||
|
||||
def get_piece_dims(coords):
|
||||
w = max(x for x, y in coords) - min(x for x, y in coords) + 1
|
||||
h = max(y for x, y in coords) - min(y for x, y in coords) + 1
|
||||
return w, h
|
||||
|
||||
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)
|
||||
|
||||
# Calculate grid sizing
|
||||
cell_size = (height - 40) // 20
|
||||
board_w = 10 * cell_size
|
||||
board_h = 20 * cell_size
|
||||
board_x = (width - board_w) // 2
|
||||
board_y = (height - board_h) // 2
|
||||
|
||||
grid = [[0 for _ in range(10)] for _ in range(20)]
|
||||
score = 0
|
||||
lines_cleared = 0
|
||||
|
||||
# Game states
|
||||
current_shape_idx = random.randint(0, len(SHAPES) - 1)
|
||||
next_shape_idx = random.randint(0, len(SHAPES) - 1)
|
||||
|
||||
# Piece rotation & coords
|
||||
piece_rot = 0
|
||||
piece_coords = rotate_coords(SHAPES[current_shape_idx], piece_rot)
|
||||
|
||||
# Initial piece placement
|
||||
pw, ph = get_piece_dims(piece_coords)
|
||||
px = (10 - pw) // 2
|
||||
py = 0
|
||||
|
||||
# Self-playing AI target
|
||||
target_x = random.randint(0, 10 - pw)
|
||||
target_rot = random.randint(0, 3)
|
||||
|
||||
frame = 0
|
||||
game_over_delay = 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 in game over state, delay and restart
|
||||
if game_over_delay > 0:
|
||||
game_over_delay -= 1
|
||||
if game_over_delay == 0:
|
||||
grid = [[0 for _ in range(10)] for _ in range(20)]
|
||||
score = 0
|
||||
lines_cleared = 0
|
||||
display.clear(0)
|
||||
display.text("GAME OVER", width // 2 - 36, height // 2 - 4, 1)
|
||||
display.show()
|
||||
frame += 1
|
||||
sleep_ms(30)
|
||||
continue
|
||||
|
||||
# AI logic: adjust rotation and column position as block falls
|
||||
# Try to rotate toward target rotation
|
||||
if piece_rot != target_rot:
|
||||
new_rot = (piece_rot + 1) % 4
|
||||
new_coords = rotate_coords(SHAPES[current_shape_idx], new_rot)
|
||||
npw, nph = get_piece_dims(new_coords)
|
||||
# Keep position valid when rotating
|
||||
npx = max(0, min(px, 10 - npw))
|
||||
# Validate
|
||||
valid = True
|
||||
for dx, dy in new_coords:
|
||||
tx, ty = npx + dx, py + dy
|
||||
if ty >= 20 or tx < 0 or tx >= 10 or grid[ty][tx]:
|
||||
valid = False
|
||||
break
|
||||
if valid:
|
||||
piece_rot = new_rot
|
||||
piece_coords = new_coords
|
||||
px = npx
|
||||
|
||||
# Try to move toward target column
|
||||
if px != target_x:
|
||||
dx_step = 1 if target_x > px else -1
|
||||
npx = px + dx_step
|
||||
valid = True
|
||||
for dx, dy in piece_coords:
|
||||
tx, ty = npx + dx, py + dy
|
||||
if ty >= 20 or tx < 0 or tx >= 10 or grid[ty][tx]:
|
||||
valid = False
|
||||
break
|
||||
if valid:
|
||||
px = npx
|
||||
|
||||
# Apply gravity every 4 frames (speeds up self-play nicely)
|
||||
if frame % 4 == 0:
|
||||
npy = py + 1
|
||||
can_drop = True
|
||||
for dx, dy in piece_coords:
|
||||
tx, ty = px + dx, npy + dy
|
||||
if ty >= 20 or grid[ty][tx]:
|
||||
can_drop = False
|
||||
break
|
||||
|
||||
if can_drop:
|
||||
py = npy
|
||||
else:
|
||||
# Lock piece
|
||||
for dx, dy in piece_coords:
|
||||
grid[py + dy][px + dx] = 1
|
||||
|
||||
# Check lines
|
||||
lines_this_turn = 0
|
||||
for r in range(20):
|
||||
if all(grid[r]):
|
||||
del grid[r]
|
||||
grid.insert(0, [0 for _ in range(10)])
|
||||
lines_this_turn += 1
|
||||
|
||||
if lines_this_turn > 0:
|
||||
lines_cleared += lines_this_turn
|
||||
score += [0, 40, 100, 300, 1200][min(lines_this_turn, 4)]
|
||||
|
||||
# Spawn next piece
|
||||
current_shape_idx = next_shape_idx
|
||||
next_shape_idx = random.randint(0, len(SHAPES) - 1)
|
||||
|
||||
piece_rot = 0
|
||||
piece_coords = rotate_coords(SHAPES[current_shape_idx], piece_rot)
|
||||
pw, ph = get_piece_dims(piece_coords)
|
||||
px = (10 - pw) // 2
|
||||
py = 0
|
||||
|
||||
# Set next targets
|
||||
target_x = random.randint(0, 10 - pw)
|
||||
target_rot = random.randint(0, 3)
|
||||
|
||||
# Check game over (spawning block collides immediately)
|
||||
for dx, dy in piece_coords:
|
||||
if grid[py + dy][px + dx]:
|
||||
game_over_delay = 60 # 2 seconds of game over display
|
||||
break
|
||||
|
||||
# Draw game state
|
||||
display.clear(0)
|
||||
|
||||
# Draw board border
|
||||
display.rect(board_x - 2, board_y - 2, board_w + 4, board_h + 4, 1)
|
||||
|
||||
# Draw grid contents
|
||||
for r in range(20):
|
||||
for c in range(10):
|
||||
cx_px = board_x + c * cell_size
|
||||
cy_px = board_y + r * cell_size
|
||||
if grid[r][c] == 1:
|
||||
display.fill_rect(cx_px + 1, cy_px + 1, cell_size - 1, cell_size - 1, 1)
|
||||
|
||||
# Draw active piece
|
||||
for dx, dy in piece_coords:
|
||||
cx_px = board_x + (px + dx) * cell_size
|
||||
cy_px = board_y + (py + dy) * cell_size
|
||||
if 0 <= py + dy < 20:
|
||||
display.fill_rect(cx_px + 1, cy_px + 1, cell_size - 1, cell_size - 1, 1)
|
||||
# Add a small white dot in falling piece for visual difference
|
||||
display.fill_rect(cx_px + cell_size//2, cy_px + cell_size//2, 2, 2, 0)
|
||||
|
||||
# Side panel (Left) - Title & Info
|
||||
display.text(text if text else "FALLING", 10, 20, 1)
|
||||
display.text("PUZZLE", 10, 32, 1)
|
||||
display.text(f"SCORE:", 10, 80, 1)
|
||||
display.text(f"{score:05d}", 10, 92, 1)
|
||||
display.text(f"LINES:", 10, 130, 1)
|
||||
display.text(f"{lines_cleared}", 10, 142, 1)
|
||||
|
||||
# Side panel (Right) - Next piece preview
|
||||
preview_x = board_x + board_w + 20
|
||||
display.text("NEXT:", preview_x, 20, 1)
|
||||
display.rect(preview_x, 35, 50, 50, 1)
|
||||
|
||||
next_coords = rotate_coords(SHAPES[next_shape_idx], 0)
|
||||
npw, nph = get_piece_dims(next_coords)
|
||||
off_x = preview_x + (50 - npw * cell_size) // 2
|
||||
off_y = 35 + (50 - nph * cell_size) // 2
|
||||
|
||||
for dx, dy in next_coords:
|
||||
display.fill_rect(off_x + dx * cell_size + 1, off_y + dy * cell_size + 1, cell_size - 1, cell_size - 1, 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