generated from pimoroni/pga
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
358 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,358 @@ | ||
import gc | ||
import random | ||
import time | ||
from collections import namedtuple | ||
|
||
from explorer import BLACK, GREEN, RED, WHITE, display, i2c | ||
from qwstpad import ADDRESSES, QwSTPad | ||
|
||
""" | ||
A single player QwSTPad 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 Pimoroni Explorer | ||
Controls: | ||
* U = Move Forward | ||
* D = Move Backward | ||
* R = Move Right | ||
* L = Move left | ||
* + = Continue (once the current level is complete) | ||
""" | ||
|
||
# General Constants | ||
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) | ||
|
||
# Colour Constants (RGB565) | ||
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) | ||
|
||
# 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 = 2 | ||
WALL_GAP = 1 | ||
TEXT_SHADOW = 2 | ||
MOVEMENT_SLEEP = 0.1 | ||
DIFFICULT_SCALE = 0.5 | ||
|
||
# Variables | ||
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") | ||
|
||
# Turn on the display | ||
display.set_backlight(BRIGHTNESS) | ||
|
||
# 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, 94, WIDTH, 50) | ||
# Draw banner | ||
display.set_pen(PLAYER) | ||
display.rectangle(0, 90, WIDTH, 50) | ||
|
||
# Draw text shadow | ||
display.set_pen(BLACK) | ||
display.text("Maze Complete!", WIDTH // 6 + TEXT_SHADOW, 96 + TEXT_SHADOW, WIDTH, 3) | ||
display.text("Press + to continue", WIDTH // 6 + 10 + TEXT_SHADOW, 120 + TEXT_SHADOW, WIDTH, 2) | ||
|
||
# Draw text | ||
display.set_pen(WHITE) | ||
display.text("Maze Complete!", WIDTH // 6, 96, WIDTH, 3) | ||
display.text("Press + to continue", WIDTH // 6 + 10, 120, WIDTH, 2) | ||
|
||
# Update the screen | ||
display.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 |