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,
dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state instead
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
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.

View file

@ -317,31 +317,53 @@ fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -
};
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
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
{
company.current_cash = 0;
company.debt = 0;
company.active = false;
if state.selected_company_id == Some(company_id) {
state.selected_company_id = None;
if let Some(remaining_debt) = remaining_debt {
company.debt = remaining_debt.min(u64::from(u32::MAX)) as u64;
}
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
}
@ -3143,20 +3165,32 @@ mod tests {
state.service_state.annual_finance_last_actions.get(&31),
Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy)
);
assert!(!state.companies[0].active);
assert_eq!(state.companies[0].current_cash, 0);
assert_eq!(state.companies[0].debt, 0);
assert_eq!(state.selected_company_id, None);
assert!(state.trains[0].retired);
assert_eq!(state.companies[0].debt, 250_000);
assert!(state.companies[0].active);
assert_eq!(state.selected_company_id, Some(31));
assert!(!state.trains[0].retired);
assert_eq!(
state.service_state.company_market_state[&31].last_bankruptcy_year,
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!(
state.service_state.company_market_state[&31]
.live_bond_slots
.is_empty()
state.service_state.company_market_state[&31].live_bond_slots
== vec![crate::RuntimeCompanyBondSlot {
slot_index: 0,
principal: 250_000,
maturity_year: 0,
coupon_rate_raw_u32: 0.08f32.to_bits(),
}]
);
assert!(
result
@ -3298,20 +3332,32 @@ mod tests {
state.service_state.annual_finance_last_actions.get(&32),
Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback)
);
assert!(!state.companies[0].active);
assert_eq!(state.companies[0].current_cash, 0);
assert_eq!(state.companies[0].debt, 0);
assert_eq!(state.selected_company_id, None);
assert!(state.trains[0].retired);
assert_eq!(state.companies[0].debt, 125_000);
assert!(state.companies[0].active);
assert_eq!(state.selected_company_id, Some(32));
assert!(!state.trains[0].retired);
assert_eq!(
state.service_state.company_market_state[&32].last_bankruptcy_year,
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!(
state.service_state.company_market_state[&32]
.live_bond_slots
.is_empty()
state.service_state.company_market_state[&32].live_bond_slots
== vec![crate::RuntimeCompanyBondSlot {
slot_index: 0,
principal: 125_000,
maturity_year: 0,
coupon_rate_raw_u32: 0.07f32.to_bits(),
}]
);
assert!(
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
chooses one annual-finance action per active company and already commits the shellless
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
next seam for shellless repayment work, and now also derives the current live coupon burden
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,
stock-issue, and bond-issue branches directly into owned dividend, company stat-post,
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,
runtime state, and annual bond summaries, which is the right next base for shellless repayment and
bond-service simulation.