(二十)组织启动、循环、保存与退出
1. 最高分复用 JSON 与完整替换
分数不放进图像或文字对象中。退出时把 World.best 保存到文件,下一次启动再读取:
# 项目文件:dodge_project/src/dodgegame/persistence.py
# 最高分沿用 JSON 与完整替换:本局分数重置,最高分跨重开和进程保留。
import json
import logging
import os
from pathlib import Path
from tempfile import NamedTemporaryFile
logger = logging.getLogger(__name__)
def read_best(path: Path):
try:
value = json.loads(path.read_text(encoding="utf-8"))["best"]
if type(value) is not int or value < 0:
raise ValueError("best 应为非负整数")
return value
except FileNotFoundError:
return 0
except (ValueError, KeyError, TypeError) as error:
logger.warning("最高分文件无法读取,先从0开始:%s", error)
return 0
def write_best(path: Path, best: int):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = None
try:
with NamedTemporaryFile(
mode="w",
encoding="utf-8",
dir=path.parent,
prefix=".best-",
suffix=".tmp",
delete=False,
) as stream:
temporary = Path(stream.name)
json.dump({"best": best}, stream)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)这一部分复用第五章的方法:先写同目录临时文件,关闭后再替换目标。第一次还没有文件时从 0 开始;文件里若是布尔值、负数或损坏文本,就记录具体原因。重开仅清空本局,最高分仍留在 World,进程退出后再落盘。
2. 主循环组合已经讲过的对象
# 项目文件:dodge_project/src/dodgegame/app.py
# 启动时准备窗口与资源;每帧只处理输入、推进逻辑和绘制,退出时保存并释放。
import argparse
import logging
from pathlib import Path
import pygame
from .assets import Images, SoundBank
from .config import HEIGHT, WIDTH
from .controller import Controller
from .models import World
from .persistence import read_best, write_best
from .stepping import Stepper
from .view import Renderer
def run(fps=60, seed=None, muted=False, save_path=Path("data/best.json")):
if fps < 1:
raise ValueError("显示帧率应至少为 1")
pygame.display.init()
pygame.font.init()
world = None
try:
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Dodge the Fall - Python Notes")
world = World(seed=seed, best=read_best(save_path))
images, sounds = Images(), SoundBank(muted)
renderer = Renderer(images, world)
controller = Controller(world, Stepper(), renderer, sounds)
clock = pygame.time.Clock()
while controller.running:
seconds = clock.tick(fps) / 1000
controller.handle_events(pygame.event.get())
if not controller.running:
break
keys = pygame.key.get_pressed()
axis = int(keys[pygame.K_RIGHT] or keys[pygame.K_d]) - int(
keys[pygame.K_LEFT] or keys[pygame.K_a]
)
controller.advance(seconds, axis)
renderer.draw(
screen,
world,
sounds.muted,
controller.debug,
clock.get_fps(),
pygame.mouse.get_pos(),
)
pygame.display.flip()
finally:
try:
if world is not None:
write_best(save_path, world.best)
finally:
pygame.quit()display 与 font 先初始化,窗口建立后再载入图像。循环中依次取经过秒数、处理事件、读取持续方向、推进逻辑、绘制并 flip。方向由“右键或 D”减“左键或 A”组成,同时按相反方向得到 0。
读取到退出事件后直接结束这一帧,不再更新运动。最外层 finally 保存最高分,内层 finally 继续确保 pygame.quit,即使写入文件失败也会关闭窗口和声音模块。
3. 命令入口保留可复盘的启动参数
# 续写同一文件:dodge_project/src/dodgegame/app.py
# 命令行参数先交给 run 组织一局游戏,运行异常则记录到日志后返回失败状态。
def main(argv=None):
parser = argparse.ArgumentParser(
description="躲避障碍:A/D或方向键移动,P暂停,R重开"
)
parser.add_argument("--fps", type=int, default=60)
parser.add_argument("--seed", type=int, help="固定障碍序列,方便复盘同一局")
parser.add_argument("--mute", action="store_true")
parser.add_argument("--save", type=Path, default=Path("data/best.json"))
args = parser.parse_args(argv)
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
try:
run(args.fps, args.seed, args.mute, args.save)
except (OSError, ValueError, pygame.error) as error:
logging.getLogger(__name__).exception("游戏运行失败:%s", error)
return 1
return 0--fps 调整显示节奏,--seed 固定本轮障碍序列,--mute 静音,--save 选择最高分文件。正常导入模块不会建立窗口,只有命令入口调用 run 才开始运行。
# 项目文件:dodge_project/src/dodgegame/__init__.py
# 导入包不会建立窗口、初始化声音或写入最高分。
"""躲避障碍:输入、逻辑与绘制分层的 Pygame 练习。"""# 项目文件:dodge_project/src/dodgegame/__main__.py
# 包启动与 dodge-game 命令共用入口。
from .app import main
if __name__ == "__main__":
raise SystemExit(main())