Welcome to the sponsor-exclusive content for the Ren'Py Patreon. Sponsors like you ensure this page exists. Thank you.

_images/python-tricks.png

Python Tricks #2 link

Welcome back to the second Python tricks article. This month, we have two snippets of Python that might come in handy in your visual novels. The first one shows a way to generate random outcomes in a way that’s more pleasing to your players, while the second is a displayable that can be used to change the background when the player hovers over a button.

Random Bag link

There are a number of reasons why a story-based game might have randomness in it. A game might simulate vagaries of the real world, like weather. It might want to have a combat simulation where the player has to experience the uncertainty of being in a real fight. Or perhaps a character might have a flighty personality, and the game wants to simulate his unpredictability.

One way to do this is to simply use renpy.random.random() to generate a random chance. For example, one could write:

if renpy.random.random() > 0.6666:
    "Good outcome."
else:
    "Bad outcome."

In this example, the bad outcome is picked 2 out of 3 times, and the good outcome is picked 1 out of 3 times. While this is fair, that fairness can lead to an unexpected outcome if it’s run more than once.

With this logic, there’s nothing preventing the bad outcome from coming up many times in a row. When this is run 20 times, there’s a 50% chance 5 bad outcomes will come up in a row. When this is run 170 times, there’s a 50% chance that the bad outcome will come up a ragequit-inducing ten times in a row. (This might seem counter-intuitive, but it’s math.)

Because it’s easy for the player to feel screwed by a fair random number generator, games tend to use different techniques to simulate randomness. A common way is to have a bag of options - in a tabletop game, it might be a literal bag with two white balls and one golden one. If the player only refills the bag when all the balls are drawn, it limits the chance of getting a run of bad outcomes.

It’s possible to simulate the bag in Ren’Py. Here’s how to do it. First off, the RandomBag class:

init python:

    class RandomBag(object):

        def __init__(self, choices):
            self.choices = choices                        # The choices that go into the bag.
            self.bag = [ ]                                # A shuffled list of things in the bag.

        def draw(self):
            if not self.bag:                              # If the bag is empty,
                self.bag = list(self.choices)             # Replace it with a copy of choices,
                renpy.random.shuffle(self.bag)            # Then randomize those choices.

            return self.bag.pop(0)                        # Return something from the bag.

Next, we create a RandomBag, with the outcomes that will fill it. Here, we use False for the two bad outcomes, and True for the good one:

define outcomebag = RandomBag([ False, False, True ])

Finally, we can call outcomebag.draw() to pick one of the outcomes from the random bag.

if outcomebag.draw():
    "Good outcome."
else:
    "Bad outcome."

Why go to this trouble? It makes the worst case far less worse. The worst case for our example is:

Good Bad Bad | Bad Bad Good

Which means the player will experience 4 bad outcomes in a row, and will then be guaranteed a good outcome - something that’s far less likely to upset the player compared with 10 bad outcomes in a row.

BGQueue link

_images/vnroom-4.jpg

Our second entry is making a repeat appearance, but this time as the star. The vnroom launcher has a feature where the background faded in when the player focused a button. I think that’s a feature that other games could plausibly benefit from, so I extracted it from the vnroom launcher and modified it to stand alone.

Since it’s a custom displayable, I put the script that defines it into its own file, which you can download from

https://patreon.renpy.org/_static/bgqueue.rpy

and drop into your game. This adds the new BGQueue displayable, which takes three parameters.

default
The default background to use.
delay
How long the dissolves take.
wait
How long to wait before a background dissolves in. This helps to prevent multiple backgrounds from showing as the player moves their mouse.

Using this in a custom screen is easy:

screen protag_picker():
    default bgq = BGQueue(default="black", delay=1.0, wait=.25)

    # Adds the background to the screen, so it shows.
    add bgq

    vbox:
        label "Who should the protagonist be?"

        textbutton "Eileen":
            action Jump("eileen_start")
            hovered bgq.Enqueue("bg eileen")

        textbutton "Lucy":
            action Jump("lucy_start")
            hovered bgq.Enqueue("bg lucy")

The default statement here creates a BGQueue displayable, while the add statement makes it the background of the screen. The real meat of this example is the .Enqueue action, which causes the background to change.

It’s a bit harder to add this to the main menu and game menu, but not by much. First, you’ll need to create the BGQueue in a global variable (define is fine for this, as it doesn’t need to be saved between sessions), and then set it as the main menu and game menu backgrounds. This goes in gui.rpy, overriding the existing definitions:

define bgq = BGQueue("gui/main_menu.png")
define gui.main_menu_background = bgq
define gui.game_menu_background = bgq

Finally, modify the navigation screen to call the Enqueue action when a button is hovered:

screen navigation():

    vbox:
        style_prefix "navigation"

        xpos gui.navigation_xpos
        yalign 0.5

        spacing gui.navigation_spacing

        if main_menu:

            textbutton _("Start") action Start() hovered bgq.Enqueue("bg start")

        else:

            textbutton _("History") action ShowMenu("history") hovered bgq.Enqueue("bg history")

            textbutton _("Save") action ShowMenu("save") hovered bgq.Enqueue("bg save")

        textbutton _("Load") action ShowMenu("load") hovered bgq.Enqueue("bg load")

        textbutton _("Preferences") action ShowMenu("preferences") hovered bgq.Enqueue("bg preferences")

I think the BGQueue is a nice effect - it really can make menus pop. I’m not sure if it would be popular enough to justify including it in Ren’Py proper, so I’m glad I could use this space to write it up.