Python绘制ASCII地图

6
我需要在我用Python构建的MUD中,以玩家当前房间为中心画一个半径2的地图(或者更大)。房间被设置为容器,其中包含一个self.exits = {'west':1, 'north':2},其中键是值(相邻房间的UID)所在的方向。只有这种方式才能链接房间。一个self.location为0的玩家可以输入'n',根据上述变量,他们的位置将是2,并且该房间的内容将附加玩家的UID。
因此,我想要显示一个地图,它看起来像上面的变量一样,其中'u'是玩家当前位置。
    [ ]
     |
[ ]-[u]

我已经完成了this部分,因为它只是一个半径为1的圆。这是一个经过大量修改后用于发布的小代码片段,您将看到为什么我正在发布它,因为它是糟糕的代码。

mloc = '[u]'
mn = '   '
mw = '   '
spn= ' '
spw= ' '
for Exit in room.exits.keys():
  if Exit == 'north':
    mn = '[ ]'
    spn = '|'
  if Exit == 'west': 
    mw = '[ ]-'
# player.hear() is our function for printing a line to the player's screen
player.hear('    '+mn)
player.hear('     '+sp)
player.hear(mw+mloc)

在我的疯狂中,我设法使这个程序可以使用所有8个不同的方向(包括对角线,但不包括上下方向)。但是我随后必须循环遍历我刚刚解析过的房间,然后绘制它们,然后分隔开它们,然后考虑到路径交叉时的空格重叠,例如'\'或'|'。这个小任务立即变成了噩梦,最后代码行数超过了200行才完成。
另一个难题是我只能逐行打印。因此,如果地图高度为50个字符,则我必须在50行上使用player.hear(),我并不反对这样做。在回答之前请记住这一点。
我对格式也不挑剔。我只是希望有一个“地图一览”来帮助玩家在世界中旅行。
谢谢大家。我希望我提供的信息足够了。如果不够,请告诉我。 (这是我参考的整个(未完成和可怕的)模块的链接。Map.py

1
所以如果地图高度为50个字符,我必须在50行上使用player.hear(),虽然我不反对这样做。但你应该反对! - David Robinson
为了简洁起见,我没有提到我确实反对它,理论上我可以只做一些打印的for循环,但是我的大脑现在已经很难弄清楚如何做到这一点,所以我更关心的是让工作地图上线,并在事后进行微调。 - jtsmith1287
我正在撰写一个答案。你的想法是让玩家只看到他去过的房间吗? - David Robinson
这实际上是一个很酷的功能,老实说。但最终它只是作为导航的辅助工具而存在。如果能够实现历史记录,我也不反对。 - jtsmith1287
还需要几分钟。 - David Robinson
3个回答

13

这段代码存在严重问题。让我们从头开始设计。这将有助于学习如何设计和构建类和数据结构。

首先,你应该围绕一个Map类来组织你的代码,然后将房间表示为网格。你不应该考虑“房间1”,“房间2”等(这很难在地图上跟踪),而是应该根据坐标来考虑房间。

现在,有一些可能被忽略的功能,包括玩家只看到他去过的房间,玩家保持在地图中心,以及对角线路径。如果你想要它们,可以在基本功能正常工作后再加入它们。现在,我们的目标是实现类似于这样的东西:

[ ]-[u] [ ] [ ]
 |
[ ]-[ ]-[ ] [ ]
     |
[ ]-[ ]-[ ] [ ]
 |
[ ]-[ ]-[ ]-[ ]

也就是说,我们将其表示为一个网格,其中有些房间相互连接,而其他房间则没有。让我们给每个房间都分配一个坐标对,就像这样:

      0   1   2   3
   0 [ ]-[u] [ ] [ ]
      |
   1 [ ]-[ ]-[ ] [ ]
          |
   2 [ ]-[ ]-[ ] [ ]
      |
   3 [ ]-[ ]-[ ]-[ ]
让 x 沿着顶部,y 沿着侧面。左上角为 (0, 0),带有 [u] 的格子为 (0, 1)。
现在,我们的 Map 类有哪些组成部分?
  1. 地图高度:整数

  2. 地图宽度:整数

  3. player_x、player_y:玩家坐标

  4. 可能的路径:一组可以在房间之间移动的坐标对。以上地图可以表示为:

  5. [((0, 0), (1, 0)), ((0, 0), (1, 0)), ((1, 0), (1, 1)), ((1, 1), (2, 1)),
     ((1, 0), (1, 2)), ((0, 2), (1, 2)), ((1, 2), (2, 2)), ((0, 2), (0, 3)),
     ((0, 3), (1, 3)), ((1, 3), (2, 3)), ((2, 3), (3, 3))]
    

请注意,我按照较大的元组先进行排序(稍后会很重要)。

现在我们有了设计,让我们来编写Map类!

我可以想到四种方法:print_mapmove和初始化器。初始化很简单:只需设置我们上面列出的四个属性:

class Map:
    def __init__(self, height, width, player_x, player_y, paths):
        self.height = height
        self.width = width
        self.x = player_x
        self.y = player_y
        self.paths = paths

现在,move非常简单。给定方向n/e/s/w:

    def move(self, direction):
        if direction == "n":
            if ((self.x, self.y - 1), (self.x, self.y)) not in self.paths:
                print "Cannot go north"
            else:
                self.y -= 1

"north"的move函数只是检查我们所在房间上方是否有一条路径。

现在最有趣的部分:打印地图。您可以通过循环行(从0到self.height)和列(从0到self.width)来完成此操作。(注意:在这种情况下,您不能使用print,因为它会自动在字符串之后添加换行符或空格。相反,我们使用sys.stdout.write。)

def print_map(self):
    for y in range(0, self.height):
        # print the yth row of rooms
        for x in range(0, self.width):
            if self.x == x and self.y == y:
                sys.stdout.write("[u]")  # this is the player's room
            else:
                sys.stdout.write("[ ]")  # empty room
            # now see whether there's a path to the next room
            if ((x, y), (x + 1, y)) in self.paths:
                sys.stdout.write("-")
            else:
                sys.stdout.write(" ")
        # now that we've written the rooms, draw paths to next row
        print  # newline
        for x in range(0, self.width):
            sys.stdout.write(" ")  # spaces for above room
            if ((x, y), (x, y + 1)) in self.paths:
                sys.stdout.write("|  ")
            else:
                sys.stdout.write("   ")
        print

现在,让我们把所有的东西都放在一起并试一下。这是代码:

import sys

class Map:
    def __init__(self, height, width, player_x, player_y, paths):
        self.height = height
        self.width = width
        self.x = player_x
        self.y = player_y
        self.paths = paths

    def move(self, direction):
        if direction == "n":
            if ((self.x, self.y - 1), (self.x, self.y)) not in self.paths:
                print "Cannot go north"
            else:
                self.y -= 1
        if direction == "s":
            if ((self.x, self.y), (self.x, self.y + 1)) not in self.paths:
                print "Cannot go south"
            else:
                self.y += 1
        if direction == "e":
            if ((self.x, self.y), (self.x + 1, self.y)) not in self.paths:
                print "Cannot go east"
            else:
                self.x += 1
        if direction == "w":
            if ((self.x - 1, self.y), (self.x, self.y)) not in self.paths:
                print "Cannot go west"
            else:
                self.x -= 1

    def print_map(self):
        for y in range(0, self.height):
            # print the yth row of rooms
            for x in range(0, self.width):
                if self.x == x and self.y == y:
                    sys.stdout.write("[u]")  # this is the player's room
                else:
                    sys.stdout.write("[ ]")  # empty room
                # now see whether there's a path to the next room
                if ((x, y), (x + 1, y)) in self.paths:
                    sys.stdout.write("-")
                else:
                    sys.stdout.write(" ")
            # now that we've written the rooms, draw paths to next row
            print  # newline
            for x in range(0, self.width):
                sys.stdout.write(" ")  # spaces for above room
                if ((x, y), (x, y + 1)) in self.paths:
                    sys.stdout.write("|  ")
                else:
                    sys.stdout.write("   ")
            print


paths = [((0, 0), (1, 0)), ((0, 0), (1, 0)), ((1, 0), (1, 1)), ((1, 1),
         (2, 1)), ((1, 1), (1, 2)), ((0, 2), (1, 2)), ((1, 2), (2, 2)),
         ((0, 2), (0, 3)), ((0, 3), (1, 3)), ((1, 3), (2, 3)), ((2, 3),
         (3, 3))]
m = Map(4, 4, 0, 0, paths)

while True:
    m.print_map()
    direction = raw_input("What direction do you want to move? [n/e/s/w] ")
    m.move(direction)

请注意,我在底部添加了一个部分,创建了一个地图并允许玩家在地图上移动。以下是运行时的效果:

Davids-MacBook-Air:test dgrtwo$ python Map.py 
[u]-[ ] [ ] [ ] 
     |          
[ ] [ ]-[ ] [ ] 
     |          
[ ]-[ ]-[ ] [ ] 
 |              
[ ]-[ ]-[ ]-[ ] 

What direction do you want to move? [n/e/s/w] e
[ ]-[u] [ ] [ ] 
     |          
[ ] [ ]-[ ] [ ] 
     |          
[ ]-[ ]-[ ] [ ] 
 |              
[ ]-[ ]-[ ]-[ ] 

What direction do you want to move? [n/e/s/w] s
[ ]-[ ] [ ] [ ] 
     |          
[ ] [u]-[ ] [ ] 
     |          
[ ]-[ ]-[ ] [ ] 
 |              
[ ]-[ ]-[ ]-[ ] 

What direction do you want to move? [n/e/s/w] w
Cannot go west
[ ]-[ ] [ ] [ ] 
     |          
[ ] [u]-[ ] [ ] 
     |          
[ ]-[ ]-[ ] [ ] 
 |              
[ ]-[ ]-[ ]-[ ] 

What direction do you want to move? [n/e/s/w] e
[ ]-[ ] [ ] [ ] 
     |          
[ ] [ ]-[u] [ ] 
     |          
[ ]-[ ]-[ ] [ ] 
 |              
[ ]-[ ]-[ ]-[ ] 

这段代码有很多可以改进的地方(特别是move方法很重复),但这是一个不错的开始。尝试将地图设置为20x20,你会发现它会扩展得很好。

附:我应该指出,print_map可以用更短的形式重写,例如:

def print_map(self):
    for y in range(0, self.height):
        print "".join(["[%s]%s" %
                    ("u" if self.x == x and self.y == y else " ",
                     "-" if ((x, y), (x + 1, y)) in self.paths else " ")
                        for x in range(0, self.width)])
        print " " + "   ".join(["|" if ((x, y), (x, y + 1)) in self.paths
                              else " " for x in range(0, self.width)])

但这要更加复杂一些。


1
不错的迷你教程!当你说你在处理时,我就停止了自己的工作。不过,在move中,我可能会将方向抽象成名称和(dx, dy)对的字典:4是边界,但添加对角线将使其变为8。 - DSM
sys.stdout.write能在MUD客户端上工作吗?游戏只设计成逐行打印。我也没有看到房间的出口字典在这个答案中得到使用。这是至关重要的,因为它不是地图功能,而是游戏链接对象的方式。如果不重写整个游戏,我就无法解决这个问题。我不幸地看不懂如何将你提供的内容实现在其中。您能否愿意解释一下如何实现呢? - jtsmith1287
@jtsmith1287:我刚发布的print_map的简短版本不需要sys.stdout(而且通过逐步构建字符串,任何情况下都很容易解决)。 - David Robinson
关于房间:你应该使用坐标系统来处理房间。使用将一个房间映射到另一个房间的字典正是你绘制地图时头痛的原因。例如,如果你只是使用了一个将房间映射到另一个房间的字典,你可能会遇到非法循环的问题。相比于计算每个房间与之前所有房间的关系,设计一个游戏并填满其中的房间不是更简单吗? - David Robinson
你说得没错。不幸的是,在我们开始这个项目时,我并不懂编程,一直依赖合作者。虽然有一个小地图会很好,但并不像重写整个游戏功能那样重要。我很抱歉。我应该更清楚地表明字典存在是必须的。你的答案非常棒,我正在努力想办法实现它。 - jtsmith1287
显示剩余3条评论

4

我把这个作为练习,让房间和网络“自己打印出来”。我将其添加到讨论中,因为在实现更大的网格时可能更容易实现。

ASCII地图

+++++++++++++++
+++++++++++++++
+++++++++++++++
++++++   ++++++
++++++ 2 ++++++
++++++/| ++++++
+++  / | ++++++
+++ 3--1 ++++++
+++     \++++++
+++++++++\  +++
+++++++++ 4 +++
+++++++++   +++
+++++++++++++++
+++++++++++++++
+++++++++++++++

这个网格中的每个单元格都是一个由'+'符号组成的三乘三的集合。四个房间用id值1到4实现。房间之间的连接以斜线、反斜线和竖线表示。

代码

class Room(object):
    def __init__(self, id, loc, exits):
        self.id = id # unique identifier, may be a name
        self.row = loc[0] # loc is tuple of (row, col)
        self.col = loc[1] 
        # exits is a list where 'X' means no exit and 
        # any other value is id of destination
        self.exits = exits 

    def __str__(self):
        directions = '\\|/- -/|\\'
        room = [ e if e == 'X' else ' ' for e in self.exits ]
        for idx in range(len(room)):
            if room[idx] == ' ':
                room[idx] = directions[idx]
            if room[idx] == 'X':
                room[idx] = ' '
        room[4] = self.id[0] # only print first char of id
        return ''.join(room)

class Map(object):
    def __init__(self, rows, cols, rooms):
        self.rows = rows
        self.cols = cols
        self.rooms = rooms

    def __str__(self):
        world = []
        for i in range(self.rows * 3):
            world.append( ['+++'] * self.cols )
        for room in self.rooms:
            ascii = str(room)
            x = room.col
            y = room.row
            for idx in range(0, 3):
                cell = ascii[idx*3:idx*3+3]
                world[y*3+idx][x] = cell
        return '\n'.join( [ ''.join(row) for row in world ] )


if __name__ == '__main__':
    # set up four rooms
    # each room has unique id (string of any length) and coordinates
    # it also has a set of 8 possible exits, represented as a list where
    # 'X' means exit is blocked and valid exits contain the id of the target room
    r1 = Room(id='1', loc=(2,2), exits=['X','2','X',
                                        '3',' ','X',
                                        'X','X','4',])
    r2 = Room(id='2', loc=(1,2), exits=['X','X','X',
                                        'X',' ','X',
                                        '3','1','X',])
    r3 = Room(id='3', loc=(2,1), exits=['X','X','2',
                                        'X',' ','1',
                                        'X','X','X',])
    r4 = Room(id='4', loc=(3,3), exits=['1','X','X',
                                        'X',' ','X',
                                        'X','X','X',])
    # initialize Map with a list of these four rooms
    map = Map(rows = 5, cols=5, rooms=[r1, r2, r3, r4])
    print map

移动例程未实现,为使此表示正常工作,只有单个字符 ID 才能显示得好。
该系统的优点:
- 容易添加和删除房间。 - 房间的定义易于人类阅读。 - 输出函数重载了 __str__,因此房间和网格“自我打印”,这可能对未来调试或适应未来格式(例如作为 HTML 表格中的单元格)很有用。

0

基于坐标的地图有很多优势,但考虑到许多高质量的mud使用传统的基于房间的世界,并且人们已经为许多mud和mud客户端制作了自动映射器,因此制作一个没有坐标的mud的自动映射器并不是不可能的。您只需要逐个案例处理冲突。

但是,您仍然可以使用@david-robinson的答案。您要做的是保持一个大致位于玩家中心的小地图,并使用出口数据动态更新它。不要尝试存储整个区域的地图;通过动态更新,您将避免一些地理冲突。

要将地图写入mud客户端,您只需要正确间隔地编写地图行并以新行终止即可。将所有地图行放入列表中,以便作为单个行组发送(当它被发送到套接字时,您不希望其他行插入地图行之间,例如),任何mud客户端都会正确打印它(当然要使用等宽字体)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接