pythontextadventure

Python: Listing methods in a dict in __init__


So what I want to do is kind of hard to describe in the title.

Here's what I want to do: In the code below, I want to have some general methods someone can call on the Room class (ex. Search, Loot) and the Player class (ex. quit, heal). The way I want that to happen is the player enters what they want to do in an input, and python will look up that choice in a dict which matches the choice to a method.

I've already successfully done that with the exits on the rooms. I can probably do it by making a child class and listing the methods there, but I really don't want to do that as that would seem cluttery.

When I run the code below, it just auto exits. If I run it with that first dictionary commented out, I get an error saying that __init__() is missing a required positional argument.

from textwrap import dedent
from sys import exit

class Player(object):


    actions = {
        'QUIT': quit
    }
    def __init__(self, actions):
        self.actions = actions
        # Want actions to be a list of actions like in the Room Class
        # 

    def quit(self):
        # Quits the game
        exit(0)

class Room(object):

    # Description is just a basic room description. No items needed to be added here.
    def __init__(self, desc, exits, exitdesc):
        self.desc = desc
        self.exits = exits
        self.exitdesc = exitdesc
        # Also want list of general actions for a room here.

    def enterroom(self):
        #First print the description of the room
        print(self.desc)
        #Then print the list of exits.
        if len(self.exits) > 1:
            print(f"You see the following exits:")
            for exd in self.exitdesc:
                print(self.exitdesc[exd])
        elif len(self.exits) == 1:
            print(f"There is one exit:")
            for exd in self.exitdesc:
                print(self.exitdesc[exd])
        else:
            print("There are no exits.")
        # Then allow the player to make a choice.
        self.roomactivity()

    # Here's what I mean about calling the methods via a dictionary
    def roomactivity(self):
        while True:
            print("What do you want to do?")
            choice = input("> ").upper()
            if choice in self.exits:
                self.exits[choice].enterroom()

    #And here's where I want to call actions other than directions.
            elif choice in player.actions:
                player.actions[choice]
            else:
                print("I don't understand.")

class VoidRoom(Room):
    def __init__(self):
        super().__init__(
            desc = "ONLY VOID.",
            exits = {},
            exitdesc = {})

class TestRoom(Room):
    def __init__(self):
        super().__init__(
            desc = dedent("""
                This room is only a test room.
                It has pure white walls and a pure white floor.
                Nothing is in it and you can hear faint echoes
                of some mad sounds."""),

            exitdesc = {
                'NORTH': 'To the NORTH is a black door.',
                'SOUTH': 'To the SOUTH is a high window.',
                'EAST': 'To the EAST is a red door.',
                'WEST': 'To the WEST is a blue door.'},
            exits = {
                'NORTH': void_room,
                'SOUTH': void_room,
                'EAST': void_room,
                'WEST': void_room})

void_room = VoidRoom()
test_room = TestRoom()
player = Player()

test_room.enterroom()

I hope I've explained the issue clearly. Still learning this language and I may have bitten off more than I can chew at the moment.

EDIT: New Code Below:

I've changed some things around, I have the player commands and stuff in a separate py file so I can expand the player scope without cluttering up the rooms.py file.

from textwrap import dedent
from sys import exit
from player import *
from enemies import *

# This is the base class for a room.
class Room(object):

    # Description is just a basic room description. No items needed to be added here.
    def __init__(self, desc, exits, exitdesc, inventory):
        self.desc = desc
        self.exits = exits
        self.exitdesc = exitdesc
        self.inventory = inventory

    def enterroom(self):
        #First print the description of the room
        Player.currentroom = self
        print(self.desc)
        for item in self.inventory:
            print(self.inventory[item].lootdesc)
        #Then print the list of exits.
        if len(self.exits) > 1:
            print(f"You see the following exits:")
            for exd in self.exitdesc:
                print(exd)
        elif len(self.exits) == 1:
            print(f"There is one exit:")
            for exd in self.exitdesc:
                print(exd)
        else:
            print("There are no exits.")
        # Then allow the player to make a choice.
        self.roomactivity()

    def roomactivity(self):
        while True:
            print("What do you want to do?")
            choice = input("> ").upper()
            if choice in self.exits:
                self.exits[choice]().enterroom()
            elif choice in Player.actions:
                Player.actions[choice]()
            else:
                print("I don't understand.")
                #Player.actions[choice]()

class Room3(Room):

    def __init__(self):
        super().__init__(
            desc = dedent("""
                You are in a large, dimly lit room.
                Torches sit in empty alcoves, giving off an eerie red glow.
                You hear scratching and squeaking from behind the walls."""),
            exits = {
                'NORTHEAST': StartRoom
            },
            exitdesc = [
                'A sturdy looking door leads to the NORTHEAST'
            ],
            inventory = {})



class Room1(Room):

    def __init__(self):
        super().__init__(
            desc = dedent("""
                You are in a medium sized, dimly lit room.
                Busts of dead men you don't know sit atop web-strewn pedestals."""),
            exits = {
                'EAST': StartRoom
            },
            exitdesc = [
                'An arch leading into a dimly lit hall lies to the EAST.'
            ],
            inventory = {'IRON SWORD': iron_sword}
        )



class StartRoom(Room):

    def __init__(self):
        super().__init__(
            desc = dedent("""
                PLACEHOLDER LINE 49"""),
            exits = {
                'SOUTHWEST': Room3,
                'WEST': Room1
            },
            exitdesc = [
                'An arch leading into a dimly lit room lies to the WEST',
                'A sturdy looking door lies to the SOUTHWEST'],
            inventory = {}
        )



class HelpPage(Room):

    def __init__(self):
        super().__init__(
            desc = dedent("""
                All actions will be listed in all caps
                When asked for input you may:
                QUIT the game
                Check your INVENTORY
                Check your player STATUS
                SEARCH the room
                EXAMINE an object or point of interest
                USE an item from your inventory or the room
                ATTACK a creature
                GET an item from the room
                or pick a direction (listed in caps)"""),
            exits = {},
            exitdesc = [
                'Press ENTER to return to the Main Menu'],
            inventory = []
            )

    def enterroom(self):
        print(self.desc)
        for exd in self.exitdesc:
            print(exd)
        self.roomactivity()

    def roomactivity(self):
        input()
        MainMenu.enterroom()

help_page = HelpPage()

# Main menu, lil bit different from a regular room
class MainMenu(Room):

    def __init__(self):
        super().__init__(
            desc = dedent("""
                THE DARK DUNGEON OF THE VAMPIRE KNIGHT
                A game by crashonthebeat"""),
            exits = {
                'START': StartRoom,
                'HELP': HelpPage
            },
            exitdesc = [
                'Press START to Start the Game',
                'Or go to the HELP Menu'],
            inventory = []
            )

        def enterroom(self):
            print(self.desc)
            for exd in self.exitdesc:
                print(exd)
            self.roomactivity()

        def roomactivity(self):
            while True:
                choice = input("Choose an Option: ")
                if choice in self.exits:
                    self.exits[choice]().enterroom()
                else:
                    print("I don't understand")

And the relevant code from player.py

from items import *
from rooms import *

class Player(object):

    @property
    def actions(self):
        actions_map = {
            'QUIT': 'quit_',
            'STATUS': 'status',
            'INVENTORY': 'printinventory',
            'EXAMINE': 'examine',
            'USE': 'useitem',
            'SEARCH': 'searchroom',
            'GET': 'getitem',
            'CURRENTROOM': 'getcurrentroom'
        }
        return actions_map

Solution

  • I see a few potential problems:

    1. Where does quit come from in Player's actions dictionary? It is presented as a known name of some kind (variable/method/object) but the only time you define quit is as a method of Player, so the class attribute actions cannot access it.

    2. quit is never actually called. When player.actions[choice] is executed on user input "QUIT" for example, even if quit did exist, that just returns whatever function it points to. It does not call that function. This is bad. player.actions[choice]() would get you there.

    3. Defining a variable in your script and referencing the script variable in your class is a no-no. It's OK to have your class method call VoidRoom() or TestRoom(), but having it reference variables test_room and void_room from a completely different namespace, not so much.

    See examples below:

    actions = {
            'QUIT': quit
        }
    

    This will not quit your program. "quit" is also a reserved word in the python IDLE, so not the best choice for a method. Python convention is to put a '_' at the end to avoid clashing with reserved words: quit_. I would remove that attribute entirely and make it a property, so you can just override it in its children and add extra functionality. You sacrifice the ability to initialize players with custom actions, but wouldn't those make more sense as classes with associated actions anyway?

    class Player(object):
        @property
        def actions(self):
            actions_map = {
                'QUIT': self.quit_
            }
            return actions_map
    
        def quit_(self):
            print("Quitting the game.")
            exit(0)
    
    class PlayerThatCanSing(Player):
        @property
        def actions(self):
            default_actions = super().actions # We still want Player actions
            new_actions = {
                'SING': self.sing
            }
            combined_actions = new_actions.update(default_actions) # Now player can quit AND sing
            return combined_actions
    
        def sing(self):
            print("Do Re Ma Fa So La Te Do")
    

    Now referencing player.actions['QUIT']() calls player.quit_, which is what you want.

    Regarding #3:

    class TestRoom(Room):
        def __init__(self):
            super().__init__(
                desc = dedent("""
                    This room is only a test room.
                    It has pure white walls and a pure white floor.
                    Nothing is in it and you can hear faint echoes
                    of some mad sounds."""),
    
                exitdesc = {
                    'NORTH': 'To the NORTH is a black door.',
                    'SOUTH': 'To the SOUTH is a high window.',
                    'EAST': 'To the EAST is a red door.',
                    'WEST': 'To the WEST is a blue door.'},
                exits = {
                    'NORTH': void_room,
                    'SOUTH': void_room,
                    'EAST': void_room,
                    'WEST': void_room})
    
    void_room = VoidRoom()
    test_room = TestRoom()
    player = Player()
    

    You are declaring void_room and test_room at script run time, which is fine. The only problem is your classes do not know anything about your runtime variables, so if you want North, South, East, and West to map to an instance of VoidRoom (which is a class that TestRoom knows about, because it's sitting right there above it in the module), just reference VoidRoom() directly, not void_room. Never assume your classes know anything about anything that happens outside that class and hasn't been passed into the __init__ for the class.

    I hope that Player example with the actions property (just think of property in this case as a way to reference a function as a variable - since actions returns a dict, we can just treat it like a dict without calling the method with actions(). player.actions will return the dict, nice and readable) makes sense, because if you implement it that way, you can have specific types of bards that inherit many layers down, and overriding actions with the call to super().actions (their parent class) means even the most specific DwarfBlacksmithWhoSingsInHisSpareTime class gets all of the parent actions all the way up (because each actions method calls its parent, and on and on until it hits Player), so you get a Dwarf guy that can quit, sing, and blacksmith. Pretty elegant, and I hope it's not too confusing, because it's a very cool concept. Best of luck!