路径总和III:从暴力DFS到前缀和+哈希表的最优解
2026/10/10 10:28:48
""" 贪吃蛇 · 穿墙版 运行: python snake.py 依赖: pip install pygame 操作: 方向键/WASD 移动 | 空格 暂停 | R 重开 | ESC 退出 """ import os import sys import random import math import pygame # ==================== 配置 ==================== CELL = 28 COLS = 22 ROWS = 22 TOP_BAR = 70 PAD = 20 BOARD_W = COLS * CELL BOARD_H = ROWS * CELL W = BOARD_W + PAD * 2 H = TOP_BAR + BOARD_H + PAD # 高帧率 + 快速移动 FPS = 120 MOVE_INTERVAL = 5 # 方向 UP = (0, -1) DOWN = (0, 1) LEFT = (-1, 0) RIGHT = (1, 0) # 颜色 COL_TEXT = (220, 240, 255) COL_SNAKE_HEAD = (110, 240, 160) COL_SNAKE_BODY = (60, 200, 120) COL_SNAKE_DARK = (30, 140, 80) # 食物类型: (名字, 颜色, 分数, 生长) FOOD_TYPES = [ ("apple", (255, 90, 120), 10, 1), ("cherry", (255, 60, 90), 20, 1), ("gold", (255, 210, 80), 50, 1), ] # 传送门颜色 PORTAL_COLORS = { "left": (255, 180, 80), "right": (80, 200, 255), "top": (180, 120, 255), "bottom": (120, 255, 180), } # ==================== 字体 ==================== def find_font(candidates): for p in candidates: if os.path.exists(p): return p return None def get_cn_font(size): path = find_font([ "C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/simhei.ttf", "C:/Windows/Fonts/simsun.ttc", "/System/Library/Fonts/PingFang.ttc", "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", ]) if path: return pygame.font.Font(path, size) return pygame.font.Font(None, size) # ==================== 粒子 ==================== class Particle: __slots__ = ("x", "y", "vx", "vy", "life", "color", "size", "gravity") def __init__(self, x, y, color, speed=4, gravity=0.1): angle = random.random() * math.pi * 2 spd = random.random() * speed + 1 self.x = x self.y = y self.vx = math.cos(angle) * spd self.vy = math.sin(angle) * spd self.life = 1.0 self.color = color self.size = random.random() * 3 + 2 self.gravity = gravity def update(self): self.x += self.vx self.y += self.vy self.vy += self.gravity self.vx *= 0.94 self.life -= 0.04 def draw(self, surf): if self.life <= 0: return alpha = int(self.life * 255) r = max(1, int(self.size * self.life)) s = pygame.Surface((r * 2, r * 2), pygame.SRCALPHA) pygame.draw.circle(s, (*self.color, alpha), (r, r), r) surf.blit(s, (self.x - r, self.y - r)) # ==================== 飘字 ==================== class FloatText: __slots__ = ("text", "x", "y", "vy", "life", "color", "font") def __init__(self, text, x, y, color, font): self.text = text self.x = x self.y = y self.vy = -1.5 self.life = 1.0 self.color = color self.font = font def update(self): self.y += self.vy self.vy *= 0.95 self.life -= 0.03 def draw(self, surf): if self.life <= 0: return alpha = int(self.life * 255) s = self.font.render(self.text, True, self.color) s.set_alpha(alpha) rect = s.get_rect(center=(self.x, self.y)) surf.blit(s, rect) # ==================== 传送门 ==================== class Portal: __slots__ = ("x", "y", "life", "color", "is_entry") def __init__(self, x, y, color, is_entry): self.x = x self.y = y self.life = 1.0 self.color = color self.is_entry = is_entry def update(self): self.life -= 0.055 def draw(self, surf): if self.life <= 0: return # 扩散圆环 progress = 1.0 - self.life alpha = int(self.life * 200) r = int(CELL * 0.3 + progress * CELL * 0.8) if r < 2: return s = pygame.Surface((r * 2, r * 2), pygame.SRCALPHA) # 外环 pygame.draw.circle(s, (*self.color, alpha), (r, r), r, 3) # 内环 if r > 8: pygame.draw.circle(s, (255, 255, 255, alpha // 2), (r, r), r - 6, 2) surf.blit(s, (self.x - r, self.y - r)) # ==================== 游戏 ==================== class SnakeGame: def __init__(self): pygame.init() self.screen = pygame.display.set_mode((W, H)) pygame.display.set_caption("贪吃蛇 · 穿墙版") self.clock = pygame.time.Clock() self.font_sm = get_cn_font(14) self.font_md = get_cn_font(20) self.font_lg = get_cn_font(32) self.font_xl = get_cn_font(48) self._build_static_bg() self.reset() def _build_static_bg(self): """预渲染棋盘背景""" surf = pygame.Surface((W, H)) # 背景渐变 for y in range(H): ratio = y / H r = int(12 + 6 * ratio) g = int(16 + 8 * ratio) b = int(30 + 10 * ratio) pygame.draw.line(surf, (r, g, b), (0, y), (W, y)) # 棋盘底 + 发光边框(强调穿墙) board_rect = pygame.Rect(PAD - 2, TOP_BAR - 2, BOARD_W + 4, BOARD_H + 4) pygame.draw.rect(surf, (22, 28, 48), board_rect, border_radius=14) pygame.draw.rect(surf, (80, 160, 255), board_rect, 3, border_radius=14) pygame.draw.rect(surf, (150, 200, 255), board_rect.inflate(-6, -6), 1, border_radius=12) # 格子 for r in range(ROWS): for c in range(COLS): gx = c * CELL + PAD gy = r * CELL + TOP_BAR color = (26, 32, 52) if (c + r) % 2 == 0 else (30, 36, 58) pygame.draw.rect(surf, color, (gx, gy, CELL, CELL)) self._static_bg = surf # ---------- 初始化 ---------- def reset(self): cx, cy = COLS // 2, ROWS // 2 self.snake = [(cx, cy), (cx - 1, cy), (cx - 2, cy)] self.direction = RIGHT self.next_direction = RIGHT self.food = None self.food_type = 0 self.score = 0 self.best_score = getattr(self, 'best_score', 0) self.game_over = False self.paused = False self.move_timer = 0 self.particles = [] self.floats = [] self.portals = [] self.grow_pending = 0 self.frame = 0 self.speed_level = 1 self.wrap_count = 0 self.spawn_food() def spawn_food(self): """在空白格随机生成食物""" empty = [] for r in range(ROWS): for c in range(COLS): if (c, r) not in self.snake: empty.append((c, r)) if not empty: return self.food = random.choice(empty) r = random.random() if r < 0.08: self.food_type = 2 elif r < 0.30: self.food_type = 1 else: self.food_type = 0 # ---------- 输入 ---------- def change_direction(self, new_dir): """立即改变方向(更灵敏)""" # 不能 180° 掉头 if (new_dir[0] + self.direction[0], new_dir[1] + self.direction[1]) == (0, 0): return self.direction = new_dir self.next_direction = new_dir # ---------- 更新 ---------- def update(self): self.frame += 1 # 特效更新(无论什么状态都跑) self._update_effects() if self.game_over or self.paused: return self.move_timer += 1 interval = max(2, MOVE_INTERVAL - (self.speed_level - 1) // 2) if self.move_timer < interval: return self.move_timer = 0 self.direction = self.next_direction hx, hy = self.snake[0] nx = hx + self.direction[0] ny = hy + self.direction[1] # ⚡ 穿墙逻辑 wrapped = False wrap_side = None if nx < 0: nx = COLS - 1 wrapped = True wrap_side = "left" elif nx >= COLS: nx = 0 wrapped = True wrap_side = "right" elif ny < 0: ny = ROWS - 1 wrapped = True wrap_side = "top" elif ny >= ROWS: ny = 0 wrapped = True wrap_side = "bottom" if wrapped: self.wrap_count += 1 self._spawn_portal(hx, hy, nx, ny, wrap_side) # 撞自己 if (nx, ny) in self.snake[:-1]: self.die() return self.snake.insert(0, (nx, ny)) # 吃食物 if (nx, ny) == self.food: name, color, pts, grow = FOOD_TYPES[self.food_type] self.score += pts self.grow_pending += grow # 粒子 fx = nx * CELL + CELL // 2 + PAD fy = ny * CELL + CELL // 2 + TOP_BAR for _ in range(14): self.particles.append(Particle(fx, fy, color, speed=5)) # 飘字 self.floats.append(FloatText( f"+{pts}", fx, fy - 20, color, self.font_md )) # 速度提升 if self.score // 50 > self.speed_level - 1: self.speed_level += 1 self.floats.append(FloatText( "加速!", W // 2, TOP_BAR + 30, (255, 220, 100), self.font_md )) self.spawn_food() else: if self.grow_pending > 0: self.grow_pending -= 1 else: self.snake.pop() def _spawn_portal(self, from_x, from_y, to_x, to_y, side): """穿墙时在出入口各生成传送门特效""" fx = from_x * CELL + CELL // 2 + PAD fy = from_y * CELL + CELL // 2 + TOP_BAR tx = to_x * CELL + CELL // 2 + PAD ty = to_y * CELL + CELL // 2 + TOP_BAR color = PORTAL_COLORS.get(side, (255, 255, 255)) self.portals.append(Portal(fx, fy, color, True)) self.portals.append(Portal(tx, ty, color, False)) # 粒子爆发 for _ in range(10): self.particles.append(Particle(fx, fy, color, speed=5, gravity=0)) self.particles.append(Particle(tx, ty, color, speed=5, gravity=0)) def _update_effects(self): for p in self.particles[:]: p.update() if p.life <= 0: self.particles.remove(p) for f in self.floats[:]: f.update() if f.life <= 0: self.floats.remove(f) for po in self.portals[:]: po.update() if po.life <= 0: self.portals.remove(po) def die(self): self.game_over = True if self.score > self.best_score: self.best_score = self.score for (sx, sy) in self.snake: px = sx * CELL + CELL // 2 + PAD py = sy * CELL + CELL // 2 + TOP_BAR for _ in range(4): self.particles.append(Particle( px, py, random.choice([COL_SNAKE_HEAD, COL_SNAKE_BODY, COL_SNAKE_DARK]), speed=5 )) # ---------- 绘制 ---------- def draw(self): self.screen.blit(self._static_bg, (0, 0)) # 传送门 for po in self.portals: po.draw(self.screen) # 食物 if self.food and not self.game_over: self._draw_food() # 蛇 self._draw_snake() # 粒子 for p in self.particles: p.draw(self.screen) # 飘字 for f in self.floats: f.draw(self.screen) # UI self._draw_ui() if self.paused and not self.game_over: self._draw_center_text("已暂停", "按空格继续") if self.game_over: self._draw_center_text("游戏结束", f"最终得分: {self.score}") def _draw_food(self): name, color, pts, grow = FOOD_TYPES[self.food_type] fx = self.food[0] * CELL + CELL // 2 + PAD fy = self.food[1] * CELL + CELL // 2 + TOP_BAR pulse = 1.0 + 0.12 * math.sin(self.frame * 0.15) r = int(CELL * 0.35 * pulse) # 光晕 glow_r = r + 10 glow = pygame.Surface((glow_r * 2, glow_r * 2), pygame.SRCALPHA) pygame.draw.circle(glow, (*color, 70), (glow_r, glow_r), glow_r) self.screen.blit(glow, (fx - glow_r, fy - glow_r)) # 主体 pygame.draw.circle(self.screen, color, (int(fx), int(fy)), r) # 高光 pygame.draw.circle(self.screen, (255, 255, 255), (int(fx - r * 0.3), int(fy - r * 0.3)), max(1, r // 3)) # 叶子 pygame.draw.circle(self.screen, (120, 220, 120), (int(fx), int(fy - r - 2)), 4) # 金星环绕 if self.food_type == 2: for i in range(4): ang = self.frame * 0.08 + i * math.pi / 2 star_x = fx + math.cos(ang) * (r + 8) star_y = fy + math.sin(ang) * (r + 8) pygame.draw.circle(self.screen, (255, 240, 150), (int(star_x), int(star_y)), 3) def _draw_snake(self): n = len(self.snake) for i, (sx, sy) in enumerate(self.snake): px = sx * CELL + PAD py = sy * CELL + TOP_BAR if i == 0: # 蛇头光晕 glow = pygame.Surface((CELL + 12, CELL + 12), pygame.SRCALPHA) pygame.draw.circle(glow, (110, 240, 160, 70), (CELL // 2 + 6, CELL // 2 + 6), CELL // 2 + 6) self.screen.blit(glow, (px - 6, py - 6)) # 头部 rect = pygame.Rect(px + 1, py + 1, CELL - 2, CELL - 2) pygame.draw.rect(self.screen, COL_SNAKE_HEAD, rect, border_radius=8) pygame.draw.rect(self.screen, (200, 255, 220), rect, 2, border_radius=8) # 眼睛 eye_size = 3 cx = px + CELL // 2 cy = py + CELL // 2 dx, dy = self.direction if dx != 0: ex1 = cx + dx * 4 ey1 = cy - 5 ey2 = cy + 5 pygame.draw.circle(self.screen, (20, 20, 30), (ex1, ey1), eye_size) pygame.draw.circle(self.screen, (20, 20, 30), (ex1, ey2), eye_size) else: ex1 = cx - 5 ex2 = cx + 5 ey = cy + dy * 4 pygame.draw.circle(self.screen, (20, 20, 30), (ex1, ey), eye_size) pygame.draw.circle(self.screen, (20, 20, 30), (ex2, ey), eye_size) else: # 身体渐变 ratio = i / n r = int(60 + 30 * (1 - ratio)) g = int(200 - 40 * ratio) b = int(120 - 30 * ratio) rect = pygame.Rect(px + 2, py + 2, CELL - 4, CELL - 4) pygame.draw.rect(self.screen, (r, g, b), rect, border_radius=6) # 高光 hl = pygame.Surface((CELL - 8, 4), pygame.SRCALPHA) hl.fill((255, 255, 255, 60)) self.screen.blit(hl, (px + 4, py + 4)) def _draw_ui(self): # 标题 title = self.font_lg.render("贪吃蛇 · 穿墙", True, (110, 240, 160)) self.screen.blit(title, (PAD, 12)) # 分数 score_str = f"分数: {self.score}" s = self.font_md.render(score_str, True, COL_TEXT) self.screen.blit(s, (W - s.get_width() - PAD, 12)) # 最高分 best_str = f"最高: {self.best_score}" bs = self.font_sm.render(best_str, True, (180, 200, 230)) self.screen.blit(bs, (W - bs.get_width() - PAD, 42)) # 长度 len_str = f"长度: {len(self.snake)}" ls = self.font_sm.render(len_str, True, (180, 200, 230)) self.screen.blit(ls, (PAD + 150, 22)) # 速度 spd_str = f"速度: {self.speed_level}" ss = self.font_sm.render(spd_str, True, (255, 200, 100)) self.screen.blit(ss, (PAD + 150, 44)) # 穿墙次数 wrap_str = f"穿墙: {self.wrap_count}" ws = self.font_sm.render(wrap_str, True, (150, 220, 255)) self.screen.blit(ws, (PAD + 270, 22)) # FPS fps_str = f"FPS: {int(self.clock.get_fps())}" fs = self.font_sm.render(fps_str, True, (120, 140, 180)) self.screen.blit(fs, (PAD + 270, 44)) def _draw_center_text(self, title_text, sub_text): overlay = pygame.Surface((W, H), pygame.SRCALPHA) overlay.fill((0, 0, 0, 160)) self.screen.blit(overlay, (0, 0)) title = self.font_xl.render(title_text, True, (255, 100, 130)) self.screen.blit(title, (W // 2 - title.get_width() // 2, H // 2 - 80)) sub = self.font_md.render(sub_text, True, (200, 220, 255)) self.screen.blit(sub, (W // 2 - sub.get_width() // 2, H // 2)) hint = self.font_sm.render("按 R 重新开始 | ESC 退出", True, (150, 170, 200)) self.screen.blit(hint, (W // 2 - hint.get_width() // 2, H // 2 + 60)) # ---------- 主循环 ---------- def run(self): running = True while running: for e in pygame.event.get(): if e.type == pygame.QUIT: running = False elif e.type == pygame.KEYDOWN: if e.key == pygame.K_ESCAPE: running = False elif e.key == pygame.K_r: self.reset() elif e.key == pygame.K_SPACE: if not self.game_over: self.paused = not self.paused elif e.key in (pygame.K_UP, pygame.K_w): self.change_direction(UP) elif e.key in (pygame.K_DOWN, pygame.K_s): self.change_direction(DOWN) elif e.key in (pygame.K_LEFT, pygame.K_a): self.change_direction(LEFT) elif e.key in (pygame.K_RIGHT, pygame.K_d): self.change_direction(RIGHT) self.update() self.draw() pygame.display.flip() self.clock.tick(FPS) pygame.quit() sys.exit() if __name__ == "__main__": print("=" * 50) print(" 贪吃蛇 · 穿墙版") print("=" * 50) print(" 方向键 / WASD 控制移动(可穿墙!)") print(" 空格 暂停 | R 重开 | ESC 退出") print("=" * 50) SnakeGame().run()