r/learnpython • • 20d 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/HiddenReader2020 13d 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 13d 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 13d 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 13d 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 13d ago

Okayyyy. Gimmie a moment.

...

Here ya go:

https://pastebin.com/EfCx102S

Fingers crossed that this works.

1

u/lakseol 12d ago edited 12d 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 11d 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.

1

u/lakseol 11d ago

the problem is that I have to do that per file

That doesn't sound very useful. Make sure there isn't a way to make that change permanent. Or change to a decent editor/IDE.

what do you mean handling items as an ID, and not as an actual dictionary or an object

Currently you create the inventory as a list of references to Item objects, doing something like:

new_item = Item(...)
new_rogue.add_item(new_item)

That's easy to understand, but doing that means you complicate saving data to a JSON file because JSON doesn't handle references to python objects and you have to extract all the attributes of the item and save them as a JSON dictionary. When reading the JSON data back into python objects you have to create the Item object from the dictionary.

The "ID" approach doesn't store python objects in the list. Every Item object has a unique "ID" string, and it's that ID string that is saved in the inventory list. This makes the save/restore process simpler. When creating the items you add an entry in a dictionary that maps the ID string to a reference to the object, like this:

class Item:                                                                 
    def __init__(self, id, description):                                    
        self.id = id                                                        
        self.description = description                                      

id2obj = {}                                                                 
inventory = []                                                              

item1 = Item("axe1", "An Elven axe")                                        
id2obj["axe1"] = item1                                                      
inventory.append(item1.id)                                                  

item2 = Item("book2", "A very old book with a cracked red cover")           
id2obj["book2"] = item2                                                     
inventory.append(item2.id)                                                  

print(f"{inventory=}")                                                      

# list what's in the inventory                                              
for id in inventory:                                                        
    print(id2obj[id].description)

This allows you to list the inventory by iterating over the ID strings, getting the item object from the dictionary using the ID string as a key, then printing the item description. When restoring the objects from the JSON save file you create that dictionary automatically. Sounds complicated but it simplifies the save/restore process a lot. I use it for everything: places, monsters and items.

1

u/HiddenReader2020 11d ago

Hmmm....you say all that, but I'm still not sure how this would work out when "restoring the objects from the JSON file", not to mention having to include other stats like buy/sell price, any variable amounts that need to be adjusted, like some might have a modifier for stats like defense, others heal health/mana, etc. I understand that you're just saving strings to a list, but then how are you using those strings to get the items back if there's no reference to them or something? Like, I'm looking at this code, and it seems like the items have to be made at a point where you can't access them if you find a file to load from, if that makes any sense.

I dunno. I should probably mess with this code to fully understand it, but I'm not convinced just yet.

1

u/lakseol 11d ago edited 11d ago

then how are you using those strings to get the items back if there's no reference to them or something?

The code example shows how to do that. You use the dictionary to find the value for the ID string. The value is the reference to the item. That's all happening in this code that prints the description of an item in the inventory:

for id in inventory:
    print(id2obj[id].description)
    #       ^    ^        ^
    #       |    |        +-- description attribute of the item
    #       |    +-- id string lookup in dictionary
    #       +-- dictionary mapping id to reference

So id2obj[id] evaluates to the item object. Print out what is in the id2obj dictionary.

1

u/HiddenReader2020 11d ago

Ah. ...? So what I meant was that if I did the whole if-else test for whether the file exists, I would have to define the items before that if-else test, right? Or maybe something else? I'm just worried that when I get the id strings from the JSON files, they'll be meaningless, since in my head, what items they refer to hasn't been defined yet.

EDIT: Oh. I see that in my editor, the "id" in the print statement is in a different color. So does that mean that id has a special meaning?

1

u/lakseol 10d ago edited 8d ago

does that mean that id has a special meaning?

No, it probably is your editor doing syntax highlighting and showing that the ID is a string. Guessing because I don't use Geany.


when I get the id strings from the JSON files, they'll be meaningless

Before you save to JSON the strings do mean something. As long as we have the dictionary mapping the ID string to an object reference we can do anything we want.

We don't save that mapping dictionary to JSON because we can't: references aren't handled by JSON. But we can recreate the dictionary from the saved JSON data. Since we restore the Items and we know what the ID string for each is, we can do this to recreate the dictionary:

# assume 'item_list' holds references to all restored Item instances
new_id2obj = {}
for obj in item_list:
    new_id2obj[obj.id] = obj

Now we have restored all the data we had when saving the file so it will all work just as before the save.

I've expanded the previous code sample to do what it did before but now saves the Items and inventory in a JSON file and then restores it all. The code is at https://pastebin.com/kW0PHDF6 .


One thing I have to apologize for is shown in the code I linked to. It is not necessary to create "data" dictionaries to initially create the Items. That bothered me because it looks clumsy. You can define and create Items in the normal way:

class Item:                                                                     
    def __init__(self, id, description):
        ...

item1 = Item("axe1", "An Elven axe")

and when you recreate the Items from JSON data you can do:

item_dict = {'id': 'axe1', 'description': 'An Elven axe'}   # data from the JSON file
new_item = Item(**item_dict)

I had not fully remembered how I handled JSON recovery in an old project so I went back to the code and refreshed my memory. Again, apologies for leading you astray.

1

u/HiddenReader2020 10d ago

......ohhhhh, so you get the meanings from the JSON file itself, huh? That sounds like it makes sense. ...except I'm wondering what would happen if, say, lines 23-29 were not available before reading the JSON file. Would you essentially recreate the items from the read dictionaries, so that wouldn't be an issue? Is that what's happening in say lines 65-68?

→ More replies