Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

Python

Dungeon Game

Hi Kenneth Love,

I've just completed the Python Collections course (and excited about the OOP course next!) and thought I would post my version of the dungeon game.

I made this after watching the video with the description, but before watching your solution, so I have done some things in different ways. I've also added the following features:

  • Choose room height and width
  • Breadcrumbs to show where the user is
  • Unicode characters for the player, getting to the door, and getting eaten
  • Use left, right, up down, or WASD keys to navigate
  • Random messages for when you're still moving but not eaten or escaped

I plan to add in future:

  • Difficulty levels so the monster will move too
  • Stages within each difficulty of the map getting bigger, each with a password so you can return to the last level you played
  • Maybe some other things.
  • Better commenting

I have learned basic Python because I teach computer science in high school here in New Zealand, but my background is in web - JS and PHP etc. So the logic is not totally new, but it's taking time to learn how things are possible in Python that I already know how to do in PHP etc, and I'm enjoying learning something different!

Any feedback appreciated

Thanks, Hannah

# Hannah's DungeonGame v1.0
# Copyright (c) 2014 Hannah Taylor

import random

rooms = []
MESSAGES = ['So far so good, still alive, but no door here',
            'Hmm, nothing here, keep moving.',
            'WAIT! What was that sound?  Oh, nothing, not a monster and no door in sight',
            'Warmer... Or maybe colder.',
            'Hurry hurry!',
            "Are you sure you know where you're going?"]

def create_map(width, height):
    global rooms

    for y in range(height):
        for x in range(width):
            rooms.append((x, y))

def create_room():
    global player, door, monster, breadcrumbs

    #Put the player in a spot
    player = random.choice(rooms)
    breadcrumbs.append(player)

    #Put the door in a different spot to player
    valid = False
    while valid == False:
        door = random.choice(rooms)
        if door != player:
            valid = True

    #Put the monster in a different spot to player and door
    valid = False
    while valid == False:
        monster = random.choice(rooms)
        if monster != player and monster != door:
            valid = True
    #Uncomment to debug
    #print('player: {}, door: {}, monster: {}'.format(player, door, monster))

def draw_map(mode):
    global player, breadcrumbs, rooms, width, height

    for y in range(height):
        print("","------" * width + "-")
        for x in range(width):
            print(" | ",end="")
            curr_position = breadcrumbs[len(breadcrumbs)-1]
            if (x, y) == curr_position:
                if mode == "door":
                    print(" \u26BF ",end="")
                elif mode == "monster":
                    print(" \u2620 ",end="")
                else:
                    print(" \u263A ",end="")
            elif (x, y) in breadcrumbs:
                print(" * ",end="")
            else:
                print("   ",end="")
        print(" |")
    print("","------" * width + "-")
    #print(rooms)

def move(direction):
    global player, breadcrumbs, width, height

    if direction in ['left','a']:
        if player[0] > 0:
            coords = rooms.index(player)
            new_coords = rooms[coords-1] 
        else:
            return None
    elif direction in ['right','d']:
        if player[0] < width-1:
            coords = rooms.index(player)
            new_coords = rooms[coords+1]
        else:
            return None

    elif direction in ['up','w']:
        if player[1] > 0:
            coords = rooms.index(player)
            new_coords = rooms[coords-width]
        else:
            return None

    elif direction in ['s','down']:
        if player[1] < height-1:
            coords = rooms.index(player)
            new_coords = rooms[coords+width]
        else:
            return None

    breadcrumbs.append(new_coords)
    return new_coords


def check_position():
    global player, door, monster

    if player == door:
        print("\n\nYou found the door! you win!")
        moving = "door"
    elif player == monster:
        print("\n\nYou got eaten by the monster, you lose!")
        moving = "monster"
    else:
        print("\n\n"+random.choice(MESSAGES)) #make a list of messages and use random.choice()
        moving = True

    return moving

def play_again(repeat):
    playagain = input("Would you like to play again? Y/N")
    if playagain in ["n","no"]:
        repeat = False

    return repeat

# Game Loop
repeat = True
while repeat == True:
    #Welcome the user
    print("Welcome to the dungeon!")
    print("Choose your challenge by setting the room size: \n\n")

    #Set room size
    width, height = "", ""
    while not width.isnumeric():
        width = input("Enter room width: ")

    while not height.isnumeric():
        height = input("Enter room height: ")

    width, height = int(width), int(height)

    #Initialise variables
    breadcrumbs = []

    player = ""
    door = ""
    monster = ""

    #Set up game
    create_map(width, height)
    create_room()

    #Instructions
    print("\n\nThere is a monster lurking in the depths of this dungeon, and only one way to escape")
    print("Find the door before you are eaten by the monster!")
    print("\n\nThis handy map will show you where you have been")

    draw_map(True)

    print("\nWhenever you see > you can choose to move left, right up or down with those words or the WASD keys, followed by enter")

    moving = True
    while moving == True:
        direction = input("> ")
        if direction in ['left','right','up','down','w','a','s','d']:
            position = move(direction)
            if position == None:
                print("\nAh, there are no more rooms that way, you're at the edge of the map!")
            else:
                player = position
                moving = check_position()
                draw_map(moving)

                #Uncomment to debug
                #print('DEBUG: player: {}, door: {}, monster: {}'.format(player, door, monster))
        else:
            print("Please choose left right up down or w a s d")

    repeat = play_again(repeat)

print("Thanks for playing!")

Amazing! Great work :)

4 Answers

Kenneth Love
STAFF
Kenneth Love
Treehouse Guest Teacher

Wow, thanks for the submission! I'll play it later and I'm sure I'll enjoy it.

One thing, try to avoid using global. Most of the time you don't need it in Python and if you find yourself needing it, you should probably just refactor your code so it's not needed. Scope bugs are horrible to try and figure out, so avoid putting yourself in that position if possible!

Ah right, thanks for the feedback. That creates a couple more questions for me though, if you have a chance to answer.

Does that mean it's better to just pass parameters/arguments in to functions that need them? Like my rooms list in the create_map() function, is it better to pass in width, height, rooms instead, then return rooms, and run that as:

rooms = create_map(width, height, rooms)

Like I have done with the repeat variable and the play_again() function?

I have only recently learned about how global and local work in python because I taught a new course this year that involves students needing to use well-chosen scopes for the variables in their program, and when they plan the program they have to list the variables they'll use, the datatype, the scope, and describe what it does.

I read somewhere that passing an argument into a function technically creates a local copy of that variable, and for top marks the students variable lists have to be really accurate (I could never make a full list of the variables I'd use before I've even written the code, but they have to try) so I thought if they just accessed the global ones then they at least don't have to list all of them twice.

In my code above, is it accurate to say that width and height are global variables because they're created outside of the functions, just in my loop, but that they are also local inside create_map()? Or would I just say they're global and not worry about the copies inside create_map()?

This is why I'm spending my holidays upskilling in Python, because it's so much easier to teach if you understand way more than you need to teach your students, rather than just being a few steps ahead :)

Sorry for the long post, and thanks in advance to anyone who can clear up any of this for me!

Hannah

Oops, now I have another question. I refactored my code to take out the globals by just passing in all the variables that I was originally accessing the globals of, and that worked, but then I found that I could actually just take out all of the arguments altogether, and my code still works, and I don't really understand why!

For example, this is the setup of my game still:

# Game Loop
repeat = True
while repeat == True:
    #Welcome the user
    print("Welcome to the dungeon!")
    print("Choose your challenge by setting the room size: \n\n")

    #Set room size
    width, height = "", ""
    while not width.isnumeric() or int(width) < 2:
        width = input("Enter room width: ")

    while not height.isnumeric() or int(height) < 2:
        height = input("Enter room height: ")

    width, height = int(width), int(height)

    #Initialise variables
    rooms = []
    breadcrumbs = []

    #Set up game
    create_map()
    player, door, monster = create_room()

So I'm creating width and height variables, but I can run my create_map() function without passing in width and height, or accessing them as global, and the game still runs fine. I thought I would have to pass them in?

I thought it might be something to do with just reading the values vs. modifying them, but appending to the rooms and breadcrumbs lists is working fine without passing them in as arguments, and I can even change an existing value without passing in the rooms list, e.g. rooms[0] = (1, 1) and that works.

Here are the create_map() and create_room() functions again:

def create_map():

    for y in range(height):
        for x in range(width):
            rooms.append((x, y))

def create_room():

    #Put the player in a spot
    player = random.choice(rooms)
    breadcrumbs.append(player)

    #Put the door in a different spot to player
    valid = False
    while valid == False:
        door = random.choice(rooms)
        if door != player:
            valid = True

    #Put the monster in a different spot to player and door
    valid = False
    while valid == False:
        monster = random.choice(rooms)
        if monster != player and monster != door:
            valid = True

    return player, door, monster
Kenneth Love
STAFF
Kenneth Love
Treehouse Guest Teacher

Variables that multiple functions need access to should be defined in the scope above the functions. That way they're all working on the same ultimate bit of data. If a function needs to work based on a value (stored in a variable or otherwise), you should pass that value into the function. Using global will eventually cause you to change a variable when you didn't mean to and you'll spend hours tracking it down.

Python isn't meant to be written in a strict, list-everything-first-and-then-build way. It's a quick, dynamic language and works best when used that way. Python garbage collection is solid and automatically cleans up variables and values that are no longer referenced anywhere.

Yes, I'd make height and width "global" variables defined at the start of your script. They get passed into create_map() because you need that function to use their values to do its work. If they're defined outside of all of the functions and loops, they'll be in the scope of the entire script so any functions can access them. The copies inside of create_map() don't matter because they'll be destroyed once create_map() exits.

Once again, you won't gain any major advantages in Python by declaring everything first and you'll likely just give yourself headaches. It's meant to be almost pseudo-code that actually works!

So does that mean I should write the width, height = "", "" line up the top before I start defining the functions, and then keep the part where the user enters the width and height inside the game loop?

Thanks so much for your replies, they're really helpful for getting my head around best practice!

Kenneth Love
Kenneth Love
Treehouse Guest Teacher

If your default width and height are 3, why not just set them to that by default? Then you have workable defaults. But, yes, I'd define them up to and then give the user the ability to overwrite them if wanted.

Kenneth Love
STAFF
Kenneth Love
Treehouse Guest Teacher

Let's look at some code together:

name = 'Kenneth'
age = 33
courses = ['Python Basics', 'Python Collections', 'Object-Oriented Python']

def change_name():
    name = 'Bob'

change_name()

What's the value of name now? It's still 'Kenneth' because inside of change_name we set the value of a local variable named name. Why didn't the outside one change? Almost a trick question! It didn't change because strings are immutable. We'd have to use global here if we wanted to change the outside variable but then, every time we call change_name, we've changed the name. That's kind of weird and bad design. It makes so much more sense and is much more explicit to make change_name return a new value and we'll just put that into our name variable.

Let's add another function.

def change_age():
    age = 55

change_age()

Is age now 55? Nope, it's still 33. Ints are also immutable (as are floats, booleans, and tuples). Again, this function should return a new value instead of trying to set one inside the function. This will make your code's design more purposeful and, again, explicit.

Alright, one last function.

def add_class():
    classes.append('Flask Basics')

add_class()

So, did classes change? Yep. This is because lists (along with dicts and sets) are mutable. That means they are changed in place. This is why your two lists can be edited inside of functions where they're not passed in. It's a handy side effect but one to definitely keep in mind. Why? Let's do one more bit of code.

def letters(start=[]):
    start.append('b')

letters()

What's going to come out? ['b'] of course. But what if I call it again?\

letters()

Now it returns ['b', 'b']. What the heck?! Why did I get back two 'b's? Well, start, in the function, is a mutable list and is hanging around because letters() is still being referenced (because we can still call it). So when we call letters() again, we modify that list in memory. What if we called letters with an argument?

letters(['a'])

This will give us back ['a', 'b'], as expected. Even if we call it multiple times, we'll still just get back that same list. We're no longer referencing that previous list so it's not being extended even further. But call letters() again without an argument and you'll get three 'b's!

This is one area of Python where it's really to understand what you're working with and whether the variables are mutable or not. It wouldn't do you any great service to have the variables defined beforehand, but knowing that, say, your rooms is a list let's you know that you can modify it from inside of functions without any problems. But since your player name is a string, you can't change it from inside of a function w/o the function returning a value and you assigning it to the name variable.

Also, don't use lists as default values :)

(tagging Ryan Carson on this as I think he'll find it interesting)

Wow, thanks, that's awesome. So is that mutability difference the reason why I can do this:

score  = 0
points = 10

def change_score(score):
    score += points
    return score

print(score) # prints 0

score = change_score(score)
print(score) # prints 10

And the function runs fine, and adds 10 to the score, but I only have to pass in score, and not points, because I'm not changing points but I am changing score? Is it better/more pythonic to do that or this with points passed in too:

score  = 0
points = 10

def change_score(score, points):
    score += points
    return score

print(score) # prints 0

score = change_score(score, points)
print(score) # prints 10

Thanks so much again for your time, and glad you enjoyed the game. I am particularly happy with the unicode icons and the WASD keys for navigating - would be better if you didn't have to hit enter, but I guess that's easier with a GUI.

Kenneth Love
Kenneth Love
Treehouse Guest Teacher

Correct. points is effectively a constant there, since you're always adjusting the score by the same amount. In your second example, it would make more sense to have the points argument have a default.

I would say both are equally Pythonic. The difference would be in the needs of your project. If the score always increases by the same amount, the first makes more sense. If different actions award different increases, the second, with a default, makes more sense.

Awesome, thanks, that makes sense.

One last question (I promise), which is kinda related - the use of break in Python. There's that old argument that break and goto are bad programming, but I notice you use break a lot, and in my experience there are situations where using break in a while True: loop is several lines of code less than using a variable to keep the loop going (like in my game I'm using while repeat == True: and changing the value of repeat as Boolean).

Is break pretty acceptable in Python or considered Pythonic? Other teachers (at schools here) have said at beginner level we should be encouraging the students to use variables and structure their code well rather than using break etc. but not many have actually worked in industry, and especially not recently, so I'd be interested to know how it's seen in the Python community.

Thanks

Kenneth Love
Kenneth Love
Treehouse Guest Teacher

Hannah Taylor: break isn't unPythonic at all. It's how you end a loop early. Even if my loop was going over a list or waiting for a variable to change to False, it's still valid to end the loop prematurely with break. In fact, Python has an else clause for for and while loops that's always fired so long as the loop completed without breaking (thankfully we don't have goto in Python).

Ideally you know if/when something will happen in your program. You'll know when a loop will end. But if you're not sure when, or you just don't want to have that extra bit of memory overhead, or whatever, you can use break to get out of the loop.

can't run in google chrome, Why?