(二十一)运行、复盘与自动检查
1. 从开始画面玩完一局
完整目录已经组合好:
dodge_project/
├── pyproject.toml、uv.lock、requirements-dev.txt
├── src/dodgegame/
│ ├── config.py、geometry.py、models.py、stepping.py
│ ├── assets.py、sprites.py、view.py、controller.py
│ ├── persistence.py、app.py、__init__.py、__main__.py
│ └── assets/ player.png、obstacle.png、score.wav、hit.wav
├── tests/
└── tools/ 素材生成与状态画面核对
# 在 dodge_project 中启动,Enter开始,A/D或方向键移动,R重开,Esc退出。
uv run dodge-game
# 用相同障碍序列复盘,并对照30与144次显示的操作感受。
uv run dodge-game --seed 7 --fps 30
uv run dodge-game --seed 7 --fps 144 --mute
按 P 暂停后,障碍、生成和分数停住,按钮与退出继续响应;恢复后从原位置继续。碰撞进入结束画面,按 R 会先清空本局再开始,而不是把挡在玩家面前的旧障碍继续留着。
2. 用相同输入核对状态
状态机、几何判断与逻辑时间可以先在没有屏幕的环境检查。显示和声音相关测试则在导入 pygame 之前选虚拟设备:
# 项目文件:dodge_project/tests/conftest.py
# 导入pygame之前选择虚拟显示和声音设备,测试不弹窗口或播放实际音量。
import os
os.environ["SDL_VIDEODRIVER"] = "dummy"
os.environ["SDL_AUDIODRIVER"] = "dummy"
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
import pygame
import pytest
@pytest.fixture
def screen():
pygame.display.init()
pygame.font.init()
display = pygame.display.set_mode((800, 600))
yield display
pygame.quit()这份 fixture 为需要显示的测试建立独立 Surface,用完后关闭模块。普通 World 与几何测试不请求 screen,也不建立显示。
# 项目文件:dodge_project/tests/test_timing.py
# 相同输入在不同显示分帧中推进同一秒,比较完整逻辑状态而不是只看画面。
import pytest
from dodgegame.models import Mode, World
from dodgegame.stepping import Stepper
def snapshot(fps):
world, stepper = World(seed=7), Stepper()
world.start()
for _ in range(fps):
stepper.advance(world, 1 / fps, 0.5)
return world
@pytest.mark.parametrize("fps", [30, 60, 144])
def test_one_second_has_same_state(fps):
baseline, actual = snapshot(120), snapshot(fps)
assert actual.player.x == pytest.approx(536)
assert actual.elapsed == pytest.approx(1)
assert actual.score == baseline.score
assert actual.obstacles == baseline.obstacles
def test_pause_drops_fraction_and_large_gap_opens_pause():
world, stepper = World(seed=7), Stepper()
world.start()
stepper.advance(world, 0.003, 1)
world.toggle_pause()
assert stepper.advance(world, 20, 1) == []
assert stepper.accumulator == 0 and world.elapsed == 0
world.toggle_pause()
assert stepper.advance(world, 0.8, 1) == ["lag_pause"]
assert world.mode == Mode.PAUSED and world.elapsed == 0固定输入的一秒更新后,除了 x=536,还比较整份障碍数据。暂停测试则核对逻辑余量清空、长时间返回后进入暂停,以及位置和计时没有突然补走。
# 核对计分只发生一次、途中碰撞、暂停重开、输入事件和文件保存。
uv run python -m pytest
配套检查还会让障碍在一步中穿过玩家并离屏,核对结果是结束且不加分;再注入退出事件走完实际入口,确认显示、字体和声音全部关闭。输入与预期一并留下,改动后就能重新比较第一次状态变化。
3. 根据实际状态定位画面问题
F1 画出游戏正在使用的矩形;位置不一致时比较模型的浮点 x、精灵 Rect 和资源宽高。运动快慢不对时先核对速度单位、tick 返回的毫秒和除以 1000 的位置;重开后突然又结束时,再检查障碍列表、生成计时与精灵引用是否都连接到了新一局。
游戏逻辑检查之外,配套还用窗口后端实际绘制并核对开始、进行、暂停、结束与重开五幅画面。界面与逻辑分别检查,才能同时发现“分数算对了但画面还显示旧值”或“图片跟着动但碰撞仍在旧位置”。