Skip to main content

Deterministic Procedural Generation (DPG)

Overview

ConjureDB supports Deterministic Procedural Generation (DPG): a set of query operators and runtime primitives that produce reproducible, seed-driven content entirely within the compiled query pipeline. DPG targets game-client scenarios where content must be generated on the fly — loot tables, daily offers, NPC populations, level layouts, reward distributions — while guaranteeing that the same seed always yields the same output on every platform and every run.

Key properties:

PropertyDescription
DeterministicSame seed + same input = identical output across platforms. The integer hashing core and hash_float are bit-reproducible; hash_normal is the one exception (see Design Notes).
Efficientgenerate compiles to an allocation-free counted loop. sample/shuffle currently buffer their input into managed lists (see Design Notes).
ComposableDPG operators (generate, sample, shuffle, materialize) compose freely with all other pipeline stages (filter, sort, join, derive, select, etc.).

Core Concepts

Deterministic Hashing

DPG is built on a runtime hashing library (DpgHash) based on xxHash64. Every hash function is pure — no side effects, no global state, and an integer-only mixing core. The integer hash functions (hash, hash_int, hash_index) and hash_float produce bit-identical results regardless of platform, JIT, or runtime version. hash_normal is the exception — its Box-Muller transform uses floating-point transcendental math (see Design Notes).

Hash Functions

FunctionSignatureReturn TypeDescription
hashhash(seed: long, key: long) → longlongCore 64-bit hash. Combines a seed with an arbitrary key.
hash_inthash_int(seed: long, value: long, min: int, max: int) → intintUniform integer in [min, max).
hash_floathash_float(seed: long, value: long) → doubledoubleUniform double in [0.0, 1.0). IEEE 754 compliant.
hash_normalhash_normal(seed: long, value: long, mean: double, stddev: double) → doubledoubleNormally distributed double (Box-Muller transform; not guaranteed bit-identical across platforms — see Design Notes).
hash_indexhash_index(seed: long, value: long, count: int) → intintUniform index in [0, count). Sugar for hash_int(seed, value, 0, count).

All public DPG hash functions accept a long seed as the first argument and produce fully deterministic output. Range, float, normal, and index helpers also take an explicit value input that is folded into the deterministic hash.


Virtual Row Generation

The generate(count, seed) operator produces virtual rows without any underlying table or DbSet. Each row receives two implicit columns:

ColumnTypeDerivation
RowIndexintSequential 0 .. count-1.
Seedlonghash(baseSeed, rowIndex) — unique per-row seed.

Virtual rows are created lazily during query execution. No intermediate collection is allocated — the emitter produces a counted loop that feeds downstream operators directly.

from generate(100, @worldSeed) g
| select g.RowIndex, g.Seed

Sampling

The sample operator performs deterministic subset selection:

sample <count> seed <seed_expr> [by <weight_expr>]

Uniform sampling (no by clause): each row is hashed with the provided seed; the top-N rows by hash priority are selected. The current implementation buffers the input rows and their priorities into managed lists, then performs an O(N·K) partial selection of the top-N (K = requested count) — it is not a single-pass, allocation-free scan.

Weighted sampling (by weight_expr): implements the Efraimidis-Spirakis one-pass weighted reservoir algorithm. Each row's priority is derived from a deterministic uniform sample raised to (1 / weight), and the top-N priorities win. This guarantees that higher-weight rows are proportionally more likely to appear.

By default, sampling is without replacement — each row can appear at most once. Append with replacement to allow duplicates.


Shuffling

The shuffle operator applies a deterministic permutation to all input rows:

shuffle <seed_expr>

Semantically, shuffle seed is sugar for sample int.MaxValue seed expr (without replacement). Every row receives a hash-derived priority and the output is ordered by that priority, producing a deterministic permutation of the entire input.


Materialization

The materialize operator persists DPG results into a named table and controls when that table is refreshed:

materialize into <TableName> [refresh on(<sources>) [every(<interval>)]]

The materialize clause records the target table and its refresh policy (the source tables and interval) as plan metadata on the compiled query. Note that automatic population of the target DbSet and scheduled regeneration are not yet wired — the compiled query currently returns the generated rows directly; treat the refresh on(...) / every(...) policy as declarative intent for now.


Usage Patterns

Pattern 1: Simple Loot Table

Select 5 weighted items from a loot template table.

from LootTemplates t
| sample 5 seed @playerSeed by t.DropWeight
| select t.ItemId, t.Name, t.Rarity

Pattern 2: Generated Inventory with Templates

Generate 100 virtual slots, join each to an item template using a hashed index, then derive per-slot quantity and quality.

from generate(100, @worldSeed) g
| join ItemTemplates t (hash_index(g.Seed, 0L, 50) == t.Id)
| derive {
Quantity = hash_int(g.Seed, 0L, 1, 10),
Quality = hash_float(g.Seed, 0L)
}
| select g.RowIndex as Slot, t.Name, Quantity, Quality

Pattern 3: Daily Offers

Deterministic daily shop: 3 weighted products, materialized and refreshed on source change or every 24 hours.

from Products p
| sample 3 seed @dailySeed by p.OfferWeight
| select p.Id, p.Name, p.Price
| materialize into DailyOffers refresh on(Products) every(86400 s)

Pattern 4: Weighted Enemy Spawning

Use sample ... by <weight_expr> for weighted deterministic picks. The standalone hash_weighted_index helper is not part of the current DSL surface, so weighted selection should be expressed through sample, while positional and level variation can still come from hash_float(seed, value) and hash_int(seed, value, min, max).

Pattern 5: Shuffled Deck

Deterministic shuffle of a card deck, then draw 5.

from Cards c
| shuffle @gameSeed
| take 5
| select c.Suit, c.Rank, c.Value

Pattern 6: Deterministic NPC Names

Combine first and last name tables via independent hashed indices to generate unique NPC names.

from generate(50, @townSeed) g
| join FirstNames fn (hash_index(g.Seed, 0L, count(fn)) == fn.Id)
| join LastNames ln (hash_index(hash(g.Seed, 100L), 0L, count(ln)) == ln.Id)
| select fn.Name as First, ln.Name as Last, g.RowIndex as NpcId

Materialization & Refresh

Source-Based Refresh

from ... | materialize into Cache refresh on(Templates, Config)

This declares that Cache should be regenerated when Templates or Config change. (The refresh policy is recorded as metadata; automatic regeneration is not yet wired — see Materialization.)

Interval-Based Refresh

from ... | materialize into DailyDeals every(86400 s)

Declares a 24-hour (86 400 s) regeneration interval for the materialized target. (Declarative today — see Materialization.)

Combined

from ... | materialize into Cache refresh on(Templates) every(3600 s)

Whichever condition fires first triggers regeneration. After regeneration the interval timer resets.


Hash Function Reference

FunctionSignatureRangeExample
hash(seed: long, key: long) → longFull long rangehash(@seed, 42L) → deterministic 64-bit value
hash_int(seed: long, value: long, min: int, max: int) → int[min, max)hash_int(@seed, 0L, 1, 100) → integer in [1, 100)
hash_float(seed: long, value: long) → double[0.0, 1.0)hash_float(@seed, 0L)0.7312...
hash_normal(seed: long, value: long, mean: double, stddev: double) → doubleUnbounded (normal dist.)hash_normal(@seed, 0L, 0.0, 1.0)−0.42...
hash_index(seed: long, value: long, count: int) → int[0, count)hash_index(@seed, 0L, 50) → index in [0, 50)

Design Notes

  • Determinism guarantee. The integer mixing core uses integer-only arithmetic, and hash, hash_int, hash_index, and hash_float are bit-reproducible across platforms. hash_normal is the exception: its Box-Muller transform calls System.Math transcendental functions (log/sqrt/cos), which are not guaranteed to be bit-identical across platforms and runtimes — treat hash_normal as best-effort rather than bit-reproducible.

  • Performance. generate compiles to an allocation-free counted loop. sample (and therefore shuffle, which desugars to it) buffers the input rows and their priorities into managed List<T> collections and performs an O(N·K) partial selection of the top-N by priority — so the sample/shuffle path does allocate and is not single-pass.

  • Composability. DPG operators are regular pipeline stages. They can be freely combined with filter, sort, join, derive, group, select, and all other operators. The optimizer handles DPG nodes like any other relational operator — they participate in predicate pushdown, join reordering, and emission.

  • Materialization. The materialize into … refresh on(…) every(…) clause is parsed and records the target table, refresh sources, and interval as plan metadata. Runtime auto-population and scheduled regeneration of the target are not yet implemented (there is no game-loop tick API), so treat the refresh policy as declarative today.

  • Compatibility with PGO. DPG queries benefit from Profile-Guided Optimization — PGO data can size output buffers, select join strategies, and tune aggregation thresholds for materialized DPG results.