use std::{ collections::{HashMap, HashSet}, fmt::Display, num::NonZeroUsize, time::Duration, }; use enum_dispatch::enum_dispatch; use serde::{Deserialize, Serialize}; use uuid::Uuid; pub mod definitions; const SAVE_FILE: &'static str = "world.bin"; fn main() { let mut world = match std::fs::read(SAVE_FILE) { Ok(data) => bincode::deserialize(&data).unwrap(), Err(_) => { let mut world = World::default(); let mut site = Site::new( "Gorbo's Keep", fastrand::usize(1..5) .try_into() .expect("Generated site with size of 0"), ); site.populate_randomly(); world.sites.push(site); std::fs::write("world.bin", bincode::serialize(&world).unwrap()).unwrap(); world } }; let source = std::fs::File::open("defs/data.def").unwrap(); definitions::DefinitionParser::parse(source).unwrap(); // for site in world.sites.iter() { // println!("{site}") // } // let mut input = String::new(); // loop { // std::io::stdin().read_line(&mut input).unwrap(); // if vec!["q", "quit", "exit"] // .iter() // .any(|quit_cmd| input.trim() == *quit_cmd) // { // break; // } // let start = std::time::Instant::now(); // world.advance_time(Duration::from_secs(3600)); // let end = std::time::Instant::now(); // println!("World tick: {}us", end.sub(start).as_micros()); // } } #[derive(Debug, Default, Serialize, Deserialize)] pub struct World { pub sites: Vec, } impl World { pub fn advance_time(&mut self, time: Duration) { for site in self.sites.iter_mut() { site.advance_time(time); } } } #[derive(Debug, Serialize, Deserialize)] pub struct Site { pub name: String, areas: Vec, accumulated_time: Duration, } impl Site { #[inline] const fn minimum_tick() -> Duration { Duration::from_secs(3600) } fn inner_tick(&mut self) { for area in self.areas.iter_mut() { match resolve_combat(&area.population, 600) { Some(results) => { for death in results.kills { area.population .retain(|creature| creature.uuid != death.killed); } } None => { // If no combat, there is a chance of migration to other areas } } } println!("Tick! {}", self); } pub fn areas(&self) -> &Vec { self.areas.as_ref() } pub fn areas_mut(&mut self) -> &mut Vec { self.areas.as_mut() } pub fn populate_randomly(&mut self) { let area_count = self.areas.len(); { let min_count = area_count; let max_count = (5 + (area_count - 1) * 3).max(min_count); for _ in 0..fastrand::usize(min_count..=max_count) { let area_t = fastrand::f32().powi(2); let index = (area_t * (area_count - 1) as f32).round() as usize; self.areas .get_mut(index) .expect("Creature generation index for depth is out of bounds") .population .push(Creature::new(CreatureBehaviour::Bandit(Bandit::default()))) } } { let min_count = 0; let max_count = area_count.max(min_count); for _ in 0..fastrand::usize(min_count..=max_count) { let area_t = 1.0 - fastrand::f32().powi(2); let index = (area_t * (area_count - 1) as f32).round() as usize; self.areas .get_mut(index) .expect("Creature generation index for depth is out of bounds") .population .push(Creature::new(CreatureBehaviour::Spider(Spider::default()))) } } } pub fn new(name: &str, size: NonZeroUsize) -> Self { Self { name: name.into(), areas: vec![SiteArea::default(); size.get()], accumulated_time: Duration::default(), } } pub fn advance_time(&mut self, time: Duration) { self.accumulated_time += time; while self.accumulated_time >= Self::minimum_tick() { self.inner_tick(); self.accumulated_time -= Self::minimum_tick(); } } /// Finds a creature with given uuid, removes it from the site and returns it with ownership pub fn remove_by_uuid(&mut self, creature_uuid: Uuid) -> Option { for area in self.areas.iter_mut() { for i in 0..area.population.len() { if area.population[i].uuid == creature_uuid { return Some(area.population.swap_remove(i)); } } } None } } impl Display for Site { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "site \"{}\" [", self.name)?; for (index, area) in self.areas.iter().enumerate() { write!(f, "\n\tarea {} [", index)?; for creature in area.population.iter() { write!(f, "\n\t\t{}", creature)?; } if area.population.len() > 0 { write!(f, "\n\t")?; } write!(f, "]")?; } if !self.areas.is_empty() { write!(f, "\n")?; } write!(f, "]") } } #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct SiteArea { pub population: Vec, } #[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)] pub struct Creature { pub uuid: Uuid, pub data: CreatureBehaviour, } impl Display for Creature { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} [{}]", self.data.species_id(), self.uuid) } } impl Creature { pub fn new(data: CreatureBehaviour) -> Self { Self { uuid: Uuid::new_v4(), data, } } } #[enum_dispatch(CreatureBehaviour)] pub trait CreatureImpl { fn species_id(&self) -> String; fn initiative_modifier(&self) -> i8 { 0 } fn threat_rating(&self) -> u8 { 1 } fn ally_predicate(&self, other: &dyn CreatureImpl) -> bool { self.species_id() == other.species_id() } } #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] #[enum_dispatch] pub enum CreatureBehaviour { Default(DefaultBehaviour), Bandit(Bandit), Spider(Spider), } #[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct DefaultBehaviour; impl CreatureImpl for DefaultBehaviour { fn species_id(&self) -> String { "default".into() } } #[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct Bandit {} impl CreatureImpl for Bandit { fn species_id(&self) -> String { "bandit".into() } fn threat_rating(&self) -> u8 { 2 } } #[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct Spider {} impl CreatureImpl for Spider { fn species_id(&self) -> String { "spider".into() } fn threat_rating(&self) -> u8 { 4 } fn initiative_modifier(&self) -> i8 { 1 } } #[derive(Debug)] pub struct KillData { pub killed: Uuid, pub killer: Uuid, } #[derive(Debug)] pub struct CombatResults { pub kills: Vec, } #[derive(Debug)] struct CombatState { initiative: i8, health: u8, damage: u8, alive: bool, enemies: HashSet, } impl CombatState { pub fn from_creature(creature: &Creature) -> Self { Self { initiative: fastrand::i8(1..=4).saturating_add(creature.data.initiative_modifier()), health: creature.data.threat_rating(), damage: creature.data.threat_rating(), alive: true, enemies: HashSet::new(), } } } pub fn resolve_combat(combatants: &Vec, max_rounds: u16) -> Option { let mut participants: HashMap = HashMap::new(); for index_1 in 0..combatants.len() { for index_2 in (index_1 + 1)..combatants.len() { if index_1 == index_2 { continue; } let c1 = &combatants[index_1]; let c2 = &combatants[index_2]; let is_enemy = !c1.data.ally_predicate(&c2.data) || !c2.data.ally_predicate(&c1.data); if is_enemy { if !participants.contains_key(&c1.uuid) { participants.insert(c1.uuid, CombatState::from_creature(&c1)); } if !participants.contains_key(&c2.uuid) { participants.insert(c2.uuid, CombatState::from_creature(&c2)); } participants .get_mut(&c1.uuid) .unwrap() .enemies .insert(c2.uuid); participants .get_mut(&c2.uuid) .unwrap() .enemies .insert(c1.uuid); } } } if participants.is_empty() { // No enemies in group :) return None; } // Time for violence let mut order: Vec<_> = participants.iter().collect(); order.sort_unstable_by_key(|(_, combat_state)| -combat_state.initiative); let order: Vec = order.iter().map(|(uuid, _)| **uuid).collect(); let mut kills: HashMap = HashMap::new(); for _ in 0..max_rounds { let should_continue = participants.iter().any(|(_, combat)| { combat.alive && combat.enemies.iter().any(|uuid| participants[uuid].alive) }); if !should_continue { break; } for uuid in order.iter() { if !participants[uuid].alive { continue; } let targets: Vec<_> = participants[uuid] .enemies .iter() .filter(|enemy_uuid| participants[*enemy_uuid].alive) .collect(); let target = match fastrand::choice(targets.iter()) { Some(target) => **target, None => continue, }; let damage = participants[uuid].damage; let enemy = participants.get_mut(&target).unwrap(); enemy.health = enemy.health.saturating_sub(fastrand::u8(0..=damage)); if enemy.health == 0 { enemy.alive = false; kills.insert(target, *uuid); } } } Some(CombatResults { kills: kills .iter() .map(|(killed, killer)| KillData { killed: *killed, killer: *killer, }) .collect(), }) }