1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use crate::orbit::OrbitValidationError;
use holding_kronos::Calendar;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;

use holding_color::Color;

use crate::orbit::Orbit;

/// A unique identifier for celestial bodies.
#[derive(Copy, Clone, Deserialize, Serialize, PartialEq, Eq, Debug)]
pub struct PlanetId(Uuid);

/// A celestial body.
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct CelestialBody {
    /// The id of the body.
    pub id: PlanetId,

    /// The name of the body.
    pub name: String,

    /// The orbit of the body.
    pub orbit: Option<Orbit>,

    /// The color of the body.
    pub color: Color,

    /// The ids of the planets in orbit of this one.
    pub children: Vec<PlanetId>,

    /// The number of seconds the planet
    /// takes to rotate.
    pub rotational_period: usize,

    /// The temperature in degrees kelvin.
    pub temperature: i32,
}

impl CelestialBody {
    /// Creates a new `CelestialBody`.
    pub fn new(name: String, temperature: i32, rotational_period: usize, color: Color) -> Self {
        Self {
            id: PlanetId(Uuid::new_v4()),
            name,
            color,
            temperature,
            rotational_period,
            orbit: None,
            children: vec![],
        }
    }

    /// Calculates whether a planet is luminous.
    ///
    /// https://qph.fs.quoracdn.net/main-qimg-6bce97c94fb60cc1fe025591653de493
    pub fn is_luminous(&self) -> bool {
        self.temperature > 3500
    }

    /// Adds a new moon to this planet.
    pub fn with_moon(&mut self, moon: &mut CelestialBody, period: usize) -> &mut Self {
        let orbit = Orbit::from_period(moon, self.id, period, 0);
        moon.orbit = Some(orbit);
        self.children.push(moon.id);
        self
    }

    /// Sets the parent of this planet.
    pub fn with_parent(&mut self, parent: &mut CelestialBody, period: usize) -> &mut Self {
        let orbit = Orbit::from_period(self, parent.id, period, 0);
        self.orbit = Some(orbit);
        parent.children.push(self.id);
        self
    }

    /// Validates an orbit against a calendar,
    /// ensuring the rotational and orbital
    /// periods are correct.
    pub fn validate_calendar(&self, calendar: &Calendar) -> Result<bool, ValidationError> {
        let planet_period = self.rotational_period as u32;
        let calendar_period = calendar.days_to_seconds(1);
        let x = if planet_period != calendar_period {
            Err(ValidationError::InconsistentRotationalPeriod(
                planet_period,
                calendar_period,
            ))
        } else {
            Ok(true)
        };

        if let Some(y) = self.orbit.as_ref().map(|o| o.validate_calendar(calendar)) {
            x.and(y.map_err(Into::into))
        } else {
            x
        }
    }
}

#[derive(Error, Debug, Copy, Clone)]
pub enum ValidationError {
    #[error("the rotational period is inconsistent. planet: {0}, calendar: {1}")]
    InconsistentRotationalPeriod(u32, u32),
    #[error("invalid orbit: {0}")]
    OrbitValidationError(OrbitValidationError),
}

impl From<OrbitValidationError> for ValidationError {
    fn from(e: OrbitValidationError) -> Self {
        Self::OrbitValidationError(e)
    }
}

/// Keeps track of `CelestialBody`s.
pub trait PlanetStore {
    /// Get a `CelestialBody` from the `PlanetStore`.
    fn get_planet(&self, id: PlanetId) -> Option<&CelestialBody>;

    /// Get a mutable reference to a `CelestialBody` from the `PlanetStore`.
    fn get_planet_mut(&mut self, id: PlanetId) -> Option<&mut CelestialBody>;

    /// Create a new `CelestialBody` in the `PlanetStore`.
    fn create_planet(
        &mut self,
        name: String,
        temperature: i32,
        rotational_period: usize,
        color: Color,
    ) -> &CelestialBody;

    /// Adds an orbit.
    ///
    /// todo(arlyon): Allow this to fail if
    /// - either planet doesn't exist
    /// - the child is orbiting something else
    fn add_orbit(&mut self, parent_id: PlanetId, child_id: PlanetId, period: usize) {
        let mut child = self.get_planet_mut(child_id).unwrap();
        let orbit = Orbit::from_period(child, parent_id, period, 0);
        child.orbit = Some(orbit);
        let parent = self.get_planet_mut(parent_id).unwrap();
        parent.children.push(child_id);
    }
}