Skip to content

Instantly share code, notes, and snippets.

@Pinacolada64
Created January 16, 2026 18:42
Show Gist options
  • Select an option

  • Save Pinacolada64/60ddefc1c7f0b78225d158e0d548025d to your computer and use it in GitHub Desktop.

Select an option

Save Pinacolada64/60ddefc1c7f0b78225d158e0d548025d to your computer and use it in GitHub Desktop.
Character class functions
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