weightedRandomPick<T> function

T weightedRandomPick<T>(
  1. List<T> items,
  2. List<double> weights, {
  3. Random? random,
})

Picks 1 item from items with probability proportional to the matching entry in weights — the shared loot-table/gacha-reward primitive so each new reward system (achievements, offline earnings, …) doesn't reimplement this. Pass a seeded random in tests for determinism.

Does NOT support sampling-without-replacement (drawing multiple items that can't repeat) — every call is independent.

Throws ArgumentError (BUG-21 — a real runtime check, not just an assert, so a misconfigured loot table still fails loudly in a release build instead of silently and deterministically always returning items.last) for: mismatched lengths, an empty items, any weight that's negative or not finite (NaN/Infinity), or a total that isn't finite and strictly positive.

Implementation

T weightedRandomPick<T>(List<T> items, List<double> weights, {Random? random}) {
  if (items.length != weights.length) {
    throw ArgumentError(
      'weightedRandomPick: items (${items.length}) and weights '
      '(${weights.length}) must be the same length',
    );
  }
  if (items.isEmpty) {
    throw ArgumentError('weightedRandomPick: items must not be empty');
  }
  for (final w in weights) {
    if (!w.isFinite || w < 0) {
      throw ArgumentError(
        'weightedRandomPick: every weight must be finite and >= 0, got $w',
      );
    }
  }

  final total = weights.fold<double>(0, (sum, w) => sum + w);
  if (!total.isFinite || total <= 0) {
    throw ArgumentError(
      'weightedRandomPick: weights must sum to a finite value > 0, got $total',
    );
  }
  final target = (random ?? Random()).nextDouble() * total;

  var cumulative = 0.0;
  for (var i = 0; i < items.length; i++) {
    cumulative += weights[i];
    if (target < cumulative) return items[i];
  }
  return items.last; // floating-point rounding safety net
}