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.