1
\$\begingroup\$

I am a total newbie when it comes to coding in general, let alone Python.

After following a few tutorials, I decided to try making my own little navigation system for simple maps, sort of drawing from what I know about how old MUD games like Achaea work. It works, but I have no idea how robust or expandable it will be.

Code here:

def r0_0():
  print("You are in room 0,0")
  while True:
    a = input("You can move North or East.\n>")
    if a == "n":
      r1_0()
    elif a == "e":
      r0_1()
    else:
      print("You can't go that way.")

def r0_1():
  print("You are in room 0,1")
  while True:
    a = input("You can move North or West.\n>")
    if a == "n":
      r1_1()
    elif a == "w":
      r0_0()
    else:
      print("You can't go that way.")

def r1_0():
  print("You are in room 1,0")
  while True:
    a = input("You can move South or East.\n>")
    if a == "s":
      r0_0()
    elif a == "e":
      r1_1()
    else:
      print("You can't go that way.")

def r1_1():
  print("You are in room 1,1")
  while True:
    a = input("You can move South, West, Up, or Down.\n>")
    if a == "s":
      r0_1()
    elif a == "w":
      r1_0()
    elif a == "u":
      r1_1_1()
    elif a == "d":
      r1_1_b1()
    else:
      print("You can't go that way.")

def r1_1_1():
  print("You are in room 1,1,1")
  a = input("You can move Down.\n>")
  if a == "d":
    r1_1()
  else:
    print("You can't go that way.")

def r1_1_b1():
  print("You are in room 1,1,-1")
  a = input("You can move Up.\n>")
  if a == "u":
    r1_1()
  else:
    print("You can't go that way.")

    
r0_0()

I'm mainly looking for feedback on how to improve this system, or if it will even be reliable. Should I continue with this system, or is it going to cause problems later on?

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

A UX problem in this app is there's no easy way to quit.

A design problem is the call stack size. Each call spawns another call that never returns, slowly eating up the call stack. After 1000 or so calls on CPython, the program crashes.

A bug seems to be that r1_1_1 and r1_1_b1 don't have while True: loops, so if the player gives bad input, the player pops back to the previous room.

Conceptually, you're making a state machine. The player moves from the current state to the next state based on an input.

The code in general is pretty WET. Sometimes this is OK, but in this case it'd benefit from identifying and extracting common patterns into a data structure that encodes the state machine logic along with a little engine that runs the state machine.

A dictionary is a good data structure for a state machine. The keys represent the state names and values are nested dictionaries mapping decisions/inputs to next state names. Here's an example rewrite that illustrates how you could go about setting this up:

from types import MappingProxyType


def imm_dict(*args, **kwargs):
    return MappingProxyType(dict(*args, **kwargs))


states = imm_dict({
    "0,0": imm_dict(
        n="1,0",
        e="0,1",
    ),
    "0,1": imm_dict(
        n="1,1",
        w="0,0",
    ),
    "1,0": imm_dict(
        s="0,0",
        e="1,1",
    ),
    "1,1": imm_dict(
        s="0,1",
        w="1,0",
        u="1,1,1",
        d="1,1,-1",
    ),
    "1,1,1": imm_dict(
        d="1,1",
    ),
    "1,1,-1": imm_dict(
        u="1,1",
    ),
})

directions = imm_dict(
    n="North",
    s="South",
    e="East",
    w="West",
    u="Up",
    d="Down",
)


def humanize(lst):
    if not lst:
        return ""
    elif len(lst) == 1:
        return lst[0]
    return ", ".join(lst[:-1]) + f" or {lst[-1]}"


def explore():
    current_room = "0,0"

    while True:
        print(f"You are in room {current_room}")
        movements = list(states[current_room].keys())
        humanized_directions = humanize([directions[x] for x in movements])

        while True:
            print(f"You can move {humanized_directions}")
            response = input("> ").lower().strip()

            if response == "exit" or (response and response[0] == "q"):
                print("Goodbye.")
                return
            elif response not in movements:
                print("You can't go that way.")
            else:
                current_room = states[current_room][response]
                break


if __name__ == "__main__":
    explore()

A few advantages:

  • no stack overflows
  • separates engine from the raw state transition/movement data (easy to move to a config/JSON file if desired)
  • easy to extend (plug in a different set of rooms or make adjustments to the current room map without changing the engine code)

Interaction was left pretty direct without much abstraction, but I didn't want to be too premature about that. In a larger app, you might want to move the inner while loop out to a generic function that asks for input until a valid response is given.

For some text interaction applications like this, the logic per room isn't so regular and you'll need custom classes or functions to handle the logic for each state, so this app is a rather "easy" case because it's highly regular.

Taking that idea a step further, your particular room structure is a 3d grid, so if it's not too sparse, you might consider using a 3d list to store it. A step in that direction might be to switch from strings to tuples, possibly keeping all of them the same length. For example:

states = imm_dict({
    (0, 0, 0): imm_dict(
        n=(1, 0, 0),
        e=(0, 1, 0),
    ),
    (0, 1, 0): imm_dict(
        n=(1, 1, 0),
        w=(0, 0, 0),
    ),
    # ...
})

You don't have to use the immutable dictionary here, but I'm a fan of immutability and I don't want to accidentally mutate the state machine. Python doesn't have a native Object.freeze() (JS) or .freeze (Ruby) at the time of writing.

As a final node, always use 4 space indentation in Python. 2 space indentation only seems to be readable in languages that have braces (most languages) or at least an end keyword (Ruby, Lua, Bash). In Python, small indents make it difficult to determine which nesting level code belongs to.

\$\endgroup\$
8
  • \$\begingroup\$ Thanks for all the info! I'll parse through the code snippets later, but this was really helpful! The 2-space indents are a symptom of me using replit to code on a work machine that I can't put pyCharm on. \$\endgroup\$ Commented Dec 13, 2022 at 15:29
  • \$\begingroup\$ What is the "1st" in the humanize function? What does it refer to? \$\endgroup\$ Commented Dec 13, 2022 at 17:31
  • \$\begingroup\$ It's a list but it's normal to call it lst to avoid clashing with the builtin function list. \$\endgroup\$
    – ggorlen
    Commented Dec 13, 2022 at 17:37
  • \$\begingroup\$ Ah, I see. That plaintext font threw me off. Lol. Thanks again for the help! \$\endgroup\$ Commented Dec 13, 2022 at 17:41
  • \$\begingroup\$ Interesting, I didn't know about MappingProxyType. I guess it's interesting we have frozenset but no frozendict. Is it perhaps for a historical reason that frozenset exists, assuming MappingProxyType can "simulate" any "frozen" (or immutable) object? \$\endgroup\$
    – Juho
    Commented Dec 13, 2022 at 18:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.