From fbe69be2acf11b64edb47b0374575b65c8e6bc4f Mon Sep 17 00:00:00 2001 From: Zachary Watts Date: Sun, 3 May 2026 22:32:50 -0400 Subject: [PATCH] cleaning up code --- README.md | 25 +++++++++++++ adventurers.py | 67 +++++++++++++++++++++++----------- app.py | 13 +++---- equipment.py | 4 +-- main.py | 77 +++++++++++++++++++--------------------- requirements.txt | 3 +- templates/character.html | 2 +- 7 files changed, 117 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index fbcca8d..3253284 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ This tool uses the Old School Essentials (OSE) ruleset for tabletop RPGs in order to randomly generate a character. +## Overview + ### What is Dungeons & Dragons? Dungeons & Dragons, also known as D&D, is a pen and paper table top game. One player is the story teller, the "dungeon master" and the others interact with the story using their "adventurers." An adventurer is a playable character, who has a mixture of abilities and powers, the details of which are often kept on piece of paper called a "character sheet." @@ -20,3 +22,26 @@ This app accomplishes a few things, but mainly it creates a character for you. - You can generate a whole party of characters - Within that party, you can adjust their level, and the number of characters - You can create a single character to edit + + +## Instructions + +### Install +This is a flask web application, to install: + +1. Clone this repo +2. Install requirements, `python3 pip install -r requirements.txt` +3. To run the webserver execute `flask run` while in your cloned repository's top folder. + +### To Use +There are two main generators, one for a single character and another for a group, or a "party" of characters. +1. Character Generator + - You may select a class and a level + - The re-roll button always starts the character back to level 1 +2. Party Generator + - You may also select the level for this group + - You may select the count, between 2 and 5. + - If you want to have the party persist across refreshes, level and size changes, keep "cached" on + - Otherwise, "not cached" will always generate a new party + - If you find a single character interesting, you may "select" them and it will load **this** character in the "charater generator." + diff --git a/adventurers.py b/adventurers.py index 53df244..aa9d69b 100755 --- a/adventurers.py +++ b/adventurers.py @@ -10,6 +10,28 @@ def roll_dice(count, sides): # Player Character Classes class Adventurer: + ability_score_modifiers = { + 'strength' : { 3 : { 'melee' : -3, 'open doors': 1 }, 5 : { 'melee' : -2, 'open doors': 1 }, 8 : { 'melee' : -1, 'open doors': 1 }, + 12 : { 'melee' : 0, 'open doors': 2 }, 15 : { 'melee' : 1, 'open doors': 3 }, 17 : { 'melee' : 2, 'open doors': 4 }, + 18 : { 'melee' : 3, 'open doors': 5 } }, + 'intelligence' : { 3 : { 'languages' : 'native, broken speech', 'literacy': 'illiterate' }, 5 : { 'languages' : 'native', 'literacy': 'illiterate' }, + 8 : { 'languages' : 'native', 'literacy': 'basic' }, 12 : { 'languages' :'native', 'literacy': 'literate' }, + 15 : { 'languages' : 'native +1 additional', 'literacy': 'literate'}, 17 : { 'languages' : 'native +2 additional', 'literacy': 'literate' }, + 18 : { 'languages' : 'native +3 additional', 'literacy': 'literate' } }, + 'wisdom' : { 3 : { 'magic saves' : -3 }, 5 : { 'magic saves' : -2 }, 8 : { 'magic saves' : -1 }, 12 : { 'magic saves' : 0,}, + 15 : { 'magic saves' : 1 }, 17 : { 'magic saves' : 2 }, 18 : { 'magic saves' : 3 } }, + 'dexterity' : { 3 : { 'AC' : -3, 'missile': -3, 'initiative' : -2 }, 5 : { 'AC' : -2, 'missile': -2, 'initiative' : -1}, + 8 : { 'AC' : -1, 'missile': -1, 'initiative' : -1 }, 12 : { 'AC' : 0, 'missile': 0, 'initiative' : 0 }, + 15 : { 'AC' : 1, 'missile': 1, 'initiative' : 1 }, 17 : { 'AC' : 2, 'missile': 2, 'initiative' : 1 }, + 18 : { 'AC' : 3, 'missile': 3, 'initiative' : 2 } }, + 'constitution' : { 3 : { 'hit points' : -3 }, 5 : { 'hit points' : -2 }, 8 : { 'hit points' : -1 }, 12 : { 'hit points' : 0 }, + 15 : { 'hit points' : 1 }, 17 : { 'hit points' : 2 }, 18 : { 'hit points' : 3 } }, + 'charisma' : { 3 : { 'npc reactions' : -2, 'max retainers': 1, 'retainer loyalty' : 4 }, 5 : { 'npc reactions' : -2, 'max retainers': 2, 'retainer loyalty' : 5 }, + 8 : { 'npc reactions' : -1, 'max retainers': 3, 'retainer loyalty' : 6 }, 12 : { 'npc reactions' : 0, 'max retainers': 4, 'retainer loyalty' : 7 }, + 15 : { 'npc reactions' : 1, 'max retainers': 5, 'retainer loyalty' : 8 }, 17 : { 'npc reactions' : 1, 'max retainers': 6, 'retainer loyalty' : 9 }, + 18 : { 'npc reactions' : 2, 'max retainers': 7, 'retainer loyalty' : 10 } } + } + def __init__(self, c_id: str, level=1, attributes={}) -> None: # using a get() method to pull an attribute else use a default value, https://python-academy.org/en/handbook/get self.identifier = c_id @@ -27,7 +49,7 @@ class Adventurer: self.gold= roll_dice(3,6) self.torches = roll_dice(1,6) self.rations = roll_dice(1,6) - self.equipment = self.roll_equipment() + self.equipment = self.set_equipment() # all armor, individual classes may have overrides self.possible_armor = list(armor.keys()) # all weapons, individual classes may have overrides @@ -43,21 +65,21 @@ class Adventurer: self.thief_skills = None self.turn_undead = None - def __str__(self): + def __str__(self) -> str: return f"{self.player_class}" - def get_subclass_dict(): + def get_subclass_dict() -> dict: subclasses = {} for subclass in Adventurer.__subclasses__(): subclasses[subclass.adv_class] = subclass return subclasses - def get_json(self): + def get_json(self) -> dict: char_dict = self.__dict__ char_json = json.dumps(char_dict) return char_dict - def vertical_sheet(self): + def vertical_sheet(self) -> list: sheet = [] sheet.append('{0: <28}'.format(f"| {self.player_class.title()} - Level {self.level}")) for key, val in self.get_attributes().items(): @@ -89,7 +111,7 @@ class Adventurer: sheet = [ line + "|" for line in sheet ] return sheet - def full_sheet(self): + def full_sheet(self) -> list: sheet = { 1: [], 2 : [] } sheet[1].append('{0: <60}'.format(f"| Character Name:")) sheet[1].append('{0: <60}'.format(f"| Player Name :")) @@ -105,26 +127,27 @@ class Adventurer: sheet[1].append('{0: <60}'.format(f"| Charisma:\t{self.charisma} SpellMod: {self.ac}")) return sheet - def get_attributes(self): + def get_attributes(self) -> dict: attribute_list = ['strength', 'intelligence', 'wisdom', 'dexterity', 'constitution', 'charisma'] return {k: self.__dict__[k] for k in attribute_list} - def get_best_prime_attribute(self): + def get_best_prime_attribute(self) -> str: attribute_list = [ 'strength', 'intelligence', 'wisdom', 'dexterity' ] prime_attributes = {k: self.__dict__[k] for k in attribute_list} # return highest, found this at https://stackoverflow.com/a/280156 return max(prime_attributes, key=prime_attributes.get) - def roll_equipment(self): + def set_equipment(self): equipment = [ 'backpack', 'tinderbox', 'waterskin' ] - random_items = [ random.choice(adventuring_gear) for i in range(2) ] - for item in random_items: - equipment += item.split("+") - while len(equipment) < 7: - equipment.append("") + for i in range(2): + item = "" + while item not in equipment: + item = random.choice(adventuring_gear) + if item not in equipment: + equipment.append(item) return equipment - def select_spells(self, spell_list): + def select_spells(self, spell_list) -> list: spell_book = [] for spell_level, count in self.spells.items(): if count != "-": @@ -132,10 +155,11 @@ class Adventurer: new_spell = "" while new_spell not in spell_book: new_spell = random.choice(spell_list[spell_level]) - spell_book.append(new_spell) + if new_spell not in spell_book: + spell_book.append(new_spell) return spell_book - def roll_all_hit_points(self): + def roll_all_hit_points(self) -> list: hp_list = [] prev_hit_dice = 0 for i in range(self.__class__.max_level): @@ -151,10 +175,10 @@ class Adventurer: hp_list.append(hp_roll) return hp_list - def get_class_progression_for_level(self): + def get_class_progression_for_level(self) -> list: return list(filter(lambda d: d['level'] == self.level, self.__class__.progression))[0] - def set_level(self, new_level): + def set_level(self, new_level) -> None: self.level = new_level self.hp = sum(self.hp_rolls[:self.level]) if self.player_class in [ "magic user", "elf" ]: @@ -163,8 +187,9 @@ class Adventurer: if self.player_class == "cleric": self.spells = Cleric.spells[self.level] self.spell_book = self.select_spells(cleric_spells) + self.turn_undead = Cleric.turn_undead[self.level] - def set_attack_bonus(self): + def set_attack_bonus(self) -> int: prog = self.get_class_progression_for_level() thac0 = prog['thac0'] atk = 19 - thac0 @@ -321,7 +346,7 @@ class Cleric(Adventurer): self.spells = Cleric.spells[self.level] self.spell_book = self.select_spells(cleric_spells) self.atk = self.set_attack_bonus() - self.turn_undead = Cleric.turn_undead + self.turn_undead = Cleric.turn_undead[self.level] class Thief(Adventurer): adv_class = "thief" diff --git a/app.py b/app.py index 41e8829..42f7810 100644 --- a/app.py +++ b/app.py @@ -18,7 +18,7 @@ def compress_character(adventurer): def return_character_from_cookie(cookie): cookie_decompressed = base64.urlsafe_b64decode(cookie) adventurer_dict = json.loads(zlib.decompress(cookie_decompressed).decode()) - adventurer = createCharacterWithDict(adventurer_dict) + adventurer = AdventurerGen.create_from_dict(adventurer_dict) return adventurer @app.route('/') @@ -47,7 +47,7 @@ def party(): if count < 2 or count > 5: count = 4 # generate an adventuring party - adv_party = returnParty(count, level) + adv_party = PartyGen.get_new_party(count, level) # check for cookies present if request.cookies: stored_count = len(request.cookies) @@ -65,7 +65,7 @@ def party(): adv_party.set_party(stored_adv_party, count, level) if count > stored_count: extension = count - stored_count - more_party_members = returnParty(extension,level).adventurers + more_party_members = PartyGen.get_new_party(extension,level).adventurers extended_party = stored_adv_party + more_party_members adv_party.set_party(extended_party, count, level) if count < stored_count: @@ -103,17 +103,14 @@ def character(): # reroll until we get our class while character.player_class != role.replace("-"," "): new_char = Adventurer(c_id, level) - selected_class = ClassSelector(new_char).selection() + selected_class = AdventurerGen(new_char).selection() character = selected_class(new_char.identifier, new_char.level, new_char.get_attributes()) else: new_char = Adventurer(c_id, level) - selected_class = ClassSelector(new_char).selection() + selected_class = AdventurerGen(new_char).selection() character = selected_class(new_char.identifier, new_char.level, new_char.get_attributes()) role = character.player_class level = character.level - # bug fix for cleric issues - if character.player_class == "cleric": - character.turn_undead = { int(k) : v for k,v in character.turn_undead.items() } response = make_response(render_template("character.html", character=character, level=level, cache=cache,role=role)) cookie_string, cookie_data = compress_character(character) response.set_cookie(cookie_string, cookie_data) diff --git a/equipment.py b/equipment.py index 8327e0e..edfb7e4 100755 --- a/equipment.py +++ b/equipment.py @@ -20,6 +20,6 @@ weapons = [ ] adventuring_gear = [ - "crowbar", "hammer (small)+12 iron spikes", "holy water", "lantern+3 flasks of oil", "mirror (small, steel)", "pole (10' long, wooden)", - "rope (50')", "rope (50')+grappling hook", "sack (large)", "sack (small)", "stakes (3)+mallet", "wolfsbane (1 bunch)" + "crowbar", "hammer (small) + 12 iron spikes", "holy water", "lantern + 3 flasks of oil", "mirror (small, steel)", "pole (10' long, wooden)", + "rope (50')", "rope (50') + grappling hook", "sack (large)", "sack (small)", "stakes (3) + mallet", "wolfsbane (1 bunch)" ] diff --git a/main.py b/main.py index ea68d61..acce82e 100755 --- a/main.py +++ b/main.py @@ -5,7 +5,7 @@ import json import random # Player Class Selector -class ClassSelector(): +class AdventurerGen(): def __init__(self, player: Adventurer) -> None: self.player = player # https://stackoverflow.com/questions/3862310/how-to-find-all-the-subclasses-of-a-class-given-its-name @@ -16,11 +16,11 @@ class ClassSelector(): # run function to randomly select an adventurer class self.selected_class = self.selection() - def return_class_by_best_attribute(self): + def return_class_by_best_attribute(self) -> Adventurer: # for adventurer classes in available classes, return the one where that classes' prime requisite is equal to the players best attribute return [adv_class for adv_class in self.available_classes if adv_class.prime_requisite == self.player.get_best_prime_attribute()][0] - def return_classes_with_requirements(self): + def return_classes_with_requirements(self) -> list: p_attrs = self.player.get_attributes() possible_classes = [] for c in self.classes_with_reqs: @@ -32,7 +32,7 @@ class ClassSelector(): possible_classes.append(c) return possible_classes - def selection(self): + def selection(self) -> Adventurer: best_prime_attribute = self.player.get_best_prime_attribute() # create an array of possible player classes, add the best choice per player's best core attributes possible_classes = [ self.return_class_by_best_attribute() ] @@ -41,7 +41,18 @@ class ClassSelector(): selected_class = random.choice(possible_classes) return selected_class -class PartyGenerator(): + def create_from_dict(character_dict) -> Adventurer: + chosen_class = character_dict['player_class'] + c_id = character_dict['identifier'] + level = character_dict['level'] + adv_dict = Adventurer.get_subclass_dict() + chosen_class = adv_dict[chosen_class] + new_char = chosen_class(c_id=c_id, level=level) + for k, v in character_dict.items(): + setattr(new_char,k, v) + return new_char + +class PartyGen(): def __init__(self, party_size: int, party_level: int) -> None: self.size = party_size self.level = party_level @@ -56,7 +67,7 @@ class PartyGenerator(): attempts = 0 while new_player.player_class not in self.adventurer_types: attempts += 1 - selected_class = ClassSelector(new_player).selection() + selected_class = AdventurerGen(new_player).selection() new_player = selected_class(new_player.identifier, new_player.level, new_player.get_attributes()) # i couldnt randomly generate a scenario where a character couldn't be added, but it seems possible, so this is the hard cut off if (new_player.player_class not in self.adventurer_types) or (attempts > 10): @@ -70,6 +81,24 @@ class PartyGenerator(): self.adventurers = adventurers self.adventurer_types = [] + def get_new_party(party_size, party_level): + # keep variables within expected ranges + if party_size <= 0 or party_size > 5: + party_size = 1 + if party_level < 1 or party_level > 5: + party_level = 1 + # generate an aventuring party per size and level + new_party = PartyGen(party_size, party_level) + # for adventurers select classes + new_party.gen_party() + # return the created adventurer party + return new_party + + def get_character(self, identifer): + for adv in self.adventurers: + if adv.identifier == identifier: + return adv + def get_character_sheets(self): sheet_string = "" character_sheets = [] @@ -87,47 +116,15 @@ class PartyGenerator(): def __str__(self): return f"{self.adventurers}" -def returnParty(party_size, party_level): - # keep variables within expected ranges - if party_size <= 0 or party_size > 5: - party_size = 1 - if party_level < 1 or party_level > 5: - party_level = 1 - # generate an aventuring party per size and level - new_party = PartyGenerator(party_size, party_level) - # for adventurers select classes - new_party.gen_party() - # return the created adventurer party - return new_party -def returnCharacter(identifer): - for adv in new_party.adventurers: - if adv.identifier == identifier: - return adv - -def createCharacterWithDict(character_dict) -> Adventurer: - chosen_class = character_dict['player_class'] - c_id = character_dict['identifier'] - level = character_dict['level'] - adv_dict = Adventurer.get_subclass_dict() - chosen_class = adv_dict[chosen_class] - new_char = chosen_class(c_id=c_id, level=level) - for k, v in character_dict.items(): - setattr(new_char,k, v) - return new_char # used for local testing def main(): adv_dict = Adventurer.get_subclass_dict() test_class = adv_dict['cleric'] new_char = test_class(c_id="adv-1",level=2) - print(new_char.adv_class) - print(new_char.level) - print(new_char.hp_rolls) - print(new_char.hp) - print(new_char.atk) - for k,v in new_char.turn_undead[new_char.level].items(): - print(k, v) + for line in new_char.vertical_sheet(): + print(line) if __name__ == "__main__": main() diff --git a/requirements.txt b/requirements.txt index 5c1bea9..af266d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -flask -flask-session +Flask==3.1.3 diff --git a/templates/character.html b/templates/character.html index b514e39..97c634c 100644 --- a/templates/character.html +++ b/templates/character.html @@ -132,7 +132,7 @@
Turn Undead
- {%for k,v in character.turn_undead[character.level].items() %} + {%for k,v in character.turn_undead.items() %} {%endfor%}
Monster Hit DieRoll to Turn
{{k}} Hit Die{{v}}