[Ren'Py] Dynamic Map Menu Display Indexing Error

PRAECESSOR

New Member
Aug 4, 2017
5
10
I need some advice from someone who really knows their shit in Ren'Py and Python. I've asked this question on the lemmasoft forums and didn't get a reply so here's hoping that somebody here can help me out.

I'm working on developing a hierarchical map system that uses a four digit number to express relative location within that hierarchy. I'm also developing a dynamic menu system that generates a menu based on which areas you can move to.

The locations are stored in a list called 'map', so for example the overworld is map[0000] = "overworld" and one level down is map[1000]. In map[1000], you can move up to map[0000] and down to map[1100] and map[1200] and so on and so forth.

I'm totally new to both Python and Ren'Py so forgive if I'm making some bonehead mistakes.

My problem is that whenever I show my navigation screen is the script, I get an IndexError. I've confirmed that the value being used that induces the error is 9000 and the maximum index of map is 4222.

My problems are almost definitely coming from some strange language quirk that I didn't catch in the documentation. I could spend some time writing a bruteforce solution to my problem, but dynamic generation seems much better for the long term.

I've included what I believe to the the relevant code as well as the traceback for the error. If anyone has an idea but needs the rest of the code, please feel free to ask me for it. Thank you for your help.
 

Attachments

anne O'nymous

Well-Known Member
Modder
Respected User
Jun 10, 2017
2,091
2,313
Difficult to catch an index error without an effectively working case. To find it you need to evaluate the value, find where it goes wild, then understand why.

This said, the traceback point to the get_str_for_location method, and just looking at it show an error :
Code:
        def get_location_id(self, compass=None):
            seer = self.get_compass(compass) # <------
            a = seer[0]
            b = seer[1]
            c = seer[2]
            d = seer[3]
            return int( str(a) + str(b) + str(c) + str(d) )
[...]
        def get_str_for_location(self, compass=None):
            seer = self.get_compass(compass) # <------
            compass = self.get_location_id(seer)
            return map[ compass ]
You call get_compass then pass the value to get_location_id, that will call get_compass again. I assume that it's the cause of your problem.


This said, using a list isn't the best way to do here. Using a would have been more interesting.
 

PRAECESSOR

New Member
Aug 4, 2017
5
10
Difficult to catch an index error without an effectively working case. To find it you need to evaluate the value, find where it goes wild, then understand why.

This said, the traceback point to the get_str_for_location method, and just looking at it show an error :
Code:
        def get_location_id(self, compass=None):
            seer = self.get_compass(compass) # <------
            a = seer[0]
            b = seer[1]
            c = seer[2]
            d = seer[3]
            return int( str(a) + str(b) + str(c) + str(d) )
[...]
        def get_str_for_location(self, compass=None):
            seer = self.get_compass(compass) # <------
            compass = self.get_location_id(seer)
            return map[ compass ]
You call get_compass then pass the value to get_location_id, that will call get_compass again. I assume that it's the cause of your problem.


This said, using a list isn't the best way to do here. Using a would have been more interesting.
Thanks for your help. I didn't know about dictionaries and I can see how that kind of container would be better suited for something like this.

That said, I don't understand your comment on calling get_compass multiple times. get_compass is essentially an "if then else" that I've put into a method to avoid retyping it over and over. As it's called in get_str_for_location, it should return the value into seer then pass it into get location id where get_compass is called again and as far as I can see there shouldn't be any way for those two instances to interfere.

Is there something about how Python compiles that makes it so you can't call the same method within a certain number of clock cycles or without leaving the object? As I said, I've got no experience with this language so please excuse me if I'm asking dumb questions. Again, thanks for your help.
 

anne O'nymous

Well-Known Member
Modder
Respected User
Jun 10, 2017
2,091
2,313
That said, I don't understand your comment on calling get_compass multiple times. [...] as far as I can see there shouldn't be any way for those two instances to interfere.
I learned with the time that "shouldn't" is something that don't really apply to codding. At least one of these call is totally useless, and useless code should be avoided as often as possible.

All this said, are you sure of the code of your get_list_of_paths method ? Just using screen prediction to get the information, here's what it return :
  • [[9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0]]
  • [[0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0]]
  • [[9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0]]
  • [[9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0]]
  • [[0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0]]
  • [[9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0]]
  • [[9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9]]
  • [[9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9]]

All the list are just a repetition of the same value, and this value is always out of range. Plus, the values change with every prediction, while they must stay the same since the initial position haven't changed.
But honestly I can't say why, because I failed to understood what you tried to do in this method.
 

PRAECESSOR

New Member
Aug 4, 2017
5
10
I learned with the time that "shouldn't" is something that don't really apply to codding. At least one of these call is totally useless, and useless code should be avoided as often as possible.

All this said, are you sure of the code of your get_list_of_paths method ? Just using screen prediction to get the information, here's what it return :
  • [[9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0], [9, 0, 0, 0]]
  • [[0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0], [0, 9, 0, 0]]
  • [[9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0], [9, 9, 0, 0]]
  • [[9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0], [9, 0, 9, 0]]
  • [[0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0], [0, 9, 9, 0]]
  • [[9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0], [9, 9, 9, 0]]
  • [[9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9], [9, 9, 0, 9]]
  • [[9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9], [9, 0, 9, 9]]

All the list are just a repetition of the same value, and this value is always out of range. Plus, the values change with every prediction, while they must stay the same since the initial position haven't changed.
But honestly I can't say why, because I failed to understood what you tried to do in this method.
The idea behind get_list_of_paths is that if you call the method with a certain compass value, then it should return a list of all compass values that that value can move to. It's the most complicated method in the class and I wrote the bulk of it dead tired. I probably should have expected that it wouldn't work at all.

Thanks for the help, I'm probably going to rewrite this whole thing to work with a dictionary. Since string elements can be accessed like list elements, it shouldn't be too different from what I already have. Though I will be rewriting get_list_of_paths from the ground up.

Could you explain what you mean by screen prediction? Is it a debugging method?
 

anne O'nymous

Well-Known Member
Modder
Respected User
Jun 10, 2017
2,091
2,313
Could you explain what you mean by screen prediction? Is it a debugging method?
It's the default behavior of Ren'py. It always look forward the actual position, and do prediction of the future possible screens, to make them be displayed faster when their time will effectively come. Mostly it's just load the used image in the internal cache, but to do this Ren'py effectively perform the screen, so call all the function/method and things like that.
Note that once displayed, a screen continue to be called regularly to update the dynamical values.

One way to see the effect of both the prediction and update is to play this script :
Code:
init python:
    def bogus():
        store.ctrlValue += 1

default ctrlValue = 0
default secondCtrl = 0

screen predicted:
    $ bogus()
    $ secondCtrl += 1
    text "The values are [ctrlValue] and [secondCtrl]"

label start:
    "wait"
    "a"
    "little"
    "please"
    show screen predicted
    "wait a little more"
    "before it's really the end"
    "just to see the value continue to change"
    "END"
In pure theory, the values displayed should be both at 1, since they are incremented only once, when the screen is displayed. But with the prediction and update, practically it's far to be the case. This example also show that Ren'py is smart enough to not keep track of the direct assignation during the prediction ; secondCtrl will change only after the screen have been shown. But it can do nothing for value indirectly changed, here cltrValue.
 
  • Like
Reactions: Rich and PRAECESSOR

Rich

Well-Known Member
Modder
Game Developer
Jun 25, 2017
753
2,256
Could you explain what you mean by screen prediction? Is it a debugging method?
An alternate explanation, to supplement what @anne O'nymous said. Ren'py wants to perform well. Two things that slow down Ren'py at runtime - loading images, and setting up screens. As a result, when your player is sitting on one of your dialog screens reading your awesome narrative, Ren'py is (in the background) "looking ahead" in the code trying to predict what's coming next, what any upcoming screen is going to look like, or what images may be required. This way, it can pre-load images so that they're already in the cache, or possibly set up a screen before it's actually shown so that the player doesn't see any lag when he/she moves to the next screen.

With respect to screens, what this means is that even if you only show your screen once, Ren'py is almost always going to execute your screen code multiple times, some of which will be before the screen is actually displayed. As a result, it is vital that any functions that you use inside your screen do not have any side effects, since they will get called more times than you would otherwise think, and at times you wouldn't expect.

This is why you have to use actions like SetVariable, as opposed to just putting in a line of Python that sets the variable directly. The SetVariable("a", a+1) doesn't actually set the variable during screen prediction or setup, it just remembers what has to be done and waits until it's triggered. If your screen had a line like
Code:
$ a = a + 1
in it in response to a button press, "a" would get incremented every time the screen prediction ran, which is almost always NOT what you want. (You can still use stuff like this in loops, of course, but in a loop you re-init the counter at the beginning of the screen each time.)

So, basically, you can absolutely use functions and Python inside your screens, but they need to be "read only" and not change any state in your game. If you need to call a function that will change game state in response to someone clicking on a button or whatever, you need to wrap that in a Function action so that the function gets called in response to the click, but not unexpectedly during prediction.

BTW, this is something that trips up a LOT of people who start to try to do sophisticated things in screens - the Ren'py forums are filled with people running into this kind of issue. (I'm not saying that this is necessarily what's causing your problem, just that it's a common error.) They look at the screen code and make assumptions about how and when it will be run that turn out not to be true because of the screen prediction logic.

One more reason why @anne O'nymous 's comment about "should" not always being "is" in programming... LOL
 
  • Like
Reactions: anne O'nymous

anne O'nymous

Well-Known Member
Modder
Respected User
Jun 10, 2017
2,091
2,313
BTW, this is something that trips up a LOT of people who start to try to do sophisticated things in screens - the Ren'py forums are filled with people running into this kind of issue. (I'm not saying that this is necessarily what's causing your problem, just that it's a common error.) They look at the screen code and make assumptions about how and when it will be run that turn out not to be true because of the screen prediction logic.
As a side note it's also why I dislike the use of custom displayable, especially when they can be avoided. Like any other object related to screens, they have a predict method, called by Ren'py each time it's predicting the screen. But honestly I'm never really sure of what will really matter here, especially when it's a complex structure. Anytime you can let Ren'py deal with it, but using directly and only the screen language, it will lead to a better prediction.
Oh, and I also dislike people using custom displayable... because 99% of them don't even care to overwrite the said predict method, rending the whole prediction totally useless.
 
  • Like
Reactions: Rich

Rich

Well-Known Member
Modder
Game Developer
Jun 25, 2017
753
2,256
As a side note it's also why I dislike the use of custom displayable, especially when they can be avoided. Like any other object related to screens, they have a predict method, called by Ren'py each time it's predicting the screen. But honestly I'm never really sure of what will really matter here, especially when it's a complex structure. Anytime you can let Ren'py deal with it, but using directly and only the screen language, it will lead to a better prediction.
Oh, and I also dislike people using custom displayable... because 99% of them don't even care to overwrite the said predict method, rending the whole prediction totally useless.
Ya, AFAIK for a custom displayable, about the only thing you can do for Ren'py is to let it know what images you're going to use so that it can get them cached for you.

I have run across a few (in my opinion) valid uses for CDD's. The primary place I've used them is to build custom transitions - I use the Ren'py AlphaDissolve transition, but use a CDD to implement the "control" displayable.

But using them for things that you can accomplish via ATL (like the Summertime Saga case discussed in the other thread) is quite likely to be sub-optimal. So, yes, I absolutely agree that if you can do it via "normal" Ren'py, you should.