cloned/luckybox
Laravel package for adding lucky-box and mystery-draw mechanics to your app: define prize pools, weights, and stock, then draw winners with configurable odds. Simple setup and helpers for running giveaways, promotions, and randomized rewards.
LuckyBox is a single-purpose library: give it items with weights, get one back with matching probability. Treat it as a ~50-line utility class, not a framework — there is no service provider, facade, or config file to publish, so integration is entirely on you.
composer require cloned/luckybox
Look at these in order:
README.md — the entire public API fits in a handful of lines.src/ — the package is small enough to read end-to-end before you use it. Confirm the exact method names (add/draw/get/lottery vary across versions) and the return type (item vs. key vs. index).composer.json — check the PHP constraint and the last release date. With ~12 stars and a low activity score, pin the version in composer.json and don't expect upstream fixes.First use case — a weighted pick:
use Cloned\LuckyBox\LuckyBox;
$box = new LuckyBox();
$box->add('common', 70);
$box->add('rare', 25);
$box->add('legendary', 5);
$prize = $box->draw(); // verify method name against the source
If a two-minute read of src/ shows the implementation is mt_rand(1, array_sum($weights)) plus a linear scan, that is worth knowing up front — it should influence whether you depend on the package at all versus inlining the fifteen lines.
Wrap it behind a domain service. Never let LuckyBox leak into controllers or models. One class, one responsibility, and a seam you can swap if the package is abandoned:
namespace App\Services;
use Cloned\LuckyBox\LuckyBox;
class LootTable
{
public function __construct(private array $weights) {}
public function pick(): string
{
$box = new LuckyBox();
foreach ($this->weights as $item => $weight) {
if ($weight > 0) {
$box->add($item, $weight);
}
}
$winner = $box->draw();
event(new PrizeAwarded($winner)); // or Log::info(), metrics, etc.
return $winner;
}
}
Drive weights from config, not code. Loot tables and rollout percentages change far more often than the code around them:
// config/loot.php
return [
'chest_common' => ['gold' => 60, 'gem' => 30, 'skin' => 10],
];
Bind it in a provider so tests can override the table:
$this->app->bind('loot.common', fn () => new LootTable(config('loot.chest_common')));
Rebuild the box when weights change. The mutable add() API makes it tempting to keep one long-lived instance in a singleton and keep appending to it. Don't — weights accumulate, and a singleton shared across a request can drift. Construct per draw, or make the wrapper immutable and construct internally on each pick().
Use it for bucketed rollouts (A/B tests, canary features, queue prioritisation) where the "items" are variant names and the weights are traffic splits. For request-scoped bucketing you generally want deterministic assignment per user — crc32($userId) % 100 — not a fresh random draw. LuckyBox is the wrong tool there; note the distinction before you reach for it.
Test statistically, not by exact value. Unless the library exposes a seedable RNG (most don't), you cannot assert draw() === 'rare'. Assert distribution instead, with a tolerance wide enough to avoid flakiness:
$counts = ['a' => 0, 'b' => 0];
for ($i = 0; $i < 10000; $i++) {
$counts[$this->table->pick()]++;
}
$this->assertEqualsWithDelta(0.25, $counts['a'] / 10000, 0.03);
Keep N high and the delta generous; a 2% tolerance on 1,000 runs will flake in CI. For unit tests of the calling code, bind a fake LootTable that returns a fixed value and skip the randomness entirely.
[70, 25, 5] and [7, 2.5, 0.5] behave identically. Don't build logic that assumes they sum to 100 — normalise only for display.int or uses mt_rand (integer-only). Passing 0.5 may silently truncate to 0 and make the item unselectable. Stick to integers unless the source clearly handles floats.add(). Negative weights corrupt array_sum and can make the range invalid, yielding an exception or an undefined index depending on the release.false/null. Always provide a fallback item rather than letting a prize wheel 500 for a user.mt_rand is not acceptable. Use random_int() yourself or verify the library uses it. Disclose the mechanism if it's user-facing.array_unique, that skews the distribution.How can I help you explore Laravel packages today?