Skill: Use random
Roll dice, pick from a list, shuffle, and scatter things with the engine's seedable random generator `BT.random` (engine 1.5.0+) instead of `Math.random()`....
Written for blit386 1.8.0. The rest of blit386.dev documents the newest engine, so an API you find there may be missing from a game pinned to 1.8.0.
BT.random is the engine's random number generator. It does everything Math.random() does, plus the things games
actually need - pick from a list, shuffle, weighted drops, a random direction - and it can be seeded so a run plays out
identically every time.
When to use
Use for spawn positions, enemy choice, loot drops, damage rolls, screen shake, shuffling a deck, scattering stars, or any "pick something at random". Also use when the user wants a daily challenge, a shareable level code, or a replay - anything where the same seed must produce the same game.
For terrain, caves, clouds, or procedural textures, use the use-noise skill instead. That is a different tool: noise
answers "what is at this coordinate", random answers "give me the next value".
How to do it
BT.random is a getter (no parentheses), and it is the same generator every time you read it.
update() {
// whole numbers, high end excluded: 0, 1, or 2
const lane = BT.random.int(3);
// whole numbers in a range, high end excluded: 10..319
const x = BT.random.int(10, 320);
// whole numbers, both ends included: 1..6, like a die
const damage = BT.random.intInclusive(1, 6);
// a decimal between two values
const speed = BT.random.float(0.5, 2.0);
// true 30% of the time
if (BT.random.bool(0.3)) {
this.spawnEnemy();
}
}int and intInclusive give you whole numbers already, so they are safe to use as screen coordinates directly. float
is not - round it with Math.floor before drawing.
Picking and shuffling
const enemy = BT.random.pick(['bat', 'slime', 'ghost']); // one item
const deck = BT.random.shuffle(this.cards); // a new shuffled array
// weights do not have to add up to anything in particular
const drop = BT.random.weighted(['coin', 'gem', 'crown'], [70, 25, 5]);pick throws on an empty array, so check length first if the list can run out. shuffle returns a new array and
leaves the original alone; use shuffleInPlace only when you deliberately want the original changed.
Scattering things around
import { Rect2i, Vector2i } from 'blit386';
const spot = BT.random.insideRect(new Rect2i(0, 0, 320, 240)); // Vector2i
const step = BT.random.direction4(); // (1,0), (-1,0), (0,1), or (0,-1)
const drift = BT.random.direction8(); // the four above plus diagonals
const heading = BT.random.angle(); // 0..2*PI radians
const shake = BT.random.gaussian(0, 2); // clustered near 0, occasionally fargaussian is the one to reach for when "mostly small, sometimes big" looks better than an even spread - screen shake,
scatter, enemy speed variation.
Same seed, same game
By default the generator is seeded from the clock, so every run differs. Seed it yourself to make a run repeatable:
async init() {
BT.randomSeed(1234); // this level now generates identically every time
// ...
return true;
}That is what makes daily challenges, level codes, and bug reports you can actually reproduce possible. Store the seed with the save and you can rebuild the same world from a single number.
To reproduce a run without touching code, open the game with ?seed=1234 in the URL. The engine seeds BT.random right
before init() runs and logs [BT] Seeded BT.random from ?seed=1234; it works in a built game too. A BT.randomSeed()
call in init() runs later and wins, so do not add one "just in case" if the user wants the URL to control the seed.
Anything that is not a safe integer - plain digits, optionally negative, within Number.MIN_SAFE_INTEGER to
Number.MAX_SAFE_INTEGER - is ignored with a warning (?seed=1.5, ?seed=1e3, ?seed=abc).
Your own generator
BT.random is shared, so anything that draws from it shifts what everything else gets next. When you need one part of
the game to be reproducible on its own, give it a private generator:
import { Random } from 'blit386';
// in init():
this.levelRng = new Random(1234); // level layout, unaffected by combat rolls
this.combatRng = new Random(99); // damage rolls, unaffected by layoutA Random instance has exactly the same methods as BT.random. Useful extras when you need them:
| Call | What it does |
|---|---|
rng.seed(n) | Restart the sequence from seed n |
rng.getState() | The current position, as a number you can save |
rng.setState(n) | Jump back to a saved position and replay from there |
rng.clone() | A copy that will produce the identical sequence from here |
rng.fork() | A new independent stream (advances the parent once) |
rng.seedValue | The seed it was last given (undefined after setState or fork) |
Notes
- Prefer
BT.randomoverMath.random()everywhere. Same convenience, plus seeding - there is no reason to keep the built-in one. BT.randomis a getter with no parentheses;BT.randomSeed(n)is a method with them.- Draw random values in
update(), notrender().render()can run a different number of times per update, so rolling there makes things flicker and breaks reproducibility. - In a per-frame loop over many items,
insideRectTo(rect, out)andpointInRangeTo(min, max, out)write into aVector2iyou already own instead of making a new one each call - see thekeep-it-fastskill. - Ranges are half-open where you would expect:
int(1, 5)never returns5. UseintInclusive(1, 5)when you want it to. Inverted or empty ranges throw rather than returning nonsense. - Needs blit386
^1.5.0.