Make random rewards reproducible, reviewable, and safe to ship.
LootLint is an engine-agnostic loot-table runtime, simulator, and CI regression gate for game teams. It executes real nested tables and bad-luck protection, measures what unlucky players experience, finds counterexample seeds, and replays exact selection traces. It has no runtime dependencies, backend, account, telemetry, or uploaded game data.
This is not a loot-table mockup. The CLI and Python API use the same deterministic runtime:
- weighted item, nested-table, and explicit no-drop entries;
- multiple draws, quantity ranges, and sampling with or without replacement;
- soft pity through additive weights and hard pity through a guaranteed roll;
- independent player streams from a documented stable PRNG;
- per-roll item rates, quantities, Wilson 95% intervals, first-hit p50/p90/p95/p99, unreached-player rate, and dry-streak tails;
- project-owned acceptance budgets that return a failing process exit code;
- paired-seed comparison between a baseline and candidate;
- counterexample seed search plus byte-stable JSON replay receipts;
- console, JSON, Markdown, self-contained HTML, and SARIF output.
From a source checkout with uv:
uv sync --all-groups
uv run lootlint validate examples/roguelike/loot.toml
uv run lootlint audit examples/roguelike/loot.toml
uv run lootlint hunt examples/broken-pity/loot.toml \
--target tag:legendary --min-misses 60 --seeds 200 --rolls 80 \
--output reports/broken-seed.json
uv run lootlint replay examples/broken-pity/loot.toml reports/broken-seed.jsonThe healthy roguelike audit exits 0. The intentionally broken fixture exits 1, and the
receipt replay exits 0 only while the specification and runtime still reproduce every weighted
choice.
To generate the committed demo evidence and run the complete release gate:
uv run python scripts/demo.py
uv run python scripts/verify.py
uv run python scripts/package_release.py
uv run python scripts/release_check.pySee acceptance commands and the repair playbook for expected output and failure-specific recovery.
Install the wheel from the latest GitHub Release:
pipx install https://github.com/KanadeK/lootlint/releases/download/v0.1.0/lootlint-0.1.0-py3-none-any.whl
lootlint --versionOr install an extracted source release into an isolated environment:
python -m venv .venv
.venv/Scripts/python -m pip install .On macOS/Linux, use .venv/bin/python in the second command.
lootlint init loot.toml --preset roguelike
lootlint audit loot.toml --format html --output loot-report.htmlThe starter is not a placeholder: it includes five items, three nested pools, an explicit no-drop outcome, soft and hard pity, a deterministic workload, and a tail budget.
version = 1
root = "boss_chest"
[simulation]
players = 2000
rolls_per_player = 120
seed = 20260730
[items.starforged_blade]
label = "Starforged Blade"
tags = ["equipment", "legendary"]
[tables.boss_chest]
draws = 1
replacement = true
[[tables.boss_chest.entries]]
id = "legendary"
item = "starforged_blade"
weight = 5
[[tables.boss_chest.entries]]
id = "empty"
nothing = true
weight = 95
[pity.legendary_bad_luck_protection]
table = "boss_chest"
entry = "legendary"
starts_after = 12
add_weight_per_miss = 5
guarantee_at = 30
[[expectations]]
id = "legendary-access"
tag = "legendary"
max_p99_first_hit = 30
max_dry_streak = 29
hard_pity_required = trueThe full format, including nested table quantities and JSON equivalents, is documented in the v1 specification. The bundled JSON Schema is available without a network request:
lootlint schema --output lootlint-v1.schema.json| Command | Real behavior | CI exit rule |
|---|---|---|
validate |
Checks references, cycles, entry IDs, weights, quantities, pity targets, unreachable content, and expectation selectors | non-zero on structural errors |
simulate |
Executes independent players and reports item rates plus configured tail metrics | non-zero on invalid input |
audit |
Runs validate and simulate, then enforces only budgets declared in the spec |
non-zero on any violated budget |
compare |
Uses identical player seeds for baseline and candidate and measures item-rate and p99 drift | non-zero when a supplied delta limit is exceeded |
hunt |
Searches seeds for a requested miss streak and writes the exact trace | non-zero when no searched seed meets the threshold |
replay |
Re-runs a receipt and compares every drop, ticket, adjusted weight, and pity transition | non-zero on fingerprint or outcome divergence |
init |
Writes a complete roguelike or gacha starter | refuses to overwrite unless --force |
schema |
Prints the packaged v1 JSON Schema | non-zero on write failure |
Run lootlint <command> --help for every option. Simulation is bounded to 50 million root rolls
per invocation and seed hunting to 10 million, preventing an accidental CI resource spike.
The repository includes a reusable composite action:
- uses: KanadeK/lootlint@v0.1.0
with:
spec: game-data/loot.tomlOr use the CLI directly:
- run: pip install lootlint-0.1.0-py3-none-any.whl
- run: lootlint audit game-data/loot.toml --format sarif --output lootlint.sarifSARIF preserves stable rule IDs such as LL1010 (nested cycles) and LL2003 (p99 budget).
See the rule catalog.
A generic validator layered over an arbitrary Unity, Godot, or custom schema usually costs more to integrate than it saves. LootLint makes the data contract part of the tool: designers can edit TOML, engines can consume canonical JSON, CI can execute the reference Python runtime, and QA can attach one replay receipt to a bug. This keeps selection semantics, pity state, and tests aligned.
The v1 contract deliberately does not claim to model an entire game economy or prove that a design is fun. It answers narrower questions with reproducible evidence:
- Can every referenced reward be reached?
- Does the implementation enforce the promised hard ceiling?
- What do the least-lucky simulated players see?
- Did this content change silently move a drop rate or first-hit tail?
- Which exact seed reproduces the reported streak?
Project owners define the budgets. LootLint does not label monetization, rarity, or a particular probability as ethical or balanced on their behalf.
LootLint uses SplitMix64 streams derived per player, integer tickets, declared entry order, and no wall-clock fields in reports. A receipt stores the specification SHA-256 plus every selected table entry, ticket, adjusted weight, and pity transition. The same LootLint version and specification must replay it exactly.
Changing any source field intentionally changes the fingerprint. Cross-version replay compatibility is tested with committed golden vectors. Read determinism and limits before using receipts as long-lived test fixtures.
flowchart LR
A["TOML or JSON spec"] --> B["Parser + structural rules"]
B --> C["Deterministic runtime"]
C --> D["Independent-player simulation"]
C --> E["Counterexample hunt"]
D --> F["Project budgets"]
D --> G["Paired-seed diff"]
E --> H["Replay receipt"]
F --> I["Console / JSON / HTML / SARIF"]
G --> I
The core is library-first: load_spec, LootEngine, simulation, auditing, comparison, and replay
are ordinary Python functions. The CLI is an adapter, not a second implementation. See
architecture.
The opportunity scan compared local projects, public GitHub repositories, game-development discussions, hosted calculators, engine-specific assets, and current English/Chinese material. Existing open-source repositories mainly provide engine-specific inventory systems or weighted selection libraries; hosted tools emphasize visual table construction. LootLint is differentiated by a versioned cross-engine contract, a reference runtime, project-owned tail gates, paired regression, SARIF, and exact counterexample replay. Sources and rejection notes are in research.
- Specifications and reports stay on the local machine.
- The parser accepts only TOML/JSON and caps source size at 5 MiB.
- There is no expression evaluator, plug-in execution, network request, analytics, or remote asset.
- HTML reports escape source-controlled labels and embed no executable JavaScript.
- Counterexample receipts can reveal item IDs and table structure; review them before attaching to a public issue.
Report vulnerabilities privately using SECURITY.md.
Issues with a small, sanitized loot table and the expected selection semantics are especially
useful. Run uv run python scripts/verify.py before opening a pull request. The
contribution guide, code of conduct, and
roadmap describe the project boundaries.
LootLint is available under the MIT License.