Enhance production certificate and election result handling: Updated the logic in run_daily to improve clarity on level progression based on weighted scores, including detailed logging for cases where players meet minimum requirements but do not advance. Introduced a new function to notify players of election results, integrating SQL queries for efficient data retrieval and notification management. Enhanced documentation for the production certificate process to clarify UI vs. daemon discrepancies.
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 2m54s
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 2m54s
This commit is contained in:
@@ -17,6 +17,19 @@ Implementierung: `src/worker/falukant_certificate.rs` (`run_daily`).
|
||||
- Aufstieg nur wenn `effective_target > current` → **`current + 1`** (gegen `effective_target` und 5 begrenzt)
|
||||
- **Bankrott** (`money <= -5000`): Zertifikat auf **1**, mit Event
|
||||
|
||||
### Wichtig: UI vs. Daemon („48 h kein Aufstieg“)
|
||||
|
||||
Die **Mindestanforderungen** (z. B. Wissen ≥ 28, Produktionen ≥ 15 für Stufe 3) sind **nur ein Teil**. Zusätzlich gilt eine **Obergrenze aus der gewichteten Wertung** (`certificateScore` → `raw_target`): Es wird die **höchste Stufe ≤ `raw_target`**, die **alle** Mindestanforderungen erfüllt (`effective_certificate_target` in `falukant_certificate.rs`).
|
||||
|
||||
Typische Folge: Die UI zeigt **nur** zwei grüne Häkchen (Wissen/Produktionen), der Spieler bleibt aber auf Stufe 2, weil:
|
||||
|
||||
1. **`raw_target` = 2** (Wertung **unter** 1,8) — dann ist Stufe 3 **fachlich ausgeschlossen**, auch wenn die Mindestzahlen für Stufe 3 erfüllt sind. Oft liegt die Wertung knapp unter 1,8, wenn z. B. **Produktionspunkte** im Daemon niedrig sind (Bucket < 20 abgeschlossene Produktionen in `falukant_log.production` trotz höherer Anzeige in der UI).
|
||||
2. **Abweichende Eingangsdaten** gegenüber der UI: anderer gewählter Charakter (`DISTINCT ON … c.id DESC`), andere Zählung `falukant_log.production` (`producer_id`), Geld/Bankrott, etc.
|
||||
|
||||
**Diagnose:** Daemon mit `YPDAEMON_CERT_VERBOSE=1` starten. Wenn jemand die Mindestanforderungen für die **nächste** Stufe erfüllt, aber **nicht** aufsteigt, erscheint eine Zeile `[falukant_certificate] fu_id=… bleibt auf Stufe …` mit `certificate_score`, `raw_target`, `effective_target` und Rohwerten.
|
||||
|
||||
**SQL:** `QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS` in `src/worker/sql.rs` — für einen Betroffenen `falukant_user.id` filtern und mit der UI abgleichen.
|
||||
|
||||
## Politische Ämter
|
||||
|
||||
Rang aus **`political_office_type.name`** (Substring-Heuristik im Daemon, ohne DB-Änderung). Anpassung über `political_name_to_rank` in `falukant_certificate.rs`.
|
||||
|
||||
@@ -81,6 +81,41 @@ pub fn run_daily(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbErro
|
||||
house_points,
|
||||
);
|
||||
|
||||
// Hilfe bei „UI sagt bereit, Stufe steigt nicht“: oft Mindestwerte erfüllt, aber
|
||||
// `raw_target` (gewichtete Wertung) zu niedrig — siehe `docs/FALUKANT_PRODUCTION_CERTIFICATE.md`.
|
||||
if current < 5 {
|
||||
let next = current + 1;
|
||||
if meets_min_for_level(
|
||||
next,
|
||||
avg_knowledge,
|
||||
completed,
|
||||
office_points,
|
||||
nobility_points,
|
||||
reputation_points,
|
||||
house_points,
|
||||
) && effective_target < next
|
||||
&& cert_verbose()
|
||||
{
|
||||
eprintln!(
|
||||
"[falukant_certificate] fu_id={} bleibt auf Stufe {}: Mindestanforderungen für Stufe {} sind erfüllt, aber die gewichtete Wertung {:.3} ergibt raw_target={} (effective_target={}). avg_knowledge={:.2} completed={} kp={} pp={} office={} nob={} rep_p={} house_p={}",
|
||||
fu_id,
|
||||
current,
|
||||
next,
|
||||
certificate_score,
|
||||
raw_target,
|
||||
effective_target,
|
||||
avg_knowledge,
|
||||
completed,
|
||||
knowledge_points,
|
||||
production_points,
|
||||
office_points,
|
||||
nobility_points,
|
||||
reputation_points,
|
||||
house_points,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let new_certificate = if effective_target > current {
|
||||
(current + 1).min(effective_target).min(5)
|
||||
} else {
|
||||
@@ -102,6 +137,13 @@ pub fn run_daily(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbErro
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cert_verbose() -> bool {
|
||||
matches!(
|
||||
std::env::var("YPDAEMON_CERT_VERBOSE").map(|v| v == "1" || v.eq_ignore_ascii_case("true")),
|
||||
Ok(true)
|
||||
)
|
||||
}
|
||||
|
||||
/// Nach Tod ohne gültigen Erben: Zertifikat auf Stufe 1 (Spec: Erbfolge bricht ab).
|
||||
pub fn reset_certificate_on_succession_no_heir(
|
||||
base: &BaseWorker,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::db::{ConnectionPool, DbError, Row};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use serde_json::json;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
@@ -20,6 +21,8 @@ use crate::worker::sql::{
|
||||
QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS,
|
||||
QUERY_GET_USERS_WITH_FILLED_OFFICES,
|
||||
QUERY_PROCESS_ELECTIONS,
|
||||
QUERY_PLAYER_ELECTION_RESULT_ROWS,
|
||||
QUERY_INSERT_NOTIFICATION,
|
||||
QUERY_TRIM_EXCESS_OFFICES_GLOBAL,
|
||||
QUERY_FIND_AVAILABLE_CHURCH_OFFICES,
|
||||
QUERY_FIND_CHURCH_SUPERVISOR,
|
||||
@@ -206,6 +209,9 @@ impl PoliticsWorker {
|
||||
|
||||
// 7) Wahlen auswerten und neu besetzte Ämter melden
|
||||
let new_offices_from_elections = Self::process_elections(pool)?;
|
||||
if let Err(e) = Self::notify_player_election_results(pool, broker) {
|
||||
eprintln!("[PoliticsWorker] notify_player_election_results: {e}");
|
||||
}
|
||||
if !new_offices_from_elections.is_empty() {
|
||||
Self::notify_office_filled(pool, broker, &new_offices_from_elections)?;
|
||||
}
|
||||
@@ -618,6 +624,94 @@ impl PoliticsWorker {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Benachrichtigt Spieler-Kandidaten mit Wahlergebnis (Stimmen, Rang, Gewinner, gewonnen?).
|
||||
/// `tr` = JSON mit `event: "election_result"` (siehe `QUERY_PLAYER_ELECTION_RESULT_ROWS`).
|
||||
fn notify_player_election_results(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare(
|
||||
"player_election_results",
|
||||
QUERY_PLAYER_ELECTION_RESULT_ROWS,
|
||||
)
|
||||
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare player_election_results: {e}")))?;
|
||||
let rows = conn
|
||||
.execute("player_election_results", &[])
|
||||
.map_err(|e| DbError::new(format!("[PoliticsWorker] exec player_election_results: {e}")))?;
|
||||
|
||||
conn.prepare("insert_notification_json", QUERY_INSERT_NOTIFICATION)
|
||||
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare insert_notification_json: {e}")))?;
|
||||
|
||||
for row in rows {
|
||||
let user_id = parse_i32(&row, "user_id", -1);
|
||||
let election_id = parse_i32(&row, "election_id", -1);
|
||||
if user_id < 1 || election_id < 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let office_type_id = parse_i32(&row, "office_type_id", -1);
|
||||
let region_id = parse_i32(&row, "region_id", -1);
|
||||
let character_id = parse_i32(&row, "character_id", -1);
|
||||
let candidate_id = parse_i32(&row, "candidate_id", -1);
|
||||
let your_votes = row
|
||||
.get("your_votes")
|
||||
.and_then(|v| v.parse::<i64>().ok())
|
||||
.unwrap_or(0);
|
||||
let rank_in_election = parse_i32(&row, "rank_in_election", 0);
|
||||
let won = row
|
||||
.get("won")
|
||||
.map(|v| v == "t" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
let top_character_id = parse_i32(&row, "top_character_id", -1);
|
||||
let top_votes = row
|
||||
.get("top_votes")
|
||||
.and_then(|v| v.parse::<i64>().ok())
|
||||
.unwrap_or(0);
|
||||
let posts_to_fill = parse_i32(&row, "posts_to_fill", 0);
|
||||
|
||||
let office_name = row.get("office_name").cloned().unwrap_or_default();
|
||||
let region_name = row.get("region_name").cloned().unwrap_or_default();
|
||||
|
||||
let payload = json!({
|
||||
"event": "election_result",
|
||||
"user_id": user_id,
|
||||
"election_id": election_id,
|
||||
"office_type_id": office_type_id,
|
||||
"office_name": office_name,
|
||||
"region_id": region_id,
|
||||
"region_name": region_name,
|
||||
"character_id": character_id,
|
||||
"candidate_id": candidate_id,
|
||||
"your_votes": your_votes,
|
||||
"rank": rank_in_election,
|
||||
"won": won,
|
||||
"leading_character_id": top_character_id,
|
||||
"leading_votes": top_votes,
|
||||
"posts_to_fill": posts_to_fill,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
conn.execute("insert_notification_json", &[&user_id, &payload])
|
||||
.map_err(|e| {
|
||||
DbError::new(format!(
|
||||
"[PoliticsWorker] insert election_result notification uid={} eid={}: {e}",
|
||||
user_id, election_id
|
||||
))
|
||||
})?;
|
||||
|
||||
broker.publish(payload.clone());
|
||||
let status =
|
||||
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
broker.publish(status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Täglich: freie Sitze, Bewerbungen (Spieler bleiben offen), NPC-Vorgesetzte entscheiden per Score,
|
||||
/// NPC-Bewerbungen erzeugen, Interimsbesetzung ohne Vorgesetzten (niedrige Hierarchie).
|
||||
fn perform_church_office_task(
|
||||
|
||||
@@ -1208,6 +1208,87 @@ pub const QUERY_PROCESS_ELECTIONS: &str = r#"
|
||||
FROM falukant_data.process_elections();
|
||||
"#;
|
||||
|
||||
/// Wahlergebnis für Spieler-Kandidaten (`falukant_data.candidate` + `character.user_id IS NOT NULL`).
|
||||
/// Läuft nach `process_elections()`; Deduplizierung über bestehende `election_result`-Notifications (`tr` JSON).
|
||||
pub const QUERY_PLAYER_ELECTION_RESULT_ROWS: &str = r#"
|
||||
WITH vote_counts AS (
|
||||
SELECT c.id AS candidate_id,
|
||||
c.election_id,
|
||||
c.character_id,
|
||||
COUNT(v.id)::bigint AS vote_count
|
||||
FROM falukant_data.candidate c
|
||||
LEFT JOIN falukant_data.vote v ON v.candidate_id = c.id
|
||||
GROUP BY c.id, c.election_id, c.character_id
|
||||
),
|
||||
top_per_election AS (
|
||||
SELECT DISTINCT ON (vc.election_id)
|
||||
vc.election_id,
|
||||
vc.character_id AS top_character_id,
|
||||
vc.vote_count AS top_votes
|
||||
FROM vote_counts vc
|
||||
ORDER BY vc.election_id, vc.vote_count DESC, vc.candidate_id
|
||||
),
|
||||
eligible AS (
|
||||
SELECT e.id AS election_id,
|
||||
e.office_type_id,
|
||||
e.region_id,
|
||||
e.posts_to_fill,
|
||||
vc.candidate_id,
|
||||
vc.character_id,
|
||||
vc.vote_count,
|
||||
ch.user_id
|
||||
FROM falukant_data.election e
|
||||
JOIN vote_counts vc ON vc.election_id = e.id
|
||||
JOIN falukant_data.character ch ON ch.id = vc.character_id
|
||||
WHERE ch.user_id IS NOT NULL
|
||||
AND e.date::date <= CURRENT_DATE
|
||||
AND e.date::date >= CURRENT_DATE - INTERVAL '7 days'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM falukant_log.notification n
|
||||
WHERE n.user_id = ch.user_id
|
||||
AND n.tr LIKE '{%'
|
||||
AND (n.tr::jsonb->>'event') = 'election_result'
|
||||
AND (n.tr::jsonb->>'election_id')::int = e.id
|
||||
)
|
||||
),
|
||||
ranked AS (
|
||||
SELECT vc.candidate_id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY vc.election_id
|
||||
ORDER BY vc.vote_count DESC, vc.candidate_id
|
||||
) AS rank_in_election
|
||||
FROM vote_counts vc
|
||||
)
|
||||
SELECT elig.user_id,
|
||||
elig.election_id,
|
||||
elig.office_type_id,
|
||||
COALESCE(pot.name::text, '') AS office_name,
|
||||
elig.region_id,
|
||||
COALESCE(NULLIF(TRIM(dr.name::text), ''), elig.region_id::text) AS region_name,
|
||||
elig.character_id,
|
||||
elig.candidate_id,
|
||||
elig.vote_count AS your_votes,
|
||||
COALESCE(rk.rank_in_election, 0)::int AS rank_in_election,
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM falukant_data.political_office po
|
||||
WHERE po.character_id = elig.character_id
|
||||
AND po.office_type_id = elig.office_type_id
|
||||
AND po.region_id = elig.region_id
|
||||
AND po.created_at >= (CURRENT_TIMESTAMP - INTERVAL '3 days')
|
||||
) AS won,
|
||||
tpe.top_character_id,
|
||||
tpe.top_votes,
|
||||
elig.posts_to_fill
|
||||
FROM eligible elig
|
||||
JOIN falukant_type.political_office_type pot ON pot.id = elig.office_type_id
|
||||
LEFT JOIN falukant_data.region dr ON dr.id = elig.region_id
|
||||
LEFT JOIN top_per_election tpe ON tpe.election_id = elig.election_id
|
||||
LEFT JOIN ranked rk ON rk.candidate_id = elig.candidate_id
|
||||
ORDER BY elig.election_id, elig.user_id;
|
||||
"#;
|
||||
|
||||
pub const QUERY_TRIM_EXCESS_OFFICES_GLOBAL: &str = r#"
|
||||
WITH seats AS (
|
||||
SELECT
|
||||
|
||||
Reference in New Issue
Block a user