Rehost bankruptcy debt-halving finance path

This commit is contained in:
Jan Petykiewicz 2026-04-18 01:48:09 -07:00
commit ad048f1528
4 changed files with 85 additions and 33 deletions

View file

@ -109,6 +109,8 @@ the shellless creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-a
stock-repurchase, stock-issue, and bond-issue branches by mutating owned company activity, stock-repurchase, stock-issue, and bond-issue branches by mutating owned company activity,
dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state instead dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state instead
of stopping at reader-only diagnostics. of stopping at reader-only diagnostics.
Those bankruptcy branches now follow the grounded owner semantics too: they stamp the bankruptcy
year and halve live bond principals in place instead of treating bankruptcy as a liquidation path.
The same save-native live bond-slot surface now also carries per-slot maturity years all the way The same save-native live bond-slot surface now also carries per-slot maturity years all the way
through runtime summaries and annual bond policy state, which is the next owner seam needed for through runtime summaries and annual bond policy state, which is the next owner seam needed for
shellless repayment and bond-burden simulation instead of another round of raw-slot guessing. shellless repayment and bond-burden simulation instead of another round of raw-slot guessing.

View file

@ -317,31 +317,53 @@ fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -
}; };
let mut company_mutated = false; let mut company_mutated = false;
if let Some(market_state) = state.service_state.company_market_state.get_mut(&company_id) {
market_state.last_bankruptcy_year = bankruptcy_year;
for slot in &mut market_state.live_bond_slots {
slot.principal /= 2;
}
market_state.live_bond_slots.retain(|slot| slot.principal > 0);
market_state.bond_count = market_state.live_bond_slots.len().min(u8::MAX as usize) as u8;
market_state.largest_live_bond_principal =
market_state.live_bond_slots.iter().map(|slot| slot.principal).max();
market_state.highest_coupon_live_bond_principal = market_state
.live_bond_slots
.iter()
.filter_map(|slot| {
let coupon = f32::from_bits(slot.coupon_rate_raw_u32) as f64;
coupon.is_finite().then_some((coupon, slot.principal))
})
.max_by(|left, right| {
left.0
.partial_cmp(&right.0)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(_, principal)| principal);
company_mutated = true;
}
let remaining_debt = state
.service_state
.company_market_state
.get(&company_id)
.map(|market_state| {
market_state
.live_bond_slots
.iter()
.map(|slot| u64::from(slot.principal))
.sum::<u64>()
});
if let Some(company) = state if let Some(company) = state
.companies .companies
.iter_mut() .iter_mut()
.find(|company| company.company_id == company_id) .find(|company| company.company_id == company_id)
{ {
company.current_cash = 0; if let Some(remaining_debt) = remaining_debt {
company.debt = 0; company.debt = remaining_debt.min(u64::from(u32::MAX)) as u64;
company.active = false;
if state.selected_company_id == Some(company_id) {
state.selected_company_id = None;
} }
company_mutated = true; company_mutated = true;
} }
if let Some(market_state) = state.service_state.company_market_state.get_mut(&company_id) {
market_state.last_bankruptcy_year = bankruptcy_year;
market_state.bond_count = 0;
market_state.largest_live_bond_principal = None;
market_state.highest_coupon_live_bond_principal = None;
market_state.live_bond_slots.clear();
company_mutated = true;
}
let retired_company_ids = vec![company_id];
retire_matching_trains(&mut state.trains, Some(&retired_company_ids), None, None);
company_mutated company_mutated
} }
@ -3143,20 +3165,32 @@ mod tests {
state.service_state.annual_finance_last_actions.get(&31), state.service_state.annual_finance_last_actions.get(&31),
Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy) Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy)
); );
assert!(!state.companies[0].active);
assert_eq!(state.companies[0].current_cash, 0); assert_eq!(state.companies[0].current_cash, 0);
assert_eq!(state.companies[0].debt, 0); assert_eq!(state.companies[0].debt, 250_000);
assert_eq!(state.selected_company_id, None); assert!(state.companies[0].active);
assert!(state.trains[0].retired); assert_eq!(state.selected_company_id, Some(31));
assert!(!state.trains[0].retired);
assert_eq!( assert_eq!(
state.service_state.company_market_state[&31].last_bankruptcy_year, state.service_state.company_market_state[&31].last_bankruptcy_year,
1845 1845
); );
assert_eq!(state.service_state.company_market_state[&31].bond_count, 0); assert_eq!(state.service_state.company_market_state[&31].bond_count, 1);
assert_eq!(
state.service_state.company_market_state[&31].largest_live_bond_principal,
Some(250_000)
);
assert_eq!(
state.service_state.company_market_state[&31].highest_coupon_live_bond_principal,
Some(250_000)
);
assert!( assert!(
state.service_state.company_market_state[&31] state.service_state.company_market_state[&31].live_bond_slots
.live_bond_slots == vec![crate::RuntimeCompanyBondSlot {
.is_empty() slot_index: 0,
principal: 250_000,
maturity_year: 0,
coupon_rate_raw_u32: 0.08f32.to_bits(),
}]
); );
assert!( assert!(
result result
@ -3298,20 +3332,32 @@ mod tests {
state.service_state.annual_finance_last_actions.get(&32), state.service_state.annual_finance_last_actions.get(&32),
Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback) Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback)
); );
assert!(!state.companies[0].active);
assert_eq!(state.companies[0].current_cash, 0); assert_eq!(state.companies[0].current_cash, 0);
assert_eq!(state.companies[0].debt, 0); assert_eq!(state.companies[0].debt, 125_000);
assert_eq!(state.selected_company_id, None); assert!(state.companies[0].active);
assert!(state.trains[0].retired); assert_eq!(state.selected_company_id, Some(32));
assert!(!state.trains[0].retired);
assert_eq!( assert_eq!(
state.service_state.company_market_state[&32].last_bankruptcy_year, state.service_state.company_market_state[&32].last_bankruptcy_year,
1845 1845
); );
assert_eq!(state.service_state.company_market_state[&32].bond_count, 0); assert_eq!(state.service_state.company_market_state[&32].bond_count, 1);
assert_eq!(
state.service_state.company_market_state[&32].largest_live_bond_principal,
Some(125_000)
);
assert_eq!(
state.service_state.company_market_state[&32].highest_coupon_live_bond_principal,
Some(125_000)
);
assert!( assert!(
state.service_state.company_market_state[&32] state.service_state.company_market_state[&32].live_bond_slots
.live_bond_slots == vec![crate::RuntimeCompanyBondSlot {
.is_empty() slot_index: 0,
principal: 125_000,
maturity_year: 0,
coupon_rate_raw_u32: 0.07f32.to_bits(),
}]
); );
assert!( assert!(
result result

View file

@ -150,7 +150,9 @@ The highest-value next passes are now:
stock-repurchase gate headlessly as another pure reader; periodic boundary service now also stock-repurchase gate headlessly as another pure reader; periodic boundary service now also
chooses one annual-finance action per active company and already commits the shellless chooses one annual-finance action per active company and already commits the shellless
creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase, creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase,
stock-issue, and bond-issue branches against owned runtime state; the same live bond-slot owner stock-issue, and bond-issue branches against owned runtime state, with bankruptcy now following
the grounded “halve live bond debt and stamp the year” path rather than a liquidation shortcut;
the same live bond-slot owner
surface now also carries save-native maturity years into annual bond policy summaries as the surface now also carries save-native maturity years into annual bond policy summaries as the
next seam for shellless repayment work, and now also derives the current live coupon burden next seam for shellless repayment work, and now also derives the current live coupon burden
directly from owned bond slots directly from owned bond slots

View file

@ -247,6 +247,8 @@ the runtime selects one annual-finance action per active company and already com
creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase, creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase,
stock-issue, and bond-issue branches directly into owned dividend, company stat-post, stock-issue, and bond-issue branches directly into owned dividend, company stat-post,
outstanding-share, issue-calendar, live bond-slot, and company activity state. outstanding-share, issue-calendar, live bond-slot, and company activity state.
The bankruptcy branches now follow the grounded owner semantics too: they stamp the bankruptcy
year and halve live bond principals in place instead of collapsing into a liquidation-only path.
The same owned live bond-slot surface now also carries maturity years through save import, The same owned live bond-slot surface now also carries maturity years through save import,
runtime state, and annual bond summaries, which is the right next base for shellless repayment and runtime state, and annual bond summaries, which is the right next base for shellless repayment and
bond-service simulation. bond-service simulation.