Random

Repeatable random numbers from seeded streams, with weighted choices, shuffles and encounter bags.

Commands

Import blitz.random As R gives your program these, with R. in front. Each name leads to its part of the reference.

Functions

Types

blitz.random.dice

An optional part. Import blitz.random.dice As DiceRules as well to use these.

Functions

Types

Guide

Random gives your game dice rolls, loot drops, shuffled decks and surprises that you can repeat. Every number comes from a stream that starts from a seed, a starting number of your choice, and the same seed always gives the same results. So a level made from seed 42 is the same level every time, players can share a seed with a friend, and a saved game can carry on with exactly the numbers it would have had.

Eight treasure chests holding coins, potions, a sword and a crown, filled from seed 3

Try it

Open chests.bb and run it. Press Right and Left to change the seed, and watch the chests fill with different loot. Then go back to a seed you've already seen: its chests hold exactly the same things as before.

; Eight treasure chests, filled from a seed. Left and Right change the seed.
; Go back to a seed you've seen, and its chests hold the same things again.
Dialect "modern"
Import blitz.random As R

Graphics 640, 330, 0, 2
SetBuffer BackBuffer()
ClsColor 18, 52, 79

; How often each kind of loot turns up. Bigger weights come up more often.
Local weights:Double List = New List
weights.Add(60D) ; 0: coins
weights.Add(25D) ; 1: potions
weights.Add(10D) ; 2: a sword
weights.Add(5D)  ; 3: a crown

Dim loot:Int(7)  ; The kind of loot in each chest...
Dim label:Str(7) ; ...and what it says on the chest.
Local seed:Int = 1
Local filled:Int = 0 ; The seed the chests were last filled from.
Local chest:Int

While Not KeyDown(1)
    If KeyHit(205) Then seed = seed + 1
    If KeyHit(203) And seed > 1 Then seed = seed - 1

    If seed <> filled
        ; A new stream from the seed. The same seed always gives the same numbers.
        Local stream:R.RandomStream = R.CreateRandom(seed)
        For chest = 0 To 7
            loot(chest) = R.ChooseWeighted(stream, weights)
            Select loot(chest)
                Case 0
                    label(chest) = R.RandomInt(stream, 5, 50) + " coins" ; 5 to 50
                Case 1
                    label(chest) = R.RandomInt(stream, 2, 4) + " potions" ; 2 to 4
                Case 2
                    label(chest) = "A sword"
                Default
                    label(chest) = "A crown"
            End Select
        Next
        Delete stream
        filled = seed
    EndIf

    Cls
    Color 255, 255, 255
    Text 20, 16, "Seed " + seed + "        Left and Right change the seed"
    For chest = 0 To 7
        Local x:Int = 20 + (chest Mod 4) * 152
        Local y:Int = 48 + (chest / 4) * 128
        Color 40, 90, 140
        Rect x, y, 140, 112, True
        Select loot(chest)
            Case 0
                Color 255, 210, 60
            Case 1
                Color 235, 80, 110
            Case 2
                Color 190, 205, 225
            Default
                Color 190, 120, 255
        End Select
        Oval x + 50, y + 10, 40, 40, True ; The loot, in an open chest.
        Color 150, 95, 45
        Rect x + 32, y + 36, 76, 36, True
        Color 105, 60, 25
        Rect x + 32, y + 46, 76, 6, True
        Color 255, 255, 255
        Text x + 70, y + 88, label(chest), True
    Next
    Color 170, 195, 215
    Text 20, 308, "Weights: coins 60, potions 25, sword 10, crown 5"
    Flip
Wend
Delete weights
End

Whenever the seed changes, the program makes a new stream from it and fills the chests. ChooseWeighted picks one of the weights, coins most often, and returns its place in the list. RandomInt then picks how many. A stream made from the same seed gives the same numbers in the same order, so the same seed fills the chests in the same way. Once they're full, the program doesn't need the stream any more, so it deletes it.

How do I...

Roll dice?

Make a stream once, when the game starts, and use it whenever you like. RandomInt includes both of the numbers you give it:

Local stream:R.RandomStream = R.CreateRandom(42) ; Once, at the start.
Local roll:Int = R.RandomInt(stream, 1, 6) ; 1, 2, 3, 4, 5 or 6.

For dice written the way tabletop games write them, such as 3d6+2 (three six-sided dice, plus 2), import the dice submodule as well:

Import blitz.random As R
Import blitz.random.dice As DiceRules
Local damage:DiceRules.Dice = DiceRules.ParseDice("3d6+2")
If damage = Null Then RuntimeError DiceRules.LastError()
Print "Damage: " + DiceRules.RollDice(stream, damage)
Delete damage

ParseDice returns Null for text it can't read, such as 3D6 or 2d6+1+1.

Do something by chance, such as a one-in-four critical hit?

RandomNumber gives a number from 0 up to, but never quite reaching, 1.

If R.RandomNumber(stream) < 0.25D Then Print "Critical hit!"

Give it two numbers for another range: R.RandomNumber(stream, -1D, 1D).

Shuffle a deck of cards?

Put a number for each card in a List, and shuffle it. The numbers can stand for anything in your game: cards, levels, or the questions in a quiz.

Local deck:Int List = New List
Local card:Int
For card = 0 To 51
    deck.Add(card)
Next
R.Shuffle stream, deck
Print "The top card is number " + deck[0]

Use every one once before any comes round again?

A shuffle bag hands out the numbers from 0 to one less than its size, in a shuffled order, each of them once. When they have all come out, it shuffles them again. Pure chance can repeat some and leave others out, so a bag suits enemy waves, music tracks and quiz questions better.

Local waves:R.ShuffleBag = R.CreateShuffleBag(stream, 5) ; Five kinds of wave.
Local wave:Int = R.DrawBag(waves) ; 0, 1, 2, 3 or 4.

Delete the bag before its stream: the bag borrows the stream.

Get a different game each time it runs?

Make the stream from a seed that changes, such as the time:

Local gameSeed:Int = MilliSecs()
Local world:R.RandomStream = R.CreateRandom(gameSeed)

Keep the seed, or show it to the player, and you can make the same game again.

Save the game, and carry on with the same numbers after loading?

CaptureRandom turns a stream into a line of text to keep in your save. RestoreRandom makes a new stream from it, which carries on with exactly the numbers the old one would have given next.

Local saved:Str = R.CaptureRandom(stream)
; Later, after loading the text back:
Local loaded:R.RandomStream = R.RestoreRandom(saved)
If loaded = Null Then Print R.LastError()

Blip's tip

A seed repeats its results only while your game asks for the same things in the same order. If the map and the loot share one stream, one extra loot roll changes every number after it, the map's included. Give each job a stream of its own: the same seed with a different second number, as in R.CreateRandom(seed, 2).

Good to know

More examples

Three more complete programs come with the module. They print their results instead of drawing them:

The reference below has every function and rule.

Reference

Needs
No other modules
Optional parts
blitz.random.dice
Examples
dice_and_deck.bb, encounters.bb, replay.bb

0.1.0 local preview; unpublished. Independent PCG streams, inclusive integer choices, half-open Double intervals, weighted choices, shuffles and encounter bags. Built-in Rnd, Rand and SeedRnd state is untouched. Not cryptographic. Requires Extended object-lifecycle-v1, runtime 1114 / module ABI 5 or newer. No package dependency; optional blitz.random.dice imports the core.

Start and replay

Blitz3D+ comes with this module, so any Extended program can Import blitz.random.

Dialect "modern"
Import blitz.random
Local stream:Random.RandomStream = Random.CreateRandom(42L, 54L)
Local saved:Str = Random.CaptureRandom(stream)
Print Random.RandomInt(stream, 1, 6)
Delete stream
stream = Random.RestoreRandom(saved)
If stream = Null Then RuntimeError Random.LastError()
Print Random.RandomInt(stream, 1, 6) ; Same result.
Delete stream
End

Import aliases qualify Types and commands. State fields, stepping helpers and constructors are private to the core module; even the dice submodule has no special access. Scalar streams and parsed dice need no empty destructor.

Core API

DeclarationContract and example
RandomStreamOpaque caller-owned state. Delete stream after its bags.
CreateRandom(seed:Long, sequence:Long=1L):RandomStreamAll seed bits, low 63 sequence bits; Random.CreateRandom(-1L, 2L). Negative Longs supply their two's-complement patterns.
RandomInt(stream, minimum:Int, maximum:Int):IntInclusive endpoints, full Int domain supported. Random.RandomInt(stream, -3, 3). Reversed interval is an error; even a singleton consumes one PCG output. Rejection can consume more.
RandomNumber(stream, minimum:Double=0D, maximum:Double=1D):DoubleStrictly increasing finite interval, lower inclusive/upper exclusive. Random.RandomNumber(stream, -1D, 1D). Two outputs per call. Equal endpoints are an empty interval and an error.
ChooseWeighted(stream, weights:Double List):IntBorrowed weights, O(n); returns index or -1 for empty/all-zero without drawing. Random.ChooseWeighted(stream, weights). Negative/non-finite weights or non-finite sum are errors. Zero weights never win.
Shuffle(stream, indices:Int List)Fisher-Yates, O(n) time/O(1) scratch, mutates a borrowed List. Random.Shuffle(stream, deck). Values can index any game-owned data. Empty/singleton consume no draws.
ShuffleBagOpaque owner of a permutation; destructor releases the List, not its stream.
CreateShuffleBag(stream, count:Int):ShuffleBagPositive count; Random.CreateShuffleBag(stream, 5). O(n) allocation, initial shuffle delayed until first draw. Caller owns bag, bag borrows stream.
DrawBag(bag):IntRandom.DrawBag(bag) returns 0..count-1 once per cycle, reshuffling on next draw after exhaustion. Count 1 always yields 0. Cross-cycle repeats are allowed.
CaptureRandom(stream):StrRandom.CaptureRandom(stream) captures current state without advancing it.
RestoreRandom(state:Str):RandomStreamReturns a new owned stream or Null plus LastError(). Never partially restores a live stream.
LastError():StrLast restore diagnostic; cleared by the next restore, including a successful one. Copy immediately. Main-thread only.

All operations reject Null/deleted resources with a contextual RuntimeError. Typed Null and deleted aliases are harmless to Delete. Deletion is explicit: assigning Null or leaving scope does not run destructors. Delete bags before their borrowed stream. Deleting one bag or stream never deletes another independent one.

Local weights:Double List = New List
weights.Add(8D)
weights.Add(2D)
weights.Add(0D) ; Disabled entry.
Print Random.ChooseWeighted(stream, weights)
Local bag:Random.ShuffleBag = Random.CreateShuffleBag(stream, 5)
Print Random.DrawBag(bag)
Delete bag
Delete weights

These fragments assume the stream from the quick start is still alive. The complete encounters example owns and cleans up all inputs.

Optional dice

Import blitz.random.dice As DiceRules
Local damage:DiceRules.Dice = DiceRules.ParseDice("3d6+2")
If damage = Null Then RuntimeError DiceRules.LastError()
Print DiceRules.RollDice(stream, damage)
Delete damage

Dice is opaque scalar state. ParseDice(text:Str):Dice accepts exact lowercase NdS, optionally +K or -K, no whitespace, up to 32 characters. Counts are 1..1000, sides 1..1000000, adjustment fits Int, and the entire possible output interval must fit Int. Leading zeros are accepted within the 10-digit component limit. Parsing malformed text returns Null and the submodule's LastError(). RollDice(stream:Random.RandomStream, dice:Dice):Int borrows both; O(N) unbiased inclusive rolls. Null/deleted inputs are programming errors. Neither expressions nor exploding dice are supported. DiceRules.ParseDice("2d6+1+1") returns Null.

One seed, several modules

noise, dungeons, cameras and audio each create their own stream from the seed you pass them, using sequence 1; steering uses each agent's ID as the sequence. Streams are independent objects, but the same seed with the same sequence produces the same numbers, so passing one game seed to both GenerateDungeon and CreateNoise correlates the layout with the terrain. Derive a distinct seed per purpose instead, for example with a fixed odd multiplier and a per-purpose constant:

Function DeriveSeed:Long(seed:Long, purpose:Long)
    Return seed * 6364136223846793005L + purpose * 1442695040888963407L
End Function
Local layout:Dungeon.Dungeon = Dungeon.GenerateDungeon(64, 64, DeriveSeed(seed, 1L), Null)
Local terrain:Noise.Noise = Noise.CreateNoise(DeriveSeed(seed, 2L), Null)

Your own CreateRandom(seed, sequence) streams can instead vary the sequence.

Stable algorithm and save format

Identifier pcg32-xsh-rr-v1: PCG XSH RR 64/32, multiplier 6364136223846793005, odd increment (sequence << 1) | 1; zero state, one step, add seed, another step. All state arithmetic wraps modulo 2^64. Integer draws reject outputs below 2^32 mod width. Unit Doubles combine 27 high bits of one output and 26 high bits of the next, divided by 2^53. Intervals spanning zero use a convex combination to avoid overflowing their width; rounding to the maximum is moved to its binary64 predecessor. Tiny/adjacent intervals necessarily have very few possible results. Weighted cumulative sums use binary64: extremely small weights can be lost beside large weights. This is a finite-resolution sampler, not exact arbitrary precision.

Capture is exactly 49 ASCII characters: pcg32-xsh-rr-v1:ssssssssssssssss:iiiiiiiiiiiiiiii, lowercase hex, 16 digits per word; s is state, i is the odd increment (not the original sequence argument). Restore validates prefix, exact length, separators, digits and parity before allocation. It accepts every valid state/odd increment, not just fresh seeds. It rejects unknown versions, uppercase hex, spaces and trailing content. Save this text alongside game data. Algorithm/consumption changes require a version change; they can alter worlds even without a signature change.

Examples

See licence, PCG licence and provenance.