diff options
| author | Daniel Schadt <kingdread@gmx.de> | 2018-06-13 13:07:48 +0200 | 
|---|---|---|
| committer | Daniel Schadt <kingdread@gmx.de> | 2018-06-13 13:08:38 +0200 | 
| commit | fe16699205b6b40aed8cafbe95820835a7052908 (patch) | |
| tree | 1c0a7ea744e090f6c18f4f71472bd641764cdfe0 /src/statistics | |
| parent | cb20d6966a4c3d386925f812fe83b00f3f803db3 (diff) | |
| download | evtclib-fe16699205b6b40aed8cafbe95820835a7052908.tar.gz evtclib-fe16699205b6b40aed8cafbe95820835a7052908.tar.bz2 evtclib-fe16699205b6b40aed8cafbe95820835a7052908.zip | |
rework damage tracker
Diffstat (limited to 'src/statistics')
| -rw-r--r-- | src/statistics/damage.rs | 95 | ||||
| -rw-r--r-- | src/statistics/math.rs | 187 | ||||
| -rw-r--r-- | src/statistics/mod.rs | 80 | ||||
| -rw-r--r-- | src/statistics/trackers.rs | 218 | 
4 files changed, 355 insertions, 225 deletions
| diff --git a/src/statistics/damage.rs b/src/statistics/damage.rs new file mode 100644 index 0000000..fdf723f --- /dev/null +++ b/src/statistics/damage.rs @@ -0,0 +1,95 @@ +use super::math::{Monoid, RecordFunc, Semigroup}; +use std::fmt; + +/// Type of the damage. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DamageType { +    Physical, +    Condition, +} + +/// Meta information about a damage log entry. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Meta { +    pub source: u64, +    pub target: u64, +    pub kind: DamageType, +    pub skill: u16, +} + +/// A small wrapper that wraps a damage number. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Damage(pub u64); + +impl Semigroup for Damage { +    #[inline] +    fn combine(&self, other: &Self) -> Self { +        Damage(self.0 + other.0) +    } +} + +impl Monoid for Damage { +    #[inline] +    fn mempty() -> Self { +        Damage(0) +    } +} + +/// Provides access to the damage log. +#[derive(Clone)] +pub struct DamageLog { +    inner: RecordFunc<u64, Meta, Damage>, +} + +impl DamageLog { +    pub fn new() -> Self { +        DamageLog { +            inner: RecordFunc::new(), +        } +    } + +    pub fn log( +        &mut self, +        time: u64, +        source: u64, +        target: u64, +        kind: DamageType, +        skill: u16, +        value: u64, +    ) { +        self.inner.insert( +            time, +            Meta { +                source, +                target, +                kind, +                skill, +            }, +            Damage(value), +        ) +    } + +    pub fn damage_between<F: FnMut(&Meta) -> bool>( +        &self, +        start: u64, +        stop: u64, +        filter: F, +    ) -> Damage { +        self.inner.between_only(&start, &stop, filter) +    } + +    pub fn damage<F: FnMut(&Meta) -> bool>(&self, filter: F) -> Damage { +        self.inner.tally_only(filter) +    } +} + +impl fmt::Debug for DamageLog { +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +        write!( +            f, +            "DamageLog {{ {} events logged, {:?} total damage }}", +            self.inner.len(), +            self.inner.tally() +        ) +    } +} diff --git a/src/statistics/math.rs b/src/statistics/math.rs new file mode 100644 index 0000000..3760ca3 --- /dev/null +++ b/src/statistics/math.rs @@ -0,0 +1,187 @@ +//! This module provides some basic mathematical structures. + +/// A semigroup. +/// +/// This trait lets you combine elements by a binary operation. +pub trait Semigroup { +    fn combine(&self, other: &Self) -> Self; +} + +/// A monoid. +/// +/// Extends the semigroup with a "neutral" element. +/// +/// # Laws +/// +/// ```raw +/// mempty.combine(x) == x +/// x.combine(mempty) == x +/// ``` +pub trait Monoid: Semigroup { +    fn mempty() -> Self; +} + +#[derive(Debug, Clone)] +struct Record<X, T, D> { +    x: X, +    tag: T, +    data: D, +} + +/// A function that records tagged data points. +/// +/// This represents a "function" as a list of increases at soem discrete points. +/// Think about it as a generalized damage log. Increases can be tagged by some +/// arbitrary data, for example which the agent ID, the skill ID, the target, +/// ... +/// +/// This offers methods to get the value at a specific point (by "summing up" +/// all increments before that point), between two points and in total. It also +/// offers variants that allow you to filter the increments by their tag. +/// +/// Type parameters: +/// +/// * `X` domain of the function. Must have a defined `Ord`ering. +/// * `T` tag for each data point. Can be arbitrary. +/// * `D` actual data. Must be [`Monoid`](trait.Monoid.html), so that it can be +///   summed up. +#[derive(Clone)] +pub struct RecordFunc<X, T, D> { +    data: Vec<Record<X, T, D>>, +} + +impl<X, T, D> RecordFunc<X, T, D> +where +    X: Ord, +    D: Monoid, +{ +    /// Create a new `RecordFunc`. +    pub fn new() -> Self { +        RecordFunc { data: Vec::new() } +    } + +    /// Insert a data point into the record func. +    /// +    /// Note that you should supply the *increment*, not the *absolute value*! +    pub fn insert(&mut self, x: X, tag: T, data: D) { +        // Usually, the list will be built up in order, which means we can +        // always append to the end. Check for this special case to make it +        // faster. +        if self.data.last().map(|r| r.x < x).unwrap_or(true) { +            self.data.push(Record { x, tag, data }); +        } else { +            let index = match self.data.binary_search_by(|r| r.x.cmp(&x)) { +                Ok(i) => i, +                Err(i) => i, +            }; +            self.data.insert(index, Record { x, tag, data }); +        } +        //self.data.sort_by(|a, b| a.x.cmp(&b.x)); +    } + +    /// Get the amount of data points saved. +    pub fn len(&self) -> usize { +        self.data.len() +    } + +    /// Get the absolute value at the specific point. +    #[inline] +    pub fn get(&self, x: &X) -> D { +        self.get_only(x, |_| true) +    } + +    /// Get the absolute value at the specific point by only considering +    /// increments where the predicate holds. +    pub fn get_only<F: FnMut(&T) -> bool>(&self, x: &X, mut predicate: F) -> D { +        self.data +            .iter() +            .take_while(|record| record.x <= *x) +            .filter(|record| predicate(&record.tag)) +            .fold(D::mempty(), |a, b| a.combine(&b.data)) +    } + +    /// Get the increments between the two given points. +    #[inline] +    pub fn between(&self, a: &X, b: &X) -> D { +        self.between_only(a, b, |_| true) +    } + +    /// Get the increments between the two given points by only considering +    /// increments where the predicate holds. +    pub fn between_only<F: FnMut(&T) -> bool>(&self, a: &X, b: &X, mut predicate: F) -> D { +        self.data +            .iter() +            .skip_while(|record| record.x < *a) +            .take_while(|record| record.x <= *b) +            .filter(|record| predicate(&record.tag)) +            .fold(D::mempty(), |a, b| a.combine(&b.data)) +    } + +    /// Get the sum of all increments. +    #[inline] +    pub fn tally(&self) -> D { +        self.tally_only(|_| true) +    } + +    /// Get the sum of all increments by only considering increments where the +    /// predicate holds. +    pub fn tally_only<F: FnMut(&T) -> bool>(&self, mut predicate: F) -> D { +        self.data +            .iter() +            .filter(|record| predicate(&record.tag)) +            .fold(D::mempty(), |a, b| a.combine(&b.data)) +    } +} + +#[cfg(test)] +mod test { +    use super::*; + +    #[derive(Debug, PartialEq, Eq)] +    struct Integer(u32); + +    impl Semigroup for Integer { +        fn combine(&self, other: &Self) -> Self { +            Integer(self.0 + other.0) +        } +    } + +    impl Monoid for Integer { +        fn mempty() -> Self { +            Integer(0) +        } +    } + +    fn create() -> RecordFunc<u32, u8, Integer> { +        let mut result = RecordFunc::new(); + +        result.insert(6, 1, Integer(6)); +        result.insert(4, 0, Integer(5)); +        result.insert(0, 1, Integer(3)); +        result.insert(2, 0, Integer(4)); + +        result +    } + +    #[test] +    fn recordfunc_get() { +        let rf = create(); + +        assert_eq!(rf.get(&3), Integer(7)); +        assert_eq!(rf.get(&4), Integer(12)); +    } + +    #[test] +    fn recordfunc_get_only() { +        let rf = create(); + +        assert_eq!(rf.get_only(&3, |t| *t == 0), Integer(4)); +    } + +    #[test] +    fn recordfunc_between() { +        let rf = create(); + +        assert_eq!(rf.between(&1, &5), Integer(9)); +    } +} diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 1dd48f7..5f2f288 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -4,9 +4,12 @@ use std::collections::HashMap;  use std::error::Error;  pub mod boon; +pub mod damage;  pub mod gamedata; +pub mod math;  pub mod trackers; +use self::damage::DamageLog;  use self::trackers::{RunnableTracker, Tracker};  pub type StatResult<T> = Result<T, StatError>; @@ -35,6 +38,8 @@ macro_rules! try_tracker {  /// A struct containing the calculated statistics for the log.  #[derive(Clone, Debug)]  pub struct Statistics { +    /// The complete damage log. +    pub damage_log: DamageLog,      /// A map mapping agent addresses to their stats.      pub agent_stats: HashMap<u64, AgentStats>,  } @@ -42,14 +47,6 @@ pub struct Statistics {  /// A struct describing the agent statistics.  #[derive(Clone, Debug, Default)]  pub struct AgentStats { -    /// Damage done per target during the fight. -    /// -    /// Maps from target address to the damage done to this target. -    pub per_target_damage: HashMap<u64, DamageStats>, -    /// Total damage dealt during the fight. -    pub total_damage: DamageStats, -    /// Damage directed to the boss. -    pub boss_damage: DamageStats,      /// Average stacks of boons.      ///      /// This also includes conditions. @@ -70,19 +67,6 @@ impl AgentStats {      }  } -/// Damage statistics for a given target. -#[derive(Debug, Clone, Copy, Default)] -pub struct DamageStats { -    /// The total damage of the player, including all minions/pets/... -    pub total_damage: u64, -    /// The condition damage that the player dealt. -    pub condition_damage: u64, -    /// The power damage that the player dealt. -    pub power_damage: u64, -    /// The damage that was done by minions/pets/... -    pub add_damage: u64, -} -  /// Takes a bunch of trackers and runs them on the given log.  ///  /// This method returns "nothing", as the statistics are saved in the trackers. @@ -103,8 +87,6 @@ pub fn calculate(log: &Log) -> StatResult<Statistics> {      let mut damage_tracker = trackers::DamageTracker::new(log);      let mut log_start_tracker = trackers::LogStartTracker::new();      let mut combat_time_tracker = trackers::CombatTimeTracker::new(); -    let mut boon_tracker = -        trackers::Multiplexer::multiplex_on_destination(|dest| trackers::BoonTracker::new(dest));      run_trackers(          log, @@ -112,7 +94,6 @@ pub fn calculate(log: &Log) -> StatResult<Statistics> {              &mut damage_tracker,              &mut log_start_tracker,              &mut combat_time_tracker, -            &mut boon_tracker,          ],      )?; @@ -131,51 +112,10 @@ pub fn calculate(log: &Log) -> StatResult<Statistics> {          }      } -    let damages = try_tracker!(damage_tracker.finalize()); -    for agent in damages.keys() { -        agent_stats -            .entry(*agent) -            .or_insert_with(Default::default) -            .per_target_damage = damages[agent].clone(); -    } - -    let boss = log.boss(); - -    for agent_stat in agent_stats.values_mut() { -        tally_damage(agent_stat); -        agent_stat.boss_damage = agent_stat -            .per_target_damage -            .get(&boss.addr) -            .cloned() -            .unwrap_or_else(Default::default); -    } - -    let boons = try_tracker!(boon_tracker.finalize()); -    for (agent, boon_map) in &boons { -        let agent = agent_stats.entry(*agent).or_insert_with(Default::default); -        if agent.exit_combat < agent.enter_combat { -            continue; -        } -        let combat_time = agent.combat_time() as f64; -        if combat_time == 0. { -            continue; -        } -        agent.boon_averages = boon_map -            .iter() -            .map(|(id, area)| (*id, *area as f64 / combat_time)) -            .collect(); -    } - -    Ok(Statistics { agent_stats }) -} +    let damage_log = try_tracker!(damage_tracker.finalize()); -/// Takes the per target damage stats and tallies them up into the total damage -/// stats. -fn tally_damage(stats: &mut AgentStats) { -    for damage in stats.per_target_damage.values() { -        stats.total_damage.total_damage += damage.total_damage; -        stats.total_damage.power_damage += damage.power_damage; -        stats.total_damage.condition_damage += damage.condition_damage; -        stats.total_damage.add_damage += damage.add_damage; -    } +    Ok(Statistics { +        damage_log, +        agent_stats, +    })  } diff --git a/src/statistics/trackers.rs b/src/statistics/trackers.rs index 7b16fce..ce87f3d 100644 --- a/src/statistics/trackers.rs +++ b/src/statistics/trackers.rs @@ -22,8 +22,8 @@ use std::hash::Hash;  use super::super::{Event, EventKind, Log};  use super::boon::BoonQueue; +use super::damage::{DamageLog, DamageType};  use super::gamedata::{self, Mechanic, Trigger}; -use super::DamageStats;  // A support macro to introduce a new block.  // @@ -82,120 +82,10 @@ impl<S, E: Error + 'static, T: Tracker<Stat = S, Error = E>> RunnableTracker for      }  } -/// A trait that allows a tracker to be multiplexed. -/// -/// Basically, this is a factory that allows new trackers to be created. Each -/// tracker can be given a key that it should listen on, which is expressed by -/// the `K` type parameter and the `key` parameter to `create`. -/// -/// A blanket implementation for closures is provided, so you can use any -/// `FnMut(K) -> T`, where `K` is the key and `T` is the tracker. -pub trait Multiplexable<K> { -    /// The type of tracker that this multiplexable/factory creates. -    type T: Tracker; - -    /// Create a new tracker, listening for the given key. -    fn create(&mut self, key: K) -> Self::T; -} - -// This implementation allows a closure to be used as a multiplexable/tracker -// factory. -impl<T, K, O> Multiplexable<K> for T -where -    T: FnMut(K) -> O, -    O: Tracker, -{ -    type T = O; - -    fn create(&mut self, key: K) -> Self::T { -        self(key) -    } -} - -/// A helper that wraps (decorates) another tracker and separates the results -/// based on the given criteria. -/// -/// Instead of outputting a single statistic, it outputs a `HashMap`, mapping -/// the criteria to its own tracker. -/// -/// This can be used for example to count damage per player: The damage tracker -/// itself only counts damage for a single player, and together with a -/// multiplexer, it will count the damage per player. -/// -/// Type parameters: -/// * `K` Key that is used to distinguish criteria. For example, `u64` for a -///   multiplexer that separates based on agents. -/// * `F` Factory that creates new trackers for each key. -/// * `T` Inner tracker type. Usually determined by the factory. -/// * `S` Selection function type. Takes an event and outputs a key. -/// -/// # Example -/// -/// ``` -/// # use evtclib::statistics::trackers::*; -/// let boons = Multiplexer::multiplex_on_destination(|agent| BoonTracker::new(agent)); -/// ``` -pub struct Multiplexer<K, F, T, S> { -    factory: F, -    trackers: HashMap<K, T>, -    selector: S, -} - -impl Multiplexer<(), (), (), ()> { -    /// Create a new multiplexer that multiplexes on the source agent. -    pub fn multiplex_on_source<Factory: Multiplexable<u64>>( -        factory: Factory, -    ) -> Multiplexer<u64, Factory, Factory::T, impl Fn(&Event) -> Option<u64>> { -        Multiplexer { -            factory, -            trackers: HashMap::new(), -            selector: |event: &Event| event.kind.source_agent_addr(), -        } -    } - -    /// Create a new multiplexer that multiplexes on the destination agent. -    pub fn multiplex_on_destination<Factory: Multiplexable<u64>>( -        factory: Factory, -    ) -> Multiplexer<u64, Factory, Factory::T, impl Fn(&Event) -> Option<u64>> { -        Multiplexer { -            factory, -            trackers: HashMap::new(), -            selector: |event: &Event| event.kind.destination_agent_addr(), -        } -    } -} - -impl<K: Hash + Eq + Clone, F: Multiplexable<K>, S: FnMut(&Event) -> Option<K>> Tracker -    for Multiplexer<K, F, F::T, S> -{ -    type Stat = HashMap<K, <F::T as Tracker>::Stat>; -    type Error = <F::T as Tracker>::Error; - -    fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { -        if let Some(key) = (self.selector)(event) { -            let factory = &mut self.factory; -            let entry = self -                .trackers -                .entry(key.clone()) -                .or_insert_with(|| factory.create(key)); -            entry.feed(event)?; -        } -        Ok(()) -    } - -    fn finalize(self) -> Result<Self::Stat, Self::Error> { -        self.trackers -            .into_iter() -            .map(|(k, v)| v.finalize().map(|inner| (k, inner))) -            .collect() -    } -} -  /// A tracker that tracks per-target damage of all agents.  pub struct DamageTracker<'l> {      log: &'l Log, -    // Source -> Target -> Damage -    damages: HashMap<u64, HashMap<u64, DamageStats>>, +    damage_log: DamageLog,  }  impl<'t> DamageTracker<'t> { @@ -203,21 +93,13 @@ impl<'t> DamageTracker<'t> {      pub fn new(log: &Log) -> DamageTracker {          DamageTracker {              log, -            damages: HashMap::new(), +            damage_log: DamageLog::new(),          }      } - -    fn get_stats(&mut self, source: u64, target: u64) -> &mut DamageStats { -        self.damages -            .entry(source) -            .or_insert_with(Default::default) -            .entry(target) -            .or_insert_with(Default::default) -    }  }  impl<'t> Tracker for DamageTracker<'t> { -    type Stat = HashMap<u64, HashMap<u64, DamageStats>>; +    type Stat = DamageLog;      type Error = !;      fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { @@ -226,36 +108,44 @@ impl<'t> Tracker for DamageTracker<'t> {                  source_agent_addr,                  destination_agent_addr,                  damage, +                skill_id,                  ..              } => { -                with! { stats = self.get_stats(source_agent_addr, destination_agent_addr) => { -                    stats.total_damage += damage as u64; -                    stats.power_damage += damage as u64; -                }} - -                if let Some(master) = self.log.master_agent(source_agent_addr) { -                    let master_stats = self.get_stats(master.addr, destination_agent_addr); -                    master_stats.total_damage += damage as u64; -                    master_stats.add_damage += damage as u64; -                } +                let source = if let Some(master) = self.log.master_agent(source_agent_addr) { +                    master.addr +                } else { +                    source_agent_addr +                }; +                self.damage_log.log( +                    event.time, +                    source, +                    destination_agent_addr, +                    DamageType::Physical, +                    skill_id, +                    damage as u64, +                );              }              EventKind::ConditionTick {                  source_agent_addr,                  destination_agent_addr,                  damage, +                condition_id,                  ..              } => { -                with! { stats = self.get_stats(source_agent_addr, destination_agent_addr) => { -                    stats.total_damage += damage as u64; -                    stats.condition_damage += damage as u64; -                }} - -                if let Some(master) = self.log.master_agent(source_agent_addr) { -                    let master_stats = self.get_stats(master.addr, destination_agent_addr); -                    master_stats.total_damage += damage as u64; -                    master_stats.add_damage += damage as u64; -                } +                let source = if let Some(master) = self.log.master_agent(source_agent_addr) { +                    master.addr +                } else { +                    source_agent_addr +                }; +                self.damage_log.log( +                    event.time, +                    source, +                    destination_agent_addr, +                    DamageType::Condition, +                    condition_id, +                    damage as u64, +                );              }              _ => (), @@ -264,7 +154,7 @@ impl<'t> Tracker for DamageTracker<'t> {      }      fn finalize(self) -> Result<Self::Stat, Self::Error> { -        Ok(self.damages) +        Ok(self.damage_log)      }  } @@ -446,9 +336,7 @@ impl Tracker for BoonTracker {          match event.kind {              EventKind::BuffApplication { -                buff_id, -                duration, -                .. +                buff_id, duration, ..              } => {                  if let Some(queue) = self.get_queue(buff_id) {                      queue.add_stack(duration as u64); @@ -457,10 +345,7 @@ impl Tracker for BoonTracker {              }              // XXX: Properly handle SINGLE and MANUAL removal types -            EventKind::BuffRemove { -                buff_id, -                .. -            } => { +            EventKind::BuffRemove { buff_id, .. } => {                  if let Some(queue) = self.get_queue(buff_id) {                      queue.clear();                  } @@ -480,30 +365,53 @@ impl Tracker for BoonTracker {  /// A tracker that tracks boss mechanics for each player.  pub struct MechanicTracker { -    mechanics: HashMap<u64, HashMap<&'static Mechanic, u32>>, +    mechanics: HashMap<&'static Mechanic, u32>,      available_mechanics: Vec<&'static Mechanic>,      boss_address: u64, +    agent_address: u64,  }  impl MechanicTracker {      /// Create a new mechanic tracker that watches over the given mechanics. -    pub fn new(boss_address: u64, mechanics: Vec<&'static Mechanic>) -> MechanicTracker { +    pub fn new( +        agent_address: u64, +        boss_address: u64, +        mechanics: Vec<&'static Mechanic>, +    ) -> MechanicTracker {          MechanicTracker {              mechanics: HashMap::new(),              available_mechanics: mechanics, -            boss_address: boss_address, +            boss_address, +            agent_address,          }      }  }  impl Tracker for MechanicTracker { -    type Stat = HashMap<u64, HashMap<&'static Mechanic, u32>>; +    type Stat = HashMap<&'static Mechanic, u32>;      type Error = !;      fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { +        fn increase(map: &mut HashMap<&'static Mechanic, u32>, entry: &'static Mechanic) { +            *map.entry(entry).or_insert(0) += 1; +        } +          for mechanic in &self.available_mechanics {              match (&event.kind, &mechanic.1) { -                (EventKind::SkillUse { .. }, Trigger::SkillOnPlayer { .. }) => (), +                ( +                    EventKind::Physical { +                        source_agent_addr, +                        destination_agent_addr, +                        skill_id, +                        .. +                    }, +                    Trigger::SkillOnPlayer(trigger_id), +                ) if skill_id == trigger_id +                    && *source_agent_addr == self.boss_address +                    && *destination_agent_addr == self.agent_address => +                { +                    increase(&mut self.mechanics, mechanic); +                }                  _ => (),              }          } | 
