diff --git a/src/worker/sql.rs b/src/worker/sql.rs index a3874db..6a56c53 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -1662,9 +1662,10 @@ pub const QUERY_AUTOBATISM: &str = r#" ); "#; -// Biologische Fruchtbarkeit nach Alter der Frau (Jahres-Wahrscheinlichkeit). -// Worker würfelt stündlich: P_stunde = 1 - (1 - P_jahr)^(1/8760), damit 24×/Tag zusammen -// dieselbe kumulative Jahresaufteilung wie früher 1×/Tag mit P_tag = 1 - (1 - P_jahr)^(1/365). +// Biologische Fruchtbarkeit nach Alter der Frau (Jahres-Wahrscheinlichkeit prob_year). +// Konzeption: genau ein Wurf pro Ehe und Kalendertag (`UserCharacterWorker`), mit random() < prob_year. +// Entspricht „ein Spieljahr pro Kalendertag“: keine 24× stündlichen Versuche mehr, keine „Hochzeitsnacht“ +// als Extra-Event — erste mögliche Konzeption am nächsten täglichen Fertilitätslauf nach der Hochzeit. // Grenzen in Tagen: 1 Jahr ≈ 365 Tage (mother_age_days). // Erstes gemeinsames Kind (0 Zeilen in child_relation für dieses Paar): prob_year mindestens 1.0 // im Alter 18–~44 (6570–16000 Tage), damit kinderlose Ehen nicht über viele Jahre ohne Nachwuchs bleiben. @@ -1715,7 +1716,7 @@ pub const QUERY_CLEAR_STALE_MARRIAGE_PREGNANCY_DUE: &str = r#" AND marriage_pregnancy_due_at < NOW() - INTERVAL '30 days'; "#; -/// Stündlicher Konzeptionswurf (Ehe): bei Treffer wird `marriage_pregnancy_due_at` auf +5 Tage gesetzt. +/// Täglicher Konzeptionswurf (Ehe): bei Treffer wird `marriage_pregnancy_due_at` auf +5 Tage gesetzt. pub const QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE: &str = r#" WITH paired AS ( SELECT @@ -1811,7 +1812,7 @@ pub const QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE: &str = r#" FROM mother_age_final ma WHERE ma.mother_age_days >= 4380 AND ma.mother_age_days < 18993 - AND random() < (1 - POWER(1 - ma.prob_year, 1.0/8760.0)) + AND random() < ma.prob_year ) UPDATE falukant_data.relationship r SET marriage_pregnancy_due_at = NOW() + INTERVAL '5 days' @@ -1829,7 +1830,7 @@ pub const QUERY_MARRIAGE_PREGNANCY_COLUMN_READY: &str = r#" ) AS ready; "#; -/// Ehe: ohne Migration 008 — stündlicher Wurf legt sofort ein Kind an (wie früher). +/// Ehe: ohne Migration 008 — täglicher Wurf legt sofort ein Kind an (Daemon ruft 1×/Kalendertag auf). pub const QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES: &str = r#" WITH paired AS ( SELECT @@ -1926,11 +1927,11 @@ pub const QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES: &str = r#" father_uid, mother_uid, mother_age_days, - (1 - POWER(1 - prob_year, 1.0/8760.0)) * 100 AS prob_pct + prob_year * 100.0 AS prob_pct FROM mother_age_final WHERE mother_age_days >= 4380 AND mother_age_days < 18993 - AND random() < (1 - POWER(1 - prob_year, 1.0/8760.0)); + AND random() < prob_year; "#; pub const QUERY_INSERT_CHILD: &str = r#" diff --git a/src/worker/user_character.rs b/src/worker/user_character.rs index e0d3bb6..69490eb 100644 --- a/src/worker/user_character.rs +++ b/src/worker/user_character.rs @@ -1,5 +1,6 @@ use crate::db::{ConnectionPool, DbError, Rows}; use crate::message_broker::MessageBroker; +use chrono::{Local, NaiveDate}; use rand::distributions::{Distribution, Uniform}; use rand::rngs::StdRng; use rand::SeedableRng; @@ -64,6 +65,8 @@ pub struct UserCharacterWorker { dist: Uniform, last_hourly_run: Option, last_pregnancy_run: Option, + /// Letzter Kalendertag, an dem Ehe-Konzeption (oder Legacy-Instant) gelaufen ist — höchstens 1×/Tag. + last_marriage_fertility_date: Option, /// `None` = noch nicht geprüft, ob Migration 008 (`marriage_pregnancy_due_at`) existiert. marriage_pregnancy_column_ready: Option, last_mood_run: Option, @@ -84,6 +87,7 @@ impl UserCharacterWorker { dist, last_hourly_run: None, last_pregnancy_run: None, + last_marriage_fertility_date: None, marriage_pregnancy_column_ready: None, last_mood_run: None, last_death_check_run: None, @@ -179,25 +183,34 @@ impl UserCharacterWorker { Ok(()) } - /// Ehe-Schwangerschaft: höchstens einmal pro Stunde (Daemon-Schleife ~1 s). + /// Ehe: Geburten/stale stündlich; Konzeption höchstens einmal pro Kalendertag (ein Jahreswurf, `prob_year`). /// Konzeption setzt `marriage_pregnancy_due_at` (+5 Tage); Geburt über `QUERY_GET_MARRIAGE_BIRTH_DELIVERIES`. - /// Stündliche Zerlegung der Jahres-Wahrscheinlichkeit (`1/8760`), siehe `QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE`. - /// Erstes gemeinsames Kind: Jahreswahrscheinlichkeit mindestens 1.0 (18–~44 J.), siehe `sql.rs`. + /// Kein separates „Hochzeitsnacht“-Event — erste Konzeption am nächsten Tageslauf nach der Hochzeit. fn maybe_run_hourly_pregnancies(&mut self) { let now = Instant::now(); - let should_run = match self.last_pregnancy_run { + let today = Local::now().date_naive(); + + let run_hourly = match self.last_pregnancy_run { None => true, Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(3600), }; - if !should_run { + let run_daily_fertility = self.last_marriage_fertility_date != Some(today); + + if !run_hourly && !run_daily_fertility { return; } - if let Err(err) = self.process_pregnancies() { + if let Err(err) = self.process_pregnancies(run_hourly, run_daily_fertility) { eprintln!("[UserCharacterWorker] Fehler in processPregnancies: {err}"); } - self.last_pregnancy_run = Some(now); + + if run_hourly { + self.last_pregnancy_run = Some(now); + } + if run_daily_fertility { + self.last_marriage_fertility_date = Some(today); + } } fn process_character_events(&mut self) -> Result<(), DbError> { @@ -536,54 +549,72 @@ impl UserCharacterWorker { } // Schwangerschafts-Logik (portiert aus processPregnancies) - fn process_pregnancies(&mut self) -> Result<(), DbError> { + fn process_pregnancies(&mut self, hourly: bool, daily_fertility: bool) -> Result<(), DbError> { + self.ensure_marriage_pregnancy_schema()?; + let mut conn = self .base .pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; - conn.prepare("autobatism", QUERY_AUTOBATISM)?; - conn.execute("autobatism", &[])?; - - if self.marriage_pregnancy_column_ready.is_none() { - conn.prepare("mp_ready", QUERY_MARRIAGE_PREGNANCY_COLUMN_READY)?; - let ready_rows = conn.execute("mp_ready", &[])?; - self.marriage_pregnancy_column_ready = Some( - ready_rows - .first() - .and_then(|r| r.get("ready")) - .map(|v| v == "true" || v == "t") - .unwrap_or(false), - ); - } - let use_gestation = self.marriage_pregnancy_column_ready.unwrap_or(false); - conn.prepare("insert_child", QUERY_INSERT_CHILD)?; conn.prepare("insert_child_relation", QUERY_INSERT_CHILD_RELATION)?; - if use_gestation { - conn.prepare("get_deliveries", QUERY_GET_MARRIAGE_BIRTH_DELIVERIES)?; - conn.prepare("clear_preg", QUERY_CLEAR_MARRIAGE_PREGNANCY_DUE)?; + let use_gestation = self.marriage_pregnancy_column_ready.unwrap_or(false); - let delivery_rows = conn.execute("get_deliveries", &[])?; - for row in delivery_rows { - self.process_single_marriage_delivery(&mut conn, &row)?; - } + if hourly { + conn.prepare("autobatism", QUERY_AUTOBATISM)?; + conn.execute("autobatism", &[])?; - conn.prepare("clear_stale_preg", QUERY_CLEAR_STALE_MARRIAGE_PREGNANCY_DUE)?; - conn.execute("clear_stale_preg", &[])?; + if use_gestation { + conn.prepare("get_deliveries", QUERY_GET_MARRIAGE_BIRTH_DELIVERIES)?; + conn.prepare("clear_preg", QUERY_CLEAR_MARRIAGE_PREGNANCY_DUE)?; - conn.prepare("try_conception", QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE)?; - conn.execute("try_conception", &[])?; - } else { - conn.prepare("legacy_preg", QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES)?; - let legacy_rows = conn.execute("legacy_preg", &[])?; - for row in legacy_rows { - self.process_single_marriage_delivery(&mut conn, &row)?; + let delivery_rows = conn.execute("get_deliveries", &[])?; + for row in delivery_rows { + self.process_single_marriage_delivery(&mut conn, &row)?; + } + + conn.prepare("clear_stale_preg", QUERY_CLEAR_STALE_MARRIAGE_PREGNANCY_DUE)?; + conn.execute("clear_stale_preg", &[])?; } } + if daily_fertility { + if use_gestation { + conn.prepare("try_conception", QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE)?; + conn.execute("try_conception", &[])?; + } else { + conn.prepare("legacy_preg", QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES)?; + let legacy_rows = conn.execute("legacy_preg", &[])?; + for row in legacy_rows { + self.process_single_marriage_delivery(&mut conn, &row)?; + } + } + } + + Ok(()) + } + + fn ensure_marriage_pregnancy_schema(&mut self) -> Result<(), DbError> { + if self.marriage_pregnancy_column_ready.is_some() { + return Ok(()); + } + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("mp_ready", QUERY_MARRIAGE_PREGNANCY_COLUMN_READY)?; + let ready_rows = conn.execute("mp_ready", &[])?; + self.marriage_pregnancy_column_ready = Some( + ready_rows + .first() + .and_then(|r| r.get("ready")) + .map(|v| v == "true" || v == "t") + .unwrap_or(false), + ); Ok(()) }