Skip to main content

canwu_world/
lib.rs

1//! Historical entity read models and detached world snapshots.
2
3use canwu_core::{ArmyId, GovernmentId, LetterId, PersonId, RouteId, TerritoryId};
4use canwu_time::{SimDuration, SimTime};
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
8pub struct MapPoint {
9    pub x: f32,
10    pub y: f32,
11}
12
13#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
14pub struct Person {
15    pub id: PersonId,
16    pub name: String,
17    pub government: GovernmentId,
18    pub current_location: TerritoryId,
19    pub roles: Vec<String>,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub transit: Option<PersonTransitState>,
22}
23
24#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25pub struct PersonTransitState {
26    pub from: TerritoryId,
27    pub to: TerritoryId,
28    pub departed_at: SimTime,
29    pub arrives_at: SimTime,
30}
31
32#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum LetterStatus {
35    HeldByPerson,
36    InTransit,
37    HeldAtLocation,
38    Delivered,
39}
40
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub struct LetterCargo {
43    pub id: LetterId,
44    pub sender: PersonId,
45    pub recipient: PersonId,
46    pub body: String,
47    pub status: LetterStatus,
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub carrier: Option<PersonId>,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub location: Option<TerritoryId>,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub delivered_at: Option<SimTime>,
54}
55
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57pub struct Government {
58    pub id: GovernmentId,
59    pub name: String,
60    pub capital: TerritoryId,
61}
62
63#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
64pub struct Territory {
65    pub id: TerritoryId,
66    pub name: String,
67    pub controller: GovernmentId,
68    pub position: MapPoint,
69}
70
71#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
72pub struct Route {
73    pub id: RouteId,
74    pub name: String,
75    pub from: TerritoryId,
76    pub to: TerritoryId,
77    pub travel_minutes: i64,
78    pub terrain: String,
79}
80
81impl Route {
82    #[must_use]
83    pub fn connects(&self, first: TerritoryId, second: TerritoryId) -> bool {
84        (self.from == first && self.to == second) || (self.from == second && self.to == first)
85    }
86
87    #[must_use]
88    pub fn other_end(&self, territory: TerritoryId) -> Option<TerritoryId> {
89        if self.from == territory {
90            Some(self.to)
91        } else if self.to == territory {
92            Some(self.from)
93        } else {
94            None
95        }
96    }
97}
98
99#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
100pub struct TransitState {
101    pub from: TerritoryId,
102    pub to: TerritoryId,
103    pub departed_at: SimTime,
104    pub arrives_at: SimTime,
105}
106
107#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
108pub struct Army {
109    pub id: ArmyId,
110    pub name: String,
111    pub government: GovernmentId,
112    pub commander: PersonId,
113    pub location: TerritoryId,
114    pub strength: u32,
115    pub morale: u16,
116    pub transit: Option<TransitState>,
117}
118
119#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
120pub struct WorldSnapshot {
121    pub people: Vec<Person>,
122    pub governments: Vec<Government>,
123    pub territories: Vec<Territory>,
124    pub routes: Vec<Route>,
125    pub armies: Vec<Army>,
126    #[serde(default, skip_serializing_if = "Vec::is_empty")]
127    pub letters: Vec<LetterCargo>,
128}
129
130impl WorldSnapshot {
131    #[must_use]
132    pub fn person(&self, id: PersonId) -> Option<&Person> {
133        self.people.iter().find(|person| person.id == id)
134    }
135
136    #[must_use]
137    pub fn government(&self, id: GovernmentId) -> Option<&Government> {
138        self.governments
139            .iter()
140            .find(|government| government.id == id)
141    }
142
143    #[must_use]
144    pub fn territory(&self, id: TerritoryId) -> Option<&Territory> {
145        self.territories.iter().find(|territory| territory.id == id)
146    }
147
148    #[must_use]
149    pub fn route(&self, id: RouteId) -> Option<&Route> {
150        self.routes.iter().find(|route| route.id == id)
151    }
152
153    #[must_use]
154    pub fn army(&self, id: ArmyId) -> Option<&Army> {
155        self.armies.iter().find(|army| army.id == id)
156    }
157
158    #[must_use]
159    pub fn letter(&self, id: LetterId) -> Option<&LetterCargo> {
160        self.letters.iter().find(|letter| letter.id == id)
161    }
162
163    #[must_use]
164    pub fn route_between(&self, from: TerritoryId, to: TerritoryId) -> Option<&Route> {
165        self.routes.iter().find(|route| route.connects(from, to))
166    }
167
168    pub fn adjacent_territories(
169        &self,
170        territory: TerritoryId,
171    ) -> impl Iterator<Item = TerritoryId> + '_ {
172        self.routes
173            .iter()
174            .filter_map(move |route| route.other_end(territory))
175    }
176
177    #[must_use]
178    pub fn travel_duration(&self, from: TerritoryId, to: TerritoryId) -> Option<SimDuration> {
179        self.route_between(from, to)
180            .map(|route| SimDuration::minutes(route.travel_minutes))
181    }
182
183    #[must_use]
184    pub fn distance(&self, from: TerritoryId, to: TerritoryId) -> Option<f32> {
185        let first = self.territory(from)?.position;
186        let second = self.territory(to)?.position;
187        Some((second.x - first.x).hypot(second.y - first.y))
188    }
189
190    pub fn territories_within(
191        &self,
192        center: TerritoryId,
193        radius: f32,
194    ) -> impl Iterator<Item = &Territory> {
195        let center_position = self.territory(center).map(|territory| territory.position);
196        self.territories.iter().filter(move |territory| {
197            center_position.is_some_and(|point| {
198                (territory.position.x - point.x).hypot(territory.position.y - point.y) <= radius
199            })
200        })
201    }
202}
203
204#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
205pub struct WorldDiff {
206    pub changed_armies: Vec<ArmyId>,
207    pub changed_people: Vec<PersonId>,
208    pub changed_territories: Vec<TerritoryId>,
209    pub changed_letters: Vec<LetterId>,
210}
211
212impl WorldDiff {
213    #[must_use]
214    pub fn between(before: &WorldSnapshot, after: &WorldSnapshot) -> Self {
215        let changed_armies = after
216            .armies
217            .iter()
218            .filter(|army| before.army(army.id) != Some(*army))
219            .map(|army| army.id)
220            .collect();
221        let changed_people = after
222            .people
223            .iter()
224            .filter(|person| before.person(person.id) != Some(*person))
225            .map(|person| person.id)
226            .collect();
227        let changed_territories = after
228            .territories
229            .iter()
230            .filter(|territory| before.territory(territory.id) != Some(*territory))
231            .map(|territory| territory.id)
232            .collect();
233        let changed_letters = after
234            .letters
235            .iter()
236            .filter(|letter| before.letter(letter.id) != Some(*letter))
237            .map(|letter| letter.id)
238            .collect();
239        Self {
240            changed_armies,
241            changed_people,
242            changed_territories,
243            changed_letters,
244        }
245    }
246}