1use crate::achievement::AchievementContext;
2use crate::evaluator::{EvalResult, Evaluator};
3use cached::proc_macro::cached;
4use num_integer::Roots;
5use std::fmt;
6use std::fmt::Debug;
7use std::time::Duration;
8
9pub trait RegistrationNumberProvider {
11 fn registration_number(&self) -> usize;
12}
13
14pub fn is_prime(number: usize) -> bool {
16 internal_is_prime(number)
17}
18
19#[cached(size = 10_000)]
20#[inline]
21fn internal_is_prime(number: usize) -> bool {
22 if number == 2 || number == 3 {
23 return true;
24 }
25
26 if number <= 1 || number.is_multiple_of(2) || number.is_multiple_of(3) {
27 return false;
28 }
29
30 let mut i = 5;
31
32 while i * i <= number {
33 if number.is_multiple_of(i) || number.is_multiple_of(i + 2) {
34 return false;
35 }
36
37 i += 6;
38 }
39
40 true
41}
42
43pub fn is_perfect_square(number: usize) -> bool {
45 internal_is_perfect_square(number)
46}
47
48#[cached(size = 10_000)]
49#[inline]
50fn internal_is_perfect_square(number: usize) -> bool {
51 number.sqrt().pow(2) == number
52}
53
54pub fn is_fibonacci(number: usize) -> bool {
56 internal_is_fibonacci(number)
57}
58
59#[cached(size = 10_000)]
60#[inline]
61fn internal_is_fibonacci(number: usize) -> bool {
62 is_perfect_square(5 * number * number + 4) || is_perfect_square(5 * number * number - 4)
63}
64
65pub struct NumberPredicateEvaluator<F>
70where
71 F: Fn(usize) -> bool + Send + Sync + 'static,
72{
73 predicate: F,
75
76 min_required: usize,
78
79 max_window: Duration,
81}
82
83impl<F> NumberPredicateEvaluator<F>
84where
85 F: Fn(usize) -> bool + Send + Sync + 'static,
86{
87 pub fn new(predicate: F, min_required: usize, max_window: Duration) -> Self {
101 Self {
102 predicate,
103 min_required,
104 max_window,
105 }
106 }
107}
108
109impl<Player, Metadata, Trigger, F> Evaluator<Player, Metadata, Trigger>
110 for NumberPredicateEvaluator<F>
111where
112 Player: RegistrationNumberProvider,
113 F: Fn(usize) -> bool + Send + Sync + 'static,
114{
115 fn evaluate(
116 &self,
117 ctx: &AchievementContext<Player, Metadata, Trigger>,
118 ) -> impl Into<EvalResult> {
119 let bloops = ctx
120 .client_bloops()
121 .filter(ctx.filter_within_window(self.max_window))
122 .take(self.min_required)
123 .collect::<Vec<_>>();
124
125 bloops
126 .iter()
127 .all(|bloop| (self.predicate)(bloop.player().registration_number()))
128 && bloops.len() >= self.min_required
129 }
130}
131
132impl<F> Debug for NumberPredicateEvaluator<F>
133where
134 F: Fn(usize) -> bool + Send + Sync + 'static,
135{
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.debug_struct("NumberPropertyEvaluator")
138 .field("predicate", &"<closure>")
139 .field("min_required", &self.min_required)
140 .field("max_window", &self.max_window)
141 .finish()
142 }
143}
144
145pub fn digit_sum(number: usize) -> usize {
146 let mut sum = 0;
147 let mut value = number;
148
149 while value > 0 {
150 sum += value % 10;
151 value /= 10;
152 }
153
154 sum
155}
156
157pub struct ProjectionMatchEvaluator<F, V>
178where
179 F: Fn(usize) -> V + Send + Sync + 'static,
180 V: PartialEq,
181{
182 projector: F,
184
185 min_required: usize,
187
188 max_window: Duration,
191}
192
193impl<F, V> ProjectionMatchEvaluator<F, V>
194where
195 F: Fn(usize) -> V + Send + Sync + 'static,
196 V: PartialEq,
197{
198 pub fn new(projector: F, min_required: usize, max_window: Duration) -> Self {
234 Self {
235 projector,
236 min_required,
237 max_window,
238 }
239 }
240}
241
242impl<F, V> Debug for ProjectionMatchEvaluator<F, V>
243where
244 F: Fn(usize) -> V + Send + Sync + 'static,
245 V: PartialEq,
246{
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 f.debug_struct("ProjectionMatchEvaluator")
249 .field("projector", &"<closure>")
250 .field("min_required", &self.min_required)
251 .field("max_window", &self.max_window)
252 .finish()
253 }
254}
255
256impl<Player, Metadata, Trigger, F, V> Evaluator<Player, Metadata, Trigger>
257 for ProjectionMatchEvaluator<F, V>
258where
259 Player: RegistrationNumberProvider,
260 F: Fn(usize) -> V + Send + Sync + 'static,
261 V: PartialEq,
262{
263 fn evaluate(
264 &self,
265 ctx: &AchievementContext<Player, Metadata, Trigger>,
266 ) -> impl Into<EvalResult> {
267 let reference = (self.projector)(ctx.current_bloop.player().registration_number());
268
269 let bloops = ctx
270 .client_bloops()
271 .filter(ctx.filter_within_window(self.max_window))
272 .take(self.min_required)
273 .collect::<Vec<_>>();
274
275 bloops
276 .iter()
277 .all(|bloop| (self.projector)(bloop.player().registration_number()) == reference)
278 && bloops.len() >= self.min_required
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::bloop::Bloop;
286 use crate::evaluator::{EvalResult, Evaluator};
287 use crate::test_utils::{MockPlayer, TestCtxBuilder};
288 use chrono::{DateTime, Utc};
289 use std::time::{Duration, SystemTime};
290
291 fn make_bloop(number: usize, seconds_ago: u64) -> Bloop<MockPlayer> {
292 let now = SystemTime::now() - Duration::from_secs(seconds_ago);
293 let timestamp: DateTime<Utc> = now.into();
294
295 let (player, _) = MockPlayer::builder().registration_number(number).build();
296 Bloop::new(player, "test", timestamp)
297 }
298
299 #[test]
300 fn predicate_eval_returns_true_when_enough_bloops_match_predicate() {
301 let evaluator = NumberPredicateEvaluator::new(|n| n % 2 == 0, 3, Duration::from_secs(60));
302
303 let current = make_bloop(8, 0);
304 let past = vec![
305 make_bloop(2, 10),
306 make_bloop(4, 20),
307 make_bloop(6, 30),
308 make_bloop(3, 40),
309 ];
310
311 let mut builder = TestCtxBuilder::new(current).bloops(past);
312 assert_eq!(
313 evaluator.evaluate(&builder.build()).into(),
314 EvalResult::AwardSelf
315 );
316 }
317
318 #[test]
319 fn predicate_eval_returns_false_when_not_enough_bloops_match_predicate() {
320 let evaluator = NumberPredicateEvaluator::new(|n| n % 2 == 0, 3, Duration::from_secs(60));
321
322 let current = make_bloop(1, 0);
323 let past = vec![make_bloop(2, 10), make_bloop(3, 20), make_bloop(5, 30)];
324
325 let mut builder = TestCtxBuilder::new(current).bloops(past);
326 assert_eq!(
327 evaluator.evaluate(&builder.build()).into(),
328 EvalResult::NoAward
329 );
330 }
331
332 #[test]
333 fn predicate_eval_ignores_bloops_outside_time_window() {
334 let evaluator = NumberPredicateEvaluator::new(|n| n % 2 == 0, 2, Duration::from_secs(20));
335
336 let current = make_bloop(8, 0);
337 let past = vec![make_bloop(2, 10), make_bloop(4, 25), make_bloop(6, 30)];
338
339 let mut builder = TestCtxBuilder::new(current).bloops(past);
340 assert_eq!(
341 evaluator.evaluate(&builder.build()).into(),
342 EvalResult::NoAward
343 );
344 }
345
346 #[test]
347 fn predicate_eval_takes_only_min_required_recent_matches() {
348 let evaluator = NumberPredicateEvaluator::new(|n| n < 10, 2, Duration::from_secs(60));
349
350 let current = make_bloop(1, 0);
351 let past = vec![make_bloop(9, 10), make_bloop(8, 15), make_bloop(100, 20)];
352
353 let mut builder = TestCtxBuilder::new(current).bloops(past);
354 assert_eq!(
355 evaluator.evaluate(&builder.build()).into(),
356 EvalResult::AwardSelf
357 );
358 }
359
360 #[test]
361 fn projection_eval_returns_true_when_enough_bloops_match_projection() {
362 let evaluator = ProjectionMatchEvaluator::new(|n| n % 2, 3, Duration::from_secs(60));
363
364 let current = make_bloop(4, 0);
365 let past = vec![
366 make_bloop(2, 10),
367 make_bloop(6, 20),
368 make_bloop(8, 30),
369 make_bloop(3, 40),
370 ];
371
372 let mut builder = TestCtxBuilder::new(current).bloops(past);
373 assert_eq!(
374 evaluator.evaluate(&builder.build()).into(),
375 EvalResult::AwardSelf
376 );
377 }
378
379 #[test]
380 fn projection_eval_returns_false_when_not_enough_bloops_match_projection() {
381 let evaluator = ProjectionMatchEvaluator::new(|n| n % 2, 4, Duration::from_secs(60));
382
383 let current = make_bloop(4, 0);
384 let past = vec![make_bloop(2, 10), make_bloop(6, 20), make_bloop(3, 30)];
385
386 let mut builder = TestCtxBuilder::new(current).bloops(past);
387 assert_eq!(
388 evaluator.evaluate(&builder.build()).into(),
389 EvalResult::NoAward
390 );
391 }
392
393 #[test]
394 fn projection_eval_ignores_bloops_outside_time_window() {
395 let evaluator = ProjectionMatchEvaluator::new(|n| n % 2, 2, Duration::from_secs(20));
396
397 let current = make_bloop(4, 0);
398 let past = vec![make_bloop(2, 10), make_bloop(6, 25), make_bloop(8, 30)];
399
400 let mut builder = TestCtxBuilder::new(current).bloops(past);
401 assert_eq!(
402 evaluator.evaluate(&builder.build()).into(),
403 EvalResult::NoAward
404 );
405 }
406
407 #[test]
408 fn projection_eval_takes_only_min_required_recent_matches() {
409 let evaluator = ProjectionMatchEvaluator::new(|n| n, 2, Duration::from_secs(60));
410
411 let current = make_bloop(4, 0);
412 let past = vec![make_bloop(4, 10), make_bloop(4, 15), make_bloop(4, 20)];
413
414 let mut builder = TestCtxBuilder::new(current).bloops(past);
415 assert_eq!(
416 evaluator.evaluate(&builder.build()).into(),
417 EvalResult::AwardSelf
418 );
419 }
420
421 #[test]
422 fn returns_true_for_perfect_squares() {
423 for n in [
424 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 1024, 4096, 10000,
425 ] {
426 assert!(
427 internal_is_perfect_square_no_cache(n),
428 "{n} should be a perfect square"
429 );
430 }
431 }
432
433 #[test]
434 fn returns_false_for_non_squares() {
435 for n in [
436 2, 3, 5, 6, 8, 10, 26, 50, 63, 65, 99, 101, 123, 10001, 12345,
437 ] {
438 assert!(
439 !internal_is_perfect_square_no_cache(n),
440 "{n} should not be a perfect square"
441 );
442 }
443 }
444
445 #[test]
446 fn returns_true_for_primes() {
447 for n in [
448 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 101, 997,
449 ] {
450 assert!(internal_is_prime_no_cache(n), "{n} should be prime");
451 }
452 }
453
454 #[test]
455 fn returns_false_for_non_primes() {
456 for n in [
457 0, 1, 4, 6, 8, 9, 10, 12, 15, 21, 25, 27, 100, 1024, 4096, 10000,
458 ] {
459 assert!(!internal_is_prime_no_cache(n), "{n} should not be prime");
460 }
461 }
462
463 #[test]
464 fn returns_true_for_fibonacci_numbers() {
465 for n in [
466 0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597,
467 ] {
468 assert!(
469 internal_is_fibonacci_no_cache(n),
470 "{n} should be a Fibonacci number"
471 );
472 }
473 }
474
475 #[test]
476 fn returns_false_for_non_fibonacci_numbers() {
477 for n in [
478 4, 6, 7, 9, 10, 11, 12, 14, 15, 22, 100, 200, 1000, 1234, 2024,
479 ] {
480 assert!(
481 !internal_is_fibonacci_no_cache(n),
482 "{n} should not be a Fibonacci number"
483 );
484 }
485 }
486}