cleaning up code

This commit is contained in:
Zachary Watts
2026-05-03 22:32:50 -04:00
parent 2173733463
commit fbe69be2ac
7 changed files with 117 additions and 74 deletions

View File

@@ -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."

View File

@@ -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])
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"

13
app.py
View File

@@ -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)

View File

@@ -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)"
]

77
main.py
View File

@@ -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()

View File

@@ -1,2 +1 @@
flask
flask-session
Flask==3.1.3

View File

@@ -132,7 +132,7 @@
<h5>Turn Undead</h5>
<table class="table">
<thead><tr><th>Monster Hit Die</th><th>Roll to Turn</th><th></th><th></th></thead>
{%for k,v in character.turn_undead[character.level].items() %}
{%for k,v in character.turn_undead.items() %}
<tbody><tr><th>{{k}} Hit Die</th><td>{{v}}</td><th></th><td></td></tr></tbody>
{%endfor%}
</table>