Innula Zenovka Posted September 11, 2021 Share Posted September 11, 2021 This is where I reveal, yet again, my compete ignorance of mathematics. I dimly remember things from high school but when I come to apply them, it's no more than half-remembered fragments of a foreign language. Anyway, I was recently trying to think how to script it to ensure that something would happen within n seconds, but randomly within that, while trying to ensure that, as time passed, the likelihood that whatever it was would happen increased. That is, in this particular case, I wanted my pathfinding critter to behave more or less realistically -- after wandering for a bit, it stops for at least 10 seconds, and then thinks about about what to do. Probably once it's arrived somewhere it'll at least want to stay there a bit, foraging or resting or whatever, but the more time passes, the more likely it is to want to move on. Sometimes it'll want to move on right away, and sometimes it'll want to stay there up to a minute or so, and most of the time somewhere between that. I came up with this, which does what I need integer iNeedToBeat; integer iRandom; default { state_entry() path_update(integer Type, list Reserved) { if(PU_GOAL_REACHED == Type){ iNeedToBeat = 100; llSetTimerEvent(1.0); } } timer() { if((integer)llFrand(100.0)>(--iNeedToBeat)){ if(iNeedToBeat < 90){ //then do whatever it is, so long as at least 10 seconds have passed } } } } That is, the pathfinding creature will always wait at least 10 seconds before deciding if it wants to do something new, and then, as time progresses, it becomes increasingly likely to move on to whatever this "something new" is, until it actually does so. That's the basic idea, anyway, though in practice it's going to be somewhat more complex than that. I have two questions, I think. First, I'm sure that what I've done there can be done more elegantly with some mathematical formula, if only I knew how to express it in LSL. Can anyone help me? Second, more generally, how else can we readily use LSL to calculate probabilities, and for what kinds of purposes might be do it? There must be dozens of other interesting things we can do with chance, odds and probability other than skill gaming and gachas. There's breedables, of course. What else? Link to comment Share on other sites More sharing options...
KT Kingsley Posted September 11, 2021 Share Posted September 11, 2021 In these situations I tend to go for something like this: float min_delay = 10.0; float max_delay = 100.0; float delay = min_delay + llFrand (max_delay - min_delay); llSetTimerEvent (delay); I'm not sure if this is what you're looking for here, though. I use this for cycling through stands (and freestyle sits) in my AO. Link to comment Share on other sites More sharing options...
Quistess Alpha Posted September 11, 2021 Share Posted September 11, 2021 So, you can obviously do all this without functions, but naming things makes the intentions more clear for examples to follow: // use this for a single 'dice roll' like event. // example: if the probability of doing the thing is 1/6: // if(prob_check(0.166)) {do it}; integer prob_check(float prob) { return llFrand(1.0)<prob; } // use this to select an item from list items, // where the probability of selecting each item is the list weights. list select_from_list(list items,list weights) { float total_weight = llListStatistics(LIST_SUM,weights); float f = llFrand(total_weight); float cumulative_sum = total_weight; integer index = llGetListLength(weights); while(~--index) { cumulative_sum -= llList2Float(weights,index); if(f>=cumulative_sum) return llList2List(items,index,index); } llOwnerSay("s_f_l: Programming error."); return []; } basically anything you might need to do with making something random happen, boils down to one of those 2 functions, and really, prob_check is just a specialized case of select_from_list. The more complicated things are usually continuous generalizations of select_from_list for specific applications. As in your example another thing is selecting the probability for a future check based on the outcome of a current check. you can do that any number of ways, but 2 that come to my mind: float dependent_prob_default=0.1; // the probability of a dependent thing happening. float dependent_prob=dependent_prob_default; float d_factor= // the change in probability after an unsuccessful try. //1.02; // if using *=, will become increasingly likely. 0.02; // if using +=, wll become 2% more likely each time, and will definitely happen after 50 checks. integer dependent_prob_check() { if(prob_check(dependent_prob)) {// reset the probability after a successful check: dependent_prob=dependent_prob_default; return TRUE; }else {// choose an option for how to modify the probability: //dependent_prob*=d_factor; dependent_prob+=d_factor; return FALSE; } } for your specific example, I'd go with @KT Kingsley's general method, or if you really wanted to be fancy you could try something like: float delay = min + llList2Float(select_from_list([0,2,4,6,8,10],[1,2,3,4,5,6])); llSetTimerEvent(delay); which would have a 1/21 chance of waiting for min seconds, 2/21 chance of waiting for min+2 . . . 1 Link to comment Share on other sites More sharing options...
Innula Zenovka Posted September 11, 2021 Author Share Posted September 11, 2021 9 minutes ago, KT Kingsley said: In these situations I tend to go for something like this: float min_delay = 10.0; float max_delay = 100.0; float delay = min_delay + llFrand (max_delay - min_delay); llSetTimerEvent (delay); I'm not sure if this is what you're looking for here, though. I use this for cycling through stands (and freestyle sits) in my AO. In my example, I'm saying that, after 10 seconds, the probability of something happening (critter moving to a new location/choosing a new animation/whatever) starts at 10% and then increases by 1% for each second that elapses. So for the first fifty seconds, it's more likely than not the critter will carry on doing whatever it is, though the likelihood it will decide on a new activity is slowly increasing over time and after fifty seconds it becomes increasingly likely it'll go off and do something else until, after 100 seconds have elapsed, it's certain to change. Link to comment Share on other sites More sharing options...
Innula Zenovka Posted September 11, 2021 Author Share Posted September 11, 2021 I know it's not what I described, exactly, but I suspect I may have some sort of sine wave in mind, if only I knew how to express it. Link to comment Share on other sites More sharing options...
Quistess Alpha Posted September 11, 2021 Share Posted September 11, 2021 (edited) 1 hour ago, Innula Zenovka said: In my example, I'm saying that, after 10 seconds, the probability of something happening (critter moving to a new location/choosing a new animation/whatever) starts at 10% and then increases by 1% for each second that elapses. Just, of note, the two ideas: 1) The probability for the thing happening increases with the time elapsed, 2) The amount of time it will take the thing to happen is selected from a distribution that weights longer time durations more heavily are equivalent, and supposing I don't make any mistakes, or errors in the fuzzy logic in my thoughts: float dependent_prob_default=0.02; // the probability of a dependent thing happening. float dependent_prob=dependent_prob_default; float d_factor=0.02 integer dependent_prob_check(); // see above, using the += version. default { state_entry() { llSetTimerEvent(10); } timer() { llSetTimerEvent(1); if(dependent_prob_check()) { //do the thing. } } } and list select_from_list(list items, list weights); // see above default { state_entry() { list temp; integer index = 50; while(--index) { list+=(float)index; } llSetTimerEvent(10 +llList2Float(select_from_list(temp,temp)); } timer() { //do the thing. } } Should be almost exactly equivalent. (up to floating point arithmetic making things weird sometimes.) The first is a bit inefficient in computation, the second inefficient in memory. (Edit: I take it back, they might be slightly different, but they still have the same general flavor) something like: default { state_entry() { integer rand = llFrand(1.0); llSetTimerEvent(10+(1-(rand*rand))*50); } timer() { //do the thing. } } would be similar and the most efficient. (confuses me, would have to test whether it's weighted twords the low or high end and adjust the formula accordingly) Edited September 11, 2021 by Quistess Alpha 1 Link to comment Share on other sites More sharing options...
Qie Niangao Posted September 11, 2021 Share Posted September 11, 2021 5 minutes ago, Innula Zenovka said: I know it's not what I described, exactly, but I suspect I may have some sort of sine wave in mind, if only I knew how to express it. Reading through this thread, the first thing that came to mind was that llFrand generates psuedorandom numbers in a linear distribution, whereas natural processes generally follow some other frequency distribution (e.g., Gaussian) where events are more likely to occur around some central timing and with decreasing likelihood at the beginning or end of the permissible interval. Of course it doesn't need to be Gaussian, and most likely nobody would notice exactly what distribution is at play (except when wagering is involved). So I searched the wiki for "gaussian" and found this which points to this tidy spot of math, all of which seems overkill for the intent here. A simpler approach would be to generate a list of values that correspond to the delay intervals, so the first element in the list is the shortest permissible interval, the last element is the longest, and we index into that list by integer-forced llFrand() over the arbitrary number of discrete values we use to represent the distribution. The values themselves would actually be the ogive cumulative over the frequency distribution we're modeling, so they monotonically increase in a shape something like an (offset and scaled) arctan—llAtan2()—thus invoking a sine-adjacent trig function. Or maybe this has nothing to do with the sine wave you were visualizing. 1 1 Link to comment Share on other sites More sharing options...
Extrude Ragu Posted September 11, 2021 Share Posted September 11, 2021 Use llGetTime to get the time since the script started or the last call to llResetTime().. Here's some psuedo code. I use next_stop_to_think to define how long it will take before I get going again. I always think for 5 seconds. .... float next_stop_to_think; default { .... timer() { float elapsed = llGetTime(); if (elapsed > next_stop_to_think) { // Wait llSay(0, "Thinking..."); next_stop_to_think = 5 + llFrand(10); // We'll stop to think again in 5 to 15s llSetTimerEvent(5.0); // Think for 5s llResetTime(); // So that elapsed will always be time since last stopped to think. } else { llSetTimerEvent(1.0); // Ensure timer resumes normal pace } } } 1 Link to comment Share on other sites More sharing options...
Profaitchikenz Haiku Posted September 11, 2021 Share Posted September 11, 2021 (edited) 1 hour ago, Innula Zenovka said: I know it's not what I described, exactly, but I suspect I may have some sort of sine wave in mind, if only I knew how to express it. If the probability of something happening increases with time as you have described then you are more likely to find an exponential function fitting your needs. this is a fudge, I know, but it should simulate what you want to achieve. have a predetermined trigger level somewhere around 90, determined by a random number added to a minimum level. start your timer counter at -10, increase by one until it is greater than 0. Once greater than 0, increase it by e to the power of the last increment, obviously save the new increment for the next timer calculation. Check and take action when it exceeds the trigger. Lots of scope for setting the timer value to a variable amount each time to add to the uncertainty Edited September 11, 2021 by Profaitchikenz Haiku 1 Link to comment Share on other sites More sharing options...
Mollymews Posted September 12, 2021 Share Posted September 12, 2021 i would go with the list approach as well. As is able to be easily tailored to fit the app. Example: timer() { if (condition == CHANGE_TO_WAIT) { //... stop moving animation //... start waiting animation condition = CHANGE_TO_MOVING; float wait = 10. + llList2Float([5,4,4,3,3,3,2,2,2,2,1,1,1,1,1], (integer)llFrand(15)); llSetTimerEvent(wait); } else if (condition == CHANGE_TO_MOVING) { //... stop waiting animation //... start moving animation condition = IS_MOVING; llSetTimerEvent(1.0); } else if (condition == IS_MOVING) { //... do moving stuff //... on some happening then condition = CHANGE_TO_WAIT; llSetTimerEvent(1.0); } } /* llList2Float([5,4,4,3,3,3,2,2,2,2,1,1,1,1,1], (integer)llFrand(15)); 15 elements 5 times will wait 10 + 1 seconds 4 times will wait 10 + 2 seconds 3 times will wait 10 + 3 seconds 2 times will wait 10 + 4 seconds 1 times will wait 10 + 5 seconds which sans 10 seconds is the gaussian distribution 5 * 1 = 5 4 * 2 = 8 3 * 3 = 9 2 * 4 = 8 1 * 5 = 5 --------- 35 */ 1 1 Link to comment Share on other sites More sharing options...
Quistess Alpha Posted September 12, 2021 Share Posted September 12, 2021 (edited) 16 hours ago, Mollymews said: float wait = 10. + llList2Float([5,4,4,3,3,3,2,2,2,2,1,1,1,1,1], (integer)llFrand(15)); FWIW is equivalent to: float wait = 10. + llList2Float(select_from_list([5,4,3,2,1],[1,2,3,4,5])); which might have a bit more overhead for the specific example, but should scale a bit better. Or if you wanted to get more specific: list select_from_list_gaussian(list items) { integer index = llGetListLength(items); float cumulative_sum = index*(index+1)*0.5; // sum from 1 .. index. float f = llFrand(cumulative_sum); while(~--index) { cumulative_sum -= (index+1); if(f>=cumulative_sum) return llList2List(items,index,index); } llOwnerSay("s_f_l_g: Programming error."); return []; } // . . . float wait = 10. + llList2Float(select_from_list_gaussian([5,4,3,2,1])); Edited September 13, 2021 by Quistess Alpha Fix an off-by-1 error. 1 Link to comment Share on other sites More sharing options...
Innula Zenovka Posted September 12, 2021 Author Share Posted September 12, 2021 (edited) I'm trying to design something that will handle generic behaviour for my pathfinding critters when nothing happens to disturb them -- they simply play a few animations for a few minutes and then they go off and do something somewhere else. So far I've got this (but I haven't really had a chance to look at Tessa's latest post. float fTotalRestTime = 290.0;//so max 5 minutes float fMinRestTime = 10.0; float fTotalAnimationTime = 20.0;//so max 30 seconds animation integer iIndex; //integer iMax; list lTimes; list lAnimationsToPlayWhileStopped; string strCurrentAnim; string strPreviousAnim; default { state_entry(){ lAnimationsToPlayWhileStopped = llListRandomize(lAnimationsToPlayWhileStopped,0); //iMax = -llGetListLength(lAnimationsToPlayWhileStopped); float fRand = llFrand(1.0); float f = (fRand*fRand); float fPeriod= fMinRestTime+(1-f)*(fMinRestTime+fTotalRestTime);//up to five minutes in all llOwnerSay("fPeriod is "+(string)fPeriod); //now build a list of how often to change anims do{ fRand = llFrand(1.0); f = (fRand*fRand); lTimes +=[fMinRestTime+ (1-f)*fTotalAnimationTime ];//animations play for between 10 and 20 seconds } while(llListStatistics(LIST_STAT_SUM, lTimes)<fPeriod); llOwnerSay(llList2CSV(lTimes)); llOwnerSay("total of lTimes is "+(string)llListStatistics(LIST_STAT_SUM,lTimes)); //this is always going to be slightly more than fPeriod, but close enough for what I need iIndex = -llGetListLength(lTimes); //start first animation strCurrentAnim = llList2String(llListRandomize(lAnimationsToPlayWhileStopped,1),0); llStartObjectAnimation(strPreviousAnim = strCurrentAnim); llSetTimerEvent(llList2Float(lTimes,iIndex)); } timer(){ if(++iIndex){ do{ //choose a new anim strCurrentAnim = llList2String(lAnimationsToPlayWhileStopped,0); if(strCurrentAnim == strPreviousAnim){ strCurrentAnim = llList2String(lAnimationsToPlayWhileStopped,1); } } while(strCurrentAnim == strPreviousAnim); lAnimationsToPlayWhileStopped = llListRandomize(lAnimationsToPlayWhileStopped,1); llStartObjectAnimation(strCurrentAnim); llStopObjectAnimation(strPreviousAnim = strCurrentAnim); llSetTimerEvent(llList2Float(lTimes,iIndex)); } else{ llSetTimerEvent(0.0); //do some pathfinding stuff } } } Edited September 12, 2021 by Innula Zenovka 1 Link to comment Share on other sites More sharing options...
Mollymews Posted September 12, 2021 Share Posted September 12, 2021 (edited) going back to your initial posit: The animal is more likely to move the longer it waits another way to do this is using combinatorics. More specifically a factorial method. With a proviso that animations are of related durations to help provide close to seamless transitions. Example: edit typo. gah! double gah! is what happens when type in absolute numbers off the top of your head. (the magnitude is 5 not 4) list sAnims = ["some", "ani", "ma, "tions"]; string sAnim; integer iTicker; timer() { iTicker = (++iTicker) % 5; if (iTicker) { integer len = llGetListLength(sAnims); integer i = (integer)llFrand(len); string anim = llList2String(sAnims, i); // allow the same animation to play in successive intervals // like a cow grazing for example if (anim != sAnim) { llStartObjectAnimation(anim); llStopObjectAnimation(sAnim); sAnim = anim; } /* always change animation if (anim == sAnim) anim = llList2String(sAnims, (++i) % len); llStartObjectAnimation(anim); llStopObjectAnimation(sAnim); sAnim = anim; */ float wait = 30. * ((integer)llFrand(5 - iTicker) + 1); llSetTimerEvent(wait); } else // ticker is 0 { // path finding llSetTimerEvent(0.0); } } /* float wait = 30. * ((integer)llFrand(5 - iTicker) + 1); combinations: factorial(4) = 24 max = 120 + 90 + 60 + 30 = 300 (5 minutes) min = 30 + 30 + 30 + 30 = 120 (2 minutes) 30. assumes animations of 30 second duration and/or some divisor of 30. [30,15,10,6,5,3,2] float wait = 10. * ((integer)llFrand(5 - iTicker) + 1); max = 40 + 30 + 20 + 10 = 100 min = 10 + 10 + 10 + 10 = 40 10. assumes animations of 10 second duration and/or some divisor of 10. [10,5,2] */ Edited September 12, 2021 by Mollymews 1 Link to comment Share on other sites More sharing options...
Quarrel Kukulcan Posted September 12, 2021 Share Posted September 12, 2021 (edited) On 9/11/2021 at 11:53 AM, Innula Zenovka said: First, I'm sure that what I've done there can be done more elegantly with some mathematical formula, if only I knew how to express it in LSL. Can anyone help me? Second, more generally, how else can we readily use LSL to calculate probabilities, and for what kinds of purposes might be do it? When you want a certain behavior, it's best to work backward from the results you want and craft a simple approximation. You'd be surprised how often simple is better -- more intuitive, more codeable, more tuneable. For instance, your complicated approach with repeated tests and an increasing chance after each failure is actually more likely to pass early than late. The behavior will trigger within 15 seconds 55% of the time and have a 95% chance of happening within 25 seconds. Here's a graph of how often the behavior lasts exactly X seconds before switching. I bet you didn't expect that since it sounds like you wanted longer delays to be more common. To weight a result toward the high end, one kind of mathy way is to take the floating point number at the core of the randomizer -- the one that's in the 0.0 to 1.0 range -- and do something to it that increases the values in the middle but leaves the 0 and 1 values alone. One straightforward option: take the square root. That tweak has to be made before multiplying out to cover the range of values you need. In LSL, where we only have llFrand() and it multiplies by the range for us, that means doing this: llFrand(max) becomes llPow(llFrand(1.0), 0.5) * max If you want a stronger or weaker skew, tweak the 0.5. There's also a non-mathy way to skew results high: generate two random numbers and use the bigger one. Skewing random numbers low means squaring the 0.0-1.0 core value (power 2.0 instead of 0.5) if you're mathing it, or rolling two unskewed numbers and picking the smaller one if you're not. If you want to do something with a bell curve -- something more likely to pick a number in the middle of a range than one near the ends -- one simple way is to add N random numbers that each cover 1/Nth the range. Say you want an eyeblink every 4 to 10 seconds. If you use 4 + llFrand(6) you're just as likely to get short or long blinks as average ones. To skew the blinks toward average delays you could use 4 + llFrand(3) + llFrand(3) instead. 4 + llFrand(2) + llFrand(2) + llFrand(2) would make the bias even stronger. P.S. The "select from an array" approach is also very simple, flexible and tuneable. Edited September 12, 2021 by Quarrel Kukulcan 1 1 Link to comment Share on other sites More sharing options...
Innula Zenovka Posted September 12, 2021 Author Share Posted September 12, 2021 I'm so glad I asked this question. I'm learning so much that I can apply not only to my animesh critters but more generally to all sorts of "random" events where a simple llFrand(n) isn't the right solution. Thank so much all! 1 Link to comment Share on other sites More sharing options...
Quistess Alpha Posted September 13, 2021 Share Posted September 13, 2021 (edited) Two more general pieces of advise, 1) graph things or do simulations to make sure things work how you think they do, 2) you can exactly mirror a distribution by subtracting it from the maximum (and adding the minimum). Some very basic distribution tests: Quote Quoted for collapsability: list select_from_list_gaussian(list items) { integer index = llGetListLength(items); float cumulative_sum = index*(index+1)*0.5; // sum from 1 .. index. float f = llFrand(cumulative_sum); while(~--index) { cumulative_sum -= (index+1); //llOwnerSay((string)cumulative_sum); if(f>=cumulative_sum) return llList2List(items,index,index); } llOwnerSay(llList2CSV([cumulative_sum,f])); llOwnerSay("s_f_l_g: Programming error."); return []; } default { state_entry() { // sum of Frands: list test = [0,0,0,0,0,0,0,0,0,0]; integer index = 1000; while(~--index) { integer rand = (integer)(llFrand(3)+ llFrand(3)+ llFrand(4)); integer old = llList2Integer(test,rand); test = llListReplaceList(test,[++old],rand,rand); } llSay(0,"basic Sum"); llSleep(0.2); llSay(0, llList2CSV(test)); // square frand: test = [0,0,0,0,0,0,0,0,0,0]; index = 1000; while(~--index) { float Frand = llFrand(1.); integer rand = (integer)(10*(Frand*Frand)); integer old = llList2Integer(test,rand); test = llListReplaceList(test,[++old],rand,rand); } llSay(0,"Square"); llSleep(0.2); llSay(0, llList2CSV(test)); // 1- square test = [0,0,0,0,0,0,0,0,0,0]; index = 1000; while(~--index) { float Frand = llFrand(1.); integer rand = (integer)(10*(1-(Frand*Frand))); integer old = llList2Integer(test,rand); test = llListReplaceList(test,[++old],rand,rand); } llSay(0,"1-Square"); llSleep(0.2); llSay(0, llList2CSV(test)); // square-root test = [0,0,0,0,0,0,0,0,0,0]; index = 1000; while(~--index) { float Frand = llFrand(1.); integer rand = (integer)(10*(llPow(Frand,0.5))); integer old = llList2Integer(test,rand); test = llListReplaceList(test,[++old],rand,rand); } //llSay(0,"debug."); llSay(0,"root"); llSleep(0.2); llSay(0, llList2CSV(test)); // 1 - square-root test = [0,0,0,0,0,0,0,0,0,0]; index = 1000; while(~--index) { float Frand = llFrand(1.); integer rand = (integer)(10*(1-llPow(Frand,0.5))); integer old = llList2Integer(test,rand); test = llListReplaceList(test,[++old],rand,rand); } llSay(0,"1-root"); llSleep(0.2); llSay(0, llList2CSV(test)); // "gaussian" test = [0,0,0,0,0,0,0,0,0,0]; index = 1000; while(~--index) { integer rand = llList2Integer(select_from_list_gaussian( [0,1,2,3,4,5,6,7,8,9]),0); integer old = llList2Integer(test,rand); test = llListReplaceList(test,[++old],rand,rand); } llSay(0,"gaussian"); llSleep(0.2); llSay(0, llList2CSV(test)); } } and their results: Quote [17:45] Object: basic Sum [17:45] Object: 3, 36, 79, 166, 200, 235, 164, 88, 27, 2 [17:45] Object: Square [17:45] Object: 309, 122, 108, 102, 61, 65, 62, 52, 66, 53 [17:45] Object: 1-Square[17:45] Object: 57, 61, 56, 51, 70, 90, 79, 101, 109, 326 [17:45] Object: root[17:45] Object: 5, 36, 49, 63, 96, 102, 160, 149, 157, 183 [17:45] Object: 1-root [17:45] Object: 145, 190, 152, 138, 129, 86, 70, 53, 30, 7 [17:45] Object: gaussian[17:45] Object: 23, 43, 58, 76, 100, 108, 123, 134, 157, 178 (( theoretical gausian: 18,36,55,73,91,109,127,145,164,182 )) as you can see, '1-square' and 'root' are both weighted towards the right, but 'root' has less of an abrupt spike at the end than '1-square' ; 'Gaussian' is the most even, and not too dissimilar from the 'root' distribution. Edited September 13, 2021 by Quistess Alpha 1 Link to comment Share on other sites More sharing options...
Recommended Posts
Please take a moment to consider if this thread is worth bumping.
Please sign in to comment
You will be able to leave a comment after signing in
Sign In Now