Refactor marriage pregnancy logic in UserCharacterWorker and SQL: Updated the conception process to occur daily instead of hourly, simplifying the fertility mechanics. Enhanced documentation for clarity on the new daily conception approach and adjusted related SQL queries to reflect these changes. Introduced a mechanism to track the last fertility date to prevent multiple conceptions in a single day.

This commit is contained in:
Torsten Schulz (local)
2026-03-30 10:21:05 +02:00
parent ac7bd1d7ed
commit 1258abbb42
2 changed files with 80 additions and 48 deletions

View File

@@ -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 (657016000 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#"

View File

@@ -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<f64>,
last_hourly_run: Option<Instant>,
last_pregnancy_run: Option<Instant>,
/// Letzter Kalendertag, an dem Ehe-Konzeption (oder Legacy-Instant) gelaufen ist — höchstens 1×/Tag.
last_marriage_fertility_date: Option<NaiveDate>,
/// `None` = noch nicht geprüft, ob Migration 008 (`marriage_pregnancy_due_at`) existiert.
marriage_pregnancy_column_ready: Option<bool>,
last_mood_run: Option<Instant>,
@@ -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(())
}