Refactor Falukant certificate management: Consolidated certificate logic into the FalukantFamilyWorker's daily processing, removing the separate FalukantCertificateWorker. Updated SQL queries to include app_user_id and enhanced documentation for clarity on certificate scoring and daily recalculation logic.
This commit is contained in:
@@ -1,132 +1,98 @@
|
||||
//! Produktionszertifikat: tägliche Neuberechnung von `falukant_user.certificate`.
|
||||
//! Spec: docs/FALUKANT_PRODUCTION_CERTIFICATE.md
|
||||
//! Produktionszertifikat: tägliche Neuberechnung von `falukant_user.certificate` im **FalukantFamilyWorker-Daily-Tick**
|
||||
//! (nicht in einem eigenen Worker-Thread). Spec: `docs/FALUKANT_PRODUCTION_CERTIFICATE.md` und
|
||||
//! „Falukant: Produktionszertifikate – Fach- und Integrationsspezifikation“.
|
||||
|
||||
use crate::db::{ConnectionPool, DbError, Row};
|
||||
use crate::db::{DbError, Row};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
use super::base::BaseWorker;
|
||||
use crate::worker::sql::{
|
||||
QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS, QUERY_UPDATE_FALUKANT_USER_CERTIFICATE,
|
||||
};
|
||||
|
||||
const DAILY_INTERVAL: Duration = Duration::from_secs(24 * 3600);
|
||||
|
||||
/// Wenn `money` darunter liegt, gilt der Spieler als bankrott → Zertifikat auf Stufe 1.
|
||||
/// Wenn `money` darunter liegt, gilt der Spieler als bankrott → Zertifikat auf Stufe 1 (Spec §4.7).
|
||||
const BANKRUPTCY_MONEY_THRESHOLD: f64 = -5000.0;
|
||||
|
||||
pub struct FalukantCertificateWorker {
|
||||
base: BaseWorker,
|
||||
}
|
||||
/// Einmal pro Daily-Tick (`FalukantFamilyWorker::process_daily`).
|
||||
pub fn run_daily(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbError> {
|
||||
let pool = &base.pool;
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
impl FalukantCertificateWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new("FalukantCertificateWorker", pool, broker),
|
||||
conn.prepare("cert_rows", QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS)?;
|
||||
conn.prepare("cert_upd", QUERY_UPDATE_FALUKANT_USER_CERTIFICATE)?;
|
||||
let rows = conn.execute("cert_rows", &[])?;
|
||||
|
||||
for row in rows {
|
||||
let fu_id = parse_i32(&row, "falukant_user_id", -1);
|
||||
if fu_id < 0 {
|
||||
continue;
|
||||
}
|
||||
let app_uid = parse_i32(&row, "app_user_id", -1);
|
||||
let event_uid = if app_uid > 0 { app_uid } else { fu_id };
|
||||
|
||||
let current = parse_i32(&row, "certificate", 1).clamp(1, 5);
|
||||
let money = parse_f64(&row, "money", 0.0);
|
||||
let avg_knowledge = parse_f64(&row, "avg_knowledge", 0.0);
|
||||
let completed = parse_i64(&row, "completed_production_count", 0);
|
||||
let max_church_hierarchy = parse_i32(&row, "max_church_hierarchy", 0);
|
||||
let pol_names = row
|
||||
.get("political_office_names")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let reputation = parse_f64(&row, "reputation", 50.0);
|
||||
let title_level = parse_i32(&row, "title_level", 0);
|
||||
let house_position = parse_i32(&row, "house_position", 0);
|
||||
|
||||
// Bankrott: Herabsetzung (Spec §4.7)
|
||||
if money <= BANKRUPTCY_MONEY_THRESHOLD && current > 1 {
|
||||
conn.execute("cert_upd", &[&1_i32, &fu_id])?;
|
||||
publish_certificate_event(broker, event_uid, current, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let knowledge_points = knowledge_points_from_avg(avg_knowledge);
|
||||
let production_points = production_points_from_count(completed);
|
||||
let political_rank = max_political_rank_from_names(&pol_names);
|
||||
let church_rank = church_rank_from_hierarchy(max_church_hierarchy);
|
||||
let highest_office_rank = political_rank.max(church_rank).min(5);
|
||||
let office_points = highest_office_rank.min(5);
|
||||
let nobility_points = (title_level - 1).clamp(0, 5);
|
||||
let reputation_points = reputation_points_from_rep(reputation);
|
||||
let house_points = house_points_from_position(house_position);
|
||||
|
||||
let certificate_score = knowledge_points as f64 * 0.45
|
||||
+ production_points as f64 * 0.30
|
||||
+ office_points as f64 * 0.08
|
||||
+ nobility_points as f64 * 0.05
|
||||
+ reputation_points as f64 * 0.07
|
||||
+ house_points as f64 * 0.05;
|
||||
|
||||
let raw_target = raw_target_from_score(certificate_score);
|
||||
let effective_target = effective_certificate_target(
|
||||
raw_target,
|
||||
avg_knowledge,
|
||||
completed,
|
||||
office_points,
|
||||
nobility_points,
|
||||
reputation_points,
|
||||
house_points,
|
||||
);
|
||||
|
||||
let new_certificate = if effective_target > current {
|
||||
(current + 1).min(effective_target).min(5)
|
||||
} else {
|
||||
current
|
||||
};
|
||||
|
||||
if new_certificate != current {
|
||||
conn.execute("cert_upd", &[&new_certificate, &fu_id])?;
|
||||
publish_certificate_event(broker, event_uid, current, new_certificate);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
|
||||
let mut last: Option<std::time::Instant> = None;
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
let now = std::time::Instant::now();
|
||||
let run = match last {
|
||||
None => true,
|
||||
Some(t) => now.saturating_duration_since(t) >= DAILY_INTERVAL,
|
||||
};
|
||||
if run {
|
||||
if let Err(e) = Self::process_daily(&pool, &broker) {
|
||||
eprintln!("[FalukantCertificateWorker] process_daily: {e}");
|
||||
}
|
||||
last = Some(now);
|
||||
}
|
||||
for _ in 0..60 {
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_daily(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("cert_rows", QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS)?;
|
||||
conn.prepare("cert_upd", QUERY_UPDATE_FALUKANT_USER_CERTIFICATE)?;
|
||||
let rows = conn.execute("cert_rows", &[])?;
|
||||
|
||||
for row in rows {
|
||||
let fu_id = parse_i32(&row, "falukant_user_id", -1);
|
||||
if fu_id < 0 {
|
||||
continue;
|
||||
}
|
||||
let current = parse_i32(&row, "certificate", 1).clamp(1, 127);
|
||||
let money = parse_f64(&row, "money", 0.0);
|
||||
let avg_knowledge = parse_f64(&row, "avg_knowledge", 0.0);
|
||||
let completed = parse_i64(&row, "completed_production_count", 0);
|
||||
let max_church_hierarchy = parse_i32(&row, "max_church_hierarchy", 0);
|
||||
let pol_names = row
|
||||
.get("political_office_names")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let reputation = parse_f64(&row, "reputation", 50.0);
|
||||
let title_level = parse_i32(&row, "title_level", 0);
|
||||
let house_position = parse_i32(&row, "house_position", 0);
|
||||
|
||||
// Bankrott: Herabsetzung erlaubt (Spec)
|
||||
if money <= BANKRUPTCY_MONEY_THRESHOLD && current > 1 {
|
||||
conn.execute("cert_upd", &[&1_i32, &fu_id])?;
|
||||
publish_certificate_event(broker, fu_id, current, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let knowledge_points = knowledge_points_from_avg(avg_knowledge);
|
||||
let production_points = production_points_from_count(completed);
|
||||
let political_rank = max_political_rank_from_names(&pol_names);
|
||||
let church_rank = church_rank_from_hierarchy(max_church_hierarchy);
|
||||
let highest_office_rank = political_rank.max(church_rank).min(5);
|
||||
let office_points = highest_office_rank.min(5);
|
||||
let nobility_points = (title_level - 1).clamp(0, 5);
|
||||
let reputation_points = reputation_points_from_rep(reputation);
|
||||
let house_points = house_points_from_position(house_position);
|
||||
|
||||
let certificate_score = knowledge_points as f64 * 0.35
|
||||
+ production_points as f64 * 0.20
|
||||
+ office_points as f64 * 0.15
|
||||
+ nobility_points as f64 * 0.10
|
||||
+ reputation_points as f64 * 0.10
|
||||
+ house_points as f64 * 0.10;
|
||||
|
||||
let raw_target = raw_target_from_score(certificate_score);
|
||||
let effective_target = effective_certificate_target(
|
||||
raw_target,
|
||||
avg_knowledge,
|
||||
completed,
|
||||
office_points,
|
||||
nobility_points,
|
||||
reputation_points,
|
||||
house_points,
|
||||
);
|
||||
|
||||
let new_certificate = if effective_target > current {
|
||||
(current + 1).min(effective_target)
|
||||
} else {
|
||||
current
|
||||
};
|
||||
|
||||
if new_certificate != current {
|
||||
conn.execute("cert_upd", &[&new_certificate, &fu_id])?;
|
||||
publish_certificate_event(broker, fu_id, current, new_certificate);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn publish_certificate_event(broker: &MessageBroker, user_id: i32, old_c: i32, new_c: i32) {
|
||||
@@ -189,7 +155,6 @@ fn production_points_from_count(n: i64) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kirchliche Ämter: `hierarchy_level` auf 0–5 begrenzen (Daemon).
|
||||
fn church_rank_from_hierarchy(h: i32) -> i32 {
|
||||
if h <= 0 {
|
||||
0
|
||||
@@ -198,7 +163,7 @@ fn church_rank_from_hierarchy(h: i32) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Politische Amtsnamen → Rang 1–5 (konfigurierbar im Daemon).
|
||||
/// Politische Amtsnamen → Rang 1–5 (Heuristik im Daemon, ohne DB-Änderung).
|
||||
fn political_name_to_rank(name: &str) -> i32 {
|
||||
let n = name.to_lowercase();
|
||||
if n.contains("reich")
|
||||
@@ -282,72 +247,36 @@ fn house_points_from_position(position: i32) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Spec §4.6 Schwellen für die Zielstufe (vor Mindestanforderungen).
|
||||
fn raw_target_from_score(score: f64) -> i32 {
|
||||
if score >= 4.0 {
|
||||
if score >= 3.8 {
|
||||
5
|
||||
} else if score >= 3.0 {
|
||||
} else if score >= 2.8 {
|
||||
4
|
||||
} else if score >= 2.1 {
|
||||
} else if score >= 1.8 {
|
||||
3
|
||||
} else if score >= 1.2 {
|
||||
} else if score >= 0.9 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn status_one(
|
||||
office_points: i32,
|
||||
nobility_points: i32,
|
||||
reputation_points: i32,
|
||||
house_points: i32,
|
||||
) -> bool {
|
||||
office_points >= 1
|
||||
|| nobility_points >= 1
|
||||
|| reputation_points >= 2
|
||||
|| house_points >= 1
|
||||
}
|
||||
|
||||
fn status_count_cert4(
|
||||
office_points: i32,
|
||||
nobility_points: i32,
|
||||
reputation_points: i32,
|
||||
house_points: i32,
|
||||
) -> i32 {
|
||||
fn cert5_two_of_social(office_points: i32, nobility_points: i32, house_points: i32) -> bool {
|
||||
let mut c = 0;
|
||||
if office_points >= 1 {
|
||||
if office_points >= 2 {
|
||||
c += 1;
|
||||
}
|
||||
if nobility_points >= 1 {
|
||||
c += 1;
|
||||
}
|
||||
if reputation_points >= 2 {
|
||||
c += 1;
|
||||
}
|
||||
if house_points >= 1 {
|
||||
c += 1;
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
fn cert5_extra_two(
|
||||
office_points: i32,
|
||||
nobility_points: i32,
|
||||
house_points: i32,
|
||||
) -> i32 {
|
||||
let mut c = 0;
|
||||
if office_points >= 2 {
|
||||
c += 1;
|
||||
}
|
||||
if nobility_points >= 2 {
|
||||
c += 1;
|
||||
}
|
||||
if house_points >= 2 {
|
||||
c += 1;
|
||||
}
|
||||
c
|
||||
c >= 2
|
||||
}
|
||||
|
||||
/// Mindestanforderungen je Stufe (Spec §4.5).
|
||||
fn meets_min_for_level(
|
||||
level: i32,
|
||||
avg_knowledge: f64,
|
||||
@@ -359,38 +288,27 @@ fn meets_min_for_level(
|
||||
) -> bool {
|
||||
match level {
|
||||
1 => true,
|
||||
2 => avg_knowledge >= 25.0 && completed >= 5,
|
||||
3 => {
|
||||
avg_knowledge >= 40.0
|
||||
&& completed >= 20
|
||||
&& status_one(
|
||||
office_points,
|
||||
nobility_points,
|
||||
reputation_points,
|
||||
house_points,
|
||||
)
|
||||
}
|
||||
2 => avg_knowledge >= 15.0 && completed >= 4,
|
||||
3 => avg_knowledge >= 28.0 && completed >= 15,
|
||||
4 => {
|
||||
avg_knowledge >= 55.0
|
||||
&& completed >= 60
|
||||
&& status_count_cert4(
|
||||
office_points,
|
||||
nobility_points,
|
||||
reputation_points,
|
||||
house_points,
|
||||
) >= 2
|
||||
avg_knowledge >= 45.0
|
||||
&& completed >= 45
|
||||
&& (office_points >= 1
|
||||
|| nobility_points >= 1
|
||||
|| reputation_points >= 2
|
||||
|| house_points >= 2)
|
||||
}
|
||||
5 => {
|
||||
avg_knowledge >= 70.0
|
||||
&& completed >= 150
|
||||
&& reputation_points >= 3
|
||||
&& cert5_extra_two(office_points, nobility_points, house_points) >= 2
|
||||
avg_knowledge >= 60.0
|
||||
&& completed >= 110
|
||||
&& reputation_points >= 2
|
||||
&& cert5_two_of_social(office_points, nobility_points, house_points)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Höchste Stufe ≤ `raw_target`, die alle Mindestanforderungen erfüllt.
|
||||
/// Höchste Stufe ≤ `raw_target`, die alle Mindestanforderungen erfüllt (Spec §4.6).
|
||||
fn effective_certificate_target(
|
||||
raw_target: i32,
|
||||
avg_knowledge: f64,
|
||||
@@ -416,23 +334,3 @@ fn effective_certificate_target(
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
impl Worker for FalukantCertificateWorker {
|
||||
fn start_worker_thread(&mut self) {
|
||||
let pool = self.base.pool.clone();
|
||||
let broker = self.base.broker.clone();
|
||||
|
||||
self.base
|
||||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||||
FalukantCertificateWorker::run_loop(pool.clone(), broker.clone(), state);
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user