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); + } _ => (), } } |