5
\$\begingroup\$

I'm wondering what would be a clever way of making a layered menu system, in terms of what data structures to use.

An example of what I mean: A unit has the ability to construct buildings, and to cast some spells. The selection of buildings and spells are accessed either through pressing a hotkey or clicking an icon in the user interface. What is the best way to achieve this in the code?

I'm thinking something like:

Menu base state: options are A and B. If the players clicks A: change state and display the suboptions of A, and so on. But how is this achieved in the actual code? Perhaps a dict() containing functions:

options = {
           'A' : self.change_state ('a', self.current_state),
           'B' : self.change_state ('b', self.current_state)
          }

def on_key_event(self, key):
     options[key]

def change_state(self, key, state):
     if state == state_1:
          if key == 'a':
              ...

And so on, this seems really tedious and if possible I want to avoid if / else loops and just define everything in the simplest, and shortest manner.

I'm interested in how you would do this, or if you've already made a similar system, how you solved it in your own code.

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

Try using a common class/interface for each of your "states", this is a fairly common approach to state machines (and avoids the long chain of if/else).

You could use just a dictionary of functions, but more often than not you'll likely find you want some form of initialisation functions too, or notification of completion - so may as well go with a "classic"

Generally a state implementation will have at minimum enter(), exit() and update() methods, and the state machine will just contain a stack of states which it updates.

It can also be handy (as you mention) to have a dictionary lookup for them, otherwise you need to keep references or instantiate new states in order to switch. For a menu system I'd highly recommend having a dictionary of some sort as it becomes a lot easier to dynamically switch between menus in a data-driven fashion. (eg. switching to a menu based on metadata in the button rather than pre-coding everything)

Depending on your needs, you can either:

  • build your menu on top of these vanilla states (and have things like input checks happen in each respective update() method)
  • extend the types of "events" states can receive so that your central application can call states with things like userSelected(), mouseClicked() or similar.

I've whipped up a (very) simple example in Python here, but I'm not a Python programmer, so apologies if it burns your eyes.

The main purpose of this code is to demonstrate that the state machine itself can be a state, allowing you to nest them for complex behaviour (or just more flexible menus)

""" A simple base state for the state machine
"""
class BaseState:
    parent = None
    name = "state"
    isDone = False
    def __init__(self, name):
        self.name = name or self.name

    def enter(self):
        pass
    def update(self):
        pass
    def exit(self):
        pass


""" A simple State Machine
    Note that it is a sub-class of BaseState, this allows
    for nested complex states.

"""
class StateMachine(BaseState):
    def __init__(self, name):
        BaseState.__init__(self, name)
        self.stateDict = {}
        self.reset()

    def update(self):
        """ Update states
            note that it's sometimes beneficial to have an extra
            method on your state objects to allow them to forcibly
            seize control in certain scenarios, this simple model
            doesn't include such things
        """

        # no processing an empty stack often this will be used
        # to exit the menu or application (for example)
        if len(self.stateStack) <= 0:
            # here we're manually exiting the last state (if any)
            # since we are ensuring predictable behaviour by delaying
            # calls to enter() and exit() ie. not exiting a state
            # when it's done, but instead once we're ready for it to be
            self.exitState(self.lastState)
            self.isDone = True
            return

        currentState = self.stateStack[-1]

        if self.lastState != currentState:
            """ We've switched states, notify everyone involved """
            self.switchState( currentState )

        if currentState != None:
            currentState.update()            
            if currentState.isDone:
                self.popState()

    def reset(self):
        """ Reset the state machine, but leave its dictionary intact """
        self.stateStack = []        
        self.lastState = None
        self.isDone = False

    def switchState(self, newState):
        """ Handles exiting and entering states appropriately """
        self.exitState( self.lastState )
        self.enterState( newState )
        self.lastState = newState

    def enterState(self, stateObj):
        """ Helper function to safely enter the specified state object """
        if stateObj != None:
            stateObj.enter()

    def exitState(self, stateObj):
        """ Helper function to safely exit the specified state object """
        if stateObj != None:
            stateObj.exit()

    def addState(self, state):
        """ Add a state to the dictionary for later
            ! Doesn't push or enter !
        """
        state.parent = self
        self.stateDict[state.name] = state

    def pushState(self, stateName):
        """ Push a state onto the stack by name """
        if not self.stateDict.has_key(stateName):
            print "here you whinge that state:%s doesn't exist.." % stateName
            return        
        newState = self.stateDict[stateName]
        self.stateStack.append(newState)

        return newState

    def popState(self):
        currentState = self.stateStack.pop()


""" A little example state for demonstration
"""

class TestState(BaseState):
    def __init__(self, name):
        BaseState.__init__(self, name)

    def enter(self):
        print "entered " + self.name

    def update(self):
        print "updated " + self.name

        # just bail out of this state for testing
        self.isDone = True

    def exit(self):
        print "exiting " + self.name


if __name__ == '__main__':
    """ Test out the state machine """

    machine = StateMachine("Master Control Program")

    ## Ultra simple
    machine.addState( TestState("State-A") )
    machine.addState( TestState("State-B") )

    ## set up a simple stack
    machine.pushState("State-A")
    machine.pushState("State-B")

    ## test it out
    print("Running simple state machine...\n")
    while not machine.isDone:
        machine.update()



    ## Nested test
    machine.reset()

    nestedMachine = StateMachine("Nested-A")
    nestedMachine.addState( TestState("Sub-State-A") )
    nestedMachine.addState( TestState("Sub-State-B") )
    nestedMachine.addState( TestState("Sub-State-C") )

    ## Set up a simple nested stack
    nestedMachine.pushState("Sub-State-A");
    nestedMachine.pushState("Sub-State-B");
    nestedMachine.pushState("Sub-State-C");

    machine.addState( nestedMachine )

    ## set up another simple stack
    machine.pushState("State-A")
    machine.pushState("Nested-A")
    machine.pushState("State-B")

    print("Running simple nested state machine...\n")
    while not machine.isDone:
        machine.update()
\$\endgroup\$
0
\$\begingroup\$

Clearly this is five years late, but I created a menu system in Python that isn't necessarily tied to state machines or games. However, it does require that you separately define the submenus and/or leaf procedures that menu options lead to. My approach takes advantage of the fact that functions in Python are first-class objects (and can therefore be stored in dictionaries, as you noted).

Here's the code:

# A Menu consists of one or more sub-Menu objects that a user can choose
# to run or subroutines that the user can call.
class Menu:

    # Constructor
    def __init__(self):
        self.options = {}
        self.setPrompt("Please choose an option:")
        self.setErrorText("Error! Please enter a valid option.")

    # Adds an option to this Menu. An option consists of a number,
    # name, and the actual object it corresponds to (submenu or procedure).
    # The last value in each key-value pair denotes whether the given option
    # should trigger an exit from the current Menu after it is called.
    def addOption(self, optionNumber, optionName, option, TriggersExit):
        self.options[optionNumber] = [optionName, option, TriggersExit]

    # Sets this Menu's error text
    def setErrorText(self, errorText):
        self.errorText = errorText

    # Sets this Menu's prompt
    def setPrompt(self, prompt):
        self.prompt = prompt

    # Returns the number of options in this Menu
    def size(self):
        return len(self.options)

    # Displays this Menu's error text
    def displayError(self):
        print("\n{}\n".format(self.errorText))

    # Prints the Menu in its entirety
    def display(self):
        print(self.prompt)
        for i in range(1, self.size()+1):
            print("{} - {}".format( i, self.options[i][0] ))
        print()

    # Runs this Menu
    def run(self):
        userInput = ""
        self.display()
        while True:
            userInput = input("Your selection: ")
            try:
                userInput = int(userInput)
                if userInput <= 0 or userInput > self.size():
                    self.displayError()
                else:
                    # If the menu option is a function, call it
                    if callable(self.options[userInput][1]):
                        self.options[userInput][1]()
                        # And if it's an option that triggers a return/exit, then return after it's called
                        if self.options[userInput][2]:
                            return
                        # Otherwise, redisplay the menu options
                        else:
                            self.display()
                    # But if the menu option is a submenu, run it
                    else:
                        self.options[userInput][1].run()
                        # And display the calling menu's options again upon return from the submenu
                        self.display()
            except ValueError:
                self.displayError()

def Exit():
    print("\n\tThanks for using my program!\n")

def Return():
    return

def Option1():
    values = input("\n\tEnter some values: ")
    print("\n\tYou entered these values:", end=" ")
    for i in range(0, len(values)):
        print(values[i], end=" ")
    print()

def MyLife():
    print("\n\tLife's hard.\n")

def GettingHelp():
    print("\n\tRecursion jokes suck\n")

def main():

    mainMenu = Menu()
    mainMenu.setPrompt("\nSelect an option:")

    menu1 = Menu()
    menu1.setPrompt("\nSelect an option:")
    menu1.addOption(1, "Option1", Option1, False)
    menu1.addOption(2, "Return", Return, True)

    menu2 = Menu()
    menu2.setPrompt("\nWhat would you like help with?")
    menu2.addOption(1, "My life", MyLife, False)
    menu2.addOption(2, "Getting help", GettingHelp, False)
    menu2.addOption(3, "Return", Return, True)

    mainMenu.addOption(1, "Enter new values", menu1, False)
    mainMenu.addOption(2, "Help", menu2, False)
    mainMenu.addOption(3, "Exit", Exit, True)
    mainMenu.run()  


main()

Note that it does take some code to set up the menu hierarchy at first. But after you set everything up, all you have to do is invoke run on the mainMenu and let it do its thing.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .