|
| 1 | +"""Maze game example using ISM330DL accelerometer and SSD1327 OLED. |
| 2 | +
|
| 3 | +Navigate through a randomly generated maze by tilting the STeaMi board. |
| 4 | +Your score is computed at the end based on how close you were to the |
| 5 | +optimal path, calculated with Dijkstra's algorithm. |
| 6 | +
|
| 7 | +Hardware: |
| 8 | + - ISM330DL accelerometer (tilt input) |
| 9 | + - SSD1327 128x128 OLED display (round) |
| 10 | +
|
| 11 | +Controls: |
| 12 | + - Tilt the board forward → move UP |
| 13 | + - Tilt the board backward → move DOWN |
| 14 | + - Tilt the board left → move LEFT |
| 15 | + - Tilt the board right → move RIGHT |
| 16 | +""" |
| 17 | + |
| 18 | +import random |
| 19 | +from time import sleep_ms |
| 20 | + |
| 21 | +import ssd1327 |
| 22 | +from ism330dl import ISM330DL |
| 23 | +from machine import I2C, SPI, Pin |
| 24 | +from steami_screen import GRAY, LIGHT, WHITE, Screen, SSD1327Display |
| 25 | + |
| 26 | +# ============================================================================= |
| 27 | +# === Display setup =========================================================== |
| 28 | +# ============================================================================= |
| 29 | + |
| 30 | +spi = SPI(1) |
| 31 | +dc = Pin("DATA_COMMAND_DISPLAY") |
| 32 | +res = Pin("RST_DISPLAY") |
| 33 | +cs = Pin("CS_DISPLAY") |
| 34 | +display = SSD1327Display(ssd1327.WS_OLED_128X128_SPI(spi, dc, res, cs)) |
| 35 | +screen = Screen(display) |
| 36 | + |
| 37 | +# ============================================================================= |
| 38 | +# === Accelerometer setup ===================================================== |
| 39 | +# ============================================================================= |
| 40 | + |
| 41 | +i2c = I2C(1) |
| 42 | +imu = ISM330DL(i2c) |
| 43 | + |
| 44 | +# ============================================================================= |
| 45 | +# === Game constants =========================================================== |
| 46 | +# ============================================================================= |
| 47 | + |
| 48 | +MAZE_W = 11 # Must be odd |
| 49 | +MAZE_H = 11 # Must be odd |
| 50 | +START_ROW = 1 |
| 51 | +START_COL = 1 |
| 52 | +GOAL_ROW = MAZE_H - 2 |
| 53 | +GOAL_COL = MAZE_W - 2 |
| 54 | +MOVE_DELAY_MS = 200 # Delay between moves in milliseconds |
| 55 | +TILT_THRESHOLD = 0.3 # Minimum tilt in g to trigger a move |
| 56 | + |
| 57 | +# Cell types |
| 58 | +WALL = 0 |
| 59 | +PATH = 1 |
| 60 | + |
| 61 | +# Directions: (row_delta, col_delta) |
| 62 | +UP = (-1, 0) |
| 63 | +DOWN = (1, 0) |
| 64 | +LEFT = (0, -1) |
| 65 | +RIGHT = (0, 1) |
| 66 | +NONE = (0, 0) |
| 67 | + |
| 68 | +# Display safe zone for round screen |
| 69 | +SAFE_X = 19 |
| 70 | +SAFE_Y = 19 |
| 71 | +SAFE_SIZE = 90 |
| 72 | +CELL_SIZE = SAFE_SIZE // MAZE_W # 8px per cell |
| 73 | + |
| 74 | +# ============================================================================= |
| 75 | +# === Maze generation (Recursive Backtracker / DFS) =========================== |
| 76 | +# ============================================================================= |
| 77 | + |
| 78 | + |
| 79 | +def generate_maze(width, height): |
| 80 | + """Generate a perfect maze using Recursive Backtracker algorithm. |
| 81 | +
|
| 82 | + Returns a 2D list of WALL/PATH values. |
| 83 | + Width and height must be odd numbers. |
| 84 | + """ |
| 85 | + maze = [[WALL] * width for _ in range(height)] |
| 86 | + |
| 87 | + def carve(row, col): |
| 88 | + maze[row][col] = PATH |
| 89 | + dirs = [(-2, 0), (2, 0), (0, -2), (0, 2)] |
| 90 | + # Shuffle directions |
| 91 | + for i in range(len(dirs) - 1, 0, -1): |
| 92 | + j = random.randint(0, i) |
| 93 | + dirs[i], dirs[j] = dirs[j], dirs[i] |
| 94 | + for dr, dc in dirs: |
| 95 | + nr, nc = row + dr, col + dc |
| 96 | + if 0 <= nr < height and 0 <= nc < width and maze[nr][nc] == WALL: |
| 97 | + maze[row + dr // 2][col + dc // 2] = PATH |
| 98 | + carve(nr, nc) |
| 99 | + |
| 100 | + carve(1, 1) |
| 101 | + return maze |
| 102 | + |
| 103 | + |
| 104 | +# ============================================================================= |
| 105 | +# === Dijkstra shortest path ================================================== |
| 106 | +# ============================================================================= |
| 107 | + |
| 108 | + |
| 109 | +def dijkstra(maze, start_row, start_col, goal_row, goal_col): |
| 110 | + """Find the shortest path length from start to goal in the maze. |
| 111 | +
|
| 112 | + Returns the number of steps in the optimal path, or -1 if not found. |
| 113 | + """ |
| 114 | + height = len(maze) |
| 115 | + width = len(maze[0]) |
| 116 | + INF = 999999 |
| 117 | + dist = [[INF] * width for _ in range(height)] |
| 118 | + dist[start_row][start_col] = 0 |
| 119 | + queue = [(0, start_row, start_col)] |
| 120 | + |
| 121 | + while queue: |
| 122 | + # Find minimum distance node |
| 123 | + min_idx = 0 |
| 124 | + for i in range(1, len(queue)): |
| 125 | + if queue[i][0] < queue[min_idx][0]: |
| 126 | + min_idx = i |
| 127 | + cost, row, col = queue.pop(min_idx) |
| 128 | + |
| 129 | + if row == goal_row and col == goal_col: |
| 130 | + return cost |
| 131 | + if cost > dist[row][col]: |
| 132 | + continue |
| 133 | + |
| 134 | + for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: |
| 135 | + nr, nc = row + dr, col + dc |
| 136 | + if 0 <= nr < height and 0 <= nc < width and maze[nr][nc] == PATH: |
| 137 | + new_cost = cost + 1 |
| 138 | + if new_cost < dist[nr][nc]: |
| 139 | + dist[nr][nc] = new_cost |
| 140 | + queue.append((new_cost, nr, nc)) |
| 141 | + |
| 142 | + return -1 |
| 143 | + |
| 144 | + |
| 145 | +def compute_score(player_steps, optimal_steps, max_score=1000): |
| 146 | + """Compute player score based on path efficiency. |
| 147 | +
|
| 148 | + Returns a score between 0 and max_score. |
| 149 | + Perfect path = max_score. Score decreases as player takes more steps. |
| 150 | + """ |
| 151 | + if optimal_steps <= 0: |
| 152 | + return 0 |
| 153 | + if player_steps <= optimal_steps: |
| 154 | + return max_score |
| 155 | + return max(0, int(max_score * optimal_steps / player_steps)) |
| 156 | + |
| 157 | + |
| 158 | +# ============================================================================= |
| 159 | +# === Accelerometer input ===================================================== |
| 160 | +# ============================================================================= |
| 161 | + |
| 162 | + |
| 163 | +def read_direction(): |
| 164 | + ax, ay, _ = imu.acceleration_g() |
| 165 | + ax = -ax # Invert forward/backward axis |
| 166 | + abs_x = ax if ax >= 0 else -ax |
| 167 | + abs_y = ay if ay >= 0 else -ay |
| 168 | + |
| 169 | + if abs_x < TILT_THRESHOLD and abs_y < TILT_THRESHOLD: |
| 170 | + return NONE |
| 171 | + if abs_x >= abs_y: |
| 172 | + return DOWN if ax > 0 else UP |
| 173 | + return RIGHT if ay > 0 else LEFT |
| 174 | + |
| 175 | + |
| 176 | +# ============================================================================= |
| 177 | +# === Display helpers ========================================================= |
| 178 | +# ============================================================================= |
| 179 | + |
| 180 | + |
| 181 | +def cell_to_pixel(row, col): |
| 182 | + """Convert maze cell coordinates to screen pixel coordinates.""" |
| 183 | + return SAFE_X + col * CELL_SIZE, SAFE_Y + row * CELL_SIZE |
| 184 | + |
| 185 | + |
| 186 | +def draw_maze(maze): |
| 187 | + """Draw all maze walls as filled rectangles.""" |
| 188 | + for row in range(len(maze)): |
| 189 | + for col in range(len(maze[0])): |
| 190 | + x, y = cell_to_pixel(row, col) |
| 191 | + if maze[row][col] == WALL: |
| 192 | + screen.rect(x, y, CELL_SIZE, CELL_SIZE, LIGHT, fill=True) |
| 193 | + |
| 194 | + |
| 195 | +def draw_player(row, col): |
| 196 | + """Draw player as a bright filled circle.""" |
| 197 | + x, y = cell_to_pixel(row, col) |
| 198 | + cx = x + CELL_SIZE // 2 |
| 199 | + cy = y + CELL_SIZE // 2 |
| 200 | + screen.circle(cx, cy, CELL_SIZE // 2 - 1, WHITE, fill=True) |
| 201 | + |
| 202 | + |
| 203 | +def draw_goal(row, col): |
| 204 | + """Draw goal marker as a gray filled circle.""" |
| 205 | + x, y = cell_to_pixel(row, col) |
| 206 | + cx = x + CELL_SIZE // 2 |
| 207 | + cy = y + CELL_SIZE // 2 |
| 208 | + screen.circle(cx, cy, CELL_SIZE // 2 - 1, GRAY, fill=True) |
| 209 | + |
| 210 | + |
| 211 | +def render(maze, player_row, player_col, steps, optimal): |
| 212 | + """Full frame render: maze, goal, player and HUD.""" |
| 213 | + screen.clear() |
| 214 | + draw_maze(maze) |
| 215 | + draw_goal(GOAL_ROW, GOAL_COL) |
| 216 | + draw_player(player_row, player_col) |
| 217 | + screen.title(f"S:{steps} B:{optimal}") |
| 218 | + screen.show() |
| 219 | + |
| 220 | + |
| 221 | +# ============================================================================= |
| 222 | +# === Screens ================================================================= |
| 223 | +# ============================================================================= |
| 224 | + |
| 225 | + |
| 226 | +def show_start_screen(): |
| 227 | + """Display start screen.""" |
| 228 | + screen.clear() |
| 229 | + screen.title("MAZE GAME") |
| 230 | + screen.face("happy") |
| 231 | + screen.subtitle("Tilt to play!") |
| 232 | + screen.show() |
| 233 | + sleep_ms(2000) |
| 234 | + |
| 235 | + |
| 236 | +def show_win_screen(steps, optimal): |
| 237 | + """Display win screen with score.""" |
| 238 | + score = compute_score(steps, optimal) |
| 239 | + screen.clear() |
| 240 | + screen.title("YOU WIN!") |
| 241 | + screen.value(str(score), unit="pts") |
| 242 | + screen.subtitle(f"Steps: {steps}", f"Best: {optimal}") |
| 243 | + screen.show() |
| 244 | + sleep_ms(5000) |
| 245 | + |
| 246 | + |
| 247 | +# ============================================================================= |
| 248 | +# === Main game loop ========================================================== |
| 249 | +# ============================================================================= |
| 250 | + |
| 251 | + |
| 252 | +def run_game(): |
| 253 | + """Generate a new maze and run one full game round. |
| 254 | +
|
| 255 | + Returns when the player reaches the goal. |
| 256 | + """ |
| 257 | + maze = generate_maze(MAZE_W, MAZE_H) |
| 258 | + optimal = dijkstra(maze, START_ROW, START_COL, GOAL_ROW, GOAL_COL) |
| 259 | + |
| 260 | + player_row = START_ROW |
| 261 | + player_col = START_COL |
| 262 | + steps = 0 |
| 263 | + |
| 264 | + show_start_screen() |
| 265 | + |
| 266 | + while True: |
| 267 | + render(maze, player_row, player_col, steps, optimal) |
| 268 | + |
| 269 | + direction = read_direction() |
| 270 | + if direction != NONE: |
| 271 | + dr, dc = direction |
| 272 | + nr = player_row + dr |
| 273 | + nc = player_col + dc |
| 274 | + if 0 <= nr < MAZE_H and 0 <= nc < MAZE_W and maze[nr][nc] == PATH: |
| 275 | + player_row = nr |
| 276 | + player_col = nc |
| 277 | + steps += 1 |
| 278 | + |
| 279 | + if player_row == GOAL_ROW and player_col == GOAL_COL: |
| 280 | + show_win_screen(steps, optimal) |
| 281 | + return |
| 282 | + |
| 283 | + sleep_ms(MOVE_DELAY_MS) |
| 284 | + |
| 285 | + |
| 286 | +# ============================================================================= |
| 287 | +# === Entry point ============================================================= |
| 288 | +# ============================================================================= |
| 289 | + |
| 290 | +print("Maze game starting...") |
| 291 | + |
| 292 | +try: |
| 293 | + while True: |
| 294 | + run_game() |
| 295 | +except KeyboardInterrupt: |
| 296 | + print("\nMaze game stopped.") |
| 297 | +finally: |
| 298 | + screen.clear() |
| 299 | + screen.show() |
| 300 | + imu.power_off() |
0 commit comments