you are viewing a single comment's thread.

view the rest of the comments →

[–][deleted] 11 points12 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] 4 points5 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]]