remove silly mistakes

This commit is contained in:
osmarks
2026-03-18 19:02:39 +00:00
parent a2da943b4e
commit 8cf83a5be9
6 changed files with 123 additions and 96 deletions
+3
View File
@@ -1,3 +1,6 @@
#![feature(test)]
extern crate test;
pub mod components; pub mod components;
pub mod map; pub mod map;
pub mod plant; pub mod plant;
+26 -22
View File
@@ -1,6 +1,3 @@
#![feature(test)]
extern crate test;
use hecs::{CommandBuffer, Entity, With, World}; use hecs::{CommandBuffer, Entity, With, World};
use futures_util::{stream::TryStreamExt, SinkExt, StreamExt}; use futures_util::{stream::TryStreamExt, SinkExt, StreamExt};
use lazy_static::lazy_static; use lazy_static::lazy_static;
@@ -178,6 +175,8 @@ struct GameState {
baseline_temperature: Map<f32>, baseline_temperature: Map<f32>,
dynamic_soil_nutrients: Map<f32>, dynamic_soil_nutrients: Map<f32>,
dynamic_groundwater: Map<f32>, dynamic_groundwater: Map<f32>,
dynamic_soil_nutrients_back: Map<f32>,
dynamic_groundwater_back: Map<f32>,
positions: PositionIndex, positions: PositionIndex,
metrics: GameMetrics, metrics: GameMetrics,
} }
@@ -275,15 +274,17 @@ fn game_state_from_saved(saved: SavedGame) -> Result<GameState> {
clients: Slab::new(), clients: Slab::new(),
ticks: saved.ticks, ticks: saved.ticks,
rng: fastrand::Rng::with_seed(saved.rng_seed), rng: fastrand::Rng::with_seed(saved.rng_seed),
map: saved.map,
baseline_soil_nutrients, baseline_soil_nutrients,
baseline_groundwater, baseline_groundwater,
baseline_salt, baseline_salt,
baseline_temperature, baseline_temperature,
dynamic_soil_nutrients: saved.dynamic_soil_nutrients, dynamic_soil_nutrients: saved.dynamic_soil_nutrients,
dynamic_groundwater: saved.dynamic_groundwater, 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, positions,
metrics: saved.metrics, metrics: saved.metrics,
map: saved.map,
}) })
} }
@@ -291,14 +292,16 @@ fn game_state_from_saved(saved: SavedGame) -> Result<GameState> {
fn establishment_probability(pos: Coord, genome: &plant::Genome, state: &GameState) -> f32 { fn establishment_probability(pos: Coord, genome: &plant::Genome, state: &GameState) -> f32 {
let mut crowding = 0.0; let mut crowding = 0.0;
for range in 1..4 { for offset in hex_range(4) {
for offset in hex_circle(range) { let range = vec_length(offset);
let newpos = pos + offset; if range == 0 {
if let Some(entity) = state.positions.entities[newpos] { continue;
if let Ok(plant) = state.world.get::<&Plant>(entity) { }
// TODO: this maybe ought to be more discontinuous? also work out scale let newpos = pos + offset;
crowding += (1.0 + plant.current_size.ln_1p()) / (range as f32); 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); buffer.apply(state);
if state.ticks % FIELD_DECAY_DELAY == 0 { 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 { } 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() { for (pos, water) in state.dynamic_groundwater.iter_mut() {
*water *= 0.999;
if state.map.water[pos] > 0.0 { if state.map.water[pos] > 0.0 {
*water *= 0.9; // reversion to baseline much faster in active water areas *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 // Spawn enemies
@@ -438,8 +440,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
plant.nutrients_consumed += difference * SOIL_NUTRIENT_CONSUMPTION_RATE; plant.nutrients_consumed += difference * SOIL_NUTRIENT_CONSUMPTION_RATE;
plant.total_growth += difference; plant.total_growth += difference;
if plant.current_size > plant.genome.mature_size() { if plant.current_size > plant.genome.mature_size() {
state.dynamic_soil_nutrients[pos] += 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.nutrients_added += plant.genome.nutrient_addition_rate() * SOIL_NUTRIENT_FIXATION_RATE * plant.current_size;
} else if plant.current_size <= 0.0 { } else if plant.current_size <= 0.0 {
plant.current_size = 0.1; plant.current_size = 0.1;
} }
@@ -792,7 +794,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
let mut nearby = vec![]; let mut nearby = vec![];
if let Ok(pos) = state.world.get::<&Position>(client.entity) { if let Ok(pos) = state.world.get::<&Position>(client.entity) {
let pos = pos.head(); let pos = pos.head();
for offset in hex_circle(VIEW) { for offset in hex_range(VIEW) {
let pos = pos + offset; let pos = pos + offset;
let mut rng = rng_from_hash(pos); let mut rng = rng_from_hash(pos);
@@ -921,7 +923,6 @@ async fn main() -> Result<()> {
ticks: 0, ticks: 0,
rng: fastrand::Rng::with_seed(fastrand::u64(..)), rng: fastrand::Rng::with_seed(fastrand::u64(..)),
positions: PositionIndex::new(world.radius), positions: PositionIndex::new(world.radius),
map: world,
baseline_soil_nutrients, baseline_soil_nutrients,
baseline_groundwater, baseline_groundwater,
baseline_salt, baseline_salt,
@@ -929,6 +930,9 @@ async fn main() -> Result<()> {
dynamic_soil_nutrients, dynamic_soil_nutrients,
dynamic_groundwater, dynamic_groundwater,
metrics: GameMetrics::new(), metrics: GameMetrics::new(),
dynamic_groundwater_back: Map::new(world.radius, 0.0),
dynamic_soil_nutrients_back: Map::new(world.radius, 0.0),
map: world,
})) }))
}; };
+87 -53
View File
@@ -46,7 +46,7 @@ pub fn sample_range(rng: &mut fastrand::Rng, range: i32) -> CoordVec {
CoordVec::new(q, r) CoordVec::new(q, r)
} }
pub fn hex_circle(range: i32) -> impl Iterator<Item=CoordVec> { pub fn hex_range(range: i32) -> impl Iterator<Item=CoordVec> {
(-range..=range).flat_map(move |q| { (-range..=range).flat_map(move |q| {
((-range).max(-q - range)..= range.min(-q+range)).map(move |r| { ((-range).max(-q - range)..= range.min(-q+range)).map(move |r| {
CoordVec::new(q, r) CoordVec::new(q, r)
@@ -54,61 +54,28 @@ pub fn hex_circle(range: i32) -> impl Iterator<Item=CoordVec> {
}) })
} }
pub fn hex_range(range: i32) -> impl Iterator<Item=(i32, CoordVec)> {
(0..=range).flat_map(|x| hex_circle(x).map(move |c| (x, c)))
}
pub fn count_hexes(x: i32) -> i32 { pub fn count_hexes(x: i32) -> i32 {
x*(x+1)*3+1 x*(x+1)*3+1
} }
struct CoordIterator { struct CoordIterMut<'a, T, I: Iterator<Item=Coord>> {
radius: i32, coords: I,
count: usize,
r: i32,
q: i32,
max: usize
}
struct CoordIterMut<'a, T> {
coords: CoordIterator,
data: *mut T, data: *mut T,
radius: i32, radius: i32,
side_length: usize, side_length: usize,
_marker: PhantomData<&'a mut T> _marker: PhantomData<&'a mut T>
} }
impl Iterator for CoordIterator {
type Item = Coord;
fn next(&mut self) -> Option<Self::Item> {
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 // blame OpenAI for this, and also Mozilla
impl<'a, T> Iterator for CoordIterMut<'a, T> { impl<'a, T, I: Iterator<Item=Coord>> Iterator for CoordIterMut<'a, T, I> {
type Item = (Coord, &'a mut T); type Item = (Coord, &'a mut T);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let coord = self.coords.next()?; let coord = self.coords.next()?;
let q = (coord.x + self.radius) as usize; let q = (coord.x + self.radius) as usize;
let r = (coord.y + 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 { unsafe {
// CoordIterator yields each valid map coordinate once, so each backing index is yielded once. // CoordIterator yields each valid map coordinate once, so each backing index is yielded once.
Some((coord, &mut *self.data.add(i))) Some((coord, &mut *self.data.add(i)))
@@ -159,18 +126,18 @@ impl<T> Map<T> {
q * self.side_length + r 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 { pub fn in_range(&self, coord: Coord) -> bool {
hex_distance(coord, Coord::origin()) <= self.radius hex_distance(coord, Coord::origin()) <= self.radius
} }
pub fn iter_coords(&self) -> impl Iterator<Item=Coord> { pub fn iter_coords(&self) -> impl Iterator<Item=Coord> {
CoordIterator { hex_range(self.radius).map(|c| Coord::new(c.x, c.y))
radius: self.radius,
count: 0,
max: self.size(),
r: -self.radius,
q: 0
}
} }
pub fn iter(&self) -> impl Iterator<Item=(Coord, &T)> { pub fn iter(&self) -> impl Iterator<Item=(Coord, &T)> {
@@ -183,13 +150,7 @@ impl<T> Map<T> {
pub fn iter_mut(&mut self) -> impl Iterator<Item=(Coord, &mut T)> { pub fn iter_mut(&mut self) -> impl Iterator<Item=(Coord, &mut T)> {
CoordIterMut { CoordIterMut {
coords: CoordIterator { coords: self.iter_coords(),
radius: self.radius,
count: 0,
max: self.size(),
r: -self.radius,
q: 0
},
data: self.data.as_mut_ptr(), data: self.data.as_mut_ptr(),
radius: self.radius, radius: self.radius,
side_length: self.side_length, side_length: self.side_length,
@@ -212,7 +173,7 @@ pub fn smooth(map: &Map<f32>, radius: i32) -> Map<f32> {
//println!("{:?}", data[(map.radius as usize + 1, map.radius as usize)]); //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); 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; 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<f32>, radius: i32) -> Map<f32> {
} }
} }
pub fn radius_1_conv_ref(inp: &Map<f32>, out: &mut Map<f32>, 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<f32>, out: &mut Map<f32>, coefficients: &[f32; 7]) {
let offsets = hex_range(1).enumerate().collect::<Vec<_>>();
let offset_offsets = hex_range(1).map(|c| inp.coordvec_to_index_offset(c)).enumerate().collect::<Vec<_>>();
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::<f32>::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::<f32>::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<T> Index<Coord> for Map<T> { impl<T> Index<Coord> for Map<T> {
type Output = T; type Output = T;
fn index(&self, index: Coord) -> &Self::Output { fn index(&self, index: Coord) -> &Self::Output {
+1 -1
View File
@@ -43,7 +43,7 @@ impl Genome {
if !self.terrain_valid(terrain) { if !self.terrain_valid(terrain) {
return 0.0; 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 temperature_diff = (temperature - sigmoid(self.optimal_temperature)).powf(2.0);
let base = 1.0 let base = 1.0
- self.nutrient_addition_rate() * 0.04 // nutrient enrichment has a growth tradeoff - self.nutrient_addition_rate() * 0.04 // nutrient enrichment has a growth tradeoff
+3 -3
View File
@@ -19,7 +19,7 @@ pub mod config {
pub const FIELD_DECAY_DELAY: u64 = 64; pub const FIELD_DECAY_DELAY: u64 = 64;
pub const PLANT_GROWTH_SCALE: f32 = 0.01; pub const PLANT_GROWTH_SCALE: f32 = 0.01;
pub const SOIL_NUTRIENT_CONSUMPTION_RATE: f32 = 0.3; 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 WATER_CONSUMPTION_RATE: f32 = 0.05;
pub const PLANT_IDLE_WATER_CONSUMPTION_SCALE: f32 = 0.02; pub const PLANT_IDLE_WATER_CONSUMPTION_SCALE: f32 = 0.02;
pub const PLANT_DIEOFF_THRESHOLD: f32 = 0.3; 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_POLLINATION_SCAN_FRACTION: f32 = 0.1;
pub const PLANT_SEEDING_RADIUS: i32 = 5; // TODO: as above pub const PLANT_SEEDING_RADIUS: i32 = 5; // TODO: as above
pub const PLANT_SEEDS_PER_EVENT: usize = 1; 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_LIFESPAN_SCALE: f32 = 1.0 / PLANT_GROWTH_SCALE;
pub const PLANT_REPRODUCTION_ATTEMPT_COST: f32 = 0.01; pub const PLANT_REPRODUCTION_ATTEMPT_COST: f32 = 0.01;
pub const INITIAL_PLANTS: usize = 131072; pub const INITIAL_PLANTS: usize = 131072;
pub const EVOLUTION_RATE: f32 = 1.0; 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) // Runtime game logic (misc)
pub const VIEW: i32 = 15; pub const VIEW: i32 = 15;
pub const RANDOM_DESPAWN_INV_RATE: u64 = 4000; pub const RANDOM_DESPAWN_INV_RATE: u64 = 4000;
+3 -17
View File
@@ -304,7 +304,7 @@ pub fn simulate_water(heightmap: &mut Map<f32>, rain_map: &Map<f32>, sea: &HashS
for point in path { for point in path {
let water_range_raw = watermap[point] * 1.0 - heightmap[point]; let water_range_raw = watermap[point] * 1.0 - heightmap[point];
let water_range = water_range_raw.ceil() as i32; 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) { if !watermap.in_range(point + nearby) {
continue; continue;
} }
@@ -314,7 +314,8 @@ pub fn simulate_water(heightmap: &mut Map<f32>, rain_map: &Map<f32>, sea: &HashS
let erosion_range_raw = (water_range_raw * 2.0 + 2.0).powf(EROSION_EXPONENT); let erosion_range_raw = (water_range_raw * 2.0 + 2.0).powf(EROSION_EXPONENT);
let erosion_range = erosion_range_raw.ceil() as i32; 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) { if !watermap.in_range(point + nearby) {
continue; continue;
} }
@@ -552,18 +553,3 @@ impl GeneratedWorld {
self.heightmap.radius 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);
});
}
}