Topic 4.3 - 地图类的设计与实现¶
地图类是我们这个游戏的核心,当中很多重要的逻辑都在这里实现。我们先来回顾一下地图类的设计思路:
-
首先,地图类会加载地图文件,并将地图内容存储在一个二维列表中
- 例如,如果我们的地图中的两行是
#P..S..^...#和#^^^..~..H.#,那么存储在二维列表中的形式就是:
[ ['#', 'P', '.', '.', 'S', '.', '.', '^', '.', '.', '.', '#'], ['#', '^', '^', '^', '.', '.', '~', '.', '.', 'H', '.', '#'] ]-
并且,我们使用一个二元组
(x, y)来表示地图上的位置坐标,x表示列索引,y表示行索引,比方说上面例子中:-
玩家
P的位置就是(1, 0),在二维列表中的索引是[0][1] -
剑
S的位置是(4, 0),在二维列表中的索引是[0][4] -
生命药水
H的位置是(9, 1),在二维列表中的索引是[1][9]
-
- 例如,如果我们的地图中的两行是
-
其次,地图还负责玩家移动
- 比方说,玩家当前的位置是
(1, 0),玩家输入向右移动,那么新的位置就是(2, 0) - 玩家在到了新的位置
(2, 0)后,如果该位置是空地.,那么玩家就可以成功移动过去,此时(2, 0)应该是P,而(1, 0)变成空地. - 如果新位置
(2, 0)是障碍物#、^、~,那么玩家就无法移动过去,玩家位置不变 -
如果新位置
(2, 0)是怪物M,那么就触发战斗逻辑- 如果玩家获胜,那么
(2, 0)位置就应该是玩家P,而(1, 0)变成空地.,怪物M就从地图上移除了 - 如果玩家失败,游戏结束
- 如果玩家获胜,那么
-
如果新位置
(2, 0)是道具S、D、H,那么玩家就拾取道具- 拾取道具后,玩家位置变成
(2, 0)的P,而(1, 0)变成空地.,原来的道具S、D、H就从地图上移除了 - 并且道具的效果会应用到玩家身上
- 拾取道具后,玩家位置变成
- 比方说,玩家当前的位置是
1. 地图的加载与显示¶
首先,我们可以实现的一个简单功能是读取地图文件,并将地图内容存储在一个二维列表中:
- 在读取地图文件时,我们其实没必要一次性整个读取,因为我们最终还是要把每一行拆成字符列表存储在二维列表中,所以我们可以逐行读取文件内容,并将每一行转换成字符列表后追加到
self.map中 - 例如,如果我们的地图中的一行是
#P..S..^...#,那么存储在二维列表中的形式就是['#', 'P', '.', '.', 'S', '.', '.', '^', '.', '.', '.', '#'],如果是这样,我们可以直接使用list(line)来将字符串转换成字符列表 - 也就是说,
self.map其实就是我们之前讲过的二维列表:
[
['#', 'P', '.', '.', 'S', '.', '.', '^', '.', '.', '.', '#'],
['#', '^', '^', '^', '.', '.', '~', '.', '.', 'H', '.', '#']
]
除此之外,游戏中还有个重要的功能,就是地图的显示:
- 也就是如何把二维列表中的内容再变回字符串形式并打印出来
- 这个其实很简单,我们把一行的字符列表使用
"".join(row)拼接成字符串,然后打印出来就行了
地图的读取与显示这部分,我们的代码实现如下:
# src/world.py
from characters import Player, Monster
from items import Sword, Shield, Potion
from battle import fight
class World:
def __init__(self, map_file):
self.map = []
def map_load(self, file_path):
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
# 行尾的换行符要去掉
line = line.rstrip('\n')
self.map.append(list(line))
def map_show(self):
for row in self.map:
print("".join(row))
2. 地图解析¶
接下来,我们需要解析地图中的各种元素:
- 怪物
M:我们需要找到所有怪物的位置,并为每个怪物创建一个Monster对象,存储在一个字典中,键是位置(x, y),值是Monster对象,例如:
self.monsters = {
(3, 4): Monster(),
(7, 2): Monster(),
}
- 道具
S、D、H:我们需要找到所有道具的位置,并为每个道具创建相应的道具对象,存储在一个字典中,键是位置(x, y),值是道具对象,例如:
self.items = {
(5, 1): Sword(),
(8, 3): Shield(),
(2, 6): Potion(),
}
-
玩家
P:这个是稍微特殊一点的,因为玩家只有一个,而且玩家的位置是会变的:- 如果像上面的怪物和道具那样存储在字典中,反而不方便我们后续的移动操作,因为字典的键是不可变的,我们没法直接修改键来更新玩家位置
- 因此,我们直接使用一个二元组
self.player_pos来存储玩家的位置(x, y)即可 - 所以这里,我们需要两个属性变量,
self.player用来存储玩家对象,self.player_pos用来存储玩家位置
综上所述,我们实现一个 find_elements 方法,遍历地图中的每个位置,根据字符创建相应的对象并存储到对应的属性变量中:
- 这里,我们使用
enumerate函数来获取每个位置的坐标(x, y)和字符symbol,这么做的好处是,又可以获得索引,又可以获得字符 - 之后,我们根据字符的不同,创建不同的对象,玩家就直接创建并存储位置,怪物和道具则存储在各自的字典中
这段代码实现如下:
# src/world.py
from characters import Player, Monster
from items import Sword, Shield, Potion
from battle import fight
class World:
def __init__(self, map_file):
self.map = []
self.player = Player()
self.player_pos = None
self.monsters = {}
self.items = {}
def map_load(self, file_path):
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
# 行尾的换行符要去掉
line = line.rstrip('\n')
self.map.append(list(line))
def map_show(self):
for row in self.map:
print("".join(row))
def find_elements(self):
for y, row in enumerate(self.map):
for x, symbol in enumerate(row):
if symbol == 'P':
self.player_pos = (x, y)
elif symbol == 'M':
self.monsters[(x, y)] = Monster()
elif symbol == 'S':
self.items[(x, y)] = Sword()
elif symbol == 'D':
self.items[(x, y)] = Shield()
elif symbol == 'H':
self.items[(x, y)] = Potion()
3. 玩家移动与交互¶
这个功能是地图类中最复杂也是最重要的的部分,因为它涉及到很多逻辑判断:
-
首先,这个功能最重要的功能就是要移动玩家的位置,我们这里定义一个通用的函数,给定玩家当前位置
(x, y)和移动增量(dx, dy),计算出新的位置(nx, ny):-
玩家当前位置
(x, y)通过self.player_pos获取即可 -
移动的增量
dx和dy分别表示玩家在 x 轴和 y 轴上的移动增量:- 如果玩家想向左移动一格,那么
dx=-1,dy=0 - 如果玩家想向右移动一格,那么
dx=1,dy=0 - 如果玩家想向上移动一格,那么
dx=0,dy=-1 - 如果玩家想向下移动一格,那么
dx=0,dy=1 - 注意,这里的坐标系是以左上角为原点,x 轴向右递增,y 轴向下递增
- 如果玩家想向左移动一格,那么
-
新的位置由
(nx, ny) = (x + dx, y + dy)计算得到
-
-
接着,在移动到新位置后,我们需要根据新位置的内容进行不同的处理:
-
如果新位置是障碍物
#、^、~,那么玩家就无法移动过去,玩家位置不变,这时直接用return终止移动的方法即可 -
如果新位置是怪物
M,那么就触发战斗逻辑:- 调用
fight(self.player, monster)函数进行战斗 - 如果玩家获胜,那么更新玩家位置为新位置,并且从地图上移除怪物
- 如果玩家失败,游戏结束,同样,使用
return终止移动的方法即可
- 调用
-
如果新位置是道具
S、D、H,那么玩家就拾取道具:- 调用道具的
apply(self.player)方法应用道具效果 - 更新玩家位置为新位置,并且从地图上移除道具
- 调用道具的
-
-
在以上所有情况下,只要函数没有被
return终止,那么最后一步就是更新玩家位置:- 把旧位置设置为空地
.,把新位置设置为玩家P - 更新
self.player_pos为新位置(nx, ny)
- 把旧位置设置为空地
整个移动与交互的逻辑我们就梳理清楚了,那么完整的代码实现起来就很清晰了,如下所示:
# src/world.py
def move_player(self, dx, dy):
x, y = self.player_pos
nx, ny = x + dx, y + dy
# 障碍物检测:# 边界墙,^ 山,~ 水
if self.map[ny][nx] in {'#', '^', '~'}:
block = self.map[ny][nx]
if block == '#':
print("前方是边界墙,无法通过。")
elif block == '^':
print("前方是高山,无法通过。")
else: # '~'
print("前方是水域,无法通过。")
return
# 怪物:踩到触发战斗
if (nx, ny) in self.monsters:
monster = self.monsters[(nx, ny)]
win = fight(self.player, monster)
# 如果玩家赢了
if win:
print("怪物被击败!")
del self.monsters[(nx, ny)]
# 如果玩家输了
else:
print("你被怪物击败了……")
return
# 道具:踩到自动使用
if (nx, ny) in self.items:
item = self.items[(nx, ny)]
item.apply(self.player)
if isinstance(item, Potion):
print(f"你捡到了生命药水,生命值为: {self.player.hp} 点!")
elif isinstance(item, Sword):
print(f"你捡到了剑,攻击力为: {self.player.attack} 点!")
elif isinstance(item, Shield):
print(f"你捡到了盾牌,防御力为: {self.player.defense} 点!")
del self.items[(nx, ny)]
# 移动玩家
self.map[y][x] = '.'
self.map[ny][nx] = 'P'
self.player_pos = (nx, ny)
到此,地图类中最重要的一个功能,玩家的移动与交互,我们就实现完成了。
4. 地图类的测试¶
地图类的测试,我们使用一种比较新颖的方式:
- 我们加载一个地图进来,并且模拟用户输入固定的移动命令
- 在我们模拟的移动路径中,会经过道具和怪物,以测试地图类中的完整功能
- 并且我们模拟两条线路,一条成功一条失败
我们使用一个新的更简单的地图 data/map/map_tiny.txt:
############
#P.S^^^^^^^#
#...D..~~~~#
#H..M..M..M#
############
-
这个地图比较简单,我们可以看到一条比较清楚的成功路径(先拿剑和盾,打死一个怪之后拿血瓶,之后打剩下两个怪):
-
同时,我们也可以模拟一条失败路径(直接去三个打怪,没装备和血瓶)
-
这样,我们只需搭建一个函数
test_world_movement,传入不同的路径即可
根据这个思路,完整的测试代码如下:
# tests/test_world.py
from config_test import *
from world import World
from config import Config
import time
def test_world(commands):
"""
测试地图:
############
#P.S^^^^^^^#
#...D..~~~~#
#H..M..M..M#
############
"""
world = World(f"{Config.PATH_DATA}/map/map_tiny.txt")
for command in commands:
world.map_show()
print(f"\n当前命令:{command}")
if "左" in command:
world.move_player(-1, 0)
elif "右" in command:
world.move_player(1, 0)
elif "上" in command:
world.move_player(0, -1)
elif "下" in command:
world.move_player(0, 1)
if not world.player.is_alive():
break
time.sleep(1)
if world.player.is_alive():
print("\n玩家获胜,游戏结束。")
else:
print("\n玩家战败,测试结束。")
if __name__ == "__main__":
commands_win = ["右", "右(剑)", "下", "右(盾)", "下(怪)", "左", "左", "左(血)", "右", "右", "右", "右", "右", "右(怪)", "右", "右", "右(怪)"]
commands_los = ["下", "右", "下", "右", "右(怪)", "右", "右", "右(怪)", "右", "右", "右(怪)"]
print("===== 测试玩家获胜 =====")
test_world(commands_win)
print()
print("===== 测试玩家失败 =====")
test_world(commands_los)
我们运行一下,看下测试结果,这里的输出有点多,大家可以耐心查看一下(运行到这里,这个游戏终于有点游戏的样子了):
===== 测试玩家获胜 =====
############
#P.S^^^^^^^#
#...D..~~~~#
#H..M..M..M#
############
当前命令:右
############
#.PS^^^^^^^#
#...D..~~~~#
#H..M..M..M#
############
当前命令:右(剑)
你捡到了剑,攻击力为: 10 点!
############
#..P^^^^^^^#
#...D..~~~~#
#H..M..M..M#
############
当前命令:下
############
#...^^^^^^^#
#..PD..~~~~#
#H..M..M..M#
############
当前命令:右(盾)
你捡到了盾牌,防御力为: 6 点!
############
#...^^^^^^^#
#...P..~~~~#
#H..M..M..M#
############
当前命令:下(怪)
【战斗开始】 玩家 VS 怪物
你的有效攻击:7
怪物的有效攻击:2
--- 第 1 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:13
怪物 攻击了 玩家,玩家 剩余血量:28
--- 第 2 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:6
怪物 攻击了 玩家,玩家 剩余血量:26
--- 第 3 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:0
怪物 被击败了!
怪物被击败!
############
#...^^^^^^^#
#......~~~~#
#H..P..M..M#
############
当前命令:左
############
#...^^^^^^^#
#......~~~~#
#H.P...M..M#
############
当前命令:左
############
#...^^^^^^^#
#......~~~~#
#HP....M..M#
############
当前命令:左(血)
你捡到了生命药水,生命值为: 36 点!
############
#...^^^^^^^#
#......~~~~#
#P.....M..M#
############
当前命令:右
############
#...^^^^^^^#
#......~~~~#
#.P....M..M#
############
当前命令:右
############
#...^^^^^^^#
#......~~~~#
#..P...M..M#
############
当前命令:右
############
#...^^^^^^^#
#......~~~~#
#...P..M..M#
############
当前命令:右
############
#...^^^^^^^#
#......~~~~#
#....P.M..M#
############
当前命令:右
############
#...^^^^^^^#
#......~~~~#
#.....PM..M#
############
当前命令:右(怪)
【战斗开始】 玩家 VS 怪物
你的有效攻击:7
怪物的有效攻击:2
--- 第 1 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:13
怪物 攻击了 玩家,玩家 剩余血量:34
--- 第 2 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:6
怪物 攻击了 玩家,玩家 剩余血量:32
--- 第 3 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:0
怪物 被击败了!
怪物被击败!
############
#...^^^^^^^#
#......~~~~#
#......P..M#
############
当前命令:右
############
#...^^^^^^^#
#......~~~~#
#.......P.M#
############
当前命令:右
############
#...^^^^^^^#
#......~~~~#
#........PM#
############
当前命令:右(怪)
【战斗开始】 玩家 VS 怪物
你的有效攻击:7
怪物的有效攻击:2
--- 第 1 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:13
怪物 攻击了 玩家,玩家 剩余血量:30
--- 第 2 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:6
怪物 攻击了 玩家,玩家 剩余血量:28
--- 第 3 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:0
怪物 被击败了!
怪物被击败!
玩家获胜,游戏结束。
===== 测试玩家失败 =====
############
#P.S^^^^^^^#
#...D..~~~~#
#H..M..M..M#
############
当前命令:下
############
#..S^^^^^^^#
#P..D..~~~~#
#H..M..M..M#
############
当前命令:右
############
#..S^^^^^^^#
#.P.D..~~~~#
#H..M..M..M#
############
当前命令:下
############
#..S^^^^^^^#
#...D..~~~~#
#HP.M..M..M#
############
当前命令:右
############
#..S^^^^^^^#
#...D..~~~~#
#H.PM..M..M#
############
当前命令:右(怪)
【战斗开始】 玩家 VS 怪物
你的有效攻击:4
怪物的有效攻击:5
--- 第 1 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:16
怪物 攻击了 玩家,玩家 剩余血量:25
--- 第 2 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:12
怪物 攻击了 玩家,玩家 剩余血量:20
--- 第 3 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:8
怪物 攻击了 玩家,玩家 剩余血量:15
--- 第 4 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:4
怪物 攻击了 玩家,玩家 剩余血量:10
--- 第 5 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:0
怪物 被击败了!
怪物被击败!
############
#..S^^^^^^^#
#...D..~~~~#
#H..P..M..M#
############
当前命令:右
############
#..S^^^^^^^#
#...D..~~~~#
#H...P.M..M#
############
当前命令:右
############
#..S^^^^^^^#
#...D..~~~~#
#H....PM..M#
############
当前命令:右(怪)
【战斗开始】 玩家 VS 怪物
你的有效攻击:4
怪物的有效攻击:5
--- 第 1 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:16
怪物 攻击了 玩家,玩家 剩余血量:5
--- 第 2 回合 ---
玩家 攻击了 怪物,怪物 剩余血量:12
怪物 攻击了 玩家,玩家 剩余血量:0
玩家 被击败了!
你被怪物击败了……
玩家战败,测试结束。