use std::iter::{repeat, zip};
use std::sync::Arc;
use dashmap::DashMap;
use itertools::Itertools;
use rayon::prelude::*;
use crate::ally::Ally;
use crate::allyset::AllySet;
use crate::death::{self, DeathReason};
use crate::decision::{DecisionPath, SquadSelection};
use crate::outcome::{DecisionPathMetadata, Outcome, OutcomeMap};
pub fn outcome_map() -> OutcomeMap {
OutcomeMapGenerator::new()
.generate()
.into_iter()
.par_bridge()
.collect()
}
type OutcomeDashMap = DashMap<Outcome, DecisionPathMetadata>;
#[derive(Clone)]
struct OutcomeMapGenerator {
choices: DecisionPathLedger,
outcomes: Arc<OutcomeDashMap>,
}
impl OutcomeMapGenerator {
fn new() -> Self {
Self {
choices: Default::default(),
outcomes: Arc::new(OutcomeDashMap::new()),
}
}
fn generate(self) -> OutcomeDashMap {
(3..=AllySet::RECRUITABLE.len())
.into_par_iter()
.for_each_init(|| self.clone(), Self::recruitment);
Arc::try_unwrap(self.outcomes).expect("`outcomes` has only one strong reference")
}
fn recruitment(&mut self, count: usize) {
AllySet::RECRUITABLE
.into_iter()
.combinations(count)
.map(|combo| combo.into_iter().collect::<AllySet>())
.par_bridge()
.for_each_init(|| self.clone(), Self::loyalty_missions);
}
fn loyalty_missions(&mut self, recruits: AllySet) {
self.choices.recruitment = recruits;
let allies = AllySet::REQUIRED | recruits;
let loyalty_mission_iter = allies
.into_iter()
.powerset()
.map(|subset| subset.into_iter().collect::<AllySet>());
zip(repeat(allies), loyalty_mission_iter)
.par_bridge()
.for_each_init(|| self.clone(), Self::recruit_morinth);
}
fn recruit_morinth(&mut self, (allies, loyalty_missions): (AllySet, AllySet)) {
self.choices.loyalty_missions = loyalty_missions;
self.upgrade_armor(allies);
if loyalty_missions.contains(Ally::Samara) {
self.choices.recruitment ^= AllySet::ASARI;
self.upgrade_armor(allies ^ AllySet::ASARI);
}
}
fn loyal(&self) -> AllySet {
self.choices.loyalty_missions | Ally::Morinth
}
fn upgrade_armor(&mut self, allies: AllySet) {
self.choices.upgraded_armor = true;
self.upgrade_shield(allies);
self.choices.upgraded_armor = false;
let victim = DeathReason::NoArmorUpgrade.get_victim(allies);
self.upgrade_shield(allies - victim);
}
fn upgrade_shield(&mut self, allies: AllySet) {
self.choices.cargo_bay_squad = SquadSelection::new();
self.choices.upgraded_shield = true;
self.upgrade_weapon(allies);
self.choices.upgraded_shield = false;
let mut potential_victims = allies;
for _ in 0..3 {
let victim = DeathReason::NoShieldUpgrade.get_victim(potential_victims);
self.choices.cargo_bay_squad.add_victim(victim);
self.upgrade_weapon(allies - victim);
potential_victims -= victim;
}
}
fn upgrade_weapon(&mut self, allies: AllySet) {
self.choices.upgraded_weapon = true;
self.tech_specialist(allies);
self.choices.upgraded_weapon = false;
let victim = DeathReason::NoWeaponUpgrade.get_victim(allies);
self.tech_specialist(allies - victim);
}
fn tech_specialist(&mut self, allies: AllySet) {
for tech in allies & AllySet::TECHS {
self.choices.tech_specialist = Some(tech);
self.second_fireteam_leader(allies);
}
}
fn second_fireteam_leader(&mut self, allies: AllySet) {
self.choices.ideal_second_fireteam_leaders = AllySet::NOBODY;
self.choices.second_fireteam_leader_is_ideal = false;
let tech = self.choices.tech_specialist.unwrap();
let ideal_leaders = (allies - tech) & self.loyal() & AllySet::IDEAL_LEADERS;
if !(self.loyal() & AllySet::IDEAL_TECHS).contains(tech) || ideal_leaders.is_empty() {
self.biotic_specialist(allies - tech);
return;
}
self.choices.ideal_second_fireteam_leaders = ideal_leaders;
self.biotic_specialist(allies - tech);
self.choices.second_fireteam_leader_is_ideal = true;
self.biotic_specialist(allies);
}
fn biotic_specialist(&mut self, allies: AllySet) {
for biotic in allies & AllySet::BIOTICS {
self.choices.biotic_specialist = Some(biotic);
self.diversion_team_leader(allies);
}
}
fn diversion_team_leader(&mut self, allies: AllySet) {
let selectable_allies = allies - self.choices.biotic_specialist;
for leader in selectable_allies {
self.choices.diversion_team_leader = Some(leader);
self.crew_escort(allies);
}
}
fn crew_escort(&mut self, allies: AllySet) {
self.choices.crew_escort = None;
self.the_long_walk(allies);
if allies.len() > 4 {
let selectable_allies = (allies & AllySet::ESCORTS)
- self.choices.biotic_specialist
- self.choices.diversion_team_leader;
for escort in selectable_allies {
self.choices.crew_escort = Some(escort);
let disloyal_escort = !self.loyal() & escort;
self.the_long_walk(allies - disloyal_escort);
}
}
}
fn the_long_walk(&mut self, allies: AllySet) {
self.choices.the_long_walk = SquadSelection::new();
let biotic = self.choices.biotic_specialist.unwrap();
if (self.loyal() & AllySet::IDEAL_BIOTICS).contains(biotic) {
self.final_squad(allies);
return;
}
let escort = self.choices.crew_escort;
let leader = self.choices.diversion_team_leader;
let mut potential_victims = allies - biotic - escort - leader;
if potential_victims.len() <= 2 {
let victim = DeathReason::BadBiotic.get_victim(potential_victims);
self.final_squad(allies - victim);
return;
}
while potential_victims.len() > 1 {
let victim = DeathReason::BadBiotic.get_victim(potential_victims);
self.choices.the_long_walk.add_victim(victim);
self.final_squad(allies - victim);
potential_victims -= victim;
}
}
fn final_squad(&mut self, allies: AllySet) {
let escort = AllySet::from(self.choices.crew_escort);
let active_allies = allies - escort;
let leader = self.choices.diversion_team_leader.unwrap();
let leader_survives = active_allies.len() < 4
|| AllySet::IMMORTAL_LEADERS.contains(leader)
|| (AllySet::IDEAL_LEADERS & self.loyal()).contains(leader);
let victim = AllySet::from((!leader_survives).then_some(leader));
let allies = allies - victim;
let active_allies = active_allies - victim;
for squad in active_allies
.into_iter()
.combinations(2)
.map(|combo| combo.into_iter().collect::<AllySet>())
{
self.choices.final_squad = squad;
let defenders = active_allies - squad;
let victims =
death::get_defense_victims(defenders, self.loyal()) | (squad - self.loyal());
self.record_outcome(allies - victims);
}
}
fn record_outcome(&mut self, survivors: AllySet) {
let outcome = Outcome {
survivors,
loyal: survivors & self.loyal(),
rescued_crew: self.choices.crew_escort.is_some(),
};
self.outcomes
.entry(outcome)
.and_modify(|metadata| metadata.count += 1)
.or_insert_with(|| DecisionPathMetadata {
count: 1,
example: self.choices.complete(),
});
}
}
#[derive(Clone, Default)]
struct DecisionPathLedger {
recruitment: AllySet,
loyalty_missions: AllySet,
upgraded_armor: bool,
upgraded_shield: bool,
cargo_bay_squad: SquadSelection,
upgraded_weapon: bool,
tech_specialist: Option<Ally>,
ideal_second_fireteam_leaders: AllySet,
second_fireteam_leader_is_ideal: bool,
biotic_specialist: Option<Ally>,
diversion_team_leader: Option<Ally>,
crew_escort: Option<Ally>,
the_long_walk: SquadSelection,
final_squad: AllySet,
}
impl DecisionPathLedger {
pub fn complete(&self) -> DecisionPath {
DecisionPath {
recruitment: self.recruitment,
loyalty_missions: self.loyalty_missions,
upgraded_armor: self.upgraded_armor,
upgraded_shield: self.upgraded_shield,
cargo_bay_squad: self.cargo_bay_squad,
upgraded_weapon: self.upgraded_weapon,
tech_specialist: self.tech_specialist.expect("tech specialist was selected"),
ideal_second_fireteam_leaders: self.ideal_second_fireteam_leaders,
second_fireteam_leader_is_ideal: self.second_fireteam_leader_is_ideal,
biotic_specialist: self
.biotic_specialist
.expect("biotic specialist was selected"),
diversion_team_leader: self
.diversion_team_leader
.expect("diversion team leader was selected"),
crew_escort: self.crew_escort,
the_long_walk: self.the_long_walk,
final_squad: self.final_squad,
}
}
}