diff --git a/.gitignore b/.gitignore index e7bbbdc07d..3bc13c3292 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ img-export/**/*.png nix/result* /result* /shell.nix + +# Bash +.history diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 8397cf46c6..a895506e0f 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -120,6 +120,47 @@ impl ProximityRequirements { all_of_compliance && any_of_compliance } + pub fn location_hint(&self, world_dims: &Aabr) -> Aabr { + let bounding_box_of_point = |point: Vec2, max_distance: i32| Aabr { + min: Vec2 { + x: point.x - max_distance, + y: point.y - max_distance, + }, + max: Vec2 { + x: point.x + max_distance, + y: point.y + max_distance, + }, + }; + let any_of_hint = self + .any_of + .iter() + .fold(None, |acc, spec| match spec.max_distance { + None => acc, + Some(max_distance) => { + let bounding_box_of_new_point = + bounding_box_of_point(spec.location, max_distance); + match acc { + None => Some(bounding_box_of_new_point), + Some(acc) => Some(acc.union(bounding_box_of_new_point)), + } + }, + }) + .map(|hint| hint.intersection(*world_dims)) + .unwrap_or_else(|| world_dims.to_owned()); + let hint = self + .all_of + .iter() + .fold(any_of_hint, |acc, spec| match spec.max_distance { + None => acc, + Some(max_distance) => { + let bounding_box_of_new_point = + bounding_box_of_point(spec.location, max_distance); + acc.intersection(bounding_box_of_new_point) + }, + }); + hint + } + pub fn new() -> Self { ProximityRequirements { all_of: Vec::new(), @@ -1452,30 +1493,6 @@ fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2) -> bool { } } -/// Return true if a position is suitable for site construction (TODO: -/// criteria?) -fn loc_suitable_for_site( - sim: &WorldSim, - loc: Vec2, - site_kind: SiteKind, - is_suitable_loc: bool, -) -> bool { - fn check_chunk_occupation(sim: &WorldSim, loc: Vec2, radius: i32) -> bool { - for x in (-radius)..radius { - for y in (-radius)..radius { - let check_loc = loc + Vec2::new(x, y); - if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) { - return false; - } - } - } - true - } - let not_occupied = || check_chunk_occupation(sim, loc, site_kind.exclusion_radius()); - // only check occupation if the location is suitable - is_suitable_loc && not_occupied() -} - /// Attempt to search for a location that's suitable for site construction fn find_site_loc( ctx: &mut GenCtx, @@ -1486,16 +1503,24 @@ fn find_site_loc( const MAX_ATTEMPTS: usize = 10000; let mut loc = None; for _ in 0..MAX_ATTEMPTS { + let world_dims = Aabr { + min: Vec2 { x: 0, y: 0 }, + max: Vec2 { + x: ctx.sim.get_size().x as i32, + y: ctx.sim.get_size().y as i32, + }, + }; + let location_hint = proximity_reqs.location_hint(&world_dims); let test_loc = loc.unwrap_or_else(|| { 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), + ctx.rng.gen_range(location_hint.min.x..location_hint.max.x), + ctx.rng.gen_range(location_hint.min.y..location_hint.max.y), ) }); let is_suitable_loc = site_kind.is_suitable_loc(test_loc, ctx.sim); if is_suitable_loc && proximity_reqs.satisfied_by(test_loc) { - if loc_suitable_for_site(ctx.sim, test_loc, site_kind, is_suitable_loc) { + if site_kind.exclusion_radius_clear(ctx.sim, test_loc) { return Some(test_loc); } @@ -1509,6 +1534,110 @@ fn find_site_loc( None } +fn town_attributes_of_site(loc: Vec2, sim: &WorldSim) -> Option { + sim.get(loc).map(|chunk| { + const RESOURCE_RADIUS: i32 = 1; + let mut river_chunks = 0; + let mut lake_chunks = 0; + let mut ocean_chunks = 0; + let mut rock_chunks = 0; + let mut tree_chunks = 0; + let mut farmable_chunks = 0; + let mut farmable_needs_irrigation_chunks = 0; + let mut land_chunks = 0; + for x in (-RESOURCE_RADIUS)..RESOURCE_RADIUS { + for y in (-RESOURCE_RADIUS)..RESOURCE_RADIUS { + let check_loc = loc + Vec2::new(x, y).cpos_to_wpos(); + sim.get(check_loc).map(|c| { + if num::abs(chunk.alt - c.alt) < 200.0 { + if c.river.is_river() { + river_chunks += 1; + } + if c.river.is_lake() { + lake_chunks += 1; + } + if c.river.is_ocean() { + ocean_chunks += 1; + } + if c.tree_density > 0.7 { + tree_chunks += 1; + } + if c.rockiness < 0.3 && c.temp > CONFIG.snow_temp { + if c.surface_veg > 0.5 { + farmable_chunks += 1; + } else { + match c.get_biome() { + common::terrain::BiomeKind::Savannah => { + farmable_needs_irrigation_chunks += 1 + }, + common::terrain::BiomeKind::Desert => { + farmable_needs_irrigation_chunks += 1 + }, + _ => (), + } + } + } + if !c.river.is_river() && !c.river.is_lake() && !c.river.is_ocean() { + land_chunks += 1; + } + } + // Mining is different since presumably you dig into the hillside + if c.rockiness > 0.7 && c.alt - chunk.alt > -10.0 { + rock_chunks += 1; + } + }); + } + } + let has_river = river_chunks > 1; + let has_lake = lake_chunks > 1; + let vegetation_implies_potable_water = chunk.tree_density > 0.4 + && !matches!(chunk.get_biome(), common::terrain::BiomeKind::Swamp); + let warm_or_firewood = chunk.temp > CONFIG.snow_temp || tree_chunks > 2; + let has_potable_water = + { has_river || (has_lake && chunk.alt > 100.0) || vegetation_implies_potable_water }; + let has_building_materials = tree_chunks > 0 + || rock_chunks > 0 + || chunk.temp > CONFIG.tropical_temp && (has_river || has_lake); + let water_rich = lake_chunks + river_chunks > 2; + let can_grow_rice = water_rich + && chunk.humidity + 1.0 > CONFIG.jungle_hum + && chunk.temp + 1.0 > CONFIG.tropical_temp; + let farming_score = if can_grow_rice { + farmable_chunks * 2 + } else { + farmable_chunks + } + if water_rich { + farmable_needs_irrigation_chunks + } else { + 0 + }; + let fish_score = lake_chunks + ocean_chunks; + let food_score = farming_score + fish_score; + let mining_score = if tree_chunks > 1 { rock_chunks } else { 0 }; + let forestry_score = if has_river { tree_chunks } else { 0 }; + let trading_score = std::cmp::min(std::cmp::min(land_chunks, ocean_chunks), river_chunks); + TownSiteAttributes { + food_score, + mining_score, + forestry_score, + trading_score, + heating: warm_or_firewood, + potable_water: has_potable_water, + building_materials: has_building_materials, + } + }) +} + +pub struct TownSiteAttributes { + food_score: i32, + mining_score: i32, + forestry_score: i32, + trading_score: i32, + heating: bool, + potable_water: bool, + building_materials: bool, +} + #[derive(Debug)] pub struct Civ { capital: Id, @@ -1594,99 +1723,19 @@ impl SiteKind { sim.get(loc).map_or(false, |chunk| { let suitable_for_town = |score_threshold: f32| -> bool { - const RESOURCE_RADIUS: i32 = 1; - let mut river_chunks = 0; - let mut lake_chunks = 0; - let mut ocean_chunks = 0; - let mut rock_chunks = 0; - let mut tree_chunks = 0; - let mut farmable_chunks = 0; - let mut farmable_needs_irrigation_chunks = 0; - let mut land_chunks = 0; - for x in (-RESOURCE_RADIUS)..RESOURCE_RADIUS { - for y in (-RESOURCE_RADIUS)..RESOURCE_RADIUS { - let check_loc = loc + Vec2::new(x, y).cpos_to_wpos(); - sim.get(check_loc).map(|c| { - if num::abs(chunk.alt - c.alt) < 200.0 { - if c.river.is_river() { - river_chunks += 1; - } - if c.river.is_lake() { - lake_chunks += 1; - } - if c.river.is_ocean() { - ocean_chunks += 1; - } - if c.tree_density > 0.7 { - tree_chunks += 1; - } - if c.rockiness < 0.3 && c.temp > CONFIG.snow_temp { - if c.surface_veg > 0.5 { - farmable_chunks += 1; - } else { - match c.get_biome() { - common::terrain::BiomeKind::Savannah => { - farmable_needs_irrigation_chunks += 1 - }, - common::terrain::BiomeKind::Desert => { - farmable_needs_irrigation_chunks += 1 - }, - _ => (), - } - } - } - if !c.river.is_river() && !c.river.is_lake() && !c.river.is_ocean() - { - land_chunks += 1; - } - } - // Mining is different since presumably you dig into the hillside - if c.rockiness > 0.7 && c.alt - chunk.alt > -10.0 { - rock_chunks += 1; - } - }); - } - } - let has_river = river_chunks > 1; - let has_lake = lake_chunks > 1; - let vegetation_implies_potable_water = chunk.tree_density > 0.4 - && !matches!(chunk.get_biome(), common::terrain::BiomeKind::Swamp); - let warm_or_firewood = chunk.temp > CONFIG.snow_temp || tree_chunks > 2; - let has_potable_water = { - has_river || (has_lake && chunk.alt > 100.0) || vegetation_implies_potable_water - }; - let has_building_materials = tree_chunks > 0 - || rock_chunks > 0 - || chunk.temp > CONFIG.tropical_temp && (has_river || has_lake); - let water_rich = lake_chunks + river_chunks > 2; - let can_grow_rice = water_rich - && chunk.humidity + 1.0 > CONFIG.jungle_hum - && chunk.temp + 1.0 > CONFIG.tropical_temp; - let farming_score = if can_grow_rice { - farmable_chunks * 2 - } else { - farmable_chunks - } + if water_rich { - farmable_needs_irrigation_chunks - } else { - 0 - }; - let fish_score = lake_chunks + ocean_chunks; - let food_score = farming_score + fish_score; - let mining_score = if tree_chunks > 1 { rock_chunks } else { 0 }; - let forestry_score = if has_river { tree_chunks } else { 0 }; - let trading_score = - std::cmp::min(std::cmp::min(land_chunks, ocean_chunks), river_chunks); - let industry_score = 3.0 * (food_score as f32 + 1.0).log2() - + 2.0 * (forestry_score as f32 + 1.0).log2() - + (mining_score as f32 + 1.0).log2() - + (trading_score as f32 + 1.0).log2(); - has_potable_water - && has_building_materials - && industry_score > score_threshold - && warm_or_firewood - // Because of how the algorithm for site2 towns work, they have to start on land. - && on_land() + let attributes = town_attributes_of_site(loc, sim); + attributes.map_or(false, |attr| { + let industry_score = 3.0 * (attr.food_score as f32 + 1.0).log2() + + 2.0 * (attr.forestry_score as f32 + 1.0).log2() + + (attr.mining_score as f32 + 1.0).log2() + + (attr.trading_score as f32 + 1.0).log2(); + attr.potable_water + && attr.building_materials + && industry_score > score_threshold + && attr.heating + // Because of how the algorithm for site2 towns work, they have to start on land. + && on_land() + }) }; match self { SiteKind::Gnarling => { @@ -1756,9 +1805,7 @@ impl SiteKind { } }) } -} -impl SiteKind { pub fn exclusion_radius(&self) -> i32 { // FIXME: Provide specific values for each individual SiteKind match self { @@ -1766,6 +1813,19 @@ impl SiteKind { _ => 8, // This is just an arbitrary value } } + + pub fn exclusion_radius_clear(&self, sim: &WorldSim, loc: Vec2) -> bool { + let radius = self.exclusion_radius(); + for x in (-radius)..radius { + for y in (-radius)..radius { + let check_loc = loc + Vec2::new(x, y); + if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) { + return false; + } + } + } + true + } } impl Site { @@ -1827,4 +1887,31 @@ mod tests { assert!(reqs.satisfied_by(Vec2 { x: 1, y: -1 })); assert!(!reqs.satisfied_by(Vec2 { x: -8, y: 8 })); } + + #[test] + fn complex_proximity_requirements() { + let a_site = Vec2 { x: 572, y: 724 }; + let reqs = ProximityRequirements::new() + .close_to_one_of(vec![a_site].into_iter(), 60) + .avoid_all_of(vec![a_site].into_iter(), 40); + assert!(reqs.satisfied_by(Vec2 { x: 572, y: 774 })); + assert!(!reqs.satisfied_by(a_site)); + } + + #[test] + fn location_hint() { + let reqs = ProximityRequirements::new().close_to_one_of( + vec![Vec2 { x: 1, y: 0 }, Vec2 { x: 13, y: 12 }].into_iter(), + 10, + ); + let expected = Aabr { + min: Vec2 { x: 0, y: 0 }, + max: Vec2 { x: 23, y: 22 }, + }; + let map_dims = Aabr { + min: Vec2 { x: 0, y: 0 }, + max: Vec2 { x: 200, y: 300 }, + }; + assert_eq!(expected, reqs.location_hint(&map_dims)); + } }