How to Write a Text Adventure in Python

People new to programming often ask for suggestions of what projects they should work on and a common reply is, “Write a text adventure game!” I think there are even some popular tutorials floating around that assign this as homework since I see it so much. This is a really good suggestion for a few reasons:

  • The concept is familiar and fun (everyone loves games!)
  • They can be written using core libraries
  • The UI is the console

But new programmers often struggle with knowing where to start. That’s why I wrote and published Make Your Own Text Adventure With Python. This book is a structured approach to learning Python that teaches the fundamentals of the language, while also guiding the development of your own customizable text adventure game.

Click here to get the book!

For those of you who know some Python and just need a little guidance, there’s an abbreviated version of the book material here on the blog. It assumes you are familiar with basic programming concepts (if-statements, loops, objects, etc.), but are still new to writing full applications.

Just looking for some code? You can view the tutorial version of the game on GitHub.

Tagged on: , , , ,

90 thoughts on “How to Write a Text Adventure in Python

  1. Robert Barton

    Hello, I’ve been testing out your Python Adventure, and for the life of me I’m unable to get it to work.

    I receive the following error.

    Traceback (most recent call last):
    File “adventuretutorial/game.py”, line 5, in
    from adventuretutorial import world
    ImportError: No module named ‘adventuretutorial’

    I’ve done a litte reasearch but I can’t find anything thats relative to the problem I’m having I’ve tested it on multiple versions of Linux / Windows and multiple versions of python 2.7, 3, 3.2, 3.3, 3.4

    But the same issue continues to occur!

    I would love pointing in the right direction to help me resolve this as I’m really interested in learning python and this guide is very comprehensive and covers many areas I find interesting.

    Best regards,

    Robert Barton

    1. Phillip

      Please see Part #4:

      To run the program, navigate to the folder containing the adventuretutorial package in your console and run python adventuretutorial/game.py. If you get warnings about packages, try setting your PYTHONPATH environment variable manually.

  2. Pingback: Capstone Day 5 | KWNull

  3. Iain Malone

    Hello,
    I have been working through your tutorial and completed it. It has been very useful in improving my knowledge of python programing. However I am unable to run the program. I am running the latest version of python 3 and I am on a mac. The terminal error says this:

    Traceback (most recent call last):
    File “/Users/macuser/Documents/Scripting/Python/adventuretutorial/game.py”, line 2, in
    from player import Player
    File “/Users/macuser/Documents/Scripting/Python/adventuretutorial/player.py”, line 4, in
    class Player:
    File “/Users/macuser/Documents/Scripting/Python/adventuretutorial/player.py”, line 5, in Player
    inventory = [items.Gold(15), items.Rock()]
    File “/Users/macuser/Documents/Scripting/Python/adventuretutorial/items.py”, line 14, in __init__
    super().__init__(name=”Gold”,
    TypeError: super() takes at least 1 argument (0 given)

    I believe the error is something to do with the super() function in the items module. However my code is identical to yours. I do not see I can fix this.

    If you have any idea I would appreciate some help.
    Thank you for your time.

    Regards, Iain

    1. Phillip

      I have a strong feeling you are trying to run this using Python 2. The super() method was introduced in Python 3. Depending on how you have Python installed, running python at the command line could open a 2.x.x version and running python3 could open a 3.x.x version. It really depends on how you have things configured.

      1. SoggyNotions

        I am having the exact same problem as lain and am running the code in the latest version of python 3. What do you think the problem could be?

        1. Phillip

          I can confirm this error exists in Python 2.x, but not in 3.x. When you go to a command line and type “python” you will enter an interactive python session and the first line will give you the version number. I have a feeling your PATH is not configured to use the newer version.

          1. SoggyNotions

            I had been running the code with python 3 and was eventually able to fix the errors I was getting. Thank you for your help!

      2. Sean Z

        `super` has been part of python since 2.2. The python3 version of `super` doesn’t take any arguments, which is why Iain got the original error: he was using it according to python3 convention and not python2.

        There is a great example of future-proofing one’s use of `super` on this page:

        http://python-future.org/overview.html

  4. Jermiah Schneider

    Hi, I was wondering, when I play this game it kind of just piles up the text, is there end way to clear the text every time I go to a different tile?

  5. cgt5501

    i cant get this game to work either. i get the same ‘no module named adventuretutorial’. how can i fix this problem? I’ve troubleshooted for hours lol

  6. SoggyNotions

    I am using enthought canopy for coding in python. Its fine and all, but doesn’t support python 3. What can I use to run this game, and how?

    1. Phillip

      Vanilla Python? I’m not sure I understand the question. Download and install Python 3.x and you should be good to go.

  7. A Developer

    Well, thank you very much for this tutorial. I have encountered a couple of errors, and I have re-checked the code and the listed code on the posts many times. Here are the errors:

    Error 1:
    Traceback most recent call:
    File Game.py, Line 27 in
    play()
    File Game.py, Line 9, in play
    room = World.tile_exists(Hero.loaction_x, Hero.location_y
    Error:(I don’t remember too well) Something like ” no attribute tile_exists for World ”

    Error 2:
    Traceback most recent call:
    File Game.py, Line 27 in
    play()
    File Game.py, Line 10, in play
    Hero = Hero()
    InboundLocalError: local variable ‘Hero’ (the last part is blurry in my head) something like used before exists or something.

    Error 2 I can easily fix by calling it ‘player’ instead of Hero, but Error 1 is really creating a wall for me here. I will put another post showing the exact wording.

    1. Phillip

      If the error was ('module' object has no attribute '') then I would make sure you review your map and the assumptions I make in the code about that map.

      1. A Developer

        TY I fixed both, but found another error:
        Traceback (most recent call last):
        File “Game.py”, line 24, in
        play()
        File “Game.py”, line 6, in play
        player = Hero()
        TypeError: ‘module’ object is not callable

        Here is the code:

        import World, Hero

        def play():
        	World.load_tiles()
        	player = Hero()
        	room = world.tile_exists(player.location_x, player.location_y)
        	print(room.intro_text())
        	while player.is_alive() and not player.victory:
        		room = world.tile_exists(player.location_x, player.location_y)
        		room.modify_player(player)
        		if player.is_alive() and not player.victory:
        			print("Choose an action:\n")
        			available_actions = room.available_actions()
        			for action in available_actions:
        				print(action)
        				action_input = input('Action: ')
        				for action in available_actions:
        					if action_input == action.hotkey:
        						player.do_action(action, **action.kwargs)
        						break
        
        if __name__ == "__main__":
        	play()
        
        1. Phillip

          Well, did you also name your Player class Hero? If the Hero class does not exist, then the code will fail when it tries to create a new Hero object.

  8. Rick

    I like the way you have put all this together. While I like the object oriented aspect of things, I’m still trying to go back to basics… like how would I store the action verbs and a list of objects the way old text adventures were written in BASIC. And I was thinking the exact same thing. Learning Python so I’ll try writing a text adventure!

    My second choice would be a D&D Character Generator and then a simple game out of it ๐Ÿ™‚

  9. Jason Kelley

    I love your tutorial. Could you possibly explain how I could add a specific enemy tile that wasn’t random? I am new to python. I have been able to modify a few things but I can’t seem to add a specific enemy. I can however change the “r” values to produce a specific enemy but I don’t want the same enemy every time. Can you help? Thank you.

    1. Phillip Johnson Post author

      Thanks, I’m glad you’re enjoying it! I would create a new class called GiantSpiderTile (or whatever your enemy is). Then in the init() function, just set self.enemy = enemies.GiantSpider() explicitly. Not sure how far you are in the book, but don’t forget to add your new tile type to tile_type_dict.

      1. Jason Kelley

        Thank you for the reply. I will try that out tonight! I’ve been trying to learn Python on my own for a few months now. I have bought a few books but yours is by far the most interesting to me. BTW, have you ever thought about making a supplemental to your book that covers modifications? I’d pay for that. Thanks.

        1. Phillip Johnson Post author

          My hope is that by the end of the book, you have all the tools you need to complete some of the modifications I suggest in the last chapter. Although I know it can be intimidating going from something guided to unguided. If you need a little more nudging, feel free to email me.

  10. David Grover

    on p67 in the code at the top
    line 35 seems incomplete.

    Should it read?:
    print(“Enemy does {} damage. You have {}HP remaining.”.format(self.enemy.damage,player.hp))

  11. othnin

    Hi, I have a question. I’m on page 63 on the Enemies chapter. The book says the code is runnable but I’m getting some errors when I move to an EnemyTile. In game.py there is the line:
    print(room.intro_text())
    This fails with the NotImplementedError that was created for the MapTile class. However, since the EnemyTile is declared as such:
    class EnemyTile(MapTile) it will try to call MapTile.intro_text().
    I assume I need to call the intro_text() in enemies but I don’t see this in your book at this point.

    1. Phillip Johnson Post author

      Take a look at page 62 where the text starts, “In order to alert the player about the enemy…”. Right below that, you should see a code snippet that shows the intro_text method. Hope that helps!

      1. chris

        I have added the intro_text() into the enemy class but it never gets called. This is what it looks like right now:
        game.py
        room = world.tile_at(player.x, player.y)
        This returns EnemyTile found in the world_map. According to the code for EnemyTile it doesn’t implement it’s own intro_text and since it is a subclass of MapTile it will use the intro_text() there and get the NotImplementedError.

        1. Phillip Johnson Post author

          Ah, that’s why, you should be adding the intro_text() method to the EnemyTile class, not the Enemy class. I can see how that might be unclear, but I switched over to the EnemyTile class at the bottom of page 61. We are referencing the Enemy class simply to get access to the name of the Enemy, but the intro text takes place in the tile.

  12. Edward Adams

    Hi, I have bought your book and have tried to run all the code but I am getting an Error:

    I have tried comparing my code to the code in the book but they appear to be the same.

    Traceback (most recent call last):
      File "C:\Users\pipan\Documents\Eddie\NAGA Quest\game.py", line 58, in 
        play()
      File "C:\Users\pipan\Documents\Eddie\NAGA Quest\game.py", line 8, in play
        world.parse_world_dsl()
      File "C:\Users\pipan\Documents\Eddie\NAGA Quest\world.py", line 275, in parse_world_dsl
        tile_type = tile_type_dict[dsl_cell]
    KeyError: '        '
    >>> 
    

    I have never got this error before and am finding it hard to realise what is wrong

    1. Phillip Johnson Post author

      That error means Python is trying to look up ' ' in your tile_type_dict but it is not finding a match. I would take a look at your world_dsl variable because that is where the map of tiles is defined. My guess is you have some extra spaces where you don’t want them. Try turning on your editor’s “show all characters” mode. That may help you track down the problem.

  13. Benjamin Calloway

    Hi, I started your tutorial and I am new to Python so this will be a great way for me to learn new things. I downloaded the final project on Github to make sure it works on my version of Python, and I get an error when trying to run the game.py in the Python 3.5 IDLE. This is the error:

    Traceback (most recent call last):
    File “C:\Users\Ben\Documents\Python Programs\text-adventure-tut-master\adventuretutorial\game.py”, line 31, in
    play()
    File “C:\Users\Ben\Documents\Python Programs\text-adventure-tut-master\adventuretutorial\game.py”, line 10, in play
    world.load_tiles()
    File “C:\Users\Ben\Documents\Python Programs\text-adventure-tut-master\adventuretutorial\world.py”, line 18, in load_tiles
    with open(‘resources/map.txt’, ‘r’) as f:
    FileNotFoundError: [Errno 2] No such file or directory: ‘resources/map.txt’

    1. Phillip Johnson Post author

      Take a look at Part 4 where I describe how to run the game. Also, I’d suggest learning how to run it from the command line first before introducing the complexity of an IDE. Happy coding!

      1. Benjamin Calloway

        Thanks, but I don’t understand how to change the path. I don’t understand the link in Part 4 since I work on Windows, not Linux. I understand how to change the path in the environment variables, but I added my game folder as a path there and nothing has changed (it still can’t find the directory).

        1. Phillip Johnson

          OK, try this:

          1. Open a command prompt
          2. Go to the directory where the adventuretutorial package exists. Probably something like cd C:\Users\Ben\Code\PythonGame
          3. Type python adventuretutorial/game.py and press enter.

          What happens?

  14. Justin Hangingtree

    Hello, and thank you very much for the tutorial! I got it all to work and it works great, except for some reason, when I’m in an enemy room and I kill the enemy inside it, instead of giving me actions to choose from to exit the room, it raises the error “TypeError: ‘method’ object is not iterable”. Any idea about why that may be? Thanks again!

    -Justin Hangingtree

    1. Phillip Johnson Post author

      This is a guess, but do you have available_actions = room.available_actions instead of available_actions = room.available_actions() in your code? The error is telling you that you are trying to loop through a method object. You actually want to loop through what the method returns, not the method itself. To call the method, you have to include the parentheses. Hope that helps!

  15. Chelsea H

    Hello! I’m creating a text game using your book and I’ve run into a problem in the world.py chapter. Whenever I run the game I get the error: ‘AttributeError: ‘NoneType; object has no attribute ‘intro_text’. I’ve gone over and over my code and I can’t seem to find where I’ve gone off course.

    I’m having a lot of fun with this, and I’m hoping to get the game to functioning soon so I can use it as a means of proposing to my boyfriend. Any help would be appreciated!

    1. Phillip Johnson Post author

      Take a look at the comments in Part 3 where this has been discussed a bit. It means Python can’t find the tile at the location you gave it, so it returns None. Because None is not the starting room, you get an error that intro_text is not part of None. You can usually fix this my modifying your map file or change the starting location of the player. Good luck and I hope he says “Yes”!

  16. Tony Youngblood

    I’m up to page 60 in the book version of your excellent tutorial. I’m finding this WAY easier to understand than Learn Python the Hard Way.

    If I run the code as it stands on page 60, I get the following error when I reach a None tile. Is this error expected at this point? Or did I type something wrong? (Btw, I already took a look in the comments at Page 3 and Page 4 of the online tutorial and did not find the answer to my question.)

    PS C:\PythonGame> python .\game.py
    Escape from Cave Terror!

    You find yourself in a cave with a flickering torch on the wall.
    You can make out four paths, each equally dark and foreboding.

    Action: e

    This is a very boring part of the cave.

    Action: e
    Traceback (most recent call last):
    File “.\game.py”, line 30, in
    play()
    File “.\game.py”, line 10, in play
    print(room.intro_text())
    AttributeError: ‘NoneType’ object has no attribute ‘intro_text’

    1. Phillip Johnson Post author

      I’m glad you’re enjoying the book! It doesn’t look like you’ve done anything wrong, that’s actually one of the bugs that gets fixed in the next chapter. See the “Limiting Actions” section of Chapter 13.

      1. Tony Youngblood

        Thanks, Phillip!

        It might be worth revising the following line in the next edition from…

        “Notably, the game doesnโ€™t end when you reach the VictoryTile and
        the player can also wrap around the map.”

        to something like

        “Notably, the game doesnโ€™t end when you reach the VictoryTile and
        entering a None tile triggers an error.”

  17. Tony Youngblood

    I also found a potential typo in the book version. The x/y coordinates appear to be flipped in the second example:

    Page 62:
    world_map = [
    [None,VictoryTile(1,0),None],
    [None,BoringTile(1,1),None],
    [BoringTile(0,2),StartTile(1,2),BoringTile(2,2)],
    [None,BoringTile(1,3),None]
    ]

    Page 68:
    world_map = [
    [None,VictoryTile(0,1),None],
    [None,EnemyTile(1,1),None],
    [EnemyTile(2,0),StartTile(2,1),EnemyTile(2,2)],
    [None,EnemyTile(3,1),None]
    ]

  18. Tony Youngblood

    Thank you, Phillip, for a wonderful resource. I completed the tutorial and am on my way to customizing the game.

    One feature I would like to add is the ability to save progress and restore later. (Serialization.) Research suggests that using Pickle to save certain player/world variables is the way to go, however, I am unable to figure out exactly what to do. Is it very tricky to do?

    Thank you.

    Best,
    Tony

    1. Phillip Johnson Post author

      It’s probably not too hard. I’d make a save data class where you write all the relevant variables to and then use the examples here as a guide. You might need to do some restructuring to make it easier to inject variables during unpickling.

  19. Tony Youngblood

    Another feature I’m struggling with:

    I’d like to add a locked room and another room featuring a key to unlock the locked room. I think I’ve got the key room figured out, but I’m having trouble figuring out how to make a locked room with the way the following is structured:

    def get_available_actions(room, player):
    actions = OrderedDict()
    print(“Choose an action: “)
    if player.inventory:
    action_adder(actions, ‘i’, player.print_inventory, “Print inventory”)
    if isinstance(room, world.TraderTile):
    action_adder(actions, ‘t’, player.trade, “Trade”)
    if isinstance(room, world.EnemyTile) and room.enemy.is_alive():
    action_adder(actions, ‘a’, player.attack, “Attack”)
    else:
    if world.tile_at(room.x, room.y – 1):
    action_adder(actions, ‘n’, player.move_north, “Go north”)
    if world.tile_at(room.x, room.y + 1):
    action_adder(actions, ‘s’, player.move_south, “Go south”)
    if world.tile_at(room.x + 1, room.y):
    action_adder(actions, ‘e’, player.move_east, “Go east”)
    if world.tile_at(room.x – 1, room.y):
    action_adder(actions, ‘w’, player.move_west, “Go west”)
    if player.hp < 100:
    action_adder(actions, 'h', player.heal, "Heal")

    return actions

    The code looks to see if a room exists next to the current room. Technically, the locked room exists, but I don't want to let the player enter it until they find the key.

    Another limitation of the above is adjacent rooms that I want to put a wall between. (Instead of say, travelling east into the next room, the player would see a wall and be forced to travel north, then east, then south.) I could put a "None" room in between the two rooms, but this would produce a undesired column.

    Any way to do the above things without having to drastically restructure the way the DSL generates?

    Thank you for your time and expertise.

    Best,
    Tony

    1. Phillip Johnson Post author

      So instead of just using if world.tile_at(room.x โ€“ 1, room.y), you will probably want to create a new method to handle all of the logic for figuring out what the player can do. To accommodate locks, I would check 1) if the room exists and 2) if the room requires a key and 3) if the player has the correct key in the inventory. Then you only add the action if the result of that new method is true.

      For walls, you could create a wall tile for the DSL. Then you would just need to add another check to the new method to make sure the tile is not a wall. The type() function may help.

  20. Tony Youngblood

    Thanks, Phillip! I’ll give all this a try.

    Re: Wall tile, that would produce an extra column. I think instead, I’ll make certain World tiles that have variables that place walls. (Maybe something like wall_east, wall_west, etc.) And then in the game logic, I’ll check to see if the wall is there and prohibit travelling in that direction.)

  21. Adrianna Foster

    Hi,

    I have gone through your tutorials online and in the book and I am having the same error (though I can get your github code to work so I know I’ve made a typo somewhere and I can’t figure it out). I have tried going through and comparing your code to mine and I still can’t find any differences besides customizable things. Do you have any idea what might be going on here?

      File "/Users/adriannafoster/Dropbox/adventure/game.py", line 24, in play
        available_actions = room.available_actions()
      File "/Users/adriannafoster/Dropbox/adventure/tiles.py", line 46, in available_actions
        moves.append(actions.ViewInventory())
    TypeError: __init__() missing 3 required positional arguments: 'method', 'name', and 'hotkey'
    

    Below is my code for the available_actions function in my tiles.py

        def available_actions(self):
            """Returns available actions in the room."""
            moves = self.adjacent_moves()
            moves.append(actions.ViewInventory( ))
            return moves
    

    And here is my code for the ViewInventory action:

    class ViewInventory(Action):
        """Prints the player's inventory"""
        def ___init__(self):
            super().__init__(method = Player.print_inventory,"View inventory",'i')
    

    It seems to be upset I haven’t given ViewInventory any arguments in the available_actions function but your code doesn’t do that either, so I’m not sure what I’ve done wrong. Any direction would be much appreciated!

    Thank you!

    ~Adrianna

    1. Phillip Johnson Post author

      The only difference I notice is that I name all of my parameters in the __init__ method. This is kind of tricky, but if you keep going back, you see that the base Action class uses **kwargs. Because of that, I believe you need to name all of your parameters, otherwise Python does not know which belong in **kwargs and which do not.

      class ViewInventory(Action):
          """Prints the player's inventory"""
          def __init__(self):
              super().__init__(method=Player.print_inventory, name='View inventory', hotkey='i')
      
  22. samuel

    I am really enjoying your book. But I am having a serious problem. My code is exactly as yours. I keep getting a “Player object has no attribute ‘inventory.’ error. This is when I try to run the game importing the player module and the items modules (all this before the World Building chapter). Says my problem is in line 13 (print_inventory) of the player.py. Cut and pasting my code from the modules here:

    game.py
    from player import Player
    def play():
        print('Escape from Planet Terror!')
        player = Player()
        while True:
            action_input = get_player_command()
            if action_input in ['n', 'N']:
                print("Go North!")
            elif action_input in ['s', 'S']:
                print("Go South!")
            elif action_input in ['e', 'E']:
                print('Go East!')
            elif action_input in ['w', 'W']:
                print('Go West!')
            elif action_input in ['i', 'I']:
                player.print_inventory()
            else:
                print('Invalid Action!')
    
    
    def get_player_command():
        return input('Action: ')
    
    
    play()
    

    (and next module is where I think the problem is, on line 13, but I am pretty sure there are no differences in my code and what is in the book. Any advice you could give me is welcome.)

    player.py
    
    import items
    
    
    class Player:
        def _init_(self):
            self.inventory = [items.Rock(),
                              items.Dagger(),
                              'Gold(5)'
                              'Crusty Bread']
    
        def print_inventory(self):
            print("Inventory:")
            for item in self.inventory:
                print(' * ' + str(item))
            best_weapon = self.most_powerful_weapon()
            print('Your best weapon is your {}'.format(best_weapon))
    
        def most_powerful_weapon(self):
            max_damage = 0
            best_weapon = None
            for item in self.inventory:
                try:
                    if item.damage > max_damage:
                        best_weapon = item
                        max_damage = item.damage
                except AttributeError:
                    pass
    
            return best_weapon
    
  23. samuel

    Just a follow up, I see when I posted that it took out my spacing. My spaces were in there, and match the book. I’m really confused as to what line 13 in the player.py is doing to generate the error. Again, there error is
    “Player object has no attribute ‘inventory.’

    This happens when I try to access the inventory when running the game.

    thanks for any assistance.

  24. Edward Adams

    Hi, I have recently bought your great programming book but want to add some more features. I have encountered a problem when trying to make a “Warp Tile” which transports the player to another position on the map. Here is my code:

    import random
    import enemies
    from player import Player
    
    class MapTile:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def intro_text(self):
            raise NotImplementedError("Create a subclass!")
    
        def modify_player(self, player):
            pass
    
    class Warp1Tile(MapTile):
        def intro_text(self):
            return """
            **WARP SUCCESSFUL**
            """
            player.x = 3
            player.y = 15
    
    1. Phillip Johnson Post author

      Hi Edward,

      Neat idea! The reason your player won’t be moved is because you have a return statement before you make changes to the player. The return keyword exits the method. If you want to make changes to the player, you should use the modify_player() method. Take a look at the final EnemyTile at the end of the Enemies section in Chapter 12 for an example. Happy coding!

  25. DJ

    Hello!

    I have gone through the book and everything is working except for one thing. The blank areas in the DSL (none) are not rooms the player can go through. So when I have it setup like this:

    world_dsl = """
    |EN|EN|VT|EN|EN|
    |EN|  |  |  |EN|
    |EN|FG|EN|  |TT|
    |TT|  |ST|FG|EN|
    |FG|  |EN|  |FG|
    """
    

    The player does not have the option of going west at the start. I am not sure where my error is but do you have any ideas?

    Thanks!

    1. Phillip Johnson Post author

      Are you saying that the player should not be able to walk West, but the player actually can? If so, check your code for get_available_actions. If you’re getting an actual error from Python, that might include some helpful details.

  26. Lynecia

    Hi! I’ve been following your book and have run into an error somewhere in Chapter 13 (I haven’t been able to test the code until the end since it’s all related.)
    It seems the map hasn’t generated properly as I am receiving this error immediately upon running the program:
    “AttributeError: ‘NoneType’ object has no attribute ‘intro_text'”,
    I know that the player location is correct as I can get the program to print it before it terminates, (1,2)
    Here’s my map generation code: (Maybe it’s something to do with the order of functions?)

    def tile_at(x, y):
        if x < 0 or y < 0:
            return None
        try:
            return world_map[y][x]
        except IndexError:
            return None
    
    world_dsl = """ #when i put 2 spaces in the empty tiles the pipes align, but not here
    |   |VT|   |
    |   |EN|   |
    |EN|ST|EN|
    |   |EN|   |
    """
    
    
    def is_dsl_valid(dsl):
        if dsl.count("|ST|") != 1:
            return False
        if dsl.count("|VT|") == 0:
            return False
        lines = dsl.splitlines()
        lines = [l for l in lines if l]
        pipe_counts = [line.count("|") for line in lines]
        for count in pipe_counts:
            if count != pipe_counts[0]:
                return False
    
        return True
    
    tile_type_dict = {"VT": VictoryTile,
                    "EN": EnemyTile,
                    "ST": StartTile,
                    "  ": None}
    
    world_map = []
    
    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)
    

    Thanks!

    1. Phillip Johnson Post author

      That error means that your tile_at is returning None instead of an actual room. You mentioned that the player’s location is (1,2). Is that x=1, y=2 (correct) or x=2, y=1 (incorrect)? This might help:

      def tile_at(x, y):
          if x < 0 or y < 0:
              return None
          try:
              return world_map[y][x]
          except IndexError:
              print("Tile at y={}, x={} does not exist.".format(str(y), str(x)))
              return None
      
      
      							
      1. Lynecia

        Hmmm, it’s x = 1, y = 2…
        “Tile at y=2, x=1 does not exist.”
        Did the map not generate properly?
        Sorry, my knowledge of python is pretty basic soo ๐Ÿ˜ฎ

        1. Lynecia

          So apparently it works when i include “world.parse_world_dsl()” at the beginning of game.py. I suppose I didn’t call the function anywhere and that’s why the map wasn’t built. Would you mind referring me to where you called the function? I must’ve missed something.

  27. Lynecia

    Hi again XD Just have a couple questions on expanding the game. I’ve finished the main content now, thanks for the awesome book! ๐Ÿ™‚
    Just wondering if there was a way to make an X, Y, Z coordinate system? I was planning on having a castle which has multiple floors. I suppose when the player goes up / down I could teleport them to another part of the grid and just say it’s down, would that be an efficient way of doing it? The map would be pretty big. :/
    Also, is there a way to seperate rooms without a “None” tile? Like lets say these 2 rooms along a corridor are adjacent, but you cant enter one room from the other, you gotta come out and enter through the corridor. (Without adding a none space inbetween them and hence another tile to the corridor)
    Sorry for all the questions! Thank you!!

    1. Phillip Johnson Post author

      It’s definitely possible to do a 3D coordinate system. The simplest way to do this is to have a list of lists of lists. That is to say: floors, rows, and columns. This is also called a 3D-array since there are three dimensions. I would then build each floor as its own DSL string and include a method to patch them together.

      Sometimes it is hard keeping track of all three dimensions in your head, especially if you end up with code like floors[z][y - 1][x + position] so you might decide to make a “Floor” class to make things a bit easier to work with.

      There’s two ways you could create a “wall”. You could make it its own room type that gets included in the map, but then tile_at method would ignore any “Wall” tiles. If you want to do something more complex, you could try introducing a new character to the DSL like ! that means “impenetrable”. So you would end up with |EN!EN|FD|. But if you did that, you would need to determine the adjacent tiles during parsing and keep that information in the MapTile class. Then the tile_at method would also need to be smarter because it would have to refer back to that information you stored during parsing.

      Hope that helps, happy programming!

      1. Lynecia

        Hi again! I’ve been tryna implement those things for the last couple days, but as I am a bit of a newbie to python, im not making much progress ๐Ÿ™
        Wouldn’t making a “wall” tile essentially be a NoneTile anyway?
        like |EN|WA|EN| = |EN| |EN|?
        Kinda want something like
        |EN|EN|
        |ST!TT|
        meaning, to get to the trader from the Start tile, you’ll have to fight 2 enemies. I’m not too sure I understand your second proposed solution. How would you store the adjacent tiles and improve the tile_at function? ๐Ÿ˜ฎ Sorry im hopeless ahah

  28. Joe

    Hey, amazing book. These are my first steps in Python and programming and I am 3/4 through your book and everything is going great so far. Really enjoying it.
    Once the game is complete, can it be made to play outside a python terminal? For example, is there a way I can get all my finished code to run in Frotz or something similar with all my other adventure games? I’d like to learn how to port it to other platforms so I can share my work with my friends, perhaps if they could play the game on Android phones?
    I’m sorry if this is a dumb noob question… but that’s what I am so far :/

  29. Kevin Parks

    Hey, im loving your book, i am up to the point where we begin to expand the world.
    i am getting a Error:

     RESTART: C:\Users\Kevin\game.py 
    Escape from Cave Terror!
    Traceback (most recent call last):
      File "C:\Users\Kevin\game.py", line 63, in 
        play()
      File "C:\Users\Kevin\game.py", line 7, in play
        player = Player()
      File "C:\Users\Kevin\player.py", line 13, in __init__
        self. x = world.start_tile_location[0]
    TypeError: 'NoneType' object is not subscriptable
    

    below is a a copy of my Code:

    def tile_at(x, y):
        if x < 0 or y < 0:
            return None
        try:
            return world_map[y][x]
        except IndexError:
            return None
    
    world_dsl = """
    |EN|EN|VT|EN|EN|
    |EN| | | |EN|
    |EN|FG|EN| |TT|
    |TT| |ST|FG|EN|
    |FG| |EN| |FG|
    """
    
    def is_dsl_valid(dsl):
        if dsl.count("|ST|") != 1:
            return False
        if dsl.count("|VT|") == 0:
            return False
        lines = dsl.splitlines()
        lines = [l for l in lines if l]
        pipe_counts = [line.count("|") for line in lines]
        for count in pipe_counts[0]:
            if count != pipe_counts[0]:
                return False
    
        return True
    
    tile_type_dict = {"VT" : VictoryTile,
                      "EN" : EnemyTile,
                      "ST" : StartTile,
                      "FG" : FindGoldTile,
                      "TT" : TraderTile,
                      "  " : None}
    
    start_tile_location = None
    
    world_map = []  
    
    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]
                if tile_type == StartTile:
                    global start_tile_location
                    start_tile_location = x, y
                row.append(tile_type(x, y) if tile_type else None)
    
            world_map.append(row)
    

    thanks in advance

    1. Phillip Johnson Post author

      If you take a look at the error message, it is telling you there is a problem with line 13 of player.py. It then includes the line with the error self. x = world.start_tile_location[0]. I am not sure if this is a copy/paste error, but the code error you pasted has a space between self. and x which might be the problem. Hope that helps!

  30. Justin Hardin

    Hello, I recently decided to try and start learning Python. I really liked this article, and I went through it, and the code seemed right; however, it wouldn’t start. So I decided to download it and see if yours worked on my computer, and found that it didn’t; instead, it gives these errors:

    C:\Users\jhard\AppData\Local\Programs\Python\Python36-32\python.exe C:/Users/jhard/Desktop/text-adventure-tut-master/adventuretutorial/game.py
    Traceback (most recent call last):
      File "C:/Users/jhard/Desktop/text-adventure-tut-master/adventuretutorial/game.py", line 31, in 
        play()
      File "C:/Users/jhard/Desktop/text-adventure-tut-master/adventuretutorial/game.py", line 10, in play
        world.load_tiles()
      File "C:\Users\jhard\Desktop\text-adventure-tut-master\adventuretutorial\world.py", line 18, in load_tiles
        with open('resources/map.txt', 'r') as f:
    FileNotFoundError: [Errno 2] No such file or directory: 'resources/map.txt'
    
    Process finished with exit code 1
    

    Any ideas?

    1. Phillip Johnson Post author

      Are you following this advice?

      To run the program, navigate to the folder containing the adventuretutorial package in your console and run python adventuretutorial/game.py. If you get warnings about packages, try setting your PYTHONPATH environment variable manually.

      If so, are you using an IDE?

    2. Justin Hardin

      I found that in moving rescources within the tutorial directory, it made it work, though I’m still confused as to why the code I made was unable to work. Here’s the errors on that:

      Traceback (most recent call last):
      File “C:/Users/jhard/PycharmProjects/ShadowsOfTlanen/game.py”, line 28, in
      play()
      File “C:/Users/jhard/PycharmProjects/ShadowsOfTlanen/game.py”, line 10, in play
      print(room.intro_text())
      AttributeError: ‘NoneType’ object has no attribute ‘intro_text’

      seems to be a problem with ‘intro_text’, yet I’m not sure which.

        1. Justin Hardin

          So now the intro_text glitch is gone, but now it is having different trouble:
          File “C:\Users\jhard\PycharmProjects\ShadowsOfTlanen\world.py”, line 18, in load_tiles
          _world[(x, y)] = None if tile_name == ” else getattr(__import__(’tiles’), tile_name)(x, y)
          AttributeError: module ’tiles’ has no attribute ‘EmptyPassage

          Anything I’m missing here? I’ve been looking through the comments and trying solutions. (BTW, thanks for the replies)

          1. Justin Hardin

            Here’s the EmptyPassage code:

            class EmptyPassage(MapTile):
            def intro_text(self):
            return “””
            The empty corridor is deathly silent, and you feel the desire to hasten through as quickly as possible.
            “””

  31. Greg

    Fantastic tutorial! Very clear, thoughtful instructions.

    I have one question though:

    So I’ve spent several hours fleshing out the map and adding monsters and items to create an exciting text adventure, but now let’s say I want to distribute the programme to friends using a stand-alone executable. Using this tutorial as an example, how would I compile game.py with all of its dependencies, including map.txt?

    I’ve been reading all over StackOverflow and several other sites looking for answers, and tinkering with PyInstaller every which way, but to no avail.

    1. Phillip Johnson Post author

      PyInstaller will probably work fine. The easiest option might be to distribute both the map file and the exe and either assume they are in the same directory or ask the user where the map is. But if you do want to bundle the map inside the exe, this looks like the way to do it. However, I have never used PyInstaller so I can’t verify that.

  32. Ian

    Hi Phillip,

    First; Thanks for the great book. I had a lot of fun learning python this way and you did a great job explaining the game as you went.

    Second; I am trying to add an “Escape” action as an acceptable action during a fight. I want it to force the user to move to the previous tile that they occupied, as opposed to picking a direction, which might allow them to simply bypass enemies. Any recommendations on how to achieve this?
    Thanks!

    1. Phillip Johnson Post author

      You’d need to keep track of the last room the player is in. Something like self.last_room = (3, 4). When the player moves, you would update that value. Then you would add an escape method that would set the player’s self.x and self.y to the values in self.last_room. Happy programming!

Leave a Reply

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