13
\$\begingroup\$

This is a simple text-based adventure game I made. I'm a beginner and I want to know if the code is optimal or if it can be improved.

The story is not very complex, but I made it more for practicing coding.

print("Welcome " + name +"!")
start = input("Do you wish to begin your adventure? ").lower()

if start == "yes":
     print("Good luck, traveler!")

     print("It's been 3 years since the outbreak... You are doing your daily scavenging for food when you hear a loud cry from the back of the store. Do you investigate or leave immediately?")

answer = input("Type leave to leave or check to investigate: ").lower()

if answer == "check":
    print("You move slow towards the source of the sound only to discover a baby. You make him stop crying and are left with two options, take him with you or leave him there.")
    answer = input("Type take to take him with you or bye to leave him there: ").lower()

    if answer == "take":
        print("You look for some more supplies then leave the store with your new friend.")
        print("On your way home you encounter a bunch on infected.")
        print("You can either fight them or sneak past them.")
        answer = input("Type fight to fight them or sneak to sneak past: ").lower()
        if answer == "fight":
            print(
                "You fight the infected and you defeat them, but right before you deal the final strike one of them scratches your leg.")
            print("You run home.")
            print("You develop a strong fever and cannot treat yourself.")
            print("On your death bed, you hear the baby cry and think that because of you he will die too.")
            print("Later that night you die.")
            k = input("press any key to exit")

        elif answer == "sneak":
            print("You manage to sneak past the group of infected and you arrive home with, now, your baby.")
            print("You make an improvised cradle for him to sleep in.")
            print("You are tired after a long day and decide to go to sleep.")
            print("You fall asleep...")
            print(".")
            print(".")
            print(".")
            print("The end!")
            k = input("press any key to exit")


    elif answer == "bye":
        print("You leave the store immediately so you don't meet any infected attracted by the baby.")
        print("Later that night you are overwhelmed by a feeling of guilt.")
        print("You can't take it and finally end your life.")
        print("While you're laying on the floor, in a pool of blood, you wonder why you didn't help that baby.")
        print("As your final moments drift away, you regret not helping the poor soul.")
        k = input("press any key to exit")

    else:
        print("Not a valid option. You lose.")
        k = input("press any key to exit")

elif answer == "leave":
     print("You leave the store immediately, and head home.")
     print("On your way home you encounter a bunch on infected.")
     print("You can either fight them or sneak past them.")
     answer = input("Type fight to fight them or sneak to sneak past: ").lower()

     if answer == "fight":
         print("You fight the infected and you defeat them, but right before you deal the final strike one of them scratches your leg.")
         print("You run home.")
         print("You develop a strong fever and cannot treat yourself.")
         print("Later that night you die.")
         k = input("press any key to exit")


     elif answer == "sneak":
         print("You manage to sneak past the group of infected and you arrive home.")
         print("You are tired after a long day and decide to go to sleep.")
         print("You fall asleep...")
         print(".")
         print(".")
         print(".")
         print("The end!")
         k = input("press any key to exit")

else:
    print("Not a valid option. You lose.")
    k = input("press any key to exit")
\$\endgroup\$
3
  • 4
    \$\begingroup\$ As you can probably imagine, this code will blow up quickly if you want to extend it with just a little complexity. Nested ifs are usually "bad" and should be avoided. That said, they can be extracted into their own functions, this will remove the nestings and may remove duplicate code. However, for this example, a statemachine might prove useful, as the different stages can easily be interpreted as states. \$\endgroup\$
    – lukstru
    Jun 9 at 17:35
  • \$\begingroup\$ You might want to check for bugs. For example, what happens when the user types an invalid answer at the start prompt or at the fight/sneak prompt? \$\endgroup\$
    – Seb
    Jun 10 at 8:49
  • 2
    \$\begingroup\$ If this will turn into a large project, you may want to look at some of the languages specifically for writing "interactive fiction" / text adventures, which will take care of parsing commands, object trees, common verb actions, etc. for you. More discussion at emshort.blog/how-to-play/writing-if \$\endgroup\$
    – aschepler
    Jun 10 at 14:28

6 Answers 6

11
\$\begingroup\$

Too much code duplication.

Consider:

        if answer == "fight":
            print(
                "You fight the infected and you defeat them, but right before you deal the final strike one of them scratches your leg.")
            print("You run home.")
            print("You develop a strong fever and cannot treat yourself.")
            print("On your death bed, you hear the baby cry and think that because of you he will die too.")
            print("Later that night you die.")
            k = input("press any key to exit")

and

     if answer == "fight":
         print("You fight the infected and you defeat them, but right before you deal the final strike one of them scratches your leg.")
         print("You run home.")
         print("You develop a strong fever and cannot treat yourself.")
         print("Later that night you die.")
         k = input("press any key to exit")

The difference between these blocks is in the first case, you've taken the baby from the store, and in the other, you haven't, so one additional line of text is emitted. This could easily be represented with a state variable, instead of a separate code path:

        if answer == "fight":
            print(
                "You fight the infected and you defeat them, but right before you deal the final strike one of them scratches your leg.")
            print("You run home.")
            print("You develop a strong fever and cannot treat yourself.")
            if baby_was_taken:
                print("On your death bed, you hear the baby cry and think that because of you he will die too.")
            print("Later that night you die.")
            k = input("press any key to exit")

A similar modification for the sneak option removes another swath of duplicate code and text.

The point here is to save the world state and player decisions as data, instead of as the path through a long chain of nested if/elif/else statements. Your next adventure game may have places you can return to multiple times, with content that can change. A nested if/elif/else structure would quickly become unmanageable in that situation.

\$\endgroup\$
1
  • \$\begingroup\$ thanks for the advice :) \$\endgroup\$ Jun 9 at 17:06
8
\$\begingroup\$

Since you don't wait between prints, you can join some of them. Use triple quotes to extend a string on multiple lines (can be used for comments too) and \n to make a line break.

print('''This is a big string.\n
Too big to fit in one line.
''')

But for narrative purposes, you may want to wait between your prints :

import time

print('You collapse for 5 seconds.')
time.sleep(5)
print('You wake up')
\$\endgroup\$
1
  • \$\begingroup\$ In terms of formatting source code, I personally prefer using C-style concatenation: print( 'foo' 'bar' ). (Ironically, SO comments won't let me insert the newlines needed to give a clear example.) \$\endgroup\$
    – Will
    Jun 11 at 9:50
4
\$\begingroup\$

As already mentioned by others, the branch conditions will get out of control quickly. It may be more suitable to encode the storyline in a separate file, such as a JSON file. Therefor we can identify common schemes in the story flow:

  1. Print some text
  2. Ask for some input
  3. Provide a set of valid options
  4. IF option is invalid, bail out
  5. IF option is correct, repeat with new data set from 1.

Hence, we can encode the storyline in a JSON file like this:

story.json

{
    "lines": [
        "Good luck, traveler!",
        "It's been 3 years since the outbreak... You are doing your daily scavenging for food when you hear a loud cry from the back of the store. Do you investigate or leave immediately?"
    ],
    "prompt": "Type leave to leave or check to investigate: ",
    "options": {
        "check": {
            "lines": [
                "You move slow towards the source of the sound only to discover a baby. You make him stop crying and are left with two options, take him with you or leave him there."
            ],
            "prompt": "Type take to take him with you or bye to leave him there: ",
            "options": {
                "take": {
                    "lines": [
                        "You look for some more supplies then leave the store with your new friend.",
                        "On your way home you encounter a bunch on infected.",
                        "You can eiter fight them or sneak past them."
                    ],
                    "prompt": "Type fight to fight them or sneak to sneak past: ",
                    "options": {
                        "fight": {
                            "lines": [
                                "You fight the infected and you defeat them, but right before you deal the final strike one of them scratches your leg.",
                                "You run home.",
                                "You develop a strong fever and cannot treat yourself.",
                                "On your death bed, you hear the baby cry and think that because of you he will die too.",
                                "Later that night you die."
                            ],
                            "prompt": "press any key to exit"
                        },
                        "sneak": {
                            "lines": [
                                "You manage to sneak past the group of infected and you arrive home with, now, your baby.",
                                "You make an improvised cradle for him to sleep in.",
                                "You are tired after a long day and decide to go to sleep.",
                                "You fall asleep...",
                                ".",
                                ".",
                                ".",
                                "The end!"
                            ]
                        }
                    }
                },
                "bye": {
                    "lines": [
                        "You leave the store immediately so you don't meet any infected attracted by the baby.",
                        "Later that night you are overwhelmed by a feeling of guilt.",
                        "You can't take it and finally end your life.",
                        "While you're laying on the floor, in a pool of blood, you wonder why you didn't help that baby.",
                        "As your final moments drift away, you regret not helping the poor soul."
                    ],
                    "prompt": "press any key to exit"
                }
            }
        },
        "leave": {
            "lines": [
                "You leave the store immediately, and head home.",
                "On your way home you encounter a bunch on infected.",
                "You can eiter fight them or sneak past them."
            ],
            "prompt": "Type fight to fight them or sneak to sneak past: ",
            "options": {
                "fight": {
                    "lines": [
                        "You fight the infected and you defeat them, but right before you deal the final strike one of them scratches your leg.",
                        "You run home.",
                        "You develop a strong fever and cannot treat yourself.",
                        "Later that night you die."
                    ],
                    "prompt": "press any key to exit"
                },
                "sneak": {
                    "lines": [
                        "You manage to sneak past the group of infected and you arrive home.",
                        "You are tired after a long day and decide to go to sleep.",
                        "You fall asleep...",
                        ".",
                        ".",
                        ".",
                        "The end!"
                    ],
                    "prompt": "press any key to exit"
                }
            }
        }
    }
}

We then need to implement only the aforementioned algorithm to walk through the nested JSON file, as well as a few helper functions to load the JSON file and print the text.

Possible implementation:

#! /usr/bin/env python3
"""A console-based adventure game."""

from json import load
from pathlib import Path
from time import sleep
from typing import Any


DEFAULT_STORY = Path.cwd() / 'story.json'


def load_story(filename: Path = DEFAULT_STORY) -> dict[str, Any]:
    """Load the story from a JSON file."""

    with filename.open('rb') as file:
        return load(file)


def print_lines(lines: list[str], *, interval: float = 1.0) -> None:
    """Print the lines one-by-one for dramaturgic effect."""
    
    for line in lines:
        print(line)
        sleep(interval)


def play(story: dict[str, Any]) -> bool:
    """Play the story."""

    while story:
        print_lines(story.get('lines'))

        if (prompt := story.get('prompt')) is None:
            break

        response = input(prompt).lower()

        if (options := story.get('options')) is None:
            break

        if (story := options.get(response)) is None:
            print('Not a valid option. You lose.')
            return False

    return True


def main() -> None:
    """Run the program."""

    play(load_story())


if __name__ == '__main__':
    main()
\$\endgroup\$
7
  • \$\begingroup\$ I would personally prefer YAML, because it is easier to type. You can convert it to JSON before distributing the application. (Any JSON document is also valid YAML.) \$\endgroup\$
    – jiwopene
    Jun 10 at 16:33
  • 4
    \$\begingroup\$ How is this better than the if/elseif ladder? This is just an if/elseif ladder but in JSON. \$\endgroup\$
    – user253751
    Jun 10 at 17:44
  • \$\begingroup\$ I like this, I do the same in my projects - code is flexible, you can generate game in runtime, or pass it from server - cool! \$\endgroup\$ Jun 10 at 19:21
  • 2
    \$\begingroup\$ I agree with @user253751. A solution would be to name the states, and for each state transition only reference the name. This way loops can be implemented and code duplication (to some degree) removed. \$\endgroup\$
    – lukstru
    Jun 10 at 21:00
  • \$\begingroup\$ @user253751 For a larger project, it's one step improved, because the state machine is now manipulable data rather than merely implicit, so you can do a lot more with its structure; e.g. you can edit it or visualise it dynamically. For a smaller project, the extra indirection is probably bad because e.g. debugging technology is largely still built with C-like languages in mind. \$\endgroup\$ Jun 11 at 18:05
3
\$\begingroup\$

This could be a place to implement a state machine. For such simple program, I would use globals to store the game state. (Unless you want to host multiple sessions at once in one process.) I would place all the important things to a while loop and maintain a deque in which we store the current room information, possibly with forcing the user to go into some other room etc. Example (not an actual program, just a idea):

#
# State.
#

money = 100
life = 100

rooms = deque()


def next_room(room, **extras):
    rooms.append((room, extras))


#
# Rooms.
#

def r_welcome():
    print("Welcome to XyzzyGame.")
    if yesno("Do you want instructions?", True):
        next_room(r_instructions)
    next_room(r_main_hall)

def r_instructions():
    print("Lorem ipsum.")

def r_main_hall():
    print("You are in the main hall.")

    navigate({
        'out':   (r_pathway, {}),
        'north': (r_named_hall, {'name': 'Foo'}),
        'west': (r_named_hall, {'name': 'Bar'}),
    })

def r_named_hall(name='Some'):
    print(f'You are in a hall called “{name}”')

...

#
# Utility functions.
#

def question(text, default):
    line = input().lower()
    if len(strip(line)) == 0:
        line = default
    return line

def yesno(text, default):
    resp = question(text, 'yes' if default else 'no')
    return resp[0] == 'y'

def navigate(options):
    while True:
        direction = question("Where do you want to go now?", 'default')
        if direction not in options:
            print("I do not know this place. You can go to these places:")
            for option in options:
                print("  • " + option)
        else:
            (room, extras) = options[direction]
            next_room(room, **extras)
            break

#
# Entry point.
#

def main():
    next_room(r_welcome)  # Initial room.
    while len(rooms) > 0:
        (room, extras) = rooms.popleft()
        room(**extras)

main()

Yes, we could even go object-oriented, but for simpler games, this approach could be enough.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ This is a lot of code to write into the answer box without actually testing. Your code has some problems. if len(strip(line)) == 0: will fail with a NameError, r_pathway is not defined, and so on. \$\endgroup\$
    – AJNeufeld
    Jun 10 at 20:47
  • \$\begingroup\$ I said that in the answer that the snippet should only present the principle. It was not written with intention of making it error-free executable program and it is not definitely complete. \$\endgroup\$
    – jiwopene
    Jun 13 at 14:05
2
\$\begingroup\$

Just a little tip on this line :

start = input("Do you wish to begin your adventure? ").lower()

When I tried your game, I just hit enter and it unintentionally skipped the intro

if start == "yes":
     print("Good luck, traveler!")

     print("It's been 3 years since the outbreak... You are doing your daily scavenging for food when you hear a loud cry from the back of the store. Do you investigate or leave immediately?")

So just to be sure have the question be :

start = input("Do you wish to begin your adventure? yes/no").lower()

Event if the no answer isn't implemented and it let you play the game, at least the wanted answer is clear.

It is quite common practice among interactive text-only programs that hitting the Enter performs some default action. This does not apply to important decisions in the gameplay etc., but you can save the user a lot of typing in some special cases. For example, when you are in a fight (a situation where you can e.g. attack, flee, or bribe), you can make the last or preferred action the default.

\$\endgroup\$
1
\$\begingroup\$

Right off the bat, instead of strings maybe it'd be better to prompt your users to input the option number instead. It'd probably be cleaner, less prone to user error and allow you to run a Switch Case instead of nested if-elif statements. Hope this helps!

\$\endgroup\$
10
  • \$\begingroup\$ so basically, use a list so they can choose : 0 - go left 1- go right soimething like this? \$\endgroup\$ Jun 9 at 15:52
  • \$\begingroup\$ You realize that Python has no switch, and unless you're invoking the new structural pattern matching support that Python has no case either, right? \$\endgroup\$
    – Reinderien
    Jun 9 at 16:26
  • 1
    \$\begingroup\$ @ThodorisEvangelakos That is the new structural pattern matching support mentioned by Reinderien. It was just added in Python 3.10. See PEP 634, PEP 635, and PEP 636. \$\endgroup\$
    – AJNeufeld
    Jun 9 at 17:18
  • 4
    \$\begingroup\$ Right. I think this answer is salvageable (and I would be happy to up-vote it) if you reword to show either the structural pattern matching, or show the typical Python alternative which is dictionary/function reference lookup. \$\endgroup\$
    – Reinderien
    Jun 9 at 17:20
  • 1
    \$\begingroup\$ @Kingsley definitely not upcoming any more, 3.10 has been out for more than half a year now. I also disagree that they should stop adding new syntax, pattern matching is a very powerful way of expressing many different kinds of data handling logic, and though it originated in functional languages, even Java (a notoriously syntax-conservative language), added new syntax to allow for limited pattern-matching using switch-expressions. C# has also had them for a while now, not to mention Rust, which had them from the beginning. \$\endgroup\$ Jun 10 at 12:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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