Noise Generator

expressions noise procedural

Noise Generator

The noise generator gives your expressions deterministic, high-quality random numbers. The same inputs always produce the same result, so a run can be reproduced exactly, yet the values vary smoothly across coordinates when you want spatial coherence rather than pure chaos.

Key Properties

  • Deterministic — identical coordinates and seed always return the same value, so runs are reproducible for testing and tuning.
  • Integer output — values come back as integers, matching Cade’s integer-first arithmetic. Use a range argument or division to scale into the band you need.
  • Three-dimensional coordinates — every noise function takes x, y, z. Unused dimensions can be set to 0 or a constant.
  • Seedable — a global seed plus per-context and per-variable derivation lets you isolate independent random streams.
  • Cached — recently generated values are cached so repeated lookups at the same coordinates are cheap.

Algorithms

Four algorithms are available. They trade speed against coherence (how smoothly neighboring coordinates relate to each other).

AlgorithmPer-sample timeQualityMemoryBest for
white~0.3μsFair (no coherence)~32 bytesPure randomness where correlation is undesirable
hash~0.5μsGood (low coherence)noneHigh-frequency sampling that just needs a good spread — the default
simplex~1.0μsExcellent~1KBBalanced quality and speed
perlin~1.5μsExcellent (smooth)~2KBSmooth, natural variation between nearby coordinates

hash is the default algorithm. Choose another when your use case calls for it:

  • white — for values with no relationship to their coordinates: a coin flip, a dice roll, a one-off random pick.
  • hash — for very frequent sampling where you want a good distribution but do not need neighboring coordinates to relate to one another.
  • simplex — the general-purpose choice: smooth, high-quality output at a moderate cost.
  • perlin — when you want the smoothest transitions between nearby coordinates, for example a value that drifts gently as a timer advances.

Expression Functions

Noise functions are available inside formula, when, points, and other expression attributes. Signatures below use plain notation; arguments are integers and results are integers (or booleans where noted).

Noise functions — coordinate-based, coherent values:

noise(x, y, z)                  -> int   # default algorithm
noise_range(x, y, z, min, max)  -> int   # scaled into [min, max]
perlin(x, y, z)                 -> int   # smooth coherent noise
simplex(x, y, z)                -> int   # balanced coherent noise
hash_noise(x, y, z)             -> int   # fast hashed noise

Random functions — uncorrelated values:

random()                 -> int    # 0 .. large positive integer
random_range(min, max)   -> int    # inclusive range
random_bool()            -> bool
random_percent()         -> int    # 0 .. 100

Distribution functions — shaped random outcomes:

weighted_choice([w1, w2, ...])  -> int    # index 0..n-1, chance proportional to weights
dice_roll(sides)                -> int    # 1 .. sides
coin_flip()                     -> bool

Probability distributions — continuous draws, returning floats rather than integers:

normal(mean, stddev)   -> float   # normal (Gaussian) distribution
uniform(min, max)      -> float   # even spread; min must be less than max
exponential(lambda)    -> float   # lambda must be positive

These return floats, while scoring arithmetic is integer-first. They fit naturally in float-valued attributes such as a synth velocity; for a point value, prefer the integer functions above.

For the general operator and math-function reference used in expressions (arithmetic, comparisons, ternary, min, max, and friends), see Console Expressions. The functions above are the additional noise and randomness functions layered on top of that language.

Seeding Hierarchy

Seeds are derived top-down so that independent parts of a table get independent random streams while remaining reproducible. The global seed anchors everything; contexts and variables derive their own seeds from it:

Global Seed (game instance)
├── Context Seed (e.g. "scoring")     = Global + hash("scoring")
├── Context Seed (e.g. "probability") = Global + hash("probability")
└── Variable Seed (a specific var)    = Context + hash(variable_name)

To make a run fully reproducible, fix the global seed in the noise_generation pragma:

pragma {
  noise_generation {
    global_seed = "deterministic"
  }
}

A "deterministic" global seed yields the same sequence every run. Use "random" or "time" when you want fresh randomness per session.

Caching

The generator caches recently produced values keyed by their coordinates, seed, and algorithm. Repeated lookups at the same coordinates return the cached value instead of recomputing it. The cache size is set with the cache_size attribute (default 256).

Caching helps most when the same coordinates are sampled repeatedly — for example a formula that re-evaluates frequently with slowly changing inputs. It helps little when every lookup uses fresh, unique coordinates (such as time-based randomness), since each call is a cache miss.

Configuration

Global pragma

Set defaults for the whole table in the noise_generation pragma:

pragma {
  noise_generation {
    global_seed       = "deterministic"  # "deterministic", "random", "time", or an integer (default: "random")
    default_algorithm = "hash"           # "perlin", "simplex", "hash", "white" (default: "hash")
    cache_size        = 256              # number of cached noise values (default: 256)
    default_quality   = "balanced"       # "fast", "balanced", "high_quality" (default: "balanced")
  }
}

Per-context configuration

Beyond the global default, you can declare named noise_context blocks that give different systems their own isolated, independently-seeded noise stream. Each block picks its own algorithm, seed, and quality:

# Deterministic scoring noise — identical results across runs
noise_context "scoring" {
  algorithm = "simplex"        # "perlin", "simplex", "hash", "white"
  seed_base = 12345            # integer, "deterministic", "random", or "time"
  quality   = "balanced"       # "fast", "balanced", "high_quality"
}

# Fast, fresh randomness for one-off draws
noise_context "expression_random" {
  algorithm = "white"
  seed_base = "time"
  quality   = "fast"
}

seed_base accepts an integer (used directly), "deterministic" (a fixed seed, so the context replays identically every run), or "random" / "time" (both seed from the clock, giving fresh randomness each session). Give a context a fixed integer seed_base when it needs deterministic behavior while the rest of the table uses time-based randomness, or choose a different algorithm when it gives a better gameplay feel (for example perlin for smooth drift versus white for sharp randomness).

An unrecognized algorithm is rejected at load with an error naming the four valid choices. quality is advisory — it is accepted and recorded, but each algorithm’s output is fixed, so treat it as documentation of intent rather than a knob that changes results. For the full attribute reference, see the Noise Context API page.

Examples

Add natural variation to a scored value so it is not perfectly predictable:

variable "int" "bumper_base_value" {
  initial = 1000
  scope   = "player"
}

score "event" "bumper_hit" {
  when = device.bumper.hit

  # Base value plus 0–20% noise variation
  points = var.bumper_base_value + noise_range(var.ball_time, event.device_id, 0, 0, var.bumper_base_value / 5)
}

Use smooth Perlin noise to wobble a timing window between 2.5 and 3.5 seconds:

variable "int" "skill_shot_window" {
  initial = 3000
  scope   = "ball"

  formula = 2500 + perlin(var.ball_number, var.player_number, 100) % 1000
}

Pick a mystery award by weight — 50% small, 30% medium, 15% large, 5% jackpot:

score "event" "mystery_award" {
  when      = device.mystery_spinner.spin
  condition = event.revolutions >= 3

  points = weighted_choice([50, 30, 15, 5])
}

Roll for a jitter value with pure random draws — random_range is uncorrelated regardless of the default algorithm:

variable "int" "jitter" {
  initial = 0
  scope   = "ball"

  formula = random_range(-10, 10)
}

Performance Notes

Noise generation is safe to use from concurrent expression evaluation; you do not need to coordinate access yourself. For per-sample cost, prefer hash or white when you sample at very high frequency and do not need coherence, and simplex or perlin when smoothness matters more than raw speed.

Tune cache_size only when profiling points to it: raise it when many evaluations re-sample the same coordinates and you want to spend memory to skip recomputation, and lower it when memory is tight or every lookup uses unique coordinates and the cache cannot help. Leaving a context’s cache_size unset (or at 0) takes the default of 256.