How to Write a Text Adventure in Python Part 2: The World Space

This is an abbreviated version of the book Make Your Own Python Text Adventure.

All games take place in some sort of world. The world can be as simple as a chess board or as complex as the Mass Effect universe and provides the foundation for the game as a whole. All elements of a game reside in the world and some elements interact with the world. In this post, you’ll learn how to add items and enemies to your world.

The coordinate plane

A text adventure usually involves a player moving through the world one section per turn. We can think of each section as a tile on an x-y grid. Note: in most game programming the x-y coordinate plane is different from the one you learned in algebra. In the game world, (0,0) is in the top left corner, x increases to the right, and y increases to the bottom.

Creating tiles

Start by creating a module tiles.py with this class:

import items, enemies

class MapTile:
    def __init__(self, x, y):
        self.x = x
        self.y = y

The import keyword means “give this module access the ‘items’ module and ‘enemies’ module. We need this because we will want to put these elements inside some of our rooms.

The MapTile class is going to provide a template for all of the tiles in our world, which means we need to define the methods that all tiles will need. First, we’ll want to display some text to the user when they enter the tile that describes the world. We also expect that some actions may take place when the player enters the tile, and that those actions change the state of the player (e.g., they pick something up, they win the game, something attacks them, etc.). Let’s add those methods now.

    def intro_text(self):
        raise NotImplementedError()

    def modify_player(self, player):
        raise NotImplementedError()

We haven’t talked about the code for the player yet, but that’s OK. The player parameter will serve as a placeholder. As you might guess, these methods aren’t going to do much in their current state. In fact, they will actually cause the program to crash! This might seem silly, but this behavior is to help us as programmers.

When thinking about our world, we don’t want to have tiles that do nothing. We may want tiles of water, tiles in a spaceship corridor, tiles with other characters, or tiles with treasure, but not empty tiles. So this MapTile class is actually just a template that all other tiles will expand on.

In the last post we learned about base classes. MapTile is actually a specific flavor of a base class. We call it an abstract base class because we don’t want to create any instances of it. In our game, we will only create specific types of tiles. We will never create a MapTile directly, instead we will create subclasses. The code raise NotImplementedError() will warn us if we accidentally create a MapTile directly.

Now on to our first tile subclass!

class StartingRoom(MapTile):
    def intro_text(self):
        return """
        You find yourself if a cave with a flickering torch on the wall.
        You can make out four paths, each equally as dark and foreboding.
        """

    def modify_player(self, player):
        #Room has no action on player
        pass

This class extends MapTile to make a more specific type of tile. We override the intro_text and modify_player methods to implement the specific behavior that this tile should have. A method is overridden when a subclass has the same method name as a superclass. Because it’s the starting room, I didn’t want anything to happen to the player. The pass keyword simply tells Python to not do anything. You might wonder why the method is even in this class if it doesn’t do anything. The reason is because if we don’t override modify_player, the superclass’s modify_player will execute and if that happens the program will crash because of raise NotImplementedError().

Next, let’s add a class for the tile where a player will find a new item.

class LootRoom(MapTile):
    def __init__(self, x, y, item):
        self.item = item
        super().__init__(x, y)

    def add_loot(self, player):
        player.inventory.append(self.item)

    def modify_player(self, player):
        self.add_loot(player)

Remember, we haven’t created player yet, but we can guess that the player will have an inventory.

Let’s define one more type of room: a room in which the player encounters an enemy.

class EnemyRoom(MapTile):
    def __init__(self, x, y, enemy):
        self.enemy = enemy
        super().__init__(x, y)

    def modify_player(self, the_player):
        if self.enemy.is_alive():
            the_player.hp = the_player.hp - self.enemy.damage
            print("Enemy does {} damage. You have {} HP remaining.".format(self.enemy.damage, the_player.hp))

This constructor should look familiar to you now. It’s very similar to the LootRoom constructor, but instead of an item, we are working with an enemy.

The logic for this room is a bit different. I didn’t want enemies to respawn. So if the player already visited this room and killed the enemy, they should not engage battle again. Assuming the enemy is alive, they attack the player and do damage to the player’s hit points.

Now that we have some basic types of tiles defined, we can make some even more specific versions. Here are some that I created:

class EmptyCavePath(MapTile):
    def intro_text(self):
        return """
        Another unremarkable part of the cave. You must forge onwards.
        """

    def modify_player(self, player):
        #Room has no action on player
        pass

class GiantSpiderRoom(EnemyRoom):
    def __init__(self, x, y):
        super().__init__(x, y, enemies.GiantSpider())

    def intro_text(self):
        if self.enemy.is_alive():
            return """
            A giant spider jumps down from its web in front of you!
            """
        else:
            return """
            The corpse of a dead spider rots on the ground.
            """

class FindDaggerRoom(LootRoom):
    def __init__(self, x, y):
        super().__init__(x, y, items.Dagger())

    def intro_text(self):
        return """
        Your notice something shiny in the corner.
        It's a dagger! You pick it up.
        """

If you remember, I also created an Ogre enemy and Gold item. You may choose to create corresponding rooms too.

Creating the world

We’re going to close out this post by actually creating a world based on the tiles we’ve defined. This delves into some advanced features so it’s OK if you don’t follow everything. I’ll explain everything briefly here, but I encourage you to read up on anything you’re interested in learning more about.

Create a new module in the same directory called world.py. Next, make a folder called “resources” that is in the same directory as the “adventuretotrial” directory and create map.txt inside. We’re going to build the world in this external file and load it into the game programatically.

I like to use a spreadsheet program and then copy the text into the map file, but you can just edit the file directly too. The goal is to lay out a grid of tiles whose names match the class names and are separated by tabs. Here’s an example in a spreadsheet:

Game tiles in spreadsheet

Remember, your map should not include MapTile, LootRoom, or EnemyRoom! Those are base classes that should not be created directly.

In the world module, add the following dictionary and method to parse the file you created.

_world = {}
starting_position = (0, 0)

def load_tiles():
    """Parses a file that describes the world space into the _world object"""
    with open('resources/map.txt', 'r') as f:
        rows = f.readlines()
    x_max = len(rows[0].split('\t')) # Assumes all rows contain the same number of tabs
    for y in range(len(rows)):
        cols = rows[y].split('\t')
        for x in range(x_max):
            tile_name = cols[x].replace('\n', '') # Windows users may need to replace '\r\n'
            if tile_name == 'StartingRoom':
                global starting_position
                starting_position = (x, y)
            _world[(x, y)] = None if tile_name == '' else getattr(__import__('tiles'), tile_name)(x, y)

The parsing method goes through each line of the file and splits the line into cells. Using a double for loop is a common way of working with grids. The x and y variables keep track of the coordinates. When we find the Starting Room, that position is saved because we will use it later. We use the global keyword to let us access the starting_position variable that lives outside of this method. The last line is the most interesting, but it’s fine if you don’t fully understand it.

The variable _world is a dictionary that maps a coordinate pair to a tile. So the code _world[(x, y)] creates the key (i.e. the coordinate pair) of the dictionary. If the cell is an empty string, we don’t want to store a tile in it’s place which is why we have the code None if tile_name == ''. However, if the cell does contain a name, we want to actually create a tile of that type. The getattr method is built into Python and lets us reflect into the tile module and find the class whose name matches tile_name. Finally the (x, y) passes the coordinates to the constructor of the tile.

Looking for an easier approach to building your world? Check out Chapter 13 of Make Your Own Python Text Adventure.

Essentially what we’re doing is using some advanced features in Python as an alternative to something like this:

tile_map = [[FindGoldRoom(),GiantSpiderRoom(),None,None,None],
            [None,StartingRoom(),EmptyCave(),EmptyCave(),None]
           ]

That’s hard to read and maintain. Using a text file makes changing our world easy. It’s also a lot simpler to visualize the world space.

Keep in mind that the only reason we are able to do this is because all of our tile classes derive from the same base class with a common constructor that accepts the parameters x and y.

Let’s add one more method to the world module that will make working with the tiles a little easier:

def tile_exists(x, y):
    return _world.get((x, y))

Congratulations for making it this far! If you’d like to see how to make the game easier or more difficult with different enemy types, see Chapter 12 of Make Your Own Python Text Adventure.

Click here for Part 3 of the abridged tutorial.

Tagged on: , , , ,

110 thoughts on “How to Write a Text Adventure in Python Part 2: The World Space

  1. Kenneth Prockl

    Thank you for this tutorial. I am getting an error on the line: tile_name = cols[x].replace(‘\n’, ”), the error is Index Error: List index is out of range. I’m pretty sure it replaces line breaks (\n) with empties (”) in the cols list and assigns it to a new varrible: title_name. I cant seem to get it to work for me. I’ve copied and pasted your github code to be sure its not a something I overlooked. What version of python are you using?

    1. Phillip

      My guess is that your text file containing the rooms does not have the same number of tabs in each rows. The error you are getting is because x is larger than the number of columns in the current row. A few lines above x_max is set to the number of columns in the first row.This assumes that all rows are “padded” with tabs to make sure they are the same length.

      This code should work on Python 3.4.x.

  2. Radd

    Im getting an error on “_world[(x, y)] = None if title_name == ” els getattr(_import_(‘titles’) . . . “. It’s tellimg me name ‘_import_ isn’t defined??

  3. Mika

    Hello Phillip,
    I’ve got some problems with enemyroom. When I create tile enemyroom, it look like this:
    world = {(2,5): tiles.Enemyroom(2,5, enemy.Cerberus(1)
    That’s how it should look like but when I write “enemy.” ,window should appear with names of superclassess, but instead in this window hp, attack and other stuff like this is written. So I am trying to write enemy.Cerberus, but error appears- “unresolved reference enemy”
    What should I do? I don’t understand what’s happening. I am just trying to call enemy just like an item, but it gives me error.

  4. Alfred

    Thanks for this tutorial, but I am getting an error, despite the fact that I have copied and pasted it. The section of code is:
    tile_name = cols[x].replace(‘\n’, ”)
    _world[(x, y)] = None if tile_name == ” else getattr(__import__(’tiles’), tile_name)(x, y)
    When I run it, I get the error (‘module’ object has no attribute ‘ ‘). Any thoughts on how to fix this? I have tried replacing ‘\n’ with ‘\r\n’, because I am running Windows, but I still got the same error. Any help will be greatly appreciated.

    1. Phillip

      Well, that suggests that cols[x] does not exist, so you are just getting an empty string. Naturally, that empty string is not found in the possible names. I would verify that your tile map is correct and make sure you understand the assumptions the code makes about that map. Happy coding!

      1. Ryan

        im still getting this error and i dont know what else I can do
        my code:

        def load_tiles():
            """Parses a file that describes the world space into the _world object"""
            with open('resources/map.txt', 'r') as f:
                rows = f.readlines()
            x_max = len(rows[4].split('\t')) # Assumes all rows contain the same number of tabs
            for y in range(len(rows)):
                cols = rows[y].split('\t')
                for x in range(x_max):
                    tile_name = cols[x].replace('\r\n', '') # Windows users may need to replace '\r\n'
                    if tile_name == 'StartingRoom':
                        global starting_position
                        starting_position = (x, y)
                    if tile_name == '':
                        _world[(x, y)] = None
                    else:
                        getattr(__import__('tiles'), tile_name)(x, y)
        

        I’ve had to make several changes to the code.

  5. Mark

    Hi
    I can’t import either of the items.py or enemies.py modules. When I type ‘import items, enemies’, I receive the following error: Traceback (most recent call last): File “”, line 1, in <modules import items ImportError: No module named 'items'. I have tried several things to fix this, like adding the _init_.py file to all folders in the path, saved tiles.py in its own subfolder, and only trying to import one module at a time. I have checked the code in the modules I created previous to tiles.py, and they are all ok. Any ideas about what else I could try? Thanks for any help in advance!

    1. Phillip

      At this point in the tutorial there’s nothing really to run. But if you are trying to run something, try taking a look at the instructions in Part 4. You need to make sure you are executing the code from the correct directory and (possibly) that your PYTHONPATH is set properly.

  6. Ricky

    Hi.
    This guide as been a huge help I have only come a across one bug in my coding that I cant figure out why it is creating problems. it tells me I have invalid syntax and it highlights the import in the

    world[(x, y)] = None if tile_name == '' else getattr(import ('tiles'), tile name)(x, y)

    statement I don’t know why. can you think of why it would be doing this?

    1. Phillip

      You have a few typos. Here’s that line in my code:

      _world[(x, y)] = None if tile_name == '' else getattr(__import__('tiles'), tile_name)(x, y)
  7. Ryan

    I am getting this error when I try to load my game:

    Traceback (most recent call last):
    File “C:\Users\Ryan\PycharmProjects\Final Project – Coffee Shop\game.py”, line 27, in
    play()
    File “C:\Users\Ryan\PycharmProjects\Final Project – Coffee Shop\game.py”, line 12, in play
    room.modify_player(player)
    AttributeError: ‘NoneType’ object has no attribute ‘modify_player’

    Any idea why that is happening?

    1. Phillip

      This means that no room was found at the given coordinates, so Python returned None. Make sure to review the assumptions about the map file above. If all else fails, use the file included in the GitHub project, and then make modifications to it. Happy coding!

  8. Andrew Maenza

    File “C:/Users/Drew/Desktop/Intro Game Dev Project 1\world.py”, line 21, in load_tiles
    _world[(x, y)] = None if tile_name == ” else getattr(__import__(’tiles’), tile_name)(x, y)
    ImportError: No module named tiles

    What is the reason for this error? I checked to make sure things aren’t mistyped

  9. Cameron Leslie

    Ok, so whenever I type “import items, enemies”, it says unresolved import and unused import for both names. Any ideas on how to fix this?

      1. Caleb

        when I run the program it says File “C:\Users\ncssurfabc\Desktop\adventure\game.py”, line 1, in
        import world
        File “C:\Users\ncssurfabc\Desktop\adventure\world.py”, line 18, in
        tile_map = [[FindDaggerRoom(),GiantSpiderRoom(),None,None,None],
        NameError: name ‘FindDaggerRoom’ is not defined

  10. James

    Hey Phillip, I’ve been getting an attribute error on line 27,

    _world[(x, y)] = None if tile_name == ” else getattr(__import__(’tiles’), tile_name)(x, y)

    It says that the module ’tiles’ has no attribute (random mess in quotes).

    Would you happen to know what I should be looking at?

    1. Phillip Johnson Post author

      There’s a difference between '('module' object has no attribute '') and '('module' object has no attribute 'OgreRoom'). The former has been discussed previously in the comments above. If you’re getting the second error, then it is probably because Python cannot find all of your code at runtime. Make sure you are in the correct directory when you run the code and that you are running it from a terminal/command line. If all else fails, you may need to adjust your PYTHONPATH, but that’s probably the solution to another more systemic problem.

  11. Jacob

    When creating the world, how would you create a room so that if the player had a certain item (ex: the dagger), they would be able to pass, and if not, the room would be impossible to go through, so you couldn’t get to the next room without this item?

    1. Phillip Johnson Post author

      I would probably implement that in the available_actions() method on the Player class. You would need to do a check to see if the adjacent room is of type XYZ and check the player’s inventory to see if they have the required item. If either of those conditions is false, remove the action from the list of available actions.

      1. Jacob

        Thanks, your idea worked perfectly! Only I’ve come across yet another error. I’ve decided to modify the entire game to create a Pokemon-type adventure. Everything works fine, only with Pikachu (a weapon), it says that it’s missing the statement “collected = 0”. And the command is clearly there. I’ve tried deleting and retyping this part of the code, and it’s still not cooperating. Suggestions?

  12. xxsns

    Hey!

    First of all I’d like to thank you for your great guide. I had no idea how to get a game loop going and this really helped me out a lot.

    However, I’m running into an error that already has been answered by you, though I don’t quite follow your explanation. (the error was a little different too)

    ” _world[(x, y)] = None if tile_name == “” else getattr(__import__(“tiles”), tile_name)(x, y)
    AttributeError: module ’tiles’ has no attribute ‘ ”
    is the error I encounter.

    I’m genuinely clueless, I’m very sorry

    1. xxsns

      EDIT:
      AttributeError: module ’tiles’ has no attribute ‘StartingRoom

      sorry I quoted the error wrongly.

      I’m running it from command line and pythonpath has to be okay since any other python script runs smoothly from commandline.
      what am I missing?

      1. Phillip Johnson Post author

        It looks like you’re having the same problem as Alfred was above. I would suggest adding a print(cols[x]) so you can actually see the value of tile_name. At some point, you’ll see that tile_name does not have a value. This should give you a hint as to where the problem is in your map file because you’ll see where the code fails.

        1. xxsns

          Thanks a lot, for some reason cols[x] prints empty a few empty lines even though I replace line breaks with ”

          I tripple checked the map.txt and there is no whitespace, only linebreaks, text and tabs.
          the rest of the print looks normal

  13. Jake

    Thanks for your continued support for this tutorial! It’s been really fun and informative as a novice programmer. I’ve solved a few things from your responses above but now I’m running into this error:

    _world[(x, y)] = None if tile_name == '' else getattr(__import__('tiles'), tile_name)(x, y)
    TypeError: __init__() missing 1 required positional argument: 'y'
    

    I’m really at a loss as to what it’s referencing. Any thoughts?

    1. Phillip Johnson Post author

      It most likely means there is a problem with the __init__ method of the tile you are trying to create. To figure out the problem tile, try printing out tile_name, x, and y. When you know the tile causing the problem, make sure it’s initializer is correct. If you’re still stuck, compare your code with the code in the GitHub repo and that should pinpoint the error.

  14. Max

    Hello
    I have been using this tutorial heavily and I have to say it explains quite a lot. However, I get this error every time I attempt to run it

    _world[(x, y)] = None if tile_name == ” else getattr(__import__(’tiles’), tile_name)(x, y)
    AttributeError: ‘module’ object has no attribute ‘End_game’
    I have not used End_game anywhere and I am incredibly confused.
    Thanks for the tutorial

  15. Meris

    I’m having a problem running the GiantSpiderRoom class on Python 3.5.2. I keep getting the error:

    ” File “/home/adventuretutorial/tiles.py”, line 81, in __init__
    super().__init__(x, y, enemies.GiantSpider())
    TypeError: super() takes at least 1 argument (0 given)”

    My code is this:

    “class GiantSpiderRoom(EnemyRoom):
    def __init__(self, x, y):
    super().__init__(x, y, enemies.GiantSpider())”

    I’ve looked at your previous answer to this problem, but it was for Python 2.x, not 3.x, so I’m very confused.

      1. Meris

        Nevermind, I started this project on WIndows 8.1, but became so frustrated with Windows Command Line that I dual booted my laptop with Ubuntu 16.04 and the default call of “python” is for python 2.7.11, not python 3.x.

  16. Meris

    I’ve been having some problems with the map.txt, actually. I’ve created a 13×13 map using LibreOffice, but no matter what I do, it says:

    ” File “/home/adventuretutorial/world.py”, line 12, in load_tiles
    tile_name = cols[x].replace(‘\n’, ”)
    IndexError: list index out of range”

    I understand that this means that what I actually created did not have the same number of rows in each column, but I cannot figure out what is wrong. I copied and pasted your map.txt in place of mine and the code still gave me the same error. (I also copied and pasted your world.py script to see if mine was the problem, but I received the same error again.

    I downloaded your GitHub file and have modified it to include everything I wanted, like healing items and more weapons and enemies, but whenever I try to modify your map.txt file, I receive this same error. Is there another way to creating a large map?

    1. Phillip Johnson Post author

      Try printing x_max and x to help debug. You will probably see that x_max is something like 12 when you expect 13. You don’t have to use LibreOffice. Just open the file in a text editor and turn on “show whitespace characters”.

      Take a look at the first comment string on this post. That may also help you out.

  17. Jake

    Hi Phillip,

    Excellent tutorial for a beginner like myself – I’ve really enjoyed it, so thank you!

    I’m running this in Python 2.7 on Ubuntu, and I’ve changed the super() calls to suit, but I’m getting an error, and not sure if it is because of my version, or I’m doing something wrong. Could you help?

    _world[(x, y)] = None if tile_name == ” else getattr(__import__(’tiles’), tile_name)(x, y)
    TypeError: GiantSpiderRoom() takes exactly 1 argument (2 given)

    Here is the tile code:
    def GiantSpiderRoom(EnemyRoom):
    def __init__(self, x, y):
    super(GiantSpiderRoom, self).__init__(x, y, enemies.GiantSpider())

    1. Phillip Johnson Post author

      This tutorial is written for Python 3, not 2. I believe some people in the comments may have posted some code changes that make it compatible with Python 2, but I highly recommend using Python 3 in general.

  18. Benjamin Bridges

    I have everything in place but when I run game.py, only the ViewInventory action is utilized. I don’t think any of the adjacent rooms are being noticed.

    I tried modifying world.py to fix it and I still get the same issue. The game runs, but I can only view my inventory.

    1. Phillip Johnson Post author

      You might try debugging by printing out information. In load_tiles(), you can do print("X: " + str(x) + " Y: " + str(y)) and print(_world[(x, y)]) to see all the rooms being added. Then you can try adding print statements to the adjacent_moves method of the Player class. If all else fails, compare your code and map to what I have up at GitHub.

  19. Allie Curtis

    Hello Phillip!

    I’m in Chapter 13 of your book, Expanding the World. I have isolated my problem down to this block of code:

    def parse_world_dsl():
        if not is_dsl_valid(world_dsl):
            raise SyntaxError("DSL is invalid!")
    
        dsl_lines = world_dsl.splitlines()
        dsl_lines = [x for x in dsl_lines if x]
    
        for y, dsl_row in enumerate(dsl_lines):
            row = []
            dsl_cells = dsl_row.split()
            dsl_cells = [c for c in dsl_cells if c]
            for x, dsl_cell in enumerate(dsl_cells):
                tile_type = tile_type_dict[dsl_cell]
                row.append(tile_type(x, y) if tile_type else None)
    
            world_map.append(row)
    

    When I run my game.py, I get the error:

    AttributeError: ‘NoneType’ object has no attribute ‘intro_text’

    I have erased the entire chapter’s code and re-entered it, so I know it’s all entered correctly. However I must be missing something. My assumption is that the rows are not correctly appending to the world_map list, so when my game tries to access the intro_text for the selected tile it returns ‘None’ as the tile type. This is totally a guess, however.

    1. Allie Curtis

      The actual error is at this point:

      room = world.tile_at(player.x, player.y)
      print(room.intro_text())
      room.modify_player(player)
      choose_action(room, player)
      

      and when I added print(room) below room = world.tile I got ‘None’ and then the error again, so I’m not getting a value for my tile_at function D:

      And I’d like to say that your book is fantastic! I’ve only ever made choose-your-own-adventure games with tons and tons of if and elif statements, so this tutorial has been invaluable in making a more involved game. Thank you so much!

      1. Allie Curtis

        I’m so sorry, I hate that you can’t edit comments on this. Found one error:

        I changed this:
        dsl_cells = dsl_row.split()

        to this:
        dsl_cells = dsl_row.split("|")

        but I still get the same error, unfortunately. From game.py I tried print(world.world_map) and got an empty list, so the items “ST” and “VT” etc. aren’t being added to the world_map list for some reason :S

        1. Phillip Johnson Post author

          Hi Allie,

          You’re on the right track here! The error you are getting is because nothing is loaded into the map. When the game tries to find the starting tile, nothing is there, so Python uses None, which of course does not have an intro_text method.

          The trick is going to be figuring out what’s wrong with your DSL. I would add print statements to the parse_world_dsl() method. If you need to, do it after each line of code. Make sure your tile_type_dict is also correct. If all else fails, you can do an online diff between my code and yours.

          Happy coding!

          1. Allie Curtis

            Hey Phillip!

            Thank you so much for answering. I ended up getting it, I needed to add world.parse_world_dsl() above player = Player() in game.py 😀

            Thanks for your help!

  20. Kelvin

    Just bought the book and I’m in the world building section. Which is more “pythonic” using the excel spreadsheet to make the world or the DSL method?

    1. Phillip Johnson Post author

      Both methods use a DSL, so it’s more a question of putting the layout as a string directly in the code or storing it in a file. I don’t think either method is more pythonic than the other. However, I personally prefer to store it in a file. Especially if you had a game with a large world and/or multiple levels, trying to put all of that directly in code would get annoying. However, reading from a file adds some complexity that I wanted to avoid when writing the book so I went with the simpler method.

      1. Kelvin

        Thanks for answering my questions! This book is awesome and been a great resource for understanding classes and python in general in a fun way.

  21. Jarvis

    Hey Phillip,

    First of this is a great resource for beginners like me, so thank you!

    Second, if I wanted to input the coordinates of the rooms directly into my tiles, how would I do it? Currently this is an example of my code:

    class Room():#template for all rooms
        def __init__(self, x, y):#constructor
            self.x=x
            self.y=y
    
        def intro_text(self):
            raise NotImplementedError()
    
        def modify_player(self, player):
            raise NotImplementedError()
    
    class road_four(Room):
        def __init__(self):
            super().__init__(2, 2)
        def intro_text(self):
            return """It's an empty road. There's a path forward and to your left.
    The right is blocked.
    Where will you go?"""
        def modify_player(self, player): #no action on player
            pass
    

    Notice how I input the coordinates into the super().__int__ of the subclass itself. But this doesn’t seem to be working. for example when I use:

    __world = {}
    starting_position = (0, 0)
    
    def tile_exists(x, y):
             return __world.get((x, y))
    

    to pull up the room at any particular coordinate I get None.

    Please help!

    1. Phillip Johnson Post author

      Well if you aren’t using the map method of creating the tiles, you need something else to put the tiles into the __world variable. Something like:

      __world[(0,0)] = starting_room()
      __world[(2,2)] = room_four()
      

      But this is really not a great idea and will give you a lot of headaches. If you don’t want to use the map file, at least something like this is still preferable:

      tile_map = [[FindGoldRoom(),GiantSpiderRoom(),None,None,None],
                  [None,StartingRoom(),EmptyCave(),EmptyCave(),None]
                 ]
      
  22. Someone

    Can you help? The section on making the map is rather vague, and I have no money to buy the book.
    I’ve made the map to the best of my ability, and I keep getting an error:

    _world[(x, y)] = None if tile_name == '' else getattr(__import__('tiles'), tile_name)(x, y)
    AttributeError: module 'tiles' has no attribute ''
    

    I really have no idea what to do at this point. I apologize if someone has asked this before, but I really can’t find anything that will help.

  23. Daniel

    Phillip,

    Bought your book.

    Really enjoyed it.

    Looking to expand my game a bit more than I have and make enemy tiles respawn.

    Not sure how I would do it though.

    1. Phillip Johnson Post author

      First I would create a respawn method in the Enemy class to set the HP back to 100.

      Next, I would create a new method called pre_check and put it into MapTile with a pass body. Then call that method in the play method in game.py. Finally, override the pre_check method in EnemyTile and do something like this:

      r = random.random()
      if r < 0.20:
          self.enemy.respawn()
      

      Hope that helps and happy programming!

      1. Daniel

        Wouldn’t this just give the same enemy that was generated originally 100 HP again?

        Trying to also make the specific tile re spawn a random enemy when it does.

        1. Phillip Johnson Post author

          Sure, that’s actually easier. Move the code that creates an enemy into a new method like def create_enemy(self). Then call the create_enemy method in both the init and pre_check methods.

  24. Caleb

    when I run the code I keep getting the error File “C:\Users\ncssurfabc\Desktop\adventure\world.py”, line 16, in load_tiles
    _world[(x, y)] = None if tile_name == ” else getattr(__import__(’tiles’), tile_name)(x, y)
    AttributeError: module ’tiles’ has no attribute ‘ def play():’
    and I looked over my code and cannot find the problem _world[(x, y)] = None if tile_name == ” else getattr(__import__(’tiles’), tile_name)(x, y)

    1. Phillip Johnson Post author

      Go back and review your code against the code in the tutorial. You can also view all the code on GitHub. I can’t tell specicially without seeing your code, but there’s no reason that the tiles module should be trying to load something called  def play(). It’s also suspicious that there is a space before def.

  25. Brandon

    I’m running into this error:

    Traceback (most recent call last):
      File "C:\Users\reede_000\Desktop\gameCode\gamefile\Game.py", line 33, in 
        play()
      File "C:\Users\reede_000\Desktop\gameCode\gamefile\Game.py", line 12, in play
        player = player()
    UnboundLocalError: local variable 'player' referenced before assignment
    

    Only thing i can think of is in my player class i have functions ex: maxHpCalc() that use player.attributes in their value. without them the functions would fail so i’m not sure if that’s the issue or something else entirely.

    1. Phillip Johnson Post author

      You’ve got a typo: there is a difference between player() and Player(). When you create the player object, you have to capitalize it so it refers back to the Player class. Hope that helps!

      1. Brandon

        all my python files start with capitals ex: Player.py my classes and all my functions are player() all starting with lowercase letter.

        1. Phillip Johnson Post author

          I would recommend not doing that since it’s not the standard way to write Python. If you still want to, then you will need to change the name of your variable to something other than player such as the_player = player().

  26. Brandon

    so i’m having issues with things like

    class moveSouth(action):
        def __init__(self):
            super().__init__(method = Player.thePlayer.moveSouth, name='Move south', hotkey='s')
    

    the Player.thePlayer.moveSouth is all correct and works works. but i’m confused what i should actually be putting in most of these. for instance.
    ————————-

      File "C:\Users\reede_000\Desktop\gameCode\gamefile\Game.py", line 32, in play
        if actionInput == actions.hotkey:
    NameError: name 'actions' is not defined
    

    Should i be putting them in like this: pythonModule.class.function? i have everything imported so i’m not understanding why i should have to put in both in the moveSouth function, but once i did it worked.

    also i started mine like this instead of play()

    def main1():
        global player
        print("\nWelcome to the most bad ass game ever!\n")
        print("What is your name?")
        playerName = input ("---->")
        print("Welcome %s." % playerName)
        time.sleep(1)
        print("\nLet's start %s!" % gameName)
        time.sleep(1)
        player = Main.pickRace(playerName)
        play()
    
    main1()
    

    ———————–
    i kept running into player not defined once i was already in the play() so i just made it global.. once my pickRace() returns the race i pick. player.moveSouth should be the same thing as typing magmar.moveSouth Correct? so i’m not seeing why i should have to put in pyfile then my class to access these things. any help on what’s going on?

    1. Phillip Johnson Post author

      Whether or not you have to use the module.Class.method syntax when creating a player action depends on your imports. In the tutorial, I use the from module import Class syntax to import the player. So you would probably do from Player import thePlayer (again, that naming style is not standard Python and may be adding to your confusion).

      I don’t fully understand your last question, but when you are referring to a method, you should refer to its definition in a class, not an instance of the class. In my code, Player.move_south refers to the definition of the move_south method in the Player class. If I were to do something like the_player_instance.move_south it would not work for the reason you stated: the_player_instance is not defined.

      1. Brandon

        so i HAD Main.py which had the main1() and pickRaise() and raiseStat() along with some elif input == “stats”, “look %s” % player.name which prints all stats, and updated them when they were raised. look name printed out hp, max hp mp and such and updated them with my maxHPCalc(). everything in it worked perfectly. then i moved the main1() before play so i could pick a race before entering into the game. makes perfect sense. but once i did that. my pickRaise i had to put Main.pickRace() and Main.raiseStat(). then when i return my pick race (player = Main.pickRace() ) it tells me magmar isn’t defined.

        option = input(“—>”)

        if option == “magmar”:
        print(“\n you have chosen %s” %option)
        time.sleep(1)
        return magmar(playerName)

        i haven’t changed any of this coding in my pickrace() or raiseStat() functions but since moving the Main1() now everything is broken. in Game.py i did use the regular import Player, import Main which are the py files. instead of from Player import thePlayer
        should i be using from Main import pickRace, raiseStat instead of importing the whole python file?
        ————————-
        once i create an instance of my race. player = magmar() i can then access that instance with things like print(player.hp) which should print out the current hp of the magmar instance i created which is equal to player? which was the move_south question from before.

  27. Brandon

    so i changed my imports from import Main to from Main import pickRace, raiseStat which got my past having to put Main.pickRace()
    then i get you have chosen magmar
    Traceback (most recent call last):
    File “C:\Users\reede_000\Desktop\gameCode\gamefile\Game.py”, line 48, in
    main1()
    File “C:\Users\reede_000\Desktop\gameCode\gamefile\Game.py”, line 46, in main1
    play()
    File “C:\Users\reede_000\Desktop\gameCode\gamefile\Game.py”, line 19, in play
    room = World.tileExists(player.locationX, player.locationY)
    NameError: name ‘player’ is not defined

    so then i readded global player in the main1() and then i get past that to loading the available rooms and get:
    you have chosen magmar
    As you enter the plane you shiver with anticipation of the coming adventure!
    Choose an action:

    Traceback (most recent call last):
    File “C:\Users\reede_000\Desktop\gameCode\gamefile\Game.py”, line 48, in
    main1()
    File “C:\Users\reede_000\Desktop\gameCode\gamefile\Game.py”, line 46, in main1
    play()
    File “C:\Users\reede_000\Desktop\gameCode\gamefile\Game.py”, line 27, in play
    availableActions = room.availableActions()
    File “C:\Users\reede_000\Desktop\gameCode\gamefile\MapTile.py”, line 44, in availableActions
    moves = self.adjacentMoves()
    File “C:\Users\reede_000\Desktop\gameCode\gamefile\MapTile.py”, line 30, in adjacentMoves
    moves.append(Actions.moveSouth())
    File “C:\Users\reede_000\Desktop\gameCode\gamefile\Actions.py”, line 36, in __init__
    super().__init__(method = player.moveSouth, name=’Move south’, hotkey=’s’)
    NameError: name ‘player’ is not defined
    not sure why player.location would work and player.METHOD doesn’t.

  28. Josh

    So I’m working with this on the IDE called Cloud9, and I was wondering how to make a spreadsheet in there. Could you walk me through that? I’m working on this for a final project in my IT class and we need to be done in two weeks

  29. Mike Foy

    I really love this tutorial. It’s great for a beginner like me. After running this in python shell within command prompt and debugging many of my typos i’m stuck.

    C:\Users\student\Desktop\PythonPractice>python adventuretutorial/game.py
    Traceback (most recent call last):
      File "adventuretutorial/game.py", line 29, in 
        play()
      File "adventuretutorial/game.py", line 6, in play
        world.load_tiles()
      File "C:\Users\student\Desktop\PythonPractice\adventuretutorial\world.py", line 6, in load_tiles
        with open('resources/map.txt', 'r') as f:
    FileNotFoundError: [Errno 2] No such file or directory: 'resources/map.txt'
    

    Any help would be greatly appreciated.

  30. Talon

    I found a bug when I tried to run it, but I could not find any errors from that.

    Traceback (most recent call last):
      File "/Users/apple/PycharmProjects/Game Development/game.py", line 19, in 
        play()
      File "/Users/apple/PycharmProjects/Game Development/game.py", line 4, in play
        world.load_tiles()
      File "/Users/apple/PycharmProjects/Game Development/world.py", line 20, in load_tiles
        _world[(x, y)] = getattr(__import__('tiles'), tile_name)(x, y)
    TypeError: __init__() missing 1 required positional argument: 'enemy'
    
    Process finished with exit code 1
    
    1. Phillip Johnson Post author

      Double check all your enemy tiles. Make sure that you have something like super().__init__(x, y, enemies.GiantSpider()) in all of them. This error is because you are trying to create an enemy tile without passing in an actual enemy, hence the message “required positional argument: enemy”.

  31. kylar

    THE CODE USED:

    _world = {}
    starting_postition = (0,0)
    
    def load_tiles():
        """Parses a file that describes the world space into the _world object"""
        with open('resources/map.txt', 'r') as f:
            rows = f.readlines()
        x_max = len(rows[0].split('\t')) 
        for y in range(len(rows)):
            cols = rows[y].split('\t')
            for x in range(x_max):
                tile_name = cols[x].replace('\r\n', '')
                if tile_name == 'StartingRoom':
                    global starting_position
                    starting_position = (x, y)
                _world[(x, y)] = None if tile_name == '' else getattr(__import__('tiles'), tile_name)(x, y)
    

    ERROR RECEIVED:

      File "C:\Users\****\Anaconda3\Scripts\Text Adventure\world.py", line 26, in load_tiles
        _world[(x, y)] = None if tile_name == '' else getattr(__import__('tiles'), tile_name)(x, y)
    
    TypeError: object() takes no parameters
    

    Hi loving this tutorial so far I really like how it can be expanded on. I have made a couple of mistakes which i have managed to resolve on my own but this one has me stumped I know I’ve done something stupid but I can’t figure out where I have gone wrong any help would be appreciated.
    Many Thanks,
    K

    1. Phillip Johnson Post author

      So the code you copied (correctly) tries to pass (x, y) into the object that it is creating. But the error means that the object being created doesn’t accept (x, y). I would take a look at your tiles.py file and make sure all the tile types you created correctly extended from MapTile, e.g. LeaveCaveRoom(MapTile). If you still can’t find the error, add print statements inside of load_tiles to print out the values of x and y so you can determine which tile, specifically, it is failing on.

      1. kylar

        Thank you so much for you fast response and you was completely correct i missed the MapTile off the Puzzle Room I had added silly mistake! 🙂

  32. Diego

    Hi, I’m completely new to python and relatively new to programming. I’ve been using your tutorial to understand python. I’ve come to an error with world.py.

    What I’ve been doing is just changing some names to make it easier for me to understand (basically just some translations into Spanish. I’ve come across with the following error when trying to load the map tiles:

       _mundo[(x,y)] = None if nombre_cuad == '' else getattr(__import__('mapa'), nombre_cuad)(x,y)
    TypeError: __init__() takes exactly 1 argument (3 given)
    

    And I’m completely clueless about why that is happening. What should I be looking for?

    1. Phillip Johnson Post author

      This error means that the “thing” found in your map at that location has an incorrect init method. You’ll need to first find out where the problem is. I recommend printing out x and y so can see where the error occurs. Then look at your code for that tile. Make sure it correctly loads from the base MapTile as in StartingRoom(MapTile) and also double-check your MapTile init looks like def __init__(self, x, y). If you still can’t figure it out, post a link to your code and I can take a look.

      1. Diego

        Thanks for helping me wthh this one! I totally failed. Here’s the link to pastebin with my world.py file (I miss-named the file in the link): https://pastebin.com/JWVPUZ21

        BTW, if I want to print the name of the tile by calling load_tiles() in world.py, is it even posible or should I make another fuction for that?

        1. Phillip Johnson Post author

          The error is probably in tiles.py, did you check there?

          Try this to print the name of the tile:

          class MapTile:
              """The base class for a tile within the world space"""
              def __init__(self, x, y):
                  """Creates a new tile.
          
                  :param x: the x-coordinate of the tile
                  :param y: the y-coordinate of the tile
                  """
                  self.x = x
                  self.y = y
                  print("Created Tile {} at ({},{})".format(type(self).__name__, self.x, self.y))
          
          1. Spencer

            Hi, can you post the whole game as one flow of code please, I’m doing a project and this would really help me out. Once I get the code I can make mods and add various things…

            Thanks Spencer

  33. Ethan

    I’m getting “NameError: name ‘Player’ is not defined. No clue what that’s about. This after debugging other things like the tabs parsing and all. I found also that using asterisks instead of empty cells was better for formatting the map and avoiding the row length errors.

  34. Ryan

    So, my code looks like this:

    _world = {}
    starting_position = (0, 0)
     
    def load_tiles():
        """Parses a file that describes the world space into the _world object"""
        with open('resources/map.txt', 'r') as f:
            rows = f.readlines()
        x_max = len(rows[0].split('\t')) # Assumes all rows contain the same number of tabs
        for y in range(len(rows)):
            cols = rows[y].split('\t')
            for x in range(x_max):
                tile_name = cols[x].replace('\r\n', '') # Windows users may need to replace '\r\n'
                if tile_name == 'StartingRoom':
                    global starting_position
                    starting_position = (x, y)
                _world[(x, y)] = None if tile_name == '' else getattr(__import__('tiles'), tile_name)(x, y)
    def tile_exists(x, y):
        return _world.get((x, y))
    

    which is exactly the same as yours, but I continue to get the error code below.

     File "C:\Users\Ryan\.spyder-py3\AdventureTutorial\world.py", line 23, in load_tiles
        _world[(x, y)] = None if tile_name == '' else getattr(__import__('tiles'), tile_name)(x, y)
    ModuleNotFoundError: No module named 'tiles'
    
      1. Ryan

        Yes, I have.
        I’m trying to run it through my command prompt, and have used the cd command to get to my directory where everything is. Then I use the START command to run the program.

          1. Ryan

            So, I went into my console, and went to where the folder is, and then typed ‘python AdventureTutorial/game.py’
            But nothing happens, should I be using the path to get there or the cd command?

  35. speedstriker

    I notice that each time I enter the FIND 5 GOLD room I get 5 gold, even though I’ve gone inside 20 times. I now have 20x 5 gold from the same room

  36. Felipe Fuentes

    Hi i’m trying to create a game based on legends of my country and get this error of wich can’t solve, i already read all the comments above and there’s some that are similar to my problem but i can’t get any solution for this.
    Thanks for the tutorial i got a lot of fun creating the game and learning at the same time.

    Here’s the error that i get in WindowsPowerShell:

    Traceback (most recent call last):
    PS C:\Users\…\python\ejercicios\leyendas del sur de chile> python game.py
    Traceback (most recent call last):
    File “game.py”, line 26, in
    play()
    File “game.py”, line 5, in play
    world.load_tiles()
    File “C:\Users\…\python\ejercicios\leyendas del sur de chile\world.py”, line 16, in load_tiles
    _world[(x, y)] = None if tile_name == ‘ ‘ else getattr(__import__(’tiles’), tile_name)(x, y)
    AttributeError: ‘module’ object has no attribute ”

    1. Phillip Johnson Post author

      It means there is extra or invalid whitespace in your map file. Make sure you don’t have any extra spaces, line breaks, etc. Sometimes it helps to use a text editor like Notepad++ so you see all the whitespace characters.

  37. The0Wolf1

    I have tried everything but I can’t get this to work:
    Traceback (most recent call last):
    File “C:\Users\chr05012\OneDrive – Vestfold og Telemark fylkeskommune\Skrivebord\Python\AdventureGameTutorial\game.py”, line 1, in
    import world
    File “C:\Users\chr05012\OneDrive – Vestfold og Telemark fylkeskommune\Skrivebord\Python\AdventureGameTutorial\world.py”, line 6
    with open(‘C:\Users\chr05012\OneDrive – Vestfold og Telemark fylkeskommune\Skrivebord\Python\AdventureGameTutorial\map.txt’, ‘r’) as f:
    ^
    SyntaxError: (unicode error) ‘unicodeescape’ codec can’t decode bytes in position 2-3: truncated \UXXXXXXXX escape
    >>>

Leave a Reply

Your email address will not be published. Required fields are marked *