Skip to main content

wowlab_sentinel/strategy/factorial/
interactions.rs

1//! Pairwise interaction item selection.
2
3#![expect(
4    clippy::cast_possible_truncation,
5    reason = "factorial item indices are bounded by manifest slot sizes and serialized as u32"
6)]
7
8use super::types::{FactorialSlot, MainEffectEstimate};
9
10pub(crate) fn select_items_for_interaction(
11    slots: &[FactorialSlot],
12    main_effects: &[MainEffectEstimate],
13    top_k: usize,
14) -> Vec<Vec<u32>> {
15    let effect_lookup = |slot_index: u32, item_idx: u32| -> i32 {
16        main_effects
17            .iter()
18            .find(|me| me.slot_index == slot_index && me.item_idx == item_idx)
19            .map_or(0, |me| me.effect_dps_x10)
20    };
21
22    slots
23        .iter()
24        .map(|slot| {
25            let mut selected: Vec<u32> = Vec::new();
26
27            selected.push(0);
28
29            for (idx, item) in slot.items.iter().enumerate() {
30                if item.has_effect() && !selected.contains(&(idx as u32)) {
31                    selected.push(idx as u32);
32                }
33            }
34
35            let mut ranked: Vec<(u32, i32)> = (0..slot.items.len() as u32)
36                .filter(|idx| !selected.contains(idx))
37                .map(|idx| (idx, effect_lookup(slot.slot_index, idx)))
38                .collect();
39
40            ranked.sort_by_key(|&(_, effect)| std::cmp::Reverse(effect));
41
42            for (idx, _) in ranked {
43                if selected.len() >= top_k.max(1) {
44                    break;
45                }
46
47                selected.push(idx);
48            }
49
50            selected
51        })
52        .collect()
53}
54
55#[cfg(test)]
56mod tests {
57    use googletest::prelude::*;
58
59    use super::*;
60    use crate::strategy::factorial::types::FactorialItem;
61
62    fn slot_with(slot_index: u32, items: &[(u32, bool)]) -> FactorialSlot {
63        FactorialSlot {
64            slot_index,
65            items: items
66                .iter()
67                .map(|&(_item_id, has_effect)| FactorialItem::new(has_effect))
68                .collect(),
69        }
70    }
71
72    #[gtest]
73    fn effect_items_always_selected() -> Result<()> {
74        let slots = vec![slot_with(
75            0,
76            &[
77                (100, false),
78                (101, false),
79                (102, false),
80                (103, false),
81                (104, false),
82                (999, true),
83            ],
84        )];
85        let selected = select_items_for_interaction(&slots, &[], 2);
86
87        verify_true!(selected[0].contains(&5))?;
88        verify_true!(selected[0].contains(&0))?;
89
90        Ok(())
91    }
92
93    #[gtest]
94    fn top_k_limits_stat_sticks() -> Result<()> {
95        let slots = vec![slot_with(
96            0,
97            &[
98                (100, false),
99                (101, false),
100                (102, false),
101                (103, false),
102                (104, false),
103            ],
104        )];
105        let selected = select_items_for_interaction(&slots, &[], 3);
106
107        verify_eq!(selected[0].len(), 3)?;
108
109        Ok(())
110    }
111}