Python 现代开发学习笔记

(二十一)运行、复盘与自动检查

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 的位置;重开后突然又结束时,再检查障碍列表、生成计时与精灵引用是否都连接到了新一局。

游戏逻辑检查之外,配套还用窗口后端实际绘制并核对开始、进行、暂停、结束与重开五幅画面。界面与逻辑分别检查,才能同时发现“分数算对了但画面还显示旧值”或“图片跟着动但碰撞仍在旧位置”。

笔记正文与全部题解 · Ctrl / ⌘ K