steel_utils/random/
weighted_random.rs1use crate::random::Random;
4
5#[must_use]
10pub fn get_total_weight<T>(items: &[T], weight: impl Fn(&T) -> i32) -> i32 {
11 let total: i64 = items.iter().map(|item| i64::from(weight(item))).sum();
12 assert!(
13 total <= i64::from(i32::MAX),
14 "Sum of weights must be <= 2147483647"
15 );
16 total as i32
17}
18
19pub fn get_random_item<'a, T, R: Random>(
25 random: &mut R,
26 items: &'a [T],
27 weight: impl Fn(&T) -> i32,
28) -> Option<&'a T> {
29 let total_weight = get_total_weight(items, &weight);
30 assert!(total_weight >= 0, "Negative total weight in getRandomItem");
31 if total_weight == 0 {
32 return None;
33 }
34 let selection = random.next_i32_bounded(total_weight);
35 get_weighted_item(items, selection, weight)
36}
37
38#[must_use]
40pub fn get_weighted_item<T>(items: &[T], mut index: i32, weight: impl Fn(&T) -> i32) -> Option<&T> {
41 items.iter().find(|item| {
42 index -= weight(item);
43 index < 0
44 })
45}
46
47#[cfg(test)]
48mod tests {
49 use crate::random::legacy_random::LegacyRandom;
50
51 use super::{get_random_item, get_total_weight, get_weighted_item};
52
53 const ITEMS: [(&str, i32); 3] = [("common", 3), ("rare", 1), ("never", 0)];
54
55 fn weight(item: &(&str, i32)) -> i32 {
56 item.1
57 }
58
59 #[test]
60 fn total_weight_sums_every_entry() {
61 assert_eq!(get_total_weight(&ITEMS, weight), 4);
62 assert_eq!(get_total_weight::<(&str, i32)>(&[], weight), 0);
63 }
64
65 #[test]
66 fn weighted_item_walks_cumulative_weights() {
67 for index in 0..3 {
68 assert_eq!(get_weighted_item(&ITEMS, index, weight), Some(&ITEMS[0]));
69 }
70 assert_eq!(get_weighted_item(&ITEMS, 3, weight), Some(&ITEMS[1]));
71 assert_eq!(get_weighted_item(&ITEMS, 4, weight), None);
72 }
73
74 #[test]
75 fn zero_total_weight_yields_nothing_without_consuming_the_random() {
76 let mut random = LegacyRandom::from_seed(5);
77 let before = random.get_seed();
78 let empty: [(&str, i32); 1] = [("never", 0)];
79 assert_eq!(get_random_item(&mut random, &empty, weight), None);
80 assert_eq!(random.get_seed(), before);
81 }
82
83 #[test]
84 fn random_item_is_deterministic_for_a_seed() {
85 let first = get_random_item(&mut LegacyRandom::from_seed(1234), &ITEMS, weight);
86 let second = get_random_item(&mut LegacyRandom::from_seed(1234), &ITEMS, weight);
87 assert!(first.is_some());
88 assert_eq!(first, second);
89 }
90}