Skip to main content
Verticopolis engineGitHub

verticopolis_engine/
clock.rs

1//! Game time: the [`Clock`] on [`Simulation::clock`](crate::sim::Simulation::clock),
2//! the [`GameMode`] a game is started in, and the two calendars.
3//!
4//! A game day is 1,440 game minutes. The canon calendar is the 1994
5//! original's short one (a 3-day week with a 1-day weekend, 3-day quarters,
6//! 12-day years); the real-world calendar has 7-day weeks, 90-day quarters
7//! and 360-day years. Classic games always run canon.
8
9/// Which calendar a game runs on.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum CalendarKind {
12    /// The 1994 original's short calendar ([`CANON`]).
13    Canon,
14    /// Weeks, quarters and years of real length ([`REAL_WORLD`]).
15    RealWorld,
16}
17
18impl CalendarKind {
19    /// The calendar a save or a host names, by its saved spelling.
20    pub fn parse(s: &str) -> Option<CalendarKind> {
21        match s {
22            "canon" => Some(CalendarKind::Canon),
23            "realWorld" => Some(CalendarKind::RealWorld),
24            _ => None,
25        }
26    }
27
28    /// The calendar's saved spelling: `canon` or `realWorld`.
29    pub fn as_str(self) -> &'static str {
30        match self {
31            CalendarKind::Canon => "canon",
32            CalendarKind::RealWorld => "realWorld",
33        }
34    }
35}
36
37/// The lengths a calendar gives the week, the quarter and the year, in days.
38#[derive(Clone, Copy, Debug)]
39pub struct Calendar {
40    /// Which calendar this is.
41    pub kind: CalendarKind,
42    /// Days in a week, the weekend included.
43    pub week_days: i64,
44    /// Days at the end of each week that are the weekend.
45    pub weekend_days: i64,
46    /// Days in a quarter, when rent is collected.
47    pub quarter_days: i64,
48    /// Days in a year.
49    pub year_days: i64,
50    /// Days between maintenance bills.
51    pub maint_period_days: i64,
52}
53
54/// The 1994 original's calendar, which Classic always runs.
55pub const CANON: Calendar = Calendar {
56    kind: CalendarKind::Canon,
57    week_days: 3,
58    weekend_days: 1,
59    quarter_days: 3,
60    year_days: 12,
61    maint_period_days: 3,
62};
63
64/// The real-world calendar a Modern game may choose.
65pub const REAL_WORLD: Calendar = Calendar {
66    kind: CalendarKind::RealWorld,
67    week_days: 7,
68    weekend_days: 2,
69    quarter_days: 90,
70    year_days: 360,
71    maint_period_days: 30,
72};
73
74/// The two ways to play, chosen when a game starts.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum GameMode {
77    /// The 1994 original's rules, prices and limits.
78    Classic,
79    /// The original with quality-of-life changes and the calendar and
80    /// bridging options. [`crate::catalog::catalog`] lists every difference
81    /// a frontend shows.
82    Modern,
83}
84
85impl GameMode {
86    /// The mode's saved spelling: `classic` or `modern`.
87    pub fn as_str(self) -> &'static str {
88        match self {
89            GameMode::Classic => "classic",
90            GameMode::Modern => "modern",
91        }
92    }
93
94    /// The mode a scenario or a host names, by its saved spelling.
95    pub fn parse(s: &str) -> Option<GameMode> {
96        match s {
97            "classic" => Some(GameMode::Classic),
98            "modern" => Some(GameMode::Modern),
99            _ => None,
100        }
101    }
102}
103
104/// The calendar a game in `mode` runs: Classic always runs canon, and
105/// Modern runs the one the player chose.
106pub fn resolve_calendar(mode: GameMode, modern_calendar: CalendarKind) -> Calendar {
107    if mode == GameMode::Classic {
108        return CANON;
109    }
110    match modern_calendar {
111        CalendarKind::Canon => CANON,
112        CalendarKind::RealWorld => REAL_WORLD,
113    }
114}
115
116/// The game clock.
117#[derive(Clone, Debug)]
118pub struct Clock {
119    /// Game minutes since 00:00 on day 0. It can be fractional.
120    pub minutes: f64,
121    /// The calendar the game runs on.
122    pub calendar: Calendar,
123}
124
125impl Clock {
126    // `new Clock(minutes, calendar)`: a literal 0 starts at 07:00 on day 0.
127    #[doc(hidden)]
128    pub fn new(minutes: f64, calendar: Calendar) -> Clock {
129        Clock {
130            minutes: if minutes == 0.0 { 7.0 * 60.0 } else { minutes },
131            calendar,
132        }
133    }
134
135    #[doc(hidden)]
136    pub fn advance(&mut self, min: f64) {
137        self.minutes += min;
138    }
139
140    /// Minutes since midnight, 0 up to 1,440.
141    pub fn minute_of_day(&self) -> f64 {
142        ((self.minutes % 1440.0) + 1440.0) % 1440.0
143    }
144
145    /// The hour, 0 to 23.
146    pub fn hour(&self) -> i64 {
147        (self.minute_of_day() / 60.0).floor() as i64
148    }
149
150    /// The minute of the hour, 0 to 59.
151    pub fn minute(&self) -> i64 {
152        (self.minute_of_day() % 60.0).floor() as i64
153    }
154
155    /// The day number, counting from day 0.
156    pub fn day(&self) -> i64 {
157        (self.minutes / 1440.0).floor() as i64
158    }
159
160    /// The day of the week, from 0 to `week_days - 1`; the weekend is last.
161    pub fn day_of_week(&self) -> i64 {
162        self.day() % self.calendar.week_days
163    }
164
165    /// Whether today is a weekend day.
166    pub fn is_weekend(&self) -> bool {
167        self.is_weekend_on(self.day())
168    }
169
170    /// Whether `day` (a day number, as `day()` gives) falls on a weekend.
171    pub fn is_weekend_on(&self, day: i64) -> bool {
172        day.rem_euclid(self.calendar.week_days)
173            >= self.calendar.week_days - self.calendar.weekend_days
174    }
175
176    /// The quarter of the year, from 0.
177    pub fn quarter(&self) -> i64 {
178        ((self.day() % self.calendar.year_days) as f64 / self.calendar.quarter_days as f64).floor()
179            as i64
180    }
181
182    /// The year, counting from 0.
183    pub fn year(&self) -> i64 {
184        (self.day() as f64 / self.calendar.year_days as f64).floor() as i64
185    }
186}
187
188impl Clock {
189    /// 07:00 to 09:59, the morning rush.
190    pub fn is_morning(&self) -> bool {
191        let h = self.hour();
192        (7..10).contains(&h)
193    }
194
195    /// 11:00 to 13:59, lunch.
196    pub fn is_lunch(&self) -> bool {
197        let h = self.hour();
198        (11..14).contains(&h)
199    }
200
201    /// 17:00 to 20:59, the evening rush.
202    pub fn is_evening(&self) -> bool {
203        let h = self.hour();
204        (17..21).contains(&h)
205    }
206
207    /// 21:00 to 05:59.
208    pub fn is_night(&self) -> bool {
209        let h = self.hour();
210        !(6..21).contains(&h)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    /// One reading of the clock on a calendar. The retired TypeScript engine
219    /// held the same rows (`clockReadings.test.ts`, #1875).
220    struct Row {
221        name: &'static str,
222        calendar: Calendar,
223        minutes: f64,
224        day: i64,
225        day_of_week: i64,
226        is_weekend: bool,
227        quarter: i64,
228        year: i64,
229        hour: i64,
230        minute: i64,
231    }
232
233    #[test]
234    fn readings_match_the_web_table() {
235        let rows = [
236            Row {
237                name: "canon, tower founded",
238                calendar: CANON,
239                minutes: 420.0,
240                day: 0,
241                day_of_week: 0,
242                is_weekend: false,
243                quarter: 0,
244                year: 0,
245                hour: 7,
246                minute: 0,
247            },
248            Row {
249                name: "canon, weekend slot",
250                calendar: CANON,
251                minutes: 3665.0,
252                day: 2,
253                day_of_week: 2,
254                is_weekend: true,
255                quarter: 0,
256                year: 0,
257                hour: 13,
258                minute: 5,
259            },
260            Row {
261                name: "canon, second quarter",
262                calendar: CANON,
263                minutes: 4380.0,
264                day: 3,
265                day_of_week: 0,
266                is_weekend: false,
267                quarter: 1,
268                year: 0,
269                hour: 1,
270                minute: 0,
271            },
272            Row {
273                name: "canon, year rollover",
274                calendar: CANON,
275                minutes: 17880.0,
276                day: 12,
277                day_of_week: 0,
278                is_weekend: false,
279                quarter: 0,
280                year: 1,
281                hour: 10,
282                minute: 0,
283            },
284            Row {
285                name: "canon, last day of the year",
286                calendar: CANON,
287                minutes: 17220.0,
288                day: 11,
289                day_of_week: 2,
290                is_weekend: true,
291                quarter: 3,
292                year: 0,
293                hour: 23,
294                minute: 0,
295            },
296            Row {
297                name: "real world, tower founded",
298                calendar: REAL_WORLD,
299                minutes: 420.0,
300                day: 0,
301                day_of_week: 0,
302                is_weekend: false,
303                quarter: 0,
304                year: 0,
305                hour: 7,
306                minute: 0,
307            },
308            Row {
309                name: "real world, first Saturday",
310                calendar: REAL_WORLD,
311                minutes: 7710.0,
312                day: 5,
313                day_of_week: 5,
314                is_weekend: true,
315                quarter: 0,
316                year: 0,
317                hour: 8,
318                minute: 30,
319            },
320            Row {
321                name: "real world, second quarter",
322                calendar: REAL_WORLD,
323                minutes: 129660.0,
324                day: 90,
325                day_of_week: 6,
326                is_weekend: true,
327                quarter: 1,
328                year: 0,
329                hour: 1,
330                minute: 0,
331            },
332            Row {
333                name: "real world, year rollover",
334                calendar: REAL_WORLD,
335                minutes: 519120.0,
336                day: 360,
337                day_of_week: 3,
338                is_weekend: false,
339                quarter: 0,
340                year: 1,
341                hour: 12,
342                minute: 0,
343            },
344        ];
345        for r in rows {
346            let c = Clock::new(r.minutes, r.calendar);
347            assert_eq!(c.day(), r.day, "{} day", r.name);
348            assert_eq!(c.day_of_week(), r.day_of_week, "{} day of week", r.name);
349            assert_eq!(c.is_weekend(), r.is_weekend, "{} weekend", r.name);
350            assert_eq!(c.quarter(), r.quarter, "{} quarter", r.name);
351            assert_eq!(c.year(), r.year, "{} year", r.name);
352            assert_eq!(c.hour(), r.hour, "{} hour", r.name);
353            assert_eq!(c.minute(), r.minute, "{} minute", r.name);
354        }
355    }
356
357    #[test]
358    fn calendar_spans_match_the_web() {
359        let spans = |c: Calendar| {
360            [
361                c.week_days,
362                c.weekend_days,
363                c.quarter_days,
364                c.year_days,
365                c.maint_period_days,
366            ]
367        };
368        assert_eq!(spans(CANON), [3, 1, 3, 12, 3]);
369        assert_eq!(spans(REAL_WORLD), [7, 2, 90, 360, 30]);
370    }
371}