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

David Dzsotjan
David Dzsotjan
6,405 Points

Dungeon of the Killer Bunnies - the Dungeon game re-imagined

Kenneth Love

Here's a juiced up version of the original Dungeon game by Kenneth, re-imagined with the killer bunny from the Knights of the Holy Grail. You can specify the dimensions of the dungeon and the numbers of the bunnies who move around in a random fashion. If you meet one, you have to fight - using the Holy Hand Grenade of Antioch, of course! The hit/miss ratio depends on the number of rabbits in the room with you. If you survived the bloody struggle, you can continue your way to find the Grail.

import random

import os

import math


def clear():
    if os.name == 'nt':
        os.system('cls')
    else:
        os.system('clear')


def make_dungeon(Xmax, Ymax):
    CELLS = []

    for x in list(range(Xmax)):
        for y in list(range(Ymax)):
            CELLS.append((x, y))
    return CELLS


# placing player, door and blood rabbits randomly
def get_positions(CELLS, monster_number, player_health):
    # 'been' : rooms where the player has been
    player = {'position': random.choice(CELLS), 'been': [], 'health': player_health}
    door = random.choice(CELLS)

    if player['position'] == door:
        return get_positions(CELLS, monster_number, player_health)

    # placing the monsters (rabbits)
    pos_monster = list(range(monster_number))

    for number in list(range(monster_number)):
        # accept generated position only if it is different from player and door!
        while True:
            pos_monster[number] = (random.choice(CELLS))
            if pos_monster[number] != player['position'] and pos_monster[number] != door:
                break
    # list of roomw where the player has been
    (x, y) = player['position']
    player['been'] = [(x, y)]
    return [player, door, pos_monster]


# determine the possible moves at a given position
def get_moves(position, Xmax, Ymax):
    moves = ['LEFT', 'RIGHT', 'UP', 'DOWN']

    if position[1] == 0:
        moves.remove('LEFT')
    if position[1] == Ymax - 1:
        moves.remove('RIGHT')
    if position[0] == 0:
        moves.remove('UP')
    if position[0] == Xmax - 1:
        moves.remove('DOWN')
    return moves


def move_player(player, move):
    (x, y) = player

    if move == 'LEFT':
        y -= 1
    elif move == 'RIGHT':
        y += 1
    elif move == 'UP':
        x -= 1
    elif move == 'DOWN':
        x += 1

    return (x, y)


def move_monster(monster, Xmax, Ymax):
    (x, y) = monster

    moves = get_moves(monster, Xmax, Ymax)
    move = random.choice(moves)

    if move == 'LEFT':
        y -= 1
    elif move == 'RIGHT':
        y += 1
    elif move == 'UP':
        x -= 1
    elif move == 'DOWN':
        x += 1
    return (x, y)


# Getting visible rooms, to be able to draw fog of war
def fog_of_war(player, CELLS):
    visible_rooms = []
    (x, y) = player['position']
    ind_list = [-1, 0, 1]
    for indx in ind_list:
        for indy in ind_list:
            if (x + indx, y + indy) in CELLS:
                visible_rooms.append((x + indx, y + indy))
    return visible_rooms


# Drawing map with fog of war: only the nearest rooms are visible to the player
def draw_map(player, door, pos_monster, CELLS, visible_rooms, Ymax, show_door):
    # legend explaining symbole
    print("\n X: You, R: Killer rabbits, O: The Holy Grail, #: Breadcrumbs, ~: Darkness")

    print(' _' * Ymax)
    tile = '|{}'

    for cell in enumerate(CELLS):
        # if it's a room on the right hand edge
        if (cell[0] + 1) % Ymax == 0:
            # is the cell within sight range?
            if cell[1] in visible_rooms:
                # if player is in the room
                if cell[1] == player['position']:
                    if cell[1] in pos_monster:
                        print(tile.format('*|'))
                    else:
                        print(tile.format('X|'))
                # if bunny in the room, but no player
                elif cell[1] in pos_monster:
                    print(tile.format('R|'))
                # if the Grail is in the room
                elif cell[1] == door:
                    show_door = True # we turn it on, s othat we see the Grail later too
                    print(tile.format('O|'))
                # if we've been here before
                elif cell[1] in player['been']:
                    print(tile.format('#|'))
                else:
                    print(tile.format('_|'))
            # if it's in darkness
            else:
                if cell[1] == door and show_door:
                    print(tile.format('O|'))
                elif cell[1] in player['been']:
                    print(tile.format('#|'))
                else:
                    print(tile.format('~|'))

        else:
            if cell[1] in visible_rooms:
                if cell[1] == player['position']:
                    if cell[1] in pos_monster:
                        print(tile.format('*'), end='')
                    else:
                        print(tile.format('X'), end='')
                elif cell[1] in pos_monster:
                    print(tile.format('R'), end='')
                elif cell[1] == door:
                    show_door = True
                    print(tile.format('O'), end='')
                elif cell[1] in player['been']:
                    print(tile.format('#'), end='')
                else:
                    print(tile.format('_'), end='')
            # if it's in darkness
            else:
                if cell[1] == door and show_door:
                    print(tile.format('O'), end='')
                elif cell[1] in player['been']:
                    print(tile.format('#'), end='')
                else:
                    print(tile.format('~'), end='')

    return show_door

# Fighting the beast(s)
def fight(player, pos_monster):
    monster_health = 2

    # the number of beasts in the room influences the hit/miss ratio
    monster_number = 0
    for beast in pos_monster:
        if beast == player['position']:
            monster_number += 1

    # chance to hit * 100
    hit_ratio = 100 / (monster_number + 1)
    # commentary
    remark2 = ''
    # the fight goes on until any of them die
    while monster_health > 0 and player['health'] > 0:

        clear()
        print("\nThey've found you! Your grip tightens on the Holy Hand Grenade of Antioch.\n")
        if monster_number == 1:
            print("1 killer bunny in the room.\n")
            print("Your chance of landing a hit: {}%".format(hit_ratio))
            print("The probability that you get hurt: {}%".format(100 - hit_ratio))
        else:
            print("{} bunnies in the room, you attack the closest one.\n".format(monster_number))
            print("Your chance to land a hit: {}%".format(round(hit_ratio, 2)))
            print("The probability that you get hurt: {}%".format(round(100 - hit_ratio), 2))
        print('\n')
        print("Player Health: " + '=' * player['health'])
        print("Rabbit health: " + '=' * monster_health)
        print('\n')
        print(remark2 + '\n')
        input("Hit Enter to launch the Holy Hand Grenade! 1, 2, 3... ")

        # BATTLE
        if hit_ratio > random.randint(0, 100):
            monster_health -= 1
            remark2 = "A fine hit!"
        else:
            player['health'] -= 1
            remark2 = "Ouch! You missed and it managed to sneak a nasty bite!"

    if not monster_health:
        clear()
        if monster_number == 1:
            input("\nThe killer bunny is dead and you're safe once again. \nPress Enter")
        else:
            plural = ''
            if monster_number == 3:
                plural = 's'
            input(
                "The lifeless carcass makes the other{} recoil and you use this time to decide which way to run. \nPress Enter".format(
                    plural))
        return True
    else:
        return False


# You have to escape the rabbits and get to the door. If you meet a rabbit, you fight to the death.
#  You can specify the dimensions of the dungeon, and the number of rabbits. Good luck!
# Your sight is limited to the room you are in and the adjacent rooms.
# If the Grail is close (your distance from it is less than 1/4 of the longest side of the dungeon), it is shown on the map

def dungeon_game():
    clear()
    print("\nWelcome to the Killer Bunny Dungeon!\n")
    print("Evade the bloodthirsty rabbits and get to the Holy Grail beyond them!")
    input("However, if you meet one (or more), you'll have to stand your ground and fight!\n")

    print("The map aids you by showing your position and the rooms where you've")
    print("already been, thanks to the breadcrumbs you scatter around.\n")
    print("Also, you can see what's going on in the rooms next to yours, but not further!")
    print("It's dark and foggy down here... and full of savage rabbits!\n")

    # getting the parameters of the dungeon
    while True:
        try:
            Ymax = int(input("How wide should the dungeon be? "))
            Xmax = int(input("How deep? "))
            monster_number = int(input("And how many bunnies? "))
        except ValueError:
            print("That is not an integer!")
            continue
        else:
            break

    player_health = 5

    CELLS = make_dungeon(Xmax, Ymax)
    (player, door, pos_monster) = get_positions(CELLS, monster_number, player_health)
    # rooms adjacent to player position
    visible_rooms = fog_of_war(player, CELLS)
    # if it's True, the Grail is shown on the map
    show_door = False

    remark = ''
    while True:

        # draw dungeon, with player and monster positions
        clear()
        show_door = draw_map(player, door, pos_monster, CELLS, visible_rooms, Ymax, show_door)

        print('\n' + remark + '\n')
        # move player
        moves = get_moves(player['position'], Xmax, Ymax)
        print("You can move {}, or 'WAIT'.".format(moves))
        print("Enter QUIT to quit.")
        move = input("> ")
        move = move.upper()

        if move == 'QUIT':
            break
        if move in moves:
            player['position'] = move_player(player['position'], move)
            # if player hasn't been here yet, append to 'been'
            if not player['position'] in player['been']:
                player['been'].append(player['position'])
            remark = ''
            # updating list of adjacent rooma
            visible_rooms = fog_of_war(player, CELLS)
            # if the Grail is close, show it on the map
            if math.sqrt(math.pow(player['position'][0] - door[0], 2) + math.pow(player['position'][1] - door[1], 2)) < max(Xmax, Ymax) / 4:
                show_door = True
                remark = 'You feel a reassuring presence... The Grail is near!'
        # player position doesn't change, only monsters move
        elif move == 'WAIT':
            remark = "You stay still and listen as they hop around in the dark... creepy..."
        else:
            remark = "** Walls are hard, stop walking into them! **"
            continue

        # move monster(s)

        for number in enumerate(pos_monster):
            pos_monster[number[0]] = move_monster(pos_monster[number[0]], Xmax, Ymax)

        if player['position'] == door:
            clear()
            print("\nYou've found the Holy Grail! You are saved!!")
            break
        elif player['position'] in pos_monster:
            # fight them and see if you stay alive
            alive = fight(player, pos_monster)
            if alive:
                # kill 1 monster that was in the same room with player
                pos_monster.remove(player['position'])
                remark = "Quickly, before there's more of them!!"
                continue
            else:
                clear()
                print("Oh no... You became rabbit food...")
                break

    # play again?
    again = input("\nWould you like to play again?(Y/n) ")
    if again.lower() == 'n':
        print("Bye-bye!")
    else:
        dungeon_game()


dungeon_game()

2 Answers

Am I meant to be able to see where everything is? It makes it fairly easy... Though you should probably start with a legend/key to tell the player what each of the symbols mean.

Great idea with the health bars! And awesome theme. :)

Oh and I just got an error:

How wide should the dungeon be? 7
How deep? 7
And how many bunnies? 5
Traceback (most recent call last):
  File "bunnies.py", line 255, in <module>
    dungeon_game()
  File "bunnies.py", line 252, in dungeon_game
    dungeon_game()
  File "bunnies.py", line 252, in dungeon_game
    dungeon_game()
  File "bunnies.py", line 252, in dungeon_game
    dungeon_game()
  File "bunnies.py", line 252, in dungeon_game
    dungeon_game()
  File "bunnies.py", line 252, in dungeon_game
    dungeon_game()
  File "bunnies.py", line 197, in dungeon_game
    (player, door, pos_monster) = get_positions(CELLS, monster_number, player_health)
  File "bunnies.py", line 27, in get_positions
    return get_positions(CELLS, monster_number)
TypeError: get_positions() missing 1 required positional argument: 'player_health'

Looks like you missed an argument on line 27:

        return get_positions(CELLS, monster_number)

Should probably be:

        return get_positions(CELLS, monster_number, player_health)

Oh and you might want to round off your probabilities to a couple of decimal places or something :)

David Dzsotjan
David Dzsotjan
6,405 Points

Iain Simmons Hi Iain, thanks so much for taking a look at the game and noticing the cracks in the code! :) And thanks for the suggestions! I have taken care of the error message, and rounded the hit/miss chances to 2 digits as suggested. Also, instead of showing the whole dungeon, I introduced a fog-of-war, so that only the adjacent rooms are visible (I think it makes it a bit more exciting if you see that there are monsters around, but you can't see all of them). Additionally, I added the feature of giving a hint about the position of the Grail, once you get close enough to it - so that it won't get so very boring to randomly walk around. Ah, and there are also breadcrumbs, showing the rooms where you've been :) I updated the code in the original message! Wishing you a nice day! :))