Bootstrap

毕业设计 经典魔塔游戏设计与实现 (源码)


0 项目简介

🔥 Hi,各位同学好呀,这里是L学长!

🥇今天向大家分享一个今年(2022)最新完成的毕业设计项目作品

python小游戏毕设 经典魔塔游戏设计与实现 (源码)

🥇 学长根据实现的难度和等级对项目进行评分(最低0分,满分5分)

1 游戏介绍

《魔塔》是一种策略类的固定数值RPG游戏。游戏需要动很多脑筋,任何一个轻率的选择都可能导致游戏的失败。魔塔游戏虽不大,但是制作精美,道具很多,而且难度不低,对智商是一次艰巨的考验。

虽然魔塔的界面很像是一般的地牢游戏,貌似随便的打打杀杀就可以过关,但事实上玩这个游戏需要动很多脑筋,任何一个轻率的选择都可能导致游戏的失败,该游戏有属性攻击、防御、生命、金币、经验。对怪物的伤害次数计算公式,是敌人的生命/(自己的攻击-敌人的防御);而伤害的数目计算是怪物伤害次数(敌人的攻击-自己的防御)。

今天我们用python复现魔塔游戏。

2 实现效果

在这里插入图片描述
在这里插入图片描述

3 开发工具

3.1 环境配置

  • Python版本:3.6.4

  • 相关模块:

  • pygame模块;

  • 以及一些Python自带的模块。

3.2 Pygame介绍

简介

Pygame是一系列专门为编写电子游戏而设计的Python模块(modules)。Pygame在已经非常优秀的SDL库的基础上增加了许多功能。这让你能够用Python语言编写出丰富多彩的游戏程序。

Pygame可移植性高,几乎能在任何平台和操作系统上运行。

Pygame已经被下载过数百万次。

Pygame免费开源。它在LGPL许可证(Lesser General Public License,GNU宽通用公共许可证)下发行。使用Pygame,你可以创造出免费开源,可共享,或者商业化的游戏。详情请见LGPL许可证。

优点

  • 能够轻松使用多核CPU(multi core CPUs) :如今双核CPU很常用,8核CPU在桌面系统中也很便宜,而利用好多核系统,能让你在你的游戏中实现更多东西。特定的pygame函数能够释放令人生畏的python GIL(全局解释器锁),这几乎是你用C语言才能做的事。

  • 核心函数用最优化的C语言或汇编语言编写:C语言代码通常比Python代码运行速度快10-20倍。而汇编语言编写的代码(assembly code)比Python甚至快到100多倍。

  • 安装便捷:一般仅需包管理程序或二进制系统程序便能安装。

  • 真正地可移植:支持Linux (主要发行版), Windows (95, 98, ME, 2000, XP, Vista, 64-bit Windows,), Windows CE, BeOS, MacOS, Mac OS X, FreeBSD, NetBSD, OpenBSD, BSD/OS, Solaris, IRIX, and QNX等操作系统.也能支持AmigaOS, Dreamcast, Atari, AIX, OSF/Tru64, RISC OS, SymbianOS and OS/2,但是还没有受到官方认可。你也可以在手持设备,游戏控制台, One Laptop Per Child (OLPC) computer项目的电脑等设备中使用pygame.

  • 用法简单:无论是小孩子还是大人都能学会用pygame来制作射击类游戏。

  • 很多Pygame游戏已发行:其中包括很多游戏大赛入围作品、非常受欢迎的开源可分享的游戏。

  • 由你来控制主循环:由你来调用pygame的函数,pygame的函数并不需要调用你的函数。当你同时还在使用其他库来编写各种各种的程序时,这能够为你提供极大的掌控权。

  • 不需要GUI就能使用所有函数:仅在命令行中,你就可以使用pygame的某些函数来处理图片,获取游戏杆输入,播放音乐……

  • 对bug反应迅速:很多bug在被上报的1小时内就能被我们修复。虽然有时候我们确实会卡在某一个bug上很久,但大多数时候我们都是很不错的bug修复者。如今bug的上报已经很少了,因为许多bug早已被我们修复。

  • 代码量少:pygame并没有数以万计的也许你永远用不到的冗杂代码。pygame的核心代码一直保持着简洁特点,其他附加物诸如GUI库等,都是在核心代码之外单独设计研发的。

  • 模块化:你可以单独使用pygame的某个模块。想要换着使用一个别的声音处理库?没问题。pygame的很多核心模块支持独立初始化与使用。

最小开发框架

import pygame,sys #sys是python的标准库,提供Python运行时环境变量的操控

pygame.init()  #内部各功能模块进行初始化创建及变量设置,默认调用
size = width,height = 800,600  #设置游戏窗口大小,分别是宽度和高度
screen = pygame.display.set_mode(size)  #初始化显示窗口
pygame.display.set_caption("小游戏程序")  #设置显示窗口的标题内容,是一个字符串类型
while True:  #无限循环,直到Python运行时退出结束
    for event in pygame.event.get():  #从Pygame的事件队列中取出事件,并从队列中删除该事件
        if event.type == pygame.QUIT:  #获得事件类型,并逐类响应
            sys.exit()   #用于退出结束游戏并退出          
    pygame.display.update()  #对显示窗口进行更新,默认窗口全部重绘

代码执行流程

在这里插入图片描述

4 具体实现

首先,我们打开原版游戏的开始界面,发现是这样的:

具体而言,我们的思路是先定义一个按钮类,来模拟原始游戏中的“开始游戏”,“游戏说明”和“离开游戏”这三个按键的功能:


'''按钮类'''
class Button(pygame.sprite.Sprite):
    def __init__(self, text, fontpath, fontsize, position, color_selected=(255, 0, 0), color_default=(255, 255, 255)):
        pygame.sprite.Sprite.__init__(self)
        self.text = text
        self.color_selected = color_selected
        self.color_default = color_default
        self.font = pygame.font.Font(fontpath, fontsize)
        self.font_render = self.font.render(text, True, color_default)
        self.rect = self.font_render.get_rect()
        self.rect.center = position
    '''更新函数: 不断地更新检测鼠标是否在按钮上'''
    def update(self):
        mouse_pos = pygame.mouse.get_pos()
        if self.rect.collidepoint(mouse_pos):
            self.font_render = self.font.render(self.text, True, self.color_selected)
        else:
            self.font_render = self.font.render(self.text, True, self.color_default)
    '''绑定到屏幕上'''
    def draw(self, screen):
        screen.blit(self.font_render, self.rect)

主要的原理就是不断检测鼠标是否在对应的按钮区域,如果在,则对按钮的颜色做出改变(这里是变成红色),否则按钮使用默认的颜色(这里是白色),以此来向玩家表明这是可点击的按钮。

然后,我们来实例化它三次来添加这三个按钮到游戏的开始界面中:


'''游戏开始界面'''
class StartGameInterface():
    def __init__(self, cfg):
        self.cfg = cfg
        self.play_btn = Button('开始游戏', cfg.FONTPATH_CN, 50, (cfg.SCREENSIZE[0]//2, cfg.SCREENSIZE[1] - 400))
        self.intro_btn = Button('游戏说明', cfg.FONTPATH_CN, 50, (cfg.SCREENSIZE[0]//2, cfg.SCREENSIZE[1] - 300))
        self.quit_btn = Button('离开游戏', cfg.FONTPATH_CN, 50, (cfg.SCREENSIZE[0]//2, cfg.SCREENSIZE[1] - 200))
    '''外部调用'''
    def run(self, screen):
        # 魔塔
        font = pygame.font.Font(self.cfg.FONTPATH_CN, 80)
        font_render_cn = font.render('魔塔', True, (255, 255, 255))
        rect_cn = font_render_cn.get_rect()
        rect_cn.center = self.cfg.SCREENSIZE[0] // 2, 200
        # Magic Tower
        font = pygame.font.Font(self.cfg.FONTPATH_EN, 80)
        font_render_en = font.render('Magic Tower', True, (255, 255, 255))
        rect_en = font_render_en.get_rect()
        rect_en.center = self.cfg.SCREENSIZE[0] // 2, 350
        # (Ver 1.12)
        font = pygame.font.Font(self.cfg.FONTPATH_CN, 40)
        font_render_version = font.render('(Ver 1.12)', True, (255, 255, 255))
        rect_ver = font_render_version.get_rect()
        rect_ver.center = self.cfg.SCREENSIZE[0] // 2, 400
        clock = pygame.time.Clock()
        while True:
            screen.fill((0, 0, 0))
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit(0)
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 1:
                        mouse_pos = pygame.mouse.get_pos()
                        if self.play_btn.rect.collidepoint(mouse_pos):
                            return True
                        elif self.quit_btn.rect.collidepoint(mouse_pos):
                            pygame.quit()
                            sys.exit(0)
                        elif self.intro_btn.rect.collidepoint(mouse_pos):
                            self.showgameintro(screen)
            for btn in [self.intro_btn, self.play_btn, self.quit_btn]:
                btn.update()
                btn.draw(screen)
            for fr, rect in zip([font_render_cn, font_render_en, font_render_version], [rect_cn, rect_en, rect_ver]):
                screen.blit(fr, rect)
            pygame.display.flip()
            clock.tick(self.cfg.FPS)
    '''显示游戏简介'''
    def showgameintro(self, screen):
        font = pygame.font.Font(self.cfg.FONTPATH_CN, 20)
        font_renders = [
            font.render('魔塔小游戏.', True, (255, 255, 255)),
            font.render('游戏素材来自: http://www.4399.com/flash/1749_1.htm.', True, (255, 255, 255)),
            font.render('游戏背景故事为公主被大魔王抓走, 需要勇士前往魔塔将其救出.', True, (255, 255, 255)),
            font.render('作者: Charles.', True, (255, 255, 255)),
            font.render('微信公众号: Charles的皮卡丘.', True, (255, 255, 255)),
            font.render('版权所有, 请勿随意删除转载.', True, (255, 255, 255)),
        ]
        rects = [fr.get_rect() for fr in font_renders]
        for idx, rect in enumerate(rects):
            rect.center = self.cfg.SCREENSIZE[0] // 2, 50 * idx + 100
        clock = pygame.time.Clock()
        while True:
            screen.fill((0, 0, 0))
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit(0)
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 1:
                        mouse_pos = pygame.mouse.get_pos()
                        if self.play_btn.rect.collidepoint(mouse_pos):
                            return True
                        elif self.quit_btn.rect.collidepoint(mouse_pos):
                            pygame.quit()
                            sys.exit(0)
                        elif self.intro_btn.rect.collidepoint(mouse_pos):
                            return
            for btn in [self.intro_btn, self.play_btn, self.quit_btn]:
                btn.update()
                btn.draw(screen)
            for fr, rect in zip(font_renders, rects):
                screen.blit(fr, rect)
            pygame.display.flip()
            clock.tick(self.cfg.FPS)

其他额外的代码主要是显示游戏的标题等信息,都很简单,就不详细讨论了,会pygame的小伙伴肯定都能写出来,最后实现的效果如下:

在这里插入图片描述
接下来,我们来看下游戏开始之后的界面是长什么样子的,我们可以发现一般都是以下这个样子的(为什么我会说一般呢? 因为为了写这个游戏,我去重玩了一波,发现一时半会我还过不了所有关。有攻略或者随便通关的小伙伴看到我复现有不对的地方可以给我留言并附上对应的截图,可能我玩游戏比较菜):

在这里插入图片描述

具体而言,我们可以先在文本文件里定义游戏地图的样子,类似下图所示这样子,其中每个数字代表一种游戏元素:

在这里插入图片描述
游戏中的图片素材我也已经收集到了(这个是网上找到的别人整理好的游戏素材,不是我自己扣的T_T):

在这里插入图片描述
于是,我们可以写一个游戏地图文件的解析类,就像这样:

'''游戏地图解析类'''
class MapParser():
    def __init__(self, blocksize, filepath, element_images, offset=(0, 0), **kwargs):
        self.count = 0
        self.switch_times = 15
        self.image_pointer = 0
        self.offset = offset
        self.blocksize = blocksize
        self.element_images = element_images
        self.map_matrix = self.parse(filepath)
    '''解析'''
    def parse(self, filepath):
        map_matrix = []
        with open(filepath, 'r') as fp:
            for line in fp.readlines():
                line = line.strip()
                if not line: continue
                map_matrix.append([c.strip() for c in line.split(',')])
        return map_matrix
    '''将游戏地图画到屏幕上'''
    def draw(self, screen):
        self.count += 1
        if self.count == self.switch_times:
            self.count = 0
            self.image_pointer = int(not self.image_pointer)
        for row_idx, row in enumerate(self.map_matrix):
            for col_idx, elem in enumerate(row):
                position = col_idx * self.blocksize + self.offset[0], row_idx * self.blocksize + self.offset[1]
                if elem+'.png' in self.element_images:
                    image = self.element_images[elem+'.png'][self.image_pointer]
                    image = pygame.transform.scale(image, (self.blocksize, self.blocksize))
                    screen.blit(image, position)

其中parse函数其实就是读取存放游戏地图信息的文本文件,然后draw函数其实就是根据读取的地图信息,将对应的游戏元素图片绑定到地图上进行显示。另外,image_pointer,switch_times,count这三个变量是为了实现原版地图中场景元素闪烁的效果,就像这样:

在这里插入图片描述
根据这个原理,我们可以轻松地画出魔塔所有层中的原始地图,定义的游戏地图文件如下图所示:
在这里插入图片描述
效果如下图所示:
在这里插入图片描述
然后,我们来定义一下我们的英雄勇士类:


'''定义我们的主角勇士'''
class Hero(pygame.sprite.Sprite):
    def __init__(self, imagepaths, blocksize, position, fontpath=None):
        pygame.sprite.Sprite.__init__(self)
        # 设置基础属性
        self.font = pygame.font.Font(fontpath, 40)
        # 加载对应的图片
        self.images = {}
        for key, value in imagepaths.items():
            self.images[key] = pygame.transform.scale(pygame.image.load(value), (blocksize, blocksize))
        self.image = self.images['down']
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = position
        # 设置等级等信息
        self.level = 1
        self.life_value = 1000
        self.attack_power = 10
        self.defense_power = 10
        self.num_coins = 0
        self.experience = 0
        self.num_yellow_keys = 0
        self.num_purple_keys = 0
        self.num_red_keys = 0
    '''将勇士绑定到屏幕上'''
    def draw(self, screen):
        screen.blit(self.image, self.rect)

就是最简单的游戏精灵定义,将其绑定到游戏主界面之后的效果如下:
在这里插入图片描述
看起来是不是哪里不对?没错,左边原来有文字显示勇士当前的状态呀!现在都没了!不过没关系,问题不大,我们写几行代码将英雄的信息显示在左边的面板上面即可:


font_renders = [
    self.font.render(str(self.level), True, (255, 255, 255)),
    self.font.render(str(self.life_value), True, (255, 255, 255)),
    self.font.render(str(self.attack_power), True, (255, 255, 255)),
    self.font.render(str(self.defense_power), True, (255, 255, 255)),
    self.font.render(str(self.num_coins), True, (255, 255, 255)),
    self.font.render(str(self.experience), True, (255, 255, 255)),
    self.font.render(str(self.num_yellow_keys), True, (255, 255, 255)),
    self.font.render(str(self.num_purple_keys), True, (255, 255, 255)),
    self.font.render(str(self.num_red_keys), True, (255, 255, 255)),
]
rects = [fr.get_rect() for fr in font_renders]
rects[0].topleft = (160, 80)
for idx in range(1, 6):
    rects[idx].topleft = 160, 127 + 42 * (idx - 1)
for idx in range(6, 9):
    rects[idx].topleft = 160, 364 + 55 * (idx - 6)
for fr, rect in zip(font_renders, rects):
    screen.blit(fr, rect)

效果是这样子的:
在这里插入图片描述
这样看起来就舒服多了。或许有小伙伴会问了,你绑定的位置坐标都是咋确定的呀?这个嘛,就是简单地试几个坐标,然后目测一下准不准该怎么调整坐标值就行了,如果你不嫌麻烦,也可以在画图软件里直接查看对应位置的像素坐标,都是ok的,条条大路通罗马嘛~

完成了勇士类最基础的定义,接下来我们就该让他动起来啦,具体而言,我们先实现一个勇士行动的类函数:


'''行动'''
def move(self, direction):
    assert direction in self.images
    self.image = self.images[direction]
    move_vector = {
        'left': (-self.blocksize, 0),
        'right': (self.blocksize, 0),
        'up': (0, -self.blocksize),
        'down': (0, self.blocksize),
    }[direction]
    self.rect.left += move_vector[0]
    self.rect.top += move_vector[1]

然后写个按键检测,并根据玩家按下的键值来决定勇士的行动方向:

key_pressed = pygame.key.get_pressed()
if key_pressed[pygame.K_w] or key_pressed[pygame.K_UP]:
    self.hero.move('up')
elif key_pressed[pygame.K_s] or key_pressed[pygame.K_DOWN]:
    self.hero.move('down')
elif key_pressed[pygame.K_a] or key_pressed[pygame.K_LEFT]:
    self.hero.move('left')
elif key_pressed[pygame.K_d] or key_pressed[pygame.K_RIGHT]:
    self.hero.move('right')

如果你觉得我上面的代码写的没问题,大功告成了,那么你一定不是公众号的老粉图片,显然,这样写是有两个问题的。

首先,这样子写会导致玩家按一次上键,勇士就移动很多格,导致玩家不好控制勇士的位置,此时我们可以添加一个行动冷却变量:

# 行动冷却
self.move_cooling_count = 0
self.move_cooling_time = 5
self.freeze_move_flag = False

在冷却中的时候进行计数:

if self.freeze_move_flag:
    self.move_cooling_count += 1
    if self.move_cooling_count > self.move_cooling_time:
        self.move_cooling_count = 0
        self.freeze_move_flag = False

计数完成后英雄方可恢复行动能力。于是move可以重写成:

'''行动'''
def move(self, direction):
    if self.freeze_move_flag: return
    assert direction in self.images
    self.image = self.images[direction]
    move_vector = {
        'left': (-self.blocksize, 0),
        'right': (self.blocksize, 0),
        'up': (0, -self.blocksize),
        'down': (0, self.blocksize),
    }[direction]
    self.rect.left += move_vector[0]
    self.rect.top += move_vector[1]
    self.freeze_move_flag = True

感兴趣的小伙伴可以自行去掉这段代码实际感受一下键盘操作我们的勇士时是否会存在区别。

另外一个问题,也是最严重的问题,那就是行动会不合法,比如勇士会出现在这样的位置:
在这里插入图片描述

'''行动'''
def move(self, direction, map_parser):
    if self.freeze_move_flag: return
    assert direction in self.images
    self.image = self.images[direction]
    move_vector = {'left': (-1, 0), 'right': (1, 0), 'up': (0, -1), 'down': (0, 1)}[direction]
    block_position = self.block_position[0] + move_vector[0], self.block_position[1] + move_vector[1]
    if block_position[0] >= 0 and block_position[0] < map_parser.map_size[1] and \
            block_position[1] >= 0 and block_position[1] < map_parser.map_size[0]:
        if map_parser.map_matrix[block_position[1]][block_position[0]] in ['0']:
            self.block_position = block_position
        elif map_parser.map_matrix[block_position[1]][block_position[0]] in ['24']:
            self.dealcollideevent(
                elem=map_parser.map_matrix[block_position[1]][block_position[0]],
                block_position=block_position,
                map_parser=map_parser,
            )
    self.rect.left, self.rect.top = self.block_position[0] * self.blocksize + self.offset[0], self.block_position[1] * self.blocksize + self.offset[1]
    self.freeze_move_flag = True

这里,为了方便判断,我们将原来采用的像素坐标改成了游戏地图中的元素块坐标(即上一期设计的游戏地图里,每个数字在地图矩阵中的位置索引)。另外,这里我们还需要想到的一个点是未来进一步复现游戏的过程中,我们需要在勇士和地图中一些元素发生碰撞时作出对应的响应,例如勇士和怪物进行决斗,捡到钥匙等等事件,因此我们也在上面的move函数中嵌入了dealcollideevent来处理这样的情况,一个简单效果展示如下:
在这里插入图片描述
当然,理论上按照原版的游戏这里应该是有一个背景故事的对话框的,这部分我们下一期再实现,本期我们主要实现一些基础的功能,比如一些简单事件的触发,包括遇到门,捡到钥匙等等:

'''处理撞击事件'''
def dealcollideevent(self, elem, block_position, map_parser):
    # 遇到不同颜色的门, 有钥匙则打开, 否则无法前进
    if elem in ['2', '3', '4']:
        flag = False
        if elem == '2' and self.num_yellow_keys > 0:
            self.num_yellow_keys -= 1
            flag = True
        elif elem == '3' and self.num_purple_keys > 0:
            self.num_purple_keys -= 1
            flag = True
        elif elem == '4' and self.num_red_keys > 0:
            self.num_red_keys -= 1
            flag = True
        if flag: map_parser.map_matrix[block_position[1]][block_position[0]] = '0'
        return flag
    # 捡到不同颜色的钥匙
    elif elem in ['6', '7', '8']:
        if elem == '6': self.num_yellow_keys += 1
        elif elem == '7': self.num_purple_keys += 1
        elif elem == '8': self.num_red_keys += 1
        map_parser.map_matrix[block_position[1]][block_position[0]] = '0'
        return True
    # 捡到宝石
    elif elem in ['9', '10']:
        if elem == '9': self.defense_power += 3
        elif elem == '10': self.attack_power += 3
        map_parser.map_matrix[block_position[1]][block_position[0]] = '0'
        return True
    # 遇到仙女, 进行对话, 并左移一格
    elif elem in ['24']:
        map_parser.map_matrix[block_position[1]][block_position[0] - 1] = elem
        map_parser.map_matrix[block_position[1]][block_position[0]] = '0'
        return False

最后,我们来实现一下勇士上下楼梯时切换当前游戏地图的效果,这咋听起来似乎有点难办,但其实不然,只需要将发生上下楼梯事件的命令返回到游戏主循环:

# 上下楼梯
elif elem in ['13', '14']:
    if elem == '13': events = ['upstairs']
    elif elem == '14': events = ['downstairs']
    return True, events

'''行动'''
def move(self, direction, map_parser):
    # 判断是否冷冻行动
    if self.freeze_move_flag: return
    assert direction in self.images
    self.image = self.images[direction]
    # 移动勇士
    move_vector = {'left': (-1, 0), 'right': (1, 0), 'up': (0, -1), 'down': (0, 1)}[direction]
    block_position = self.block_position[0] + move_vector[0], self.block_position[1] + move_vector[1]
    # 判断该移动是否合法, 并触发对应的事件
    events = []
    if block_position[0] >= 0 and block_position[0] < map_parser.map_size[1] and \
            block_position[1] >= 0 and block_position[1] < map_parser.map_size[0]:
        # --合法移动
        if map_parser.map_matrix[block_position[1]][block_position[0]] in ['0']:
            self.block_position = block_position
        # --触发事件
        elif map_parser.map_matrix[block_position[1]][block_position[0]] in ['2', '3', '4', '6', '7', '8', '9', '10', '13', '14', '24']:
            flag, events = self.dealcollideevent(
                elem=map_parser.map_matrix[block_position[1]][block_position[0]],
                block_position=block_position,
                map_parser=map_parser,
            )
            if flag: self.block_position = block_position
    # 重新设置勇士位置
    self.rect.left, self.rect.top = self.block_position[0] * self.blocksize + self.offset[0], self.block_position[1] * self.blocksize + self.offset[1]
    # 冷冻行动
    self.freeze_move_flag = True
    # 返回需要在主循环里触发的事件
    return events

然后在主循环中进行响应即可:


# --触发游戏事件
for event in move_events:
    if event == 'upstairs':
        self.map_level_pointer += 1
        self.loadmap()
    elif event == 'downstairs':
        self.map_level_pointer -= 1
        self.loadmap()

上下楼梯切换游戏地图时,我们可以利用该标识符重置角色所在的位置:

# --触发游戏事件
for event in move_events:
    if event == 'upstairs':
        self.map_level_pointer += 1
        self.loadmap()
        self.hero.placenexttostairs(self.map_parser, 'down')
    elif event == 'downstairs':
        self.map_level_pointer -= 1
        self.loadmap()
        self.hero.placenexttostairs(self.map_parser, 'up')

其中重置位置的函数实现如下:

'''放置到上/下楼梯口旁'''
def placenexttostairs(self, map_parser, stairs_type='up'):
    assert stairs_type in ['up', 'down']
    for row_idx, row in enumerate(map_parser.map_matrix):
        for col_idx, elem in enumerate(row):
            if (stairs_type == 'up' and elem == '13') or (stairs_type == 'down' and elem == '14'):
                if row_idx > 0 and map_parser.map_matrix[row_idx - 1][col_idx] == '00':
                    self.block_position = col_idx, row_idx - 1
                elif row_idx < map_parser.map_size[0] - 1 and map_parser.map_matrix[row_idx + 1][col_idx] == '00':
                    self.block_position = col_idx, row_idx + 1
                elif col_idx > 0 and map_parser.map_matrix[row_idx][col_idx - 1] == '00':
                    self.block_position = col_idx - 1, row_idx
                elif col_idx < map_parser.map_size[1] - 1 and map_parser.map_matrix[row_idx][col_idx + 1] == '00':
                    self.block_position = col_idx + 1, row_idx
    self.rect.left, self.rect.top = self.block_position[0] * self.blocksize + self.offset[0], self.block_position[1] * self.blocksize + self.offset[1]

5 最后

项目获取:https://gitee.com/sinonfin/system-sharing

;