20251222秦家昌 实验四《Python程序设计》 Python综合实验报告
课程:《Python程序设计》
班级: 2512
姓名: 秦家昌
学号:20251222
实验教师:王志强
实验日期:2026年4月15日
必修/选修: 公选课
1.实验内容
1.1 实验目的
综合运用 Python 基础语法、函数、列表、字典、文件读写、模块调用等课堂知识;
掌握 Pygame 图形库使用,理解 GUI 界面绘制、事件监听、动画渲染原理;
学会模块化编程,拆分复杂功能,提升代码逻辑梳理与问题调试能力;
完成一款功能完整的小型 RPG 游戏,实现交互、剧情、战斗、养成、存档等一体化功能。
1.2 实验内容
使用 Python + Pygame 开发一款二维回合制冒险小游戏,实现存档管理、地图探索、角色移动、NPC 剧情交互、回合战斗、属性养成、数据持久化七大核心功能,完成从界面设计、逻辑编写到调试优化的全流程开发。
1.3 开发环境
操作系统:Windows 10/11
编程语言:Python 3.9+
依赖库:pygame、os、random、ast
开发工具:IDLE / VS Code
2. 需求分析与整体设计
2.1 功能需求分析
结合游戏玩法,梳理核心需求:
界面需求:启动界面、游戏主界面、属性面板、战斗界面、事件对话界面,支持鼠标悬浮、点击交互;
操作需求:键盘控制角色移动,鼠标点击按钮、选项;
玩法需求:地图探索、NPC 对话、怪物战斗、角色升级加点;
数据需求:角色属性、地图数据、事件配置、怪物属性通过本地文件存储,实现存档读档。
2.2 整体架构设计
采用模块化分层设计,将程序拆分为五大模块,降低代码耦合度:
初始化模块:Pygame 初始化、窗口创建、字体 / 颜色 / 全局变量定义;
界面绘制模块:通用文本绘制、按钮绘制、背景渲染、文字自动换行;
事件监听模块:鼠标点击、鼠标悬浮、键盘按键、窗口关闭事件处理;
游戏逻辑模块:角色移动、地图渲染、NPC 交互、回合战斗、属性计算;
文件读写模块:地图、事件、怪物、存档文件的读取与写入。
2.3 程序运行流程
启动程序 → 初始化资源 → 进入开始界面(选择存档) → 加载地图与角色数据 → 主游戏循环(移动 / 交互 / 战斗) → 打开属性面板(加点养成) → 退出游戏 → 自动保存数据。
3. 详细功能设计
3.1 启动与存档模块
程序启动后遍历存档文件夹,读取所有存档文件,展示存档名称、角色等级、经验。支持鼠标点击选择存档进入游戏,右键返回、关闭窗口退出程序。
3.2 地图与移动模块
地图采用二维列表存储,每个格子对应不同地形,配置通行规则;
W/A/S/D 四向移动,移动前判定地形,障碍物禁止通行;
角色位于地图边缘时,视口自动偏移,实现大地图探索。
3.3 NPC 剧情交互模块
地图特定格子标记 NPC 点位,角色到达后触发交互按钮;
读取事件文件夹内配置文件,解析列表格式的对话与逻辑;
长对话文本自动换行,支持条件判断、消耗金币、状态修改等联动逻辑。
3.4 回合战斗模块
角色踩中怪物格子,随机加载怪物属性,开启战斗;
根据角色与怪物速度判定行动顺序,轮流释放技能;
计算伤害、护甲减伤、吸血效果,战斗结束结算经验与金币。
3.5 属性养成模块
独立属性面板展示角色全部属性,升级获得自由点数,可手动分配至血量、攻击、防御等属性,强化角色能力。
3.6 数据持久化模块
角色等级、血量、金币、坐标、地图数据实时写入本地文件,保证关闭程序后进度不丢失。
4. 代码实现与核心技术解析
4.1 基础初始化
导入依赖库,初始化 Pygame 窗口、全局字体、颜色表、全局列表与变量,统一管理游戏基础资源。
python
运行
import pygame as pg
import os
import random
import ast
pg.init()
win=pg.display.set_mode((1220,620))
4.2 通用工具函数封装
文本自定义绘制:封装文本自定义函数,统一生成带坐标、尺寸、颜色的文本对象,支持按钮背景、悬浮效果;
中文自动换行:判断汉字、中文标点字符宽度,逐字符累计像素,超出宽度自动换行,解决 Pygame 原生不支持自动换行的问题;
鼠标悬浮判定:if_ontxt函数判断鼠标是否在按钮区域,实现悬浮变色效果。
4.3 文件解析优化
原生eval无法安全解析配置文件,且不支持赋值语句:
使用ast.literal_eval替代eval解析列表型配置文件,规避语法错误与安全风险;
赋值、自增类语句改用exec执行,区分eval与exec使用场景;
文件读写统一增加utf-8编码,解决中文乱码问题。
4.4 核心逻辑实现
角色移动:监听键盘按键,结合地形通行列表whecanmove做障碍物判定;
地图渲染:双层循环遍历地图列表,根据地形编号绘制对应颜色方块;
回合战斗:通过速度值排序判定行动顺序,循环执行技能、伤害计算逻辑;
事件分支:使用字典映射替代大量if/elif判断,简化 NPC 与事件的匹配逻辑。
4.5 数据持久化
游戏退出时,将角色属性、坐标、地图数据以字符串形式写入本地 txt 文件;启动时反向读取解析,实现存档读档。
5. 运行测试与结果展示
5.1 测试环境
Python 3.10 + Pygame 2.1.2,Windows 11 系统。
5.2 功能测试结果
启动界面:正常读取存档,选择存档可顺利进入游戏,功能正常;
角色移动:W/A/S/D 移动流畅,墙壁、河流等障碍物无法穿行,视野跟随正常;
NPC 交互:走近医生、士兵、联络员均可弹出交互按钮,对话文本自动换行,金币消耗逻辑正常;
战斗系统:踩怪正常触发战斗,回合顺序、伤害计算、胜利 / 失败结算均正常,奖励发放无误;
属性养成:属性面板正常打开,属性点可自由分配,点数不足时有弹窗提示;
存档功能:退出游戏后重新启动,所有进度完整保留,数据持久化正常。
5.3 运行视频说明(视屏链接见最下面,和gitee在一起)
录制程序完整运行视频,内容包含:启动选档 → 地图移动探索 → NPC 剧情交互 → 触发怪物战斗 → 战斗胜利升级加点 → 退出重进读取存档,完整展示全部功能。
2. 实验过程及结果
1.编写思路
1.1编写移动效果
1.2编写地图,渲染地图(包括地形是否可以走,什么颜色,是否有事件,NPC)
1.3复合地图和移动效果,实现视角跟随效果,同时优化移动操作,
1.4编写人物属性
1.5实现人物属性更改
1.6编写怪物属性
1.7怪物属性的等级计算
1.8实现战斗显示
1.9实现战斗提示,和结算画面,增加战利品
1.10实现基础NPC功能
1.11实现NPC对话加NPC效果
2.2代码原文
点击查看代码
import pygame as pg
import os
import random
import time
import ast
# 初始化
pg.init()
# 画布
win=pg.display.set_mode((1220,620))
fur_all=[
pg.Surface((1220,620), pg.SRCALPHA).set_alpha(50),
pg.Surface((1220,620), pg.SRCALPHA).set_alpha(0),
pg.Surface((1000,200), pg.SRCALPHA).set_alpha(50),
pg.Surface((300,50), pg.SRCALPHA).set_alpha(100)
]
tipblock=[[],[]]
who_turn=10
font_all=[]
for i in range(0,50):
font_all.append(pg.font.Font("C:/Windows/Fonts/msyh.ttc",i+1))
# 名字
pg.display.set_caption("暂定")
# 时钟
clock=pg.time.Clock()
# 承认汉字的文本
COLOR = {
0: (0, 0, 0), # 无
1: (60, 60, 60), # 墙壁
2: (170, 170, 170), # 边界
3: (120, 120, 120), # 马路
4: (88, 185, 87), # 草地
5: (74, 152, 232), # 河流
6: (196, 147, 107), # 桥
7: (255, 215, 0), # 出口 - 金黄色
8: (110, 115, 120), # 石板
9: (91, 192, 235)
}
whecanmove=[1,0,0,1,1,0,1,2,1,1]
性质=len(whecanmove)
run=True
# text="map1"
存档文件="存档"
地图文件="地图"
怪物文件="怪物属性"
怪物分配={
"map1.txt":[["danger1.txt","danger2.txt","danger3.txt"],[1,10]],
}
size_x=60
size_y=30
insight_x=30
insight_y=14
mx=0
my=0
帧数=60
坐标={"x":10,"y":20}
点击坐标=(0,0)
显示基准=[0,0,0,0]
speed=20
GRID_SIZE=20
移动lis=[[pg.K_w,-1,'y'],[pg.K_a,-1,'x'],[pg.K_s,1,'y'],[pg.K_d,1,'x']]
顺序lis=[0,1,2,3]
移动time=[0,0]
mapsign=[
font_all[16].render("!",True,(90, 210, 255)),
None,
font_all[16].render("@",True,(90, 210, 255)),
font_all[16].render("#",True,(220, 190, 120)),
font_all[16].render("!",True,(255, 70, 70)),
]
runlis=[True,True,True,True,True]
lis=[]
tip=['',0]
fight_coice=["无","物理","道","魔"]
fightend=[0,[],[]]
# 事件_ALL={
# ""
# }
def tiptext():
if tip[1]!=0 and tip[0]!='':
tpt=文本自定义(tip[0],600,20,21,[(255,255,255)])
textdeal(tpt,alpha=tip[1])
tip[1]-=2
# def 存档读取函数():
def 读取地图(text):
global size_y
global size_x
global lis
lis=[]
# try: 地图\AI地图.txt
with open(text,"r") as f:
print("读取成功")
lines=f.readlines()
for i in lines:
try:
lis.append(eval(i))
except:
print("读取错误!")
exit(0)
size_x=len(lis[0])
size_y=len(lis)
return 1
def 地图输入(text):
for i in lis:
for j in i:
if j[1]==4:
j[2]=True
with open(text,"w") as f:
for i in lis:
f.write(str(i)+'\n')
f.close()
def 退出():
for i in range(len(runlis)):
runlis[i]=False
def get_len(str1):
lenth=0
for i in str1:
if '\u4e00' <= i <= '\u9fff':
lenth+=3
else:
lenth+=1
return lenth
def textdeal(text,choice=0,alpha=0,x=1,y=1):
# font=pg.font.Font("C:/Windows/Fonts/msyh.ttc",text["size"])
# txt=font.render(text["text"],True,text["color"][choice])
font=font_all[int(text["height"])]
if x==-1 and y==-1:
if alpha:
txt=font.render(text["text"],True,text["color"][choice])
txt.set_alpha(alpha)
win.blit(txt,(x,y))
else:
win.blit(font.render(text["text"],True,text["color"][choice]),(x,y))
else:
if alpha:
txt=font.render(text["text"],True,text["color"][choice])
txt.set_alpha(alpha)
win.blit(txt,(text["x"],text["y"]))
else:
win.blit(font.render(text["text"],True,text["color"][choice]),(text["x"],text["y"]))
def backdeal(back,choice=0,size=1,border_radiu=8,surface=None):
if surface:
fur=pg.Surface((back["weight"],back["height"]), pg.SRCALPHA)
fur.set_alpha(surface)
pg.draw.rect(fur, back["color"][0+choice], back["rect"], border_radius=border_radiu)
win.blit(fur,(back["x"],back["y"]))
else:
pg.draw.rect(win,back["color"][0+choice],back["rect"],border_radius=border_radiu)
if size:
rect=(back["x"]+size,back["y"]+size,back["weight"]-2*size,back["height"]-2*size)
pg.draw.rect(win,back["color"][2+choice],rect,size,border_radius=border_radiu)
# def tack(back):
# def tack(deal):
def 文本自定义(text,x,y,size=21,color=[(255,255,255),(255, 225, 0)],weigh=-1):
if weigh<=-1:
count=0
for i in text:
if '\u4e00' <= i <= '\u9fff':
count+=3
else:
count+=1
weigh=count*size/3
elif weigh==-2:
x=x-weigh/2
return {"text":text,"x":x,"y":y,"height":size,"color":color,"weight":weigh,"rect":pg.Rect(x,y,weigh,size)}
def 游戏开始界面():
global mx,my,存档
界面显示=0
text1=文本自定义("开始游戏",500,220,48,[(255,255,255),(255, 225, 0)],192)
text2=文本自定义("离开",550,344,48,[(255,255,255),(255, 225, 0)],96)
backgrang1=文本自定义("背景1",480,210,130,[(30,30,50),(30,30,50),(100,150,255),(255, 225, 0)],360)
backgrang2=文本自定义("背景2",480,340,130,[(30,30,50),(30,30,50),(100,150,255),(255, 225, 0)],360)
backgrang3=文本自定义("背景3",480,470,130,[(30,30,50),(30,30,50),(100,150,255),(255, 225, 0)],360)
backlis1=[backgrang1,backgrang2,backgrang3]
存档=[]
存档显示=[]
for filename in os.listdir(存档文件):
with open(os.path.join(存档文件,filename),"r") as f:
存档.append(eval(f.read()))
for i in range(3):
location=[480,210+i*130]
if i<len(存档):
存档_1=存档[i]
存档显示.append(文本自定义(存档_1[4],location[0]+20,location[1]+20,32,[(160,160,160),(255,255,255)]))
存档显示.append(文本自定义(f'''等级:{存档_1[2]}''',location[0]+20,location[1]+82,26,[(160,160,160),(255,255,255)]))
存档显示.append(文本自定义(f'''{EXP(存档_1,2)}''',location[0]+100,location[1]+82,26,[(160,160,160),(255,255,255)]))
else:
存档显示.append(文本自定义("无存档",location[0]+20,location[1]+48,32))
while runlis[1]:
mx, my = pg.mouse.get_pos()
events=pg.event.get()
win.fill((0,0,0))
if 界面显示==0:
textdeal(text1,if_ontxt(text1))
textdeal(text2,if_ontxt(text2))
count=事件监听(events,[text1,text2])
if count==2:
退出()
elif count==1:
界面显示=1
elif 界面显示==1:
backdeal(backgrang1,if_ontxt(backgrang1))
backdeal(backgrang2,if_ontxt(backgrang2))
backdeal(backgrang3,if_ontxt(backgrang3))
for i in 存档显示:
textdeal(i,if_ontxt(i))
count=事件监听(events,backlis1)
if count==-999:
return -1
elif 1<=count<=len(存档):
存档=存档[count-1]
return 1
pg.display.update()
clock.tick(帧数)
def 事件监听(events,list_1=[]):
count=0
for e in events:
if e.type == pg.QUIT:
退出()
return -999
elif e.type ==pg.MOUSEBUTTONDOWN and e.button==1:
for i in list_1:
count+=1
if if_ontxt(i):
return count
return -2
elif e.type ==pg.MOUSEBUTTONDOWN and e.button==3:
return -1
return 0
def if_ontxt(txt):
x_max=txt["x"]+txt["weight"]
y_max=txt["y"]+txt["height"]
# 判断鼠标是否悬浮按钮区域
if txt["x"] < mx < x_max and txt["y"] < my < y_max:
return 1
return 0
def if_back(rect):
x_max=rect[0]+rect[2]
y_max=rect[1]+rect[3]
if rect[0] < mx <x_max and rect[2] < my < y_max:
return 1
return 0
def 游戏进入():
global turnmapnow,点击坐标,mx,my,坐标
turnmapnow=[0,0]
读取地图(os.path.join(地图文件,存档[7]))
坐标=存档[8]
text=[]
background=文本自定义(f"背景",0,580,40,[(30,35,45),(30,35,45),(80,90,110),(80,90,110)],1220)
classchange1(text)
run1=True
EXPADD(num=0)
nowpage=['空',0,0,None]
while runlis[1]:
mx,my=pg.mouse.get_pos()
eventdecide()
back,block=classchange1(text)
events=pg.event.get()
count=事件监听(events,text)
if count==5:
查看界面()
# elif count!=0:
# 点击坐标=pg.mouse.get_pos()
# print("点击:",点击坐标)
# 地图修改(count)
win.fill((0,0,0))
地图渲染进化版()
fightendpicture()
backdeal(background,0,border_radiu=0)
事件文件夹='事件'
# 遍历事件列表
for item in back:
# 页面切换,重新读取事件文件
if nowpage[0] != item['text']:
nowpage[0] = item['text']
file_path = os.path.join(事件文件夹, item['text'])
with open(file_path, 'r', encoding='utf-8') as f:
# 替换 eval,修复之前解析报错问题
content = f.read()
nowpage[2] = ast.literal_eval(content)
nowpage[1]=0
# 判断是否为结束选项
try:
if nowpage[2][nowpage[1]][0] == -1:
continue
except:
print(nowpage)
# 执行界面绘制、事件处理
backdeal(item, if_ontxt(item))
eventtextread(nowpage)
# 指定状态下执行选项逻辑
if count == -2 and if_ontxt(item):
choice = nowpage[2][nowpage[1]]
opt_id = choice[0]
if opt_id != 1:
# 交易类型 6
if opt_id == 6:
# 条件判断
if not eval(choice[2]):
tip[0] = "条件不够"
tip[1] = 200
else:
# 逐个执行函数字符串
for func_str in choice[3]:
eval(func_str)
try:
block[3]=eval(choice[4])
except:
pass
# 类型 2
elif opt_id == 2:
for func_str in choice[3]:
eval(func_str)
# 战斗类型 4
elif opt_id == 4:
with open(choice[2], 'r', encoding='utf-8') as f:
line_data = ast.literal_eval(f.read())
if fight(line_data) == 1:
for func_str in choice[3]:
eval(func_str)
else:
fullrour()
坐标["x"] = 0
坐标["y"] = 0
nowpage[1]+=1
for i in text:
textdeal(i,if_ontxt(i))
key=pg.key.get_pressed()
if 方向移动进阶(key):
nowpage=['空',0,0,None]
pg.display.update()
clock.tick(帧数)
# 先定义判断函数(放在代码顶部)
def is_chinese_char(c):
return (
'\u4e00' <= c <= '\u9fff'
or '\u3002' <= c <= '\u303f'
or '\uff00' <= c <= '\uffef'
)
def eventtextread(nowpage):
font = font_all[15]
x, y, weigh, height = (870, 470, 300, 15)
# 取出要绘制的文本
choice = nowpage[2][nowpage[1]][1]
line_list = [] # 存放每一行文本
current_line = "" # 当前行字符串
count = 0 # 字符宽度计数
max_width = weigh - 10 # 最大行宽
# 逐字符遍历文本,自动换行
for char in choice:
# 计算当前字符宽度
if is_chinese_char(char):
add = 3
else:
add = 1
# 超出宽度则换行
if (count + add)*5 >= max_width:
line_list.append(current_line)
current_line = char
count = add
else:
current_line += char
count += add
# 把最后一行加入列表
if current_line:
line_list.append(current_line)
# 逐行绘制文本
for idx, line in enumerate(line_list):
txt_surf = font.render(line, True, (255, 255, 255))
win.blit(txt_surf, (x, y + idx * height))
def is_chinese_char(c):
# 汉字、中文标点、全角符号
return (
'\u4e00' <= c <= '\u9fff'
or '\u3002' <= c <= '\u303f' # 中文标点
or '\uff00' <= c <= '\uffef' # 全角符号、全角引号/数字字母
)
def 地图修改(choice):
mx ,my=点击坐标
# print("点击:",点击坐标)
mx = mx // GRID_SIZE
my = my // GRID_SIZE
if 0<= mx <len(lis[0]) and 0<= my <len(lis):
# print(choice)
mx = mx+显示基准[0]
my = my+显示基准[2]
# print("显示",显示基准)
# lis[my][mx]=[lis[my][mx][0], 4, False]
if choice==-2:
# print(lis[my][mx][0])
lis[my][mx][0]=(lis[my][mx][0]+1)%性质
# print(lis[my][mx][0])
elif choice==-1:
turnmapnow[1]=(turnmapnow[1]+1)%2
turnmapnow[0]=lis[my][mx][0]
def classdeal(line):
cla=line[2]
for i in line[5]:
if type(i)==list:
i[0]+=(cla*i[3])
if i[1]:
i[1]=i[0]
else:
for _,v in i.items():
v[0]+=(cla*v[3])
if v[1]:
v[1]=v[0]
def fullrour():
存档[5][0][1]=存档[5][0][0]
存档[5][3][1]=存档[5][3][0]
存档[5][4][1]=存档[5][4][0]
# 数值 0 血量 1 攻击 2 速度 3 体力 4 外装甲
# [1,1.2,[]]
def abilitychis(ability,A,B,rour=0):
if ability[0]==1:
# print(A[5][3][1],ability[1])
if A[5][3][1]+ability[1]>=0:
A[5][3][1]+=ability[1]
a,b=ability_1(ability,A,B)
try:
HPADD(A,-b*A[5][5]["吸血"])
except:
pass
return (a,b)
else:
if rour==1:
ability=A[10][0]
if ability[0]==1:
if A[5][3][1]+ability[1]>=0:
A[5][3][1]+=ability[1]
a,b=ability_1(ability,A,B)
try:
HPADD(A,-b*A[5][5]["吸血"])
except:
pass
return (a,b)
return (0,0)
def ability_1(ability,A,B):
cls_HAD=A[5][1]
num=ability[2]*cls_HAD[0]*cls_HAD[2]*cls_HAD[4]
tip[0]=f"伤害:{-num}"
tip[1]=150
return decHP(B,num)
def decHP(B,num):
decide=1
dec=0
if B[5][4][1]>0:
B[5][4][1]+=num
if B[5][4][1]<0:
dec=B[5][4][1]
decide=HPADD(B,B[5][4][1])
B[5][4][1]=0
else:
decide=HPADD(B,num)
dec=num
return (decide,dec)
def fight(lines):
global ability
ability=[[1]]
global mx,my
text,textback,abilityback=fightchange(lines)
runlis[2]=True
time1=[]
turndecid=[0,0]
flag=200
while runlis[2]:
turn,turntext,turnback=turnexpor(turndecid,lines)
mx,my= pg.mouse.get_pos()
win.fill((0,0,0))
events=pg.event.get()
count=事件监听(events,abilityback)
if flag:
flag-=5
if flag==0:
if turn[0]==0:
if count<=len(abilityback) and count>=1:
flag=200
re1,re2=abilitychis(存档[10][count-1],存档,lines)
text,textback,abilityback=fightchange(lines)
if re1==re2==0:
tip[0]="条件不足"
tip[1]=100
elif re1==-1:
fightendpicture(1)
return 1
else:
turndecid=turn[1]
elif turn[0]==1:
flag=200
tip[0]="敌方发动攻击"
tip[1]=150
chice=random.randint(1,len(lines[10]))
re1,re2=abilitychis(lines[10][chice-1],lines,存档,rour=1)
text,textback,abilityback=fightchange(lines)
if re1==-1:
fightendpicture(-1)
return 0
else:
turndecid=turn[1]
text1=text+turntext
back1=textback+abilityback+turnback
for i in back1:
if i:
backdeal(i,if_ontxt(i))
for i in text1:
if i:
textdeal(i)
tiptext()
pg.display.update()
clock.tick(帧数)
def tipALL():
global tipblock
x,y,h=(600,150,21)
if len(tipblock[0])<5 and len(tipblock[1])!=0:
tipblock[0].append([200,tipblock[1][0]])
del tipblock[1][0]
for i in range(len(tipblock[0])-1,-1,-1):
if tipblock[0][i][0]<=0:
del tipblock[0][i]
else:
textdeal(tipblock[0][i][1],x=-1,y=-1)
tipblock[0][i][0]-=5
def fightendpicture(chice=0):
tipALL()
if chice==0:
if fightend[0]>0:
for i in fightend[2]:
backdeal(i,size=0,surface=fightend[0])
for i in fightend[1]:
textdeal(i,alpha=fightend[0])
fightend[0]-=3
elif chice==1:
fightend[0]=255
fightend[1].clear()
# fightend[2].clear()
fightend[1].append(文本自定义("战斗胜利",504,100,48,[(80, 255, 80),(255,255,255)],192))
# fightend[2].append(文本自定义("",480,210,130,[(30,30,50),(30,30,50),(100,150,255),(255, 225, 0)],360))
elif chice==-1:
fightend[0]=255
fightend[1].clear()
# fightend[2].clear()
fightend[1].append(文本自定义("战斗失败",504,100,48,[(255, 80, 80),(255,255,255)],192))
# fightend[2].append(文本自定义("",480,340,130,[(30,30,50),(30,30,50),(100,150,255),(255, 225, 0)],360))
def fightchange(lines,num=0):
if num==0:
text=[]
textback=[]
abilityback=[]
x=800
y=300
z=20
text.append(文本自定义(f"""{HP(lines)}""",x,y+20,21,[(255,255,255),(255,255,255)]))
text.append(文本自定义(f"""{DF(lines)}""",x,y+50,21,[(255,255,255),(255,255,255)]))
textback.append(文本自定义("背景",x,y,20,[(95, 12, 12),(255, 45, 45),(95, 12, 12),(255, 45, 45)],20))
text.append(文本自定义(f"""{HP()}""",400,320,21,[(255,80,80),(255,220,220)]))
text.append(文本自定义(f"""{DF()}""",400,340,21,[(100,180,255),(180,220,255)]))
text.append(文本自定义(f"""{AT(num=2)}""",400,360,21,[(60,220,120),(180,255,200)]))
textback.append(文本自定义("背景",400,300,20,[(0,255,0),(0,255,0),(0,255,0),(0,255,0)],20))
x1=200
y1=400
z=0
for i in 存档[10]:
if i!= None:
text.append(文本自定义(f"""{fight_coice[i[0]]}""",x1+z,y1,21,[(255,255,255),(255, 225, 0)]))
text.append(文本自定义(f"""消耗:{-i[1]}""",x1+z,y1+22,21,[(255,255,255),(255, 225, 0)]))
text.append(文本自定义(f"""倍率:{-i[2]}""",x1+z,y1+44,21,[(255,255,255),(255, 225, 0)]))
abilityback.append(文本自定义("技能",x1+z,y1,130,[(30,30,50),(30,30,50),(100,150,255),(255, 225, 0)],250))
z=z+250
return (text,textback,abilityback)
# elif num==1:
def turnexpor(turndecid, lines):
turn1=[]
turn1.append(turndecid[0])
turn1.append(turndecid[1])
x = 0
y = 0
x1 = 4
y1 = 4
H1 = 30
W1 = 100
i = 0
turn = []
turndecid1=[]
turntext = []
turnback = []
backcolor = [(50,50,70),(70,70,100),(120,120,150),(180,180,220)]
# 两层文字:普通、悬浮
txt_self = [(80,255,80),(200,255,200)]
txt_enemy = [(255,80,80),(255,200,200)]
font=18
while len(turn) < 8:
if turn1[0] >= who_turn and turn1[1] >= who_turn:
if turn1[0] >= turn1[1]:
turn1[0] -= who_turn
turn.append([0,[turn1[0],turn1[1]]])
turntext.append(文本自定义("你的回合", x1,y1+i*H1, font, txt_self))
turnback.append(文本自定义("背景",x,y+i*H1,H1,backcolor,W1))
else:
turn1[1] -= who_turn
turn.append([1,[turn1[0],turn1[1]]])
turntext.append(文本自定义("敌人回合", x1,y1+i*H1, font, txt_enemy))
turnback.append(文本自定义("背景",x,y+i*H1,H1,backcolor,W1))
i += 1
elif turn1[0] >= who_turn:
turn1[0] -= who_turn
turn.append([0,[turn1[0],turn1[1]]])
turntext.append(文本自定义("你的回合", x1,y1+i*H1, font, txt_self))
turnback.append(文本自定义("背景",x,y+i*H1,H1,backcolor,W1))
i += 1
elif turn1[1] >= who_turn:
turn1[1] -= who_turn
turn.append([1,[turn1[0],turn1[1]]])
turntext.append(文本自定义("敌人回合", x1,y1+i*H1, font, txt_enemy))
turnback.append(文本自定义("背景",x,y+i*H1,H1,backcolor,W1))
i += 1
# 速度累加也改成 turn1
else:
turn1[0] += 存档[5][2][0]
turn1[1] += int(lines[5][2][0])
return turn[0], turntext, turnback
def eventdecide():
global 存档,坐标
now=lis[坐标["y"]][坐标["x"]]
if now[1]==4 and now[2]==True:
num=fightdeal()
if num==1:
now[2]=False
elif num==-1:
with open(os.path.join(存档文件,存档[0]),"r") as f:
存档.clear()
存档=eval(f.read())
存档[8]['x']=0
存档[8]['y']=0
存档[7]='map1.txt'
坐标=存档[8]
tip[0]='您已死亡'
tip[1]=200
f.close()
读取地图(os.path.join(地图文件,存档[7]))
return 1
# elif mapsign[now[1]] and now[2][1]:
def fightdeal():
dangerlis=怪物分配[存档[7]][0]
mapdanger=怪物分配[存档[7]]
with open(os.path.join(怪物文件,dangerlis[random.randint(0,len(怪物分配[存档[7]])-1)]),"r") as f:
danger=eval(f.read())
danger[2]=random.randint(mapdanger[1][0],mapdanger[1][1])
danger[3]=danger[2]
classdeal(danger)
if fight(danger):
EXPADD(num=danger[3])
tipblock[1].append(文本自定义(f"经验增加:{danger[3]}",-1,-1,weigh=-2))
for k,v in danger[6].items():
存档[6][k]+=v
tipblock[1].append(文本自定义(f"{k}增加:{v}",-1,-1,weigh=-2))
return 1
else:
print("失败")
return -1
# def fightpicture():www
# (220, 30, 30)选中
# 敌人主体底色 (95, 12, 12) 暗红压迫感
# 敌人选中边框 (255, 45, 45) 亮红醒目
# 敌人血条底色 (55, 10, 10)
# 敌人残血高亮 (230, 20, 20)
# 方向移动判定
def 方向移动(key):
for i in range(4):
idx=顺序lis[3-i]
if key[移动lis[idx][0]]==1:
中间量=移动lis[idx]
del 移动lis[idx]
移动lis.append(中间量)
if 移动判定(中间量[2],坐标[中间量[2]]+中间量[1]):
return 中间量
return None
def 方向移动进阶(key):
移动量=方向移动(key)
if 移动量!=None:
if 移动量[2]=='x':
坐标['x']+=移动量[1]
elif 移动量[2]=='y':
坐标['y']+=移动量[1]
return 1
return 0
def 移动判定(atext,记录):
记录x1=坐标['x']
记录y1=坐标['y']
if atext=='x':
节点=lis[记录y1][记录][0]
if whecanmove[节点]:
return True
else:
return False
if atext=='y':
节点=lis[记录][记录x1][0]
if whecanmove[节点]:
return True
else:
return False
def 地图渲染进化版():
记录x1=坐标['x']
记录y1=坐标['y']
坐标map=[]
if 记录x1<insight_x:
显示基准[0]=0
显示基准[1]=insight_x*2
坐标map.append(记录x1)
elif 记录x1>=size_x-insight_x-1:
显示基准[1]=size_x-1
显示基准[0]=size_x-insight_x*2-1
坐标map.append(2*insight_x+记录x1-size_x+1)
else:
显示基准[0]=记录x1-insight_x
显示基准[1]=记录x1+insight_x
坐标map.append(insight_x)
if 记录y1<insight_y:
显示基准[2]=0
显示基准[3]=insight_y*2
坐标map.append(记录y1)
elif 记录y1>=size_y-insight_y-1:
显示基准[3]=size_y-1
显示基准[2]=size_y-insight_y*2-1
坐标map.append(2*insight_y+记录y1-size_y+1)
else:
显示基准[2]=记录y1-insight_y
显示基准[3]=记录y1+insight_y
坐标map.append(insight_y)
for i in range(显示基准[0],显示基准[1]+1):
for j in range(显示基准[2],显示基准[3]+1):
x=i-显示基准[0]
y=j-显示基准[2]
pg.draw.rect(win,COLOR[lis[j][i][0]],(x*GRID_SIZE,y*GRID_SIZE,20,20))
try:
if mapsign[lis[j][i][1]]:
win.blit(mapsign[lis[j][i][1]],(x*GRID_SIZE+7,y*GRID_SIZE+1))
except:
print(lis[j][i], i,j)
if turnmapnow[1]:
lis[坐标['y']][坐标['x']][0]=turnmapnow[0]
pg.draw.rect(win,(0,255,0),(坐标map[0]*speed,坐标map[1]*speed,20,20))
return 坐标map
def classchange1(text):
# 清空文本列表
text.clear()
# 直接操作全局back,而非新建局部变量
back=[]
# 公共配置抽离,统一管理
attr_pos = [30, 590, 150, 0]
base_x, base_y, gap, _ = attr_pos
text_size = 20
btn_x = 850
btn_y = 450
btn_w = 130
btn_color = [(30,30,50),(30,30,50),(100,150,255),(255, 225, 0)]
btn_ext = 360
name_text_cfg = (850, 400, 45, [(255,255,255),(255, 225, 0)])
# 顶部属性文本
t1 = 文本自定义(f"{HP(num=2)}", base_x, base_y, text_size, color=[(255,80,80),(255,255,0)])
t2 = 文本自定义(f"{DF()}", base_x + gap, base_y, text_size, color=[(100,180,255),(255,255,0)])
t3 = 文本自定义(f"{AT(num=1)}", base_x + 2*gap, base_y, text_size, color=[(60,230,120),(255,255,0)])
t4 = 文本自定义(f"{EXP()}", base_x + 3*gap, base_y, text_size, color=[(255,210,60),(255,255,0)])
t5 = 文本自定义("人物", 1100, 590, text_size, color=[(160,160,160),(255,255,255)])
t6 = 文本自定义(f"金币:{存档[6]['gold']}", base_x + 4*gap, base_y, text_size, color=[(255,215,0), (255,230,80)])
text.extend([t1, t2, t3, t4, t5, t6])
block = None
y = 坐标['y']
x = 坐标['x']
# 下标越界防护
if 0 <= y < len(lis) and 0 <= x < len(lis[y]):
if lis[y][x][0] == 9:
block = lis[y][x]
# 绘制NPC名称
text.append(文本自定义(f"{block[2]}", *name_text_cfg))
# NPC与文件映射表,新增/修改只需改这里
npc_file_map = {
"医生": lambda b, s: "事件1.txt",
"协会联络员": lambda b, s: "事件2.txt" if b[3] != s[2] else "事件5.txt",
"士兵": lambda b, s: "事件3.txt" if b[3] > 0 else "事件4.txt"
}
npc_name = block[2]
if npc_name in npc_file_map:
file_name = npc_file_map[npc_name](block, 存档)
back.append(文本自定义(file_name, btn_x, btn_y, btn_w, btn_color, btn_ext))
return back, block
def 属性刷新():
text=[]
textpoint=[]
属性位置=[630,300,0,25]
x,y,_,行距 = 属性位置
# 文本顺序完全不变
文本内容组 = [
f"{HP(num=2)}",
f"{AT(num=1)}",
f"速度:{存档[5][2][0]}",
f"体力:{存档[5][3][1]/存档[5][3][0]}",
f"{DF()}",
f"血量回复:{存档[5][0][2]}",
f"体力回复:{存档[5][3][2]}",
f"剩余点数:{存档[1]}",
f"等级:{存档[2]} {EXP()}"
]
txtcolors = [
[(255,80,80),(255,220,220)], # 1. HP
[(255,160,60),(255,220,180)], # 2. AT
[(60,220,120),(180,255,200)], # 3. 体力
[(80,220,200),(180,240,240)], # 4. 速度
[(100,180,255),(180,220,255)], # 5. DF
[(255,110,110),(255,235,235)], # 6. 血量回复
[(90,235,140),(200,255,215)], # 7. 体力回复
[(200,130,255),(235,205,255)], # 8. 剩余点数
[(255,210,60),(255,240,160)] # 9. 等级经验
]
for i in range(len(文本内容组)):
ty = y + i * 行距
text.append(文本自定义(文本内容组[i],x,ty-1,21,txtcolors[i]))
if i<5:
tp= 文本自定义(f"+1/-{存档[9][i]}点",1000,ty-1,21,txtcolors[i],84)
textpoint.append(tp)
return (text,textpoint)
def 查看界面():
global mx,my
属性位置=[630,300,0,25]
x,y,_,行距 = 属性位置
# 统一共用一套背景四色:底色/悬浮底色/边框/悬浮边框
backcolors = [(30,30,50),(50,50,80),(100,150,255),(255,225,0)]
# 批量生成
text = []
textpoint = []
backgronuds = []
text,textpoint=属性刷新()
for i in range(len(text)):
ty = y + i * 行距
# 背景全部统一一套四色
b = 文本自定义("背景",610,ty+2,26,backcolors,1220)
backgronuds.append(b)
runlis[2]=True
while runlis[2]:
mx ,my = pg.mouse.get_pos()
win.fill((0,0,0))
for i in backgronuds:
backdeal(i,border_radiu=0)
for i in text:
textdeal(i,0)
for i in textpoint:
textdeal(i,choice=if_ontxt(i))
events=pg.event.get()
count=事件监听(events,textpoint)
if count<=5 and count>0:
for i in range(5):
if count==i+1:
if 存档[9][i]<=存档[1]:
存档[1]-=存档[10][i]
存档[5][i]+=1
存档[5][i+6]+=1
text,textpoint = 属性刷新()
else:
tip[0]="点数不够!!"
tip[1]=100
elif count==-1:
runlis[2]=False
tiptext()
pg.display.update()
clock.tick(int(帧数/3))
界面切换=0
def HP(存档_1=None,num=3):
if 存档_1==None:
HP存=存档[5][0]
else:
HP存=存档_1[5][0]
if num==1:
return f"血量:{int(HP存[0])}"
elif num==2:
return f"血量:{int(HP存[1])}/{int(HP存[0])}"
elif num==3:
return f"血量:{int(HP存[1])}/{int(HP存[0])} 回复力:{int(HP存[2])}"
def HPADD(A,num):
A[5][0][1]+=num
if A[5][0][1]>A[5][0][0]:
A[5][0][1]=A[5][0][0]
elif A[5][0][1]<=0:
return -1
return 1
def AT(存档_1=None,num=3):
if 存档_1==None:
AT1=存档[5][1][0]
AT2=存档[5][3]
else:
AT1=存档_1[5][1][0]
AT2=存档_1[5][3]
if num==1:
return f"力量:{int(AT1)}"
elif num==2:
return f"力量:{int(AT1)} 体力:{int(AT2[1])}/{int(AT2[0])}"
elif num==3:
return f"力量:{int(AT1)} 体力:{int(AT2[1])}/{int(AT2[0])} 回复力:{int(AT2[2])}"
def DF(存档_1=None,num=2):
if 存档_1==None:
DF1=存档[5][4]
else:
DF1=存档_1[5][4]
if num==1:
return f"装甲:{int(DF1[0])}"
elif num==2:
return f"装甲:{int(DF1[1])}/{int(DF1[0])}"
def EXP(存档_1=None,num=2):
if 存档_1==None:
if num==1:
return f"经验:{存档[3]}"
elif num==2:
return f"经验:{存档[3]}/{20*存档[2]**2}"
else:
if num==1:
return f"经验:{存档_1[3]}"
elif num==2:
return f"经验:{存档_1[3]}/{20*存档_1[2]**2}"
def EXPADD(存档_1=None,num=0):
if 存档_1==None:
存档_1=存档
存档_1[2]+=num
print("经验:",存档_1[2])
if 存档_1[3]>=20*存档_1[2]**2:
存档_1[3]-=20*存档_1[2]**2
存档_1[2]+=1
存档_1[1]+=10
print(存档_1)
def goldADD(存档_1=None,num=0):
if 存档_1==None:
存档_1=存档
if 存档[6]["gold"]+num<0:
tip[0]=f"金币不足,仅可以治疗"
tip[1]=150
return False
else:
存档[6]["gold"]+=num
return True
def abilityADD(num=0):
存档[1]+=num
# if 存档_1==None:
# 存档
while runlis[0]:
if 界面切换==0:
界面切换=游戏开始界面()
elif 界面切换==1:
界面切换=游戏进入()
elif 界面切换==-1:
退出()
win.fill((0,0,0))
pg.display.update()
clock.tick(帧数)
if lis:
地图输入(os.path.join(地图文件,存档[7]))
keeprourfile="存档"
if len(存档)>3:
with open(os.path.join(keeprourfile,存档[0]),"w") as f:
f.write(str(存档))
f.close()
pg.quit()
游戏界面

战斗画面

胜利画面

对话画面

属性画面

4.gitee代码提交截图
完整文件甲界面截图

代码截图
















代码地址
建议下整个文件夹运行,操作中含文件打开,可能报错
完整文件夹地址
主代码地址
视屏地址
3. 实验过程中遇到的问题和解决过程
开发过程中遇到多处典型问题,逐一排查并优化:
3.1 问题 1:eval解析文件报语法错误
原因:配置文件中布尔值拼写错误Ture,且原生eval安全性低。
解决方案:修正拼写为True,改用ast.literal_eval解析列表数据。
3.2 问题 2:eval无法执行赋值语句
原因:eval仅支持表达式,不支持+=、=等赋值语句。
解决方案:区分函数使用,纯函数调用用eval,赋值 / 自增语句统一改用exec。
3.3 问题 3:列表下标越界报错
原因:nowpage列表长度不足,直接使用下标[2]赋值;地图坐标超出列表范围。
解决方案:增加列表长度校验、坐标边界判断,从源头防止越界崩溃。
3.4 问题 4:中文文本无法自动换行
原因:Pygame 原生render不支持换行,中文、标点宽度不一致。
解决方案:判断汉字与中文标点字符,按字符宽度累计,超出设定宽度自动拆分换行。
3.5 问题 5:代码冗余、重复参数过多
原因:界面元素坐标、颜色、尺寸大量硬编码,维护困难。
解决方案:提取公共参数为变量,使用字典映射简化多分支判断,精简代码。
3.6 问题 6:全局变量与局部变量冲突
原因:函数内新建同名局部列表,导致全局数据无法更新。
解决方案:使用global关键字声明全局变量,或直接操作原列表。
4.全课总结
本次 Python 公选课从基础语法到综合项目,我完成了完整的学习与实践,整体总结如下:
4.1 基础知识掌握
基础语法:熟练掌握变量、数据类型、运算符、分支语句if-elif、循环语句for/while,能够独立编写逻辑代码;
容器类型:深入理解列表、字典的使用,本项目大量使用二维列表存储地图、事件、怪物数据,字典存储角色金币、配置映射;
函数编程:理解函数封装、参数传递、全局 / 局部变量作用域,学会将重复逻辑封装为通用函数,提升代码复用率;
文件操作:掌握open函数读写本地文件,理解编码、文件关闭、异常捕获等要点,实现项目存档功能。
正则表达式运用:能够运用正则表达式,来经行输入,经行数据的查早,更改,这在综合实验的更改变量错误,查找错误上起到了极为重要的作用
字符串:了解一些字符串处理函数,比如strip()等
类的使用和封装:了解了类的子和父的关系,继承等,会基础的使用类,也会本地类的引用,让我更加了解python。
rocket:了解套接字,同时会使用套接字传递信息到其他信息。会更改IPV4了解查看自己的IP等。
4.2 拓展知识掌握
第三方库:学会 Pygame 库的基础使用,理解窗口创建、图形绘制、文字渲染、事件循环、动画刷新等 GUI 开发核心思想;
异常处理:学会使用try-except捕获运行异常,提升程序稳定性;
代码优化思想:理解模块化编程、解耦、参数抽离、字典映射替代多分支等优化思路,告别堆砌式代码。
云服务器:了解了云服务器的开启,以及其便捷性
html:了解了一定的html,css,js等代码。
4.3 综合能力提升
逻辑思维:开发游戏需要梳理复杂的业务逻辑(回合、移动、交互),极大锻炼了逻辑拆解能力;
调试排错:从语法报错、逻辑报错到隐性 BUG,学会根据报错信息定位问题,掌握 Python 程序调试方法;
工程思维:不再只追求 “代码能运行”,而是考虑代码可读性、可维护性、稳定性。
5.课程感想体会
经过一学期的 Python 程序设计课程学习,我从对Python这门语言的感兴趣,到真正科学的,全面的掌握了 Python 的基础语法、逻辑思维和代码编写方式。最开始接触编程时,我对变量、循环、条件判断、函数、文件操作等内容并不熟悉,很多都是直接找AI质询,然而AI的代码,往往过于高阶不利于理解,有时还会出现设备不相容的情况,经常出现语法错误、逻辑混乱、代码无法运行等问题。但随着课堂学习和课后不断练习,我开始逐步的掌握简单且基础的语法,这为我理解那些先前询问的提供了平台,是我从知其然,到知其所以然的转变。
本学期最大的收获,是最后完成的RPG 回合制小游戏综合项目。从最基础的窗口创建、文字绘制、鼠标键盘监听,到实现地图渲染、角色移动、NPC 交互、回合战斗、属性养成、存档读档等完整功能,我一步步把课堂知识落地成了可视化、可交互的成品项目。在开发过程中,我遇到了大量问题,例如下标越界、eval 与 exec 用法混淆、全局变量冲突、文本无法自动换行、文件解析报错、功能逻辑混乱等。一次次报错、排查、修改、优化的过程,让我真正学会了独立查错、独立思考、独立实现功能。
这门课让我明白,编程不是机械敲代码,而是用逻辑解决现实问题,很多时候由于人脑的灵活性,我们往往可以凭借经验跳过许多复杂的思考过程,这是优点也是缺点,学习这门课,使得我懂得如何从机器的语言方向来理解问题,使得我可以更加科学的解决问题。课堂上学的列表、字典、循环、函数、文件读写看似简单,但组合起来就能实现完整的游戏系统。同时我也体会到了模块化编程、代码优化、代码规范的重要性,重复代码过多、变量混乱、不做容错判断,会让程序极难维护。
通过本次课程实践,我的逻辑思维、耐心、细心和问题解决能力都得到了很大提升。从只会写简单的控制台代码,到能独立完成一个功能丰富的游戏项目,是我本学期最大的进步。未来我也会继续学习 Python 相关知识,尝试更多有趣的项目,真正把编程变成自己的实用技能。此外,将来我一定要把我的游戏做出来,制作一个我构思中的游戏,是我从小到大所一直渴望的。
6 意见和建议
我十分的喜欢Python这节课,它比C语言更简单,同时有更丰富的库函数可以使用,令人眼前一新,导致我往往下课后,都还沉浸在代码的海洋里,所以建议给下一届延长一点课程时间,让他们也能体会到代码的快乐,同时布置一些可以应用在生活中的作业,可以让学生切实感受到,代码正在改变世界,且这种力量不仅仅是那些编程大佬所拥有的,也是我们触手可及的

浙公网安备 33010602011771号