超解压的弹珠迷宫游戏

2026-09-242

一、背景分析

不知道大家有没有玩过这样一个游戏。尺子上的弹珠迷宫。就是一个弹珠,尺子中间有镂空的迷宫,通过控制尺子的角度,利用重力移动弹珠通过迷宫。小时上课不听讲,就是在玩这个游戏。现在有时候压力大了,需要能心平气和地做一件事情,就想到了这个游戏,占用时间不长,但游戏过程中必须保持心态平和。

二、硬件选用

手头有块Stick S3开发板,这是一款紧凑且高性能的可编程控制器,专为远程控制,物联网应用设计。核心搭载 ESP32-S3-PICO-1-N8R8 主控芯片,支持 2.4 GHz Wi-Fi 无线通信,内置 8MB Flash 与 8MB PSRAM,满足多样化应用开发需求,提供出色的性能与扩展性。人机交互方面配备 1.14" LCD 显示屏、 6 轴 IMU 传感器、可编程按钮。音频系统采用 ES8311 单声道编解码器,结合高灵敏 MEMS 麦克风 与 AW8737 功率放大器,实现清晰拾音与高保真音频输出,赋能语音识别与交互体验。同时集成 IR 发送和接收管,250mAh 锂电池。有屏幕输出,有IMU感应重力,还有扬声器,不需要额外的硬件,就用这个板子来做这个游戏啦!

三、项目实现

开发语言使用Arduino,使用Vscode+platforomIO作为编辑器,来实现项目。

既然是个迷宫游戏,就要先有个迷宫。生成迷宫的算法主要有三种思路,其中最小生成树算法又可以分为选点法(prim)和选边法(kruskal):随机深度优先算法。递归分割算法(TODO)。随机prim最小生成树算法。*kruskal最小生成树算法(使用并查集实现)。生成的迷宫需要在屏幕上显示,屏幕135*240像素的。我这里使用15*15的矩形块作为迷宫的通道和障碍物。则迷宫的规模就是15*27。使用随机深度优先算法来生成迷宫。深度优先算法过程核心是随机选择遍历上下左右四个方向的顺序,然后开始搜索。将整个迷宫看做一个【15*27】的矩阵,每个节点使用一位来存储,每一行就使用两个byte来存储。迷宫就使用一个无符号整型的数组来表示,长度为27。用python做个简单的脚本,来生成迷宫数组。

import numpy as np
import random

class Maze(object):
    def __init__(self, width=11, height=11):
        # 迷宫最小长宽为5
        assert width >= 5 and height >= 5, "Length of width or height must be larger than 5."

        # 确保迷宫的长和宽均为奇数
        self.width = (width // 2) * 2 + 1
        self.height = (height // 2) * 2 + 1
        self.start = [1, 0]
        self.destination = [self.height - 2, self.width - 1]
        self.matrix = None

    def print_matrix(self):
        for i in range(self.height):
            rowval=0
            for j in range(self.width):
                if self.matrix[i][j] == -1:
                    rowval=(rowval<<1)+1
                elif self.matrix[i][j] == 0:
                    rowval = (rowval<<1) + 0

            print(rowval,end=",")
            # print('')

    def generate_matrix_dfs(self):
        # 地图初始化,并将出口和入口处的值设置为0
        self.matrix = -np.ones((self.height, self.width))
        self.matrix[self.start[0], self.start[1]] = 0
        self.matrix[self.destination[0], self.destination[1]] = 0

        visit_flag = [[0 for i in range(self.width)] for j in range(self.height)]

        def check(row, col, row_, col_):
            temp_sum = 0
            for d in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
                temp_sum += self.matrix[row_ + d[0]][col_ + d[1]]
            return temp_sum <= -3

        def dfs(row, col):
            visit_flag[row][col] = 1
            self.matrix[row][col] = 0
            if row == self.start[0] and col == self.start[1] + 1:
                return

            directions = [[0, 2], [0, -2], [2, 0], [-2, 0]]
            random.shuffle(directions)
            for d in directions:
                row_, col_ = row + d[0], col + d[1]
                if row_ > 0 and row_ < self.height - 1 and col_ > 0 and col_ < self.width - 1 and visit_flag[row_][
                    col_] == 0 and check(row, col, row_, col_):
                    if row == row_:
                        visit_flag[row][min(col, col_) + 1] = 1
                        self.matrix[row][min(col, col_) + 1] = 0
                    else:
                        visit_flag[min(row, row_) + 1][col] = 1
                        self.matrix[min(row, row_) + 1][col] = 0
                    dfs(row_, col_)

        dfs(self.destination[0], self.destination[1] - 1)
        self.matrix[self.start[0], self.start[1] + 1] = 0


# 这里的长和宽设置的是50,但是实际生成的迷宫长宽会是51
maze = Maze(15, 27)
maze.generate_matrix_dfs()
maze.print_matrix()

 

一口气生成了5个迷宫数据,用头文件方式保存迷宫数据,每次启动游戏,随机选择一组迷宫数据进行游戏。

// =========================================
// 迷宫数据 — 由 maze_prim.py 自动生成
// =========================================
#ifndef MAZE_DATA_H
#define MAZE_DATA_H

#include <stdint.h>

#define MAZE_COUNT 5

static const uint16_t maze_library[MAZE_COUNT][27] = {
    { // === Maze #1 ===
        32767, 4161, 21981, 21509, 22525, 17413, 24053, 20737, 22525, 21525, 21973, 20817, 24415, 17473, 30205, 20741, 23925, 17745, 21853, 20805, 24405, 16725, 24405, 20561, 22527, 20480, 32767
    },
    { // === Maze #2 ===
        32767,4161,21973,17429,24565,16645,32637,16453,22517,21521,24031,17473,30589,17681,21877,20805,24413,20805,21877,17477,24541,16657,32607,16465,24565,16388,32767
    },
    { // === Maze #3 ===
        32767,4097,22015,21505,22517,17685,23893,20821,23901,17729,30077,20801,23933,16645,24565,20549,23903,17729,30589,17413,23933,17729,30173,21573,22391,16400,32767
    },
    { // === Maze #4 ===
       32767,257,30557,17477,30197,17685,23925,16709,24413,17493,30037,17745,24023,20561,24413,16465,32599,16465,24541,20549,22005,21525,24535,20497,22493,17412,32767
    },
    { // === Maze #5 ===
       32767,1,30685,16661,24437,20561,22487,21569,21887,20753,24533,17477,32117,17685,21981,21761,22013,21573,22357,20565,24533,20753,22015,17409,24573,16388,32767
    },
};

#endif

有了迷宫,再定一下游戏规则。小球依赖重力移动,向着低的地方滚动,如果移动方向上有障碍物则无法移动。游戏开始,小球的位置和目标的位置都固定,小球在屏幕的左下方,目标在屏幕的右上方。小球沿迷宫运动,直到与目标位置重叠,就胜利,游戏结束。

FlZy6kNgcuFGbii8sZHs9_3fJjDn

整个游戏控制就是靠手去移动板子,通过IMU感知与水平面的角度,来移动小球。

   // 读取IMU加速度数据
    M5.Imu.getAccelData(&accX, &accY, &accZ);

    // ---- 死区过滤:微小噪声不触发移动 ----
    float ax = (fabs(accX) > IMU_DEADZONE) ? accX : 0;
    float ay = (fabs(accY) > IMU_DEADZONE) ? accY : 0;

    // ---- 累加器:IMU读数 → 累加到 pending 中 ----
    // IMU_SENS 控制灵敏度,MAX_SPEED 限制最大累加量
    pending_r += (int)(ax * IMU_SENS);
    pending_c -= (int)(ay * IMU_SENS);
    pending_r = constrain(pending_r, -MAX_SPEED, MAX_SPEED);
    pending_c = constrain(pending_c, -MAX_SPEED, MAX_SPEED);

    // ---- 每帧只走1步(每轴),配合累加器实现平滑加速/减速 ----
    // 先走行方向,再走列方向(支持同时对角线移动)
    if (pending_r > 0)
    {
        tryMoveOneStep(1, 0);
        pending_r--;
    }
    else if (pending_r < 0)
    {
        tryMoveOneStep(-1, 0);
        pending_r++;
    }

    if (pending_c < 0)
    {
        tryMoveOneStep(0, -1);
        pending_c++;
    }
    else if (pending_c > 0)
    {
        tryMoveOneStep(0, 1);
        pending_c--;
    }

为了增加游戏的乐趣,让AI帮忙添加了背景音乐,如果不喜欢,也可以关闭音乐。

// ============ 音符频率 ============
#define REST 0
#define NOTE_C4 262
#define NOTE_D4 294
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_G4 392
#define NOTE_A4 440
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_D5 587
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_G5 784
#define NOTE_A5 880
#define NOTE_B5 988
#define NOTE_C6 1047

// 背景旋律:{频率, 时长ms},频率0=休止符
static const uint16_t bgm_song[][2] = {
    {NOTE_E4, 180}, {NOTE_G4, 180}, {NOTE_A4, 180}, {NOTE_G4, 180},
    {NOTE_E4, 180}, {NOTE_G4, 180}, {NOTE_A4, 180}, {NOTE_G4, 180},
    {NOTE_E4, 180}, {NOTE_G4, 180}, {NOTE_A4, 360}, {NOTE_G4, 180},
    {NOTE_E4, 180}, {NOTE_G4, 180}, {NOTE_A4, 180}, {NOTE_G4, 180},
    {NOTE_D4, 180}, {NOTE_G4, 180}, {NOTE_A4, 180}, {NOTE_G4, 180},
    {NOTE_D4, 180}, {NOTE_G4, 180}, {NOTE_A4, 180}, {NOTE_G4, 180},
    {NOTE_D4, 180}, {NOTE_G4, 180}, {NOTE_A4, 360}, {NOTE_G4, 180},
    {NOTE_D4, 180}, {NOTE_G4, 180}, {NOTE_A4, 180}, {NOTE_G4, 180},
    {NOTE_C4, 180}, {NOTE_E4, 180}, {NOTE_G4, 180}, {NOTE_E4, 180},
    {NOTE_C4, 180}, {NOTE_E4, 180}, {NOTE_G4, 180}, {NOTE_E4, 180},
    {NOTE_C4, 180}, {NOTE_E4, 180}, {NOTE_G4, 360}, {NOTE_E4, 180},
    {NOTE_C4, 180}, {NOTE_E4, 180}, {NOTE_G4, 180}, {NOTE_E4, 180},
    {REST, 120},
};
#define BGM_LEN (sizeof(bgm_song) / sizeof(bgm_song[0]))

四、效果演示

游戏过程中,手要稳,心态要平和。急躁不得。图中黄色的是需要移动的小球,红色是目的地。蓝色线是可以行走的通道。一共有5张迷宫地图,按复位键可以随机选择。按中间的按键可以关闭背景音乐。

超解压的弹珠迷宫游戏_image_1.webp
超解压的弹珠迷宫游戏_image_2.webp

五、源码

见附件

硬件清单

  • Stick S31

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -