From c51c430cdf26436d9271cf2462cf3c9ccebe1488 Mon Sep 17 00:00:00 2001 From: thirdr Date: Fri, 22 Nov 2024 15:48:32 +0000 Subject: [PATCH] Examples: adding random_maze.py --- .../examples/interstate_75_w/random_maze.py | 377 ++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 micropython/examples/interstate_75_w/random_maze.py diff --git a/micropython/examples/interstate_75_w/random_maze.py b/micropython/examples/interstate_75_w/random_maze.py new file mode 100644 index 0000000..2758042 --- /dev/null +++ b/micropython/examples/interstate_75_w/random_maze.py @@ -0,0 +1,377 @@ +import gc +import random +import time +from collections import namedtuple + +from interstate75 import DISPLAY_INTERSTATE75_128X128, Interstate75 +from machine import I2C +from qwstpad import ADDRESSES, QwSTPad + +""" +A single player game demo. Navigate a set of mazes from the start (red) to the goal (green). +Mazes get bigger / harder with each increase in level. +Makes use of 1 QwSTPad and a 128x128 LED matrix + i75W. + +Controls: +* U = Move Forward +* D = Move Backward +* R = Move Right +* L = Move left +* + = Continue (once the current level is complete) +""" + +# General Constants +I2C_PINS = {"id": 0, "sda": 20, "scl": 21} # The I2C pins the QwSTPad is connected to +I2C_ADDRESS = ADDRESSES[0] # The I2C address of the connected QwSTPad +BRIGHTNESS = 1.0 # The brightness of the LCD backlight (from 0.0 to 1.0) + +# Gameplay Constants +Position = namedtuple("Position", ("x", "y")) +MIN_MAZE_WIDTH = 2 +MAX_MAZE_WIDTH = 5 +MIN_MAZE_HEIGHT = 2 +MAX_MAZE_HEIGHT = 5 +WALL_SHADOW = 1 +WALL_GAP = 1 +TEXT_SHADOW = 1 +MOVEMENT_SLEEP = 0.1 +DIFFICULT_SCALE = 0.5 + +# Setup for the display +i75 = Interstate75( + display=DISPLAY_INTERSTATE75_128X128, stb_invert=False, panel_type=Interstate75.PANEL_GENERIC) +display = i75.display + +# Colour Constants +WHITE = display.create_pen(255, 255, 255) +BLACK = display.create_pen(0, 0, 0) +RED = display.create_pen(255, 0, 0) +GREEN = display.create_pen(0, 255, 0) +PLAYER = display.create_pen(227, 231, 110) +WALL = display.create_pen(127, 125, 244) +BACKGROUND = display.create_pen(60, 57, 169) +PATH = display.create_pen((227 + 60) // 2, (231 + 57) // 2, (110 + 169) // 2) + +# Variables +i2c = I2C(**I2C_PINS) # The I2C instance to pass to the QwSTPad +complete = False # Has the game been completed? +level = 0 # The current "level" the player is on (affects difficulty) + +# Get the width and height from the display +WIDTH, HEIGHT = display.get_bounds() + + +# Classes +class Cell: + def __init__(self, x, y): + self.x = x + self.y = y + self.bottom = True + self.right = True + self.visited = False + + @staticmethod + def remove_walls(current, next): + dx, dy = current.x - next.x, current.y - next.y + if dx == 1: + next.right = False + if dx == -1: + current.right = False + if dy == 1: + next.bottom = False + if dy == -1: + current.bottom = False + + +class MazeBuilder: + def __init__(self): + self.width = 0 + self.height = 0 + self.cell_grid = [] + self.maze = [] + + def build(self, width, height): + if width <= 0: + raise ValueError("width out of range. Expected greater than 0") + + if height <= 0: + raise ValueError("height out of range. Expected greater than 0") + + self.width = width + self.height = height + + # Set the starting cell to the centre + cx = (self.width - 1) // 2 + cy = (self.height - 1) // 2 + + gc.collect() + + # Create a grid of cells for building a maze + self.cell_grid = [[Cell(x, y) for y in range(self.height)] for x in range(self.width)] + cell_stack = [] + + # Retrieve the starting cell and mark it as visited + current = self.cell_grid[cx][cy] + current.visited = True + + # Loop until every cell has been visited + while True: + next = self.choose_neighbour(current) + # Was a valid neighbour found? + if next is not None: + # Move to the next cell, removing walls in the process + next.visited = True + cell_stack.append(current) + Cell.remove_walls(current, next) + current = next + + # No valid neighbour. Backtrack to a previous cell + elif len(cell_stack) > 0: + current = cell_stack.pop() + + # No previous cells, so exit + else: + break + + gc.collect() + + # Use the cell grid to create a maze grid of 0's and 1s + self.maze = [] + + row = [1] + for x in range(0, self.width): + row.append(1) + row.append(1) + self.maze.append(row) + + for y in range(0, self.height): + row = [1] + for x in range(0, self.width): + row.append(0) + row.append(1 if self.cell_grid[x][y].right else 0) + self.maze.append(row) + + row = [1] + for x in range(0, self.width): + row.append(1 if self.cell_grid[x][y].bottom else 0) + row.append(1) + self.maze.append(row) + + self.cell_grid.clear() + gc.collect() + + self.grid_columns = (self.width * 2 + 1) + self.grid_rows = (self.height * 2 + 1) + + def choose_neighbour(self, current): + unvisited = [] + for dx in range(-1, 2, 2): + x = current.x + dx + if x >= 0 and x < self.width and not self.cell_grid[x][current.y].visited: + unvisited.append((x, current.y)) + + for dy in range(-1, 2, 2): + y = current.y + dy + if y >= 0 and y < self.height and not self.cell_grid[current.x][y].visited: + unvisited.append((current.x, y)) + + if len(unvisited) > 0: + x, y = random.choice(unvisited) + return self.cell_grid[x][y] + + return None + + def maze_width(self): + return (self.width * 2) + 1 + + def maze_height(self): + return (self.height * 2) + 1 + + def draw(self, display): + # Draw the maze we have built. Each '1' in the array represents a wall + for row in range(self.grid_rows): + for col in range(self.grid_columns): + # Calculate the screen coordinates + x = (col * wall_separation) + offset_x + y = (row * wall_separation) + offset_y + + if self.maze[row][col] == 1: + # Draw a wall shadow + display.set_pen(BLACK) + display.rectangle(x + WALL_SHADOW, y + WALL_SHADOW, wall_size, wall_size) + + # Draw a wall top + display.set_pen(WALL) + display.rectangle(x, y, wall_size, wall_size) + + if self.maze[row][col] == 2: + # Draw the player path + display.set_pen(PATH) + display.rectangle(x, y, wall_size, wall_size) + + +class Player(object): + def __init__(self, x, y, colour, pad): + self.x = x + self.y = y + self.colour = colour + self.pad = pad + + def position(self, x, y): + self.x = x + self.y = y + + def update(self, maze): + # Read the player's gamepad + button = self.pad.read_buttons() + + if button['L'] and maze[self.y][self.x - 1] != 1: + self.x -= 1 + time.sleep(MOVEMENT_SLEEP) + + elif button['R'] and maze[self.y][self.x + 1] != 1: + self.x += 1 + time.sleep(MOVEMENT_SLEEP) + + elif button['U'] and maze[self.y - 1][self.x] != 1: + self.y -= 1 + time.sleep(MOVEMENT_SLEEP) + + elif button['D'] and maze[self.y + 1][self.x] != 1: + self.y += 1 + time.sleep(MOVEMENT_SLEEP) + + maze[self.y][self.x] = 2 + + def draw(self, display): + display.set_pen(self.colour) + display.rectangle(self.x * wall_separation + offset_x, + self.y * wall_separation + offset_y, + wall_size, wall_size) + + +def build_maze(): + global wall_separation + global wall_size + global offset_x + global offset_y + global start + global goal + + difficulty = int(level * DIFFICULT_SCALE) + width = random.randrange(MIN_MAZE_WIDTH, MAX_MAZE_WIDTH) + height = random.randrange(MIN_MAZE_HEIGHT, MAX_MAZE_HEIGHT) + builder.build(width + difficulty, height + difficulty) + + wall_separation = min(HEIGHT // builder.grid_rows, + WIDTH // builder.grid_columns) + wall_size = wall_separation - WALL_GAP + + offset_x = (WIDTH - (builder.grid_columns * wall_separation) + WALL_GAP) // 2 + offset_y = (HEIGHT - (builder.grid_rows * wall_separation) + WALL_GAP) // 2 + + start = Position(1, builder.grid_rows - 2) + goal = Position(builder.grid_columns - 2, 1) + + +# Create the maze builder and build the first maze and put +builder = MazeBuilder() +build_maze() + +# Create the player object if a QwSTPad is connected +try: + player = Player(*start, PLAYER, QwSTPad(i2c, I2C_ADDRESS)) +except OSError: + print("QwSTPad: Not Connected ... Exiting") + raise SystemExit + +print("QwSTPad: Connected ... Starting") + +# Store text strings and calculate centre location +text_1_string = "Maze Complete!" +text_1_size = display.measure_text(text_1_string, 1) + +text_2_string = "Press + to continue" +text_2_size = display.measure_text(text_2_string, 1) + +text_1_location = ((WIDTH // 2) - (text_1_size // 2), 55) +text_2_location = ((WIDTH // 2) - (text_2_size // 2), 65) + +# Wrap the code in a try block, to catch any exceptions (including KeyboardInterrupt) +try: + # Loop forever + while True: + if not complete: + # Update the player's position in the maze + player.update(builder.maze) + + # Check if any player has reached the goal position + if player.x == goal.x and player.y == goal.y: + complete = True + else: + # Check for the player wanting to continue + if player.pad.read_buttons()['+']: + complete = False + level += 1 + build_maze() + player.position(*start) + + # Clear the screen to the background colour + display.set_pen(BACKGROUND) + display.clear() + + # Draw the maze walls + builder.draw(display) + + # Draw the start location square + display.set_pen(RED) + display.rectangle(start.x * wall_separation + offset_x, + start.y * wall_separation + offset_y, + wall_size, wall_size) + + # Draw the goal location square + display.set_pen(GREEN) + display.rectangle(goal.x * wall_separation + offset_x, + goal.y * wall_separation + offset_y, + wall_size, wall_size) + + # Draw the player + player.draw(display) + + # Display the level + display.set_pen(BLACK) + display.text(f"Lvl: {level}", 2 + TEXT_SHADOW, 2 + TEXT_SHADOW, WIDTH, 1) + display.set_pen(WHITE) + display.text(f"Lvl: {level}", 2, 2, WIDTH, 1) + + if complete: + # Draw banner shadow + display.set_pen(BLACK) + display.rectangle(4, 44, WIDTH, 50) + # Draw banner + display.set_pen(PLAYER) + display.rectangle(0, 40, WIDTH, 50) + + # Draw text shadow + display.set_pen(BLACK) + display.text(f"{text_1_string}", text_1_location[0] + TEXT_SHADOW, text_1_location[1] + TEXT_SHADOW, WIDTH, 1) + display.text(f"{text_2_string}", text_2_location[0] + TEXT_SHADOW, text_2_location[1] + TEXT_SHADOW, WIDTH, 1) + + # Draw text + display.set_pen(WHITE) + display.text(f"{text_1_string}", text_1_location[0], text_1_location[1], WIDTH, 1) + display.text(f"{text_2_string}", text_2_location[0], text_2_location[1], WIDTH, 1) + + # Finally we update the display with our changes :) + i75.update() + +# Handle the QwSTPad being disconnected unexpectedly +except OSError: + print("QwSTPad: Disconnected .. Exiting") + +# Turn off the LEDs of the connected QwSTPad +finally: + try: + player.pad.clear_leds() + except OSError: + pass