character-traits-warrior-capability 0.1.0

A Rust crate for modeling and simulating a broad spectrum of warrior capabilities, aimed at enhancing intricate game mechanics and strategic simulations.
Documentation
// ---------------- [ File: character-traits-warrior-capability/src/warrior_profile_adaptive_sampler.rs ]
crate::ix!();

/// Adaptive sampler with verbose tracing.
/// Compile with `RUST_LOG=trace` (or use `traced_test`) to see everything.
#[derive(Debug, Clone, Builder, Getters)]
#[builder(pattern = "owned", setter(into, strip_option), default)]
#[getset(get = "pub")]
pub struct WarriorProfileAdaptiveSampler {
    /// Desired fraction of top‑level capability categories
    /// that should get at least one variant (e.g. 0.65 ⇒ ≈65 %).
    target_fill: f64,
    /// Desired effective sample size **per category** after soft‑max.
    ess_target:  f64,
    /// Safety clamp on temperature search.
    temp_bounds: (f64, f64),
}

impl Default for WarriorProfileAdaptiveSampler {
    fn default() -> Self {
        Self { target_fill: 0.65, ess_target: 3.0, temp_bounds: (0.05, 4.0) }
    }
}

impl WarriorProfileAdaptiveSampler {
    /*───────────────────────────────────────────────────────────────────*/
    /*  0. tiny helpers                                                 */
    /*───────────────────────────────────────────────────────────────────*/
    #[inline(always)]
    fn dot(a: &[f64], b: &[f64]) -> f64 {
        debug_assert_eq!(a.len(), b.len());
        a.iter().zip(b).map(|(x, y)| x * y).sum()
    }

    #[inline(always)]
    fn max_sim<V>(space_vec: &[f64]) -> f64
    where
        V: CapabilityVariant + Clone + 'static,
    {
        V::all_variants()
            .iter()
            .map(|v| Self::dot(space_vec, &v.intrinsic_ratings().to_vec()))
            .fold(f64::MIN, f64::max)
    }

    /*───────────────────────────────────────────────────────────────────*/
    /*  1. public entry point                                           */
    /*───────────────────────────────────────────────────────────────────*/
    pub fn sample_profile<R>(
        &self,
        space: &WarriorCapabilityIntrinsicDimensionRatings,
        rng: &mut R,
    ) -> WarriorCapabilityProfile
    where
        R: rand::Rng + ?Sized,
    {
        let space_vec = space.to_vec();

        /*--------- first pass: per‑category maxima --------------------*/
        let max_sims = [
            Self::max_sim::<WarriorBraveryCapability>(&space_vec),
            Self::max_sim::<WarriorLeadershipCapability>(&space_vec),
            Self::max_sim::<WarriorStrategicCapability>(&space_vec),
            Self::max_sim::<WarriorProtectiveCapability>(&space_vec),
            Self::max_sim::<WarriorPhysicalCapability>(&space_vec),
            Self::max_sim::<WarriorResilienceCapability>(&space_vec),
            Self::max_sim::<WarriorResponsivenessCapability>(&space_vec),
            Self::max_sim::<WarriorExplorationCapability>(&space_vec),
            Self::max_sim::<WarriorThreatAdaptationCapability>(&space_vec),
            Self::max_sim::<WarriorConflictMitigationCapability>(&space_vec),
            Self::max_sim::<WarriorApproachCapability>(&space_vec),
            Self::max_sim::<WarriorEmbodimentCapability>(&space_vec),
            Self::max_sim::<WarriorMetaCapability>(&space_vec),
            Self::max_sim::<WarriorOffensiveMagicCapability>(&space_vec),
            Self::max_sim::<WarriorDefensiveMagicCapability>(&space_vec),
            Self::max_sim::<WarriorIllusionaryMagicCapability>(&space_vec),
            Self::max_sim::<WarriorCyberDefenseCapability>(&space_vec),
            Self::max_sim::<WarriorTechAdaptationCapability>(&space_vec),
        ];

        trace!(?max_sims, "per‑category maximum similarities");

        /*--------- adaptive threshold ---------------------------------*/
        let mut sorted = max_sims;
        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
        let q          = 1.0 - self.target_fill;
        let cut_idx    = ((q * sorted.len() as f64).floor() as usize).min(sorted.len() - 1);
        let min_sim    = sorted[cut_idx];

        trace!(
            target_fill = self.target_fill,
            quantile = 1.0 - q,
            cut_idx,
            min_similarity = min_sim,
            "adaptive threshold chosen",
        );

        /*--------- second pass: build profile -------------------------*/
        WarriorCapabilityProfileBuilder::default()
            .bravery          (self.sample_set::<WarriorBraveryCapability,           R>(space, rng, min_sim))
            .leadership       (self.sample_set::<WarriorLeadershipCapability,        R>(space, rng, min_sim))
            .strategy         (self.sample_set::<WarriorStrategicCapability,         R>(space, rng, min_sim))
            .protection       (self.sample_set::<WarriorProtectiveCapability,        R>(space, rng, min_sim))
            .physical         (self.sample_set::<WarriorPhysicalCapability,          R>(space, rng, min_sim))
            .resilience       (self.sample_set::<WarriorResilienceCapability,        R>(space, rng, min_sim))
            .responsiveness   (self.sample_set::<WarriorResponsivenessCapability,    R>(space, rng, min_sim))
            .exploration      (self.sample_set::<WarriorExplorationCapability,       R>(space, rng, min_sim))
            .threat_adaptation(self.sample_set::<WarriorThreatAdaptationCapability,  R>(space, rng, min_sim))
            .conflict_mitigation(self.sample_set::<WarriorConflictMitigationCapability,R>(space, rng, min_sim))
            .approach         (self.sample_set::<WarriorApproachCapability,          R>(space, rng, min_sim))
            .embodiment       (self.sample_set::<WarriorEmbodimentCapability,        R>(space, rng, min_sim))
            .meta             (self.sample_set::<WarriorMetaCapability,              R>(space, rng, min_sim))
            .offensive_magic  (self.sample_set::<WarriorOffensiveMagicCapability,    R>(space, rng, min_sim))
            .defensive_magic  (self.sample_set::<WarriorDefensiveMagicCapability,    R>(space, rng, min_sim))
            .illusionary_magic(self.sample_set::<WarriorIllusionaryMagicCapability,  R>(space, rng, min_sim))
            .cyber_defense    (self.sample_set::<WarriorCyberDefenseCapability,      R>(space, rng, min_sim))
            .tech_adaptation  (self.sample_set::<WarriorTechAdaptationCapability,    R>(space, rng, min_sim))
            .build()
            .unwrap()
    }

    /*───────────────────────────────────────────────────────────────────*/
    /*  2. per‑category sampling                                        */
    /*───────────────────────────────────────────────────────────────────*/
    #[instrument(level = "trace", skip(self, space, rng))]
    fn sample_set<V, R>(
        &self,
        space: &WarriorCapabilityIntrinsicDimensionRatings,
        rng: &mut R,
        min_sim: f64,
    ) -> HashSet<V>
    where
        V: CapabilityVariant + Clone + Eq + std::hash::Hash + std::fmt::Debug + 'static,
        R: rand::Rng + ?Sized,
    {
        let variants  = V::all_variants();
        let space_vec = space.to_vec();

        /*-- 1. gate + shift ------------------------------------------*/
        let mut kept: Vec<(usize, f64)> = variants
            .iter()
            .enumerate()
            .filter_map(|(idx, v)| {
                let sim = Self::dot(&space_vec, &v.intrinsic_ratings().to_vec());
                (sim > min_sim).then(|| (idx, sim - min_sim))
            })
            .collect();

        if kept.is_empty() {
            trace!("category left empty (no variant > min_sim)");
            return HashSet::new();
        }

        /*-- 2. find τ to hit ESS -------------------------------------*/
        let shifted: Vec<f64> = kept.iter().map(|&(_, s)| s).collect();
        let tau               = self.find_temperature(&shifted);
        let weights: Vec<f64> = shifted.iter().map(|&s| (s / tau).exp()).collect();
        let sum_w: f64        = weights.iter().sum();
        let probs: Vec<f64>   = weights.iter().map(|w| w / sum_w).collect();
        let ess               = 1.0 / probs.iter().map(|p| p * p).sum::<f64>();

        trace!(
            category = std::any::type_name::<V>(),
            min_sim,
            tau,
            ess,
            n_kept = kept.len(),
            kept_max = shifted.iter().cloned().fold(f64::MIN, f64::max),
            "gated variants and tuned temperature",
        );

        /*-- 3. sample -------------------------------------------------*/
        let dist   = WeightedIndex::new(&weights).expect("positive weights");
        let choice = variants[kept[dist.sample(rng)].0].clone();

        debug!(
            category = std::any::type_name::<V>(),
            ?choice,
            ?probs,
            "selected capability variant",
        );

        HashSet::from([choice])
    }

    /*───────────────────────────────────────────────────────────────────*/
    /*  3. one‑dimensional bisection for τ                              */
    /*───────────────────────────────────────────────────────────────────*/
    fn find_temperature(&self, shifted: &[f64]) -> f64 {
        let (mut lo, mut hi) = self.temp_bounds;
        for _ in 0..15 {
            let mid = 0.5 * (lo + hi);
            let ws: Vec<f64> = shifted.iter().map(|&s| (s / mid).exp()).collect();
            let sum: f64     = ws.iter().sum();
            let probs: Vec<f64> = ws.iter().map(|w| w / sum).collect();
            let ess = 1.0 / probs.iter().map(|p| p * p).sum::<f64>();
            if ess < self.ess_target { hi = mid; } else { lo = mid; }
        }
        0.5 * (lo + hi)
    }
}