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