veloren/world/src/civ/mod.rs
2021-03-07 14:25:07 +00:00

1144 lines
40 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![allow(dead_code)]
mod econ;
use self::{Occupation::*, Stock::*};
use crate::{
config::CONFIG,
sim::WorldSim,
site::{namegen::NameGen, Castle, Dungeon, Settlement, Site as WorldSite, Tree},
site2,
util::{attempt, seed_expan, MapVec, CARDINALS, NEIGHBORS},
Index, Land,
};
use common::{
astar::Astar,
path::Path,
spiral::Spiral2d,
store::{Id, Store},
terrain::{MapSizeLg, TerrainChunkSize},
vol::RectVolSize,
};
use core::{
fmt,
hash::{BuildHasherDefault, Hash},
ops::Range,
};
use fxhash::{FxHasher32, FxHasher64};
use hashbrown::{HashMap, HashSet};
use rand::prelude::*;
use rand_chacha::ChaChaRng;
use tracing::{debug, info, warn};
use vek::*;
const fn initial_civ_count(map_size_lg: MapSizeLg) -> u32 {
// NOTE: since map_size_lg's dimensions must fit in a u16, we can safely add
// them here.
//
// NOTE: 48 at "default" scale of 10 × 10 chunk bits (1024 × 1024 chunks).
(3 << (map_size_lg.vec().x + map_size_lg.vec().y)) >> 16
}
pub struct CaveInfo {
pub location: (Vec2<i32>, Vec2<i32>),
pub name: String,
}
#[allow(clippy::type_complexity)] // TODO: Pending review in #587
#[derive(Default)]
pub struct Civs {
pub civs: Store<Civ>,
pub places: Store<Place>,
pub tracks: Store<Track>,
/// We use this hasher (FxHasher64) because
/// (1) we don't care about DDOS attacks (ruling out SipHash);
/// (2) we care about determinism across computers (ruling out AAHash);
/// (3) we have 8-byte keys (for which FxHash is fastest).
pub track_map: HashMap<
Id<Site>,
HashMap<Id<Site>, Id<Track>, BuildHasherDefault<FxHasher64>>,
BuildHasherDefault<FxHasher64>,
>,
pub sites: Store<Site>,
pub caves: Store<CaveInfo>,
}
// Change this to get rid of particularly horrid seeds
const SEED_SKIP: u8 = 0;
pub struct GenCtx<'a, R: Rng> {
sim: &'a mut WorldSim,
rng: R,
}
impl<'a, R: Rng> GenCtx<'a, R> {
pub fn reseed(&mut self) -> GenCtx<'_, impl Rng> {
let mut entropy = self.rng.gen::<[u8; 32]>();
entropy[0] = entropy[0].wrapping_add(SEED_SKIP); // Skip bad seeds
GenCtx {
sim: self.sim,
rng: ChaChaRng::from_seed(entropy),
}
}
}
impl Civs {
pub fn generate(seed: u32, sim: &mut WorldSim, index: &mut Index) -> Self {
let mut this = Self::default();
let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
let initial_civ_count = initial_civ_count(sim.map_size_lg());
let mut ctx = GenCtx { sim, rng };
// TODO: Care about world size when generating caves.
for _ in 0..100 {
this.generate_cave(&mut ctx);
}
for _ in 0..initial_civ_count {
debug!("Creating civilisation...");
if this.birth_civ(&mut ctx.reseed()).is_none() {
warn!("Failed to find starting site for civilisation.");
}
}
info!(?initial_civ_count, "all civilisations created");
for _ in 0..initial_civ_count * 3 {
attempt(5, || {
let (kind, size) = match ctx.rng.gen_range(0..64) {
0..=4 => (SiteKind::Castle, 3),
5..=28 => (SiteKind::Refactor, 6),
29..=31 => (SiteKind::Tree, 4),
_ => (SiteKind::Dungeon, 0),
};
let loc = find_site_loc(&mut ctx, None, size)?;
this.establish_site(&mut ctx.reseed(), loc, |place| Site {
kind,
center: loc,
place,
site_tmp: None,
population: 0.0,
stocks: Stocks::from_default(100.0),
surplus: Stocks::from_default(0.0),
values: Stocks::from_default(None),
labors: MapVec::from_default(0.01),
yields: MapVec::from_default(1.0),
productivity: MapVec::from_default(1.0),
last_exports: Stocks::from_default(0.0),
export_targets: Stocks::from_default(0.0),
//trade_states: Stocks::default(),
coin: 1000.0,
})
});
}
// Tick
const SIM_YEARS: usize = 1000;
for _ in 0..SIM_YEARS {
this.tick(&mut ctx, 1.0);
}
// Flatten ground around sites
for site in this.sites.values() {
let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32);
let (radius, flatten_radius) = match &site.kind {
SiteKind::Settlement => (32i32, 10.0),
SiteKind::Dungeon => (8i32, 2.0),
SiteKind::Castle => (16i32, 5.0),
SiteKind::Refactor => (0i32, 0.0),
SiteKind::Tree => (12i32, 8.0),
};
let (raise, raise_dist): (f32, i32) = match &site.kind {
SiteKind::Settlement => (10.0, 6),
SiteKind::Castle => (0.0, 6),
_ => (0.0, 0),
};
// Flatten ground
if let Some(center_alt) = ctx.sim.get_alt_approx(wpos) {
for offs in Spiral2d::new().take(radius.pow(2) as usize) {
let center_alt = center_alt
+ if offs.magnitude_squared() <= raise_dist.pow(2) {
raise
} else {
0.0
}; // Raise the town centre up a little
let pos = site.center + offs;
let factor = ((1.0
- (site.center - pos).map(|e| e as f32).magnitude() / flatten_radius)
* 1.25)
.min(1.0);
let rng = &mut ctx.rng;
ctx.sim
.get_mut(pos)
// Don't disrupt chunks that are near water
.filter(|chunk| !chunk.river.near_water())
.map(|chunk| {
let diff = Lerp::lerp_precise(chunk.alt, center_alt, factor) - chunk.alt;
// Make sure we don't fall below sea level (fortunately, we don't have
// to worry about the case where water_alt is already set to a correct
// value higher than alt, since this chunk should have been filtered
// out in that case).
chunk.water_alt = CONFIG.sea_level.max(chunk.water_alt + diff);
chunk.alt += diff;
chunk.basement += diff;
chunk.rockiness = 0.0;
chunk.surface_veg *= 1.0 - factor * rng.gen_range(0.25..0.9);
});
}
}
}
// Place sites in world
let mut cnt = 0;
for sim_site in this.sites.values_mut() {
cnt += 1;
let wpos = sim_site
.center
.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
e * sz as i32 + sz as i32 / 2
});
let mut rng = ctx.reseed().rng;
let site = index.sites.insert(match &sim_site.kind {
SiteKind::Settlement => {
WorldSite::settlement(Settlement::generate(wpos, Some(ctx.sim), &mut rng))
},
SiteKind::Dungeon => {
WorldSite::dungeon(Dungeon::generate(wpos, Some(ctx.sim), &mut rng))
},
SiteKind::Castle => {
WorldSite::castle(Castle::generate(wpos, Some(ctx.sim), &mut rng))
},
SiteKind::Refactor => WorldSite::refactor(site2::Site::generate(
&Land::from_sim(&ctx.sim),
&mut rng,
wpos,
)),
SiteKind::Tree => {
WorldSite::tree(Tree::generate(wpos, &Land::from_sim(&ctx.sim), &mut rng))
},
});
sim_site.site_tmp = Some(site);
let site_ref = &index.sites[site];
let radius_chunks =
(site_ref.radius() / TerrainChunkSize::RECT_SIZE.x as f32).ceil() as usize;
for pos in Spiral2d::new()
.map(|offs| sim_site.center + offs)
.take((radius_chunks * 2).pow(2))
{
ctx.sim.get_mut(pos).map(|chunk| chunk.sites.push(site));
}
debug!(?sim_site.center, "Placed site at location");
}
info!(?cnt, "all sites placed");
//this.display_info();
this
}
// TODO: Move this
fn generate_cave(&mut self, ctx: &mut GenCtx<impl Rng>) {
let mut pos = ctx
.sim
.get_size()
.map(|sz| ctx.rng.gen_range(0..sz as i32) as f32);
let mut vel = pos
.map2(ctx.sim.get_size(), |pos, sz| sz as f32 / 2.0 - pos)
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
let path = (-100..100)
.filter_map(|i: i32| {
let depth = (i.abs() as f32 / 100.0 * std::f32::consts::PI / 2.0).cos();
vel = (vel
+ Vec2::new(
ctx.rng.gen_range(-0.35..0.35),
ctx.rng.gen_range(-0.35..0.35),
))
.try_normalized()
.unwrap_or_else(Vec2::unit_y);
let old_pos = pos.map(|e| e as i32);
pos = (pos + vel * 0.5)
.clamped(Vec2::zero(), ctx.sim.get_size().map(|e| e as f32 - 1.0));
Some((pos.map(|e| e as i32), depth)).filter(|(pos, _)| *pos != old_pos)
})
.collect::<Vec<_>>();
for locs in path.windows(3) {
let to_prev_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[0].0 - locs[1].0)
.expect("Track locations must be neighbors")
.0;
let to_next_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[2].0 - locs[1].0)
.expect("Track locations must be neighbors")
.0;
ctx.sim.get_mut(locs[0].0).unwrap().cave.0.neighbors |=
1 << ((to_prev_idx as u8 + 4) % 8);
ctx.sim.get_mut(locs[1].0).unwrap().cave.0.neighbors |=
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
ctx.sim.get_mut(locs[2].0).unwrap().cave.0.neighbors |=
1 << ((to_next_idx as u8 + 4) % 8);
}
for loc in path.iter() {
let mut chunk = ctx.sim.get_mut(loc.0).unwrap();
let depth = loc.1 * 250.0 - 20.0;
chunk.cave.1.alt =
chunk.alt - depth + ctx.rng.gen_range(-4.0..4.0) * (depth > 10.0) as i32 as f32;
chunk.cave.1.width = ctx.rng.gen_range(6.0..32.0);
chunk.cave.0.offset = Vec2::new(ctx.rng.gen_range(-16..17), ctx.rng.gen_range(-16..17));
if chunk.cave.1.alt + chunk.cave.1.width + 5.0 > chunk.alt {
chunk.spawn_rate = 0.0;
}
}
self.caves.insert(CaveInfo {
location: (
path.first().unwrap().0 * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32),
path.last().unwrap().0 * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32),
),
name: {
let name = NameGen::location(&mut ctx.rng).generate();
match ctx.rng.gen_range(0..7) {
0 => format!("{} Hole", name),
1 => format!("{} Cavern", name),
2 => format!("{} Hollow", name),
3 => format!("{} Tunnel", name),
4 => format!("{} Mouth", name),
5 => format!("{} Grotto", name),
_ => format!("{} Den", name),
}
},
});
}
pub fn place(&self, id: Id<Place>) -> &Place { self.places.get(id) }
pub fn sites(&self) -> impl Iterator<Item = &Site> + '_ { self.sites.values() }
#[allow(dead_code)]
#[allow(clippy::print_literal)] // TODO: Pending review in #587
fn display_info(&self) {
for (id, civ) in self.civs.iter() {
println!("# Civilisation {:?}", id);
println!("Name: {}", "<unnamed>");
println!("Homeland: {:#?}", self.places.get(civ.homeland));
}
for (id, site) in self.sites.iter() {
println!("# Site {:?}", id);
println!("{:#?}", site);
}
}
/// Return the direct track between two places
fn track_between(&self, a: Id<Site>, b: Id<Site>) -> Option<Id<Track>> {
self.track_map
.get(&a)
.and_then(|dests| dests.get(&b))
.or_else(|| self.track_map.get(&b).and_then(|dests| dests.get(&a)))
.copied()
}
/// Return an iterator over a site's neighbors
fn neighbors(&self, site: Id<Site>) -> impl Iterator<Item = Id<Site>> + '_ {
let to = self
.track_map
.get(&site)
.map(|dests| dests.keys())
.into_iter()
.flatten();
let fro = self
.track_map
.iter()
.filter(move |(_, dests)| dests.contains_key(&site))
.map(|(p, _)| p);
to.chain(fro).filter(move |p| **p != site).copied()
}
/// Find the cheapest route between two places
fn route_between(&self, a: Id<Site>, b: Id<Site>) -> Option<(Path<Id<Site>>, f32)> {
let heuristic = move |p: &Id<Site>| {
(self
.sites
.get(*p)
.center
.distance_squared(self.sites.get(b).center) as f32)
.sqrt()
};
let neighbors = |p: &Id<Site>| self.neighbors(*p);
let transition =
|a: &Id<Site>, b: &Id<Site>| self.tracks.get(self.track_between(*a, *b).unwrap()).cost;
let satisfied = |p: &Id<Site>| *p == b;
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(
100,
a,
heuristic,
BuildHasherDefault::<FxHasher64>::default(),
);
astar
.poll(100, heuristic, neighbors, transition, satisfied)
.into_path()
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
}
fn birth_civ(&mut self, ctx: &mut GenCtx<impl Rng>) -> Option<Id<Civ>> {
let site = attempt(5, || {
let loc = find_site_loc(ctx, None, 1)?;
self.establish_site(ctx, loc, |place| Site {
kind: SiteKind::Settlement,
site_tmp: None,
center: loc,
place,
population: 24.0,
stocks: Stocks::from_default(100.0),
surplus: Stocks::from_default(0.0),
values: Stocks::from_default(None),
labors: MapVec::from_default(0.01),
yields: MapVec::from_default(1.0),
productivity: MapVec::from_default(1.0),
last_exports: Stocks::from_default(0.0),
export_targets: Stocks::from_default(0.0),
//trade_states: Stocks::default(),
coin: 1000.0,
})
})?;
let civ = self.civs.insert(Civ {
capital: site,
homeland: self.sites.get(site).place,
});
Some(civ)
}
fn establish_place(
&mut self,
ctx: &mut GenCtx<impl Rng>,
loc: Vec2<i32>,
area: Range<usize>,
) -> Option<Id<Place>> {
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut dead = HashSet::with_hasher(BuildHasherDefault::<FxHasher64>::default());
let mut alive = HashSet::with_hasher(BuildHasherDefault::<FxHasher64>::default());
alive.insert(loc);
// Fill the surrounding area
while let Some(cloc) = alive.iter().choose(&mut ctx.rng).copied() {
for dir in CARDINALS.iter() {
if site_in_dir(&ctx.sim, cloc, *dir) {
let rloc = cloc + *dir;
if !dead.contains(&rloc)
&& ctx
.sim
.get(rloc)
.map(|c| c.place.is_none())
.unwrap_or(false)
{
alive.insert(rloc);
}
}
}
alive.remove(&cloc);
dead.insert(cloc);
if dead.len() + alive.len() >= area.end {
break;
}
}
// Make sure the place is large enough
if dead.len() + alive.len() <= area.start {
return None;
}
let place = self.places.insert(Place {
center: loc,
nat_res: NaturalResources::default(),
});
// Write place to map
for cell in dead.union(&alive) {
if let Some(chunk) = ctx.sim.get_mut(*cell) {
chunk.place = Some(place);
self.places.get_mut(place).nat_res.include_chunk(ctx, *cell);
}
}
Some(place)
}
fn establish_site(
&mut self,
ctx: &mut GenCtx<impl Rng>,
loc: Vec2<i32>,
site_fn: impl FnOnce(Id<Place>) -> Site,
) -> Option<Id<Site>> {
const SITE_AREA: Range<usize> = 1..4; //64..256;
let place = match ctx.sim.get(loc).and_then(|site| site.place) {
Some(place) => place,
None => self.establish_place(ctx, loc, SITE_AREA)?,
};
let site = self.sites.insert(site_fn(place));
// Find neighbors
const MAX_NEIGHBOR_DISTANCE: f32 = 2000.0;
let mut nearby = self
.sites
.iter()
.filter(|(_, p)| matches!(p.kind, SiteKind::Settlement | SiteKind::Castle))
.map(|(id, p)| (id, (p.center.distance_squared(loc) as f32).sqrt()))
.filter(|(_, dist)| *dist < MAX_NEIGHBOR_DISTANCE)
.collect::<Vec<_>>();
nearby.sort_by_key(|(_, dist)| *dist as i32);
if let SiteKind::Settlement | SiteKind::Castle = self.sites[site].kind {
for (nearby, _) in nearby.into_iter().take(5) {
// Find a novel path
if let Some((path, cost)) = find_path(ctx, loc, self.sites.get(nearby).center) {
// Find a path using existing paths
if self
.route_between(site, nearby)
// If the novel path isn't efficient compared to existing routes, don't use it
.filter(|(_, route_cost)| *route_cost < cost * 3.0)
.is_none()
{
// Write the track to the world as a path
for locs in path.nodes().windows(3) {
let to_prev_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[0] - locs[1])
.expect("Track locations must be neighbors")
.0;
let to_next_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[2] - locs[1])
.expect("Track locations must be neighbors")
.0;
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
1 << ((to_prev_idx as u8 + 4) % 8);
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1 << ((to_next_idx as u8 + 4) % 8);
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.0.neighbors |=
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
chunk.path.0.offset =
Vec2::new(ctx.rng.gen_range(-16..17), ctx.rng.gen_range(-16..17));
}
// Take note of the track
let track = self.tracks.insert(Track { cost, path });
self.track_map
.entry(site)
.or_default()
.insert(nearby, track);
}
}
}
}
Some(site)
}
fn tick(&mut self, _ctx: &mut GenCtx<impl Rng>, years: f32) {
for site in self.sites.values_mut() {
site.simulate(years, &self.places.get(site.place).nat_res);
}
// Trade stocks
// let mut stocks = TRADE_STOCKS;
// stocks.shuffle(ctx.rng); // Give each stock a chance to be traded
// first for stock in stocks.iter().copied() {
// let mut sell_orders = self.sites
// .iter_ids()
// .map(|(id, site)| (id, {
// econ::SellOrder {
// quantity:
// site.export_targets[stock].max(0.0).min(site.stocks[stock]),
// price:
// site.trade_states[stock].sell_belief.choose_price(ctx) * 1.25, //
// Trade cost q_sold: 0.0,
// }
// }))
// .filter(|(_, order)| order.quantity > 0.0)
// .collect::<Vec<_>>();
// let mut sites = self.sites
// .ids()
// .collect::<Vec<_>>();
// sites.shuffle(ctx.rng); // Give all sites a chance to buy first
// for site in sites {
// let (max_spend, max_price, max_import) = {
// let site = self.sites.get(site);
// let budget = site.coin * 0.5;
// let total_value = site.values.iter().map(|(_, v)|
// (*v).unwrap_or(0.0)).sum::<f32>(); (
// 100000.0,//(site.values[stock].unwrap_or(0.1) /
// total_value * budget).min(budget),
// site.trade_states[stock].buy_belief.price,
// -site.export_targets[stock].min(0.0), )
// };
// let (quantity, spent) = econ::buy_units(ctx, sell_orders
// .iter_mut()
// .filter(|(id, _)| site != *id && self.track_between(site,
// *id).is_some()) .map(|(_, order)| order),
// max_import,
// 1000000.0, // Max price TODO
// max_spend,
// );
// let mut site = self.sites.get_mut(site);
// site.coin -= spent;
// if quantity > 0.0 {
// site.stocks[stock] += quantity;
// site.last_exports[stock] = -quantity;
// site.trade_states[stock].buy_belief.update_buyer(years,
// spent / quantity); println!("Belief: {:?}",
// site.trade_states[stock].buy_belief); }
// }
// for (site, order) in sell_orders {
// let mut site = self.sites.get_mut(site);
// site.coin += order.q_sold * order.price;
// if order.q_sold > 0.0 {
// site.stocks[stock] -= order.q_sold;
// site.last_exports[stock] = order.q_sold;
//
// site.trade_states[stock].sell_belief.update_seller(order.q_sold /
// order.quantity); }
// }
// }
}
}
/// Attempt to find a path between two locations
fn find_path(
ctx: &mut GenCtx<impl Rng>,
a: Vec2<i32>,
b: Vec2<i32>,
) -> Option<(Path<Vec2<i32>>, f32)> {
const MAX_PATH_ITERS: usize = 100_000;
let sim = &ctx.sim;
let heuristic = move |l: &Vec2<i32>| (l.distance_squared(b) as f32).sqrt();
let neighbors = |l: &Vec2<i32>| {
let l = *l;
NEIGHBORS
.iter()
.filter(move |dir| walk_in_dir(sim, l, **dir).is_some())
.map(move |dir| l + *dir)
};
let transition =
|a: &Vec2<i32>, b: &Vec2<i32>| 1.0 + walk_in_dir(sim, *a, *b - *a).unwrap_or(10000.0);
let satisfied = |l: &Vec2<i32>| *l == b;
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(
MAX_PATH_ITERS,
a,
heuristic,
BuildHasherDefault::<FxHasher64>::default(),
);
astar
.poll(MAX_PATH_ITERS, heuristic, neighbors, transition, satisfied)
.into_path()
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
}
/// Return Some if travel between a location and a chunk next to it is permitted
/// If permitted, the approximate relative const of traversal is given
// (TODO: by whom?)
fn walk_in_dir(sim: &WorldSim, a: Vec2<i32>, dir: Vec2<i32>) -> Option<f32> {
if loc_suitable_for_walking(sim, a) && loc_suitable_for_walking(sim, a + dir) {
let a_chunk = sim.get(a)?;
let b_chunk = sim.get(a + dir)?;
let hill_cost = ((b_chunk.alt - a_chunk.alt).abs() / 5.0).powi(2);
let water_cost = if b_chunk.river.near_water() {
50.0
} else {
0.0
} + (b_chunk.water_alt - b_chunk.alt + 8.0).clamped(0.0, 8.0) * 3.0; // Try not to path swamps / tidal areas
let wild_cost = if b_chunk.path.0.is_way() {
0.0 // Traversing existing paths has no additional cost!
} else {
3.0 // + (1.0 - b_chunk.tree_density) * 20.0 // Prefer going through forests, for aesthetics
};
Some(1.0 + hill_cost + water_cost + wild_cost)
} else {
None
}
}
/// Return true if a position is suitable for walking on
fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
if let Some(chunk) = sim.get(loc) {
!chunk.river.is_ocean() && !chunk.river.is_lake() && !chunk.near_cliffs()
} else {
false
}
}
/// Return true if a site could be constructed between a location and a chunk
/// next to it is permitted (TODO: by whom?)
fn site_in_dir(sim: &WorldSim, a: Vec2<i32>, dir: Vec2<i32>) -> bool {
loc_suitable_for_site(sim, a) && loc_suitable_for_site(sim, a + dir)
}
/// Return true if a position is suitable for site construction (TODO:
/// criteria?)
fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2<i32>) -> bool {
if let Some(chunk) = sim.get(loc) {
!chunk.river.is_ocean()
&& !chunk.river.is_lake()
&& !chunk.river.is_river()
&& sim
.get_gradient_approx(loc)
.map(|grad| grad < 1.0)
.unwrap_or(false)
} else {
false
}
}
/// Attempt to search for a location that's suitable for site construction
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
fn find_site_loc(
ctx: &mut GenCtx<impl Rng>,
near: Option<(Vec2<i32>, f32)>,
size: i32,
) -> Option<Vec2<i32>> {
const MAX_ATTEMPTS: usize = 100;
let mut loc = None;
for _ in 0..MAX_ATTEMPTS {
let test_loc = loc.unwrap_or_else(|| match near {
Some((origin, dist)) => {
origin
+ (Vec2::new(ctx.rng.gen_range(-1.0..1.0), ctx.rng.gen_range(-1.0..1.0))
.try_normalized()
.unwrap_or(Vec2::zero())
* ctx.rng.gen::<f32>()
* dist)
.map(|e| e as i32)
},
None => Vec2::new(
ctx.rng.gen_range(0..ctx.sim.get_size().x as i32),
ctx.rng.gen_range(0..ctx.sim.get_size().y as i32),
),
});
for offset in Spiral2d::new().take((size * 2 + 1).pow(2) as usize) {
if loc_suitable_for_site(&ctx.sim, test_loc + offset) {
return Some(test_loc);
}
}
loc = ctx.sim.get(test_loc).and_then(|c| {
Some(
c.downhill?
.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / (sz as i32)),
)
});
}
None
}
#[derive(Debug)]
pub struct Civ {
capital: Id<Site>,
homeland: Id<Place>,
}
#[derive(Debug)]
pub struct Place {
center: Vec2<i32>,
nat_res: NaturalResources,
}
// Productive capacity per year
#[derive(Default, Debug)]
pub struct NaturalResources {
wood: f32,
rock: f32,
river: f32,
farmland: f32,
}
impl NaturalResources {
fn include_chunk(&mut self, ctx: &mut GenCtx<impl Rng>, loc: Vec2<i32>) {
let chunk = if let Some(chunk) = ctx.sim.get(loc) {
chunk
} else {
return;
};
self.wood += chunk.tree_density;
self.rock += chunk.rockiness;
self.river += if chunk.river.is_river() { 5.0 } else { 0.0 };
self.farmland += if chunk.humidity > 0.35
&& chunk.temp > -0.3
&& chunk.temp < 0.75
&& chunk.chaos < 0.5
&& ctx
.sim
.get_gradient_approx(loc)
.map(|grad| grad < 0.7)
.unwrap_or(false)
{
1.0
} else {
0.0
};
}
}
pub struct Track {
/// Cost of using this track relative to other paths. This cost is an
/// arbitrary unit and doesn't make sense unless compared to other track
/// costs.
cost: f32,
path: Path<Vec2<i32>>,
}
#[derive(Debug)]
pub struct Site {
pub kind: SiteKind,
// TODO: Remove this field when overhauling
pub site_tmp: Option<Id<crate::site::Site>>,
pub center: Vec2<i32>,
pub place: Id<Place>,
population: f32,
// Total amount of each stock
stocks: Stocks<f32>,
// Surplus stock compared to demand orders
surplus: Stocks<f32>,
// For some goods, such a goods without any supply, it doesn't make sense to talk about value
values: Stocks<Option<f32>>,
// Proportion of individuals dedicated to an industry
labors: MapVec<Occupation, f32>,
// Per worker, per year, of their output good
yields: MapVec<Occupation, f32>,
productivity: MapVec<Occupation, f32>,
last_exports: Stocks<f32>,
export_targets: Stocks<f32>,
//trade_states: Stocks<TradeState>,
coin: f32,
}
impl fmt::Display for Site {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{:?}", self.kind)?;
writeln!(f, "- population: {}", self.population.floor() as u32)?;
writeln!(f, "- coin: {}", self.coin.floor() as u32)?;
writeln!(f, "Stocks")?;
for (stock, q) in self.stocks.iter() {
writeln!(f, "- {:?}: {}", stock, q.floor())?;
}
writeln!(f, "Values")?;
for stock in TRADE_STOCKS.iter() {
writeln!(
f,
"- {:?}: {}",
stock,
self.values[*stock]
.map(|x| x.to_string())
.unwrap_or_else(|| "N/A".to_string())
)?;
}
writeln!(f, "Laborers")?;
for (labor, n) in self.labors.iter() {
writeln!(
f,
"- {:?}: {}",
labor,
(*n * self.population).floor() as u32
)?;
}
writeln!(f, "Export targets")?;
for (stock, n) in self.export_targets.iter() {
writeln!(f, "- {:?}: {}", stock, n)?;
}
Ok(())
}
}
#[derive(Debug)]
pub enum SiteKind {
Settlement,
Dungeon,
Castle,
Refactor,
Tree,
}
impl Site {
#[allow(clippy::let_and_return)] // TODO: Pending review in #587
pub fn simulate(&mut self, years: f32, nat_res: &NaturalResources) {
// Insert natural resources into the economy
if self.stocks[Fish] < nat_res.river {
self.stocks[Fish] = nat_res.river;
}
if self.stocks[Wheat] < nat_res.farmland {
self.stocks[Wheat] = nat_res.farmland;
}
if self.stocks[Logs] < nat_res.wood {
self.stocks[Logs] = nat_res.wood;
}
if self.stocks[Game] < nat_res.wood {
self.stocks[Game] = nat_res.wood;
}
if self.stocks[Rock] < nat_res.rock {
self.stocks[Rock] = nat_res.rock;
}
// We use this hasher (FxHasher32) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 1-byte keys (for which FxHash is supposedly fastest).
let orders = vec![
(None, vec![(Food, 0.5)]),
(Some(Cook), vec![(Flour, 16.0), (Meat, 4.0), (Wood, 3.0)]),
(Some(Lumberjack), vec![(Logs, 4.5)]),
(Some(Miner), vec![(Rock, 7.5)]),
(Some(Fisher), vec![(Fish, 4.0)]),
(Some(Hunter), vec![(Game, 4.0)]),
(Some(Farmer), vec![(Wheat, 4.0)]),
]
.into_iter()
.collect::<HashMap<_, Vec<(Stock, f32)>, BuildHasherDefault<FxHasher32>>>();
// Per labourer, per year
let production = MapVec::from_list(
&[
(Farmer, (Flour, 2.0)),
(Lumberjack, (Wood, 1.5)),
(Miner, (Stone, 0.6)),
(Fisher, (Meat, 3.0)),
(Hunter, (Meat, 0.25)),
(Cook, (Food, 20.0)),
],
(Rock, 0.0),
);
let mut demand = Stocks::from_default(0.0);
for (labor, orders) in &orders {
let scale = if let Some(labor) = labor {
self.labors[*labor]
} else {
1.0
} * self.population;
for (stock, amount) in orders {
demand[*stock] += *amount * scale;
}
}
let mut supply = Stocks::from_default(0.0);
for (labor, (output_stock, _)) in production.iter() {
supply[*output_stock] += self.yields[labor] * self.labors[labor] * self.population;
}
let last_exports = &self.last_exports;
let stocks = &self.stocks;
self.surplus = demand
.clone()
.map(|stock, _| supply[stock] + stocks[stock] - demand[stock] - last_exports[stock]);
// Update values according to the surplus of each stock
let values = &mut self.values;
self.surplus.iter().for_each(|(stock, surplus)| {
let val = 3.5f32.powf(1.0 - *surplus / demand[stock]);
values[stock] = if val > 0.001 && val < 1000.0 {
Some(val)
} else {
None
};
});
// Update export targets based on relative values
let value_avg = values
.iter()
.map(|(_, v)| (*v).unwrap_or(0.0))
.sum::<f32>()
.max(0.01)
/ values.iter().filter(|(_, v)| v.is_some()).count() as f32;
let export_targets = &mut self.export_targets;
let last_exports = &self.last_exports;
self.values.iter().for_each(|(stock, value)| {
let rvalue = (*value).map(|v| v - value_avg).unwrap_or(0.0);
//let factor = if export_targets[stock] > 0.0 { 1.0 / rvalue } else { rvalue };
export_targets[stock] = last_exports[stock] - rvalue * 0.1; // + (trade_states[stock].sell_belief.price - trade_states[stock].buy_belief.price) * 0.025;
});
let population = self.population;
// Redistribute workforce according to relative good values
let labor_ratios = production.clone().map(|labor, (output_stock, _)| {
self.productivity[labor] * demand[output_stock] / supply[output_stock].max(0.001)
});
let labor_ratio_sum = labor_ratios.iter().map(|(_, r)| *r).sum::<f32>().max(0.01);
production.iter().for_each(|(labor, _)| {
let smooth = 0.8;
self.labors[labor] = smooth * self.labors[labor]
+ (1.0 - smooth)
* (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum);
});
// Production
let stocks_before = self.stocks.clone();
for (labor, orders) in orders.iter() {
let scale = if let Some(labor) = labor {
self.labors[*labor]
} else {
1.0
} * population;
// For each order, we try to find the minimum satisfaction rate - this limits
// how much we can produce! For example, if we need 0.25 fish and
// 0.75 oats to make 1 unit of food, but only 0.5 units of oats are
// available then we only need to consume 2/3rds
// of other ingredients and leave the rest in stock
// In effect, this is the productivity
let productivity = orders
.iter()
.map(|(stock, amount)| {
// What quantity is this order requesting?
let _quantity = *amount * scale;
// What proportion of this order is the economy able to satisfy?
let satisfaction = (stocks_before[*stock] / demand[*stock]).min(1.0);
satisfaction
})
.min_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or_else(|| {
panic!("Industry {:?} requires at least one input order", labor)
});
for (stock, amount) in orders {
// What quantity is this order requesting?
let quantity = *amount * scale;
// What amount gets actually used in production?
let used = quantity * productivity;
// Deplete stocks accordingly
self.stocks[*stock] = (self.stocks[*stock] - used).max(0.0);
}
// Industries produce things
if let Some(labor) = labor {
let (stock, rate) = production[*labor];
let workers = self.labors[*labor] * population;
let final_rate = rate;
let yield_per_worker = productivity * final_rate;
self.yields[*labor] = yield_per_worker;
self.productivity[*labor] = productivity;
self.stocks[stock] += yield_per_worker * workers.powf(1.1);
}
}
// Denature stocks
self.stocks.iter_mut().for_each(|(_, v)| *v *= 0.9);
// Births/deaths
const NATURAL_BIRTH_RATE: f32 = 0.15;
const DEATH_RATE: f32 = 0.05;
let birth_rate = if self.surplus[Food] > 0.0 {
NATURAL_BIRTH_RATE
} else {
0.0
};
self.population += years * self.population * (birth_rate - DEATH_RATE);
}
pub fn is_dungeon(&self) -> bool { matches!(self.kind, SiteKind::Dungeon) }
pub fn is_settlement(&self) -> bool { matches!(self.kind, SiteKind::Settlement) }
pub fn is_castle(&self) -> bool { matches!(self.kind, SiteKind::Castle) }
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Occupation {
Farmer = 0,
Lumberjack = 1,
Miner = 2,
Fisher = 3,
Hunter = 4,
Cook = 5,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Stock {
Wheat = 0,
Flour = 1,
Meat = 2,
Fish = 3,
Game = 4,
Food = 5,
Logs = 6,
Wood = 7,
Rock = 8,
Stone = 9,
}
const TRADE_STOCKS: [Stock; 5] = [Flour, Meat, Food, Wood, Stone];
#[derive(Debug, Clone)]
struct TradeState {
buy_belief: econ::Belief,
sell_belief: econ::Belief,
}
impl Default for TradeState {
fn default() -> Self {
Self {
buy_belief: econ::Belief {
price: 1.0,
confidence: 0.25,
},
sell_belief: econ::Belief {
price: 1.0,
confidence: 0.25,
},
}
}
}
pub type Stocks<T> = MapVec<Stock, T>;