Noise Generator

Noise Generator

The parametric noise generator provides deterministic, high-quality random number generation for Cade expressions. It integrates with the expression engine through built-in functions and supports multiple algorithms tuned for different performance/quality trade-offs.

Unlike standard pseudo-random generators, parametric noise functions produce values that are spatially coherent — nearby inputs produce nearby outputs — while remaining fully deterministic and reproducible across runs.

Key Properties

Algorithms

Four algorithms are available, covering the full spectrum from raw speed to smooth coherence:

AlgorithmPer-sample timeQualityMemoryBest for
white~0.3μsFast32 bytesTrue random, no correlation needed
hash~0.5μsBalanced0 bytesHigh-frequency discrete randomness
simplex~1.0μsHigh1 KBSmooth variation, better speed/quality ratio than Perlin
perlin~1.5μsHigh2 KBMaximum smoothness, natural-looking variation

The default algorithm is simplex.

White Noise

Uses Go’s PRNG seeded from the noise seed. Coordinates are ignored — each call returns an independent random value. Use this when you need true randomness with no spatial relationship between consecutive values.

Hash Noise

A stateless xxHash-inspired 64-bit hash over the floored coordinates and seed. Pure function with no permutation table — the fastest option when you need discrete, reproducible per-cell randomness (e.g., per-device, per-player-slot).

Simplex Noise

3D Simplex noise using a 512-entry permutation table initialized from the seed. Computes gradient contributions from all four tetrahedron corners, producing smooth spatially-coherent noise. Better performance/quality ratio than Perlin for most use cases.

Perlin Noise

Classic 3D Perlin noise with fade curves and trilinear interpolation across all eight cube corners. Produces slightly smoother results than Simplex at the cost of ~50% more computation. Use when maximum coherence matters more than throughput.

Expression Functions

All noise functions are available directly in Cade expressions.

Noise functions

noise(x, y, z)                   → int  — default algorithm, [0, MaxInt32]
noise_range(x, y, z, min, max)   → int  — default algorithm, [min, max]
perlin(x, y, z)                  → int  — Perlin algorithm, [0, MaxInt32]
simplex(x, y, z)                 → int  — Simplex algorithm, [0, MaxInt32]
hash_noise(x, y, z)              → int  — Hash algorithm, [0, MaxInt32]

Arguments are integers. The noise generator treats them as float64 coordinates internally.

Random functions

random()                         → int  — [0, MaxInt32]
random_range(min, max)           → int  — [min, max]
random_bool()                    → bool
random_percent()                 → int  — [0, 100]

random() and random_range() use white noise with the current timestamp as a coordinate, giving true randomness on each call.

Distribution functions

weighted_choice([w1, w2, w3])    → int  — index 0, 1, or 2 proportional to weights
dice_roll(sides)                 → int  — [1, sides]
coin_flip()                      → bool — 50/50

weighted_choice selects an index based on proportional weights. Weights are integers; only their ratios matter.

Seeding Hierarchy

Noise seeds form a three-level hierarchy, ensuring different systems and variables get isolated but deterministic streams without manual configuration.

Global Seed (set at game startup)
├── Context Seed = hash(globalSeed, contextName)
│   ├── "scoring" context
│   ├── "probability" context
│   └── "expression_random" context
└── Variable Seed = hash(contextSeed, variableName)
    ├── var.dynamic_bonus
    └── var.skill_shot_window

Derivation uses FNV-1a hashing of the context or variable name combined with the parent seed. Two variables with different names but the same parent seed always receive different noise streams.

Global seed defaults: The global seed is set to a time-based value at startup, producing different results each session. For deterministic testing or replay, set global_seed in the noise_generation pragma.

Batch Generation

The Generator interface exposes GenerateBatch(coords []NoiseCoord) []int64 for multi-value generation in a single call. The Context also provides GenerateBatch, which populates the cache for each coordinate.

Batch generation is the preferred path for scoring rules and probability variables that evaluate multiple noise values per frame. The interface contract is identical to sequential Generate() calls, enabling future SIMD or parallel optimizations without changing call sites.

type NoiseCoord struct {
    X, Y, Z float64
}

Caching

Each Context maintains a coordinate-keyed result cache. Coordinates are quantized to 0.01 precision before lookup, so calls with nearby float inputs share cache entries. When the cache reaches its configured size, the least recently used entry is evicted.

Default cache size is 256 entries per context. For scoring rules that sample the same coordinates repeatedly within a frame, this provides significant throughput improvement. For white noise (coordinates ignored), the cache is ineffective and can be reduced or disabled.

Generator Interface

Every algorithm implements the Generator interface:

type Generator interface {
    Generate(x, y, z float64) int64
    GenerateRange(x, y, z float64, minVal, maxVal int64) int64
    GenerateBatch(coords []NoiseCoord) []int64
    Algorithm() Algorithm
    Quality() Quality
    MemoryUsage() int
}

Quality() and MemoryUsage() expose algorithm metadata for introspection and auto-selection logic. Quality values: QualityFast (white), QualityBalanced (hash), QualityHighQuality (simplex, perlin).

Configuration

Global pragma

Set defaults for all noise generation in the pragma block:

pragma {
  noise_generation {
    global_seed        = "deterministic"  # "deterministic", "time", or a specific int64
    default_algorithm  = "simplex"        # "perlin", "simplex", "hash", "white"
    cache_size         = 256
    quality_mode       = "balanced"       # "fast", "balanced", "high_quality"
    debug_mode         = false
    deterministic_time = false            # Fix time for time-based functions (testing)
  }
}

global_seed = "deterministic" uses a fixed seed (0) — useful for reproducible test runs. global_seed = "time" uses the startup timestamp for a different stream each session.

Per-context configuration

Use noise_context blocks to configure isolated noise contexts with independent seeds and algorithms:

noise_context "scoring" {
  algorithm = "simplex"
  seed_base = 12345
  quality   = "balanced"
}

noise_context "probability_variables" {
  algorithm = "hash"
  seed_base = 54321
  quality   = "fast"
}

noise_context "expression_random" {
  algorithm = "white"
  seed_base = "time"
  quality   = "fast"
}

Each named context derives its effective seed from seed_base combined with the global seed. Variables evaluated within a context inherit that context’s seed, further derived by variable name.

Examples

Dynamic scoring variation

Add natural variation to bumper values to prevent predictability:

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

score "event" "bumper_hit" {
  trigger = "device.bumper.hit"
  points  = "${var.bumper_base_value} + noise_range(${var.ball_time}, ${event.device_id}, 0, 0, ${var.bumper_base_value / 5})"
}

Ball time and device ID serve as x/y coordinates, so each bumper produces a distinct variation pattern that evolves smoothly over time.

Skill shot timing window

Use Perlin noise for smooth variation in skill shot duration:

variable "int" "skill_shot_window" {
  initial = 3000
  scope   = "ball"
  formula = "2500 + perlin(${var.ball_number}, ${var.player_number}, 100) % 1000"
}

flow "window" "skill_shot_opportunity" {
  trigger  = "device.plunger.ball_launch"
  duration = "${var.skill_shot_window}ms"
  on_success { points = 50000 }
}

Mystery award distribution

Weighted random selection using weighted_choice:

score "event" "mystery_spinner" {
  trigger   = "device.mystery_spinner.spin"
  condition = "${event.revolutions} >= 3"

  # 50% small / 30% medium / 15% large / 5% jackpot
  points = "${weighted_choice([50, 30, 15, 5]) * [1000, 5000, 25000, 100000]}"
}

Adaptive difficulty

Combine player score with noise for variance-aware difficulty scaling:

variable "int" "difficulty_factor" {
  initial = 100
  scope   = "player"
  formula = "100 + (${var.player_score} / 100000) + noise_range(${var.game_time}, ${var.player_number}, 0, -10, 10)"
}

score "event" "target_hit" {
  trigger = "device.target.hit"
  points  = "max(500, 2000 - (${var.difficulty_factor} * 10))"
}

Seasonal variation

Use day-of-year as a noise coordinate for recurring seasonal patterns:

variable "int" "seasonal_bonus" {
  initial = 0
  scope   = "global"
  formula = "perlin(${system.date.day_of_year}, ${system.date.year}, 0) % 1000"
}

score "event" "seasonal_target" {
  trigger   = "device.special_target.hit"
  condition = "${var.seasonal_bonus} > 500"
  points    = "10000 + ${var.seasonal_bonus}"
}

Performance Notes