Add symbolic company target runtime import

This commit is contained in:
Jan Petykiewicz 2026-04-15 09:13:51 -07:00
commit f918d0c4f7
17 changed files with 1230 additions and 80 deletions

View file

@ -3,7 +3,8 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{
RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate, RuntimeState, RuntimeSummary,
RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimeState, RuntimeSummary,
calendar::BoundaryEventKind,
};
@ -430,6 +431,49 @@ fn resolve_company_target_ids(
}
Ok(ids.clone())
}
RuntimeCompanyTarget::HumanCompanies => {
if state
.companies
.iter()
.any(|company| company.controller_kind == RuntimeCompanyControllerKind::Unknown)
{
return Err(
"target requires company role context but at least one company has unknown controller_kind"
.to_string(),
);
}
Ok(state
.companies
.iter()
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Human)
.map(|company| company.company_id)
.collect())
}
RuntimeCompanyTarget::AiCompanies => {
if state
.companies
.iter()
.any(|company| company.controller_kind == RuntimeCompanyControllerKind::Unknown)
{
return Err(
"target requires company role context but at least one company has unknown controller_kind"
.to_string(),
);
}
Ok(state
.companies
.iter()
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Ai)
.map(|company| company.company_id)
.collect())
}
RuntimeCompanyTarget::SelectedCompany => state
.selected_company_id
.map(|company_id| vec![company_id])
.ok_or_else(|| "target requires selected_company_id context".to_string()),
RuntimeCompanyTarget::ConditionTrueCompany => {
Err("target requires condition-evaluation context".to_string())
}
}
}
@ -451,9 +495,9 @@ mod tests {
use super::*;
use crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimeSaveProfileState, RuntimeServiceState,
RuntimeWorldRestoreState,
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeWorldRestoreState,
};
fn state() -> RuntimeState {
@ -470,9 +514,11 @@ mod tests {
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 0,
}],
selected_company_id: None,
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
@ -630,11 +676,13 @@ mod tests {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 5,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 20,
debt: 8,
},
@ -674,6 +722,165 @@ mod tests {
assert_eq!(result.service_events[0].mutated_company_ids, vec![2]);
}
#[test]
fn resolves_symbolic_company_targets() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20,
debt: 2,
},
],
selected_company_id: Some(1),
event_runtime_records: vec![
RuntimeEventRecord {
record_id: 11,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 5,
}],
},
RuntimeEventRecord {
record_id: 12,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AiCompanies,
delta: 3,
}],
},
RuntimeEventRecord {
record_id: 13,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
delta: 7,
}],
},
],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("symbolic target effects should succeed");
assert_eq!(state.companies[0].current_cash, 22);
assert_eq!(state.companies[0].debt, 0);
assert_eq!(state.companies[1].current_cash, 20);
assert_eq!(state.companies[1].debt, 5);
assert_eq!(result.service_events[0].mutated_company_ids, vec![1, 2]);
}
#[test]
fn rejects_selected_company_target_without_selection_context() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 14,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::SelectedCompany,
delta: 1,
}],
}],
..state()
};
let error = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect_err("selected company target should require selection context");
assert!(error.contains("selected_company_id"));
}
#[test]
fn rejects_human_or_ai_targets_without_role_context() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 15,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 1,
}],
}],
..state()
};
let error = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect_err("human target should require controller metadata");
assert!(error.contains("controller_kind"));
}
#[test]
fn rejects_condition_true_company_target_without_condition_context() {
let mut state = RuntimeState {
event_runtime_records: vec![RuntimeEventRecord {
record_id: 16,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::ConditionTrueCompany,
delta: 1,
}],
}],
..state()
};
let error = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect_err("condition-relative target should remain blocked");
assert!(error.contains("condition-evaluation context"));
}
#[test]
fn one_shot_record_only_fires_once() {
let mut state = RuntimeState {
@ -718,6 +925,7 @@ mod tests {
let mut state = RuntimeState {
companies: vec![RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 2,
}],