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

298 lines
11 KiB
Python

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