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
- Deterministic: The same inputs always produce the same output. Replaying a session produces identical noise values.
- Integer output: All noise functions return
int64values in the range[0, 2^31-1], aligning with Cade’s integer-only arithmetic. - Three-dimensional: Every generator accepts
x,y,zcoordinates. Use whichever dimensions carry meaning for your use case; set unused dimensions to a constant. - Seedable: A global seed controls the noise stream. Per-context and per-variable seeds are derived automatically from the global seed via hashing.
- Cached: Quantized coordinate results are cached (default 256 entries, LRU eviction) to avoid redundant computation in hot paths.
Algorithms
Four algorithms are available, covering the full spectrum from raw speed to smooth coherence:
| Algorithm | Per-sample time | Quality | Memory | Best for |
|---|---|---|---|---|
white | ~0.3μs | Fast | 32 bytes | True random, no correlation needed |
hash | ~0.5μs | Balanced | 0 bytes | High-frequency discrete randomness |
simplex | ~1.0μs | High | 1 KB | Smooth variation, better speed/quality ratio than Perlin |
perlin | ~1.5μs | High | 2 KB | Maximum 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
- All four generators are thread-safe.
- The cache uses a single
sync.Mutex(notRWMutex) because LRU promotion requires a write on every hit. - For hot paths sampling the same coordinates repeatedly, the cache typically absorbs most calls. Profile before increasing cache size — the LRU overhead grows with size.
- White noise ignores coordinates, so it never benefits from the cache. Consider disabling the cache for white noise contexts if memory is constrained.
- Batch generation (
GenerateBatch) reduces function call overhead and improves cache locality for multi-coordinate evaluations.