1
0
mirror of https://github.com/osmarks/ewo3.git synced 2026-03-15 14:59:43 +00:00
This commit is contained in:
osmarks
2026-03-04 18:48:11 +00:00
parent a323d18582
commit 708eea7b43
3 changed files with 43 additions and 8 deletions

View File

@@ -9,6 +9,7 @@ use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::tungstenite::protocol::Message;
use tokio::sync::{mpsc, Mutex};
use anyhow::{Result, Context, anyhow};
use argh::FromArgs;
use std::{convert::TryFrom, hash::{Hash, Hasher}, net::SocketAddr, ops::DerefMut, sync::Arc, time::Duration};
use slab::Slab;
use serde::{Serialize, Deserialize};
@@ -20,6 +21,17 @@ pub mod world_serde;
use map::*;
#[derive(FromArgs)]
/// Run the game server.
struct Args {
/// websocket listen address
#[argh(option, default = "String::from(\"0.0.0.0:8011\")")]
listen_addr: String,
/// simulation tick interval in milliseconds
#[argh(option, default = "56")]
tick_interval_ms: u64,
}
async fn handle_connection(raw_stream: TcpStream, addr: SocketAddr, mut frames_rx: mpsc::Receiver<Frame>, inputs_tx: mpsc::Sender<Input>) -> Result<()> {
let ws_stream = tokio_tungstenite::accept_async(raw_stream).await.context("websocket handshake failure")?;
let (mut outgoing, incoming) = ws_stream.split();
@@ -87,6 +99,19 @@ struct Client {
entity: Entity
}
#[derive(Debug, Serialize, Deserialize)]
struct GameMetrics {
plants_died: u64,
plants_reproduced: u64,
enemies_spawned: u64
}
impl GameMetrics {
fn new() -> Self {
GameMetrics { plants_died: 0, plants_reproduced: 0, enemies_spawned: 0 }
}
}
struct GameState {
world: World,
clients: Slab<Client>,
@@ -99,7 +124,8 @@ struct GameState {
baseline_temperature: Map<f32>,
dynamic_soil_nutrients: Map<f32>,
dynamic_groundwater: Map<f32>,
positions: PositionIndex
positions: PositionIndex,
metrics: GameMetrics
}
impl GameState {
@@ -496,6 +522,7 @@ impl SavedGame {
dynamic_soil_nutrients: self.dynamic_soil_nutrients,
dynamic_groundwater: self.dynamic_groundwater,
positions,
metrics: GameMetrics::new()
})
}
}
@@ -524,7 +551,7 @@ const SOIL_NUTRIENT_CONSUMPTION_RATE: f32 = 0.04;
const SOIL_NUTRIENT_FIXATION_RATE: f32 = 0.0002;
const WATER_CONSUMPTION_RATE: f32 = 0.02;
const PLANT_IDLE_WATER_CONSUMPTION_OFFSET: f32 = 0.05;
const PLANT_DIEOFF_THRESHOLD: f32 = 0.01;
const PLANT_DIEOFF_THRESHOLD: f32 = 0.3;
const PLANT_DIEOFF_RATE: f32 = 0.2;
const SAVE_FILE: &str = "save.bin";
const AUTOSAVE_INTERVAL_TICKS: u64 = 1024;
@@ -648,6 +675,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
))
};
*spawn_count += 1;
state.metrics.enemies_spawned += 1;
}
}
}
@@ -672,6 +700,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
// TODO: this is inelegant and should be shared with the other death code
// also, it might break the position tracker
kill(&mut buffer, &mut despawn_buffer, &state, &mut rng, entity, None);
state.metrics.plants_died += 1;
}
}
}
@@ -700,6 +729,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
state.dynamic_groundwater[pos] -= water_consumed;
plant.water_consumed += water_consumed;
println!("water={} nutrient={} diff={} consume={}", water, soil_nutrients, difference, water_consumed);
if difference > 0.0 {
plant.growth_ticks += 1;
if let Ok(mut health) = state.world.get::<&mut Health>(entity) {
@@ -755,11 +786,11 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Plant::new(hybrid_genome.clone()),
NewlyAdded
));
println!("plant reproduced {:?} {:?} {} {}.", plant.genome, other_plant.genome, plant.current_size, other_plant.current_size);
plant.children += 1;
other_plant.children += 1;
plant.current_size *= plant.genome.reproductive_size_fraction(); // TODO: explicit cooldown?
other_plant.current_size *= other_plant.genome.reproductive_size_fraction(); // TODO: vary this by plant component gender?
state.metrics.plants_reproduced += 1;
break;
}
}
@@ -1140,7 +1171,9 @@ const INITIAL_PLANTS: usize = 262144;
#[tokio::main]
async fn main() -> Result<()> {
let addr = std::env::args().nth(1).unwrap_or_else(|| "0.0.0.0:8011".to_string());
let args: Args = argh::from_env();
let addr = args.listen_addr;
let tick_interval_ms = args.tick_interval_ms;
let mut loaded_save = false;
let state = if let Some(saved) = load_saved_game().await? {
println!("Loaded game state from {}", SAVE_FILE);
@@ -1176,7 +1209,8 @@ async fn main() -> Result<()> {
baseline_salt,
baseline_temperature,
dynamic_soil_nutrients,
dynamic_groundwater
dynamic_groundwater,
metrics: GameMetrics::new()
}))
};
@@ -1207,7 +1241,7 @@ async fn main() -> Result<()> {
let state_ = state.clone();
tokio::spawn(async move {
let state = state_.clone();
let mut interval = tokio::time::interval(Duration::from_millis(56));
let mut interval = tokio::time::interval(Duration::from_millis(tick_interval_ms));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
let mut state = state.lock().await;
@@ -1222,6 +1256,7 @@ async fn main() -> Result<()> {
}
let tick_elapsed = time.elapsed();
println!("Tick time: {:?}", tick_elapsed);
println!("{:?}", state.metrics);
interval.tick().await;
}
});

View File

@@ -218,7 +218,7 @@ pub fn smooth(map: &Map<f32>, radius: i32) -> Map<f32> {
// TODO: this is still really slow!
//let result = if radius < 3 {
let result = ConvExt::conv_par(&data, &kernel, ConvMode::Same, PaddingMode::Replicate).unwrap();
let result = ConvExt::conv(&data, &kernel, ConvMode::Same, PaddingMode::Replicate).unwrap();
//} else {
// ConvFFTExt::conv_fft(&data, &kernel, ConvMode::Same, PaddingMode::Replicate).unwrap()
//};

View File

@@ -60,7 +60,7 @@ impl Genome {
- salt_excess * 1.5
- self.water_tolerance * 0.2
- self.temperature_tolerance * 0.2
- self.salt_tolerance * 0.2;
- self.salt_tolerance * 0.15;
(base * (-nutrients.min(0.0)).exp()).max(0.0)
}