Ren'Py Managing lists of lists

obsessionau

Member
Game Developer
Sep 27, 2018
270
376
I have the following lists:
Code:
    Player = []
    Spent = []
    Conversation = [[1,"Keep cool",0,"sanity",1,"conversation",1,"","","sanity",1,"","","","","conversation",-1,"","","",""]
    [2,"Keep cool",0,"sanity",1,"conversation",1,"","","sanity",1,"","","","","conversation",-1,"","","",""]
    [3,"Small talk",0,"conversation",3,"","","","","conversation",2,"","","","","conversation",1,"endhour",1,"",""]
    [4,"Small talk",0,"conversation",3,"","","","","conversation",2,"","","","","conversation",1,"endhour",1,"",""]
    [5,"What do you want?",0,"conversation",2,"flipdemand",1,"","","flipconversation",1,"","","","","sanity",-1,"","","",""]
    [6,"What do you want?",0,"conversation",2,"flipdemand",1,"","","flipconversation",1,"","","","","sanity",-1,"","","",""]
    [7,"What I mean is",1,"chancesnext",1,"","","","","","","","","","","","","","","",""]
    [8,"What I mean is",1,"chancesnext",1,"","","","","","","","","","","","","","","",""]
    [9,"It's all a misunderstanding",2,"conversation",4,"","","","","conversation",3,"sanity",-1,"","","sanity",-2,"endhour",1,"",""]
    [10,"It's all a misunderstanding",2,"conversation",4,"","","","","conversation",3,"sanity",-1,"","","sanity",-2,"endhour",1,"",""]
    [11,"You need to trust me",2,"oddshour",1,"","","","","oddsnext",1,"","","","","sanity",-1,"conversation",1,"",""]
    [12,"You need to trust me",2,"oddshour",1,"","","","","oddsnext",1,"","","","","sanity",-1,"conversation",1,"",""]
    [13,"Just stay calm",2,"sanity",2,"","","","","sanity",1,"","","","","sanity",-1,"endhour",1,"",""]
    [14,"Just stay calm",2,"sanity",2,"","","","","sanity",1,"","","","","sanity",-1,"endhour",1,"",""]
    [15,"We'll get through this",3,"eventgood",2,"","","","","sanity",1,"eventgood",1,"","","eventbad",1,"endhour",1,"",""]
    [16,"We'll get through this",3,"eventgood",2,"","","","","sanity",1,"eventgood",1,"","","eventbad",1,"endhour",1,"",""]
    [17,"Can we talk over here?",4,"conversation",2,"chancesnext",2,"","","chancesnext",2,"","","","","chancesnext",-1,"","","",""]
    [18,"A bold lie",4,"conversation",1,"sanity",3,"","","conversation",1,"sanity",1,"","","sanity",-3,"","","",""]
    [19,"Offer a empty promise",5,"chanceshour",2,"","","","","chanceshour",1,"","","","","remove",19,"","","",""]
    [20,"Girls, over here",6,"eventgood",4,"endday",1,"endhour",1,"eventgood",3,"sanity",-3,"endhour",1,"eventbad",2,"sanity",-2,"endhour",1]
    [21,"We can work this out",6,"eventgood",3,"sanity",-2,"endhour",1,"eventgood",2,"sanity",-2,"endhour",1,"eventbad",2,"sanity",-2,"endhour",1]
    [22,"Take action",6,"eventgood",99,"endday",1,"endhour",1,"eventgood",2,"endday",1,"endhour",1,"eventbad",99,"endday",1,"endhour",1]]
[1,"Keep cool",0,"sanity",1,"conversation",1,"","","sanity",1,"","","","","conversation",-1,"","","",""]
Essentially the conversation list is made of [unique id], [conversation text], [cost], [good outcomes x3], [neutral outcomes x3], [bad outcomes x3].

The plan is for the player to start with the first 6 [cost 0] conversation options which will move into the Player list at the start of the game.
These will appear as menu choices the player can have with their kidnapper or discard to gain a conversation point.
If they select one of the choices or discard, it then gets moved over to the Spent list.
Non used conversation choices stay in the Player list for the next hour.
Once they end the hour the game then moves to a "buy" phase where the player can spend conversation points to buy other menu choices from the ones left in the Conversation list.
Once they have spent their conversation points the cards that had moved to the Spent list are then purchasable again at the end of the following hour.

I'm not worried with having lots of menu choices, I can manage this by splitting the menu into 2 and stacking the duplicates, but is this multidimensional list thing the best way to manage this?
I realise I just need to track the id's and not the whole list but I felt it would be easier for me to follow in the code and I can't see it being much overhead.
 
Last edited:
Apr 24, 2020
192
257
First of all, I would HIGHLY recommend looking into using dictionaries.
Lists are all well and good, but as soon as you need to add more data to them, they either breaks or becomes VERY unintuitive to use.
Python:
[1,"Keep cool",0,"sanity",1,"conversation",1,"","","sanity",1,"","","","","conversation",-1,"","","",""]
becomes (I added line breaks for readability)
Python:
{"id": 1,
  "text":  "Keep cool",
  "cost": 0,
  "good": ["sanity",1,"conversation",1,"",""]
  "neutral": ["sanity",1,"","","",""]
  "bad": ["conversation",-1,"","","",""]}
The stuff in good, neutral and bad could also be made a dictionary.

Now you could also make the entire conversation list into a dictionary, which would look something like this:
Python:
Conversation = {"1": {"id": 1, "text":"Keep cool", ...},
    "2": {"id": 2, "text": "Keep cool", ...},
    "3": {"id": 3, "text": "Small talk", ...},
    "4": {"id": 4, "text": "Small talk", ...},
    "5": {"id": 5, "text": "What do you want?", ...}
    }
At that point you simply move the key (or the ID in this case) around between the Player and the Spent list
Python:
def SpendConversation(id):
  Player.remove(id)
  Spent.append(id)
You might also want the ID's to be a little more descriptive when doing it this way, since your ID is no longer limited to describing the index number.
 

obsessionau

Member
Game Developer
Sep 27, 2018
270
376
I had another project that was similar in some respects however I play with the orders a lot, which is why I didn't use dictionaries for that... I guess I got use to it.

But it could be rather easy working with them here!! :)
 
Apr 24, 2020
192
257
It happens. You work with one style for so long you get in that habit.
"When you have a hammer, every problem becomes a nail" as the saying goes.

I've been there myself. My suggestion is really just how I fixed my own code.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,976
16,231
I'm with InCompleteController on this, you've really too much things on your list. It's an opened gate to insidious bugs that you'll take hours to identify.

Organization of the data is probably the main key to a good working code. Not that there's a good way to organize them, but there's ones that are really bad and should be avoided. The more you need time to identify what a value mean, the less your organization is effective. And it's clearly the case with your list of list.
Can you tell me, right now without looking at your code or notes, and without spending too much time thinking about it, what is the meaning of the tenth value ?


Python, and so Ren'py, come with enough data types to avoid this problem. If you don't want to use dictionaries, at least use tuples, it would really ease your works, passing each rows from 21 entries, to 6.

Something that would be :
Code:
[1,"Keep cool",0, ( ("sanity",1), ("conversation",1), ("","") ), ( ( "sanity",1 ), ("",""), ("","") ), ( ("conversation",-1 ), ("","" ), ("","") )]
Therefore, row[0] is the ID ; hmm, why an Unique ID if it correspond to the rank of the row in the list ?
row[1] is still the conversation text, while row[2] is still the cost, then come the innovations...
row[3] are the good outcomes, row[4] are the neutral ones, and finally row[5] the bad ones. Which mean that you can address them all at once, and proceed them all at once.

By example, you'll pass from something like :
Python:
# For all good outcomes, with a step of 2 because the value works as pair.
for i in range( 3, 9, 2):
   # If the entry is empty, nothing to do
    if row[i] == "": continue
   #  The variable defined at /i/ will be incremented by the value
   # defined at /i+1/
    setattr( store, row[i], getattr( store, row[i] ) + row[i+1] )
to something way more explicit and easy to understand :
Python:
for target, cost in row[3]:
    setattr( store, target, getattr( store, target ) + cost )
In the first case, you can mess with the boundaries for the range, or forget to give it a step. You can also forget the +1 to access the cost. And you are limited to your "x3", that need to be all present, even if they are empties, because you need to fill the void in order for each value to be always at the same place.

In the second case, you can not iterate further than the tuple located in row[3], and each value will be assigned to a local variable that will have an explicit name. In top of that, like the rows 3, 4 and 5 are a tuple, you don't need the "", "" couples when there's nothing. Whatever the number of entry in the tuple, you'll always find the values at the same place.
It imply that you can easily have just 1 entry for the good outcome of one conversation, and 50 entries for the good outcome of another conversation. This without changing the structure of your data, nor the code used to process them.

Therefore, your list would looks like :
Code:
 Conversation = [  [ 1, "Keep cool", 0, 
                                           ( ("sanity", 1 ), ( "conversation",1 ) ), 
                                           ( ( "sanity",1 ), ), 
                                           ( ( "conversation",-1 ) , ) ],
                                       [ 2, "Keep cool", 0, 
                                           ( ( "sanity",1), ("conversation",1 ) ),
                                           ( ( "sanity",1 ), ),
                                           ( ( "conversation",-1 ) , ) ],
                                      [3, "Small talk", 0, 
                                          ( ( "conversation", 3 ) , ),
                                          ( ( "conversation", 2 ) , ),
                                          ( ( "conversation", 1 ) , ( "endhour", 1 ) )],
[...]
With a little help from constants like values, you can even make the creation of the list more explicit, and so limit considerably the risk of errors :
Code:
san1 = ( "sanity", 1 )
conv1 = ( "conversation", 1 )
conv2 = ( "conversation", 2 )
conv3 = ( "conversation", 3 )
conv1N = ( "conversation", -1 )

 Conversation = [  [ 1, "Keep cool", 0, 
                                           ( san1, conv1 ), 
                                           ( san1, ), 
                                           ( conv1N, ) ],
                                       [ 2, "Keep cool", 0, 
                                           ( san1, conv1 ),
                                           ( san1, ),
                                           ( conv1N, ) ],
                                      [3, "Small talk", 0, 
                                          ( conv3, ),
                                          ( conv2, ),
                                          ( conv1 , ( "endhour", 1 ) )],
[...]
 
  • Like
Reactions: obsessionau

obsessionau

Member
Game Developer
Sep 27, 2018
270
376
The id is just a unique identifier as there will only ever be those 22 choices that get moved around. So there are 2 copies of "keep cool" but only one copy of "girls, over here!". I don't need to track rank or order or anything for this project.

For me my main annoyance was not having the ability to add a fourth attribute for the good/bad outcomes without breaking things.
The dictionaries work well because the good/bad outcomes can be any length without gaining complexity:
Python:
{"id": 1,
"text": "Keep cool",
"cost": 0,
"good": ["sanity",1,"conversation",1]
"neutral": ["sanity",1]
"bad": ["conversation",-1]}
So if it is a good result the kidnappers sanity goes up by 1 and your conversation points go up by 1, a bad result the conversation points will go down by 1.


The only special cases I have to cater for are for:
"what I mean is" - Where the outcome is fixed, there are no good, neutral, or bad results.
and
"what do you want" - Where instead of changing this stat AND that stat. "flipconversation" is needed because it is a case of change this stat OR that stat (player choice).
 
Last edited:

obsessionau

Member
Game Developer
Sep 27, 2018
270
376
Python:
# For all good outcomes, with a step of 2 because the value works as pair.
for i in range( 3, 9, 2):
# If the entry is empty, nothing to do
if row[i] == "": continue
# The variable defined at /i/ will be incremented by the value
# defined at /i+1/
setattr( store, row[i], getattr( store, row[i] ) + row[i+1] )
<snip>
In the first case, you can mess with the boundaries for the range, or forget to give it a step. You can also forget the +1 to access the cost.
You will probably roll your eyes at me but honestly the only problem I experienced is CONSTANTLY forgetting the : at the ends!!!!
But your way does reduce the number of conditional statements, in turn reducing the number of times I will forget to put the ":"!
 
Apr 24, 2020
192
257
As anne O'nymous pointed out, many of your choices are identical so it would be a good idea to not store any duplicates.
Duplicating them does ensure that they all have a unique ID, but you might run the risk of bugs if you want to change some of the choices layer, as you might not apply the changes to all the versions of the choice.

An easy fix is simply having multiple of the same key (or ID) in your Player and Spent list.
Just remember to not remove ALL the keys when moving them between lists. So the function I suggested previously could be changed to something like this to make it work:
Python:
def SpendConversation(id):
    Spent.append(id)
    for i, currentId in enumerate(Player):
        if currentID == id:
            del Player[i]
            break
 
Apr 24, 2020
192
257
In the above example, am I assuming right that the "1": is different from "id": 1.
As such I can drop the id field as it is just a unique identifier which serves the same purpose so it now becomes redundant.
They are indeed.
I sometimes use an ID field that is identical to the key so that I can easily refer back and forth between the two.
Based on what you're are saying I would agree that the id field is redundant as well.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,976
16,231
You will probably roll your eyes at me but honestly the only problem I experienced is CONSTANTLY forgetting the : at the ends!!!!
I'll not roll eyes. Each day for works I use languages that need a ";" at the end of each line, don't care about indentation, and don't need to pre-declare a block (the said ":"). And for pleasure I use Python, that do the exact opposite of this.
There isn't a day where I don't forget something, or add something...
 
Apr 24, 2020
192
257
I'll not roll eyes. Each day for works I use languages that need a ";" at the end of each line, don't care about indentation, and don't need to pre-declare a block (the said ":").
Yeah, jumping between languages can really mess you up.
In my case the ";" isn't needed, but if it's not there then the result of the line will be printed to the console. There's nothing like forgetting it and then have the console crash because you just accidentally asked to print all your data.
The thing that gets me all the time with python is when I make a pointer, rather than a copy, without realizing it.
 

obsessionau

Member
Game Developer
Sep 27, 2018
270
376
Think of the 22 conversation options as items that are moving constantly between conversation -> player -> spent and then back to conversation. Yes there are a number of duplicates that may change later down the track.

I did remove the duplicates, this is what it looks like now... I could optimise further by making the text the id as they are now unique and more informative.
Python:
    ConDict = {
    "1": {"text": "Keep cool", "cost": 0, "good":["sanity",1,"conversation",1], "neutral": ["sanity",1], "bad": ["conversation",-1]},
    "2": {"text": "Small talk", "cost": 0, "good":["conversation",3], "neutral": ["conversation",2], "bad": ["conversation",1,"endhour",1]},
    "3": {"text": "What do you want?", "cost": 0, "good":["conversation",2,"flipdemand",1], "neutral": ["flipconversation",1], "bad": ["sanity",-1]},
The default setup would then look like this:
Python:
label Setup:
    $ ConPlayer = [1,1,2,2,3,3]
    $ ConSpent = []
    $ ConChoice = [4,4,5,5,6,7,8,9,10,11,12,13,14]
    return
The conversation Phase:
Python:
label ConversationPhase:
    $ ConPlayerCleaned = (list(set(ConPlayer))) #Remove duplicates for display
    call screen smartMenu(ConPlayerCleaned, "What should I say")
    # Do conversation action here
    $ ConPlayer.remove(_return)
    $ ConSpent.append(_return)
    pause
The custom menu at the moment for the conversation phase. The buy phase needs to show cost and only choices player can afford so will need to be different.
Python:
screen smartMenu(items, what, title = None):
    style_prefix "say"
    window:
        id "window"

        if title is not None:
            window:
                id "namebox"
                style "namebox"
                text title style "say_label"

        text what id "what" xpos 50 ypos 50

    if items:
        # Show the menu options
        vbox:
            style_prefix "choice"
            yalign 0.0 xalign 1.0
            # If list is small just display in one row otherwise split into two
            if len(items) < 6:
                vbox:
                    xsize 250
                    for i in range( 0, len(items)):
                        textbutton _(ConDict[str(items[i])]["text"] ):
                            action Return( items[i] )
            else:
                vbox:
                    for i in range( 0, len(items), 2 ):
                        hbox:
                            textbutton _(ConDict[str(items[i])]["text"] ):
                                xsize 300
                                action Return( items[i] )
                            if( i + 1 < len(items) ):
                                textbutton _(ConDict[str(items[i+1])]["text"]):
                                    xsize 300
                                    action Return( items[i+1] )
Thank you InComplete and Aon .... all appears to be going good so far :)
 
Last edited:
Apr 24, 2020
192
257
Personally I would still rename the keys in ConDict to a descriptive string, rather than an integer. Give it a few months and you'll forget what 10 does.

What usually happens is that you want something that's similar to 10, but the next available integer is 15. Now you could move 11-14, but that would require you to change the code everywhere else as well. The end result is that you now have 10 and 15, which have similar effects that you now have to keep a mental note of.
Much easier to just have them be "offerEmptyPromise" and "offerHalfEmptyPromise" for when you have to deal with them in the future.
 
  • Like
Reactions: anne O'nymous