Skip to main content

A fast, explicit random generation toolkit

Project description

Fortuna

Fortuna is a fast random-generation toolkit for Python 3.14. It combines an explicit generator model, deterministic stream derivation, native bulk generation, practical probability distributions, and prepared value selectors for games, simulations, procedural generation, and generative systems.

Fortuna 6 is built on the vendored, immutable Storm 5.1.0 engine and is licensed under MIT.

Toolchain-free installs on supported systems: Fortuna 6 ships precompiled CPython 3.14 wheels for macOS arm64 and x86-64, Linux arm64 and x86-64, and Windows x86-64. On those targets, pip and uv install Fortuna without a C++ compiler, platform SDK, or development toolchain. Windows support is notably improved: Visual Studio Build Tools are not required.

Not for cryptography: Fortuna uses MT19937-64. Do not use it for passwords, tokens, keys, secrets, authentication, cryptography, gambling security, or security decisions. Entropy-seeded construction does not make Fortuna cryptographically secure. Python's secrets module is the standard library starting point for security-sensitive randomness.

Installation

Fortuna requires Python 3.14:

python -m pip install Fortuna

With uv:

uv add Fortuna

A source build requires a C++20 toolchain. Contributors working from a checkout should use the reproducible development setup in the development guide.

Five-minute tour

Fortuna's module functions are convenient when a process-local random stream is appropriate:

import Fortuna

initiative = Fortuna.d(20)
fireball_damage = Fortuna.dice(8, 6)
ability_scores = Fortuna.ability_dice(count=6)
morale_adjustment = Fortuna.plus_or_minus_triangular(2)

The corresponding methods on Generator provide explicit ownership and repeatability:

world = Fortuna.Generator(0xD06E_0A5E)

room_count = world.random_int(5, 12)
room_sizes = world.random_int(3, 10, count=room_count)

Two generators with the same seed produce the same stable Fortuna-owned draw schedule:

first = Fortuna.Generator(42)
second = Fortuna.Generator(42)

assert first.dice(3, 6, count=20) == second.dice(3, 6, count=20)

Module defaults and explicit generators

Module-level calls use a Fortuna-owned thread-local generator initialized from process-local entropy. They are appropriate for convenient independent draws:

wand_charges = Fortuna.random_int(1, 20)

Use an explicit Generator when ownership, reproducibility, or worker stream identity matters:

campaign_seed = 8128
encounters = Fortuna.for_stream(campaign_seed, "encounters")
treasure = Fortuna.for_stream(campaign_seed, "treasure")

encounter_roll = encounters.d(100)
treasure_roll = treasure.d(100)

for_stream(root_seed, stream_id) deterministically derives a generator from an unsigned 64-bit root seed and an int, str, or bytes identifier. The stream identifier is an input to seed derivation, not a decorative label, so changing it produces a different deterministic generator sequence. Identifier types are intentionally distinct.

Use from_entropy() when an explicitly owned but nondeterministic generator is required:

generator = Fortuna.from_entropy()

seed(value) is deterministic, including seed(0). It changes only the calling thread's module default. Other threads and existing explicit generators keep their current states.

Scalar and bulk generation

Every numeric generation function and its Generator method accepts the keyword-only argument count:

one_roll = Fortuna.d(20)
many_rolls = Fortuna.d(20, count=100_000)

one_sample = Fortuna.normal_variate(0.0, 1.0)
many_samples = Fortuna.normal_variate(0.0, 1.0, count=100_000)

Bulk generation performs the sampling loop in the native extension without holding the Python GIL and returns a list. count=0 returns an empty list. Arguments are validated before the engine advances.

The public numeric families include:

  • Uniform booleans, integers, ranges, indexes, floats, and canonical draws.
  • Dice, ability dice, and symmetric plus_or_minus distributions.
  • Triangular, normal, log-normal, exponential, gamma, Weibull, beta, Pareto, von Mises, binomial, negative-binomial, geometric, Poisson, extreme-value, chi-squared, Cauchy, Fisher F, and Student's t distributions.
  • Front, center, and back triangular index profiles and prepared normal value profiles.

The complete domains and failure contracts live in the API reference.

Collection operations

Fortuna provides uniform selection, sampling without replacement, and an in-place native Knuth-B shuffle:

monsters = ["goblin", "owlbear", "mimic", "dragon"]

monster = Fortuna.random_value(monsters)
patrol = Fortuna.sample(monsters, 2)
Fortuna.shuffle(monsters)

An explicit generator can be supplied to each module helper or used directly:

generator = Fortuna.Generator(7)

monster = Fortuna.random_value(monsters, generator=generator)
patrol = generator.sample(monsters, 2)
generator.shuffle(monsters)

Fortuna selects Knuth-B for its large-workload behavior. In controlled macOS arm64 release-build measurements it holds a substantial advantage at one million elements; at intermediate sizes the two traversals are close and can exchange small wins. Exact ratios remain machine- and build-specific.

Prepared value generation

Prepared value engines materialize their inputs once and remain ordinary Python callables. They are useful when the same table is drawn repeatedly.

RandomValue

RandomValue combines uniform, cyclic, wide, and positional selection behind one small interface:

loot = Fortuna.RandomValue(["copper", "potion", "wand"])

uniform_drop = loot()
same_as_uniform = loot.uniform()
next_in_order = loot.cycle()
front_loaded_drop = loot.front_triangular()
center_loaded_drop = loot.center_triangular()
back_loaded_drop = loot.back_triangular()
strongly_front_loaded_drop = loot.front_normal()
strongly_center_loaded_drop = loot.center_normal()
strongly_back_loaded_drop = loot.back_normal()
wide_drop = loot.truffle_shuffle()

Its methods are ordinary bound callables. This supports the prepared-generator pattern directly:

loot_gen = Fortuna.RandomValue(["copper", "potion", "wand"]).front_triangular

drop = loot_gen()
more_drops = [loot_gen() for _ in range(20)]

take(count, ...) repeats the engine's default uniform strategy:

loot = Fortuna.RandomValue(["copper", "potion", "wand"])
treasure_pile = loot.take(10)

TruffleShuffle

TruffleShuffle is a stateful wide selector designed for broad uniformity with reduced localized clumping. It shuffles the table once, then moves through that permutation by randomized short distances:

encounter = Fortuna.TruffleShuffle(
    ["goblins", "wolves", "bandits", "skeletons", "giant spiders"]
)

next_encounter = encounter()
encounter_week = encounter.take(7)

TruffleShuffle allows repeats. Its design combines reduced localized repetition with broad long-run coverage. See Algorithm and design notes for the model and its origin.

WeightedChoice

WeightedChoice accepts relative weights directly or through the explicit relative= keyword:

rarity = Fortuna.WeightedChoice(
    relative=[
        (80, "common"),
        (18, "rare"),
        (2, "legendary"),
    ]
)

item_rarity = rarity()

Weights are relative and may total any positive finite value. A weight of zero is allowed, and at least one weight must be positive. Passing the table as the first positional argument is equivalent to relative=table.

Cumulative tables can be transcribed directly from printed roll tables:

treasure = Fortuna.WeightedChoice(
    cumulative=[
        (30, "coins"),
        (60, "gem"),
        (90, "potion"),
        (100, "magic item"),
    ]
)

treasure_found = treasure()

Cumulative boundaries must be finite, nonnegative, and nondecreasing, with a positive final boundary. Equal boundaries represent zero-weight entries. Native generators use Storm's prepared cumulative selector with logarithmic lookup for repeated draws.

Callable values

Prepared value engines resolve selected callables after selection. This makes nested procedural tables concise and evaluates only the selected path:

coins = Fortuna.RandomValue(
    [
        lambda: f"{Fortuna.dice(3, 6)} copper pieces",
        lambda: f"{Fortuna.dice(2, 6)} silver pieces",
        lambda: f"{Fortuna.d(6)} gold pieces",
    ]
)

treasure = coins()

Pass resolve_callables=False when callable objects are the intended values. Callable cycles and runaway chains raise RuntimeError.

Positional profiles

Use a prepared RandomValue when selecting values from an ordered table:

table = ["common", "uncommon", "rare", "very rare", "legendary"]
treasure = Fortuna.RandomValue(table)

front = treasure.front_triangular()
center = treasure.center_triangular()
back = treasure.back_triangular()

strong_front = treasure.front_normal()
strong_center = treasure.center_normal()
strong_back = treasure.back_normal()
  • front_triangular uses the smaller of two uniform indexes.
  • center_triangular uses their integer midpoint.
  • back_triangular uses the larger index.
  • front_normal and back_normal stretch mirrored half-normal curves across the table.
  • center_normal stretches a complete centered normal curve across the table.

The normal profiles place their farthest positions at three standard deviations. Every item remains reachable while the ends of each curve carry approximately 1.1% of its peak weight before normalization.

Process and thread behavior

Module defaults are thread-local. A forked child invalidates the inherited module default and reseeds it before its next draw. A generator created through from_entropy() behaves similarly.

Explicit deterministic generators intentionally preserve copied state through fork. Derive a separate stream for each worker that needs a distinct sequence:

worker_generator = Fortuna.for_stream(root_seed, worker_id)

Built-in operations sharing one exact Generator are serialized, but thread scheduling still determines which thread receives each draw. Value engines also contain mutable strategy state, so callers own their synchronization. Use one value-engine instance per worker or synchronize it externally.

Fork only after concurrent use of a shared explicit generator has quiesced; a child could otherwise inherit its native mutex in a locked state.

Reproducibility boundary

Fortuna's bounded integer, index, range, dice, canonical, bounded-triangular, stream-derivation, uniform value-selection, sampling, and shuffle schedules are stable across supported platforms throughout the Fortuna 6 line.

Standard-library probability distributions, random_float, custom floating transforms, RandomValue's normal profiles, TruffleShuffle's Poisson movement, and WeightedChoice's real draw depend partly on C++ standard-library implementations. They are repeatable within one platform and toolchain build. Their exact seeded sequences are platform- and toolchain-specific. See Algorithm and design notes for the complete boundary.

Documentation

Development

uv sync --frozen --group dev --reinstall-package Fortuna
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run pyright
uv run python -m benchmarks
uv build

Correctness and statistical validation live under tests/. Performance measurement lives under benchmarks/; ordinary CI gates correctness, while timing provides evidence for engineering decisions.

License

Fortuna is available under the MIT License. Its vendored Storm header is also MIT licensed; provenance and checksums are recorded in the vendoring record.

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fortuna-6.1.1.tar.gz (120.9 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

fortuna-6.1.1-cp314-cp314-win_amd64.whl (350.5 kB view details)

Uploaded CPython 3.14Windows x86-64

fortuna-6.1.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl (203.7 kB view details)

Uploaded CPython 3.14manylinux: glibc 2.24+ x86-64manylinux: glibc 2.28+ x86-64

fortuna-6.1.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl (188.4 kB view details)

Uploaded CPython 3.14manylinux: glibc 2.24+ ARM64manylinux: glibc 2.28+ ARM64

fortuna-6.1.1-cp314-cp314-macosx_11_0_arm64.whl (161.2 kB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

fortuna-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl (182.7 kB view details)

Uploaded CPython 3.14macOS 10.15+ x86-64

File details

Details for the file fortuna-6.1.1.tar.gz.

File metadata

  • Download URL: fortuna-6.1.1.tar.gz
  • Upload date:
  • Size: 120.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fortuna-6.1.1.tar.gz
Algorithm Hash digest
SHA256 541270edafadd725c67ed028da1a3c3126e002aae39eadb33357701e590cca7a
MD5 64001575f0f3dfad5f7fe7368b97faa2
BLAKE2b-256 72b5cfbc232497002d68068a9a9fb923b9c2bb425e206ae19ee4faa95ee38684

See more details on using hashes here.

File details

Details for the file fortuna-6.1.1-cp314-cp314-win_amd64.whl.

File metadata

  • Download URL: fortuna-6.1.1-cp314-cp314-win_amd64.whl
  • Upload date:
  • Size: 350.5 kB
  • Tags: CPython 3.14, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fortuna-6.1.1-cp314-cp314-win_amd64.whl
Algorithm Hash digest
SHA256 1124a6272d90b156829830b127ebe1016ddb484429e060190122911808eca8db
MD5 cb41eb137b1fee43cceabcc5f96c922d
BLAKE2b-256 f9a2eecd09de21b2cf34d8321ad2c2669d8740d8762d1e5a220acdf86322525b

See more details on using hashes here.

File details

Details for the file fortuna-6.1.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl.

File metadata

  • Download URL: fortuna-6.1.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
  • Upload date:
  • Size: 203.7 kB
  • Tags: CPython 3.14, manylinux: glibc 2.24+ x86-64, manylinux: glibc 2.28+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fortuna-6.1.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 833845f3269a3a59c11f8a8154180045bdfd673fdf20a752896d31575c2c9de3
MD5 56d1dc2ad5bccebd95a338820fe2a695
BLAKE2b-256 df3918f7279c13bdf0e689da75f0961367cce340dc29ea281f33cf7bfe31bac2

See more details on using hashes here.

File details

Details for the file fortuna-6.1.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl.

File metadata

  • Download URL: fortuna-6.1.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl
  • Upload date:
  • Size: 188.4 kB
  • Tags: CPython 3.14, manylinux: glibc 2.24+ ARM64, manylinux: glibc 2.28+ ARM64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fortuna-6.1.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 f647cc821b462c2786c9441d02da06ba442f5f9d3bdc56d4ee53a27753ddc055
MD5 82ea46cf2730997684ea9946bc4f4d98
BLAKE2b-256 8f3abb149a29f497971d18f621a8972a0e96e82e6aca99fd5357374df304e581

See more details on using hashes here.

File details

Details for the file fortuna-6.1.1-cp314-cp314-macosx_11_0_arm64.whl.

File metadata

  • Download URL: fortuna-6.1.1-cp314-cp314-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 161.2 kB
  • Tags: CPython 3.14, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fortuna-6.1.1-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 0713bd3498c8db14bea4ac60252a15c9af617af371dc98774f22079df5c8a22e
MD5 977e21a53e97c804358cd0e97305720c
BLAKE2b-256 3c3b01280088ae6b6314e626cf36226081f360e48a11971d092c200f14d93002

See more details on using hashes here.

File details

Details for the file fortuna-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl.

File metadata

  • Download URL: fortuna-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl
  • Upload date:
  • Size: 182.7 kB
  • Tags: CPython 3.14, macOS 10.15+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fortuna-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 72564433d498c6ec240ae42e3baa0f3903c7ce74e0aba3df2a21906b33eaf2f9
MD5 ee792fe4e2cbdfee863c3acbbd81a3e5
BLAKE2b-256 3f4f60a415ea57b3942290f89b844ee68688e82b6e7f1f8dd41db1f41120a8cc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page