Класс персонажа (для всех персонажей) для текстовых приключенческих

Я расширил множество классов персонажей, ибо в прошлый раз я разместил вопрос. Это становится довольно большой и запутанный. Любую обратную связь, чтобы тон его вниз или структура лучше - особенно с построением функций.

import common
import item_class
import random
import time
stop_message = "No. Stop. Try again."
attribute_names = ['Strength', 'Perception', 'Endurance',
                   'Charisma', 'Intelligence', 'Agility', 'Luck']
monster_dict = {"Imp": {"species": "Imp", "health": 20, "attack": 5},
                "Salamander": {"species": "Salamander", "health": 10, "attack": 20},
                "Ogre": {"species": "Orge", "health": 100, "attack": 10}}
# Values will need to be tweaked later for balance
# Kinda stuck with basic home values
classes = {"Knight": [9, 1, 9, 1, 1, 4, 1],
           "Seer": [1, 6, 4, 1, 6, 1, 6],
           "Rogue": [4, 1, 1, 6, 1, 6, 6]}
build_options = {
    "Predestinated": "Choose from balanced builds favourable to a class.",
    "Random": "Let the gods roll the dice.",
    "Custom": "For the MIN/MAXers."}

class BaseCharacter():

    def __init__(self, name, species=None, default=0, **stats):

        # Basic info
        self.name = name
        self.species = species

        # Basic attributes
        self.health = stats.get('health', 20)
        self.speed = stats.get('speed', 10)
        self.attack = stats.get('attack', 20)
        self.critical_attack = stats.get('critical_attack', 0.01)
        self.defense = stats.get('defense', 0.1)
        self.evasiveness = stats.get('evasiveness', 0.01)

        # Bonuses and other attributes
        # What's Fallout?
        self.Strength = stats.get('Strength', default)
        self.Perception = stats.get('Perception', default)
        self.Endurance = stats.get('Endurance', default)
        self.Charisma = stats.get('Charisma', default)
        self.Intelligence = stats.get('Intelligence', default)
        self.Agility = stats.get('Agility', default)
        self.Luck = stats.get('Luck', default)
        # Used to raise monster difficulty along with player
        self.difficulty_modifier = 1

    def description():
        # Describe the character's profile here.

class Player(BaseCharacter):
    def __init__(self, name, predestined_stats=None, build=True, specie='human'):
        super().__init__(name, species='human')
        # In case the player controls NPCs which don't need building
        # Maybe create a separate class for NPCs? Even if they are almost
        # the same as the Player class.
        if build:
            build_attributes(self, predestined_stats=predestined_stats)

        # How stat bonuses affect basic attributes
        self.health += self.Endurance * 1.5
        self.speed += self.Agility
        self.attack += self.Strength * 1.5
        # Chance of landing a critical hit, which is double the attack
        self.critical_attack = (self.Perception + (self.Luck / 5)) / 300
        # Defense decreases damge taken by 25& at level 10
        self.defense = self.Endurance / 40
        self.evasiveness = (self.Perception + self.Agility +
                            (self.Luck / 5)) / 220
        # At 1, the modifier will be 1x
        # At 10, the modifier will be 2x
        self.levelling_modifier = (self.Intelligence + 8) / 9
        # Easy displaying of stats to the player, in the form of:
        # [S, P, E, C, I, A, L]
        self.attribute_list = [getattr(self, name)
                               for name in attribute_names]
        self.level = 0

        # Inventory: 5 items and one weapon
        # Will be implemented later
        # Ignore for now
        self.item_one = item_class.ItemBase()
        self.item_two = item_class.ItemBase()
        self.item_three = item_class.ItemBase()
        self.item_four = item_class.ItemBase()
        self.item_five = item_class.ItemBase()
        self.weapon = item_class.Weapon()

    def inventory_initiate(self):
        # Ignore this
        self.inventory = (
            [self.item_one, self.item_two,
             self.item_three, self.item_four,
             self.item_five, self.weapon])

class Monster(BaseCharacter):

    def __init__(self, attributes, name=None, default=0, **stats):
        super().__init__(name, default, **stats)
        for attribute, value in attributes.items():
            setattr(self, attribute, value)
        self.health += self.Endurance * 0.5 * self.difficulty_modifier
        self.attack += self.Strength * 0.5 * self.difficulty_modifier
        # Consider allowing monsters to have these as well.

def custom_build(attribute, points, position):
    print(f"You have {points} points left.")
    while True:
        stat = common.error_handle_int(">>>")
        # Boundary for cusotmizing stats
        if 1 <= stat <= 10:
            if (points - stat) < (6 - position):
                points -= stat
            return stat, points

# Another way to assign points, if the user chooses to.
# or is too lazy to do so.

def random_build(max_points, player, filling_in=False):
    while True:
        print("Random assignment of points in process!")
        points = max_points
        while points > 0:
            attribute = random.choice(attribute_names)
            value = getattr(player, attribute)
            if value <= 10 and points > 0:
                setattr(player, attribute, value + 1)
                points -= 1
        attribute_output = {name: getattr(player, name)
                            for name in attribute_names}
        print(f"Here are your stats: {attribute_output}")
        if not filling_in:
            print("Roll again?")
            if common.check_in_list() in ['Yes', 'Y']:
                [setattr(player, name, 0) for name in attribute_names]
                return points

def predestined_build(player, predestined_stats=None):
    while True:
        if predestined_stats is None:
            print("Here are the builds:")
            [print(f"{name}: {build}") for name, build in classes.items()]
            print("Choose your build.")
            choice = common.check_in_list(classes.keys())
            print("Are you sure?")
            if common.check_in_list() in ['No', 'N']:
            predestined_stats = classes[choice]
        [setattr(player, attribute, value) for attribute, value in
         dict(zip(attribute_names, predestined_stats)).items()]
        return 0

def build_attributes(player, max_points=26, predestined_stats=None):
    if predestined_stats is not None:
        predestined_build(player, predestined_stats)

    print(f"There are currently {len(build_options)} ways \
of building your stats.\n")
    [print(f"{option}: {explain}")
     for option, explain in build_options.items()]

    while True:
        points = max_points
        print("\nChoose your method.")

        # This is just to dsplay attributes in process of builidng them.
        # Maybe this is batter:
        # [print(f"{attribute[:3]} = {getattr(player, attribute)})
        # for attribute in attribute_names]
        # See 2nd answer in previously posted question:
        # https://codereview.stackexchange.com/questions/189654/stats-classes-for-a-text-based-adventure/189748#189748
        attribute_output = {name: getattr(player, name)
                            for name in attribute_names}
        choice = common.check_in_list(build_options.keys())
        print("Are you sure?")
        if common.check_in_list() in ["No", "N"]:
        if choice == "Predestinated":
            points = predestined_build(player, predestined_stats)
        elif choice == "Random":
            points = random_build(points, player)
        elif choice == "Custom":
            for position, name in enumerate(attribute_names):
                print(f"Stats: {attribute_output}")
                value, points = custom_build(
                    name, points, position)
                setattr(player, name, value)
                # Updating the displaying dictionary
                attribute_output[name] = value
        attribute_output = {name: getattr(player, name)
                            for name in attribute_names}
        print(f"You're stats are: {attribute_output}")

        # Inform player if they still have points left.
        # And also random reallocation of said remainding points
        if points > 0:
            print(f"You still have {points} points left")
            print("Do you want rest of the points be randomly \
                assigned to the different attributes?")
            if common.check_in_list() in ['Yes', 'Y']:
                random_build(points, player, filling_in=True)
                attribute_output = {name: getattr(player, name)
                                    for name in attribute_names}
                print(f"You're stats are: {attribute_output}")
        print("Do you wish to change your stats?")
        if common.check_in_list().title() in ['Yes', 'Y']:
            [setattr(player, name, 0) for name in attribute_names]

# Stats Debug
if __name__ == "__main__":
    while True:
        print("Choose which class to check.")
        choice = input(">>>")
        if choice == "Monster":
            Nob = Monster(monster_dict['Imp'], "Nob", Endurance=20)
            print(f"You encounter {Nob.name}!")
            print(f"It is an {Nob.species} with {Nob.health} health \
and {Nob.attack} attack!")
        elif choice == "Player":
            Bob = Player("Bob")
        elif choice == "Story":
            Fred = Player("Fred", predestined_stats=[0, 0, 0, 0, 0, 0, 13])
            print(f"""{Fred.name} is a {Fred.species} with these stats: {Fred.attribute_list}
His inventory is as follows: {Fred.inventory}.""")

Я не дал много думал на баланс за бонусы/атрибутов; это просто набросок того, что могло бы выглядеть.

Общий Модуль:

import sys
stop_message = "No. Stop. Try again."
default = ['yes', 'y', 'no', 'n']

def check_in_list(list_to_check_in=default, return_boolean=False):
    while True:
        input_to_check = "".join(
            [letter for letter in input(">>>") if str.isalnum(letter)]).title()
        if input_to_check in ["Exit", "Stop"]:
        elif input_to_check not in [x.title() for x in list_to_check_in]:
            print(f"The options are: {[x.title() for x in list_to_check_in]}")
        if return_boolean:
            return True
            return input_to_check

def error_handle_int(message_to_display=">>>"):
    while True:
        value = input(message_to_display)
        if value.lower() == "stop":
            value = int(value)
            return value
        except ValueError:

if __name__ == "__main__":
    test_list = ("yes", "no", "stop")
    test_number = error_handle_int()
    test_input = check_in_list(test_list, return_state="upper")
    another_test_input = check_in_list(test_list, True)
    yet_another_test_input = check_in_list(return_boolean=True)