# 无头测试:桩替代 pygame,验证 snake 游戏核心逻辑 import sys, types, os, shutil, tempfile # ---------------- pygame 桩 ---------------- class Surf: def fill(self, *a, **k): pass def blit(self, *a, **k): pass class Font: def __init__(self, *a, **k): pass def render(self, text, aa, color): s = Surf() s.get_width = lambda: 10 * len(str(text)) s.get_height = lambda: 20 return s class Rect: def __init__(self, x, y, w, h, *a, **k): self.x, self.y, self.width, self.height = x, y, w, h @property def right(self): return self.x + self.width class Clock: def tick(self, *a): return 0 def get_time(self): return 16 fake = types.ModuleType("pygame") fake.init = lambda: None fake.quit = lambda: None fake.joystick = types.SimpleNamespace(init=lambda: None, Joystick=lambda i: None, get_count=lambda: 0) fake.display = types.SimpleNamespace(set_mode=lambda s: Surf(), set_caption=lambda s: None, flip=lambda: None) fake.font = types.SimpleNamespace(Font=Font) fake.Rect = Rect fake.draw = types.SimpleNamespace(rect=lambda *a, **k: None, line=lambda *a, **k: None) fake.time = types.SimpleNamespace(Clock=Clock) for name, val in dict(K_ESCAPE=27, K_UP=273, K_DOWN=274, K_LEFT=276, K_RIGHT=275, K_w=119, K_s=115, K_a=97, K_d=100, K_RETURN=13, K_SPACE=32, QUIT=256, KEYDOWN=768, JOYBUTTONDOWN=769, JOYHATMOTION=770, JOYAXISMOTION=771).items(): setattr(fake, name, val) sys.modules["pygame"] = fake sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "games", "snake")) import importlib.util spec = importlib.util.spec_from_file_location( "snake_main", os.path.join(os.path.dirname(os.path.abspath(__file__)), "games", "snake", "main.py")) m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) # 把存档指到临时目录,避免污染 tmp = tempfile.mkdtemp() m.SAVE_DIR = tmp m.SAVE_FILE = os.path.join(tmp, "snake_best.txt") g = m.SnakeGame() # 1. 初始为菜单态 assert g.state == "menu" g.confirm() assert g.state == "play" assert len(g.snake) == 3 print("1. 菜单→开始 OK") # 2. 移动两步,蛇头前进 head0 = g.snake[0] g.step() assert g.snake[0] == (head0[0] + 1, head0[1]), g.snake[0] assert len(g.snake) == 3 print("2. 移动 OK") # 3. 禁止 180° 掉头 g.push_dir((-1, 0)) g.step() assert g.dir == (1, 0), g.dir print("3. 禁止掉头 OK") # 4. 吃到苹果:加分 + 变长 g.apple = (g.snake[0][0] + 1, g.snake[0][1]) score0, len0 = g.score, len(g.snake) g.step() assert g.score == score0 + 1 and len(g.snake) == len0 + 1 print("4. 吃苹果 OK") # 5. 撞墙 → 结束并保存最高分 g.snake = [(m.COLS - 1, 5), (m.COLS - 2, 5), (m.COLS - 3, 5)] g.dir, g.pending = (1, 0), [] g.score = 12 g.step() assert g.state == "over" and g.best == 12 assert open(m.SAVE_FILE).read() == "12" print("5. 撞墙/最高分保存 OK") # 6. 重新开始 g.confirm() assert g.state == "play" and g.score == 0 and len(g.snake) == 3 print("6. 重开 OK") # 7. 存档路径在仓库外(约定 games/_saves) m.SAVE_DIR = os.path.normpath(os.path.join( os.path.dirname(os.path.abspath(__file__)), "games", "snake", "..", "_saves")) assert "_saves" in m.SAVE_DIR and "snake" not in m.SAVE_DIR.replace("_saves", "", 1).rstrip(os.sep).split(os.sep)[-1] print("7. 存档位于仓库外 OK") shutil.rmtree(tmp, ignore_errors=True) print("\n游戏逻辑全部测试通过 ✔")