diff --git a/crates/sb-api/src/routes/mod.rs b/crates/sb-api/src/routes/mod.rs index edd8d65..343fc10 100644 --- a/crates/sb-api/src/routes/mod.rs +++ b/crates/sb-api/src/routes/mod.rs @@ -25,6 +25,11 @@ use sb_shared::{ use std::sync::Arc; use tokio::sync::RwLock; +fn request_rng() -> rand::rngs::StdRng { + use rand::SeedableRng; + rand::rngs::StdRng::from_os_rng() +} + pub(crate) struct AppState { users: RwLock>, items: Vec, @@ -285,7 +290,10 @@ async fn create_character( return Err(axum::http::StatusCode::BAD_REQUEST); } - let mut char = character::create_character(user_id, name, input.race, input.class); + let mut char = { + let mut rng = request_rng(); + character::create_character(&mut rng, user_id, name, input.race, input.class) + }; { let mut id = state.next_char_id.write().await; @@ -424,6 +432,8 @@ async fn run_adventure( .unwrap() .as_millis() as i64; + let mut rng = request_rng(); + let (char, enemy_def) = { let mut chars = state.characters.write().await; let char = chars.iter_mut().find(|c| c.id == char_id) @@ -439,7 +449,6 @@ async fn run_adventure( } char.nerve -= zone.nerve_cost; - let mut rng = rand::rng(); let enemy = domain::pick_enemy(&mut rng, &zone.encounters, &state.enemies); (char.clone(), enemy) }; @@ -494,14 +503,13 @@ async fn run_adventure( songs::BARD_SONGS.iter().find(|s| s.id == sid).map(|s| s.combat_effect) }), spell_effect.as_ref(), - &mut rand::rng(), + &mut rng, ); let (loot_names, updated_char) = { let mut chars = state.characters.write().await; let dropped_ids: Vec = if let Some(c) = chars.iter_mut().find(|c| c.id == char_id) { use rand::Rng; - let mut rng = rand::rng(); if log.winner == c.name { c.xp += log.xp_gained as i64; diff --git a/crates/sb-core/src/character.rs b/crates/sb-core/src/character.rs index 1c89d2d..927fe04 100644 --- a/crates/sb-core/src/character.rs +++ b/crates/sb-core/src/character.rs @@ -126,10 +126,15 @@ pub fn training_cost(stat: i64) -> i32 { (5.0_f64 * 1.3_f64.powi(scaled)) as i32 } -pub fn create_character(user_id: u32, name: String, race: Race, class: Class) -> Character { - let mut rng = rand::rng(); +pub fn create_character( + rng: &mut impl rand::Rng, + user_id: u32, + name: String, + race: Race, + class: Class, +) -> Character { let class_str = class.as_str().to_string(); - let stats = roll_stats(&mut rng, &race); + let stats = roll_stats(rng, &race); let max_hp = calc_max_hp(&class, &stats); let max_energy = calc_max_energy(&class); let max_nerve = calc_max_nerve(&class); @@ -412,4 +417,28 @@ mod tests { assert!(dwarf.strength > human.strength); assert!(dwarf.intelligence < human.intelligence); } + + #[test] + fn create_character_is_deterministic_under_seeded_rng() { + use rand::SeedableRng; + let first = create_character( + &mut rand::rngs::StdRng::seed_from_u64(42), + 7, + "Twin".into(), + Race::Human, + Class::Warrior, + ); + let second = create_character( + &mut rand::rngs::StdRng::seed_from_u64(42), + 7, + "Twin".into(), + Race::Human, + Class::Warrior, + ); + assert_eq!(format!("{:?}", first.stats), format!("{:?}", second.stats)); + assert_eq!(first.hp, second.hp); + assert_eq!(first.max_hp, second.max_hp); + assert_eq!(first.energy, second.energy); + assert_eq!(first.nerve, second.nerve); + } }