Draw from a deck

Drawing random "cards" from a deck eliminates most of the issues often seen with dice-roll mechanics. With enough draws from the deck all the results will be seen eventually, and unless there are duplicate cards you won't get a run of the same results.

While the idea of cards may lead you to think of the standard 52 card deck of four suits, it is not limited to that. The objects in question need not be flat, or rectangular. Other examples you may be familiar with are:

Any time the probability of a win must be precise, such as modern screen-based one-armed bandits, the underlying system is based on a draw from a style deck mechanic. Unlike their mechanical predecessors that worked like rolling dice, the modern ones determine the result first, then animate the wheels accordingly.

Draw pile

All deck systems need a draw pile: the collection of objects that random picks are taken from. For Ren'Py the most common implementation is a list of objects declared with default. It needs to be a defaulted variable as it will change as objects are taken from it. Here the draw pile is a list of strings, but it could be some other type such as numbers, NPCs, or Ren'Py labels.


default drawPile = ['cat', 'dog', 'ferret', 'goldfish', 'guinea pig', 'hamster']

Discard pile

Many, but not all, deck systems also have a discard pile. This is a list of used or discarded results and starts out empty. Typically once the draw pile is empty the discard pile is shuffled and becomes the new draw pile.


default discardPile = []

Random pick

This method is closer to drawing from a bag of tiles rather than a pre-shuffled deck of cards. It has the advantage that the randomization is carried out as needed, and if necessary new or used "cards" can be added to the draw pile without having to re-shuffle. Care needs to be taken to not call renpy.random.choice if the draw pile is empty (empty lists are False in Python).


    if not drawPile:
        $ pick = None
    else:
        $ pick = renpy.random.choice(drawPile)
        $ drawPile.remove(pick)

Used picks can be added to the discard pile by appending them to it:


    $ discardPile.append(pick)

When the draw pile is empty it can be re-filled from the discard pile by copying the entries in the discard pile back into the draw pile with extend and emptying the discard pile with clear:


    if not drawPile:
        $ drawPile.extend(discardPile)
        $ discardPile.clear()

Draw and discard

These operations can be combined to draw a choice and immediately discard it, and re-fill the draw pile as needed.
Note: When the last choice is taken from the draw pile it will need to be re-filled, and there is a chance that the next draw will be the same choice again.


    if not drawPile:
        $ drawPile.extend(discardPile)
        $ discardPile.clear()
    $ pick = renpy.random.choice(drawPile)
    $ drawPile.remove(pick)
    $ discardPile.append(pick)

Shuffled deck

It is possible to instead shuffle the draw pile and take from the shuffled deck which is a closer simulation of a true deck of cards. This may be useful if your game requires the ability to peek at the next card in the draw pile. Before using the draw pile needs to be shuffled:


    $ renpy.random.shuffle(drawPile)

To pick the "top" card (actually the last card in the list):


    if not drawPile:
        $ pick = None
    else:
        $ pick = drawPile.pop()

To peek at the next card to draw the index -1 can be used:


    if not drawPile:
        $ peek = None
    else:
        $ peek = drawPile[-1]

To add picked cards to the discard pile and automatically re-use it:


    if not drawPile:
        $ drawPile.extend(discardPile)
        $ renpy.random.shuffle(drawPile)
        $ discardPile.clear()
    $ pick = drawPile.pop()
    $ discardPile.append(pick)

Using a deck of Ren'Py labels

One common use for deck-style randomisation is to pick events or scenes for the player to experience. In this case the draw pile can be a list of label names which can then be invoked using call expression:


default eventDrawPile = ['event1', 'event2', 'event3']
default eventDiscardPile = []

label randomEvent:
    $ renpy.dynamic('pick')
    if not eventDrawPile:
        $ eventDrawPile.extend(eventDiscardPile)
        $ eventDiscardPile.clear()
    $ pick = renpy.random.choice(eventDrawPile)
    $ eventDrawPile.remove(pick)
    $ eventDiscardPile.append(pick)
    if renpy.has_label(pick):
        call expression pick from random_event_dyn
    else:
        dbg "In randomEvent: label [pick] does not exist."
    return

label event1:
    "Event 1"
    return

    ...

As this uses the draw from a bag style, new events can be added as the story develops using eventDrawPile.append('event4') or removed using eventDrawPile.remove('event1')

Sampling

The method renpy.random.sample  provides a way of making several deck-style draws from a sequence and returns a list of the picked elements. This can be useful for example for choosing a sub-set of NPCs who might be present at a location. The example below picks three NPCs from a list:


    $ npcs = ['amanda', 'brian', 'chloe', 'derek', 'emily', 'fredrick']
    $ present = renpy.random.sample(npcs, 3)