sim/src/world.rs

672 lines
20 KiB
Rust
Raw Normal View History

use std::fmt::Write;
pub const W: usize = 96;
pub const H: usize = 64;
pub const DAY: u64 = 1800;
const WATER_DEEP: u8 = 0;
const WATER: u8 = 1;
const SAND: u8 = 2;
const GRASS: u8 = 3;
const FOREST: u8 = 4;
const HILLS: u8 = 5;
const MOUNTAIN: u8 = 6;
const TREE: u8 = 1;
const BERRY: u8 = 2;
const STONE: u8 = 3;
pub struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545F4914F6CDD1D)
}
fn f(&mut self) -> f32 {
(self.next() >> 40) as f32 / (1u64 << 24) as f32
}
fn chance(&mut self, p: f32) -> bool {
self.f() < p
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
}
fn hash(x: i32, y: i32, seed: u64) -> f32 {
let mut h = (x as u64).wrapping_mul(0x9E3779B97F4A7C15)
^ (y as u64).wrapping_mul(0xC2B2AE3D27D4EB4F)
^ seed.wrapping_mul(0x165667B19E3779F9);
h ^= h >> 33;
h = h.wrapping_mul(0xFF51AFD7ED558CCD);
h ^= h >> 33;
(h >> 40) as f32 / (1u64 << 24) as f32
}
fn value_noise(x: f32, y: f32, seed: u64) -> f32 {
let (xi, yi) = (x.floor() as i32, y.floor() as i32);
let (fx, fy) = (x - xi as f32, y - yi as f32);
let s = |t: f32| t * t * (3.0 - 2.0 * t);
let (u, v) = (s(fx), s(fy));
let a = hash(xi, yi, seed);
let b = hash(xi + 1, yi, seed);
let c = hash(xi, yi + 1, seed);
let d = hash(xi + 1, yi + 1, seed);
let top = a + (b - a) * u;
let bot = c + (d - c) * u;
top + (bot - top) * v
}
fn fbm(x: f32, y: f32, seed: u64) -> f32 {
let (mut sum, mut amp, mut freq, mut norm) = (0.0, 1.0, 1.0 / 16.0, 0.0);
for o in 0..4 {
sum += amp * value_noise(x * freq, y * freq, seed + o);
norm += amp;
amp *= 0.5;
freq *= 2.0;
}
sum / norm
}
#[derive(Clone)]
pub struct Person {
id: u32,
x: usize,
y: usize,
hunger: f32,
food: u32,
wood: u32,
stone: u32,
action: &'static str,
dead: bool,
}
impl Person {
fn carry(&self) -> u32 {
self.food + self.wood + self.stone
}
}
#[derive(Clone)]
pub struct Animal {
x: usize,
y: usize,
energy: f32,
dead: bool,
}
pub struct World {
tiles: Vec<u8>,
res: Vec<(u8, u16)>,
people: Vec<Person>,
rabbits: Vec<Animal>,
wolves: Vec<Animal>,
houses: Vec<(usize, usize)>,
home: (usize, usize),
food: u32,
wood: u32,
stone: u32,
tools: u32,
rng: Rng,
tick: u64,
next_id: u32,
log: Vec<String>,
}
fn cheb(a: (usize, usize), b: (usize, usize)) -> usize {
a.0.abs_diff(b.0).max(a.1.abs_diff(b.1))
}
impl World {
pub fn new(seed: u64) -> World {
let mut tiles = vec![0u8; W * H];
for y in 0..H {
for x in 0..W {
let dx = (x as f32 / W as f32 - 0.5) * 2.0;
let dy = (y as f32 / H as f32 - 0.5) * 2.0;
let d = (dx * dx + dy * dy).min(1.5);
let h = fbm(x as f32, y as f32, seed) + 0.25 - 0.55 * d;
let m = fbm(x as f32 + 500.0, y as f32 + 500.0, seed + 99);
tiles[y * W + x] = if h < 0.28 {
WATER_DEEP
} else if h < 0.36 {
WATER
} else if h < 0.41 {
SAND
} else if h < 0.68 {
if m > 0.52 { FOREST } else { GRASS }
} else if h < 0.78 {
HILLS
} else {
MOUNTAIN
};
}
}
let mut w = World {
tiles,
res: vec![(0, 0); W * H],
people: vec![],
rabbits: vec![],
wolves: vec![],
houses: vec![],
home: (W / 2, H / 2),
food: 4,
wood: 0,
stone: 0,
tools: 0,
rng: Rng(seed.wrapping_mul(0x9E3779B97F4A7C15) | 1),
tick: DAY / 4,
next_id: 1,
log: vec![],
};
w.home = w
.find_land(GRASS)
.unwrap_or_else(|| w.find_land(SAND).unwrap_or((W / 2, H / 2)));
for i in 0..W * H {
let r = w.rng.f();
w.res[i] = match w.tiles[i] {
FOREST if r < 0.6 => (TREE, 5),
GRASS if r < 0.04 => (BERRY, 4),
HILLS if r < 0.15 => (STONE, 6),
_ => (0, 0),
};
}
let (hx, hy) = w.home;
w.res[hy * W + hx] = (0, 0);
for _ in 0..6 {
w.spawn_person();
}
for _ in 0..60 {
if let Some(p) = w.random_land() {
w.rabbits.push(Animal {
x: p.0,
y: p.1,
energy: 70.0,
dead: false,
});
}
}
for _ in 0..4 {
if let Some(p) = w.random_land() {
if cheb(p, w.home) > 15 {
w.wolves.push(Animal {
x: p.0,
y: p.1,
energy: 70.0,
dead: false,
});
}
}
}
w
}
fn find_land(&self, kind: u8) -> Option<(usize, usize)> {
let c = (W / 2, H / 2);
let mut best: Option<(usize, usize)> = None;
for y in 0..H {
for x in 0..W {
if self.tiles[y * W + x] == kind
&& best.is_none_or(|b| cheb((x, y), c) < cheb(b, c))
{
best = Some((x, y));
}
}
}
best
}
fn random_land(&mut self) -> Option<(usize, usize)> {
for _ in 0..50 {
let (x, y) = (self.rng.below(W), self.rng.below(H));
if self.walkable(x as i32, y as i32) {
return Some((x, y));
}
}
None
}
fn spawn_person(&mut self) {
let (x, y) = self.home;
self.people.push(Person {
id: self.next_id,
x,
y,
hunger: 10.0,
food: 0,
wood: 0,
stone: 0,
action: "idle",
dead: false,
});
self.next_id += 1;
}
fn say(&mut self, s: String) {
self.log
.push(format!("[day {}] {}", self.tick / DAY + 1, s));
if self.log.len() > 8 {
self.log.remove(0);
}
}
pub fn daylight(&self) -> f32 {
let phase = (self.tick % DAY) as f32 / DAY as f32;
let v = -(phase * std::f32::consts::TAU).cos();
(0.5 + v * 0.8).clamp(0.0, 1.0)
}
fn walkable(&self, x: i32, y: i32) -> bool {
if x < 0 || y < 0 || x >= W as i32 || y >= H as i32 {
return false;
}
matches!(
self.tiles[y as usize * W + x as usize],
SAND | GRASS | FOREST | HILLS
)
}
fn move_dir(&mut self, x: usize, y: usize, dx: i32, dy: i32) -> (usize, usize) {
for (a, b) in [(dx, dy), (dx, 0), (0, dy)] {
if (a, b) != (0, 0) && self.walkable(x as i32 + a, y as i32 + b) {
return ((x as i32 + a) as usize, (y as i32 + b) as usize);
}
}
self.wander(x, y)
}
fn wander(&mut self, x: usize, y: usize) -> (usize, usize) {
for _ in 0..4 {
let (a, b) = (self.rng.below(3) as i32 - 1, self.rng.below(3) as i32 - 1);
if (a, b) != (0, 0) && self.walkable(x as i32 + a, y as i32 + b) {
return ((x as i32 + a) as usize, (y as i32 + b) as usize);
}
}
(x, y)
}
fn move_toward(&mut self, from: (usize, usize), to: (usize, usize)) -> (usize, usize) {
let dx = (to.0 as i32 - from.0 as i32).signum();
let dy = (to.1 as i32 - from.1 as i32).signum();
self.move_dir(from.0, from.1, dx, dy)
}
fn nearest_res(&self, from: (usize, usize), kind: u8, r: usize) -> Option<(usize, usize)> {
let mut best: Option<(usize, usize)> = None;
let (x0, x1) = (from.0.saturating_sub(r), (from.0 + r).min(W - 1));
let (y0, y1) = (from.1.saturating_sub(r), (from.1 + r).min(H - 1));
for y in y0..=y1 {
for x in x0..=x1 {
if self.res[y * W + x].0 == kind
&& best.is_none_or(|b| cheb((x, y), from) < cheb(b, from))
{
best = Some((x, y));
}
}
}
best
}
fn nearest_animal(list: &[Animal], from: (usize, usize), r: usize) -> Option<usize> {
let mut best: Option<(usize, usize)> = None;
for (i, a) in list.iter().enumerate() {
let d = cheb((a.x, a.y), from);
if !a.dead && d <= r && best.is_none_or(|(_, bd)| d < bd) {
best = Some((i, d));
}
}
best.map(|b| b.0)
}
pub fn step(&mut self) {
self.tick += 1;
self.regrow();
if self.tick % 2 == 0 {
for i in 0..self.people.len() {
self.tick_person(i);
}
for i in 0..self.rabbits.len() {
self.tick_rabbit(i);
}
} else {
for i in 0..self.wolves.len() {
self.tick_wolf(i);
}
}
self.people.retain(|p| !p.dead);
self.rabbits.retain(|a| !a.dead);
self.wolves.retain(|a| !a.dead);
self.population();
}
fn regrow(&mut self) {
for _ in 0..20 {
let i = self.rng.below(W * H);
if self.res[i].0 != 0 {
continue;
}
let p = self.rng.f();
match self.tiles[i] {
FOREST if p < 0.02 => self.res[i] = (TREE, 5),
GRASS if p < 0.02 => self.res[i] = (BERRY, 4),
HILLS if p < 0.004 => self.res[i] = (STONE, 6),
_ => {}
}
}
}
fn population(&mut self) {
if self.tick % 400 == 0 && self.food >= 10 && self.people.len() < 8 + self.houses.len() * 3
{
self.food -= 6;
self.spawn_person();
self.say("A child was born".into());
}
if self.tick % 50 == 0 {
if self.rabbits.len() < 15 {
if let Some(p) = self.random_land() {
self.rabbits.push(Animal {
x: p.0,
y: p.1,
energy: 70.0,
dead: false,
});
}
}
if self.wolves.len() < 2 && self.rabbits.len() > 40 {
if let Some(p) = self.random_land() {
if cheb(p, self.home) > 15 {
self.wolves.push(Animal {
x: p.0,
y: p.1,
energy: 70.0,
dead: false,
});
}
}
}
}
}
fn tick_person(&mut self, i: usize) {
let mut p = self.people[i].clone();
let night = self.daylight() < 0.2;
let pos = (p.x, p.y);
let at_home = cheb(pos, self.home) <= 1;
p.hunger += if night && at_home { 0.04 } else { 0.1 };
if p.hunger >= 100.0 {
p.dead = true;
self.say(format!("Person #{} starved", p.id));
self.people[i] = p;
return;
}
if at_home {
self.food += p.food;
self.wood += p.wood;
self.stone += p.stone;
p.food = 0;
p.wood = 0;
p.stone = 0;
if p.hunger > 40.0 && self.food > 0 {
self.food -= 1;
p.hunger = (p.hunger - 25.0).max(0.0);
}
if self.wood >= 3 && self.stone >= 1 && self.tools < self.people.len() as u32 {
self.wood -= 3;
self.stone -= 1;
self.tools += 1;
}
if self.wood >= 12 && self.stone >= 4 && self.houses.len() < 10 {
let (hx, hy) = self.home;
let x = (hx as i32 + self.rng.below(13) as i32 - 6).clamp(0, W as i32 - 1);
let y = (hy as i32 + self.rng.below(13) as i32 - 6).clamp(0, H as i32 - 1);
if self.walkable(x, y)
&& self.res[y as usize * W + x as usize].0 == 0
&& !self.houses.contains(&(x as usize, y as usize))
{
self.wood -= 12;
self.stone -= 4;
self.houses.push((x as usize, y as usize));
self.say("A new house was built".into());
}
}
}
if p.hunger > 50.0 && p.food > 0 {
p.food -= 1;
p.hunger = (p.hunger - 25.0).max(0.0);
}
let home = self.home;
let yield_n = if self.tools >= self.people.len() as u32 {
2
} else {
1
};
let mut target: Option<(usize, usize)> = None;
let mut gather: Option<u8> = None;
let mut hunt = false;
if night {
if at_home {
p.action = "sleeping";
} else {
p.action = "going home";
target = Some(home);
}
} else if p.hunger > 50.0 && p.food == 0 && self.food > 0 {
p.action = "going to eat";
target = Some(home);
} else if p.carry() >= 6 {
p.action = "hauling goods";
target = Some(home);
} else {
let order: [u8; 3] = if self.food < 12 || p.hunger > 50.0 {
[BERRY, TREE, STONE]
} else if self.wood <= self.stone * 2 + 8 {
[TREE, STONE, BERRY]
} else {
[STONE, TREE, BERRY]
};
for k in order {
if let Some(t) = self.nearest_res(pos, k, 25) {
target = Some(t);
gather = Some(k);
p.action = match k {
BERRY => "foraging",
TREE => "chopping wood",
_ => "mining stone",
};
break;
}
}
if (self.food < 12 || p.hunger > 50.0)
&& (gather != Some(BERRY) || self.rng.chance(0.3))
{
if let Some(r) = Self::nearest_animal(&self.rabbits, pos, 25) {
target = Some((self.rabbits[r].x, self.rabbits[r].y));
hunt = true;
gather = None;
p.action = "hunting";
}
}
}
if let Some(t) = target {
if hunt {
if cheb(pos, t) <= 1 {
if let Some(r) = Self::nearest_animal(&self.rabbits, pos, 1) {
if self.rng.chance(0.5) {
self.rabbits[r].dead = true;
p.food += 3;
}
}
} else {
(p.x, p.y) = self.move_toward(pos, t);
}
} else if let Some(k) = gather {
let idx = pos.1 * W + pos.0;
if pos == t && self.res[idx].0 == k {
let take = (yield_n as u16).min(self.res[idx].1);
self.res[idx].1 -= take;
if self.res[idx].1 == 0 {
self.res[idx].0 = 0;
}
match k {
BERRY => p.food += take as u32,
TREE => p.wood += take as u32,
_ => p.stone += take as u32,
}
} else {
(p.x, p.y) = self.move_toward(pos, t);
}
} else if !at_home || p.action == "going to eat" && cheb(pos, t) > 1 {
(p.x, p.y) = self.move_toward(pos, t);
}
} else if p.action != "sleeping" {
p.action = "wandering";
(p.x, p.y) = self.wander(p.x, p.y);
}
self.people[i] = p;
}
fn tick_rabbit(&mut self, i: usize) {
let mut a = self.rabbits[i].clone();
a.energy -= 0.05;
if matches!(self.tiles[a.y * W + a.x], GRASS | FOREST) {
a.energy = (a.energy + 0.2).min(100.0);
}
let pos = (a.x, a.y);
if let Some(w) = Self::nearest_animal(&self.wolves, pos, 4) {
if self.rng.chance(0.8) {
let dx = (a.x as i32 - self.wolves[w].x as i32).signum();
let dy = (a.y as i32 - self.wolves[w].y as i32).signum();
(a.x, a.y) = self.move_dir(a.x, a.y, dx, dy);
}
} else if self.rng.chance(0.5) {
(a.x, a.y) = self.wander(a.x, a.y);
}
if a.energy > 80.0 && self.rabbits.len() < 120 && self.rng.chance(0.01) {
a.energy -= 30.0;
self.rabbits.push(Animal {
x: a.x,
y: a.y,
energy: 50.0,
dead: false,
});
}
if a.energy <= 0.0 {
a.dead = true;
}
self.rabbits[i] = a;
}
fn tick_wolf(&mut self, i: usize) {
let mut a = self.wolves[i].clone();
a.energy -= 0.15;
let pos = (a.x, a.y);
if let Some(r) = Self::nearest_animal(&self.rabbits, pos, 10) {
let t = (self.rabbits[r].x, self.rabbits[r].y);
if cheb(pos, t) <= 1 {
if self.rng.chance(0.4) {
self.rabbits[r].dead = true;
a.energy = (a.energy + 50.0).min(100.0);
}
} else {
(a.x, a.y) = self.move_toward(pos, t);
}
} else {
(a.x, a.y) = self.wander(a.x, a.y);
}
if a.energy > 90.0 && self.wolves.len() < 10 && self.rng.chance(0.02) {
a.energy -= 40.0;
self.wolves.push(Animal {
x: a.x,
y: a.y,
energy: 50.0,
dead: false,
});
}
if a.energy <= 0.0 {
a.dead = true;
}
self.wolves[i] = a;
}
pub fn terrain_json(&self) -> String {
let tiles: String = self.tiles.iter().map(|t| (b'0' + t) as char).collect();
format!("{{\"w\":{},\"h\":{},\"tiles\":\"{}\"}}", W, H, tiles)
}
pub fn state_json(&self) -> String {
let mut s = String::new();
let _ = write!(
s,
"{{\"tick\":{},\"day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"stock\":{{\"food\":{},\"wood\":{},\"stone\":{},\"tools\":{}}},\"home\":[{},{}],",
self.tick,
self.tick / DAY + 1,
(self.tick % DAY) as f32 / DAY as f32 * 24.0,
self.daylight(),
self.food,
self.wood,
self.stone,
self.tools,
self.home.0,
self.home.1
);
s.push_str("\"houses\":[");
let j: Vec<String> = self
.houses
.iter()
.map(|h| format!("[{},{}]", h.0, h.1))
.collect();
s.push_str(&j.join(","));
s.push_str("],\"people\":[");
let j: Vec<String> = self
.people
.iter()
.map(|p| {
format!(
"[{},{},{},\"{}\",{:.0},{},{},{}]",
p.id, p.x, p.y, p.action, p.hunger, p.food, p.wood, p.stone
)
})
.collect();
s.push_str(&j.join(","));
s.push_str("],\"rabbits\":[");
let j: Vec<String> = self
.rabbits
.iter()
.map(|a| format!("[{},{},{:.0}]", a.x, a.y, a.energy))
.collect();
s.push_str(&j.join(","));
s.push_str("],\"wolves\":[");
let j: Vec<String> = self
.wolves
.iter()
.map(|a| format!("[{},{},{:.0}]", a.x, a.y, a.energy))
.collect();
s.push_str(&j.join(","));
s.push_str("],\"res\":[");
let mut first = true;
for (i, r) in self.res.iter().enumerate() {
if r.0 != 0 {
if !first {
s.push(',');
}
first = false;
let _ = write!(s, "[{},{},{},{}]", i % W, i / W, r.0, r.1);
}
}
s.push_str("],\"log\":[");
let j: Vec<String> = self.log.iter().map(|l| format!("\"{}\"", l)).collect();
s.push_str(&j.join(","));
s.push_str("]}");
s
}
}