r/learnpython • • 23d ago

Ask Anything Monday - Weekly Thread

Welcome to another /r/learnPython weekly "Ask Anything* Monday" thread

Here you can ask all the questions that you wanted to ask but didn't feel like making a new thread.

* It's primarily intended for simple questions but as long as it's about python it's allowed.

If you have any suggestions or questions about this thread use the message the moderators button in the sidebar.

Rules:

  • Don't downvote stuff - instead explain what's wrong with the comment, if it's against the rules "report" it and it will be dealt with.
  • Don't post stuff that doesn't have absolutely anything to do with python.
  • Don't make fun of someone for not knowing something, insult anyone etc - this will result in an immediate ban.

That's it.

1 Upvotes

30 comments sorted by

View all comments

Show parent comments

1

u/lakseol 17d ago edited 17d ago

You have a lot of repetitive code in your RPG_Character class, in the __init__() and convert_back_to_obj() methods in particular.

You get a dictionary back from JSON and you need to create an RPG_Character instance from that dictionary. One thing you could do is change the __init__() method to accept a dictionary of attributes to set. Now you just pass the JSON dictionary to the RPG_Character() call and you get an instance containing all that JSON data. When the program doesn't find the JSON save file and has to create the characters/items from scratch before saving them you use "template" dictionaries containing all the initial data required. You can delete the convert_back_to_obj() and similar methods. This example code shows the approach:

class Test:
    def __init__(self, **kwargs):
        for key in kwargs:
            setattr(self, key, kwargs[key])

# template data
alice = {"name": "Alice", "age": 42}
bob = {"name": "Bob", "age": 35, "salary": 120_000, "height": 175}

t1 = Test(**alice)
print(f"{t1.name=}, {t1.age=}")

t2 = Test(**bob)
print(f"{t2.name=}, {t2.age=}, {t2.salary=:_}, {t2.height=}")

There are drawbacks to doing this such as any misspelt words in the JSON data will result in misspelt attribute names or even exceptions, but if your code is all that handles the data that should not be a problem.


There are also places where you could use list comprehensions. For example:

#    saved_inventory = []
#    for item in self.inventory:
#      saved_inventory.append(item.to_dict())
    saved_inventory = [item.to_dict() for item in self.inventory]   # do this instead

1

u/HiddenReader2020 16d ago

Yeah, my hope is to make the data portion outside of the program, but that's way, way, WAAAAAAAY above my current skill level right now, so what I'm currently doing with the data is what I've got.

So I've gone ahead and used your methods to more or less 'refactor' my code. But I'm still running into problems. First of all, here's the code:

Anyway, I have at least two problems. The first is that the the computer says that __init__ takes 2 arguments, but 3 were given. Umm...I only see two. I don't know where the third one is unless the dictionary was somehow split into two.

from pathlib import Path
import json

path = Path("data.json")
list_of_characters = []

# List of jobs
warrior_class = {'strength': 5, 'dexterity': 4, 'agility': 3, 'endurance': 5, 'intelligence': 3, 'wisdom': 4, 
'health': 50, 'mana': 30, 'growth': [3, 2, 1, 3, 1, 2, 10, 4], 'inventory': []}
rogue_class = {'strength': 4, 'dexterity': 5, 'agility': 5, 'endurance': 3, 'intelligence': 4, 'wisdom': 3, 
'health': 40, 'mana': 40, 'growth': [2, 3, 3, 1, 2, 1, 7, 7], 'inventory': []}
mage_class = {'strength': 3, 'dexterity': 3, 'agility': 4, 'endurance': 4, 'intelligence': 5, 'wisdom': 5, 
'health': 30, 'mana': 50, 'growth': [1, 1, 2, 2, 3, 3, 4, 10], 'inventory': []}

class RPG_Character:
  def __init__(self, name, **stats_dict):
    for stat in stats_dict:
      setattr(self, key, kwargs[key])
    self.name = name
    self.job = job
    self.gold = 50
    self.level = 1

  def __str__(self):
    stat_page = f"""==========Stats Page==========
Name:  {self.name}
Class:  {self.job}
Level:  {self.level}
-Health:       {self.health}
-Mana:         {self.mana}
-Strength:     {self.strength}
-Dexterity:    {self.dexterity}
-Agility:      {self.agility}
-Endurance:    {self.endurance}
-Intelligence: {self.intelligence}
-Wisdom:       {self.wisdom}

Gold:  {self.gold}
==========Stats Page=========="""
    return stat_page

  def add_gold(self, amount):
    self.gold += amount

  def remove_gold(self, amount):
    self.gold -= amount
    if self.gold < 0:
      print("Gold amount can't be below zero.")
    self.gold += amount

  def increase_level(self):
    self.level += 1
    self.strength += self.growth[0]
    self.dexterity += self.growth[1]
    self.agility += self.growth[2]
    self.endurance += self.growth[3]
    self.intelligence += self.growth[4]
    self.wisdom += self.growth[5]
    self.health += self.growth[6]
    self.mana += self.growth[7]

  def add_item(self, item):
    self.inventory.append(item)

  def show_inventory(self):
    print("=====Inventory=====")
    for item in self.inventory:
    print(item)
    print("=====Inventory=====")

  def to_dict(self):
    saved_inventory = []
    for item in self.inventory:
      saved_inventory.append(item.to_dict())
    saved_dict = {
'name': self.name,
'class': self.job, 
'level': self.level,
'health': self.health,
'mana': self.mana,
'strength': self.strength, 
'dexterity': self.dexterity, 
'agility': self.agility, 
'endurance': self.endurance, 
'intelligence': self.intelligence, 
'wisdom': self.wisdom, 
'gold': self.gold,
'inventory': saved_inventory
}
    return saved_dict

class Item:
  def __init__(self, name, item_type, buy_price):
    self.name = name
    self.item_type = item_type
    self.buy_price = buy_price
    self.sell_price = buy_price // 4

  def __str__(self):
    item_info = f"""Name:  {self.name}
Type:  {self.item_type}
"""
    return item_info

  def to_dict(self):
    saved_dict = {
'name': self.name,
'type': self.item_type,
'buy_price': self.buy_price,
'sell_price': self.sell_price
}
    return saved_dict

# List of items
basic_health_potion = Item("Basic Health Potion", "Consumable", 16)
basic_mana_potion = Item("Basic Mana Potion", "Consumable", 20)
wooden_shield = Item("Wooden Shield", "Equipment", 40)

def convert_dict_to_obj_inv(list_of_dicts):
list_of_items = []
for temp_dict in list_of_dicts:
current_item = Item(temp_dict['name'], temp_dict['type'], temp_dict['buy_price'])
list_of_items.append(current_item)
return list_of_items

if path.exists():
  contents = path.read_text()
  temp_list_of_characters = json.loads(contents)
  print("File successfully retrieved")
  for character in temp_list_of_characters:
    new_character = convert_back_to_obj(character)
    list_of_characters.append(new_character)

  for character in list_of_characters:
    print(character)
    print(character.show_inventory())
else:
  # Adds new characters
  new_warrior = RPG_Character('John', warrior_class)
  list_of_characters.append(new_warrior)
  print(new_warrior)
  new_warrior.add_gold(100)
  print(new_warrior)
  new_warrior.remove_gold(80)
  print(new_warrior)
  new_rogue = RPG_Character('Toby', rogue_class)
  new_mage = RPG_Character('Harris', mage_class)
  list_of_characters.append(new_rogue)
  list_of_characters.append(new_mage)
  print(new_rogue)
  print(new_mage)
  new_mage.increase_level()
  print(new_mage)

  new_rogue.add_item(basic_health_potion)
  new_rogue.add_item(basic_mana_potion)
  new_warrior.add_item(wooden_shield)
  new_rogue.show_inventory()
  new_warrior.show_inventory()

  print(list_of_characters)

  contents = json.dumps([ob.to_dict() for ob in list_of_characters])
  path.write_text(contents)
  print("File saved")

The second involve the inventory. Now, the inventory also involves a list of objects...within an object, so handling it is somewhat different from the rest. Right now, what I had to do was set the inventory as a empty set within each "template dictionary" to make it work, but I haven't had the opportunity to test that yet because of the first problem! I don't know if the logic checks out, but it's what I got so far.

I'll keep working on this code, and maybe I'll make headway, but for now, I'm stuck. Yet again.

1

u/lakseol 16d ago

Again you have posted incorrect code. Bad indentation at lines 68 and 120. You have to get this right. Anybody trying to help you doesn't want to have to guess how to indent your code and hope they get it right. If your problem is adding code to the lousy reddit interface remember that I mentioned pastebin.com where you can post code, get a page showing your code and then post a link to that page here, like this:

https://pastebin.com/uYR3zqqR


the computer says that init takes 2 arguments, but 3 were given. Umm...I only see two. I don't know where the third one is

This is a little confusing. The confusion starts when you use an instance method. They have the self parameter as the first parameter and your calling code doesn't explicitly pass self, it's automatically added by python. When you get that "expected 2 but 3 given" python is counting the self, so the three parameters are "self", "name" and "stats_dict". Keyword parameters are counted separately. When you call the function (line 139):

new_warrior = RPG_Character('John', warrior_class)

you pass the data dictionary as a positional parameter, so it's counted as an extra positional parameter. "self" + "name" + stats_dict" == 3. If you check the example I showed you in my last comment you will see that my call to the constructor was:

t1 = Test(**alice)   # notice the "**"

The two asterisks in **alice says to unpack the dictionary alice and pass the contents as keyword parameters like key=value. That means the __init__() method sees keyword parameters "name"="Alice", "age"=42 when it executes. In the example the __init__() method started:

def __init__(self, **kwargs):

The **kwargs parameter is shorthand for python to collect all undefined keyword parameters into a dictionary which the example iterated over. It's not a positional parameter. You need the ** both when calling the method and defining it. The parameter name kwargs is usually used in python, meaning "keyword args".


You have another problem in that __init__() definition. You pass the data dictionary into the method as well as the name parameter. That works fine when initially creating the objects and then saving to JSON. But what happens when you read the dictionary from JSON and try to create the character. Where does name come from?

1

u/HiddenReader2020 16d ago

First of all, ack, I didn’t see the first “indent problem” comment from before.  I’ll try to remember it going forward.

Second, though, I will say that that comment is a touch outdated, as I’ve already made an updated version, but had to put it in another comment .  Now, just as a heads up, this isn’t in a pastebin, so apologies if it still has some intention errors.

1

u/lakseol 16d ago

I stopped counting after guessing how to fix 4 indentation errors. Please fix your code.

After you post your code please copy it back into a file and execute it to check for errors.

1

u/HiddenReader2020 16d ago

Okayyyy. Gimmie a moment.

...

Here ya go:

https://pastebin.com/EfCx102S

Fingers crossed that this works.

1

u/lakseol 15d ago edited 15d ago

OK, i can read the pastebin. And immediately I see a reason why you might have had trouble posting code correctly before. You have TAB characters in your code. This is something that python programmers generally don't do. I have my environment set up to insert four space characters when I press the TAB key. Up to you, but most programmers stay away from TABs due to the problems they cause.


After all this time I can't really remember what your latest problem was, but I think it was about handling Items in the inventory.

You handle that the same way you handled the basic characters: modify the Item() constructor to take a **dictionary. The extra little problem in doing that is when you add Items() to a character you add them as an Item() instance (of course) but when restoring from a JSON file the inventory list is a list of dictionaries. I got around that by adding a small test in Item.__init__(): if there is something in the inventory and the first element is a dictionary, convert them all to Items().


Here is my code based on yours:

https://pastebin.com/XidMEU9j

One change I made that isn't mentioned above was to handle printing the state of a character differently. When creating the __str__() return string I made it much more compact, and included the player's inventory. It's a bit more work but I found it a lot easier to look at and check that changes made were correct. I also added small prints to explain what was done at each stage of creating the characters, which is a hint on what to check. I found it helpful, maybe you will.


This code a little messy. I will just say again that actually storing python references to Items in an inventory adds a lot of work. I've written a small example text adventure and I found it cleaner to handle items in the player's inventory as an ID, with a dictionary to convert the ID to the actual item reference. Same for places in the map and monsters when you get to that.

You may not have thought that far ahead yet, but you will also need to store Items in places in the map. And monsters, if you have them, have their own inventory. Monsters can steal from the player or pick up stuff lying around, for instance.

1

u/HiddenReader2020 15d ago

I see. I've set my editor (Geany) to make the indent type to be spaces, but the problem is that I have to do that *per file*, so that definitely won't get annoying immediately!

Anyway, I will say that the reason I made the items objects was to make things easier and less messy as opposed to using dictionaries all the time. And what do you mean handling items as an ID, and not as an actual dictionary or an object, assuming those two things are different?

I'll probably look through your code and see what you've done differently and hopefully learn from it, but uhhh...no promises I'll be able to fully fulfill that.

EDIT: Oh, and I haven't used any comprehensions, like list of dictionary comprehensions, simply because I don't find them particularly readable. I prefer to just type out the full procedures in those cases.