From d607ea16837092cad561c87d81fd687112dd26a1 Mon Sep 17 00:00:00 2001 From: "Tormod G. Hellen" Date: Mon, 15 May 2023 15:44:37 +0200 Subject: [PATCH 1/7] Git ignore local bash history. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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 From 057c5022187dca8ed3e0ffa974c9743ff1d24994 Mon Sep 17 00:00:00 2001 From: "Tormod G. Hellen" Date: Mon, 28 Nov 2022 21:50:37 +0100 Subject: [PATCH 2/7] Factor out town attribute calculation. --- world/src/civ/mod.rs | 210 ++++++++++++++++++++++++------------------- 1 file changed, 117 insertions(+), 93 deletions(-) diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 8397cf46c6..5efe878d4e 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -1509,6 +1509,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 +1698,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 => { From 1572a5a759dcd6b9cd30922fce26596f56d7ef0e Mon Sep 17 00:00:00 2001 From: "Tormod G. Hellen" Date: Tue, 16 May 2023 12:55:39 +0200 Subject: [PATCH 3/7] Add location hint optimization. --- world/src/civ/mod.rs | 87 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 5efe878d4e..282ca7c31e 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -112,6 +112,14 @@ struct ProximityRequirements { any_of: Vec, } +#[derive(Debug, PartialEq, Clone)] +struct SquareLocationRestriction { + pub min_x: i32, + pub max_x: i32, + pub min_y: i32, + pub max_y: i32, +} + impl ProximityRequirements { pub fn satisfied_by(&self, site: Vec2) -> bool { let all_of_compliance = self.all_of.iter().all(|spec| spec.satisfied_by(site)); @@ -120,6 +128,53 @@ impl ProximityRequirements { all_of_compliance && any_of_compliance } + pub fn location_hint( + &self, + world_dims: &SquareLocationRestriction, + ) -> SquareLocationRestriction { + use std::cmp::{max, min}; + let any_of_hint = self + .any_of + .iter() + .fold(None, |acc, spec| match spec.max_distance { + None => acc, + Some(max_distance) => match acc { + None => Some(SquareLocationRestriction { + min_x: spec.location.x - max_distance, + max_x: spec.location.x + max_distance, + min_y: spec.location.y - max_distance, + max_y: spec.location.y + max_distance, + }), + Some(acc) => Some(SquareLocationRestriction { + min_x: min(acc.min_x, spec.location.x - max_distance), + max_x: max(acc.max_x, spec.location.x + max_distance), + min_y: min(acc.min_y, spec.location.y - max_distance), + max_y: max(acc.max_y, spec.location.y + max_distance), + }), + }, + }) + .map(|hint| SquareLocationRestriction { + min_x: max(world_dims.min_x, hint.min_x), + max_x: min(world_dims.max_x, hint.max_x), + min_y: max(world_dims.min_y, hint.min_y), + max_y: min(world_dims.max_y, hint.max_y), + }) + .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) => SquareLocationRestriction { + min_x: max(acc.min_x, spec.location.x - max_distance), + max_x: min(acc.max_x, spec.location.x + max_distance), + min_y: max(acc.min_y, spec.location.y - max_distance), + max_y: min(acc.max_y, spec.location.y + max_distance), + }, + }); + hint + } + pub fn new() -> Self { ProximityRequirements { all_of: Vec::new(), @@ -1486,10 +1541,17 @@ fn find_site_loc( const MAX_ATTEMPTS: usize = 10000; let mut loc = None; for _ in 0..MAX_ATTEMPTS { + let world_dims = SquareLocationRestriction { + min_x: 0, + max_x: ctx.sim.get_size().x as i32, + min_y: 0, + max_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), ) }); @@ -1851,4 +1913,25 @@ mod tests { assert!(reqs.satisfied_by(Vec2 { x: 1, y: -1 })); assert!(!reqs.satisfied_by(Vec2 { x: -8, y: 8 })); } + + #[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 = SquareLocationRestriction { + min_x: 0, + max_x: 23, + min_y: 0, + max_y: 22, + }; + let map_dims = SquareLocationRestriction { + min_x: 0, + max_x: 200, + min_y: 0, + max_y: 300, + }; + assert_eq!(expected, reqs.location_hint(&map_dims)); + } } From 2397209b5d29e1f511a622777cb1f8b4467a3816 Mon Sep 17 00:00:00 2001 From: "Tormod G. Hellen" Date: Tue, 16 May 2023 16:48:28 +0200 Subject: [PATCH 4/7] Add proximity test. --- world/src/civ/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 282ca7c31e..5ff79f2b3e 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -1914,6 +1914,16 @@ mod tests { 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( From a58b589c1a43c596d02c497f13740358ff8d62e7 Mon Sep 17 00:00:00 2001 From: "Tormod G. Hellen" Date: Mon, 22 May 2023 16:26:30 +0200 Subject: [PATCH 5/7] Consolidate SiteKind impl. --- world/src/civ/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 5ff79f2b3e..d0f830ec33 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -1842,9 +1842,7 @@ impl SiteKind { } }) } -} -impl SiteKind { pub fn exclusion_radius(&self) -> i32 { // FIXME: Provide specific values for each individual SiteKind match self { From af080016ab21285579d98759d453b44a4c153f98 Mon Sep 17 00:00:00 2001 From: "Tormod G. Hellen" Date: Mon, 22 May 2023 16:35:12 +0200 Subject: [PATCH 6/7] Get rid of the old and clunky loc_suitable_for_site function. --- world/src/civ/mod.rs | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index d0f830ec33..fb5ebeab36 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -1507,30 +1507,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, @@ -1557,7 +1533,7 @@ fn find_site_loc( 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); } @@ -1850,6 +1826,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 { From a2ecd0b4030636fcf402f968127ea6d1bd3e6476 Mon Sep 17 00:00:00 2001 From: "Tormod G. Hellen" Date: Tue, 23 May 2023 00:26:47 +0200 Subject: [PATCH 7/7] Replace SquareLocationRestriction with Aabr. This is a fixup for 1572a5a759dcd6b9cd30922fce26596f56d7ef0e --- world/src/civ/mod.rs | 91 ++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index fb5ebeab36..a895506e0f 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -112,14 +112,6 @@ struct ProximityRequirements { any_of: Vec, } -#[derive(Debug, PartialEq, Clone)] -struct SquareLocationRestriction { - pub min_x: i32, - pub max_x: i32, - pub min_y: i32, - pub max_y: i32, -} - impl ProximityRequirements { pub fn satisfied_by(&self, site: Vec2) -> bool { let all_of_compliance = self.all_of.iter().all(|spec| spec.satisfied_by(site)); @@ -128,48 +120,42 @@ impl ProximityRequirements { all_of_compliance && any_of_compliance } - pub fn location_hint( - &self, - world_dims: &SquareLocationRestriction, - ) -> SquareLocationRestriction { - use std::cmp::{max, min}; + 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) => match acc { - None => Some(SquareLocationRestriction { - min_x: spec.location.x - max_distance, - max_x: spec.location.x + max_distance, - min_y: spec.location.y - max_distance, - max_y: spec.location.y + max_distance, - }), - Some(acc) => Some(SquareLocationRestriction { - min_x: min(acc.min_x, spec.location.x - max_distance), - max_x: max(acc.max_x, spec.location.x + max_distance), - min_y: min(acc.min_y, spec.location.y - max_distance), - max_y: max(acc.max_y, spec.location.y + max_distance), - }), + 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| SquareLocationRestriction { - min_x: max(world_dims.min_x, hint.min_x), - max_x: min(world_dims.max_x, hint.max_x), - min_y: max(world_dims.min_y, hint.min_y), - max_y: min(world_dims.max_y, hint.max_y), - }) + .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) => SquareLocationRestriction { - min_x: max(acc.min_x, spec.location.x - max_distance), - max_x: min(acc.max_x, spec.location.x + max_distance), - min_y: max(acc.min_y, spec.location.y - max_distance), - max_y: min(acc.max_y, spec.location.y + max_distance), + 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 @@ -1517,17 +1503,18 @@ fn find_site_loc( const MAX_ATTEMPTS: usize = 10000; let mut loc = None; for _ in 0..MAX_ATTEMPTS { - let world_dims = SquareLocationRestriction { - min_x: 0, - max_x: ctx.sim.get_size().x as i32, - min_y: 0, - max_y: ctx.sim.get_size().y as i32, + 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(location_hint.min_x..location_hint.max_x), - ctx.rng.gen_range(location_hint.min_y..location_hint.max_y), + ctx.rng.gen_range(location_hint.min.x..location_hint.max.x), + ctx.rng.gen_range(location_hint.min.y..location_hint.max.y), ) }); @@ -1917,17 +1904,13 @@ mod tests { vec![Vec2 { x: 1, y: 0 }, Vec2 { x: 13, y: 12 }].into_iter(), 10, ); - let expected = SquareLocationRestriction { - min_x: 0, - max_x: 23, - min_y: 0, - max_y: 22, + let expected = Aabr { + min: Vec2 { x: 0, y: 0 }, + max: Vec2 { x: 23, y: 22 }, }; - let map_dims = SquareLocationRestriction { - min_x: 0, - max_x: 200, - min_y: 0, - max_y: 300, + let map_dims = Aabr { + min: Vec2 { x: 0, y: 0 }, + max: Vec2 { x: 200, y: 300 }, }; assert_eq!(expected, reqs.location_hint(&map_dims)); }