aboutsummaryrefslogtreecommitdiff
path: root/src/statistics
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2018-06-13 13:07:48 +0200
committerDaniel Schadt <kingdread@gmx.de>2018-06-13 13:08:38 +0200
commitfe16699205b6b40aed8cafbe95820835a7052908 (patch)
tree1c0a7ea744e090f6c18f4f71472bd641764cdfe0 /src/statistics
parentcb20d6966a4c3d386925f812fe83b00f3f803db3 (diff)
downloadevtclib-fe16699205b6b40aed8cafbe95820835a7052908.tar.gz
evtclib-fe16699205b6b40aed8cafbe95820835a7052908.tar.bz2
evtclib-fe16699205b6b40aed8cafbe95820835a7052908.zip
rework damage tracker
Diffstat (limited to 'src/statistics')
-rw-r--r--src/statistics/damage.rs95
-rw-r--r--src/statistics/math.rs187
-rw-r--r--src/statistics/mod.rs80
-rw-r--r--src/statistics/trackers.rs218
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);
+ }
_ => (),
}
}