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()
3.运行结果截图(视频见最下方地址)

游戏界面
image
战斗画面
image
胜利画面
image
对话画面
image
属性画面
image

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

代码地址

建议下整个文件夹运行,操作中含文件打开,可能报错
完整文件夹地址
主代码地址
视屏地址

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语言更简单,同时有更丰富的库函数可以使用,令人眼前一新,导致我往往下课后,都还沉浸在代码的海洋里,所以建议给下一届延长一点课程时间,让他们也能体会到代码的快乐,同时布置一些可以应用在生活中的作业,可以让学生切实感受到,代码正在改变世界,且这种力量不仅仅是那些编程大佬所拥有的,也是我们触手可及的

其他(感悟、思考等)

参考资料

posted @ 2026-06-02 17:37  秦家昌  阅读(27)  评论(0)    收藏  举报