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