Skip to main content

git_koi/
behavior.rs

1//! The day's movement, chosen from recent activity by one ordered set of rules.
2
3use crate::koi::{KoiDevelopment, RecentActivity};
4
5/// One movement per run.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum Behavior {
8    /// Sits low in the water, blue-tinted, barely moving.
9    DeepRest,
10    /// Hovers in place a little deeper, with a slow beat and glide.
11    Rest,
12    /// Hovers at the surface and gulps.
13    SurfaceFeeding,
14    /// Two fast laps of strong bursts and glides.
15    Dart,
16    /// One slow lap; the everyday swim.
17    Cruise,
18}
19
20const DEEP_REST_QUIET_DAYS: u32 = 60;
21const REST_QUIET_DAYS: u32 = 14;
22/// Totals whose crossing earns a week of darting.
23const DART_MILESTONES: [u64; 6] = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000];
24/// A busy week is compared with at most this many of the latest complete weeks.
25const DART_COMPARED_WEEKS: usize = 52;
26/// A top tenth means little with fewer complete weeks than this.
27const DART_MIN_COMPLETE_WEEKS: usize = 10;
28/// A busy week is matched or beaten by at most one in this many compared weeks.
29const DART_WEEK_SHARE_DENOMINATOR: usize = 10;
30const DAYS_PER_WEEK: usize = 7;
31
32/// Choose the movement for the day after the observation date. A nightly run judges
33/// the day that just ended, and the koi shows the result all of the next day. The
34/// first matching rule wins:
35///
36/// | Behavior | When the observation date |
37/// | --- | --- |
38/// | Deep rest | ends DEEP_REST_QUIET_DAYS or more quiet days |
39/// | Rest | ends REST_QUIET_DAYS or more quiet days |
40/// | Surface feeding | is the first day with contributions in its Monday-Sunday week |
41/// | Dart | falls within seven days of the total crossing a milestone |
42/// | Dart | is a Sunday that finishes a busy week |
43/// | Cruise | is anything else |
44///
45/// A finished week is busy when at most one in ten of the latest
46/// DART_COMPARED_WEEKS complete weeks before it match or beat its total, and at
47/// least DART_MIN_COMPLETE_WEEKS weeks are compared. So a steady account cruises,
48/// and the koi darts at most one day a week beyond milestones. This reads no clock
49/// and changes no growth, colors, or marks.
50pub fn select_behavior(koi_development: &KoiDevelopment) -> Behavior {
51    let recent_activity = &koi_development.recent_activity;
52    if recent_activity.quiet_day_count >= DEEP_REST_QUIET_DAYS {
53        Behavior::DeepRest
54    } else if recent_activity.quiet_day_count >= REST_QUIET_DAYS {
55        Behavior::Rest
56    } else if is_first_contribution_of_week(recent_activity) {
57        Behavior::SurfaceFeeding
58    } else if has_crossed_milestone(koi_development.total_contributions, recent_activity)
59        || is_finished_busy_week(recent_activity)
60    {
61        Behavior::Dart
62    } else {
63        Behavior::Cruise
64    }
65}
66
67fn is_first_contribution_of_week(recent_activity: &RecentActivity) -> bool {
68    matches!(
69        recent_activity.observation_week_contributions.split_last(),
70        Some((&observed_day, earlier_days))
71            if observed_day > 0 && earlier_days.iter().all(|&day_contributions| day_contributions == 0)
72    )
73}
74
75fn has_crossed_milestone(total_contributions: u64, recent_activity: &RecentActivity) -> bool {
76    let earlier_total = total_contributions.saturating_sub(recent_activity.recent_contributions);
77    DART_MILESTONES
78        .iter()
79        .any(|&milestone| earlier_total < milestone && milestone <= total_contributions)
80}
81
82fn is_finished_busy_week(recent_activity: &RecentActivity) -> bool {
83    let finished_week = &recent_activity.observation_week_contributions;
84    let complete_weeks = &recent_activity.complete_week_contributions;
85    let compared_weeks =
86        &complete_weeks[complete_weeks.len().saturating_sub(DART_COMPARED_WEEKS)..];
87    let week_total: u64 = finished_week.iter().sum();
88    let matching_week_count = compared_weeks
89        .iter()
90        .filter(|&&week_contributions| week_contributions >= week_total)
91        .count();
92    finished_week.len() == DAYS_PER_WEEK
93        && compared_weeks.len() >= DART_MIN_COMPLETE_WEEKS
94        && matching_week_count * DART_WEEK_SHARE_DENOMINATOR <= compared_weeks.len()
95}