Python Drawing ASCII Map
Asked Answered
S

3

6

I need to have a 2 radius map drawn off the player's current room in a MUD I'm building in python (or more, if possible). Rooms are set up as containers with a self.exits = {'west':1, 'north':2} where the key is the direction that the value (UID of the adjacent room) is located. Rooms are linked only in this way. A player with a self.location of 0 could type 'n' and their location, based on the above variable, would then be 2 and that room's contents would have the player's UID appended to its contents.

So, I would like to have a map displayed that looks like the following, based on the above variable, where 'u' is the player's current location..

    [ ]
     |
[ ]-[u]

I've achieved this portion, as this is just a radius of 1. Here is a small (heavily modified for posting here) snippet of how I did this, and you'll see why I'm posting, as it's poor code.

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)

In my insanity, I managed to make this work with all 8 different directions (diagonals, and not including up or down). But I then have to for loop the rooms I just parsed with my first for loop, and then draw those, and then space it all out, and then take into account the overlap of the (sp)aces such as '\' or '|' if there are paths that cross each other. This small task turned nightmarish immediately, and well into 200 lines before I was done.

Another hurdle is that I can only print line by line. So if the map is 50 characters high, I have to have player.hear() on 50 lines, which I'm not opposed to. Just keep that in mind before posting an answer.

I'm also not picky about formatting. I just simply want a 'map at a glance' to aid players while traveling around the world.

Thanks guys. I hope I provided enough info. Let me know, if not. (Here is a link to the entire (unfinished and HORRIBLE) module I'm referencing. Map.py

Sparhawk answered 28/7, 2012 at 18:49 Comment(5)
"So if the map is 50 characters high, I have to have player.hear() on 50 lines, which I'm not opposed to." But you should be!Flagella
In an effort to be brief, I left out that I am indeed opposed to it, and I could theoretically just do some for loop that prints, but my brain is having a hard enough time figuring out how to do this right now that I'm more concerned with getting the working map up, and fine tuning it after that fact.Sparhawk
I'm composing an answer. Is the idea that you want the player to see only the rooms he's been to?Flagella
That would actually be a cool feature, tbo. But ultimately this is just meant to be as an aid for navigation. If a history can be implemented I wouldn't be opposed though.Sparhawk
Will take a few more minutes.Flagella
F
13

This code is in serious trouble. Let's start designing from scratch. This will hopefully serve as a good lesson in how to design and build classes and data structures.

To start with, you should organize your code around a Map class, which then represents your rooms as a grid. You shouldn't think about "room 1", "room 2", etc (which is very hard to keep track of on a map), and rather think of rooms in terms of coordinates.

Now, there are a few possible features we are ignoring at the beginning, including the player seeing only rooms he's been to, the player remaining at the center of the map, and diagonal paths. If you want them you can put them in later, once the basic functionality works. For now, we are aiming for something that looks a little like this:

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

That is, we're representing it as a grid where some rooms are connected and others are not. Let's have each room have a coordinate pair, a little like this:

      0   1   2   3
   0 [ ]-[u] [ ] [ ]
      |
   1 [ ]-[ ]-[ ] [ ]
          |
   2 [ ]-[ ]-[ ] [ ]
      |
   3 [ ]-[ ]-[ ]-[ ]

Let x be along the top and y be along the side. The top left is (0, 0), the one with [u] in it is (0, 1).

Now, what are the components of our Map class?

  1. map height: integer

  2. map width: integer)

  3. player_x, player_y: coordinates of player

  4. possible paths: a list of pairs of rooms that we can move between. The above map would be represented as:

    [((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))]
    

Notice that I ordered each pair such that the larger tuple goes first (that's important later).

So now that we have our design, let's write that Map class!

I can think of four methods we want: print_map, move, and an initializer. Intitialization is simple: just set the four attributes we listed above:

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

Now, move is quite simple. Given a direction 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

The move function for "north" just checks if there is a path to the room above the one we are in.

Now for the most interesting part: printing the map. You do this by looping over the rows (0 to self.height) and over the columns (0 to self.width). (Note: you can't use print in this situation since it automatically puts a newline or space after the string. Instead we use 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

Now, let's put it all together and try it out. Here's the code:

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)

Notice that I added a section on the bottom that creates a map and allows the player to move around it. Here's how it looks when it runs:

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] [ ] 
     |          
[ ]-[ ]-[ ] [ ] 
 |              
[ ]-[ ]-[ ]-[ ] 

There are many improvements that can be made to this code (in particular, the move method is repetitive), but it's a good start. Try making the map 20x20 and you'll see it expands just fine.

ETA: I should note that print_map could be rewritten in a much shorter form as something like:

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)])

But this is a bit more intense.

Flagella answered 28/7, 2012 at 20:20 Comment(8)
Nice mini-tutorial! I stopped working on my own when you said you were on the case. I might have abstracted away the directions into a dictionary of names and (dx, dy) pairs in move, though: 4 is borderline but adding diagonals would make it 8..Apopemptic
Can sys.stdout.write work over or a MUD client? The game is designed to only print out line by line. I'm also not seeing the room's exit dictionary used at all in this answer. That's crucial, as it is not a map feature, but how the game links objects. I can't get around that without rewriting my entire game, essentially. I unfortunately can't see how to implement what you've provided into that. Would you be willing to explain how that could be done?Sparhawk
@jtsmith1287: the shorter version of print_map I just posted doesn't need sys.stdout (and it's easy to work around in any case by building a string gradually)Flagella
As for the rooms: you really should be working with a coordinate system for your rooms. Using dictionaries mapping room to room is exactly why you've gotten such a headache drawing a map. For example, if you just have a dictionary mapping room to room, you could end up with illegal loops. And isn't it simpler to design a game by sketching a map and filling in rooms than figuring each one in relation to all the previous ones?Flagella
You are correct, for sure. Unfortunately, when we started the project I didn't know any programming and was piggy-backing off a collaborator. Having a small map would be very nice, but is not so important as to rewrite how our entire game functions. I apologize. I should have been more clear that the dictionary exits were a must. Your answer is fantastic though, and I'm racking my brain on how to implement.Sparhawk
I might have time later to set up such a map. So it would have to be centered around the player?Flagella
Yes. Centered around the player, and showing diagonal exits. If the map were just basic cardinal directions it would be much simpler, but the diagonals having the possibility of over-lapping (Which I would draw with an X instead of a \ or /.) make it much more difficult. But really, any kind of map at all that utilizes the exits dictionary would be perfect. :) Thanks for all your help. I love your example given, despite being unable to implement it.Sparhawk
let us continue this discussion in chatSparhawk
B
4

I did this as an exercise where the rooms and the grid "print themselves". I add this to the discussion as it may be easier to implement with an eventual larger grid.

The ASCII Map

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

Each cell in this grid is a three by three set of '+' signs. Four rooms are implemented with id value 1 through 4. Connections between the rooms are represented as slashes, backslashes and pipes.

The Code

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

The moving routine is not implemented, and for this representation to work, only single character ids will show up nicely.

The advantages of this system:

  • easy to add rooms and to remove them
  • the definition of a room is human readable
  • the output functions overload __str__ and hence rooms and grid "print themselves" and this might come in useful for future debugging or adaptation to future formats e.g. as cells in an HTML table.
Braxy answered 29/7, 2012 at 5:30 Comment(0)
O
0

Coordinate based maps have a lot of advantages, but considering that many quality muds use a traditional room-based world, and people have made automappers for many muds and mud clients, it's not out of the question to make an automap for a mud without coordinates. You'll just have to deal with conflicts on a case by case basis.

However, you still can use the answer by @david-robinson. What you want to do is keep a minimap roughly centered on the player and dynamically update it using the exit data. Don't try to keep a map of the entire area stored; by dynamically updating you'll avoid some geographical conflicts.

For writing the map to a mud client, all you need to do is write your map line properly spaced and terminate it with a new line. You put all the map lines into a list so it's sent as a single group of lines (you don't want some other line inserted between lines of the map for example when it's sent out the socket), and any mud client will print it properly (with a monospace font of course).

Ocular answered 29/7, 2012 at 6:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.