feat: create the world simulation server
This commit is contained in:
commit
5760d097f7
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ascii"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chunked_transfer"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sim"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tiny_http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny_http"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
|
||||||
|
dependencies = [
|
||||||
|
"ascii",
|
||||||
|
"chunked_transfer",
|
||||||
|
"httpdate",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "sim"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tiny_http = "0.12"
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>World Simulation</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #111;
|
||||||
|
color: #ddd;
|
||||||
|
font: 13px monospace;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#side {
|
||||||
|
width: 280px;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log div {
|
||||||
|
margin: 2px 0;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hover {
|
||||||
|
color: #fc6;
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<canvas id="c"></canvas>
|
||||||
|
<div id="side">
|
||||||
|
<h3>World</h3>
|
||||||
|
<div id="hud"></div>
|
||||||
|
<h3>Selected</h3>
|
||||||
|
<div id="hover">click a tile or drag to select a region</div>
|
||||||
|
<h3>Events</h3>
|
||||||
|
<div id="log"></div>
|
||||||
|
<h3>Legend</h3>
|
||||||
|
<div>yellow: person, white: rabbit, red: wolf<br>green dot: tree, pink: berries, grey: stone<br>brown square: house
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const T = 10;
|
||||||
|
const COLORS = ['#10305e', '#2a62a8', '#e0d08a', '#6aaa4a', '#3f7d3a', '#8a8a6a', '#aaaaaa'];
|
||||||
|
const cv = document.getElementById('c');
|
||||||
|
const ctx = cv.getContext('2d');
|
||||||
|
let terrain = null, off = null, state = null;
|
||||||
|
|
||||||
|
async function loadTerrain() {
|
||||||
|
const t = await (await fetch('/api/terrain')).json();
|
||||||
|
terrain = t;
|
||||||
|
cv.width = t.w * T; cv.height = t.h * T;
|
||||||
|
off = document.createElement('canvas');
|
||||||
|
off.width = cv.width; off.height = cv.height;
|
||||||
|
const o = off.getContext('2d');
|
||||||
|
for (let y = 0; y < t.h; y++)
|
||||||
|
for (let x = 0; x < t.w; x++) {
|
||||||
|
o.fillStyle = COLORS[+t.tiles[y * t.w + x]];
|
||||||
|
o.fillRect(x * T, y * T, T, T);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dot(x, y, r, color) {
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x * T + T / 2, y * T + T / 2, r, 0, 7);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (!off || !state) return;
|
||||||
|
ctx.drawImage(off, 0, 0);
|
||||||
|
for (const [x, y, k] of state.res) {
|
||||||
|
if (k === 1) dot(x, y, 3.5, '#1d4a1d');
|
||||||
|
else if (k === 2) dot(x, y, 2, '#e85a8a');
|
||||||
|
else { ctx.fillStyle = '#666'; ctx.fillRect(x * T + 2, y * T + 2, 6, 6); }
|
||||||
|
}
|
||||||
|
ctx.fillStyle = '#7a4a22';
|
||||||
|
for (const [x, y] of state.houses) ctx.fillRect(x * T + 1, y * T + 1, T - 2, T - 2);
|
||||||
|
ctx.fillStyle = '#b05a2a';
|
||||||
|
ctx.fillRect(state.home[0] * T, state.home[1] * T, T, T);
|
||||||
|
for (const [x, y] of state.rabbits) dot(x, y, 2.5, '#fff');
|
||||||
|
for (const [x, y] of state.wolves) dot(x, y, 4, '#d22');
|
||||||
|
for (const p of state.people) dot(p[1], p[2], 4, '#ff0');
|
||||||
|
const dark = (1 - state.daylight) * 0.65;
|
||||||
|
ctx.fillStyle = `rgba(5,10,50,${dark})`;
|
||||||
|
ctx.fillRect(0, 0, cv.width, cv.height);
|
||||||
|
if (sel) {
|
||||||
|
const [x0, y0, x1, y1] = selRect();
|
||||||
|
ctx.strokeStyle = '#fff';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(x0 * T, y0 * T, (x1 - x0 + 1) * T, (y1 - y0 + 1) * T);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function poll() {
|
||||||
|
try {
|
||||||
|
state = await (await fetch('/api/state')).json();
|
||||||
|
const s = state.stock, h = Math.floor(state.hour), m = Math.floor((state.hour % 1) * 60);
|
||||||
|
document.getElementById('hud').innerHTML =
|
||||||
|
`Day ${state.day}, ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}<br>` +
|
||||||
|
`People: ${state.people.length}<br>Rabbits: ${state.rabbits.length}<br>Wolves: ${state.wolves.length}<br>` +
|
||||||
|
`Houses: ${state.houses.length}<br>Stockpile: food ${s.food}, wood ${s.wood}, stone ${s.stone}, tools ${s.tools}`;
|
||||||
|
document.getElementById('log').innerHTML = state.log.map(l => `<div>${l}</div>`).join('');
|
||||||
|
draw();
|
||||||
|
showSelection();
|
||||||
|
} catch (e) { }
|
||||||
|
setTimeout(poll, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TERRAIN = ['Deep water', 'Shallow water', 'Sand', 'Grass', 'Forest', 'Hills', 'Mountain'];
|
||||||
|
const RES = { 1: 'Tree', 2: 'Berry bush', 3: 'Stone deposit' };
|
||||||
|
let sel = null, dragging = false;
|
||||||
|
|
||||||
|
function selRect() {
|
||||||
|
return [Math.min(sel.x0, sel.x1), Math.min(sel.y0, sel.y1), Math.max(sel.x0, sel.x1), Math.max(sel.y0, sel.y1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function tileAt(e) {
|
||||||
|
const r = cv.getBoundingClientRect();
|
||||||
|
const x = Math.floor((e.clientX - r.left) * (cv.width / r.width) / T);
|
||||||
|
const y = Math.floor((e.clientY - r.top) * (cv.height / r.height) / T);
|
||||||
|
return { x: Math.max(0, Math.min(terrain.w - 1, x)), y: Math.max(0, Math.min(terrain.h - 1, y)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSelection() {
|
||||||
|
const el = document.getElementById('hover');
|
||||||
|
if (!sel || !state || !terrain) return;
|
||||||
|
const [x0, y0, x1, y1] = selRect();
|
||||||
|
const inside = (x, y) => x >= x0 && x <= x1 && y >= y0 && y <= y1;
|
||||||
|
const people = state.people.filter(p => inside(p[1], p[2]));
|
||||||
|
const rabbits = state.rabbits.filter(a => inside(a[0], a[1]));
|
||||||
|
const wolves = state.wolves.filter(a => inside(a[0], a[1]));
|
||||||
|
const res = state.res.filter(r => inside(r[0], r[1]));
|
||||||
|
const houses = state.houses.filter(h => inside(h[0], h[1]));
|
||||||
|
const counts = {};
|
||||||
|
for (let y = y0; y <= y1; y++)
|
||||||
|
for (let x = x0; x <= x1; x++) {
|
||||||
|
const n = TERRAIN[+terrain.tiles[y * terrain.w + x]];
|
||||||
|
counts[n] = (counts[n] || 0) + 1;
|
||||||
|
}
|
||||||
|
let html;
|
||||||
|
if (x0 === x1 && y0 === y1) {
|
||||||
|
html = `<b>Tile (${x0}, ${y0})</b><br>Terrain: ${Object.keys(counts)[0]}`;
|
||||||
|
if (state.home[0] === x0 && state.home[1] === y0) html += '<br>Village center (stockpile)';
|
||||||
|
if (houses.length) html += '<br>House';
|
||||||
|
for (const r of res) html += `<br>${RES[r[2]]}: ${r[3]} left`;
|
||||||
|
} else {
|
||||||
|
html = `<b>Region (${x0},${y0}) to (${x1},${y1})</b><br>` +
|
||||||
|
`Terrain: ${Object.entries(counts).map(([k, v]) => `${k} ${v}`).join(', ')}<br>` +
|
||||||
|
`Resources: ` + [1, 2, 3].map(k => `${RES[k]} ${res.filter(r => r[2] === k).length}`).join(', ') +
|
||||||
|
`<br>Houses: ${houses.length}`;
|
||||||
|
}
|
||||||
|
html += `<br>People: ${people.length}, rabbits: ${rabbits.length}, wolves: ${wolves.length}`;
|
||||||
|
for (const p of people)
|
||||||
|
html += `<br>- Person #${p[0]}: ${p[3]}, hunger ${p[4]}, carrying food ${p[5]} wood ${p[6]} stone ${p[7]}`;
|
||||||
|
if (rabbits.length + wolves.length <= 6) {
|
||||||
|
for (const a of rabbits) html += `<br>- Rabbit: energy ${a[2]}`;
|
||||||
|
for (const a of wolves) html += `<br>- Wolf: energy ${a[2]}`;
|
||||||
|
}
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
cv.addEventListener('mousedown', e => {
|
||||||
|
if (!terrain) return;
|
||||||
|
const t = tileAt(e);
|
||||||
|
sel = { x0: t.x, y0: t.y, x1: t.x, y1: t.y };
|
||||||
|
dragging = true;
|
||||||
|
draw(); showSelection();
|
||||||
|
});
|
||||||
|
cv.addEventListener('mousemove', e => {
|
||||||
|
if (!dragging) return;
|
||||||
|
const t = tileAt(e);
|
||||||
|
sel.x1 = t.x; sel.y1 = t.y;
|
||||||
|
draw(); showSelection();
|
||||||
|
});
|
||||||
|
window.addEventListener('mouseup', () => { dragging = false; });
|
||||||
|
|
||||||
|
loadTerrain().then(poll);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
mod world;
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tiny_http::{Header, Response, Server};
|
||||||
|
use world::World;
|
||||||
|
|
||||||
|
const INDEX: &str = include_str!("index.html");
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let seed = std::env::args()
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(42u64);
|
||||||
|
let addr = std::env::var("SIM_ADDR").unwrap_or_else(|_| "127.0.0.1:7878".into());
|
||||||
|
let world = Arc::new(Mutex::new(World::new(seed)));
|
||||||
|
|
||||||
|
{
|
||||||
|
let world = Arc::clone(&world);
|
||||||
|
thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
world.lock().unwrap().step();
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let server = Server::http(&addr).expect("failed to bind");
|
||||||
|
println!("Simulation running at http://{addr}");
|
||||||
|
for req in server.incoming_requests() {
|
||||||
|
let world = Arc::clone(&world);
|
||||||
|
thread::spawn(move || {
|
||||||
|
let (body, ctype) = match req.url() {
|
||||||
|
"/" => (INDEX.to_string(), "text/html; charset=utf-8"),
|
||||||
|
"/api/terrain" => (world.lock().unwrap().terrain_json(), "application/json"),
|
||||||
|
"/api/state" => (world.lock().unwrap().state_json(), "application/json"),
|
||||||
|
_ => {
|
||||||
|
let _ = req.respond(Response::from_string("not found").with_status_code(404));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let header = Header::from_bytes("Content-Type", ctype).unwrap();
|
||||||
|
let _ = req.respond(Response::from_string(body).with_header(header));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,671 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue