Ren'Py Creating a 'smart' Ren'py inventory system (using lists in Python)

danasavage

Member
Game Developer
Mar 9, 2020
147
273
Hi! Some of you might know me from my HTML/Twine game Girl Games! Well, I'm currently trying to port it across to Ren'py and it was all going great until i start trying to replicate some of the more advanced features!

The main snag I've hit upon is in my wardrobe/inventory. I've managed to get a working Inventory system going which uses Objects and Classes. I've cobbled it together using parts from a couple of inventories on the LemmaSoft forums, namely this and this ... well, so far it's doing what I want it to. I have a 'Wardrobe' screen that lists both what my Player(Inventory) is wearing and also what the Wardrobe(Inventory) contains, and if I click on any item in either list it automatically moves it from one inventory to the other ... Just as I intended.

Only issue is, say the player is wearing a t-shirt, and they click on another t-shirt from their wardrobe, it would put TWO shirts on them, when in reality they'd swap from one to the other. Or what if they were wearing pants and a shirt and they wanted to put a dress on - they'd need to take off BOTH things first, right?

Well, I had a solution for this in my Twine game, which was code that was kindly written for me by Hiev over on the TFGames forum, and now I'm trying to write something similar in Python. Only issue is, my python coding skills are totally not up to scratch!!

The way the orignal verson worked, I would add in a 'place' tag to each item in my inventory - so for pants it would be place = ["legs"], shirt woud be place = ["torso"] and dress would be place = ["legs", "torso"] .. I know that I can do the same thing in Python with lists. And in theory work up some way of cycling through to check if each new item the player wanted to wear already had that tag in anything they were currently wearing and if so send it from the player back to the wardrobe.

Maybe it's a little clearer if I share the code i have currently. This is for my Ren'py version:

Python:
init python:
    import renpy.store as store


    class Clothing(store.object):
        def __init__(self,name,desc,place):
            self.name = name
            self.desc = desc
            self.place = place
            place = []   ### <--- I feel like this is going to be important later!


    class Inventory(store.object):
        def __init__(self, name):
            self.name=name
            self.wearing=[]

        def remove(self,clothes):
            if clothes in self.wearing:
                self.wearing.remove(clothes)
            return

        def wear(self,clothes):
            self.wearing.append(clothes)
            return

        def remove_all(self):
            list = [item for item in self.wearing]
            if list!=[]:
                for item in list:
                    self.wearing.remove(item)
            return

    def trade(seller, buyer, item):
        seller.remove(item)
        buyer.wear(item)
And I'm creating each of my clothing items like so:

Python:
$ pants = Clothing(name="pants", desc="a pair of pants", place=["legs"])
$ tshirt = Clothing(name="shirt", desc="a boring old t-shirt", place=["torso"])
$ dress = Clothing(name="dress", desc="a pretty blue dress", place=["legs","torso"])
As you can see, in the Clothing objects I'm adding in a "place" list, which specifies which place on the body the item is to be worn. Which means that in theory, if my player was wearing pants and a shirt and they put on a dress - I could use Python to make sure that any items the player was currently wearing with those matching tags got sent to the wardrobe!

At present, i'm using the 'trade' function from the list above to move each item back and forth between the player and wardrobe inventories, but I know I need to make a new, more complex function - let's say it's called clothes_check.

And I know that in theory, it needs to do (roughly) the following things:

1) check the "place" tags of the new item selected
2) compare them to the "place" tags of all items the Player is currently wearing
3) move any items with matching "place" tags to the Wardrobe
4) move the new item onto the Player

Only problem is, I don't know HOW to do this. I only have a really basic beginner's knowledge of Python. I've been reading up on lists and I know that there's a number of functions that might work, ways of comparing two lists, like about comparing the common elements in two lists using Python ... But even so, translating that into a working inventory function just seems like one step beyond me!

I mean, I obviously want to figure this out on my own, but I feel like it's a three-week Python learn-at-home course away and I really wanted to get my Inventory system up and running asap! If anyone has any ideas of how to create this function, that would be totally amazing!

The closest I've got on my own is this total trainwreck:
Python:
def clothes_check(player, wardrobe, item)
        player.wear(item)
        for [place] in player.wear(item):
            if [place] in player.wearing:
                 wardrobe.wear(item)
which obviously doesn't work! So yeah, if anyone has any ideas or solutions that would be awesome! Thank you in advance. :)

p.s. I have also posted a similarly-worded version of this question over on the LemmaSoft forum too. If it gets answered there, I'll make sure to update this post with the solution, just in case anyone ever runs into the same issue in the future!
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
So yeah, if anyone has any ideas or solutions that would be awesome!
Normally you just need to change the wear method to check if there's a conflict between the slot(s) used by the clothes actually worn and the slot used by the new clothe.

Something like this should do it :
Code:
        def wear(self,clothes):
            # Browse all the actually worn clothes.
            for c in self.wearing:
                # For each one, browse all the slots used.
                for p in c.place:
                    #  If at least one is part of the slot(s) used
                    # by the new clothe...
                    if p in clothes.place: 
                       # remove the old clothe.
                       self.remove( c )
            # Then wear the new clothe.
            self.wearing.append(clothes)
            return
 

danasavage

Member
Game Developer
Mar 9, 2020
147
273
OMG, thank you for your speedy reply! I've tried out your code, and it's almost working but unfortunately there's still a few strange things going on which I don't have the coding smarts to figure out!

Whereas before the wardrobe screen looked like this, with two distinctive lists of items (wardrobe and player):


Screen Shot 2020-06-18 at 14.59.11.png

Now, when I open up the screen, it seems to duplicate every item for both the player and wardrobe, cascading for what i'm guessing is infinity off the screen!

Screen Shot 2020-06-18 at 14.57.32.png

Also, when I click on any item the game crashes and i get the error message:

Code:
I'm sorry, but an uncaught exception occurred.

While running game code:
  File "game/intro.rpy", line 88, in script
    menu:
  File "renpy/common/00nvl_mode.rpy", line 487, in nvl_menu
    type="nvl",
  File "renpy/common/00action_other.rpy", line 537, in __call__
    rv = self.callable(*self.args, **self.kwargs)

TypeError: 'NoneType' object is not callable

I've tried out a few things on my own but nothing has worked and again I'm a little stumped. I'm aware that I'm totally out of my depth when it comes to Python. I do intend to learn more about lists/etc so if problems like this come up in future I can solve them on my own, but at the moment I'm afraid I'm drawing a blank!!

Again, thank you so much for your input - and any more thoughts on what I could try/change to get it working?

(Part of me wonders if there's some kind of conflict/mess going on in the way i merged two inventory systems - with one referring to 'items' and the other referring to 'clothing' ... but again, i have no idea really how to solve this on my own!!)
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
Now, when I open up the screen, it seems to duplicate every item for both the player and wardrobe, cascading for what i'm guessing is infinity off the screen!
Strange. There's no obvious reason for my code to lead to this reaction, since it shouldn't be used while building the screen.


Code:
I'm sorry, but an uncaught exception occurred.

While running game code:
  File "game/intro.rpy", line 88, in script
    menu:
  File "renpy/common/00nvl_mode.rpy", line 487, in nvl_menu
    type="nvl",
  File "renpy/common/00action_other.rpy", line 537, in __call__
    rv = self.callable(*self.args, **self.kwargs)

TypeError: 'NoneType' object is not callable
The error come from the Function screen action. The code apparently try to call a function that don't exist. But I can't really say more, it come from the screen, and apparently not from the code you gave.
 

danasavage

Member
Game Developer
Mar 9, 2020
147
273
The error come from the Function screen action. The code apparently try to call a function that don't exist. But I can't really say more, it come from the screen, and apparently not from the code you gave.
Ah!!! That was totally the case, yes. I just searched the error again in relation to function and it turned out it was the way I was writing it in the inventory screen. i.e.

action(Function(wardrobe_inv.try_on(item))) <-- which was throwing up the error message.

and instead should have been:

action(Function( wardrobe_inv.try_on, item )) !!

OMG, i'm so close to getting this working now - thank you again. That said, I'm still not getting the inventory to function quite how I want it to. There's no error messages any more, no cascading items ... but (and maybe i'm missing something obvious here) it's still not doing the whole "Move anything with corresponding tags to the wardrobe" thing I need it to :(

To recap, here are my Inventory, Clothing objects, defined functions and also my items:

Python:
init python:

    import renpy.store as store



    class Clothing(store.object):
        def __init__(self,name,desc,place):
            self.name = name
            self.desc = desc
            self.place = place
            place = []



    class Inventory(store.object):
        def __init__(self, name):
            self.name=name
            self.wearing=[]


        def remove(self,item):
            if item in self.wearing:
                self.wearing.remove(item)
            return


        def wear(self,item):  ### Note: I tried to clean up the definitions, as when i was merging the two inventory codes, some things were 'items' some were 'clothing', etc, etc - hope i haven't fucked something up in the process!
            self.wearing.append(item)
            return


        def has_item(self, item):
            if item in self.wearing:
                return True
            else:
                return False


        def try_on(self,item): ##note that i made this a seperate "try on" function instead of modifying "wear" just in case i need that too at some point
            # Browse all the actually worn clothes.
            for c in self.wearing:
                # For each one, browse all the slots used.
                for p in c.place:
                    #  If at least one is part of the slot(s) used
                    # by the new clothe...
                    if p in item.place:
                       # remove the old clothe.
                       self.remove( c ) ### <-- i initially thought this was supposed to be clothes so switched it to 'item' and that wasn't working. but now i have realized it was supposed to be c all along!
            # Then wear the new clothe.
            self.wearing.append(item)
            return


        def remove_all(self):
            list = [item for item in self.wearing]
            if list!=[]:
                for item in list:
                    self.wearing.remove(item)
            return


    def trade(seller, buyer, item):
        seller.remove(item)
        buyer.wear(item)



###

$ jeans = Clothing(name="Jeans", desc="blue jeans", place=["legs"])
    $ tshirt = Clothing(name="T-shirt", desc="white tee", place=["torso"])
    $ boxers = Clothing(name="Boxer shorts", desc="white boxer shorts", place=["crotch"])

    $ socks = Clothing(name="Socks", desc="sports socks", place=["shins"])
    $ sneakers = Clothing(name="Sneakers", desc="beat up old sneakers", place=["feet"])

    $ towel = Clothing(name="Towel", desc="a basic towel", place=["torso", "chest", "legs", "crotch", "shins", "feet"])
And here's my current inventory screen:

Code:
screen inventory_screen():
    zorder 60
    modal False
    frame:
        style_group "invstyle"
        area (0, 0, 440, 520)
        xalign 0.5 yalign 0.5 xpos 430 ypos 116
        hbox:
            spacing 25
            vbox:
                text "{font=GreatVibes-Regular.ttf}{color=#ff00da}{size=50}Wardrobe{/size}{/color}{/font}{vspace=5}"
                text "{u}Your wardrobe contains{/u}:\n"
                if len(wardrobe_inv.wearing) == 0:
                    text "Nothing"
                else:
                    for item in wardrobe_inv.wearing:
                        $ name = item.desc
                        textbutton ("[name]") action(Function( player_inv.try_on, item ))

                text "\n\n{u}You are wearing{/u}:\n"
                if len(player_inv.wearing) == 0:
                    text "Nothing"
                else:
                    for item in player_inv.wearing:
                        $ name = item.desc
                        textbutton ("[name]") action(Function( wardrobe_inv.try_on, item))
As i said, it's all "working" again now, in the sense that there's no error messages ... but it's not working in quite the way it should.

So I open up my wardrobe screen and it's all looking good:

Screen Shot 2020-06-19 at 11.54.37.png

But when I click on the towel , this happens:

Screen Shot 2020-06-19 at 12.09.36.png

Then if I click on white tee ... this happens!


Screen Shot 2020-06-19 at 12.10.06.png

Yeah. It's so close - I worry this latest issue is perhaps due to somethng in the way I maybe modified the code to try and merge 'clothing' and 'items' and whatnot. or is it just something else that needs tweaking?

(And obviously i'm not expecting the paperdoll to change just yet - thats a whole 'nother puzzle to figure out! But right now i just wanna get the wardrobe/player inventory list part sorted.)

Thank you again for helping me out with this btw! I really do appreciate it! :)
 
Last edited:
  • Like
Reactions: SenMizeri

danasavage

Member
Game Developer
Mar 9, 2020
147
273
Holy cow .. I am SOOO close to getting this working now! I've basically got your code almost functional anne O'nymous!! After a little tinkering and experimenting, I came up with this version which does 99% of what I want it to!:

Python:
def try_on(self,item):
            # Browse all the actually worn clothes.
            for all_clothes in self.wearing:
                # For each one, browse all the slots used.
                for matching_tags in all_clothes.place:
                    #  If at least one is part of the slot(s) used
                    # by the new clothe...
                    if matching_tags in item.place:
                        wardrobe_inv.wear(all_clothes) ##(<-- this was the only part I added, so that the discarded item moves across properly instead of just disappearing into thin air, as before. I also changed the abstract c and p to words I could keep track of, so that i could work out what everything did at each step!)
                        self.remove(all_clothes)
            self.wearing.append(item)
            wardrobe_inv.drop(item)##(<-- oh and I added that too, so that the item would be removed from the player's wardrobe)
            return
As wanted, it now cycles through the 'place' tags of each item and moves the items across. Only issue is, it only moves the first item it comes across with each tag. So shoes (which just have a single 'feet' tag) work 100% perfectly. But if it's say a dress - with both 'legs' and 'torso' tags - it just moves the first item with that tag across.

I know there must be some way of getting it to pause there and just loop around that section of "all_clothes" until every item with the tag in that section is moved across then carry on, right? If Anne - or anyone else - can shed some light on this, i'd be so so grateful! :)
 
  • Like
Reactions: SenMizeri

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
Only issue is, it only moves the first item it comes across with each tag. So shoes (which just have a single 'feet' tag) work 100% perfectly. But if it's say a dress - with both 'legs' and 'torso' tags - it just moves the first item with that tag across.
Hmm... Yeah... it's my fault ; at my defense, I past the last few weeks using many languages but never Python.

The error should come from here :
Code:
           for all_clothes in self.wearing:
[...]
                        self.remove(all_clothes)
Modifying the list inside the loop is a really bad idea with Python, because the list is iterated in real time.

By example, this :
Code:
aaa = [ "aaa", "bbb", "ccc", "ddd" ]
for a in aaa:
    print( a )
    aaa.remove( a )
Will give you :
"aaa"
"ccc"
Therefore, you need to works with a copy of the list. Just change the for line to this :
Code:
           for all_clothes in [a for a in self.wearing ]:
It will create an anonymous list that will be a copy of self.wearing as it was before the loop start.

Not sure it will really change something, but you can also stop the second loop when you remove the item.

In the end, the code would looks like :
Python:
      def try_on(self,item):
            for all_clothes in [a for a in self.wearing]:
                for matching_tags in all_clothes.place:
                    if matching_tags in item.place:
                        wardrobe_inv.wear(all_clothes)
                        self.remove(all_clothes)
                        break       # The clothe is already removed, no need to test more for it.
            self.wearing.append(item)
            wardrobe_inv.drop(item)
            return
I have the feeling that I forgot something, but like I said, I haven't used Python lately, and in my head it's a big mess ; I'm not sure anymore how Python react to this or that :(
 

danasavage

Member
Game Developer
Mar 9, 2020
147
273
The error should come from here :
Haha, you were totally right! I was so impatient and eager to get this solved I actually asked a question over on StackOverflow and the responder basically said the same thing: that self.wearing: needed an extra [:] to make it into a copy of a list? Tbh, I don't totally understand it - I do want to get much better at Python going forward but for now, I'm really more of a writer than a game dev, but holy shit. it's working and that's the main thing. Thank you SO MUCH again for all your help on this! I'm so excited to get this version of my game finished and out now!

For anyone that's wondering or strays across this post in the future, the full final code for the working function is:

Code:
def try_on(self,item):
            # Browse all the actually worn clothes.
            for all_clothes in self.wearing[:]:
                # For each one, browse all the slots used.
                for matching_tags in all_clothes.place:
                    #  If at least one is part of the slot(s) used
                    # by the new clothe...
                    if matching_tags in item.place:

                        wardrobe_inv.wear(all_clothes)
                        self.remove(all_clothes)

            self.wearing.append(item)
            wardrobe_inv.remove(item)
            return
 
Nov 23, 2017
72
63
needed an extra [:] to make it into a copy of a list?
I know it is probably irrelevant now, but that is the extended slicing[1] notation. It works because, even though, when used like this it doesn't really slice anything, it still returns a copy of the array, which was the intention here. Slicing like this is a powerful feature of the language and is certainly useful to anybody.

[1]
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
[...] it still returns a copy of the array [...]
The "still" don't have its place here. It's the default way to have a copy of a list.

When you assign a list to a variable, Python do not copy it, it just duplicate the reference to the list ; whatever if you address the list through its initial name or through its new name, it will make absolutely no difference.

If you play this code :
Code:
label start:
    $ a = [1,2,3]
    $ b = a
    call compare
    "----"
    $ a = [1,2,3]
    $ b = a[:]
    call compare
    "END"
    return

label compare:
    $ idA = id(a)
    $ idB = id(b)
    "[idA] <-> [idB]"
    if a is b:
        "Both variables are strictly identical."
    else:
        "Those variables are different."
    $ a.append( 4 )
    "b: [b]"
    $ b.append( 5 )
    "a: [a]"
    return
You'll get something that will looks like this :
93568528 <-> 93568528
Both variables are strictly identical.
b: [1, 2, 3, 4]
a: [1, 2, 3, 4, 5]
----
93568528 <-> 93667417
Those variables are different.
b: [1, 2, 3]
a: [1, 2, 3, 4]
END
Note that the same principle apply to all the data type. Whatever if a is a list, a dictionary, or an object, b = a will not generate a copy of a but duplicate its reference.

It also apply to strings and numbers, but with no consequences. In Python variables are immutable, which mean that instead of changing the value, Python recreate the variable with its new value. Therefore, for strings and numbers, the duplication do not survive to an update of the value.
If you play this :
Code:
label start:
    $ a = 1
    $ idA = id(a)
    "[idA]"
    $ a += 1
    $ idA = id(a)
    "[idA]"
You'll get two different values ; the variable isn't the same and so its id is different.

But lists, dictionaries and objects works differently. The variable do not store directly the list, dictionary or object, but the reference to it. Therefore an update will change what's behind the reference, but not the reference itself.
Code:
label start:
    $ a = [ 1, 2 ]
    $ idA = id(a)
    "[idA]"
    $ a.append( 3 )
    $ idA = id(a)
    "[idA]"
Will give you the same value twice.

If b = a[:] works, it's because it assign to b a slice of a. It happen that this slice is the entirety of the list, but it's not enough for Python to change its behavior and do not create a totally new variable to receive this list.
 
  • Like
Reactions: VentureZapatist
Nov 23, 2017
72
63
The "still" don't have its place here. It's the default way to have a copy of a list.

When you assign a list to a variable, Python do not copy it, it just duplicate the reference to the list ; whatever if you address the list through its initial name or through its new name, it will make absolutely no difference.

If you play this code :
Code:
label start:
    $ a = [1,2,3]
    $ b = a
    call compare
    "----"
    $ a = [1,2,3]
    $ b = a[:]
    call compare
    "END"
    return

label compare:
    $ idA = id(a)
    $ idB = id(b)
    "[idA] <-> [idB]"
    if a is b:
        "Both variables are strictly identical."
    else:
        "Those variables are different."
    $ a.append( 4 )
    "b: [b]"
    $ b.append( 5 )
    "a: [a]"
    return
You'll get something that will looks like this :


Note that the same principle apply to all the data type. Whatever if a is a list, a dictionary, or an object, b = a will not generate a copy of a but duplicate its reference.

It also apply to strings and numbers, but with no consequences. In Python variables are immutable, which mean that instead of changing the value, Python recreate the variable with its new value. Therefore, for strings and numbers, the duplication do not survive to an update of the value.
If you play this :
Code:
label start:
    $ a = 1
    $ idA = id(a)
    "[idA]"
    $ a += 1
    $ idA = id(a)
    "[idA]"
You'll get two different values ; the variable isn't the same and so its id is different.

But lists, dictionaries and objects works differently. The variable do not store directly the list, dictionary or object, but the reference to it. Therefore an update will change what's behind the reference, but not the reference itself.
Code:
label start:
    $ a = [ 1, 2 ]
    $ idA = id(a)
    "[idA]"
    $ a.append( 3 )
    $ idA = id(a)
    "[idA]"
Will give you the same value twice.

If b = a[:] works, it's because it assign to b a slice of a. It happen that this slice is the entirety of the list, but it's not enough for Python to change its behavior and do not create a totally new variable to receive this list.
I meant to say that the slice object is different than the original list. I agree the "still" was misplaced, that's my bad.

There are other ways to copy a compound object. I believe python 2.7 has a built-in copy() method on such objects too, although I'm not familiar with its internal behavior.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
I meant to say that the slice object is different than the original list. I agree the "still" was misplaced, that's my bad.
Don't worry, I'm far to be the last when it come to make an error and/or write too fast.
My answer wasn't a reproach, nor directed against you. The list = otherlist error is relatively frequent, so I jumped on the occasion to explain why.


There are other ways to copy a compound object. I believe python 2.7 has a built-in copy() method on such objects too, although I'm not familiar with its internal behavior.
Only dictionaries have a copy method, you need to import the "copy" module to make a copy of a list without using the slice trick, or to make a copy of an object.

But I don't recommend it. It works perfectly fine, but the notion of "shallow copy" and "deep copy" are probably too confusing for most game devs, because they generally start with a basic coding knowledge, when not no knowledge at all. Since the copy limitation regard only objects, it's better to point them an alternative design that don't need to copy the object.
 
  • Like
Reactions: VentureZapatist