Least Recently Used Random choices

The idea of a least recently used pick is to take the options offered and remove recently used ones before making a random choice from the ones that remain. This means that the same choice won't be picked two or more times in a row. It gives a more "human" feel of randomness, but does not eliminate the possibility of a choice never being picked.

Implementation

The code to do this style of pick is more complicated than other sorts of randomness. It will need to do the following:

  1. Make a copy of the options list called the working list so it can be modified by removing entries.
  2. While there's more than one option remaining remove used options from the working list. It's important to leave a minimum number of options so that there's still a random element.
  3. Pick one of the options from the working list. This will be the result.
  4. Update the used list. If the picked option is already in the used list move it to the top. Otherwise add it to the top of the list. Then truncate the list to the required number of entries.
It makes sense to make a Python function to do this.


init python:
    def rndPickLru(optionsList, usedList, usedDepth=1, minOpt=2):
        """
        Pick from a list, but avoid recently used options.

        :param optionsList: List of options to pick from.
        :param usedList:    List of recently used options.
        :param usedDepth:   How many used options to ignore.
        :param minOpt:      Minimum number of options to choose randomly from.
        """
        if usedList is None:
            raise ValueError("usedList cannot be None")
        if not optionsList:
            return None
        #
        # Make a copy of the options list.
        #
        workingList = optionsList.copy()
        #
        # Remove options that have been used recently.
        # Stop if there are only minOpt options left.
        #
        for used in usedList:
            if len(workingList) <= minOpt:
                break
            if used in workingList:
                workingList.remove(used)
        #
        # Pick one of the remaining options.
        #
        result = renpy.random.choice(workingList)
        #
        # Add the pick to the head of the used list.
        #
        if result in usedList:
            usedList.remove(result)
        usedList.insert(0, result)
        #
        # Trim the used list to size.
        #
        while len(usedList) > usedDepth:
            usedList.pop()

        return result

Choosing a minimum options value

The default minimum number of options to choose randomly from is two. This means there will always be some randomness providing there are at least two options. If there are only two options this means that both will be in play, even if they are also in the recently used list. If you would prefer the selection to alternate if only given two options to pick from, set this to one.

Choosing a depth value

The depth of the recently used list needs to be tuned to fit the number of options that there are to choose from. The default depth of one just eliminates the last choice if possible. A higher depth value eliminates more old choices. The default minimum options setting will ensure some randomness remains. Players are unlikely to remember more than the last six or seven choices, so even if you have many options higher depth values may not be noticed. However, if you have many options then too low a depth may allow picks to be repeated more often than you would like, and increase the likelihood that some options will never be chosen.

Calling least recently used labels

A frequent use for this style of randomness is picking a random label to call. This Ren'Py script wrapper makes this easy:


    # Call one of the labels from the provided list of labels, avoiding
    # recently used ones.
    #
label callRndLabelLru(labelList, usedList, usedDepth=1, minOpt=2):
    $ renpy.dynamic('pick')
    $ pick = rndPickLru(labelList, usedList, usedDepth, minOpt)
    if renpy.has_label(pick):
        call expression pick from call_rnd_label_lru_dyn
    else:
        dbg "In callRndLabelLru: label [pick] does not exist."
    return

Example use

Here's an example of this code in use to randomly call one of four labels, but avoiding the last two used.


default labelList = ['alleyEmpty', 'alleyCat', 'alleyRat', 'alleyVomit']
default labelLru = []

label randomAlley:
    call callRndLabelLru(labelList, labelLru, 2)
    return

label alleyEmpty:
    "The alley is empty save for the normal dumpsters and wind-blown litter."
    return

label alleyCat:
    "A tomcat jumps out from behind a dumpster arches its back and hisses at you."
    return

label alleyRat:
    "A rat that was nibbling on a discarded pizza darts under a dumpster."
    return

label alleyVomit:
    "As you search the alley you narrowly avoid treading in a pool of vomit."
    return

Dynamic option lists

The list of possible options can be built dynamically if the story needs it. For example some options may only be possible under certain conditions. It does not matter if an option that is in the recently used list is not one being offered as a possible choice.

Multiple lists

If you are using this technique for multiple kinds of random choices then you will need a separate recently used list for each kind of choice.