Created
January 16, 2026 18:42
-
-
Save Pinacolada64/60ddefc1c7f0b78225d158e0d548025d to your computer and use it in GitHub Desktop.
Character class functions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from dataclasses import dataclass | |
| import datetime | |
| import doctest | |
| import logging | |
| from enum import Enum, auto | |
| from random import randrange | |
| from typing import Sequence, Union, Any | |
| # some of this was--sadly--cribbed from Bing AI. | |
| # https://www.reddit.com/r/learnpython/comments/17oblv2/class_method_vs_instance_method/ | |
| # https://stackoverflow.com/questions/12179271/meaning-of-classmethod-and-staticmethod-for-beginner/12179752#12179752 | |
| # https://www.statuscake.com/blog/creating-a-character-in-python/ | |
| # https://stackoverflow.com/questions/60418497/how-do-i-use-kwargs-in-python-3-class-init-function | |
| @dataclass | |
| class Character(object): | |
| weapon_readied: Any | None | |
| def __init__(self, **kwargs): | |
| # logging.info("%s" % __classname__) | |
| self.name = None | |
| logging.info("Character.__init__()") | |
| """ | |
| A base class for in-game characters. Non-Player Characters (NPCs) have fewer attributes | |
| than Player Characters (PCs). | |
| :param name: Character's name | |
| :param inventory: stuff Character is carrying | |
| """ | |
| # self.__dict__.update(kwargs) # Store all the extra variables | |
| # logging.info(self.__dict__) | |
| # self.name = self.__dict__.get(kwargs['name'], None) | |
| if not hasattr(self, 'name'): | |
| self.name = 'default' | |
| self.gender = self.__dict__.get(kwargs['gender'], None) # female | male | |
| self.flag = self.__dict__.get(kwargs['flag'], None) | |
| # huge, large, big, man-sized, short, small, swift[, tiny {new}] [in descending size] | |
| self.size = self.__dict__.get(kwargs['size'], None) | |
| self.agility = self.__dict__.get(kwargs['agility'], None) # for Pixie, mainly? | |
| # inventory: | |
| self.max_inventory = self.__dict__.get(kwargs['max_inventory'], None) | |
| self.inventory = self.__dict__.get(kwargs['inventory'], None) # {"item": count} | |
| # attributes with defaults: | |
| self.horse_rider = self.__dict__.get(kwargs['horse_rider'], True) # bool = True | |
| self.weapon_readied = self.__dict__.get(kwargs['weapon_readied'], None) | |
| logging.info(self) | |
| def adjust_stats(self, current_stat: dict, stat_adjustments: dict, | |
| expert_mode=False) -> dict: | |
| """ | |
| :param current_stat: dict of current stats | |
| :param stat_adjustments: dict of adjustments to make | |
| :param expert_mode: if Expert Mode is True, | |
| will display a message about the stat change | |
| :returns dict: dict of modified statistics | |
| """ | |
| # iterate through stat_adjustments{}, adjusting each item in | |
| # current_stat by the given amount: | |
| modified_stats = current_stat | |
| logging.info(f"in: {current_stat}") | |
| descriptive = {'chr': 'influential', | |
| 'con': 'hearty', | |
| 'dex': 'agile', | |
| 'int': 'intelligent', | |
| 'str': 'strong', | |
| 'wis': 'wise', | |
| 'egy': 'energetic'} | |
| for k, v in stat_adjustments.items(): | |
| stat_name = k | |
| before = current_stat[stat_name] | |
| adj = stat_adjustments[stat_name] | |
| after = before - adj | |
| logging.info(f"{stat_name=} {before=} {after=}") | |
| modified_stats[stat_name] = after | |
| if self.flag["expert_mode"] is False: | |
| print(f"You feel {'more' if after > before else 'less'} " | |
| f"{descriptive[stat_name]}.") | |
| logging.info(f"out: {modified_stats}") | |
| return modified_stats | |
| def add_to_inventory(self, items: list | str): | |
| # TODO: finish this | |
| if len(self.inventory) == self.max_inventory: | |
| print(f"You can't carry any more.") | |
| # TODO: {csv_list(item_list)} | |
| def check_inventory(self, item: str): | |
| # check if item is in inventory, return True if it is | |
| return item in self.inventory | |
| class PlayerCharacter(Character): | |
| name = None | |
| def __init__(self, **kwargs): | |
| logging.info("PlayerCharacter.__init__()") | |
| self.__dict__.update(kwargs) # Store all the extra variables | |
| logging.info(f'{self.__dict__}, calling super().__init__') | |
| super().__init__(**kwargs) | |
| # additional attributes for player characters: | |
| # charisma, constitution, dexterity, intelligence, strength, wisdom | |
| self.stats = dict['cha', 0, 'con', 0, 'dex', 0, 'int', 0, 'str', 0, 'wis', 0] | |
| self.race = self.__dict__.get(kwargs['race'], None) | |
| self.class_name = self.__dict__.get(kwargs['class_name'], None) | |
| # class name is set by PlayerCharacter subclass: | |
| # self.class_name = class_name # can't use 'class' since that is a reserved Python keyword | |
| birthday: datetime.datetime.now() # was tuple: (day, month, year) | |
| # FIXME: am I using this correctly? | |
| ip_address: Sequence[int, int, int, int] | |
| """ | |
| NPCs won't have an IP address, since they're not connected to the game: | |
| ip_address: tuple[int: 0, int: 0, int: 0, int: 0] | |
| """ | |
| logging.info(self) | |
| @classmethod | |
| def create_character(cls): | |
| """A factory method that creates different instances of Character based on the given class and race.""" | |
| # TODO: replace with a more robust logic that checks the valid combinations of class and race | |
| # TODO: get each class's relative size (Pixie is smallest) | |
| logging.info("create_character: Starting character creation") | |
| # from SPUR.LOGON.S: | |
| print(""" | |
| Available classes: | |
| 1) Wizard 4) Paladin 7) Archer | |
| 2) Druid 5) Ranger 8) Assassin | |
| 3) Fighter 6) Thief 9) Knight | |
| """) | |
| name = cls.name | |
| if name is None: | |
| name = input("Name? ") | |
| new_character = Character(name=name, gender=None) | |
| char_class = 0 | |
| while 1 < char_class < 9: | |
| char_class = int(input("Please choose a class [1-9]: ")) | |
| new_character.class_name = CHAR_CLASSES[char_class - 1] | |
| print(""" | |
| Please choose a race: | |
| 1) Human 4) Elf 7) Dwarf | |
| 2) Ogre 5) Hobbit 8) Orc | |
| 3) Pixie 6) Gnome 9) Half-Elf | |
| """) | |
| char_race = 0 # (variable 'pr' in TLoS) | |
| while 1 < char_race < 9: | |
| char_race = int(input("Please choose a race [1-9]: ")) | |
| new_character.race = CHAR_RACES[char_race - 1] # FIXME: this should update attributes of the Character class | |
| logging.info(f'{char_race=} {char_class=}') | |
| new_character.max_inventory = 10 # zo in TLoS | |
| """ | |
| zo=10 ;..(zo=carrying capacity) | |
| if (pr=4) or (pr=7) or (pr=8) zo=9 | |
| if (pr=5) or (pr=6) zo=8 | |
| if pr=3 zo=7 | |
| """ | |
| if char_race == 1: # Human | |
| new_character.size = CHAR_SIZES[3] # "man-sized" | |
| elif char_race == 2: # Ogre | |
| new_character.size = CHAR_SIZES[1] # "huge" | |
| elif char_race == 3: # Pixie | |
| new_character.size = CHAR_SIZES[7] # "tiny" {new} | |
| new_character.agility: int = 7 // 10 # FIXME | |
| new_character.max_inventory = 7 | |
| return Pixie(gender=new_character.gender) | |
| elif char_race == 4: # Elf | |
| new_character.max_inventory = 9 | |
| pass | |
| elif char_race == 5: # "Hobbit" | |
| new_character.size = "short" | |
| new_character.max_inventory = 8 | |
| elif char_race == 6: # "Gnome" | |
| # TODO: no torch required underground to see | |
| new_character.max_inventory = 8 | |
| elif char_race == 7: # "Dwarf" | |
| new_character.max_inventory = 9 | |
| elif char_race == 8: # "Orc" | |
| new_character.max_inventory = 9 | |
| elif char_race == 9: # "Half-Elf" | |
| print("Dragons are great admirers of Half-Elfs for some reason.") | |
| # TODO: agility may go away | |
| return new_character | |
| def check_class_race_combination(self) -> None | bool: | |
| """ | |
| Check whether the player's choices of 'class_name' and 'race' attributes are | |
| acceptable according to game rules | |
| :return None: either class_name or race has not been set; player is still midway | |
| through character creation process | |
| bool: True: class and race combination is acceptable according to game rules | |
| bool: False: class and race combination is not acceptable | |
| """ | |
| # Newly-created characters won't have both 'class_name' & 'race' attributes set on the | |
| # first run; do a check for this: | |
| if self.class_name is None and self.race is None: | |
| # can't check for valid combinations if both aren't set yet | |
| return None | |
| else: | |
| pass | |
| # the following two definitions are bing ai stuff: | |
| @property | |
| def age(self) -> int: | |
| """A property that returns the age of the character based on the current date and the birthday.""" | |
| today = datetime.datetime.now() | |
| return today.year - self.birthday.year - ((today.month, today.day) < (self.birthday.month, self.birthday.day)) | |
| @age.setter | |
| def age(self, value: int): | |
| """A setter that updates the birthday of the character based on the given age.""" | |
| today = datetime.datetime.now() | |
| year = today.year - value | |
| self.birthday.replace(year=year) | |
| def __str__(self): | |
| return f"{self.name} is a {self.size} {self.gender} {self.class_name} {self.race}." | |
| class MagicUser(PlayerCharacter): | |
| """ | |
| A subclass of PlayerCharacter that represents a magic-user. | |
| In the character creation process, prompt for character gender comes before prompt for class | |
| so this should work. | |
| Females are Witches; males are Wizards. | |
| """ | |
| def __init__(self): | |
| super().__init__() | |
| self.class_name = "Wizard" if self.gender == "male" else "Witch" | |
| self.magic_user = True | |
| class Knight(PlayerCharacter): | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| self.class_name = "knight" | |
| self.horse_rider = True | |
| class Pixie(PlayerCharacter): | |
| """A tiny, winged class of flying creatures which can use magic to grant any fair wish.""" | |
| # A subclass of PlayerCharacter that represents a pixie. | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| self.size = "tiny" | |
| self.gender = self.__dict__.get(kwargs['gender'], None) | |
| self.race = CHAR_RACES[3] # "Pixie" | |
| self.agility *= 2 # double agility | |
| self.horse_rider = False # that would just be silly | |
| # TODO: can fly over open water (other classes need a boat) | |
| def check_class_race_combination(self): | |
| logging.info(f"check_class_race_combination: {self.class_name=}, {self.race=}: check class and race") | |
| pass | |
| class Hobbit(PlayerCharacter): | |
| """Hairy-toed, reluctant adventurers who live in the Shire.""" | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| self.race = "Hobbit" | |
| self.size = "short" | |
| self.age_range = list[300, 600] # FIXME | |
| # TODO: implement different age ranges for different classes | |
| @dataclass | |
| class Ally(Character): | |
| """NPCs which can join the player's party via either reputation or being charmed.""" | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| self.flags: list[str] | |
| self.max_inventory = 10 # FIXME | |
| class Item(object): | |
| def __init__(self, **kwargs): | |
| self.name = self.__dict__.get(kwargs['name'], None) | |
| self.number = self.__dict__.get(kwargs['number'], 1) # FIXME: auto-increment? | |
| self.flag = self.__dict__.get(kwargs['flag'], None) | |
| # the 'examined' flag will be true if the item has successfully been EXAMINEd. | |
| self.examined: bool = False | |
| # if the item is cursed, has been successfully EXAMINEd, and the item is LOOKed at, | |
| # the description will say e.g., "You see some cursed IVORY BONES." | |
| self.cursed: bool = False | |
| self.__dict__.update(kwargs) # Store all the extra variables | |
| def get(self): | |
| # called when getting items | |
| # TODO: check for full inventory | |
| pass | |
| def drop(self): | |
| # called when dropping items | |
| # TODO: check for water room flags, heavy things can sink | |
| # TODO: (except a dinghy, which would float) | |
| pass | |
| def is_in_inventory(self, item): | |
| return item in self.inventory | |
| def look(self, player: PlayerCharacter): | |
| # TODO: finish this - pass Player object so if either Admin or Dungeon Master flags are True, | |
| # or Debug flag is True, will print "object_name [<obj_type> #<id>]" | |
| if player.elevated_privs() or player.query_flag(PlayerFlags.DEBUG_MODE): | |
| # will print | |
| return f"The {self.name} {f'[#{self.number}]' if self.number else ''} " \ | |
| f"{'are ' if self.name.endswith('s') else 'is '}" \ | |
| f"{'an ' if self.name.startswith('aeiou') else 'a '}" \ | |
| f"{'cursed ' if 'cursed' in self.flag else ''}" \ | |
| f"item." | |
| def __str__(self): | |
| # TODO: finish this - pass Player object so either Admin flag or owning the item | |
| # will print | |
| return f"The {self.name} {f'[#{self.number}]' if self.number else ''} " \ | |
| f"{'are ' if self.name.endswith('s') else 'is '}" \ | |
| f"{'an ' if self.name.startswith('aeiou') else 'a '}" \ | |
| f"{'cursed ' if 'cursed' in self.flag else ''}" \ | |
| f"item." | |
| class WeaponClass(Enum): | |
| BASH_SLASH = auto() | |
| POLE_RANGE = auto() | |
| POKE_JAB = auto() | |
| class WeaponFlags(Enum): | |
| class Weapon(Item): | |
| """Offense weapon such as a sword, etc. which tries to inflict damage to an ememy.""" | |
| def __init__(self, **kwargs): | |
| # should init name, number: | |
| for k, v in kwargs.items(): | |
| print(f"{k}: {v}") | |
| super().__init__(**kwargs) | |
| # super().__init__(number=randrange(1, 65535), name="Weapon", flags=None) | |
| self.weapon_class = self.__dict__.get(kwargs['weapon_class'], WeaponClass.BASH_SLASH) # 'bash/slash', etc | |
| self.stability = self.__dict__.get(kwargs['stability'], None) | |
| # set in super(): | |
| # flags: list[str] # 'cursed', etc | |
| self.handedness = self.__dict__.get(kwargs['handedness'], 1) | |
| # 1=single-handed, 2=double-handed (can't wield a shield if already using a double-handed weapon) | |
| # TODO: 0 might be handy for future expansion; look, no hands ma! | |
| self.defense_bonus = self.__dict__.get(kwargs['defense_bonus'], None) | |
| self.percent_left = self.__dict__.get(kwargs['percent_left'], None) | |
| self.readied: bool = False | |
| # defaults: | |
| # self.name = kwargs['name'] | |
| # number: int = randrange(1, 65535) | |
| def ready(self): | |
| # handle READYing an iten to USE it | |
| if self.readied: | |
| print(f"The {self.name} is already readied.") | |
| else: | |
| self.readied: bool = True | |
| print(f"You ready the {self.name}.") | |
| def unready(self): | |
| # handle UNREADYing a weapon by putting it in your pack | |
| if not self.readied: | |
| print("The {self.name} is already unreadied.") | |
| else: | |
| self.readied: bool = False | |
| print(f"You put the {self.name} in your pack.") | |
| def get_offensive_bonus(self, player_character: PlayerCharacter): | |
| """ | |
| A method that calculates the offensive bonus for the character based on the weapon class, | |
| and probably some PlayerCharacter attributes, which are to be determined. | |
| Should only be called at the beginning of combat. | |
| """ | |
| # just a sample interaction: | |
| if player_character.class_name == "pixie": | |
| if player_character.weapon_readied == "magic wand": | |
| self.defense_bonus += 10 # FIXME | |
| else: | |
| if "cursed" in player_character.weapon_readied.flags: | |
| self.defense_bonus -= 10 | |
| self.defense_bonus -= 5 | |
| def heal(self): | |
| # heal damage | |
| pass | |
| def magic(self): | |
| # handle any magical enchantments here | |
| pass | |
| def ready(self): | |
| # handle READYing a weapon to USE it | |
| pass | |
| def take_damage(self, damage_range: int): | |
| # TODO: check for enchantments, gauntlet etc. which reduce the damage taken | |
| # based on the weapon class | |
| try: | |
| damage = randrange(damage_range) | |
| self.percent_left -= damage | |
| except ValueError: | |
| logging.info(f"take_damage: {damage=}") | |
| def unready(self): | |
| # handle unreadying weapon | |
| pass | |
| def __str__(self): | |
| _ = f"The {self.name} " \ | |
| f"{f'[#{self.number}] ' if self.number else ''}" \ | |
| f"is a {'single' if self.handedness == 1 else 'double'}-handed weapon." | |
| return _ | |
| class Shield(Weapon): | |
| """shields which deflect blows from harming the person who carries it""" | |
| def __init__(self): | |
| super().__init__() | |
| self.defense_bonus = 10 | |
| self.percent_left = 100 | |
| self.alignment = "neutral" # ['good', 'neutral', 'evil'] | |
| def bash(self): | |
| """bash monster in same space as Character with shield""" | |
| pass | |
| def get_defense_bonus(self, char: PlayerCharacter): | |
| """A method that calculates the defensive bonus for the character based on the weapon class.""" | |
| self.defense_bonus = 50 | |
| if char.class_name == "pixie": | |
| if char.weapon_readied == "magic wand": | |
| self.defense_bonus += 10 # FIXME | |
| else: | |
| if "cursed" in char.weapon_readied.flags: | |
| self.defense_bonus -= 10 | |
| self.defense_bonus = 5 | |
| if self.weapon_class == "bash/slash": | |
| pass | |
| @dataclass | |
| class Spell(Item): | |
| spell: dict[str: list[str, int, int]] # {"spell_name": ['type', chance_to_cast, charges]} | |
| def __str__(self): | |
| for spell_name, spell_info in self.spell.items(): | |
| # (###x) 12345678901234567890: ###% | |
| # ( 10x) test_spell..........: 100% | |
| """ | |
| >>> test_spell = Spell(dict("test_spell", ["type", 100, 10]) | |
| >>> print(test_spell) | |
| ( 10x) test_spell..........: 100% | |
| """ | |
| spell_type, chance_to_cast, charges = spell_info | |
| # TODO: add spell_type output | |
| return f"({charges:>3}x) {spell_name.ljust(20, '.')}: {chance_to_cast:>3}%" | |
| @dataclass | |
| class StormWeapon(Weapon): | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| self.intelligence = self.__dict__.get('intelligence', 0) | |
| # spells it could cast; can be [None | list[Spell,...]]: | |
| self.spells = self.__dict__.get('spells', None) | |
| def __str__(self): | |
| _ = f"Int: {self.intelligence}\n" | |
| if self.spells: | |
| for spell in self.spells: | |
| # this should use the Spell.__str__ method: | |
| _ += f"{spell}\n" | |
| return _ | |
| @dataclass | |
| class Spellbook(Spell): | |
| """Spellbook is made up of pages with one spell per page""" | |
| pages: list[Spell] | |
| def __str__(self): | |
| _ = "" | |
| for spell in self.pages: | |
| # this should use the Spell.__str__ method: | |
| _ += f"{spell}\n" | |
| return _ | |
| @dataclass | |
| class Monster(Character): | |
| name: str | |
| flags: list | |
| @dataclass | |
| class Party(PlayerCharacter): | |
| # TODO: list of party members, containing: Character, Ally or Monster | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| members: Sequence[Character | Ally | Monster] | |
| if __name__ == '__main__': | |
| # initialize doctest | |
| doctest.testmod(verbose=True) | |
| # initialize logging | |
| logging.basicConfig(level="INFO") | |
| # class name will be 'Wizard' or 'Witch' depending on character's gender | |
| # first element CHAR_CLASSES[] is None; that makes the element numbers for the remaining items | |
| # match up with TLoS pc/pr numbers 1-9, not 0-8 | |
| CHAR_CLASSES = [None, "Wizard", "Druid", "Fighter", "Paladin", "Ranger", "Thief", "Archer", "Assassin", "Knight"] | |
| # same reasoning with CHAR_RACES[]: | |
| CHAR_RACES = [None, "Human", "Ogre", "Pixie", "Elf", "Hobbit", "Gnome", "Dwarf", "Orc", "Half-Elf"] | |
| # 'tiny' is new (was added by Pinacolada), for Pixie player race, possibly other monsters: | |
| CHAR_SIZES = [None, "huge", "large", "big", "man-sized", "short", "small", "swift", "tiny"] | |
| # initialize some stuff | |
| lance = Weapon(number=25, name="Lance", stability=4, flag=None, handedness=2, | |
| defense_bonus=30, weapon_class="bash/slash", percent_left=20) | |
| print(lance) | |
| ivory_bones = Item(number=38, name="ivory bones", flag='cursed') | |
| print(ivory_bones) | |
| # NOTE: this calling convention is not final: | |
| rulan = PlayerCharacter(name="Rulan", gender="male", flag='debug', | |
| class_name="tinkerer", race="Polarfuchs", | |
| size="man-sized", agility=20, | |
| stats={'str', 4, 'int', 10}, | |
| ip_address="127.0.0.1", | |
| max_inventory=1, | |
| inventory=dict["inventory", dict['ivory_bones', 4]], | |
| horse_rider=True, weapon_readied="lance") | |
| print("1:") | |
| print(rulan) | |
| rulan.create_character() | |
| rulan.add_to_inventory([lance, ivory_bones]) | |
| print("2:") | |
| print(rulan) | |
| # print(rulan.stats[]) | |
| # test instantiating a subclassed Character: | |
| shaia = Pixie(name="Shaia", gender="female", stats={'con': 10, 'int': 25}) | |
| print("1:") | |
| print(shaia) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment