Last active
June 10, 2025 05:46
-
-
Save Pinacolada64/cdd0ae630eddfd2dd3030dd12099f70d to your computer and use it in GitHub Desktop.
Uses dict unpacking and .get()
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
import logging | |
import random | |
from dataclasses import dataclass | |
from datetime import datetime, timedelta | |
from enum import Enum, auto, StrEnum | |
import textwrap | |
import doctest | |
class Translation(Enum): | |
COMMODORE = auto() | |
ANSI = auto() | |
class Color(Enum): | |
BLACK = auto() | |
WHITE = auto() | |
class PlayerStat(StrEnum): | |
CHR = "Charisma" | |
CON = "Constitution" | |
DEX = "Dexterity" | |
EGY = "Energy" | |
INT = "Intelligence" | |
STR = "Strength" | |
WIS = "Wisdom" | |
''' | |
def __str__(self) -> str: | |
""" | |
Returns a string representation of the player statistic. | |
Returns: | |
str: A formatted string containing the stat name | |
""" | |
return f"<PlayerStat.{self.value}>" | |
''' | |
@dataclass | |
class Client: | |
name: str = "Generic Client" | |
rows: int = 40 | |
columns: int = 25 | |
translation: Translation = Translation.ANSI | |
# colors for [bracket reader] text highlighting on C64/128: | |
text: Color = Color.WHITE | |
highlight: Color = Color.BLACK | |
background: Color = Color.BLACK | |
border: Color = Color.BLACK | |
def __str__(self): | |
"""Return a formatted string showing all client settings""" | |
settings = f"""Client Settings: | |
{'Name: '.rjust(17)} {self.name.title()} | |
{'Rows:'.rjust(17)} {self.rows} | |
{'Columns:'.rjust(17)} {self.columns} | |
{'Translation:'.rjust(17)} {self.translation.name.title()} | |
{'Text Color:'.rjust(17)} {self.text.name.title()} | |
{'Highlight Color:'.rjust(17)} {self.highlight.name.title()} | |
{'Background Color:'.rjust(17)} {self.background.name.title()} | |
{'Border Color:'.rjust(17)} {self.border.name.title()} | |
""" | |
return textwrap.dedent(settings) | |
class Gender(StrEnum): | |
MALE = "Male" | |
FEMALE = "Female" | |
class PlayerMoneyTypes(StrEnum): | |
IN_HAND = "In hand" | |
IN_BAR = "In bar" | |
IN_BANK = "In bank" | |
class Guild(StrEnum): | |
CIVILIAN = "Civilian" | |
FIST = "Fist" | |
SWORD = "Sword" | |
CLAW = "Claw" | |
OUTLAW = "Outlaw" | |
def make_random_id(): | |
random_number = random.randint(1, 65_535) | |
logging.debug("%i", random_number) | |
return random_number | |
def make_random_stat(): | |
random_number = random.randint(1, 18) | |
logging.debug("%i", random_number) | |
return random_number | |
def set_up_silver() -> dict: | |
silver_types = {v: k * 1_000 for k, v in enumerate(PlayerMoneyTypes, start=1)} | |
logging.info("%s" % silver_types) | |
return silver_types | |
def set_up_stats() -> dict: | |
stats = {k: make_random_stat() for k in PlayerStat} | |
logging.debug("%s" % stats) | |
return stats | |
class Player(object): | |
""" | |
Attributes, flags, and other stuff about players. | |
""" | |
""" | |
TODO: There should be methods here for Inventory: | |
Inventory.item_held(item): check player/ally inventory, return True or False | |
(is it important to know whether the player or ally is carrying an item?) | |
maybe return Player or Ally object if they hold it, or None if no-one holds it | |
""" | |
def __init__(self, **kwargs): | |
"""this code is called when creating a new character""" | |
# specifying e.g., 'hit_points=None' makes it a required parameter | |
# FIXME: probably just forget this, net_server.py handles connected_users(set) | |
""" | |
connection_id: list of CommodoreServer IDs: {'connection_id': id, 'name': 'name'} | |
for k in len(connection_ids): | |
if connection_id in connection_ids[1][k]: | |
logging.info(f'Player.__init__: duplicate {connection_id['id']} assigned to ' | |
f'{connection_ids[1][connection_id]}') | |
return | |
temp = {self.name, connection_id} | |
connection_ids.append({'name': name, connection_id}) | |
logging.info(f'Player.__init__: Connections: {len(connection_ids)}, {connection_ids}') | |
self.connection_id = connection_id # 'id' shadows built-in name | |
""" | |
""" | |
The point behind all this is that dataclasses can't account for unknown parameters, and I'll | |
be adding attributes to the Player class definition for some time until it gets stable. | |
The .get() method avoids KeyErrors since it replaces missing parameters with the 2nd param | |
""" | |
self.connection_id = kwargs.get('connection_id', make_random_id()) | |
# keep this until I figure out where it is in net_server.py | |
self.name = kwargs.get('name', "Generic Name") | |
self.gender = kwargs.get('gender', Gender.MALE) | |
# creates a new stats dict for each Player, zero all stats: | |
# set with Player.set_stat(PlayerStat.xyz, value) | |
self.stats = kwargs.get("stats", set_up_stats()) | |
# flags: | |
self.flags = kwargs.get('flags') | |
logging.info("flags: %s" % self.flags) | |
# creates a new silver dict for each Player: | |
# in_bank may be cleared on character death (TODO: look in TLOS source) | |
# in_bar should be preserved after character's death (TODO: same) | |
self.silver = kwargs.get('silver', set_up_silver()) | |
if self.silver: | |
""" | |
>>> print(f"{PlayerMoneyTypes.IN_HAND}: {silver_types[PlayerMoneyTypes.IN_HAND]:,}") | |
In hand: 1,000 | |
""" | |
silver_in_hand = self.get_silver(PlayerMoneyTypes.IN_HAND) | |
logging.info("Silver in hand: %i" % silver_in_hand) | |
# client settings - set up some defaults | |
self.client = kwargs.get('client', Client()) | |
self.times_played = kwargs.get('times_played', None) | |
self.last_play_date = kwargs.get('last_play_date', None) # like birthday | |
""" | |
proposed stats: | |
some (not all) other stats, still collecting them: | |
special_items[ | |
SCRAP OF PAPER is randomly placed on level 1 with a random elevator combination | |
BOAT # does not actually need to be carried around in inventory, I don't suppose, just a flag? | |
combinations{'elevator', 'locker', 'castle'} # tuple? combo is 3 digits: (nn, nn, nn) | |
] | |
""" | |
self.map_level = kwargs.get('map_level', 1) # cl | |
self.map_room = kwargs.get('map_room', 1) # cr | |
self.moves_made = kwargs.get('moves_made', None) | |
self.birthday = kwargs.get('birthday', None) # datetime | |
self.guild = kwargs.get('guild', Guild.CIVILIAN) # [civilian | fist | sword | claw | outlaw] | |
# 1 2 3 4 5 6 7 8 9 | |
self.char_class = kwargs.get('char_class', None) # Wizard Druid Fighter Paladin Ranger Thief Archer Assassin Knight | |
self.race = kwargs.get('char_race', None) # Human Ogre Pixie Elf Hobbit Gnome Dwarf Orc Half-Elf | |
self.hit_points = kwargs.get('hit_points', 0) | |
self.shield = kwargs.get('shield', None) | |
self.armor = kwargs.get('armor', None) | |
self.experience = kwargs.get('experience', 0) | |
""" | |
TODO: MORE CLASSES | |
combat: | |
honor: int | |
class Weapon: | |
percent_left: int | |
class AmmoWeapon: | |
ammunition_for: Weapon | |
loaded_with: Ammunition | |
class Ammunition: | |
rounds_per_unit: int # how many rounds | |
bad_hombre_rating (BHR) is calculated from stats, not stored in player log | |
once_per_day[] flags: # things you can only do once per day (file_formats.txt) | |
'pr' has PRAYed once | |
'pr2' can PRAY twice per day (only if player class is Druid) | |
""" | |
self.changes = True | |
def __str__(self): | |
"""print formatted Player object (double-quoted since ' in string)""" | |
age = "Undetermined" | |
birthdate = "None" | |
if self.birthday: | |
delta = datetime.now() - self.birthday | |
age = f"{delta.days // 365} years" | |
date_format_string = "%a %b %d, %Y" # weekday, month, date, year | |
birthdate = self.birthday.strftime(date_format_string) | |
_ = f""" | |
{'Name:'.rjust(20)} {self.name} | |
{'Age:'.rjust(20)} {age} | |
{'Gender:'.rjust(20)} {self.gender.title()} | |
{'Birthday:'.rjust(20)} {birthdate} | |
{'Silver: In hand:'.rjust(20)} {self.silver[PlayerMoneyTypes.IN_HAND]} | |
{'Guild:'.rjust(20)} {self.guild.title()} | |
""" | |
return textwrap.dedent(_) | |
def set_stat(self, stat: PlayerStat, adj: int): | |
""" | |
:param stat: statistic in stats{} dict to adjust | |
:param adj: adjustment (+x or -x) | |
:return: stat, maybe also 'success': True if 0 > stat > <limit> | |
TODO: example for doctest: | |
>>> rulan.adjust_stat(PlayerStat.STR, -5) # decrement Rulan's strength by 5 | |
""" | |
if stat not in self.stats: | |
logging.warning(f"Stat {stat} doesn't exist.") | |
# raise ValueError? | |
return | |
# adjust stat by <adjustment>: | |
before = self.stats[stat] | |
after = before + adj | |
logging.info("set_stat: Before: %s %i" % (stat, after)) | |
if not self.flags['expert_mode']: | |
descriptive = zip(['chr', 'con', 'dex', 'int', 'str', 'wis', 'egy'], | |
['influential', 'hearty', 'agile', 'intelligent', | |
'strong', 'wise', 'energetic']) | |
# TODO: jwhoag suggested adding 'confidence' -> 'brave' -- good idea, | |
# not sure where it can be added yet. | |
# returns ('con', 'hearty') -- etc. | |
for n in descriptive: | |
# FIXME: I don't know of a more efficient way to refer to a subscript in this case. | |
# This may be good enough, it's a small loop | |
if n[0] == stat: | |
print(f"You feel {'more' if after > before else 'less'} {n[1]}.") | |
logging.info("set_stat: After: %s %i" % (stat, after)) | |
self.stats[stat] = after | |
def set_stats_absolute(self, stats: dict[PlayerStat, int]): | |
try: | |
for k, v in stats: | |
self.stats[k] = v | |
logging.info("Setting stat %s to %i" % (self.stats[k], v)) | |
except KeyError: | |
logging.debug("Couldn't find stat %s" % stats) | |
def get_stat(self, stat: PlayerStat): | |
""" | |
if 'stat' is str: return value of single stat as str: 'stat' | |
TODO: if 'stat' is list: sum up contents of list: ['str', 'wis', 'int']... | |
-- avoids multiple function calls | |
""" | |
if type(stat) is list: | |
total = 0 | |
for k in stat: | |
try: | |
total += self.stats[k] | |
except KeyError: | |
logging.warning(f"Stat {stat} doesn't exist.") | |
# TODO: raise ValueError? | |
logging.info(f'get_stat[list]: {stat=} {total=}') | |
return total | |
# otherwise, get just a single stat: | |
if stat not in self.stats: | |
logging.warning(f"get_stat: Stat {stat} doesn't exist.") | |
# TODO: raise ValueError? | |
return None | |
return self.stats[stat] | |
def print_all_stats(self, abbreviate=False): | |
""" | |
Print all player stats in title case: '<Stat>: <value>' | |
Should call `print_stat()` to save overhead. | |
""" | |
""" | |
>>> rulan = Player() | |
>>> rulan.set_stats_absolute({PlayerStat.CHR: 8, | |
... PlayerStat.CON: 15, | |
... PlayerStat.DEX: 3, | |
... PlayerStat.EGY: 3, | |
... PlayerStat.INT: 5, | |
... PlayerStat.STR: 8, | |
... PlayerStat.WIS: 3, | |
... }) | |
>>> rulan.print_all_stats(abbreviate=True) | |
Chr: 8 Int: 5 Egy: 3 | |
Con: 15 Str: 8 | |
Dex: 3 Wis: 3 | |
""" | |
""" | |
>>> for stat in rulan.stats: | |
... print(f'{f"{stat}".rjust(15)}: {self.stats[stat]}') | |
Charisma: 2 | |
Constitution: 2 | |
Dexterity: 15 | |
Energy: 18 | |
Intelligence: 5 | |
Strength: 15 | |
Wisdom: 18 | |
""" | |
if abbreviate: | |
longest_name = max(len([stat_name for stat_name in PlayerStat])) | |
right_justified_stats = [{}] | |
for stat in [PlayerStat.CHR, PlayerStat.INT, PlayerStat.EGY]: | |
print(f'{stat.title()}: {self.stats[stat]:2} ', end='') | |
print() | |
for stat in [PlayerStat.CON, PlayerStat.STR]: | |
print(f'{stat.title()}: {self.stats[stat]:2} ', end='') | |
print() | |
for stat in [PlayerStat.DEX, PlayerStat.WIS]: | |
print(f'{stat.title()}: {self.stats[stat]:2} ', end='') | |
print() | |
def get_silver(self, kind: PlayerMoneyTypes) -> int | None: | |
""" | |
Get the amount of silver the player has for a specific money type. | |
:param kind: Type of money storage (IN_HAND, IN_BANK, or IN_BAR) | |
:returns int | None: Amount of silver if found, None if invalid money type | |
>>> player.get_silver(PlayerMoneyTypes.IN_HAND) | |
1000 | |
""" | |
try: | |
amount = self.silver[kind] | |
logging.info("%s: %i" % (kind.value, amount)) | |
return amount | |
except KeyError: | |
logging.info("Invalid money type requested: %s" % kind.value) | |
return None | |
def show_silver(self, kind: PlayerMoneyTypes) -> str | None: | |
""" | |
Show the amount of silver the player has for a specific money type. | |
:param kind: Type of money storage (IN_HAND, IN_BANK, or IN_BAR) | |
:returns str | None: Amount of silver if found, None if invalid money type | |
>>> player.show_silver(PlayerMoneyTypes.IN_HAND) | |
"Silver in hand: 1,000" | |
""" | |
try: | |
amount = self.silver[kind] | |
logging.info("%s: %i" % (kind.value, amount)) | |
return f"Silver {kind}: {amount:,}" | |
except KeyError: | |
logging.info("Invalid money type requested: %s" % kind.value) | |
return None | |
def adjust_silver(self, kind: PlayerMoneyTypes, adj: int): | |
""" | |
Add to or subtract from a value relative to the amount already in the account. | |
:param kind: PlayerMoneyTypes.IN_BANK | IN_BAR | IN_HAND | |
:param adj: amount to add (<adj>) or subtract (-<adj>) | |
:return: None if KeyError | |
""" | |
before = self.silver[kind] | |
# TODO: check for negative amount | |
after = before + adj | |
logging.info("Before: %s %i, after: %i" % (kind, before, after)) | |
if after < 0: | |
logging.info('%i, negative amount not allowed' % after) | |
return None | |
self.silver[kind] = after | |
return None | |
def set_silver(self, kind: PlayerMoneyTypes, amount: int) -> bool: | |
""" | |
Set an absolute amount of silver for a specific type of money. | |
:param kind: Type of money storage (IN_HAND, IN_BANK, or IN_BAR) | |
:param amount: Amount to set | |
:return: True if successful, False if invalid amount | |
""" | |
if amount < 0: | |
logging.warning("Cannot set negative silver amount: %i" % amount) | |
return False | |
before = self.silver[kind] | |
self.silver[kind] = amount | |
logging.info("Silver %s changed from %i to %i" % (kind.value, before, amount)) | |
return True | |
def show_stat(self, stat: PlayerStat, abbreviate: bool = True) -> str | None: | |
""" | |
Generates a string for a specific statistic. | |
:param stat: The PlayerStat enum member to display. | |
:param abbreviate: If True, use the short name (e.g., 'Wis'). | |
If False, use the full name (e.g., 'Wisdom'). | |
:returns: A formatted string like "Wis: 14" or "Wisdom: 14". | |
""" | |
try: | |
# Get the stat's value from the dictionary | |
stat_value = self.stats[stat] | |
if abbreviate: | |
# Use the enum member's built-in 'name' property (e.g., "WIS") | |
stat_name = stat.name.title() | |
else: | |
# Use the enum member's built-in 'value' property (e.g., "Wisdom") | |
stat_name = f"{stat.value}".rjust(15) | |
# Return the formatted string | |
return f"{stat_name}: {stat_value}" | |
except AttributeError: | |
# Handle cases where the stat doesn't exist for the player | |
logging.error("Stat '%s' does not exist for player '%s'." % (stat.name, self.name)) | |
return None | |
if __name__ == '__main__': | |
# set up logging | |
log = logging.getLogger(__name__) | |
logging.basicConfig(level=logging.DEBUG, | |
format='%(levelname)10s | %(funcName)15s() | %(message)s') | |
logging.info("Info message") | |
# birthday = datetime.date(1976, 6, 16) | |
# print(datetime.date(2025, 6, 16).strftime("%a, %b %d %Y")) | |
rulan_settings = {'name': 'Rulan', 'times_played': 400, 'guild': Guild.FIST, } | |
rulan = Player(**rulan_settings) | |
silver_amount = 499 | |
if not rulan.set_silver(PlayerMoneyTypes.IN_HAND, silver_amount): | |
print(f"Could not set silver in hand to {silver_amount}") | |
else: | |
print(f"Set silver in hand to {silver_amount}.") | |
if rulan.times_played is None: | |
print("This is a new player.") | |
print(rulan) | |
print(rulan.client) | |
# statistic adjusting tests: | |
# 1. Create a player instance | |
gandalf_settings = {'name': "Gandalf"} | |
my_player = Player(**gandalf_settings) | |
right_spacing = 30 | |
# 2. Get the abbreviated stat string | |
# Here we pass the enum member directly | |
wis_abbr = my_player.show_stat(PlayerStat.WIS, abbreviate=True) | |
print(f"{'Abbreviated version: '.rjust(right_spacing)}{wis_abbr}") # Output: Abbreviated version: WIS: 14 | |
# 3. Get the full name stat string | |
wis_full = my_player.show_stat(PlayerStat.WIS, abbreviate=False) | |
print(f"{'Full name version: '.rjust(right_spacing)}{wis_full}") # Output: Full name version: Wisdom: 14 | |
# 4. Example with another stat | |
int_full = my_player.show_stat(PlayerStat.INT, abbreviate=False) | |
print(f"{'Another example: '.rjust(right_spacing)}{int_full}") # Output: Another example: Intelligence: 18 | |
rulan.show_stat(PlayerStat.INT, True) | |
rulan.show_stat(PlayerStat.WIS, False) | |
# rulan.show_stat(PlayerStat.obviously_false, False) | |
rulan.adjust_silver(PlayerMoneyTypes.IN_HAND, 3_000) | |
print(rulan.show_silver(PlayerMoneyTypes.IN_HAND)) | |
# rulan.print_all_stats() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment