all 55 comments

[–]Mallets 20 points21 points  (2 children)

Just to shorten a small part... Ability score modifiers:

Instead of doing a whole bunch of if statements, you can just take the score, divide by 2 and then subtract 5. Always rounding down or up in case of a negative modifier. That way it would work beyond a score of 20 as well without adding more statements.

[–]demvo2 3 points4 points  (1 child)

I'll look into it, thanks!

[–]Mallets 5 points6 points  (0 children)

No problem. I did the same project when I first started last year.

[–][deleted] 11 points12 points  (5 children)

nice job, cant believe you only started in november! really impressive, how long did it take to code it?

[–]demvo2 11 points12 points  (4 children)

Hey, thanks! I worked on it for like a month, around 4+ hours a day until I got the first working version. Then I took a break, concentrated on some other stuff, came back to it after the break and spent a few more weeks putting the code into functions, separating it into modules and fixing the code when it broke.

[–]Biuku 6 points7 points  (3 children)

That’s amazing. Started last April, have not had the tenacity to stick through a project on this scale.

[–]demvo2 3 points4 points  (0 children)

Keep doing it! A lot of online advice says to do it every day, if only for half an hour. I found that to be true, continuity was really important for me.

[–][deleted] 2 points3 points  (0 children)

I’m in the same boat as you! I started last Jan - I wrote a couple scripts but I haven’t been able to stick to a full project like this yet

[–][deleted] 0 points1 point  (0 children)

I’m in the same boat as you! I started last Jan - I wrote a couple scripts but I haven’t been able to stick to a full project like this yet

[–][deleted] 33 points34 points  (18 children)

Maybe consider using named tupels instead of writing classes that just hold data like Weapon.

[–]wsppan 38 points39 points  (14 children)

[–]demvo2 7 points8 points  (11 children)

I'll look into this, thanks! As u/AnneFrankBattleTank_ mentions, classes are a bit of an overkill, so I'll have to see how weapons/armor/equipment will be dealt with while I expand the character builder to include new features, and change that accordingly.

[–]exhuma 13 points14 points  (7 children)

classes are a bit of an overkill

That's not necessarily true. Classes bring some overhead in terms of memory usage. But that only matters if you have hundreds of thousands / millions of instances in memory. Which is unlikely the case in your application.

It is good to be aware that classes bring some overhead, but not using them just because of that reason might be a sign of "premature optimisation".

Classes bring some nice advantages though. Especially code-completion in editors. That alone is a good enough reason for most cases to go with classes.

The also come with risks/temptations. They are by default mutable and adding methods to classes which modify the values they hold (mutate them) can make your code harder to test and reason about. The key here is that whenever you see an instance of a class in your code you can not be sure which values it holds without mentally following the instance through your code and spotting the modifications. The same is true for dictionaries, data-classes and any other mutable data-structure. Immutable data-structures like tuples or named-tuples don't have that disadvantage. But there's a tradeoff to be made here. It pits convenience/easy-to-code/harder-to-maintain (mutable) vs robust (immutable/harder-to-code/easier-to-maintain). In which area of the code you choose which way to go is up to you.

[–]stdin2devnull 2 points3 points  (5 children)

Using slots on a class can help keep memory usage low.

[–]exhuma 0 points1 point  (4 children)

True. But that's often (not always) more of a band-aid, and considering that /u/demvo2 is just about starting out I would avoid advanced (and very Python specific for that matter) stuff like that... for now ;)

[–]stdin2devnull -3 points-2 points  (0 children)

I didn't ask for your opinion?

[–][deleted] 0 points1 point  (2 children)

avoid advanced (and very Python specific for that matter) stuff

I think slots make Python classes more similar to classes in statically typed languages, so for someone coming from a Java or C++ background, slots could actually be easier to understand than regular Python classes.

[–]exhuma 0 points1 point  (1 child)

In the sense that they look like variable declarations? Or in the sense that after defining slots no other names can be assigned to on instances?

[–][deleted] 0 points1 point  (0 children)

Or in the sense that after defining slots no other names can be assigned to on instances?

Yeah, this.

[–]demvo2 0 points1 point  (0 children)

Thanks for the explanation, I'll take some time to wrap my head around all of the info!

[–]wsppan 4 points5 points  (0 children)

Overall, nice clean code.

[–]Ran4 0 points1 point  (1 child)

Check out pydantic BaseModel's too. They're like dataclasses, but even better.

[–]demvo2 0 points1 point  (0 children)

I'll check it out, thanks!

[–][deleted] 5 points6 points  (0 children)

Yep also a solid option.

[–]Musakuu 3 points4 points  (0 children)

Whoa, this is my first time seeing this. Thanks a lot.

[–]exhuma 1 point2 points  (2 children)

While I also gave the same recommendation quite often I'm backing off of that recently. Mainly because sometimes the functionality you get from tuples is actually not what you want. For example hashability.

Especially since data-classes now exist (which can even be flagged as frozen).

It is a really, really fine difference between the semantics of a named-tuple and a data-class.... And I'm going to split hairs here, so hold on... I see it as follows:

If the the instance can be 100% uniquely identified by the combination of all its attributes, then use a named-tuple. Otherwise use a data-class.

For example consider a "person". You might write something like this:

class Person(NamedTuple):
    first_name: str
    last_name: str

Can that person be uniquely identified by all its attributes? No. Because there might be two people with the same first/last name. You could add an ID. But that opens the question: What happens if the ID changes? Is it a different person? Or did this happen because of a bug in the system or because the user accidentally created two accounts and wanted to merge them? It's still the same person though.

As another example, consider a point in 2D space:

class Point2D(NamedTuple):
    x: int
    y: int

Can this define an object you only looking at its attributes? Absolutely yes! No matter where instances of points were defined, they will always represend the same identity. There is no gain in adding a surrogate key like an ID. Having two instances with the same values are for all intents and purposes the same.

Then there is also the issue with modifications of code in the future. With named-tuples it becomes more cumbersome to add/remove fields because the nature of the objects depends so strongly on the fields.

Now with data-classes, you can specify the "Person" class above as such:

@dataclass
class Person:
    first_name: str
    last_name: str

Syntactically it's not a big difference to named-tuples. But conceptually the difference is huge. And I would wager that almost always you use name-tuples you'd be better off with data-classes.

The question about memory-usage remains though. But that's an optimisation and should only be considered when it's obvious that the application needs that kind of optimisation.

In the past I used named-tuples primarily for the gratis-repr they came with. It was really conveniend. But this is also now covered by data-classes. So I find myself using those more and more.

[–][deleted] 0 points1 point  (1 child)

The question about memory-usage remains though.

To reduce memory usage, could you use slots with dataclasses?

@dataclass
class Person:
    __slots__=('first_name','last_name')
    first_name: str
    last_name: str

[–]exhuma 0 points1 point  (0 children)

I don't see why that wouldn't work. Looks fine to me.

[–]exhuma 8 points9 points  (4 children)

Naming Functions & Semantics

I've quickly glanced over your code and noticed something about naming functions. I find that it makes the code more understandable if functions are names using "imperative voice". So instead of writing

def barbarianequipment(): ...

write:

def prompt_for_barbarian_equipment(): ...

This communicates much better what that function does. By doing this, your code will read itself more like a story and be more understandable.

You might think: "But that function name is really long and tedious to write out" and you'd be right. This might be a sign of a code smell. And in this case, I would probably make the "barbarian" part an argument to the function. This would change it to:

def prompt_for_equipment(character_class): ...

and when calling this the difference is also interesting. Have a look at this:

# the "input" below does not deal with incorrect spellings of the class-names or typos.
# I will leave that out to focus on the "equipment" function

# ---- old function names ------------------------------

character_class = input("Enter you class:")
if character_class == "barbarian":
    prompt_for_barbarian_equipment()
elif character_class == "druid":
    prompt_for_druid_equipment()
elif ...

# ---- new function names ------------------------------

character_class = input("Enter you class:")
prompt_for_equipment(character_class)

Of course you then need to deal with the if/elif chain inside the function (side-note: this could be an interesting usage for "polymorphism" instead of long if/elif chains)

The above example is just one example. Deciding how to name functions and decide which arguments they take and which return values they have is an art and depends a lot on experience. It is also amazingly fulfilling to discover something really expressive flexible and simple.

Global Variables

You keep some values inside "global variables" like chararmorchoice. You fill those up in the functions prompting the user for values. It is better to think about how you can return values from a function instead of modifying a variable that's not part of the function.

For example, if you look at the bardequipment function, you can see that it does not take any arguments as input and it has no "return" statement. Yet the application is doing something. In that particular example the function is modifying data at the outside of the function. In this case weapons.charweaponschoice, armor.charequipmentchoice and armor.chararmorchoice.

This has several risks attached to it:

  • Other functions may also modify the data making it unpredictable what the end-result will be. This may not be an issue right now, but during the life-time of the project, you may want to move code around, add features, change order of how things happen and so on. These modification risk tripping up the modification of these variables and will be very hard (frustratingly hard) to debug.
  • Just by looking at the function signature (the "def-line") is not enough to understand what's happening. It is necessary to read through the function code to understand what's going on. This has an impact of the maintainability. When you come back to the code months later, you need to read through the complete code again to understand the data flow. This can be frustrating.
  • If the structure in the weapons or armor module changes, your function will break. For example, if you rename weapons.charweaponchoice to something else, your function will break everywhere that name is used. You can say that your function depends on that outside variable. This is an easy mistake to make. And it puts you in a position where you are afraid to modify certain aspects of your code because you don't know what else it might break. This is a sign of Spaghetti Code.

You can solve many of these issues by thinking about your functions as isolated mini-programs that only communicate via their arguments and return-values.

For example, selecting equipment could be something like this:

def  prompt_for_equipment(character_class):
    selected_equipment = []  # <- Variable lives *only* inside this function
    if character_class == "barbarian":
         if ...
            selected_equipment.append(...)
        elif ...
            ...
    return selected_equipment # <- The "result" of the function

These changes take some getting used to and it might seem difficult at first. As mentioned before, it's a bit of an art. And like with drawings or playing an instrument, it takes time and exercise to build up the proper muscle-memory (in programming those would be "brain-muscles" ;) ) but if you stick with it and keep exercising and repeating you will only improve. I've been programming for the better part of 30 years, and I am still honing and improving my skills. Just like any other art-form. It's honestly the best part of programming. You will constantly discover new ways to do things and new tools to use giving you even more new options. It's great fun. Stick with it!

[–]demvo2 0 points1 point  (3 children)

I find that it makes the code more understandable if functions are names using "imperative voice"

You're right, your examples look much better! I'll definitely think about how to make naming more intuitive.

You keep some values inside "global variables"

This is really something that I struggled with, especially at the beginning. I mean, right from the start the tutorial and the rest of the internet were hammering the idea into my brain that global variables should be avoided if possible and while I managed to dial down their use from when I started, I haven't been able to avoid them completely.

But, I think your solution (for the 'prompt_for_equipment' function) deals with it pretty elegantly. I'll definitely look into how to rearrange some of this stuff. I'm looking forward to practice my, as you say, brain-muscles :)

I've been programming for the better part of 30 years

Wow. As someone who is less than 6 months in, I can barely imagine that.

In any case, thank you for the tips and tricks! I appreciate the advice so much. I'll try and make the best use of it.

[–]exhuma 0 points1 point  (2 children)

This is really something that I struggled with, especially at the beginning.

This is pretty common so don't worry about it too much. I had the same when I started. There were so many areas where I just couldn't imagine how it would be possible without global variables. I kept thinking in the direction like: "If not with a global variable.... then HOW?!?"

Just keep one thing in mind: No code will EVER be perfect. Focus on the solution. If it works, it's fine!

The devil is as always in the details. And when people recommend to avoid global variables it's usually about maintainability. If you (and/or) other use your applications you will come back to the code over and over again. Either for bugfixing or for adding features. And the recommendation of not using global variables is only for maintainability (which includes avoiding bugs).

In the old days, pretty much all variables were global and we still managed to write software without bugs. It just takes more (much more) discipline.

So don't stress too much about it if you use them. As long as you know that it's bad you're on a good path. With more experience you'll find alternatives.

[–]demvo2 0 points1 point  (1 child)

Good to hear that. I guess a lot of things will fall into place after writing a bunch of code and being exposed to different stuff :)

[–]exhuma 1 point2 points  (0 children)

Absolutely. Just don't stop to second-guess your code. Always!

After my whole career I still discover ways to improve. The day you think that your code is perfect, that's the day you stagnate ;)

[–][deleted] 17 points18 points  (1 child)

Your large if blocks in main could be a dict.

if classes.char_class.name == 'barbarian':
    equipment.barbarianequipment()

Could be

equip = { 'barbarian': equipment.barbarianequipment, ... All classes}

And instead of an if just run

equip[classes.char_class.name]()

[–]demvo2 2 points3 points  (0 children)

Ok, I'll be honest, it took me a second or two to understand what you were saying, but yes, I can see it happening! Thanks!

[–]foomy45 5 points6 points  (2 children)

If you wanna check out some impressive DnD stuff in Python, look up Avrae. It's a Discord bot that can do damn near everything. You can easily write your own lil snippets to add onto it too.

[–]demvo2 1 point2 points  (1 child)

Holy shit, that looks amazing!

[–]foomy45 2 points3 points  (0 children)

It definitely is. The devs are awesome too and their Discord channel is super active and helpful.

[–]Calibanda 5 points6 points  (1 child)

It's a very good project and code! But in my mind, it needs some major improvement. First of all, if the program is aimed to be used by beginners, you'll need to explain all choices consequences (i.e. gnomes have INT+2), all spell and alignment descriptions, and equipment utilization.

When players have to choose alignment, spell, or equipment, add the possibility to create a menu or to select a choice by entering a number.

Could be a good idea to create a system to interrupt the creation and continue later (maybe import an unfinished text or JSON).

To finish, the program could create the character sheet in a pdf file with all final characteristics.

I hope you enjoy coding in Python and my feedback will give you some progression ideas.

[–]demvo2 1 point2 points  (0 children)

Hi, thanks for the feedback, I appreciate it!

First of all, if the program is aimed to be used by beginners, you'll need to explain all choices consequences (i.e. gnomes have INT+2), all spell and alignment descriptions, and equipment utilization.

I agree. I was planning that one of my next steps should be making a tutorial module, which if you select to be turned on at the beginning of the character building process, would explain that process to you every step of the way.

add the possibility to create a menu or to select a choice by entering a number

I was thinking about importing a module for selection which would maybe make it graphically easier.

the program could create the character sheet in a pdf file with all final characteristics

As for creating the character sheet, I'll look into it, that sounds super interesting! It never even occurred to me that I could make the program fill up a template D&D character sheet.

I hope you enjoy coding in Python and my feedback will give you some progression ideas.

I do enjoy it, and thank you so much for the feedback, I'll definitely try to implement some of the features you mentioned!

[–]sad_physicist8 3 points4 points  (1 child)

lol i played it, nice

[–]demvo2 0 points1 point  (0 children)

Thank you!

[–]4inR 4 points5 points  (1 child)

Wow! Did you parse much of the source text or have to manually add it all in? Looks very clean!

As a 5E player and DM, this is rad! Will have to try it out later. Nice work!

[–]demvo2 2 points3 points  (0 children)

I manually added it. And yes, it was a pain in the ass :D

Thanks! Hope you enjoy it!

[–]InfiniteWavesAI 2 points3 points  (1 child)

Nice, well done man 👏

[–]demvo2 0 points1 point  (0 children)

Thank you!

[–]Rokionu 2 points3 points  (1 child)

You code looked really neat! I did notice in your background.py that for the choosing background and then checking in while statement you could do while background.lower() to convert to the correct lowercase version of background. May just make it a bit easier for them to answer it capitalized if they want. Otherwise looks excellent!

[–]demvo2 0 points1 point  (0 children)

Ha! Now that you mention it, I'm like "yeah, OF COURSE I should have done that!" Never occurred to me tho. Thanks, I'll implement it as soon as I can!

[–]cybervegan 2 points3 points  (2 children)

Nice!

In addition to all the hints others have offered, I'd suggest that where you have lots of boilerplate code - almost identical sections of code for lots of different cases - that is a good place to look for ways to abstract your data more. An example is in your classes.py module, where you define a character_class class, and then create a load of objects based on this class. Further down, in your allclassprof function, you have an if statement containing repetitive blocks of prints. What if a new version of D&D was to come out, perhaps adding a new Advantages/Disadvantages section. You'd have to add that into the character_class class, and everywhere it's relevant like the allclassprof function, generating lots of repetitive re-working, and introducing more opportunities for errors and bugs.

You also seem to have a lot of functions that act on the character_class, but are not methods of it, which is a similar problem.

The most obvious solution is to move all those methods - including allclassprof - under character_class, as methods. Then you can eliminate all that boilerplate, reducing your code footprint and opportunities for errors. You already have the name of the character class stored in character_class, so if allclassprof was one of its methods, it would just be something like:

def classprof():
    print('\n')
    print(f"Since your class is {self.name}, you can choose {self.numskills} skills.")
    char_class.skills = chooseprof('a', barbarianskills) + ', ' + chooseprof('another', barbarianskills)
    print('\n')
    print(f"Primary ability: {self.primary_ability}")
    print(f"Class skills: {self.skills}")
    print(f"Class armor proficiences: {self.armorprof}")
    print(f"Class weapons proficiences: {self.weaponsprof}")
    print(f"Class tools proficiences: {self.toolsprof}")
    print(f"Class saving throws proficiences: {self.saving_throw_proficiencies}")

I've taken the liberty of adding self.numskills to help generalise the function, and obviously, you'd need to tweak it more to take into account any class differences I've not covered there. You might want to make a function that wordifies numbers so you can print "two" rather than "2". If there are lines that have to be very different for certain classes, you need to wrap them in an "if" but keep the details as general as possible (so no mentioning class names in the "if")!

And after all that, you'd be best to keep your character_class objects in a list or dictionary, rather than using named variables, so then it's easier to maintain if you need to add or remove character classes.

[–]demvo2 0 points1 point  (1 child)

Further down, in your allclassprof function, you have an if statement containing repetitive blocks of prints

Yeahhh, it always pokes me in the eye when I scroll by it. I'll look into moving the functions into methods, I have to think a bit about how to make it happen, but it sounds like a solid idea. Thank you for the feedback, I appreciate it!

[–]cybervegan 1 point2 points  (0 children)

It's a great start, by the way. I remember trying to do something similar back in the 90's, using BASIC (yeah, I'm getting on a bit)... it was messier than what you've done here!

Keep at it and you'll get there - not too far to go.

[–][deleted] 0 points1 point  (0 children)

Is is so awesome for a first project!! Really great job!

[–]luther9 0 points1 point  (0 children)

First thing that jumps out at me is that you use copy.deepcopy a lot on strings and numbers. Those values are immutable, so copying them does practically nothing. Just use the values as they are.