Repeatable random numbers from seeded streams, with weighted choices, shuffles and encounter bags.
Import blitz.random As R gives your program these, with R. in front. Each name leads to its part of the reference.
LastError:Str()CreateRandom:RandomStream(seed:Long, sequence:Long = 1L)RandomInt:Int(stream:RandomStream, minimum:Int, maximum:Int)RandomNumber:Double(stream:RandomStream, minimum:Double = 0D, maximum:Double = 1D)ChooseWeighted:Int(stream:RandomStream, weights:Double List)Shuffle(stream:RandomStream, indices:Int List)CreateShuffleBag:ShuffleBag(stream:RandomStream, count:Int)DrawBag:Int(bag:ShuffleBag)CaptureRandom:Str(stream:RandomStream)RestoreRandom:RandomStream(state:Str)An optional part. Import blitz.random.dice As DiceRules as well to use these.
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.

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.
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.
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).
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]
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.
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.
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).
RandomInt includes both ends, RandomNumber only the first.
R.RandomNumber(stream, 0D, 10D) can give 0, but never 10.ChooseWeighted returns -1.Rnd and Rand carry on as before. The module's streams are its own,
and never change Blitz's Rnd, Rand or SeedRnd.Three more complete programs come with the module. They print their results instead of drawing them:
3d6+2 damage for each with the dice submodule, and picks a number between
0 and 1.The reference below has every function and rule.
blitz.random.dice0.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.
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.
| Declaration | Contract and example |
|---|---|
RandomStream | Opaque caller-owned state. Delete stream after its bags. |
CreateRandom(seed:Long, sequence:Long=1L):RandomStream | All seed bits, low 63 sequence bits; Random.CreateRandom(-1L, 2L). Negative Longs supply their two's-complement patterns. |
RandomInt(stream, minimum:Int, maximum:Int):Int | Inclusive 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):Double | Strictly 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):Int | Borrowed 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. |
ShuffleBag | Opaque owner of a permutation; destructor releases the List, not its stream. |
CreateShuffleBag(stream, count:Int):ShuffleBag | Positive count; Random.CreateShuffleBag(stream, 5). O(n) allocation, initial shuffle delayed until first draw. Caller owns bag, bag borrows stream. |
DrawBag(bag):Int | Random.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):Str | Random.CaptureRandom(stream) captures current state without advancing it. |
RestoreRandom(state:Str):RandomStream | Returns a new owned stream or Null plus LastError(). Never partially restores a live stream. |
LastError():Str | Last 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.
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.
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.
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.
See licence, PCG licence and provenance.