adventure-sim/src/main.rs

387 lines
11 KiB
Rust

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<Site>,
}
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<SiteArea>,
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<SiteArea> {
self.areas.as_ref()
}
pub fn areas_mut(&mut self) -> &mut Vec<SiteArea> {
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<Creature> {
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<Creature>,
}
#[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<KillData>,
}
#[derive(Debug)]
struct CombatState {
initiative: i8,
health: u8,
damage: u8,
alive: bool,
enemies: HashSet<Uuid>,
}
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<Creature>, max_rounds: u16) -> Option<CombatResults> {
let mut participants: HashMap<Uuid, CombatState> = 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<Uuid> = order.iter().map(|(uuid, _)| **uuid).collect();
let mut kills: HashMap<Uuid, Uuid> = 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(),
})
}