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