diff --git a/src/lib.rs b/src/lib.rs index a8acaad..99d3242 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +#![feature(test)] +extern crate test; + pub mod components; pub mod map; pub mod plant; diff --git a/src/main.rs b/src/main.rs index 9100994..52e74d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,3 @@ -#![feature(test)] -extern crate test; - use hecs::{CommandBuffer, Entity, With, World}; use futures_util::{stream::TryStreamExt, SinkExt, StreamExt}; use lazy_static::lazy_static; @@ -178,6 +175,8 @@ struct GameState { baseline_temperature: Map, dynamic_soil_nutrients: Map, dynamic_groundwater: Map, + dynamic_soil_nutrients_back: Map, + dynamic_groundwater_back: Map, positions: PositionIndex, metrics: GameMetrics, } @@ -275,15 +274,17 @@ fn game_state_from_saved(saved: SavedGame) -> Result { clients: Slab::new(), ticks: saved.ticks, rng: fastrand::Rng::with_seed(saved.rng_seed), - map: saved.map, baseline_soil_nutrients, baseline_groundwater, baseline_salt, baseline_temperature, dynamic_soil_nutrients: saved.dynamic_soil_nutrients, dynamic_groundwater: saved.dynamic_groundwater, + dynamic_groundwater_back: Map::new(saved.map.radius, 0.0), + dynamic_soil_nutrients_back: Map::new(saved.map.radius, 0.0), positions, metrics: saved.metrics, + map: saved.map, }) } @@ -291,14 +292,16 @@ fn game_state_from_saved(saved: SavedGame) -> Result { fn establishment_probability(pos: Coord, genome: &plant::Genome, state: &GameState) -> f32 { let mut crowding = 0.0; - for range in 1..4 { - for offset in hex_circle(range) { - let newpos = pos + offset; - if let Some(entity) = state.positions.entities[newpos] { - if let Ok(plant) = state.world.get::<&Plant>(entity) { - // TODO: this maybe ought to be more discontinuous? also work out scale - crowding += (1.0 + plant.current_size.ln_1p()) / (range as f32); - } + for offset in hex_range(4) { + let range = vec_length(offset); + if range == 0 { + continue; + } + let newpos = pos + offset; + if let Some(entity) = state.positions.entities[newpos] { + if let Ok(plant) = state.world.get::<&Plant>(entity) { + // TODO: this maybe ought to be more discontinuous? also work out scale + crowding += (1.0 + plant.current_size.ln_1p()) / (range as f32); } } } @@ -319,18 +322,17 @@ async fn game_tick(state: &mut GameState) -> Result<()> { buffer.apply(state); if state.ticks % FIELD_DECAY_DELAY == 0 { - state.dynamic_soil_nutrients.for_each_mut(|nutrients| *nutrients *= 0.9996); + state.dynamic_soil_nutrients.for_each_mut(|nutrients| *nutrients *= 0.9999); + radius_1_conv(&state.dynamic_soil_nutrients, &mut state.dynamic_soil_nutrients_back, &[0.9999, 0.9999, 0.9999, 0.9999, 0.9999, 0.9999, 0.9999]); + std::mem::swap(&mut state.dynamic_soil_nutrients, &mut state.dynamic_soil_nutrients_back); } else if state.ticks % FIELD_DECAY_DELAY == 1 { + radius_1_conv(&state.dynamic_groundwater, &mut state.dynamic_groundwater_back, &[0.999, 0.999, 0.999, 0.999, 0.999, 0.999, 0.999]); + std::mem::swap(&mut state.dynamic_groundwater, &mut state.dynamic_groundwater_back); for (pos, water) in state.dynamic_groundwater.iter_mut() { - *water *= 0.999; if state.map.water[pos] > 0.0 { *water *= 0.9; // reversion to baseline much faster in active water areas } } - } else if state.ticks % FIELD_DECAY_DELAY == 2 { - state.dynamic_soil_nutrients = smooth(&state.dynamic_soil_nutrients, 1); - } else if state.ticks % FIELD_DECAY_DELAY == 3 { - state.dynamic_groundwater = smooth(&state.dynamic_groundwater, 1); } // Spawn enemies @@ -438,8 +440,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> { plant.nutrients_consumed += difference * SOIL_NUTRIENT_CONSUMPTION_RATE; plant.total_growth += difference; if plant.current_size > plant.genome.mature_size() { - state.dynamic_soil_nutrients[pos] += plant.genome.nutrient_addition_rate() * SOIL_NUTRIENT_FIXATION_RATE; - plant.nutrients_added += plant.genome.nutrient_addition_rate() * SOIL_NUTRIENT_FIXATION_RATE; + state.dynamic_soil_nutrients[pos] += plant.genome.nutrient_addition_rate() * SOIL_NUTRIENT_FIXATION_RATE * plant.current_size; + plant.nutrients_added += plant.genome.nutrient_addition_rate() * SOIL_NUTRIENT_FIXATION_RATE * plant.current_size; } else if plant.current_size <= 0.0 { plant.current_size = 0.1; } @@ -792,7 +794,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> { let mut nearby = vec![]; if let Ok(pos) = state.world.get::<&Position>(client.entity) { let pos = pos.head(); - for offset in hex_circle(VIEW) { + for offset in hex_range(VIEW) { let pos = pos + offset; let mut rng = rng_from_hash(pos); @@ -921,7 +923,6 @@ async fn main() -> Result<()> { ticks: 0, rng: fastrand::Rng::with_seed(fastrand::u64(..)), positions: PositionIndex::new(world.radius), - map: world, baseline_soil_nutrients, baseline_groundwater, baseline_salt, @@ -929,6 +930,9 @@ async fn main() -> Result<()> { dynamic_soil_nutrients, dynamic_groundwater, metrics: GameMetrics::new(), + dynamic_groundwater_back: Map::new(world.radius, 0.0), + dynamic_soil_nutrients_back: Map::new(world.radius, 0.0), + map: world, })) }; diff --git a/src/map.rs b/src/map.rs index c9ae90c..b78014e 100644 --- a/src/map.rs +++ b/src/map.rs @@ -46,7 +46,7 @@ pub fn sample_range(rng: &mut fastrand::Rng, range: i32) -> CoordVec { CoordVec::new(q, r) } -pub fn hex_circle(range: i32) -> impl Iterator { +pub fn hex_range(range: i32) -> impl Iterator { (-range..=range).flat_map(move |q| { ((-range).max(-q - range)..= range.min(-q+range)).map(move |r| { CoordVec::new(q, r) @@ -54,61 +54,28 @@ pub fn hex_circle(range: i32) -> impl Iterator { }) } -pub fn hex_range(range: i32) -> impl Iterator { - (0..=range).flat_map(|x| hex_circle(x).map(move |c| (x, c))) -} - - pub fn count_hexes(x: i32) -> i32 { x*(x+1)*3+1 } -struct CoordIterator { - radius: i32, - count: usize, - r: i32, - q: i32, - max: usize -} - -struct CoordIterMut<'a, T> { - coords: CoordIterator, +struct CoordIterMut<'a, T, I: Iterator> { + coords: I, data: *mut T, radius: i32, side_length: usize, _marker: PhantomData<&'a mut T> } -impl Iterator for CoordIterator { - type Item = Coord; - fn next(&mut self) -> Option { - if self.count == self.max { - return None; - } - let result = Coord::new(self.q, self.r); - self.count += 1; - self.q += 1; - if self.r < 0 && self.q == self.radius + 1 { - self.r += 1; - self.q = -self.radius - self.r; - } - if self.r >= 0 && self.q + self.r == self.radius + 1 { - self.r += 1; - self.q = -self.radius; - } - Some(result) - } -} // blame OpenAI for this, and also Mozilla -impl<'a, T> Iterator for CoordIterMut<'a, T> { +impl<'a, T, I: Iterator> Iterator for CoordIterMut<'a, T, I> { type Item = (Coord, &'a mut T); fn next(&mut self) -> Option { let coord = self.coords.next()?; let q = (coord.x + self.radius) as usize; let r = (coord.y + self.radius) as usize; - let i = q + r * self.side_length; + let i = q * self.side_length + r; // oops unsafe { // CoordIterator yields each valid map coordinate once, so each backing index is yielded once. Some((coord, &mut *self.data.add(i))) @@ -159,18 +126,18 @@ impl Map { q * self.side_length + r } + pub fn coordvec_to_index_offset(&self, c: CoordVec) -> isize { + let q = c.x as isize; + let r = c.y as isize; + q * self.side_length as isize + r + } + pub fn in_range(&self, coord: Coord) -> bool { hex_distance(coord, Coord::origin()) <= self.radius } pub fn iter_coords(&self) -> impl Iterator { - CoordIterator { - radius: self.radius, - count: 0, - max: self.size(), - r: -self.radius, - q: 0 - } + hex_range(self.radius).map(|c| Coord::new(c.x, c.y)) } pub fn iter(&self) -> impl Iterator { @@ -183,13 +150,7 @@ impl Map { pub fn iter_mut(&mut self) -> impl Iterator { CoordIterMut { - coords: CoordIterator { - radius: self.radius, - count: 0, - max: self.size(), - r: -self.radius, - q: 0 - }, + coords: self.iter_coords(), data: self.data.as_mut_ptr(), radius: self.radius, side_length: self.side_length, @@ -212,7 +173,7 @@ pub fn smooth(map: &Map, radius: i32) -> Map { //println!("{:?}", data[(map.radius as usize + 1, map.radius as usize)]); let mut kernel = Array2::from_elem((radius as usize * 2 + 1, radius as usize * 2 + 1), 0.0); - for (_, offset) in hex_range(radius) { + for offset in hex_range(radius) { kernel[(offset.x as usize + radius as usize, offset.y as usize + radius as usize)] = 1.0 / count_hexes(radius) as f32; } @@ -225,6 +186,79 @@ pub fn smooth(map: &Map, radius: i32) -> Map { } } +pub fn radius_1_conv_ref(inp: &Map, out: &mut Map, coefficients: &[f32; 7]) { + for coord in inp.iter_coords() { + let mut sum = 0.0; + let mut count = 0.0; + for (i, offset) in hex_range(1).enumerate() { + //println!("{} {:?}", i, offset); + let neighbor = coord + offset; + if inp.in_range(neighbor) { + sum += inp[neighbor] * coefficients[i]; + count += 1.0; + } + } + out[coord] = sum / count; + } +} + +// TODO: improve performance +pub fn radius_1_conv(inp: &Map, out: &mut Map, coefficients: &[f32; 7]) { + let offsets = hex_range(1).enumerate().collect::>(); + let offset_offsets = hex_range(1).map(|c| inp.coordvec_to_index_offset(c)).enumerate().collect::>(); + for coord in inp.iter_coords() { + let i = inp.coord_to_index(coord); + let range = hex_distance(coord, Coord::origin()); + let mut sum = 0.0; + let mut count = 0.0; + if range < inp.radius { + for (k, offset) in &offset_offsets { + sum += inp.data[(i as isize + *offset) as usize] * coefficients[*k]; + } + count = 7.0; + } else { + for (i, offset) in &offsets { + //println!("{} {:?}", i, offset); + let neighbor = coord + *offset; + if inp.in_range(neighbor) { + sum += inp[neighbor] * coefficients[*i]; + count += 1.0; + } + } + } + out.data[i] = sum / count; + } +} + +#[cfg(test)] +mod test { + use super::{Map, radius_1_conv_ref, radius_1_conv}; + use test::bench::Bencher; + + #[bench] + fn bench_r1_conv(b: &mut Bencher) { + use std::hint::black_box; + let map = black_box(Map::::from_fn(|_| fastrand::f32(), 512)); + let mut out_map = Map::new(512, 0.0f32); + b.iter(|| { + radius_1_conv(&map, &mut out_map, &[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]); + }); + } + + #[test] + fn test_r1_conv() { + let map = Map::::from_fn(|_| fastrand::f32(), 512); + let mut out_map = Map::new(512, 0.0f32); + let mut out_map_2 = Map::new(512, 0.0f32); + radius_1_conv_ref(&map, &mut out_map, &[1.0, 1.5, -1.0, 2.0, 3.0, 0.1, 0.6]); + radius_1_conv(&map, &mut out_map_2, &[1.0, 1.5, -1.0, 2.0, 3.0, 0.1, 0.6]); + for coord in map.iter_coords() { + assert!(out_map[coord] - out_map_2[coord] < 1e-6); + } + } +} + + impl Index for Map { type Output = T; fn index(&self, index: Coord) -> &Self::Output { diff --git a/src/plant.rs b/src/plant.rs index 7e8e072..217d57a 100644 --- a/src/plant.rs +++ b/src/plant.rs @@ -43,7 +43,7 @@ impl Genome { if !self.terrain_valid(terrain) { return 0.0; } - let mut water_diff = (water - sigmoid(self.optimal_water_level)).powf(2.0); + let water_diff = (water - sigmoid(self.optimal_water_level)).powf(2.0); let temperature_diff = (temperature - sigmoid(self.optimal_temperature)).powf(2.0); let base = 1.0 - self.nutrient_addition_rate() * 0.04 // nutrient enrichment has a growth tradeoff diff --git a/src/util.rs b/src/util.rs index e1560da..9b16b9d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -19,7 +19,7 @@ pub mod config { pub const FIELD_DECAY_DELAY: u64 = 64; pub const PLANT_GROWTH_SCALE: f32 = 0.01; pub const SOIL_NUTRIENT_CONSUMPTION_RATE: f32 = 0.3; - pub const SOIL_NUTRIENT_FIXATION_RATE: f32 = 0.03; + pub const SOIL_NUTRIENT_FIXATION_RATE: f32 = 0.005; pub const WATER_CONSUMPTION_RATE: f32 = 0.05; pub const PLANT_IDLE_WATER_CONSUMPTION_SCALE: f32 = 0.02; pub const PLANT_DIEOFF_THRESHOLD: f32 = 0.3; @@ -30,12 +30,12 @@ pub mod config { pub const PLANT_POLLINATION_SCAN_FRACTION: f32 = 0.1; pub const PLANT_SEEDING_RADIUS: i32 = 5; // TODO: as above pub const PLANT_SEEDS_PER_EVENT: usize = 1; - pub const SOIL_NUTRIENT_DECOMP_RETURN_RATE: f32 = 1.0 / SOIL_NUTRIENT_CONSUMPTION_RATE * 0.5; + pub const SOIL_NUTRIENT_DECOMP_RETURN_RATE: f32 = 1.0 / SOIL_NUTRIENT_CONSUMPTION_RATE * 0.4; pub const PLANT_LIFESPAN_SCALE: f32 = 1.0 / PLANT_GROWTH_SCALE; pub const PLANT_REPRODUCTION_ATTEMPT_COST: f32 = 0.01; pub const INITIAL_PLANTS: usize = 131072; pub const EVOLUTION_RATE: f32 = 1.0; - pub const CROWDING_MULTIPLIER: f32 = 0.3; + pub const CROWDING_MULTIPLIER: f32 = 0.35; // Runtime game logic (misc) pub const VIEW: i32 = 15; pub const RANDOM_DESPAWN_INV_RATE: u64 = 4000; diff --git a/src/worldgen.rs b/src/worldgen.rs index e5b8bbe..c32611b 100644 --- a/src/worldgen.rs +++ b/src/worldgen.rs @@ -304,7 +304,7 @@ pub fn simulate_water(heightmap: &mut Map, rain_map: &Map, sea: &HashS for point in path { let water_range_raw = watermap[point] * 1.0 - heightmap[point]; let water_range = water_range_raw.ceil() as i32; - for (_, nearby) in hex_range(water_range) { + for nearby in hex_range(water_range) { if !watermap.in_range(point + nearby) { continue; } @@ -314,7 +314,8 @@ pub fn simulate_water(heightmap: &mut Map, rain_map: &Map, sea: &HashS let erosion_range_raw = (water_range_raw * 2.0 + 2.0).powf(EROSION_EXPONENT); let erosion_range = erosion_range_raw.ceil() as i32; - for (this_range, nearby) in hex_range(erosion_range) { + for nearby in hex_range(erosion_range) { + let this_range = hex_distance(point + nearby, point); if !watermap.in_range(point + nearby) { continue; } @@ -552,18 +553,3 @@ impl GeneratedWorld { self.heightmap.radius } } - -#[cfg(test)] -mod test { - use super::{Map, smooth, WORLD_RADIUS}; - use test::bench::Bencher; - - #[bench] - fn bench_smooth_map(b: &mut Bencher) { - use std::hint::black_box; - b.iter(|| { - let map = black_box(Map::new(WORLD_RADIUS, 0.0f32)); - smooth(&map, 3); - }); - } -}