1use crate::achievement::AchievementContext;
2use crate::bloop::Bloop;
3use crate::builder::{NoValue, Value};
4use crate::evaluator::{AwardMode, EvalResult, Evaluator};
5use std::collections::HashSet;
6use std::fmt;
7use std::fmt::Debug;
8use std::hash::Hash;
9use std::marker::PhantomData;
10use std::time::Duration;
11
12#[derive(Debug)]
14pub enum ExtractResult<V> {
15 Single(V),
16 Multiple(Vec<V>),
17 Abort,
18}
19
20#[derive(Debug, Default)]
22pub struct DistinctValuesEvaluatorBuilder<R, W> {
23 min_required: R,
24 max_window: W,
25 award_mode: AwardMode,
26}
27
28impl DistinctValuesEvaluatorBuilder<NoValue, NoValue> {
29 pub fn new() -> Self {
30 Self::default()
31 }
32}
33
34impl<W, R> DistinctValuesEvaluatorBuilder<R, W> {
35 pub fn award_all(self) -> Self {
37 Self {
38 award_mode: AwardMode::All,
39 ..self
40 }
41 }
42}
43
44impl<W> DistinctValuesEvaluatorBuilder<NoValue, W> {
45 pub fn min_required(
47 self,
48 min_required: usize,
49 ) -> DistinctValuesEvaluatorBuilder<Value<usize>, W> {
50 DistinctValuesEvaluatorBuilder {
51 min_required: Value(min_required),
52 max_window: self.max_window,
53 award_mode: self.award_mode,
54 }
55 }
56}
57
58impl<R> DistinctValuesEvaluatorBuilder<R, NoValue> {
59 pub fn max_window(
61 self,
62 max_window: Duration,
63 ) -> DistinctValuesEvaluatorBuilder<R, Value<Duration>> {
64 DistinctValuesEvaluatorBuilder {
65 min_required: self.min_required,
66 max_window: Value(max_window),
67 award_mode: self.award_mode,
68 }
69 }
70}
71
72impl DistinctValuesEvaluatorBuilder<Value<usize>, Value<Duration>> {
73 pub fn build<Player, State, Trigger, V, E>(
75 self,
76 extract: E,
77 ) -> impl Evaluator<Player, State, Trigger> + Debug
78 where
79 Player: 'static,
80 State: 'static,
81 Trigger: 'static,
82 E: Fn(&Player) -> ExtractResult<V> + Send + Sync + 'static,
83 V: Eq + Hash + 'static,
84 {
85 fn derive_ctx<Player, State, Trigger>(_ctx: &AchievementContext<Player, State, Trigger>) {}
86 let extract_wrapper = move |bloop: &Player, _: &()| extract(bloop);
87
88 DistinctValuesEvaluator {
89 min_required: self.min_required.0,
90 max_window: self.max_window.0,
91 award_mode: self.award_mode,
92 derive_ctx,
93 extract: extract_wrapper,
94 _marker: PhantomData,
95 }
96 }
97
98 pub fn build_with_derived_ctx<Player, State, Trigger, V, C, DC, E>(
103 self,
104 derive_ctx: DC,
105 extract: E,
106 ) -> impl Evaluator<Player, State, Trigger> + Debug
107 where
108 DC: Fn(&AchievementContext<Player, State, Trigger>) -> C + Send + Sync + 'static,
109 E: Fn(&Player, &C) -> ExtractResult<V> + Send + Sync + 'static,
110 V: Eq + Hash + 'static,
111 {
112 DistinctValuesEvaluator {
113 min_required: self.min_required.0,
114 max_window: self.max_window.0,
115 award_mode: self.award_mode,
116 derive_ctx,
117 extract,
118 _marker: PhantomData,
119 }
120 }
121}
122
123pub struct DistinctValuesEvaluator<Player, State, Trigger, V, C, DC, E>
128where
129 DC: Fn(&AchievementContext<Player, State, Trigger>) -> C + Send + Sync + 'static,
130 E: Fn(&Player, &C) -> ExtractResult<V> + Send + Sync + 'static,
131 V: Eq + Hash + 'static,
132{
133 min_required: usize,
134 max_window: Duration,
135 award_mode: AwardMode,
136 derive_ctx: DC,
137 extract: E,
138 _marker: PhantomData<(Player, State, Trigger, V)>,
139}
140
141impl<Player, State, Trigger, V, C, DC, E> Evaluator<Player, State, Trigger>
142 for DistinctValuesEvaluator<Player, State, Trigger, V, C, DC, E>
143where
144 DC: Fn(&AchievementContext<Player, State, Trigger>) -> C + Send + Sync + 'static,
145 E: Fn(&Player, &C) -> ExtractResult<V> + Send + Sync + 'static,
146 V: Eq + Hash + 'static,
147{
148 fn evaluate(&self, ctx: &AchievementContext<Player, State, Trigger>) -> impl Into<EvalResult> {
149 let derived_ctx = (self.derive_ctx)(ctx);
150 let mut seen_values = HashSet::with_capacity(self.min_required);
151 let mut player_ids = Vec::with_capacity(self.min_required + 1);
152 player_ids.push(ctx.current_bloop.player_id);
153
154 let bloops = ctx
155 .client_bloops()
156 .filter(ctx.filter_within_window(self.max_window))
157 .take(self.min_required);
158
159 for bloop in bloops {
160 if player_ids.contains(&bloop.player_id) {
161 return EvalResult::NoAward;
162 }
163
164 player_ids.push(bloop.player_id);
165
166 let extract_result = (self.extract)(&bloop.player(), &derived_ctx);
167
168 match extract_result {
169 ExtractResult::Single(value) => {
170 seen_values.insert(value);
171 }
172 ExtractResult::Multiple(values) => seen_values.extend(values),
173 ExtractResult::Abort => return EvalResult::NoAward,
174 };
175
176 if seen_values.len() >= self.min_required {
177 break;
178 }
179 }
180
181 if seen_values.len() < self.min_required {
182 return EvalResult::NoAward;
183 }
184
185 match self.award_mode {
186 AwardMode::Current => EvalResult::AwardSelf,
187 AwardMode::All => EvalResult::AwardMultiple(player_ids),
188 }
189 }
190}
191
192impl<Player, State, Trigger, V, C, DC, E> Debug
193 for DistinctValuesEvaluator<Player, State, Trigger, V, C, DC, E>
194where
195 DC: Fn(&AchievementContext<Player, State, Trigger>) -> C + Send + Sync + 'static,
196 E: Fn(&Player, &C) -> ExtractResult<V> + Send + Sync + 'static,
197 V: Eq + Hash + 'static,
198{
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 f.debug_struct("DistinctValuesEvaluator")
201 .field("derive_ctx", &"<closure>")
202 .field("extract", &"<closure>")
203 .field("min_required", &self.min_required)
204 .field("max_window", &self.max_window)
205 .field("award_mode", &self.award_mode)
206 .finish()
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::bloop::Bloop;
214 use crate::evaluator::{EvalResult, Evaluator};
215 use crate::test_utils::{MockPlayer, TestCtxBuilder};
216 use chrono::{DateTime, Utc};
217 use std::time::SystemTime;
218 use std::vec;
219
220 fn make_bloop(client_id: &str, name: &str, seconds_ago: u64) -> Bloop<MockPlayer> {
221 let now = SystemTime::now() - Duration::from_secs(seconds_ago);
222 let timestamp: DateTime<Utc> = now.into();
223
224 let (player, _) = MockPlayer::builder().name(name).build();
225
226 Bloop::new(player, client_id, timestamp)
227 }
228
229 #[test]
230 fn award_when_required_distinct_values_are_met() {
231 let evaluator = DistinctValuesEvaluatorBuilder::new()
232 .min_required(3)
233 .max_window(Duration::from_secs(60))
234 .build(|player: &MockPlayer| ExtractResult::Single(player.name.clone()));
235
236 let current = make_bloop("client1", "alice", 0);
237 let past = vec![
238 make_bloop("client1", "bob", 10),
239 make_bloop("client1", "carol", 20),
240 make_bloop("client1", "dave", 30),
241 ];
242
243 let mut ctx_builder = TestCtxBuilder::new(current).bloops(past);
244 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
245
246 assert_eq!(res, EvalResult::AwardSelf);
247 }
248
249 #[test]
250 fn award_all_mode_returns_all_player_ids() {
251 let evaluator = DistinctValuesEvaluatorBuilder::new()
252 .min_required(3)
253 .max_window(Duration::from_secs(60))
254 .award_all()
255 .build(|player: &MockPlayer| ExtractResult::Single(player.name.clone()));
256
257 let current = make_bloop("client1", "alice", 0);
258 let b1 = make_bloop("client1", "bob", 10);
259 let b2 = make_bloop("client1", "carol", 20);
260 let b3 = make_bloop("client1", "dave", 30);
261 let b4 = make_bloop("client1", "joe", 40);
262
263 let expected_ids = vec![current.player_id, b1.player_id, b2.player_id, b3.player_id];
264
265 let mut ctx_builder = TestCtxBuilder::new(current).bloops(vec![b1, b2, b3, b4]);
266 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
267
268 match res {
269 EvalResult::AwardMultiple(ids) => {
270 assert_eq!(ids, expected_ids);
271 }
272 other => panic!("expected AwardMultiple, got {:?}", other),
273 };
274 }
275
276 #[test]
277 fn abort_from_extractor_leads_to_no_award() {
278 let evaluator = DistinctValuesEvaluatorBuilder::new()
279 .min_required(1)
280 .max_window(Duration::from_secs(60))
281 .build(|_player: &MockPlayer| ExtractResult::<()>::Abort);
282
283 let current = make_bloop("client1", "alice", 0);
284 let b1 = make_bloop("client1", "bob", 10);
285
286 let mut ctx_builder = TestCtxBuilder::new(current).bloops(vec![b1]);
287 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
288
289 assert_eq!(res, EvalResult::NoAward);
290 }
291
292 #[test]
293 fn duplicate_player_in_recent_bloops_causes_no_award() {
294 let evaluator = DistinctValuesEvaluatorBuilder::new()
295 .min_required(2)
296 .max_window(Duration::from_secs(60))
297 .build(|player: &MockPlayer| ExtractResult::Single(player.name.clone()));
298
299 let current = make_bloop("client1", "alice", 0);
300
301 let b1 = make_bloop("client1", "bob", 10);
302 let mut b2 = make_bloop("client1", "carol", 10);
303 b2.player_id = b1.player_id;
304
305 let mut ctx_builder = TestCtxBuilder::new(current).bloops(vec![b1, b2]);
306 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
307
308 assert_eq!(res, EvalResult::NoAward);
309 }
310
311 #[test]
312 fn ignores_bloops_outside_time_window() {
313 let evaluator = DistinctValuesEvaluatorBuilder::new()
314 .min_required(2)
315 .max_window(Duration::from_secs(20))
316 .build(|player: &MockPlayer| ExtractResult::Single(player.name.clone()));
317
318 let current = make_bloop("client1", "alice", 0);
319 let b1 = make_bloop("client1", "bob", 10);
320 let b2 = make_bloop("client1", "carol", 30);
321
322 let mut ctx_builder = TestCtxBuilder::new(current).bloops(vec![b1, b2]);
323 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
324
325 assert_eq!(res, EvalResult::NoAward);
326 }
327
328 #[test]
329 fn build_with_derived_ctx_supplies_context_to_extractor() {
330 let evaluator = DistinctValuesEvaluatorBuilder::new()
331 .min_required(2)
332 .max_window(Duration::from_secs(60))
333 .build_with_derived_ctx(
334 |_ctx| vec!["bob", "carol"],
335 |player: &MockPlayer, allowed: &Vec<&str>| {
336 if allowed.contains(&player.name.as_str()) {
337 ExtractResult::Single(player.name.clone())
338 } else {
339 ExtractResult::Abort
340 }
341 },
342 );
343
344 let current = make_bloop("client1", "alice", 0);
345 let past = vec![
346 make_bloop("client1", "bob", 10),
347 make_bloop("client1", "carol", 20),
348 ];
349
350 let mut ctx_builder = TestCtxBuilder::new(current).bloops(past);
351 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
352
353 assert_eq!(res, EvalResult::AwardSelf);
354 }
355}