| 现象 | 根因 | 修法 |
|---|---|---|
| 人物卡进墙里 | 走横向时会把角色向行中心"吸附"以便穿缝,但这个吸附没做碰撞检测,会把人挤进旁边的墙 | 吸附前检查迎面那条边;另加 _unstick() 兜底,中心真进了实心格就推回最近空格 |
| 放下糖泡就自杀(探险模式最明显) | 从自己的糖泡上走下来后,身后半个身子还压在糖泡格里(此时糖泡已对他变实心)。原本要求整个包围盒无碰撞才允许移动,于是人被自己的糖泡卡死在原地,然后被自己炸死 | 移动只检查迎面那条边,不看整个包围盒。顺带 AI 自杀率从 ~25% 降到 ~5% —— 机器人也一直被同一个 bug 卡着 |
| 出生点被糖果箱围死 | 探险地图中心出生点四邻全是箱子,1 级威力放泡覆盖自己脚下,无处可躲 | 地图重排;并给所有地图加保险:出生点 3×3 内的可炸物一律清空,附带一个自动校验(每个出生点放泡后必须存在逃生路线) |
| 文字不在同一水平线上 | 控件标签用 topleft 锚点却传了 centery 的 y,数值用的是垂直居中锚点,一高一低 |
text() 增加 midleft 锚点,标签与数值统一垂直居中 |
| 音量只能变大 | 鼠标点击和回车都只走 shift(+1),没有"减小"的入口 |
滑条改成点哪儿就是哪儿、可拖动、滚轮可调;回车在满值时回到最小值 |
| 左右切换显示成方框 | 用了 ◀ ▶ 字符,很多中文字体没有这两个字形 |
改成 pygame.draw.polygon 手绘三角;成就的 ★ ☆ 同样改为手绘 |
| 界面被切掉(顶栏/底部玩家卡看不全) | 固定 1152×720 窗口,在小屏或 125%/150% DPI 缩放的机器上放不进桌面 | Windows 下声明 DPI 感知;改成固定画布 + 等比缩放架构,窗口可随意拖动大小、可全屏,鼠标坐标自动换算 |
| 切换界面语言后中英混排 | 字体缓存没清,且控件文字在创建时就定死了 | init_fonts() 清缓存,切语言后重建界面 |
一款用 Python + pygame 写的休闲竞技游戏,按前一份《QQ堂》产品分析文档里的设计结论逐条落地, 可用 PyInstaller 打包成单文件 exe(双击即玩,不需要装 Python)。
零素材依赖:所有美术都是 pygame 图元手绘,所有音效与背景音乐都是 numpy 实时合成的, 所以打包产物就是一个自包含的可执行文件,没有任何 assets 目录。
- 装 Python 3.9~3.12(安装时勾选 Add Python to PATH)
- 双击
build.bat
脚本会依次做:装依赖 → 跑自检 → PyInstaller 打包,产物在 dist\SugarBubble.exe(约 3545 MB)。
单文件 exe 首次启动需要 38 秒解包,属正常现象;想改中文名直接重命名即可。
想先跑起来看看而不打包:双击 run.bat。
中文版打包说明与常见问题(杀软误报、pip 换源、崩溃日志位置)见 build-readme-cn.txt。
build.bat/run.bat是纯 ASCII + CRLF 的,请勿在里面加中文注释。 cmd.exe 按系统 ANSI 代码页(GBK)读 .bat,UTF-8 中文注释解码错位后会夹出&这类分隔符,把注释截成半截命令执行,报'xxx' 不是内部或外部命令。
bash build.sh # 产物 dist/sugarbubble打包链路在本机(Linux / Python 3.12 / pygame 2.6.1 / PyInstaller 6.x)已完整跑通,
产物 34 MB,冻结后的可执行文件 --selftest 通过。Windows 侧用同一份 spec。
spec 里两条注释别删:
- 不要把
xml/email/http放进 excludes ——pkg_resources的运行时 hook 会用到, 排掉会在启动瞬间崩(ModuleNotFoundError: No module named 'xml')。 upx=False—— UPX 压缩过的单文件 exe 被 360 / 火绒 / Defender 误报的概率明显更高。
命令行开关:--selftest 无窗口跑一遍全部模式并退出(打包后验证用)、--version。
| 操作 | 1P | 2P(同一台机器) |
|---|---|---|
| 移动 | 方向键 | W A S D |
| 放糖泡 | 空格 | F |
| 飞镖引爆 | 左 Ctrl | G |
| 叉子自救 | 左 Shift | R |
暂停 Esc · 帮助 F1 · 全屏 F11 · 回放变速 Tab
分析文档里的第一条结论是:一个反转型死亡机制,胜过十个数值系统。所以整套代码是围绕这条写的。
糖泡 2.6s 引信 → 十字爆炸(半径=威力) → 生成糖浆(0.75s)
碰到糖浆 → 被困(不能动,4.2s 后出局)
├─ 队友接触 0.25s → 救出(+营救计数)
├─ 敌人接触 → 立刻出局(补刀)
└─ 自己有叉子 → 自救
全队出局 = 本局输
一次爆炸同时创造了一个救援机会和一个补刀机会 —— 这是原作最高频的情绪来源, 也是 AI 决策优先级里"营救队友"排在"打人"之前的原因。
实测 4 个高手 AI 混战,约 63% 的出局来自敌人、24% 自炸、10% 队友误伤, 说明这个博弈是真的在跑,不是摆设。
| 模式 | 胜利条件 | 关键差异 |
|---|---|---|
| 经典模式 | 团灭对手,多局决胜 | 道具全开 |
| 无道具竞技 | 团灭对手 | 固定数值、不掉道具,纯走位与卡泡 |
| 足球模式 | 限时内补刀数多者胜 | 糖泡从天而降,全员自带踢泡,可复活 |
| 抢包子 | 先运回 3 个包子 | 抢敌方包子铺 → 搬回自家大本营,被困会掉包子 |
| 探险模式 | 单人 PvE,撑波数 | 一波波糖怪,3 条命,波数越高 AI 越强 |
操作永远只有"走 + 放泡",换的只是胜利条件和场景元素 —— 这是文档里那条"最便宜的内容扩量"。
局内道具:威力 / 糖泡数 / 速度(三类基础)、叉子(自救)、飞镖(远程引爆)、 护盾(挡一次)、踢泡(撞向糖泡把它踢走)、变身(8s 满属性,被炸只变回原形)、 捣蛋(负面:威力降到 1 且减速 6s)。
地形:糖果箱(一炸开,可能掉道具)、硬糖箱(炸两次)、传送带、传送门、减速泥地、 大本营 / 包子铺。
商店只卖两类东西:皮肤(纯外观)和出场携带(开局 +1 威力 / +1 叉子这类,最多 3 格)。 糖泡威力上限、爆炸判定、糖浆时长不因任何消费改变。
这正是文档里指出的那个悖论 —— 公平换口碑、变现受限。这里保留公平设计, 把变现压力交给等级/成就/皮肤,同时用确定性录像和成就系统替代当年缺失的长线内容。
core/ai.py,决策优先级:
自救 > 逃离爆炸 > 营救队友 > 补刀被困敌人 > 模式目标(送包子) >
埋泡打人 > 拆箱开路 > 追道具 > 逼近敌人 > 随机游走
两层安全保证:
- 危险图
danger[格子] = 该格变致命的剩余时间,含连锁引爆时间松弛(3 轮迭代) - 放泡前逃生检查:算出这颗泡引发的完整爆炸区(含它会连锁引爆的其他泡), BFS 确认引信内存在可达安全格,否则不放
第 2 条里"含连锁"这句是关键。最初只算自己那颗泡的十字范围,AI 自杀率高得离谱; 把连锁引爆算进去之后才正常。
难度分 4 档(新手/普通/高手/宗师),差异在反应间隔、失误率、攻击欲、搜索深度。 失误按每秒发生率 + 冷却建模,而不是"每次重规划都掷一次骰子" —— 后者会让重规划越频繁的高难度 AI 反而死得越多。
对局是固定步长(1/60s)+ 单一随机种子的确定性模拟,AI 也只依赖种子, 所以录像只需存人类玩家每帧的输入位掩码(还做了变化点编码),一场几 KB。
自测里有确定性验证:同种子跑两遍,帧数、坐标、战绩必须逐位一致(当前 PASS)。
main.py 入口 + 场景栈(菜单/设置/商店/生涯/对局/结算/录像)
core/settings.py 全部调参常量(数值都集中在这,改平衡不用翻代码)
core/maps.py 6 张手工地图(ASCII 图例)+ 随机地图生成器 + Board
core/entities.py Player / Bubble / Syrup / Item / Bun / 粒子 / 飘字
core/match.py 对局核心:物理、爆炸链、被困判定、5 种模式规则、胜负
core/ai.py 机器人:危险图、BFS 寻路、逃生检查、行为树
core/audio.py numpy 程序化音效 + 8 小节循环 BGM(无声卡自动静音降级)
core/profile.py 存档:勇气/宝石/等级/皮肤解锁/携带/成就/生涯统计
core/replay.py 录像录制与回放
ui/widgets.py 中英字体自适应 + 按钮/滑条/轮换控件 + 菜单容器
ui/render.py 对局渲染(棋盘、角色、糖泡、糖浆、HUD、特效)
tools/smoke_test.py 无头自测:全模式跑通 + 渲染 + 确定性验证
tools/screenshot.py 把各界面渲染成 PNG
约 4500 行。存档位置:Windows %APPDATA%\SugarBubble\,其他 ~/.sugarbubble/。
界面文字全部走 W.T("中文", "English")。启动时按
微软雅黑 → 黑体 → Noto Sans CJK → 文泉驿 → PingFang 的顺序找系统字体,
并实际渲染一个汉字验证字形存在(不是只看文件在不在);
找不到就整体切英文界面,而不是满屏豆腐块。设置里也能手动锁定 English。
python tools/smoke_test.py # 全模式 × 全地图跑到分出胜负 + 渲染 + 确定性
python tools/screenshot.py # 输出各界面 PNG
python main.py --selftest # 快速冒烟(打包后也能对 exe 跑)当前 smoke_test 结果:15 个模式×地图组合全部正常分出胜负,
模拟速度 300010000 逻辑帧/秒(约 50160 倍实时),确定性 PASS。
- 没有联网对战。原作的命脉是 QQ 关系链,但网络同步(尤其是这种要求判定精确的爆炸游戏) 是另一个量级的工程。当前是本机 1~2 人 + AI。确定性模拟这套底子是为帧同步准备的, 真要做联网,从这里接 lockstep 是最省力的路径。
- 没有反外挂。文档里说外挂治理失效等于把公平设计的价值清零 —— 单机版这条不适用, 但如果要联网,服务端权威判定必须在第一版就做,不能留到后面。
- 美术是几何图元。可爱风靠配色和眼睛朝向撑,没有真正的骨骼动画。
- 音频合成在启动时约占 0.3~0.6 秒。