Skip to main content

galactic_war/
system.rs

1use core::panic;
2use indexmap::IndexMap;
3
4use crate::config::{GalaxyConfig, StructureConfig, SystemConfig};
5use crate::{Details, Resources, StructureInfo, SystemInfo, SystemProduction};
6use std::fmt;
7use std::str::FromStr;
8
9/// An System in the Galaxy
10#[derive(Debug, Default)]
11pub struct System {
12    /// List of events that are happening in the system.
13    events: Vec<Event>,
14
15    /// Current resources available in the system.
16    resources: Resources,
17
18    /// List of structures in the system.
19    structures: Vec<Structure>,
20}
21
22#[derive(Debug)]
23struct Structure {
24    name: StructureType,
25    level: usize,
26}
27
28#[derive(Clone, Debug)]
29pub struct Event {
30    pub completion: usize,
31    pub action: EventCallback,
32    pub structure: Option<StructureType>,
33}
34
35pub type EventInfo = Event;
36
37#[derive(Clone, Debug)]
38pub enum EventCallback {
39    Metal,
40    Water,
41    Crew,
42    Build,
43}
44
45#[derive(Hash, Debug, Clone, Copy, PartialEq, Eq)]
46pub enum StructureType {
47    Colony,
48    AsteroidMine,
49    WaterHarvester,
50    Hatchery,
51    StorageDepot,
52}
53
54impl fmt::Display for StructureType {
55    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56        write!(f, "{:?}", self)
57    }
58}
59
60impl FromStr for StructureType {
61    type Err = ();
62
63    fn from_str(s: &str) -> Result<Self, Self::Err> {
64        match s.to_lowercase().as_str() {
65            "colony" => Ok(StructureType::Colony),
66            "asteroidmine" => Ok(StructureType::AsteroidMine),
67            "waterharvester" => Ok(StructureType::WaterHarvester),
68            "hatchery" => Ok(StructureType::Hatchery),
69            "storagedepot" => Ok(StructureType::StorageDepot),
70            _ => Err(()),
71        }
72    }
73}
74
75impl System {
76    /// Create a new system
77    ///
78    /// This takes an SystemConfig because there may be multiple system types in future
79    pub fn new(tick: usize, system_config: &SystemConfig, galaxy_config: &GalaxyConfig) -> Self {
80        let resources = Resources {
81            metal: *system_config.resources.get("metal").unwrap_or(&0),
82            water: *system_config.resources.get("water").unwrap_or(&0),
83            crew: *system_config.resources.get("crew").unwrap_or(&0),
84        };
85        let mut structures = Vec::new();
86        for (name, structure) in system_config.structures.iter() {
87            structures.push(Structure {
88                name: StructureType::from_str(name).unwrap(),
89                level: structure.starting_level.unwrap_or(0),
90            });
91        }
92        let mut new_system = Self {
93            events: Vec::new(),
94            resources,
95            structures,
96        };
97
98        // Kick off initial production events
99        // Yes, this adds a new resource of every type
100        new_system.event_callback(
101            tick,
102            galaxy_config,
103            Event {
104                completion: tick,
105                action: EventCallback::Metal,
106                structure: None,
107            },
108        );
109        new_system.event_callback(
110            tick,
111            galaxy_config,
112            Event {
113                completion: tick,
114                action: EventCallback::Crew,
115                structure: None,
116            },
117        );
118        new_system.event_callback(
119            tick,
120            galaxy_config,
121            Event {
122                completion: tick,
123                action: EventCallback::Water,
124                structure: None,
125            },
126        );
127
128        new_system
129    }
130
131    /// Get the index of the structure by type
132    ///
133    /// The structure may not exist, so it returns an Option
134    fn structure(&self, structure: StructureType) -> Option<usize> {
135        self.structures.iter().position(|b| b.name == structure)
136    }
137
138    /// Get the level of a structure
139    fn structure_level(&self, structure: StructureType) -> usize {
140        if let Some(index) = self.structure(structure) {
141            self.structures[index].level
142        } else {
143            0
144        }
145    }
146
147    /// Get the structure configuration from the GalaxyConfig
148    fn get_structure_config(
149        galaxy_config: &GalaxyConfig,
150        structure: StructureType,
151    ) -> &StructureConfig {
152        galaxy_config
153            .systems
154            .structures
155            .get(&structure.to_string().to_lowercase())
156            .unwrap()
157    }
158
159    /// Get the production of the system
160    fn get_production(&mut self, tick: usize, galaxy_config: &GalaxyConfig) -> SystemProduction {
161        self.process_events(tick, galaxy_config);
162        let mut production = SystemProduction {
163            metal: 0,
164            crew: 0,
165            water: 0,
166        };
167        for structure in self.structures.iter() {
168            let production_config = galaxy_config
169                .get_structure_production(&structure.name.to_string(), structure.level);
170            if let Some(metal) = production_config.metal {
171                production.metal += metal;
172            }
173            if let Some(crew) = production_config.crew {
174                production.crew += crew;
175            }
176            if let Some(water) = production_config.water {
177                production.water += water;
178            }
179        }
180        production
181    }
182
183    /// Get the available resource storage in the system.
184    fn get_storage(&mut self, tick: usize, galaxy_config: &GalaxyConfig) -> SystemProduction {
185        self.process_events(tick, galaxy_config);
186        let mut production = SystemProduction {
187            metal: 0,
188            crew: 0,
189            water: 0,
190        };
191        for structure in self.structures.iter() {
192            let production_config =
193                galaxy_config.get_structure_storage(&structure.name.to_string(), structure.level);
194            if let Some(metal) = production_config.metal {
195                production.metal += metal;
196            }
197            if let Some(crew) = production_config.crew {
198                production.crew += crew;
199            }
200            if let Some(water) = production_config.water {
201                production.water += water;
202            }
203        }
204        production
205    }
206
207    /// Callback for events
208    ///
209    /// This will process the event and update the state of the system.
210    /// It will also create new events if needed.
211    fn event_callback(&mut self, tick: usize, galaxy_config: &GalaxyConfig, event: Event) {
212        // Check the completion time
213        if event.completion > tick {
214            return;
215        }
216        match event.action {
217            EventCallback::Metal => {
218                self.resources.metal += 1;
219                let storage = self.get_storage(tick, galaxy_config);
220                if self.resources.metal > storage.metal {
221                    self.resources.metal = storage.metal;
222                }
223                let production = self.get_production(tick, galaxy_config);
224                if production.metal > 0 {
225                    // Create a new event for the next metal piece
226                    self.register_event(Event {
227                        completion: event.completion + (3600 / production.metal),
228                        action: EventCallback::Metal,
229                        structure: None,
230                    });
231                }
232            }
233            EventCallback::Water => {
234                self.resources.water += 1;
235                let storage = self.get_storage(tick, galaxy_config);
236                if self.resources.water > storage.water {
237                    self.resources.water = storage.water;
238                }
239                let production = self.get_production(tick, galaxy_config);
240                if production.water > 0 {
241                    // Create a new event for the next water unit
242                    self.register_event(Event {
243                        completion: event.completion + (3600 / production.water),
244                        action: EventCallback::Water,
245                        structure: None,
246                    });
247                }
248            }
249            EventCallback::Crew => {
250                self.resources.crew += 1;
251                let storage = self.get_storage(tick, galaxy_config);
252                if self.resources.crew > storage.crew {
253                    self.resources.crew = storage.crew;
254                }
255                let production = self.get_production(tick, galaxy_config);
256                if production.crew > 0 {
257                    // Create a new event for the next crew member
258                    self.register_event(Event {
259                        completion: event.completion + (3600 / production.crew),
260                        action: EventCallback::Crew,
261                        structure: None,
262                    });
263                }
264            }
265            EventCallback::Build => {
266                // Build the structure
267                if let Some(structure) = event.structure {
268                    let index = self.structure(structure).unwrap();
269                    self.structures[index].level += 1;
270                } else {
271                    panic!("Structure event without StructureType");
272                }
273            }
274        }
275    }
276
277    /// Get the current metal amount
278    pub fn metal(&mut self, tick: usize, galaxy_config: &GalaxyConfig) -> usize {
279        self.resources(tick, galaxy_config).water
280    }
281
282    /// Get the current water amount
283    pub fn water(&mut self, tick: usize, galaxy_config: &GalaxyConfig) -> usize {
284        self.resources(tick, galaxy_config).water
285    }
286
287    /// Get the current crew count
288    pub fn crew(&mut self, tick: usize, galaxy_config: &GalaxyConfig) -> usize {
289        self.resources(tick, galaxy_config).crew
290    }
291
292    /// Get the current resources of the system
293    pub fn resources(&mut self, tick: usize, galaxy_config: &GalaxyConfig) -> Resources {
294        self.process_events(tick, galaxy_config);
295        self.resources.clone()
296    }
297
298    /// Check if there is an event that needs to be processed
299    pub fn event_to_process(&mut self, tick: usize) -> bool {
300        // Check if the first event is ready to be processed
301        if let Some(event) = self.events.first() {
302            event.completion <= tick
303        } else {
304            false
305        }
306    }
307
308    /// Register a new event
309    /// The event will be sorted by the completion time
310    pub fn register_event(&mut self, event: Event) {
311        self.events.push(event);
312        self.events.sort_by_key(|e| e.completion)
313    }
314
315    /// Process all events that are expected to happen
316    pub fn process_events(&mut self, tick: usize, galaxy_config: &GalaxyConfig) {
317        let mut dirty = true;
318        while dirty && self.event_to_process(tick) {
319            dirty = false;
320            let events = self.events.clone();
321            self.events.clear();
322            for event in events.iter() {
323                if event.completion <= tick {
324                    dirty = true;
325                    self.event_callback(tick, galaxy_config, event.clone());
326                } else {
327                    self.register_event(event.clone());
328                }
329            }
330        }
331    }
332
333    /// Get the score of a system.
334    ///
335    /// The score is the summation of every level of every structure in the system.
336    /// A structure with a level of 4 will contribute 1+2+3+4=10 to the score.
337    pub fn score(&mut self, tick: usize, galaxy_config: &GalaxyConfig) -> usize {
338        self.process_events(tick, galaxy_config);
339        self.structures
340            .iter()
341            .map(|b| (1..=b.level).sum::<usize>())
342            .sum()
343    }
344
345    /// Build a structure
346    pub fn build(
347        &mut self,
348        tick: usize,
349        galaxy_config: &GalaxyConfig,
350        structure: StructureType,
351    ) -> Result<Event, String> {
352        self.process_events(tick, galaxy_config);
353        // Check if we're already building a structure, we can only build one at a time
354        if self.events.iter().any(|e| e.structure.is_some()) {
355            return Err("Already building a structure".to_string());
356        }
357        if self.structure(structure).is_some() {
358            // Verify if the structure can be built
359            let cost = &System::get_structure_config(galaxy_config, structure).cost
360                [self.structure_level(structure)];
361            if self.resources.metal >= *cost.get("metal").unwrap_or(&0)
362                && self.resources.water >= *cost.get("water").unwrap_or(&0)
363                && self.resources.crew >= *cost.get("crew").unwrap_or(&0)
364            {
365                // Deduct the cost
366                self.resources = self.resources.clone()
367                    - Resources {
368                        metal: *cost.get("metal").unwrap_or(&0),
369                        water: *cost.get("water").unwrap_or(&0),
370                        crew: *cost.get("crew").unwrap_or(&0),
371                    };
372                // Increase the level
373                let event = Event {
374                    completion: tick + cost.get("time").unwrap_or(&1),
375                    action: EventCallback::Build,
376                    structure: Some(structure),
377                };
378                self.register_event(event.clone());
379                Ok(event)
380            } else {
381                // Not enough resources
382                Err("Not enough resources".to_string())
383            }
384        } else {
385            Err("Structure not found".to_string())
386        }
387    }
388
389    /// Get the details of the system
390    pub fn get_details(
391        &mut self,
392        tick: usize,
393        galaxy_config: &GalaxyConfig,
394        structure: Option<StructureType>,
395    ) -> Result<Details, String> {
396        self.process_events(tick, galaxy_config);
397        if let Some(structure) = structure {
398            let production_config = galaxy_config
399                .get_structure_production(&structure.to_string(), self.structure_level(structure));
400            let mut details = StructureInfo {
401                level: self.structure_level(structure),
402                production: Some(Resources {
403                    metal: production_config.metal.unwrap_or(0),
404                    water: production_config.water.unwrap_or(0),
405                    crew: production_config.crew.unwrap_or(0),
406                }),
407                builds: None,
408            };
409            if structure == StructureType::Colony {
410                if details.builds.is_none() {
411                    details.builds = Some(IndexMap::new());
412                }
413                let builds = details.builds.as_mut().unwrap();
414                for structure in self.structures.iter() {
415                    builds.insert(
416                        structure.name,
417                        System::get_structure_config(galaxy_config, structure.name)
418                            .get_cost(structure.level),
419                    );
420                }
421            }
422            Ok(Details::Structure(details))
423        } else {
424            let mut details = SystemInfo {
425                score: self.score(tick, galaxy_config),
426                resources: self.resources.clone(),
427                structures: IndexMap::new(),
428                production: self.get_production(tick, galaxy_config),
429                events: self.events.clone(),
430            };
431            for structure in self.structures.iter() {
432                details.structures.insert(structure.name, structure.level);
433            }
434            Ok(Details::System(details.clone()))
435        }
436    }
437}