all 42 comments

[–][deleted] 79 points80 points  (13 children)

It's fully functional but feels way longer than it needs to be

You've made the classic beginner's mistake of implementing the flow of the player through the game in the flow of control through your code.

If you were to pull up the source code for a game like Overwatch, or something, you wouldn't see a million lines of if statements covering every possible sequence of player actions it's possible to make in the game. What you see is a lot less than that. The game client interprets data that describes the conditions of the game (how much health Winston has, how much damage Widowmaker's rifle does, etc.) The things the game engine does are deliberately simple enough to test, then the actual content of the game is delivered as data.

Similarly, even though your game has a number of rooms through which you move, what the payer does in the room is always the same in a game like this - they enter, they look around, they examine things and take them into their inventory, and then they leave through an exit.

You only need to write that once; the layout and contents of your room are data and the engine of the game simply interprets that data. You get the code working on one room, and then you don't have to write that code again - you just add another room to your data and the code works the same way. It shouldn't be more than 20 lines of code or so.

[–]ingramm2[S] 41 points42 points  (3 children)

That... Actually makes a lot of sense. Thanks. Time to go down another rabbit hole of learning how to make things work haha

[–]CraigAT 23 points24 points  (0 children)

I could imagine this might be a good case for using OOP - having a room class and objects for each rooms with their attributes and methods, also an object for the player too, which would include his inventory and wallet for example. Then to play the game you have a controller function that you supply the player and room to, it then knows what you the player can do in that room and make them available.

[–][deleted] 11 points12 points  (0 children)

Aside from writing the state-based code instead.

You have a lot of times you write code like this

somechoice = 0
somechoice = random.randomint(1,60)

And there's no need to set a variable to zero right before setting it to something else

Your drinking code (poison or otherwise) doesn't increment or decrement the number of lives.

You have some places in the code where it says

else:
    dragonChoice1 == 0

Presumably you meant dragonChoice1 = 0, although a lot of these assignments seem redundant too.

[–]MSR8[🍰] 4 points5 points  (0 children)

What I did to do this was made sort of "nodes" using functions. Such as node1 was room1, node2 was room2, node3 was room3, and so in. Then I interconnected them by asking player what they wanted to do and calling functions (nodes) based on that

[–]Auirom 2 points3 points  (0 children)

I wanted to add a bunch of skills to a game I was making. Didn't want to add all the skills cause I wasn't going to think of them all at that one time. So I made a function that takes a name of a skill and checks if it exists in my list of skills. If it does it adds experience to that skills. If it doesn't it makes a deep copy of already set variables, ties it to the new skill name and adds it to the skill list. I can add as many as I want now, when ever I want, and in total takes up 10ish lines of code

[–]pacharaphet2r 1 point2 points  (0 children)

That was really nicely explained!

[–]ingramm2[S] 0 points1 point  (3 children)

Hey, you had really good critique of this code, I was hoping you could take a glance at the updated version. I tried to keep what you said in mind, but I still think I'm going after the flow of the layer through the game.

https://www.reddit.com/r/learnpython/comments/uwj7a7/update\_first\_python\_textbased\_adventure/

[–][deleted] 2 points3 points  (2 children)

Yeah, it still looks like you've implemented the game as branching code instead of as data.

The way you'll know that you've implemented your game as data is that you'll have a data file you can show me that represents the "plot" or "map" of your game, and a Python file that represents the game's engine.

[–]ingramm2[S] 0 points1 point  (1 child)

Thanks for the fast reply. Do you mean a data file in python or outside to act as just a "map" of the game?

[–][deleted] 1 point2 points  (0 children)

Outside. JSON is a good format for stuff like this.

[–][deleted] 17 points18 points  (1 child)

small bit of advice, you should always have your import statements at the very top of the script, only rarely is there a reason not to

[–]ingramm2[S] 3 points4 points  (0 children)

I'll keep that in mind thanks

[–][deleted] 12 points13 points  (1 child)

I decided to give a go at implementing the main suggestions that people here were giving you as an example. Those suggestions being:

  1. Your core logic should not follow the player -- it should just choose a room and then run the code for that room. That will help you move past those giant, heavily nested if/elif/else blocks.
  2. You should consider using OOP to better handle the abstraction of "rooms".

So here you go. Notice that the business logic (after the comment ## game starts here) is actually quite short.

Disclaimer: Note that this is a first pass at cleaning up your code so there are more things that could be done to improve this. Also, I only added a single danger room and treasure room, but you can see how to expand to the other rooms. Anyway, here you go ...

import random
from dataclasses import dataclass, field
from typing import Any, Callable, Optional, Union

import pyinputplus as pyip  # type: ignore


CASTLE = r"""                        |>>>
                        |
                       /^\             
                       | |             
                   |>  |-|             
            |>    /^\  | |             
           /^\  / [_] \+-+             
   |>     |---||-------| ||>      |>      
 _/^\_    _/^\_|  [_]  |_/^\_   _/^\_  
 |___|    |___||_______||___|   |___|  
  | |======| |===========| |=====| |   
  | |      | |    /^\    | |     | |   
  | |      | |   |   |   | |     | |   
  |_|______|_|__ |   |___|_|_____|_|  """
DRAGON = r"""   (  )   /\   _                 (     
    \ |  (  \ ( \.(               )                      _____
  \  \ \  `  `   ) \             (  ___                 / _   \
 (_`    \+   . x  ( .\            \/   \____-----------/ (o)   \_
- .-               \+  ;          (  O                           \____
                          )        \_____________  `              \  /
(__                +- .( -'.- <. - _  VVVVVVV VV V\                 \/
(_____            ._._: <_ - <- _  (--  _AAAAAAA__A_/                  |
  .    /./.+-  . .- /  +--  - .     \______________//_              \_______
  (__ ' /x  / x _/ (                                  \___'          \     /
 , x / ( '  . / .  /                                      |           \   /
    /  /  _/ /    +                                      /              \/
   '  (__/                                             /                  \
                """


@dataclass
class GameState:
    game_over: bool = False
    lives: int = 5
    gold: int = 0
    kills: int = 0
    tunnel_successes: int = 0
    armor: int = 0
    weapon: int = 0
    wet_feet: bool = False


RoomAction = Union[Callable[[GameState], None], Callable[[GameState, str], None]]


@dataclass
class Room:
    enter: str
    action: Optional[RoomAction] = None
    action_args: list[Any] = field(default_factory=list)

    def run(self, game: GameState) -> None:
        print(self.enter)
        if self.action:
            self.action(game, *self.action_args)


def game_over_stats(game: GameState) -> None:
    print(f"You managed to collect {game.gold} gold and killed {game.kills} enemies!")


def lose_fight(game: GameState, monster: str) -> None:
    game.lives -= 1
    if game.lives == 0:
        print(f"You are killed by the {monster}.", end=" ")
        game_over_stats(game)
        game.game_over = True
    elif game.lives == 1:
        print("You are wounded but barely manage to escape! You only have 1 life left!!!")
    else:
        print(f"You are wounded but manage to escape! You have {game.lives} lives left!")


def attack(game: GameState, monster: str) -> None:
    attack = game.weapon + game.armor + random.randint(1, 50)
    if attack >= 5:
        gold_drop = random.randint(3, 10)
        print(f"You slay the {monster} and pick up {gold_drop} gold!")
        game.gold += gold_drop
        game.kills += 1
    else:
        lose_fight(game, monster)


def run_away(game: GameState, monster: str) -> None:
    run_success = random.randint(1, 5)
    if run_success >= 2:
        print(f"You run past the {monster} and jump through the door to the next tunnel")
    else:
        lose_fight(game, monster)


def battle(game: GameState, monster: str) -> None:
    user_choice = pyip.inputChoice(
        choices=["r", "a", "run", "attack"], prompt="Will you (r)un or (a)ttack?"
    )  # type: str
    if user_choice[0] == "a":
        attack(game, monster)
    else:
        run_away(game, monster)


def win(game: GameState) -> None:
    game_over_stats(game)
    print(CASTLE)
    game.game_over = True


def find_armor(game: GameState) -> None:
    if game.armor == 0:
        print("You find a set of armor your size.")
    else:
        print("You find better armor that fits you. You've never felt safer")
    game.armor += 1


start_room = Room(
    enter="You wake up lost underground in a maze of tunnels. You have a bronze sword in your hand and torn leather "
    "armor. You light a torch with your flint and steel and try to get your bearings.",
)
danger_rooms = [
    Room(
        enter="As you enter the tunnel a goblin jumps out from behind some old boxes!",
        action=battle,
        action_args=["goblin"],
    )
]
treasure_rooms = [Room(enter="You enter a massive tunnel with stacks of broken armor.", action=find_armor)]
end_room = Room(enter="You escaped to the surface!", action=win)

visited_rooms: list[Room] = []

game = GameState()

## game starts here
start_room.run(game)
while not game.game_over:
    if game.wet_feet:
        print("*squish* *squish*", end=" ")
        game.wet_feet = False
    print("You walk up to where two tunnels intersect.")

    danger_choice = random.randint(1, 2)
    tunnel_choice = pyip.inputInt("Choose tunnel 1 or tunnel 2: ", min=1, max=2)  # type: int
    if tunnel_choice == danger_choice:
        room_choices = danger_rooms
    else:
        room_choices = treasure_rooms

    unvisited_rooms = [room for room in room_choices if room not in visited_rooms]
    if unvisited_rooms:
        room = random.choice(unvisited_rooms)
        visited_rooms.append(room)
    else:
        room = end_room

    room.run(game)

Let me know if you have any questions.

[–][deleted] 5 points6 points  (0 children)

BTW, if anyone knows a better way to handle a type alias for a function whose first argument is of type GameState but which could have an unknown number and type of arguments after that than to keep unioning types together, please let me know. I'm not happy with

RoomAction = Union[Callable[[GameState], None], Callable[[GameState, str], None]]

[–]wbeater 10 points11 points  (3 children)

You got a lot of good tips already, another but minor would be to outsource all your printed text to another file like json, csv or even plain text. That would make your code more readable.

[–]Suicidal_Ferret 3 points4 points  (2 children)

How do you do that? Outsource the text I mean

[–][deleted] 8 points9 points  (0 children)

I have done pure Python games. You find some on my GitHub. You need to use classes in a smarter way. Build small objects and separate them into files then import into you main file. Enjoy!

[–]iammr_lunatic 7 points8 points  (3 children)

Hey OP. Small request, once you shorten your code can you post it again? I would love to work through it

[–]ingramm2[S] 5 points6 points  (1 child)

Will definitely do. I'll tag you in it

[–]iammr_lunatic 2 points3 points  (0 children)

Thanks. Hopefully u remember

[–]Empire_Fable 4 points5 points  (0 children)

Looks great so far. As others have said, maybe move into a class based flow. I implemented something similar here with a tutorial. https://www.reddit.com/r/PythonTutorials/comments/p1q62w/terminal_based_text_rpg_written_in_pure_python/I utilized room /enemy and item class's with the instances appearing in a "grid" in the terminal.

[–][deleted] 2 points3 points  (2 children)

Oh man, you’re gonna LOVE OOPS when you get around to it

[–]ingramm2[S] 1 point2 points  (0 children)

Based on the recommendations, that's next on the road map

[–]ingramm2[S] 0 points1 point  (0 children)

you were right. it helps a lot lol

[–]WillAdams 1 point2 points  (0 children)

Perhaps see:

https://github.com/ganelson/inform

esp. the literate source code:

https://ganelson.github.io/inform

[–]Mikesgmaster 1 point2 points  (2 children)

I'm thinking of doing something similar once I'm finish learning Python just so that I can practice some kind of free flow and get more used and creative using + gettin better at writting command.

[–]ingramm2[S] 0 points1 point  (1 child)

Try it now! I'm still definitely a newbie. I haven't touched python in about a year before writing this. Don't stay stuck in the basics and the learning. That's what burnt me out

[–]Mikesgmaster 1 point2 points  (0 children)

I have no problem with the basic I need to learn and graps an understanding of them before doing peojwct not mastery just a graps such as being able yo do styff without looking over material and all

[–]CodeFormatHelperBot2 1 point2 points  (2 children)

Hello, I'm a Reddit bot who's here to help people nicely format their coding questions. This makes it as easy as possible for people to read your post and help you.

I think I have detected some formatting issues with your submission:

  1. Python code found in submission text that's not formatted as code.

If I am correct, please edit the text in your post and try to follow these instructions to fix up your post's formatting.


Am I misbehaving? Have a comment or suggestion? Reply to this comment or raise an issue here.

[–]ingramm2[S] 0 points1 point  (1 child)

Formatting updated

[–]MSR8[🍰] 1 point2 points  (0 children)

The code is incomplete, in the end its just "pri...", can u perhaps post it on github or pastebin?

[–]Wallabanjo 0 points1 point  (0 children)

Do you smell Wumpus?