热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
本提示词专为Python游戏开发设计,能够根据用户需求生成结构完整、可运行的PyGame游戏代码。通过智能分析游戏类型、核心玩法和视觉风格等关键要素,提供从基础框架到完整功能的代码实现。提示词采用分步推理机制,确保代码逻辑严谨、注释清晰,支持多种游戏类型的开发需求,包括但不限于射击游戏、平台跳跃、益智解谜等。特别适合编程初学者学习游戏开发基础,也可帮助有经验的开发者快速实现原型设计。输出代码包含完整的游戏循环、事件处理、碰撞检测等核心模块,并遵循PyGame最佳实践规范。
# 完整的PyGame平台跳跃游戏代码(像素风)
# 需求特性:
# - 方向键移动与跳跃
# - 收集星星
# - 敌人简单巡逻
# - 主循环内处理键盘事件与矩形碰撞
# - 关卡结束显示得分与简单报告提示
# - 像素风格(低分辨率渲染到高分辨率窗口)
import pygame
import sys
from pygame.math import Vector2
# ----------------------------
# 基本设置(像素风低分辨率 + 整数放大)
# ----------------------------
pygame.init()
VIRTUAL_W, VIRTUAL_H = 320, 180 # 虚拟渲染分辨率(像素风)
SCALE = 3 # 窗口缩放倍数(整数,保证像素清晰)
WIN_W, WIN_H = VIRTUAL_W * SCALE, VIRTUAL_H * SCALE
TILE = 16 # 瓦片大小(像素)
FPS = 60
# 颜色定义(简洁像素调色)
COL_BG = (25, 25, 35) # 背景深蓝灰
COL_PLATFORM = (60, 60, 80) # 平台块深灰
COL_PLAYER = (90, 200, 255) # 玩家青蓝
COL_PLAYER_OUTLINE = (20, 40, 55)
COL_ENEMY = (240, 90, 90) # 敌人桃红
COL_STAR = (255, 220, 90) # 星星黄
COL_STAR_GLOW = (255, 255, 180)
COL_TEXT = (240, 240, 240) # 文本
COL_DOOR_LOCK = (120, 120, 160)
COL_DOOR_OPEN = (120, 200, 120)
# ----------------------------
# 关卡数据(20列 x 11行)
# 字符说明:
# . 空 | X 平台 | S 星星 | E 敌人 | P 玩家出生点 | D 出口
# ----------------------------
LEVEL_DATA = [
"....................",
"...S....XXX....S....",
"...X.........E......",
"...........XXX......",
"..XXX.............S.",
"...........S........",
".P....XXX...........",
"XXXX.........XXXX...",
".....S....E.........",
".................D..",
"XXXXXXXXXXXXXXXXXXXX",
]
ROWS = len(LEVEL_DATA)
COLS = len(LEVEL_DATA[0])
# ----------------------------
# 工具函数:创建简单像素块贴图
# ----------------------------
def make_block_surface(w, h, color, outline=None):
surf = pygame.Surface((w, h), pygame.SRCALPHA)
surf.fill(color)
if outline:
pygame.draw.rect(surf, outline, surf.get_rect(), 1)
return surf
def clamp(v, lo, hi):
return max(lo, min(hi, v))
# ----------------------------
# 精灵类定义
# ----------------------------
class Platform(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = make_block_surface(TILE, TILE, COL_PLATFORM)
self.rect = self.image.get_rect(topleft=(x, y))
class Star(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
# 星星是小块,加轻微发光边
self.base = make_block_surface(TILE, TILE, (0, 0, 0, 0))
pygame.draw.rect(self.base, COL_STAR, pygame.Rect(5, 5, 6, 6))
pygame.draw.rect(self.base, COL_STAR_GLOW, pygame.Rect(4, 4, 8, 8), 1)
self.image = self.base.copy()
self.rect = self.image.get_rect(topleft=(x, y))
self.t = 0.0
def update(self, dt):
# 简单闪烁动画
self.t += dt
pulse = (pygame.math.sin(self.t * 6.0) + 1.0) * 0.5 # 0..1
# 通过Alpha实现闪烁
self.image = self.base.copy()
self.image.set_alpha(180 + int(75 * pulse))
class Enemy(pygame.sprite.Sprite):
def __init__(self, x, y, solids_map):
super().__init__()
# 敌人是16x12的小矩形,带描边
self.image = make_block_surface(TILE, TILE - 4, COL_ENEMY, outline=(80, 20, 20))
self.rect = self.image.get_rect(midbottom=(x + TILE // 2, y + TILE))
self.pos = Vector2(self.rect.x, self.rect.y)
self.vel = Vector2(40.0, 0.0) # 初速度向右
self.solids_map = solids_map
self.gravity = 900.0
self.speed = 40.0
def is_solid_at(self, px, py):
tx = int(px // TILE)
ty = int(py // TILE)
if 0 <= tx < COLS and 0 <= ty < ROWS:
return self.solids_map[ty][tx]
return False
def update(self, dt, platforms):
# 重力
self.vel.y += self.gravity * dt
# 水平巡逻:前方阻挡或前方地面缺失则反向
dir_sign = 1 if self.vel.x >= 0 else -1
ahead_x = self.rect.centerx + dir_sign * (self.rect.width // 2 + 1)
foot_y = self.rect.bottom + 1
# 检查前方是否墙
if self.is_solid_at(ahead_x, self.rect.centery) or self.is_solid_at(ahead_x, self.rect.top + 1):
self.vel.x *= -1
else:
# 检查前方脚下是否有地面
if not self.is_solid_at(ahead_x, foot_y):
self.vel.x *= -1
# 应用水平位移
self.pos.x += self.vel.x * dt
self.rect.x = int(self.pos.x)
# 水平方向碰撞(防止嵌入墙体)
hits = pygame.sprite.spritecollide(self, platforms, False)
for p in hits:
if self.vel.x > 0:
self.rect.right = p.rect.left
elif self.vel.x < 0:
self.rect.left = p.rect.right
self.pos.x = self.rect.x
self.vel.x *= -1 # 撞墙反向
# 竖直方向
self.pos.y += self.vel.y * dt
self.rect.y = int(self.pos.y)
hits = pygame.sprite.spritecollide(self, platforms, False)
for p in hits:
if self.vel.y > 0:
self.rect.bottom = p.rect.top
elif self.vel.y < 0:
self.rect.top = p.rect.bottom
self.pos.y = self.rect.y
self.vel.y = 0.0
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
# 玩家:14x16,有外描边
self.image = make_block_surface(14, 16, COL_PLAYER, outline=COL_PLAYER_OUTLINE)
self.rect = self.image.get_rect(midbottom=(x + TILE // 2, y + TILE))
self.pos = Vector2(self.rect.x, self.rect.y)
self.vel = Vector2(0.0, 0.0)
self.move_speed = 110.0
self.gravity = 900.0
self.jump_speed = 270.0
self.on_ground = False
# 跳跃辅助(容错)
self.coyote_time = 0.1
self.coyote_timer = 0.0
self.jump_buffer_time = 0.12
self.jump_buffer_timer = 0.0
def handle_input(self, dt):
keys = pygame.key.get_pressed()
left = keys[pygame.K_LEFT]
right = keys[pygame.K_RIGHT]
self.vel.x = 0.0
if left: self.vel.x -= self.move_speed
if right: self.vel.x += self.move_speed
def queue_jump(self):
self.jump_buffer_timer = self.jump_buffer_time
def update(self, dt, platforms):
# 输入
self.handle_input(dt)
# 更新计时器
if self.on_ground:
self.coyote_timer = self.coyote_time
else:
self.coyote_timer = max(0.0, self.coyote_timer - dt)
self.jump_buffer_timer = max(0.0, self.jump_buffer_timer - dt)
# 跳跃判定(缓冲 + 土狼时间)
if self.jump_buffer_timer > 0 and self.coyote_timer > 0:
self.vel.y = -self.jump_speed
self.on_ground = False
self.jump_buffer_timer = 0.0
# 重力
self.vel.y += self.gravity * dt
self.vel.y = clamp(self.vel.y, -1000, 1000)
# X轴移动 + 碰撞
self.pos.x += self.vel.x * dt
self.rect.x = int(self.pos.x)
hits = pygame.sprite.spritecollide(self, platforms, False)
for p in hits:
if self.vel.x > 0:
self.rect.right = p.rect.left
elif self.vel.x < 0:
self.rect.left = p.rect.right
self.pos.x = self.rect.x
# Y轴移动 + 碰撞
self.pos.y += self.vel.y * dt
self.rect.y = int(self.pos.y)
hits = pygame.sprite.spritecollide(self, platforms, False)
self.on_ground = False
for p in hits:
if self.vel.y > 0:
self.rect.bottom = p.rect.top
self.on_ground = True
elif self.vel.y < 0:
self.rect.top = p.rect.bottom
self.pos.y = self.rect.y
self.vel.y = 0.0
# 限制在屏幕内(虚拟坐标)
self.rect.left = max(0, self.rect.left)
self.rect.right = min(VIRTUAL_W, self.rect.right)
self.pos.x = self.rect.x
# 若掉出屏幕底部,交由Game处理复位
class Door(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.closed_img = make_block_surface(TILE, TILE*2, COL_DOOR_LOCK, outline=(50, 50, 70))
self.open_img = make_block_surface(TILE, TILE*2, COL_DOOR_OPEN, outline=(50, 70, 50))
self.image = self.closed_img
self.rect = self.image.get_rect(bottomleft=(x, y + TILE)) # 门两格高,底对齐地面
self.is_open = False
def set_open(self, open_state: bool):
self.is_open = open_state
self.image = self.open_img if self.is_open else self.closed_img
# ----------------------------
# 游戏主类
# ----------------------------
class Game:
def __init__(self):
pygame.display.set_caption("像素平台跳跃 - 收集星星")
self.screen = pygame.display.set_mode((WIN_W, WIN_H))
self.canvas = pygame.Surface((VIRTUAL_W, VIRTUAL_H))
self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont("arial", 10, bold=False)
self.running = True
self.state = "playing" # "playing" or "finished"
# 关卡与对象容器
self.platforms = pygame.sprite.Group()
self.stars = pygame.sprite.Group()
self.enemies = pygame.sprite.Group()
self.all_sprites = pygame.sprite.Group()
self.door = None
self.player = None
# 统计
self.star_total = 0
self.star_collected = 0
self.deaths = 0
self.time_elapsed = 0.0
# solids map(用于快速检测瓦片是否为实心)
self.solids_map = [[False for _ in range(COLS)] for _ in range(ROWS)]
self.load_level()
self.reset_runtime_stats()
def reset_runtime_stats(self):
self.state = "playing"
self.star_collected = 0
self.deaths = 0
self.time_elapsed = 0.0
def load_level(self):
# 清空
self.platforms.empty()
self.stars.empty()
self.enemies.empty()
self.all_sprites.empty()
self.door = None
self.player = None
# 重建solids map
for r in range(ROWS):
for c in range(COLS):
self.solids_map[r][c] = (LEVEL_DATA[r][c] == 'X')
# 解析关卡
spawn_pos = (0, 0)
door_pos = None
for r, row in enumerate(LEVEL_DATA):
for c, ch in enumerate(row):
x, y = c * TILE, r * TILE
if ch == 'X':
block = Platform(x, y)
self.platforms.add(block)
elif ch == 'S':
star = Star(x, y)
self.stars.add(star)
self.all_sprites.add(star)
elif ch == 'E':
enemy = Enemy(x, y, self.solids_map)
self.enemies.add(enemy)
self.all_sprites.add(enemy)
elif ch == 'P':
spawn_pos = (x, y)
elif ch == 'D':
door_pos = (x, y)
self.star_total = len(self.stars)
# 创建玩家与门
self.player = Player(*spawn_pos)
self.all_sprites.add(self.player)
if door_pos:
self.door = Door(*door_pos)
self.all_sprites.add(self.door)
# 平台需要在独立组中用于碰撞
# 绘制时两者都画到canvas上
def respawn_player(self):
# 找到出生点
for r, row in enumerate(LEVEL_DATA):
for c, ch in enumerate(row):
if ch == 'P':
x, y = c * TILE, r * TILE
self.player.rect.midbottom = (x + TILE // 2, y + TILE)
self.player.pos = Vector2(self.player.rect.x, self.player.rect.y)
self.player.vel = Vector2(0, 0)
return
def finish_level(self):
self.state = "finished"
def draw_text(self, surf, text, x, y, color=COL_TEXT, align="topleft"):
img = self.font.render(text, True, color)
rect = img.get_rect(**{align: (x, y)})
surf.blit(img, rect)
def update_play(self, dt):
self.time_elapsed += dt
# 处理事件(跳跃按键在事件循环里加缓冲)
# 移动输入在Player内部处理
# 更新对象
for star in self.stars:
star.update(dt)
for enemy in self.enemies:
enemy.update(dt, self.platforms)
self.player.update(dt, self.platforms)
# 掉出屏幕底部 -> 复位并计死亡
if self.player.rect.top > VIRTUAL_H + 50:
self.deaths += 1
self.respawn_player()
# 碰到敌人 -> 复位并计死亡
if pygame.sprite.spritecollide(self.player, self.enemies, False):
self.deaths += 1
self.respawn_player()
# 收集星星
hits = pygame.sprite.spritecollide(self.player, self.stars, dokill=True)
if hits:
self.star_collected += len(hits)
# 门状态
if self.door:
self.door.set_open(self.star_collected >= self.star_total)
# 开门后碰撞即通关
if self.door.is_open and self.player.rect.colliderect(self.door.rect):
self.finish_level()
def draw_world(self):
# 背景
self.canvas.fill(COL_BG)
# 绘制平台
for p in self.platforms:
self.canvas.blit(p.image, p.rect)
# 绘制道具与敌人、玩家、门
# 控制层级:星星 -> 敌人 -> 门 -> 玩家(随意,像素风简单)
for star in self.stars:
self.canvas.blit(star.image, star.rect)
for enemy in self.enemies:
self.canvas.blit(enemy.image, enemy.rect)
if self.door:
self.canvas.blit(self.door.image, self.door.rect)
self.canvas.blit(self.player.image, self.player.rect)
# UI
self.draw_text(self.canvas, f"Stars: {self.star_collected}/{self.star_total}", 4, 4)
self.draw_text(self.canvas, f"Deaths: {self.deaths}", 4, 16)
self.draw_text(self.canvas, f"Time: {self.time_elapsed:0.2f}s", 4, 28)
# 像素放大绘制到窗口
surf_scaled = pygame.transform.scale(self.canvas, (WIN_W, WIN_H))
self.screen.blit(surf_scaled, (0, 0))
pygame.display.flip()
def draw_finish(self):
# 背景 + 结束面板
self.canvas.fill(COL_BG)
# 简单报告
total = self.star_total
got = self.star_collected
deaths = self.deaths
time_used = self.time_elapsed
# 简单评级(仅提示)
rank = "S"
if deaths >= 3 or time_used > 60:
rank = "B"
elif deaths >= 1 or time_used > 40:
rank = "A"
# 面板
panel = pygame.Surface((VIRTUAL_W - 40, VIRTUAL_H - 60))
panel.fill((35, 35, 50))
pygame.draw.rect(panel, (80, 80, 110), panel.get_rect(), 2)
# 文本内容
cx = panel.get_width() // 2
self.draw_text(panel, "关卡完成!", cx, 14, align="midtop")
self.draw_text(panel, f"用时: {time_used:0.2f} 秒", cx, 38, align="midtop")
self.draw_text(panel, f"收集: {got}/{total} 颗星星", cx, 54, align="midtop")
self.draw_text(panel, f"死亡: {deaths} 次", cx, 70, align="midtop")
self.draw_text(panel, f"评价: {rank}", cx, 86, align="midtop")
self.draw_text(panel, "按 R 重新开始,Esc 退出", cx, panel.get_height() - 22, color=(200, 220, 255), align="midtop")
self.canvas.blit(panel, (20, 30))
surf_scaled = pygame.transform.scale(self.canvas, (WIN_W, WIN_H))
self.screen.blit(surf_scaled, (0, 0))
pygame.display.flip()
def run(self):
while self.running:
dt = self.clock.tick(FPS) / 1000.0
# 事件处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN:
if self.state == "playing":
if event.key in (pygame.K_UP, pygame.K_SPACE):
self.player.queue_jump()
if event.key == pygame.K_ESCAPE:
self.running = False
elif self.state == "finished":
if event.key == pygame.K_r:
# 重置关卡(重新加载星星与敌人,统计归零)
self.load_level()
self.reset_runtime_stats()
if event.key == pygame.K_ESCAPE:
self.running = False
# 更新与绘制
if self.state == "playing":
self.update_play(dt)
self.draw_world()
else:
self.draw_finish()
pygame.quit()
sys.exit()
if __name__ == "__main__":
Game().run()
关键函数和类的作用说明
运行要求和依赖说明
可能的扩展和改进建议
本代码严格遵循矩形碰撞与主循环最佳实践,逻辑清晰、即开即玩,适合学习与原型开发。
# -*- coding: utf-8 -*-
# 霓虹俯视射击(非暴力) - PyGame 单文件可运行示例
# 控制:
# - 移动:WASD
# - 射击:鼠标左键
# - 切换模式:T(反弹/穿透)
# - 退出:ESC 或 关闭窗口
#
# 依赖:pygame
# 安装:pip install pygame
# 运行:python neon_topdown.py
import math
import random
import sys
import pygame
# -----------------------
# 全局配置
# -----------------------
WIDTH, HEIGHT = 960, 600
FPS = 60
BG_COLOR = (5, 5, 10) # 深色底
NEON_CYAN = (0, 255, 255)
NEON_MAGENTA = (255, 0, 200)
NEON_YELLOW = (255, 255, 0)
NEON_WHITE = (255, 255, 255)
NEON_GREEN = (0, 255, 160)
PLAYER_COLOR = NEON_CYAN
SEEKER_COLOR = NEON_MAGENTA
DASHER_COLOR = NEON_YELLOW
BULLET_COLOR = NEON_WHITE
from pygame.math import Vector2 as Vec2
# -----------------------
# 霓虹绘制与缓存
# -----------------------
class NeonCache:
circle_cache = {} # key: (color, radius, glow), value: Surface
@staticmethod
def get_glow_circle(color, radius, glow=10):
key = (color, radius, glow)
if key in NeonCache.circle_cache:
return NeonCache.circle_cache[key]
size = (radius + glow) * 2 + 2
surf = pygame.Surface((size, size), pygame.SRCALPHA)
cx, cy = size // 2, size // 2
# 多层圆模拟光晕(从外到内,透明度递增,半径递减)
layers = max(4, glow // 2)
for i in range(layers, 0, -1):
# 插值半径与透明度
ri = radius + int(glow * (i / layers))
alpha = int(30 * (i / layers)) # 外圈更淡
col = (color[0], color[1], color[2], alpha)
pygame.draw.circle(surf, col, (cx, cy), ri)
# 最内层实体圆
pygame.draw.circle(surf, (*color, 240), (cx, cy), radius)
NeonCache.circle_cache[key] = surf
return surf
def blit_neon_circle(surface, pos, color, radius, glow=10):
surf = NeonCache.get_glow_circle(color, radius, glow)
rect = surf.get_rect(center=(int(pos[0]), int(pos[1])))
# 使用加法混合让光叠加
surface.blit(surf, rect, special_flags=pygame.BLEND_ADD)
def draw_neon_line(surface, color, start, end, width=2, glow=6):
# 用一个临时透明层绘制多层线条,再以加法混合叠加
temp = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
layers = max(3, glow // 2)
for i in range(layers, 0, -1):
w = width + i * 2
alpha = int(40 * (i / layers))
col = (color[0], color[1], color[2], alpha)
pygame.draw.line(temp, col, start, end, w)
# 实体线
pygame.draw.line(temp, (*color, 240), start, end, width)
surface.blit(temp, (0, 0), special_flags=pygame.BLEND_ADD)
# -----------------------
# 工具函数
# -----------------------
def clamp(value, a, b):
return max(a, min(b, value))
def random_pos_away_from(center: Vec2, min_dist=120):
for _ in range(64):
x = random.uniform(40, WIDTH - 40)
y = random.uniform(40, HEIGHT - 40)
p = Vec2(x, y)
if p.distance_to(center) >= min_dist:
return p
return Vec2(random.uniform(80, WIDTH - 80), random.uniform(80, HEIGHT - 80))
# -----------------------
# 精灵基类与实体
# -----------------------
class GlowSprite(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
# 用于 collide_circle
self.radius = 10
self.image = pygame.Surface((2 * self.radius, 2 * self.radius), pygame.SRCALPHA)
self.rect = self.image.get_rect()
def draw(self, surface):
pass
class Player(GlowSprite):
def __init__(self, pos: Vec2):
super().__init__()
self.pos = Vec2(pos)
self.vel = Vec2(0, 0)
self.speed = 280
self.radius = 14
self.image = pygame.Surface((2 * self.radius, 2 * self.radius), pygame.SRCALPHA)
self.rect = self.image.get_rect(center=self.pos)
self.color = PLAYER_COLOR
self.shoot_cooldown = 0.12 # 连发频率
self.shoot_timer = 0.0
self.bullet_mode = "bounce" # "bounce" or "pierce"
def toggle_mode(self):
self.bullet_mode = "pierce" if self.bullet_mode == "bounce" else "bounce"
def handle_input(self, dt):
keys = pygame.key.get_pressed()
move = Vec2(0, 0)
if keys[pygame.K_w]:
move.y -= 1
if keys[pygame.K_s]:
move.y += 1
if keys[pygame.K_a]:
move.x -= 1
if keys[pygame.K_d]:
move.x += 1
if move.length_squared() > 0:
move = move.normalize()
self.vel = move * self.speed
self.pos += self.vel * dt
self.pos.x = clamp(self.pos.x, 20, WIDTH - 20)
self.pos.y = clamp(self.pos.y, 20, HEIGHT - 20)
self.rect.center = (int(self.pos.x), int(self.pos.y))
def try_shoot(self, dt, bullets_group):
self.shoot_timer -= dt
if self.shoot_timer > 0:
return
mouse_buttons = pygame.mouse.get_pressed(num_buttons=3)
if mouse_buttons[0]:
mouse_pos = Vec2(pygame.mouse.get_pos())
dir_vec = mouse_pos - self.pos
if dir_vec.length_squared() == 0:
return
direction = dir_vec.normalize()
# 发射位置略微离开玩家,避免与自身碰撞
spawn_pos = self.pos + direction * (self.radius + 10)
speed = 520
if self.bullet_mode == "bounce":
bullets_group.add(Bullet(spawn_pos, direction * speed, mode="bounce", bounces=3))
else:
bullets_group.add(Bullet(spawn_pos, direction * speed, mode="pierce"))
self.shoot_timer = self.shoot_cooldown
def update(self, dt, bullets_group):
self.handle_input(dt)
self.try_shoot(dt, bullets_group)
def draw(self, surface):
# 玩家主体
blit_neon_circle(surface, self.pos, self.color, self.radius, glow=10)
# 指向线(短线)
mouse_pos = Vec2(pygame.mouse.get_pos())
dir_vec = mouse_pos - self.pos
if dir_vec.length_squared() > 0:
direction = dir_vec.normalize()
a = self.pos + direction * (self.radius + 4)
b = self.pos + direction * 28
draw_neon_line(surface, NEON_CYAN, a, b, width=2, glow=8)
# 鼠标瞄准目标圈
blit_neon_circle(surface, mouse_pos, (80, 200, 255), 4, glow=6)
class Bullet(GlowSprite):
def __init__(self, pos: Vec2, velocity: Vec2, mode="bounce", bounces=3):
super().__init__()
self.pos = Vec2(pos)
self.vel = Vec2(velocity)
self.mode = mode # "bounce" or "pierce"
self.radius = 4
self.image = pygame.Surface((2 * self.radius, 2 * self.radius), pygame.SRCALPHA)
self.rect = self.image.get_rect(center=self.pos)
self.color = BULLET_COLOR
self.lifetime = 4.0 if mode == "bounce" else 2.6
self.bounces_left = bounces if mode == "bounce" else 0
self.hit_cooldowns = {} # enemy_id -> remaining_time (用于穿透模式的多帧重复命中抑制)
def can_hit(self, enemy):
eid = id(enemy)
return self.hit_cooldowns.get(eid, 0) <= 0
def register_hit(self, enemy, cooldown=0.12):
eid = id(enemy)
self.hit_cooldowns[eid] = cooldown
def update(self, dt):
self.lifetime -= dt
if self.lifetime <= 0:
self.kill()
return
# 冷却计时更新
if self.hit_cooldowns:
to_del = []
for k in list(self.hit_cooldowns.keys()):
self.hit_cooldowns[k] -= dt
if self.hit_cooldowns[k] <= 0:
to_del.append(k)
for k in to_del:
del self.hit_cooldowns[k]
# 移动
self.pos += self.vel * dt
# 边界处理
if self.mode == "bounce":
bounced = False
if self.pos.x <= self.radius or self.pos.x >= WIDTH - self.radius:
self.vel.x *= -1
self.pos.x = clamp(self.pos.x, self.radius, WIDTH - self.radius)
bounced = True
if self.pos.y <= self.radius or self.pos.y >= HEIGHT - self.radius:
self.vel.y *= -1
self.pos.y = clamp(self.pos.y, self.radius, HEIGHT - self.radius)
bounced = True
if bounced:
self.bounces_left -= 1
if self.bounces_left < 0:
self.kill()
return
else:
# 穿透模式越界直接销毁
if (self.pos.x < -20 or self.pos.x > WIDTH + 20 or
self.pos.y < -20 or self.pos.y > HEIGHT + 20):
self.kill()
return
self.rect.center = (int(self.pos.x), int(self.pos.y))
def draw(self, surface):
blit_neon_circle(surface, self.pos, self.color, self.radius, glow=6)
class Enemy(GlowSprite):
def __init__(self, pos: Vec2, color, radius=12, health=2):
super().__init__()
self.pos = Vec2(pos)
self.color = color
self.radius = radius
self.health = health
self.image = pygame.Surface((2 * self.radius, 2 * self.radius), pygame.SRCALPHA)
self.rect = self.image.get_rect(center=self.pos)
self.hit_flash = 0.0
def take_hit(self, dmg=1):
self.health -= dmg
self.hit_flash = 0.12
if self.health <= 0:
self.kill()
return True
return False
def update(self, dt, player_pos: Vec2):
pass
def draw(self, surface):
# 击中闪烁叠加绿色
base_color = self.color
if self.hit_flash > 0:
self.hit_flash -= 0.016
flash_color = (
min(255, base_color[0] + 40),
min(255, base_color[1] + 80),
min(255, base_color[2] + 40),
)
blit_neon_circle(surface, self.pos, flash_color, self.radius + 1, glow=10)
blit_neon_circle(surface, self.pos, base_color, self.radius, glow=10)
class Seeker(Enemy):
def __init__(self, pos: Vec2, wave=1):
# 追踪者:速度随波次略增,血量随波次每隔几波+1
base_speed = 70 + wave * 6
health = 2 + (wave // 4)
super().__init__(pos, SEEKER_COLOR, radius=12, health=health)
self.speed = base_speed
def update(self, dt, player_pos: Vec2):
dir_vec = player_pos - self.pos
if dir_vec.length_squared() > 1:
self.pos += dir_vec.normalize() * self.speed * dt
self.pos.x = clamp(self.pos.x, self.radius, WIDTH - self.radius)
self.pos.y = clamp(self.pos.y, self.radius, HEIGHT - self.radius)
self.rect.center = (int(self.pos.x), int(self.pos.y))
class Dasher(Enemy):
def __init__(self, pos: Vec2, wave=1):
# 弹射者:平时匀速移动,撞墙弹射;周期性朝玩家加速
base_speed = 110 + wave * 6
dash_speed = 260 + wave * 8
health = 2 + (wave // 5)
super().__init__(pos, DASHER_COLOR, radius=12, health=health)
angle = random.uniform(0, math.tau)
self.vel = Vec2(math.cos(angle), math.sin(angle)) * base_speed
self.base_speed = base_speed
self.dash_speed = dash_speed
self.dash_cooldown = random.uniform(1.4, 2.1)
self.dash_timer = self.dash_cooldown
def update(self, dt, player_pos: Vec2):
# 定时指向玩家的冲刺
self.dash_timer -= dt
if self.dash_timer <= 0:
dir_vec = player_pos - self.pos
if dir_vec.length_squared() > 1:
self.vel = dir_vec.normalize() * self.dash_speed
self.dash_timer = self.dash_cooldown
# 逐步回落到基础速度(避免一直超高速)
if self.vel.length() > self.base_speed:
self.vel.scale_to_length(max(self.base_speed, self.vel.length() - 120 * dt))
# 移动与墙面弹射
self.pos += self.vel * dt
bounced = False
if self.pos.x <= self.radius or self.pos.x >= WIDTH - self.radius:
self.vel.x *= -1
self.pos.x = clamp(self.pos.x, self.radius, WIDTH - self.radius)
bounced = True
if self.pos.y <= self.radius or self.pos.y >= HEIGHT - self.radius:
self.vel.y *= -1
self.pos.y = clamp(self.pos.y, self.radius, HEIGHT - self.radius)
bounced = True
self.rect.center = (int(self.pos.x), int(self.pos.y))
# 弹墙微弱加速感
if bounced and self.vel.length() < self.base_speed * 1.2:
self.vel *= 1.05
class Particle(GlowSprite):
def __init__(self, pos: Vec2, color, radius=10, life=0.3):
super().__init__()
self.pos = Vec2(pos)
self.color = color
self.radius = radius
self.life = life
self.image = pygame.Surface((2, 2), pygame.SRCALPHA)
self.rect = self.image.get_rect(center=self.pos)
# 随机运动
ang = random.uniform(0, math.tau)
self.vel = Vec2(math.cos(ang), math.sin(ang)) * random.uniform(20, 80)
def update(self, dt):
self.life -= dt
if self.life <= 0:
self.kill()
return
self.pos += self.vel * dt
self.vel *= (1 - 2.2 * dt) # 快速阻尼
self.rect.center = (int(self.pos.x), int(self.pos.y))
def draw(self, surface):
alpha = max(0, min(255, int(255 * (self.life / 0.3))))
col = (self.color[0], self.color[1], self.color[2], alpha)
# 临时绘制,粒子较小可直接简单画
temp = pygame.Surface((16, 16), pygame.SRCALPHA)
pygame.draw.circle(temp, col, (8, 8), max(1, int(self.radius * (self.life / 0.3))))
surface.blit(temp, (self.pos.x - 8, self.pos.y - 8), special_flags=pygame.BLEND_ADD)
# -----------------------
# 游戏主类(循环、波次、碰撞、渲染)
# -----------------------
class Game:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Neon Top-Down (A/B 模式对比)")
self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont("Consolas", 20)
if self.font is None:
self.font = pygame.font.SysFont(None, 20)
# 精灵组
self.all_sprites = pygame.sprite.Group()
self.enemies = pygame.sprite.Group()
self.bullets = pygame.sprite.Group()
self.particles = pygame.sprite.Group()
# 玩家
self.player = Player(Vec2(WIDTH / 2, HEIGHT / 2))
self.all_sprites.add(self.player)
# 计分与时间
self.score = 0
self.elapsed = 0.0
# 波次管理
self.wave = 0
self.wave_timer = 0.0
self.wave_duration = 18.0 # 每波基准时长(也可清场提前进入下一波)
self.start_next_wave()
def start_next_wave(self):
self.wave += 1
self.wave_timer = 0.0
# 根据波次生成敌人
# 数量与难度递增
num_seekers = 3 + self.wave // 1
num_dashers = max(1, self.wave // 2)
# 稍微随机
num_seekers += random.randint(0, 1)
num_dashers += random.randint(0, 1)
for _ in range(num_seekers):
pos = random_pos_away_from(self.player.pos, min_dist=180)
e = Seeker(pos, wave=self.wave)
self.enemies.add(e)
self.all_sprites.add(e)
for _ in range(num_dashers):
pos = random_pos_away_from(self.player.pos, min_dist=200)
e = Dasher(pos, wave=self.wave)
self.enemies.add(e)
self.all_sprites.add(e)
def spawn_hit_particles(self, pos, base_color):
for _ in range(6):
self.particles.add(Particle(pos, base_color, radius=6, life=0.25))
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.quit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.quit()
elif event.key == pygame.K_t:
self.player.toggle_mode()
def update(self, dt):
self.elapsed += dt
self.wave_timer += dt
# 更新玩家和子弹
self.player.update(dt, self.bullets)
for b in list(self.bullets):
b.update(dt)
# 更新敌人
for e in list(self.enemies):
e.update(dt, self.player.pos)
# 更新粒子
for p in list(self.particles):
p.update(dt)
# 碰撞:子弹 vs 敌人(圆碰撞)
collisions = pygame.sprite.groupcollide(self.bullets, self.enemies, False, False, collided=pygame.sprite.collide_circle)
for bullet, enemy_list in collisions.items():
for enemy in enemy_list:
if bullet.mode == "pierce":
if not bullet.can_hit(enemy):
continue
destroyed = enemy.take_hit(1)
bullet.register_hit(enemy)
self.spawn_hit_particles(enemy.pos, NEON_GREEN)
if destroyed:
self.score += 1
else:
# 反弹模式:命中即销毁子弹
destroyed = enemy.take_hit(1)
self.spawn_hit_particles(enemy.pos, NEON_GREEN)
bullet.kill()
if destroyed:
self.score += 1
# 波次推进:清场或时间到
if (len(self.enemies) == 0 and self.wave_timer > 1.0) or self.wave_timer >= self.wave_duration:
self.start_next_wave()
def draw_grid(self, surface):
# 轻微网格,增强科技感
step = 40
grid_color = (20, 20, 30, 40)
temp = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
for x in range(0, WIDTH, step):
pygame.draw.line(temp, grid_color, (x, 0), (x, HEIGHT), 1)
for y in range(0, HEIGHT, step):
pygame.draw.line(temp, grid_color, (0, y), (WIDTH, y), 1)
surface.blit(temp, (0, 0))
def draw_ui(self, surface):
mode_text = f"模式: {'反弹(A)' if self.player.bullet_mode=='bounce' else '穿透(B)'} [T 切换]"
time_text = f"时间: {self.elapsed:.1f}s"
wave_text = f"波次: {self.wave}"
score_text = f"得分: {self.score}"
def draw_label(txt, x, y, color=NEON_CYAN):
surf = self.font.render(txt, True, color)
# 轻微光晕
glow = self.font.render(txt, True, (min(255, color[0]//2+60), min(255, color[1]//2+60), min(255, color[2]//2+60)))
surface.blit(glow, (x-1, y-1))
surface.blit(glow, (x+1, y+1))
surface.blit(surf, (x, y))
draw_label(time_text, 14, 10, NEON_CYAN)
draw_label(wave_text, WIDTH - 160, 10, NEON_YELLOW)
draw_label(score_text, 14, 36, NEON_GREEN)
draw_label(mode_text, 14, HEIGHT - 32, NEON_WHITE)
def draw(self):
self.screen.fill(BG_COLOR)
self.draw_grid(self.screen)
# 绘制顺序:粒子(底层)-> 敌人 -> 子弹 -> 玩家
for p in self.particles:
p.draw(self.screen)
for e in self.enemies:
e.draw(self.screen)
for b in self.bullets:
b.draw(self.screen)
self.player.draw(self.screen)
self.draw_ui(self.screen)
pygame.display.flip()
def quit(self):
pygame.quit()
sys.exit(0)
def run(self):
while True:
dt = self.clock.tick(FPS) / 1000.0
self.handle_events()
self.update(dt)
self.draw()
def main():
Game().run()
if __name__ == "__main__":
main()
关键函数和类的作用说明
运行要求和依赖说明
可能的扩展和改进建议
游戏类型说明
核心功能特点
技术实现要点
# 完整的PyGame游戏代码
# 推箱子(Sokoban)教学版:三档难度、计时、参数即改、随机生成、关卡加载与验证、事件队列与渲染优化
# 依赖:pygame
# 运行:python this_file.py
import os
import sys
import time
import random
from collections import deque
import pygame
# ---------------------------
# 全局设置与常量
# ---------------------------
pygame.init()
pygame.display.set_caption("推箱子教学版 - 难度&计时&参数即改&验证&优化示范")
# 事件类型(自定义)
TICK_EVENT = pygame.USEREVENT + 1 # 计时每秒刷新
PERF_EVENT = pygame.USEREVENT + 2 # 性能统计更新
# 允许的事件类型:减少事件队列冗余(优化示范)
pygame.event.set_blocked(None)
pygame.event.set_allowed([pygame.QUIT, pygame.KEYDOWN, pygame.KEYUP,
pygame.VIDEORESIZE, pygame.WINDOWFOCUSGAINED, pygame.WINDOWFOCUSLOST,
pygame.DROPFILE, TICK_EVENT, PERF_EVENT])
# 屏幕与帧率
BASE_WIDTH, BASE_HEIGHT = 900, 700
FLAGS = pygame.RESIZABLE | pygame.SCALED
screen = pygame.display.set_mode((BASE_WIDTH, BASE_HEIGHT), FLAGS)
clock = pygame.time.Clock()
TARGET_FPS = 60
# 网格绘制相关
MARGIN = 10 # 画布边距像素
# ---------------------------
# 主题与颜色
# ---------------------------
def theme_pastel():
return {
"bg": (248, 248, 255),
"wall": (180, 200, 230),
"goal": (255, 210, 120),
"floor": (240, 240, 240),
"box": (255, 160, 160),
"box_on_goal": (180, 230, 180),
"player": (140, 180, 255),
"grid": (220, 220, 220),
"text": (40, 40, 40),
"hud_bg": (255, 255, 255, 180),
}
def theme_neon():
return {
"bg": (10, 10, 15),
"wall": (35, 50, 65),
"goal": (255, 85, 0),
"floor": (25, 25, 35),
"box": (0, 200, 255),
"box_on_goal": (0, 255, 120),
"player": (255, 0, 155),
"grid": (55, 55, 80),
"text": (230, 230, 240),
"hud_bg": (20, 20, 30, 190),
}
def theme_mono():
return {
"bg": (250, 250, 250),
"wall": (110, 110, 110),
"goal": (80, 80, 80),
"floor": (230, 230, 230),
"box": (150, 150, 150),
"box_on_goal": (100, 100, 100),
"player": (50, 50, 50),
"grid": (200, 200, 200),
"text": (10, 10, 10),
"hud_bg": (255, 255, 255, 180),
}
THEMES = [theme_pastel(), theme_neon(), theme_mono()]
THEME_NAMES = ["Pastel", "Neon", "Mono"]
# ---------------------------
# 配置(可课堂即改)
# ---------------------------
class Config:
def __init__(self):
self.difficulty = "easy" # "easy" | "medium" | "hard"
# 三档缺省参数(仅用于随机生成时提供建议)
self.diff_params = {
"easy": {"w": 9, "h": 9, "boxes": 2, "density": 0.08},
"medium": {"w": 11, "h": 11, "boxes": 3, "density": 0.11},
"hard": {"w": 13, "h": 13, "boxes": 4, "density": 0.14},
}
self.step_limit = 300
self.undo_limit = 20
self.obstacle_density = 0.10 # 用于随机生成
self.theme_index = 0 # 0 Pastel, 1 Neon, 2 Mono
self.show_help = True
self.validate_timeout_sec = 1.2 # 求解器最大时长
self.validate_node_limit = 50000 # 求解器最大节点展开
def apply_difficulty(self, name):
name = name.lower()
if name in self.diff_params:
self.difficulty = name
p = self.diff_params[name]
self.obstacle_density = p["density"]
def adjust_step_limit(self, delta):
self.step_limit = max(10, min(9999, self.step_limit + delta))
def adjust_undo_limit(self, delta):
self.undo_limit = max(0, min(999, self.undo_limit + delta))
def adjust_density(self, delta):
self.obstacle_density = max(0.00, min(0.40, round(self.obstacle_density + delta, 3)))
def cycle_theme(self, idx=None):
if idx is None:
self.theme_index = (self.theme_index + 1) % len(THEMES)
else:
self.theme_index = idx % len(THEMES)
CONFIG = Config()
# ---------------------------
# 工具:方向与网格帮助
# ---------------------------
DIRS = {
pygame.K_UP: (0, -1), pygame.K_w: (0, -1),
pygame.K_DOWN: (0, 1), pygame.K_s: (0, 1),
pygame.K_LEFT: (-1, 0), pygame.K_a: (-1, 0),
pygame.K_RIGHT: (1, 0), pygame.K_d: (1, 0),
}
VEC4 = [(0,-1), (0,1), (-1,0), (1,0)]
def add(p, d): return (p[0]+d[0], p[1]+d[1])
# ---------------------------
# 关卡对象
# ---------------------------
class Level:
def __init__(self, name=""):
self.name = name
self.w = 0
self.h = 0
self.walls = set()
self.goals = set()
self.boxes = set()
self.player = None
self.raw_lines = []
self._bg_cached_surface = None # 预渲染静态层缓存
self._tile_size = 32
self._origin = (0, 0) # 网格起点在窗口坐标
# 游戏运行态
self.steps = 0
self.start_ticks = pygame.time.get_ticks()
self.undos_used = 0
self.undo_stack = [] # [(player, frozenset(boxes))]
self.redo_stack = []
# 解析文本关卡
@staticmethod
def from_text(text, name="FromText"):
lvl = Level(name)
lines = [ln.rstrip("\n") for ln in text.splitlines()]
if not lines:
raise ValueError("空关卡")
width = max(len(ln) for ln in lines)
height = len(lines)
lvl.w, lvl.h = width, height
lvl.raw_lines = lines
player_count = 0
for y, ln in enumerate(lines):
for x in range(width):
ch = ln[x] if x < len(ln) else " "
if ch == "#":
lvl.walls.add((x, y))
elif ch in ".G":
lvl.goals.add((x, y))
elif ch == "*":
lvl.goals.add((x, y))
lvl.boxes.add((x, y))
elif ch == "B":
lvl.boxes.add((x, y))
elif ch == "+":
lvl.goals.add((x, y))
lvl.player = (x, y); player_count += 1
elif ch == "P":
lvl.player = (x, y); player_count += 1
# 其他字符默认地板或空白
if player_count == 0:
raise ValueError("未找到玩家 P")
if player_count > 1:
raise ValueError("玩家数量超过1")
if not lvl.boxes:
raise ValueError("未找到箱子 B 或 *")
if len(lvl.goals) != len(lvl.boxes):
raise ValueError("目标与箱子数量不一致")
return lvl
# 基础结构验证与简单死锁检测
def validate_structure(self):
# 检查边界:如果外围没有墙,允许通过,但提示
warnings = []
# 角落死锁检查(非目标)
for bx, by in self.boxes:
if (bx, by) not in self.goals:
# 是否在两面夹墙
up = (bx, by-1) in self.walls
down = (bx, by+1) in self.walls
left = (bx-1, by) in self.walls
right = (bx+1, by) in self.walls
# 角落
if (up and left) or (up and right) or (down and left) or (down and right):
warnings.append(f"箱子({bx},{by})位于角落且非目标,可能无解")
return warnings
def is_completed(self):
return self.boxes == self.goals
def clone_state(self):
return (self.player, frozenset(self.boxes))
def restore_state(self, state):
self.player = state[0]
self.boxes = set(state[1])
def reset_run_state(self):
self.steps = 0
self.undos_used = 0
self.undo_stack.clear()
self.redo_stack.clear()
self.start_ticks = pygame.time.get_ticks()
# 尝试移动/推箱
def try_move(self, dx, dy, step_limit, undo_limit):
if self.is_completed():
return False, "completed"
if self.steps >= step_limit:
return False, "step_limit_reached"
px, py = self.player
np = (px + dx, py + dy)
pushed = False
# 墙阻挡
if np in self.walls:
return False, "wall"
# 如果下一格是箱子,检查再下一格是否可推
if np in self.boxes:
b2 = (np[0] + dx, np[1] + dy)
if b2 in self.walls or b2 in self.boxes:
return False, "box_blocked"
# 记录状态以便撤销
if len(self.undo_stack) < undo_limit:
self.undo_stack.append(self.clone_state())
else:
# 撤销栈到达上限时,仍允许移动,但无法撤销更多
pass
self.redo_stack.clear()
# 推箱
self.boxes.remove(np)
self.boxes.add(b2)
self.player = np
pushed = True
self.steps += 1
return True, "pushed"
else:
# 空地移动
if len(self.undo_stack) < undo_limit:
self.undo_stack.append(self.clone_state())
self.redo_stack.clear()
self.player = np
self.steps += 1
return True, "moved"
def undo(self):
if not self.undo_stack:
return False
prev = self.undo_stack.pop()
self.redo_stack.append(self.clone_state())
self.restore_state(prev)
self.undos_used += 1
return True
def redo(self):
if not self.redo_stack:
return False
nxt = self.redo_stack.pop()
self.undo_stack.append(self.clone_state())
self.restore_state(nxt)
return True
# ---------------------------
# 渲染相关
# ---------------------------
def compute_layout(self, surf_size):
# 根据窗口尺寸计算tile size与原点,使网格居中
win_w, win_h = surf_size
avail_w = max(100, win_w - 2 * MARGIN)
avail_h = max(100, win_h - 2 * MARGIN)
tile = min(avail_w // self.w, avail_h // self.h)
tile = max(16, min(64, tile))
grid_w = tile * self.w
grid_h = tile * self.h
ox = (win_w - grid_w) // 2
oy = (win_h - grid_h) // 2
self._tile_size = tile
self._origin = (ox, oy)
def grid_to_screen(self, cell):
x, y = cell
return (self._origin[0] + x * self._tile_size, self._origin[1] + y * self._tile_size)
def pre_render_static(self, theme, target_surface):
# 预渲染静态层(地板、墙、目标、网格线)
self.compute_layout(target_surface.get_size())
tile = self._tile_size
ox, oy = self._origin
bg = pygame.Surface(target_surface.get_size()).convert()
bg.fill(theme["bg"])
# 网格地板
floor_col = theme["floor"]
grid_col = theme["grid"]
for y in range(self.h):
for x in range(self.w):
rx, ry = ox + x*tile, oy + y*tile
pygame.draw.rect(bg, floor_col, (rx, ry, tile, tile))
# 网格线
pygame.draw.rect(bg, grid_col, (rx, ry, tile, tile), 1)
# 目标点
for (x, y) in self.goals:
rx, ry = ox + x*tile, oy + y*tile
gcol = theme["goal"]
inset = max(2, tile // 6)
pygame.draw.rect(bg, gcol, (rx+inset, ry+inset, tile-2*inset, tile-2*inset), border_radius=max(2, tile//8))
# 墙
for (x, y) in self.walls:
rx, ry = ox + x*tile, oy + y*tile
wcol = theme["wall"]
pygame.draw.rect(bg, wcol, (rx, ry, tile, tile))
self._bg_cached_surface = bg
def draw_dynamic(self, target_surface, theme, dirty_rects=None):
# 绘制动态对象(箱子、玩家)
tile = self._tile_size
ox, oy = self._origin
def rect_of(c):
rx, ry = ox + c[0]*tile, oy + c[1]*tile
return pygame.Rect(rx, ry, tile, tile)
# 箱子
for (x, y) in self.boxes:
rc = rect_of((x, y))
col = theme["box_on_goal"] if (x, y) in self.goals else theme["box"]
inset = max(2, tile // 10)
pygame.draw.rect(target_surface, col, rc.inflate(-2*inset, -2*inset), border_radius=max(2, tile//10))
if dirty_rects is not None:
dirty_rects.append(rc)
# 玩家
rc = rect_of(self.player)
pcol = theme["player"]
inset = max(2, tile // 8)
pygame.draw.ellipse(target_surface, pcol, rc.inflate(-2*inset, -2*inset))
if dirty_rects is not None:
dirty_rects.append(rc)
# ---------------------------
# 求解器(宏推 BFS)
# ---------------------------
def reachable_cells(player, walls, boxes, w, h):
# 计算玩家可到达区域(不穿墙与箱子)
q = deque([player])
seen = {player}
blocked = walls.union(boxes)
while q:
x, y = q.popleft()
for dx, dy in VEC4:
nx, ny = x+dx, y+dy
if 0 <= nx < w and 0 <= ny < h and (nx, ny) not in blocked and (nx, ny) not in seen:
seen.add((nx, ny))
q.append((nx, ny))
return seen
def is_deadlock_box(pos, walls, goals):
if pos in goals:
return False
x, y = pos
up = (x, y-1) in walls
down = (x, y+1) in walls
left = (x-1, y) in walls
right = (x+1, y) in walls
if (up and left) or (up and right) or (down and left) or (down and right):
return True
return False
def solve_sokoban(level, time_limit_sec=1.2, node_limit=50000):
# 返回 (solvable: bool, steps: int or None)
start_time = time.time()
walls = set(level.walls)
goals = set(level.goals)
boxes = frozenset(level.boxes)
w, h = level.w, level.h
# 初步死锁检查
for b in boxes:
if is_deadlock_box(b, walls, goals):
return False, None
# 状态:仅用箱子位置(宏推),玩家位置用于可达性判定
# 队列元素:(boxes_frozenset, player_pos)
from_state = {}
start_state = (boxes, level.player)
q = deque([start_state])
seen = set([boxes]) # 仅按箱子集去重
expansions = 0
while q:
if time.time() - start_time > time_limit_sec:
return False, None
if expansions > node_limit:
return False, None
cur_boxes, cur_player = q.popleft()
expansions += 1
# 终止条件
if set(cur_boxes) == goals:
return True, None
# 玩家可达区域
reach = reachable_cells(cur_player, walls, set(cur_boxes), w, h)
# 尝试每个箱子的四向推
for bx, by in cur_boxes:
for dx, dy in VEC4:
behind = (bx - dx, by - dy) # 玩家站位
ahead = (bx + dx, by + dy) # 箱子推后位置
# 先判断 ahead 合法,behind 可达
if not (0 <= ahead[0] < w and 0 <= ahead[1] < h):
continue
if ahead in walls or ahead in cur_boxes:
continue
if behind not in reach:
continue
# 死锁剪枝(推后)
if is_deadlock_box(ahead, walls, goals):
continue
new_boxes = set(cur_boxes)
new_boxes.remove((bx, by))
new_boxes.add(ahead)
new_boxes_fs = frozenset(new_boxes)
if new_boxes_fs in seen:
continue
seen.add(new_boxes_fs)
# 推后玩家位置在原箱子位置
q.append((new_boxes_fs, (bx, by)))
return False, None
# ---------------------------
# 随机关卡生成
# ---------------------------
def generate_random_level(name, width, height, box_count, obstacle_density, max_tries=200):
# 基本框架:外墙 + 内部障碍(随机),随机放置目标/箱子/玩家,验证可解
# 为确保可解,尝试多次生成
for attempt in range(1, max_tries+1):
lvl = Level(f"{name}#{attempt}")
lvl.w, lvl.h = width, height
# 外墙
for x in range(width):
lvl.walls.add((x, 0))
lvl.walls.add((x, height-1))
for y in range(height):
lvl.walls.add((0, y))
lvl.walls.add((width-1, y))
# 内障碍
cell_count = (width-2)*(height-2)
obstacles = int(cell_count * obstacle_density)
# 为避免过度碎片化,先随机挑格子
candidates = [(x, y) for x in range(1, width-1) for y in range(1, height-1)]
random.shuffle(candidates)
for i in range(obstacles):
lvl.walls.add(candidates[i])
# 可放置区域
free = [(x, y) for x in range(1, width-1) for y in range(1, height-1) if (x, y) not in lvl.walls]
if len(free) < (2*box_count + 1):
continue
random.shuffle(free)
# 放目标
goals = free[:box_count]
lvl.goals = set(goals)
# 放箱子(与目标不同位置,允许部分重叠也可,但更难)
p2 = free[box_count:]
if len(p2) < box_count + 1:
continue
random.shuffle(p2)
boxes = []
# 尽量避免初始就角落死锁
for cell in p2:
if len(boxes) >= box_count:
break
if not is_deadlock_box(cell, lvl.walls, lvl.goals):
boxes.append(cell)
if len(boxes) < box_count:
# 容错:允许少量角落,但降低成功率
boxes = p2[:box_count]
lvl.boxes = set(boxes)
# 放玩家
p3 = [c for c in p2 if c not in boxes][:1]
if not p3:
continue
lvl.player = p3[0]
# 验证结构与求解
try:
warns = lvl.validate_structure()
except Exception:
continue
solvable, _ = solve_sokoban(lvl, time_limit_sec=CONFIG.validate_timeout_sec, node_limit=CONFIG.validate_node_limit)
if solvable:
return lvl, warns
raise RuntimeError("随机生成失败:未能在限定次数内生成可解关卡,请降低障碍密度或减少箱子数")
# ---------------------------
# 内置示例关卡
# ---------------------------
BUILTIN_LEVELS = {
"easy": [
"""#########
# . #
# #
# B #
# P #
# B #
# #
# . #
#########""",
"""#########
# . #
# B #
# B #
# P #
# #
# . #
# #
#########"""
],
"medium": [
"""###########
# . . #
# B #
# B #
# ### #
# P #
# B #
# . #
###########"""
],
"hard": [
"""#############
# . # . #
# B B #
# ### #
# P B #
# # B #
# . # . #
#############"""
],
}
def load_builtin_level(diff):
levels = BUILTIN_LEVELS.get(diff, [])
if not levels:
return None
text = random.choice(levels)
return Level.from_text(text, name=f"{diff}_builtin")
# ---------------------------
# HUD / 文本绘制
# ---------------------------
def draw_hud(surface, theme, level, width, height, msg=""):
font = pygame.font.SysFont("arial", 18)
font_small = pygame.font.SysFont("arial", 14)
# 背景
hud = pygame.Surface((width, 100), pygame.SRCALPHA)
hud.fill(theme["hud_bg"])
# 时间
elapsed_ms = pygame.time.get_ticks() - level.start_ticks
sec = elapsed_ms // 1000
mm, ss = divmod(sec, 60)
time_str = f"{mm:02d}:{ss:02d}"
# 状态文本
lines = [
f"难度: {CONFIG.difficulty} 主题: {THEME_NAMES[CONFIG.theme_index]}",
f"用时: {time_str} 步数: {level.steps}/{CONFIG.step_limit} 撤销: {level.undos_used}/{CONFIG.undo_limit}",
f"障碍密度: {CONFIG.obstacle_density:.3f} 关卡: {level.name}",
"操作: 方向/WASD移动 Z撤销 Y重做 R重开 V验证 G随机新关 L拖拽txt加载 M菜单 1/2/3主题",
"课堂即改: [/]步数上限 ;/'撤销上限 ,/.障碍密度 F1/F2/F3选择难度",
]
if msg:
lines.append(f"提示: {msg}")
# 绘制文字
y = 5
color = theme["text"]
for ln in lines:
surf = font.render(ln, True, color)
hud.blit(surf, (10, y))
y += 20
surface.blit(hud, (0, 0))
def draw_center_text(surface, text, theme, y_offset=0, big=False):
font = pygame.font.SysFont("arial", 42 if big else 32, bold=big)
surf = font.render(text, True, theme["text"])
rect = surf.get_rect(center=(surface.get_width()//2, surface.get_height()//2 + y_offset))
surface.blit(surf, rect)
# ---------------------------
# 游戏状态管理
# ---------------------------
class Game:
def __init__(self):
self.theme = THEMES[CONFIG.theme_index]
self.mode = "menu" # menu | playing | complete | failed | paused
self.level = None
self.info_msg = ""
self.performance = {"fps": 0.0, "event_queue": 0}
self.last_dirty = [] # 上一帧脏矩形,用于增量更新
# 定时事件
pygame.time.set_timer(TICK_EVENT, 1000) # 每秒刷新计时显示
pygame.time.set_timer(PERF_EVENT, 1000) # 每秒更新性能指标
def new_level_builtin(self, diff):
CONFIG.apply_difficulty(diff)
self.theme = THEMES[CONFIG.theme_index]
try:
lvl = load_builtin_level(CONFIG.difficulty)
if lvl is None:
raise RuntimeError("无内置关卡")
warns = lvl.validate_structure()
self.level = lvl
self.level.reset_run_state()
self.level.pre_render_static(self.theme, screen)
self.info_msg = "; ".join(warns) if warns else "已载入内置关卡"
self.mode = "playing"
except Exception as e:
self.info_msg = f"载入失败: {e}"
def new_level_random(self):
p = CONFIG.diff_params[CONFIG.difficulty]
try:
lvl, warns = generate_random_level(
name=f"{CONFIG.difficulty}_rand",
width=p["w"], height=p["h"],
box_count=p["boxes"],
obstacle_density=CONFIG.obstacle_density,
max_tries=200
)
self.level = lvl
self.level.reset_run_state()
self.level.pre_render_static(self.theme, screen)
self.info_msg = "随机关卡成功" + ((" | " + "; ".join(warns)) if warns else "")
self.mode = "playing"
except Exception as e:
self.info_msg = f"随机生成失败: {e}"
def load_level_from_file(self, path):
try:
with open(path, "r", encoding="utf-8") as f:
text = f.read()
lvl = Level.from_text(text, name=os.path.basename(path))
warns = lvl.validate_structure()
self.level = lvl
self.level.reset_run_state()
self.level.pre_render_static(self.theme, screen)
self.info_msg = f"文件载入成功: {path}" + ((" | " + "; ".join(warns)) if warns else "")
self.mode = "playing"
except Exception as e:
self.info_msg = f"文件载入失败: {e}"
def validate_current(self):
if self.level is None:
self.info_msg = "无关卡"
return
st = time.time()
solvable, _ = solve_sokoban(self.level, time_limit_sec=CONFIG.validate_timeout_sec, node_limit=CONFIG.validate_node_limit)
dt = time.time() - st
if solvable:
self.info_msg = f"验证结果:可解({dt*1000:.0f}ms)"
else:
self.info_msg = f"验证结果:未证可解({dt*1000:.0f}ms)"
def restart_level(self):
if self.level is None:
return
# 重新从原始文本加载,确保初始状态一致
try:
base = Level.from_text("\n".join(self.level.raw_lines), name=self.level.name)
self.level = base
self.level.reset_run_state()
self.level.pre_render_static(self.theme, screen)
self.info_msg = "已重开关卡"
except Exception:
# 随机生成或未保存的关卡,简单清空运行态
self.level.reset_run_state()
self.level.pre_render_static(self.theme, screen)
self.info_msg = "已重开(运行态复位)"
def handle_event(self, e):
if e.type == pygame.QUIT:
pygame.quit()
sys.exit(0)
elif e.type == pygame.VIDEORESIZE:
# 重新绘制静态层
if self.level:
self.level.pre_render_static(self.theme, screen)
elif e.type == pygame.DROPFILE:
self.load_level_from_file(e.file)
elif e.type == TICK_EVENT:
# 计时每秒刷新:不必做事,HUD会读取系统计时
pass
elif e.type == PERF_EVENT:
self.performance["fps"] = clock.get_fps()
self.performance["event_queue"] = pygame.event.peek() # 简要查看队列
elif e.type == pygame.KEYDOWN:
self.on_keydown(e)
def on_keydown(self, e):
k = e.key
mods = pygame.key.get_mods()
shift = mods & pygame.KMOD_SHIFT
if self.mode == "menu":
if k == pygame.K_F1:
self.new_level_builtin("easy")
elif k == pygame.K_F2:
self.new_level_builtin("medium")
elif k == pygame.K_F3:
self.new_level_builtin("hard")
elif k == pygame.K_g:
self.new_level_random()
elif k in (pygame.K_1, pygame.K_2, pygame.K_3):
CONFIG.cycle_theme({pygame.K_1:0, pygame.K_2:1, pygame.K_3:2}[k])
self.theme = THEMES[CONFIG.theme_index]
elif k == pygame.K_m:
# 留在菜单
pass
elif k == pygame.K_ESCAPE:
pygame.quit(); sys.exit(0)
return
# playing/complete/failed/paused 公共快捷键
if k == pygame.K_m:
self.mode = "menu"
self.info_msg = "返回菜单"
return
if k in (pygame.K_1, pygame.K_2, pygame.K_3):
CONFIG.cycle_theme({pygame.K_1:0, pygame.K_2:1, pygame.K_3:2}[k])
self.theme = THEMES[CONFIG.theme_index]
if self.level:
self.level.pre_render_static(self.theme, screen)
return
if k == pygame.K_F1:
self.new_level_builtin("easy"); return
if k == pygame.K_F2:
self.new_level_builtin("medium"); return
if k == pygame.K_F3:
self.new_level_builtin("hard"); return
if k == pygame.K_g:
self.new_level_random(); return
if k == pygame.K_r:
self.restart_level(); return
if k == pygame.K_v:
self.validate_current(); return
if k == pygame.K_ESCAPE:
pygame.quit(); sys.exit(0)
# 参数调整
if k == pygame.K_LEFTBRACKET: # [
CONFIG.adjust_step_limit(-50 if shift else -10); self.info_msg = f"步数上限: {CONFIG.step_limit}"
return
if k == pygame.K_RIGHTBRACKET: # ]
CONFIG.adjust_step_limit(50 if shift else +10); self.info_msg = f"步数上限: {CONFIG.step_limit}"
return
if k == pygame.K_SEMICOLON: # ;
CONFIG.adjust_undo_limit(-5 if shift else -1); self.info_msg = f"撤销上限: {CONFIG.undo_limit}"
return
if k == pygame.K_QUOTE: # '
CONFIG.adjust_undo_limit(+5 if shift else +1); self.info_msg = f"撤销上限: {CONFIG.undo_limit}"
return
if k == pygame.K_COMMA: # ,
CONFIG.adjust_density(-0.02 if shift else -0.01); self.info_msg = f"障碍密度: {CONFIG.obstacle_density:.3f}"
return
if k == pygame.K_PERIOD: # .
CONFIG.adjust_density(+0.02 if shift else +0.01); self.info_msg = f"障碍密度: {CONFIG.obstacle_density:.3f}"
return
# 游戏动作(playing)
if self.mode == "playing" and self.level:
if k in (pygame.K_z,):
if self.level.undo():
self.info_msg = "撤销"
else:
self.info_msg = "无法撤销"
return
if k in (pygame.K_y,):
if self.level.redo():
self.info_msg = "重做"
else:
self.info_msg = "无法重做"
return
if k in DIRS:
dx, dy = DIRS[k]
moved, reason = self.level.try_move(dx, dy, CONFIG.step_limit, CONFIG.undo_limit)
# 更新绘制脏矩形:上一帧的动态清空,新帧绘制时会加入
if moved:
# 判断胜利/失败
if self.level.is_completed():
self.mode = "complete"
self.info_msg = "成功!按G随机新关,或M回菜单"
elif reason == "step_limit_reached" or self.level.steps >= CONFIG.step_limit:
self.mode = "failed"
self.info_msg = "步数用尽!按R重开,G随机新关,或M回菜单"
else:
if reason == "wall":
self.info_msg = "前方是墙"
elif reason == "box_blocked":
self.info_msg = "箱子后有阻挡"
return
# complete/failed 模式下额外键位
if self.mode in ("complete", "failed"):
if k == pygame.K_r:
self.restart_level()
self.mode = "playing"
elif k == pygame.K_g:
self.new_level_random()
return
# ---------------------------
# 渲染
# ---------------------------
def render(self):
self.theme = THEMES[CONFIG.theme_index]
dirty = []
# 背景
if self.level and self.level._bg_cached_surface:
screen.blit(self.level._bg_cached_surface, (0, 0))
else:
screen.fill(self.theme["bg"])
# 动态层
if self.level:
self.level.draw_dynamic(screen, self.theme, dirty_rects=dirty)
# HUD
if self.level:
draw_hud(screen, self.theme, self.level, screen.get_width(), screen.get_height(), self.info_msg)
else:
# 菜单页HUD
font = pygame.font.SysFont("arial", 18)
hud = pygame.Surface((screen.get_width(), 100), pygame.SRCALPHA)
hud.fill(self.theme["hud_bg"])
tx = "F1简 F2中 F3难 G随机 拖拽txt加载 1/2/3主题 ESC退出"
hud.blit(font.render(tx, True, self.theme["text"]), (10, 10))
screen.blit(hud, (0, 0))
# 中央提示
if self.mode == "menu":
draw_center_text(screen, "推箱子教学版", self.theme, y_offset=-40, big=True)
draw_center_text(screen, "选择难度:F1/F2/F3,或按 G 随机生成", self.theme, y_offset=10)
draw_center_text(screen, "支持拖拽txt关卡文件加载", self.theme, y_offset=50)
elif self.mode == "complete":
draw_center_text(screen, "关卡完成!", self.theme, y_offset=-20, big=True)
draw_center_text(screen, "R重开 G随机新关 M菜单", self.theme, y_offset=20)
elif self.mode == "failed":
draw_center_text(screen, "失败:步数用尽", self.theme, y_offset=-20, big=True)
draw_center_text(screen, "R重开 G随机新关 M菜单", self.theme, y_offset=20)
# 性能角标
font = pygame.font.SysFont("consolas", 14)
perf = f"{self.performance['fps']:.0f} FPS | EQ:{self.performance['event_queue']}"
ps = font.render(perf, True, self.theme["text"])
screen.blit(ps, (screen.get_width()-ps.get_width()-8, screen.get_height()-ps.get_height()-6))
pygame.display.update(dirty if dirty else None)
# ---------------------------
# 主循环
# ---------------------------
def main():
game = Game()
# 默认进入菜单;若需要,可直接加载一个内置关卡
# game.new_level_builtin("easy")
while True:
# 事件处理
for e in pygame.event.get():
game.handle_event(e)
# 渲染
game.render()
# 帧率
clock.tick(TARGET_FPS)
if __name__ == "__main__":
main()
关键函数和类的作用说明
运行要求和依赖说明
可能的扩展和改进建议
把你的游戏创意在极短时间内变成“可玩”的PyGame作品,助你从学习入门到原型验证一路加速。核心目标:
借助模板快速完成课程作业或个人项目,理解游戏循环、事件处理与碰撞检测,交付可玩的成品并撰写报告。
把玩法想法在数分钟内变成可玩Demo,AB对比不同机制与视觉风格,用于内测与投资人展示,快速迭代验证。
批量生成不同难度的教学案例与作业素材,课堂即跑即改,拆解架构与优化思路,提升授课效率与学习效果。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
免费获取高级提示词-优惠即将到期