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(&Bloop<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: &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(&Bloop<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(&Bloop<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>
142 DistinctValuesEvaluator<Player, State, Trigger, V, C, DC, E>
143where
144 DC: Fn(&AchievementContext<Player, State, Trigger>) -> C + Send + Sync + 'static,
145 E: Fn(&Bloop<Player>, &C) -> ExtractResult<V> + Send + Sync + 'static,
146 V: Eq + Hash + 'static,
147{
148 pub fn builder() -> DistinctValuesEvaluatorBuilder<NoValue, NoValue> {
149 DistinctValuesEvaluatorBuilder::new()
150 }
151}
152
153impl<Player, State, Trigger, V, C, DC, E> Evaluator<Player, State, Trigger>
154 for DistinctValuesEvaluator<Player, State, Trigger, V, C, DC, E>
155where
156 DC: Fn(&AchievementContext<Player, State, Trigger>) -> C + Send + Sync + 'static,
157 E: Fn(&Bloop<Player>, &C) -> ExtractResult<V> + Send + Sync + 'static,
158 V: Eq + Hash + 'static,
159{
160 fn evaluate(&self, ctx: &AchievementContext<Player, State, Trigger>) -> impl Into<EvalResult> {
161 let derived_ctx = (self.derive_ctx)(ctx);
162 let mut seen_values = HashSet::with_capacity(self.min_required);
163 let mut player_ids = Vec::with_capacity(self.min_required + 1);
164 player_ids.push(ctx.current_bloop.player_id);
165
166 let bloops = ctx
167 .client_bloops()
168 .filter(ctx.filter_within_window(self.max_window))
169 .take(self.min_required);
170
171 for bloop in bloops {
172 if player_ids.contains(&bloop.player_id) {
173 return EvalResult::NoAward;
174 }
175
176 player_ids.push(bloop.player_id);
177
178 let extract_result = (self.extract)(bloop, &derived_ctx);
179
180 match extract_result {
181 ExtractResult::Single(value) => {
182 seen_values.insert(value);
183 }
184 ExtractResult::Multiple(values) => seen_values.extend(values),
185 ExtractResult::Abort => return EvalResult::NoAward,
186 };
187
188 if seen_values.len() >= self.min_required {
189 break;
190 }
191 }
192
193 if seen_values.len() < self.min_required {
194 return EvalResult::NoAward;
195 }
196
197 match self.award_mode {
198 AwardMode::Current => EvalResult::AwardSelf,
199 AwardMode::All => EvalResult::AwardMultiple(player_ids),
200 }
201 }
202}
203
204impl<Player, State, Trigger, V, C, DC, E> Debug
205 for DistinctValuesEvaluator<Player, State, Trigger, V, C, DC, E>
206where
207 DC: Fn(&AchievementContext<Player, State, Trigger>) -> C + Send + Sync + 'static,
208 E: Fn(&Bloop<Player>, &C) -> ExtractResult<V> + Send + Sync + 'static,
209 V: Eq + Hash + 'static,
210{
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 f.debug_struct("DistinctValuesEvaluator")
213 .field("derive_ctx", &"<closure>")
214 .field("extract", &"<closure>")
215 .field("min_required", &self.min_required)
216 .field("max_window", &self.max_window)
217 .field("award_mode", &self.award_mode)
218 .finish()
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::bloop::Bloop;
226 use crate::evaluator::{EvalResult, Evaluator};
227 use crate::test_utils::{MockPlayer, TestCtxBuilder};
228 use chrono::{DateTime, Utc};
229 use std::time::SystemTime;
230 use std::vec;
231
232 fn make_bloop(client_id: &str, name: &str, seconds_ago: u64) -> Bloop<MockPlayer> {
233 let now = SystemTime::now() - Duration::from_secs(seconds_ago);
234 let timestamp: DateTime<Utc> = now.into();
235
236 let (player, _) = MockPlayer::builder().name(name).build();
237
238 Bloop::new(player, client_id, timestamp)
239 }
240
241 #[test]
242 fn award_when_required_distinct_values_are_met() {
243 let evaluator = DistinctValuesEvaluatorBuilder::new()
244 .min_required(3)
245 .max_window(Duration::from_secs(60))
246 .build(|bloop: &Bloop<MockPlayer>| ExtractResult::Single(bloop.player().name.clone()));
247
248 let current = make_bloop("client1", "alice", 0);
249 let past = vec![
250 make_bloop("client1", "bob", 10),
251 make_bloop("client1", "carol", 20),
252 make_bloop("client1", "dave", 30),
253 ];
254
255 let mut ctx_builder = TestCtxBuilder::new(current).bloops(past);
256 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
257
258 assert_eq!(res, EvalResult::AwardSelf);
259 }
260
261 #[test]
262 fn award_all_mode_returns_all_player_ids() {
263 let evaluator = DistinctValuesEvaluatorBuilder::new()
264 .min_required(3)
265 .max_window(Duration::from_secs(60))
266 .award_all()
267 .build(|bloop: &Bloop<MockPlayer>| ExtractResult::Single(bloop.player().name.clone()));
268
269 let current = make_bloop("client1", "alice", 0);
270 let b1 = make_bloop("client1", "bob", 10);
271 let b2 = make_bloop("client1", "carol", 20);
272 let b3 = make_bloop("client1", "dave", 30);
273 let b4 = make_bloop("client1", "joe", 40);
274
275 let expected_ids = vec![current.player_id, b1.player_id, b2.player_id, b3.player_id];
276
277 let mut ctx_builder = TestCtxBuilder::new(current).bloops(vec![b1, b2, b3, b4]);
278 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
279
280 match res {
281 EvalResult::AwardMultiple(ids) => {
282 assert_eq!(ids, expected_ids);
283 }
284 other => panic!("expected AwardMultiple, got {:?}", other),
285 };
286 }
287
288 #[test]
289 fn abort_from_extractor_leads_to_no_award() {
290 let evaluator = DistinctValuesEvaluatorBuilder::new()
291 .min_required(1)
292 .max_window(Duration::from_secs(60))
293 .build(|_bloop: &Bloop<MockPlayer>| ExtractResult::<()>::Abort);
294
295 let current = make_bloop("client1", "alice", 0);
296 let b1 = make_bloop("client1", "bob", 10);
297
298 let mut ctx_builder = TestCtxBuilder::new(current).bloops(vec![b1]);
299 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
300
301 assert_eq!(res, EvalResult::NoAward);
302 }
303
304 #[test]
305 fn duplicate_player_in_recent_bloops_causes_no_award() {
306 let evaluator = DistinctValuesEvaluatorBuilder::new()
307 .min_required(2)
308 .max_window(Duration::from_secs(60))
309 .build(|bloop: &Bloop<MockPlayer>| ExtractResult::Single(bloop.player().name.clone()));
310
311 let current = make_bloop("client1", "alice", 0);
312
313 let b1 = make_bloop("client1", "bob", 10);
314 let mut b2 = make_bloop("client1", "carol", 10);
315 b2.player_id = b1.player_id;
316
317 let mut ctx_builder = TestCtxBuilder::new(current).bloops(vec![b1, b2]);
318 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
319
320 assert_eq!(res, EvalResult::NoAward);
321 }
322
323 #[test]
324 fn ignores_bloops_outside_time_window() {
325 let evaluator = DistinctValuesEvaluatorBuilder::new()
326 .min_required(2)
327 .max_window(Duration::from_secs(20))
328 .build(|bloop: &Bloop<MockPlayer>| ExtractResult::Single(bloop.player().name.clone()));
329
330 let current = make_bloop("client1", "alice", 0);
331 let b1 = make_bloop("client1", "bob", 10);
332 let b2 = make_bloop("client1", "carol", 30);
333
334 let mut ctx_builder = TestCtxBuilder::new(current).bloops(vec![b1, b2]);
335 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
336
337 assert_eq!(res, EvalResult::NoAward);
338 }
339
340 #[test]
341 fn build_with_derived_ctx_supplies_context_to_extractor() {
342 let evaluator = DistinctValuesEvaluatorBuilder::new()
343 .min_required(2)
344 .max_window(Duration::from_secs(60))
345 .build_with_derived_ctx(
346 |_ctx| vec!["bob", "carol"],
347 |bloop: &Bloop<MockPlayer>, allowed: &Vec<&str>| {
348 if allowed.contains(&bloop.player().name.as_str()) {
349 ExtractResult::Single(bloop.player().name.clone())
350 } else {
351 ExtractResult::Abort
352 }
353 },
354 );
355
356 let current = make_bloop("client1", "alice", 0);
357 let past = vec![
358 make_bloop("client1", "bob", 10),
359 make_bloop("client1", "carol", 20),
360 ];
361
362 let mut ctx_builder = TestCtxBuilder::new(current).bloops(past);
363 let res: EvalResult = evaluator.evaluate(&ctx_builder.build()).into();
364
365 assert_eq!(res, EvalResult::AwardSelf);
366 }
367}