Skip to content

Lottery

Try the interactive example ↓

Weighted random selection over named tables: items carry relative weights, draws pick with or without replacement, seeds make results reproducible, and consumable draws remove what they return. Built for loot tables, raffles, A/B variant picks, and weighted rollouts.

When to reach for it: loot drops, raffle winners, weighted A/B test variant assignment, "spin the wheel" mechanics, any place you need "pick N items where weight sets the odds".

When not to: probabilistic vault reveals (loot boxes with rarity tiers, pity timers, and inventory) belong to Vault; allocating items by competitive bidding belongs to Auction.

Concepts

  • Tables are created implicitly - adding the first item creates the table. There is no separate create endpoint; a name is valid if it is 1-64 characters of [A-Za-z0-9_-] (else 400 invalid_table_name).
  • Weights are relative - an item with weight 60 is six times as likely as weight 10. Weights do not need to sum to anything; info reports the computed percentage per item.
  • Draws replace by default - the same item can appear twice in one draw. unique forbids repeats within a single call (not across calls), and remove consumes drawn items from the table.
  • Seeds make draws deterministic - the same seed against the same table state reproduces the same result, for testing and replay.
  • Tables are shared - they are not per-user. A second user's token reads and draws from a table another user built.
  • add upserts - re-adding an existing item_id replaces its weight and returns the old one in previous_weight.

Build a table

snug lottery add -t loot-drops -i rare-sword -w 10 -m '{"rarity":"rare"}'
snug lottery add -t loot-drops -i common-potion -w 60
snug lottery add -t loot-drops -i scroll -w 30

snug lottery batch-add -t loot-drops -f items.json   # array of {item_id, weight, meta}
snug lottery info -t loot-drops                      # items, weights, percentages
snug lottery list                                    # tables, 25 per page

Weights must be in 1..=1000000 - weight 0 fails with 400 invalid_weight. Batch add applies items in order and stops at the first error.

Over HTTP, adding an item is a POST to /api/v1/lottery/{table_name}:

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"item_id": "gem", "weight": 5, "meta": {"rarity": "epic"}}' \
  http://localhost:4000/api/v1/lottery/loot-drops
{
  "status": 200,
  "msg": "OK",
  "data": { "item_id": "gem", "weight": 5, "previous_weight": null, "table_size": 4 }
}

Draws

snug --output json lottery draw -t loot-drops -c 2
{
  "items": [
    { "item_id": "scroll", "weight": 30, "meta": {} },
    { "item_id": "common-potion", "weight": 60, "meta": {} }
  ],
  "table_size": 3,
  "total_weight": 100
}

table_size and total_weight describe the table after the draw, which matters with --remove. Items added without metadata come back with an empty meta object.

Draw frequencies track the weights. 300 draws against weights 60/30/10/5 landed 176/81/28/15 - within two points of the expected 57/29/10/5 percent split.

snug lottery draw -t loot-drops -c 4 --unique     # no repeats in this call
snug lottery draw -t loot-drops -c 3 --seed 42    # deterministic
snug lottery draw -t loot-drops -c 1 --remove     # consume the drawn item
  • Unique draws of count N need at least N items - asking for 5 from a 4-item table fails with 400 insufficient_items ("Requested 5 unique items but only 4 available").
  • Seeded draws were verified identical across repeated calls with the same seed, and different under another seed. A seed pins the randomness, not the table - it replays only against the same table state.
  • Consumable draws remove what they return. Asking for more than remains is not an error: the draw returns everything left and empties the table. Count is capped at 100 per draw by default.

Drawing from a table with no items - never created, or emptied by --remove - fails with 400 empty_table (not 404).

Adjusting weights

adjust applies a signed delta atomically, clamped to [--min-weight, --max-weight] (defaults 1 and 1000000):

snug lottery adjust -t loot-drops -i gem --increment 495     # 5 -> 500
snug lottery adjust -t loot-drops -i gem --increment=-10000  # clamps to 1

The response carries previous_weight and new_weight. Raising one item from 5 to 500 moved it from 5% of draws to 83% in a 200-draw sample. Adjusting an item that is not in the table fails with 404 item_not_found. Note the --increment=-N form - a bare -10000 after a space is parsed as a flag.

Deleting

snug lottery delete-item -t loot-drops -i scroll --force
snug lottery delete-table -t loot-drops --force

Both prompt for confirmation unless --force is given (required in JSON and quiet modes). Deleting an absent item is 200 with removed: false over HTTP; the CLI treats it as a failure and exits non-zero. Deleting a table returns items_removed; deleting it again is 404 table_not_found.

One asymmetry to know: a table emptied by consumable draws or delete-item is 404 table_not_found on info, but still appears in list with size: 0 until you delete-table it.

Finding tables

GET /api/v1/lottery speaks the standard list contract: q matches table name and description, filters apply to item_count, total_weight, created_at, and updated_at, and sort accepts created_at (newest first by default), item_count, or total_weight. For example, ?filter[item_count][gte]=1 returns only non-empty tables:

snug lottery list --filter 'item_count[gte]=1' --sort -total_weight

Limits and configuration

Up to 10000 items per table, 100 items per draw, and 500 items per batch add by default - tunable, along with the maximum item weight, via the LOTTERY_* variables.

Reference

  • Lottery API - every endpoint, callable
  • Related: Vault for loot boxes with rarity tiers and inventory, Auction for bidding-based allocation, Remote Config for rule-based (non-random) feature targeting

Interactive example

Choose from a weighted pool with a repeatable seed. Use draws for rewards or selections, with options to consume entries as they are picked.

An illustrative example that runs in your browser. No account or API calls required.