diff --git a/README.md b/README.md index fa55804..399cfc1 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index 3d251b7..37ab470 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -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::() + }); 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 diff --git a/docs/README.md b/docs/README.md index b36c4e8..98f55fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 061d849..04bc10a 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -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.