Skip to content

Commit df19bf2

Browse files
committed
Examples: adding 'maze_qwstpad.py
1 parent 2faefb5 commit df19bf2

File tree

1 file changed

+358
-0
lines changed

1 file changed

+358
-0
lines changed

examples/maze_qwstpad.py

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
import gc
2+
import random
3+
import time
4+
from collections import namedtuple
5+
6+
from explorer import BLACK, GREEN, RED, WHITE, display, i2c
7+
from qwstpad import ADDRESSES, QwSTPad
8+
9+
"""
10+
A single player QwSTPad game demo. Navigate a set of mazes from the start (red) to the goal (green).
11+
Mazes get bigger / harder with each increase in level.
12+
Makes use of 1 QwSTPad and a Pimoroni Explorer
13+
14+
Controls:
15+
* U = Move Forward
16+
* D = Move Backward
17+
* R = Move Right
18+
* L = Move left
19+
* + = Continue (once the current level is complete)
20+
"""
21+
22+
# General Constants
23+
I2C_ADDRESS = ADDRESSES[0] # The I2C address of the connected QwSTPad
24+
BRIGHTNESS = 1.0 # The brightness of the LCD backlight (from 0.0 to 1.0)
25+
26+
# Colour Constants (RGB565)
27+
PLAYER = display.create_pen(227, 231, 110)
28+
WALL = display.create_pen(127, 125, 244)
29+
BACKGROUND = display.create_pen(60, 57, 169)
30+
PATH = display.create_pen((227 + 60) // 2, (231 + 57) // 2, (110 + 169) // 2)
31+
32+
# Gameplay Constants
33+
Position = namedtuple("Position", ("x", "y"))
34+
MIN_MAZE_WIDTH = 2
35+
MAX_MAZE_WIDTH = 5
36+
MIN_MAZE_HEIGHT = 2
37+
MAX_MAZE_HEIGHT = 5
38+
WALL_SHADOW = 2
39+
WALL_GAP = 1
40+
TEXT_SHADOW = 2
41+
MOVEMENT_SLEEP = 0.1
42+
DIFFICULT_SCALE = 0.5
43+
44+
# Variables
45+
complete = False # Has the game been completed?
46+
level = 0 # The current "level" the player is on (affects difficulty)
47+
48+
# Get the width and height from the display
49+
WIDTH, HEIGHT = display.get_bounds()
50+
51+
52+
# Classes
53+
class Cell:
54+
def __init__(self, x, y):
55+
self.x = x
56+
self.y = y
57+
self.bottom = True
58+
self.right = True
59+
self.visited = False
60+
61+
@staticmethod
62+
def remove_walls(current, next):
63+
dx, dy = current.x - next.x, current.y - next.y
64+
if dx == 1:
65+
next.right = False
66+
if dx == -1:
67+
current.right = False
68+
if dy == 1:
69+
next.bottom = False
70+
if dy == -1:
71+
current.bottom = False
72+
73+
74+
class MazeBuilder:
75+
def __init__(self):
76+
self.width = 0
77+
self.height = 0
78+
self.cell_grid = []
79+
self.maze = []
80+
81+
def build(self, width, height):
82+
if width <= 0:
83+
raise ValueError("width out of range. Expected greater than 0")
84+
85+
if height <= 0:
86+
raise ValueError("height out of range. Expected greater than 0")
87+
88+
self.width = width
89+
self.height = height
90+
91+
# Set the starting cell to the centre
92+
cx = (self.width - 1) // 2
93+
cy = (self.height - 1) // 2
94+
95+
gc.collect()
96+
97+
# Create a grid of cells for building a maze
98+
self.cell_grid = [[Cell(x, y) for y in range(self.height)] for x in range(self.width)]
99+
cell_stack = []
100+
101+
# Retrieve the starting cell and mark it as visited
102+
current = self.cell_grid[cx][cy]
103+
current.visited = True
104+
105+
# Loop until every cell has been visited
106+
while True:
107+
next = self.choose_neighbour(current)
108+
# Was a valid neighbour found?
109+
if next is not None:
110+
# Move to the next cell, removing walls in the process
111+
next.visited = True
112+
cell_stack.append(current)
113+
Cell.remove_walls(current, next)
114+
current = next
115+
116+
# No valid neighbour. Backtrack to a previous cell
117+
elif len(cell_stack) > 0:
118+
current = cell_stack.pop()
119+
120+
# No previous cells, so exit
121+
else:
122+
break
123+
124+
gc.collect()
125+
126+
# Use the cell grid to create a maze grid of 0's and 1s
127+
self.maze = []
128+
129+
row = [1]
130+
for x in range(0, self.width):
131+
row.append(1)
132+
row.append(1)
133+
self.maze.append(row)
134+
135+
for y in range(0, self.height):
136+
row = [1]
137+
for x in range(0, self.width):
138+
row.append(0)
139+
row.append(1 if self.cell_grid[x][y].right else 0)
140+
self.maze.append(row)
141+
142+
row = [1]
143+
for x in range(0, self.width):
144+
row.append(1 if self.cell_grid[x][y].bottom else 0)
145+
row.append(1)
146+
self.maze.append(row)
147+
148+
self.cell_grid.clear()
149+
gc.collect()
150+
151+
self.grid_columns = (self.width * 2 + 1)
152+
self.grid_rows = (self.height * 2 + 1)
153+
154+
def choose_neighbour(self, current):
155+
unvisited = []
156+
for dx in range(-1, 2, 2):
157+
x = current.x + dx
158+
if x >= 0 and x < self.width and not self.cell_grid[x][current.y].visited:
159+
unvisited.append((x, current.y))
160+
161+
for dy in range(-1, 2, 2):
162+
y = current.y + dy
163+
if y >= 0 and y < self.height and not self.cell_grid[current.x][y].visited:
164+
unvisited.append((current.x, y))
165+
166+
if len(unvisited) > 0:
167+
x, y = random.choice(unvisited)
168+
return self.cell_grid[x][y]
169+
170+
return None
171+
172+
def maze_width(self):
173+
return (self.width * 2) + 1
174+
175+
def maze_height(self):
176+
return (self.height * 2) + 1
177+
178+
def draw(self, display):
179+
# Draw the maze we have built. Each '1' in the array represents a wall
180+
for row in range(self.grid_rows):
181+
for col in range(self.grid_columns):
182+
# Calculate the screen coordinates
183+
x = (col * wall_separation) + offset_x
184+
y = (row * wall_separation) + offset_y
185+
186+
if self.maze[row][col] == 1:
187+
# Draw a wall shadow
188+
display.set_pen(BLACK)
189+
display.rectangle(x + WALL_SHADOW, y + WALL_SHADOW, wall_size, wall_size)
190+
191+
# Draw a wall top
192+
display.set_pen(WALL)
193+
display.rectangle(x, y, wall_size, wall_size)
194+
195+
if self.maze[row][col] == 2:
196+
# Draw the player path
197+
display.set_pen(PATH)
198+
display.rectangle(x, y, wall_size, wall_size)
199+
200+
201+
class Player(object):
202+
def __init__(self, x, y, colour, pad):
203+
self.x = x
204+
self.y = y
205+
self.colour = colour
206+
self.pad = pad
207+
208+
def position(self, x, y):
209+
self.x = x
210+
self.y = y
211+
212+
def update(self, maze):
213+
# Read the player's gamepad
214+
button = self.pad.read_buttons()
215+
216+
if button['L'] and maze[self.y][self.x - 1] != 1:
217+
self.x -= 1
218+
time.sleep(MOVEMENT_SLEEP)
219+
220+
elif button['R'] and maze[self.y][self.x + 1] != 1:
221+
self.x += 1
222+
time.sleep(MOVEMENT_SLEEP)
223+
224+
elif button['U'] and maze[self.y - 1][self.x] != 1:
225+
self.y -= 1
226+
time.sleep(MOVEMENT_SLEEP)
227+
228+
elif button['D'] and maze[self.y + 1][self.x] != 1:
229+
self.y += 1
230+
time.sleep(MOVEMENT_SLEEP)
231+
232+
maze[self.y][self.x] = 2
233+
234+
def draw(self, display):
235+
display.set_pen(self.colour)
236+
display.rectangle(self.x * wall_separation + offset_x,
237+
self.y * wall_separation + offset_y,
238+
wall_size, wall_size)
239+
240+
241+
def build_maze():
242+
global wall_separation
243+
global wall_size
244+
global offset_x
245+
global offset_y
246+
global start
247+
global goal
248+
249+
difficulty = int(level * DIFFICULT_SCALE)
250+
width = random.randrange(MIN_MAZE_WIDTH, MAX_MAZE_WIDTH)
251+
height = random.randrange(MIN_MAZE_HEIGHT, MAX_MAZE_HEIGHT)
252+
builder.build(width + difficulty, height + difficulty)
253+
254+
wall_separation = min(HEIGHT // builder.grid_rows,
255+
WIDTH // builder.grid_columns)
256+
wall_size = wall_separation - WALL_GAP
257+
258+
offset_x = (WIDTH - (builder.grid_columns * wall_separation) + WALL_GAP) // 2
259+
offset_y = (HEIGHT - (builder.grid_rows * wall_separation) + WALL_GAP) // 2
260+
261+
start = Position(1, builder.grid_rows - 2)
262+
goal = Position(builder.grid_columns - 2, 1)
263+
264+
265+
# Create the maze builder and build the first maze and put
266+
builder = MazeBuilder()
267+
build_maze()
268+
269+
# Create the player object if a QwSTPad is connected
270+
try:
271+
player = Player(*start, PLAYER, QwSTPad(i2c, I2C_ADDRESS))
272+
except OSError:
273+
print("QwSTPad: Not Connected ... Exiting")
274+
raise SystemExit
275+
276+
print("QwSTPad: Connected ... Starting")
277+
278+
# Turn on the display
279+
display.set_backlight(BRIGHTNESS)
280+
281+
# Wrap the code in a try block, to catch any exceptions (including KeyboardInterrupt)
282+
try:
283+
# Loop forever
284+
while True:
285+
if not complete:
286+
# Update the player's position in the maze
287+
player.update(builder.maze)
288+
289+
# Check if any player has reached the goal position
290+
if player.x == goal.x and player.y == goal.y:
291+
complete = True
292+
else:
293+
# Check for the player wanting to continue
294+
if player.pad.read_buttons()['+']:
295+
complete = False
296+
level += 1
297+
build_maze()
298+
player.position(*start)
299+
300+
# Clear the screen to the background colour
301+
display.set_pen(BACKGROUND)
302+
display.clear()
303+
304+
# Draw the maze walls
305+
builder.draw(display)
306+
307+
# Draw the start location square
308+
display.set_pen(RED)
309+
display.rectangle(start.x * wall_separation + offset_x,
310+
start.y * wall_separation + offset_y,
311+
wall_size, wall_size)
312+
313+
# Draw the goal location square
314+
display.set_pen(GREEN)
315+
display.rectangle(goal.x * wall_separation + offset_x,
316+
goal.y * wall_separation + offset_y,
317+
wall_size, wall_size)
318+
319+
# Draw the player
320+
player.draw(display)
321+
322+
# Display the level
323+
display.set_pen(BLACK)
324+
display.text(f"Lvl: {level}", 2 + TEXT_SHADOW, 2 + TEXT_SHADOW, WIDTH, 1)
325+
display.set_pen(WHITE)
326+
display.text(f"Lvl: {level}", 2, 2, WIDTH, 1)
327+
328+
if complete:
329+
# Draw banner shadow
330+
display.set_pen(BLACK)
331+
display.rectangle(4, 94, WIDTH, 50)
332+
# Draw banner
333+
display.set_pen(PLAYER)
334+
display.rectangle(0, 90, WIDTH, 50)
335+
336+
# Draw text shadow
337+
display.set_pen(BLACK)
338+
display.text("Maze Complete!", WIDTH // 6 + TEXT_SHADOW, 96 + TEXT_SHADOW, WIDTH, 3)
339+
display.text("Press + to continue", WIDTH // 6 + 10 + TEXT_SHADOW, 120 + TEXT_SHADOW, WIDTH, 2)
340+
341+
# Draw text
342+
display.set_pen(WHITE)
343+
display.text("Maze Complete!", WIDTH // 6, 96, WIDTH, 3)
344+
display.text("Press + to continue", WIDTH // 6 + 10, 120, WIDTH, 2)
345+
346+
# Update the screen
347+
display.update()
348+
349+
# Handle the QwSTPad being disconnected unexpectedly
350+
except OSError:
351+
print("QwSTPad: Disconnected .. Exiting")
352+
353+
# Turn off the LEDs of the connected QwSTPad
354+
finally:
355+
try:
356+
player.pad.clear_leds()
357+
except OSError:
358+
pass

0 commit comments

Comments
 (0)