From 85e203629580192413dbaa8156c2b4ca351c4bfc Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 25 Apr 2018 19:41:43 +0200 Subject: introduce trackers Trackers help us to keep the code somewhat cleaner, especially in the statistics::calculate function. --- src/statistics/mod.rs | 144 +++++++++++++---------------- src/statistics/trackers.rs | 221 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+), 81 deletions(-) create mode 100644 src/statistics/trackers.rs (limited to 'src/statistics') diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index a9dd178..1ebb517 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -1,17 +1,36 @@ //! This module aids in the creation of actual boss statistics. use super::*; use std::collections::HashMap; +use std::error::Error; pub mod boon; +pub mod trackers; + +use self::trackers::{RunnableTracker, Tracker}; pub type StatResult = Result; quick_error! { - #[derive(Clone, Debug)] + #[derive(Debug)] pub enum StatError { + TrackerError(err: Box) { + description("tracker error") + display("tracker returned an error: {}", err) + cause(&**err) + } } } +macro_rules! try_tracker { + ($expr:expr) => { + #[allow(unreachable_code)] + match $expr { + Ok(e) => e, + Err(e) => return Err(StatError::TrackerError(e)), + } + }; +} + /// A struct containing the calculated statistics for the log. #[derive(Clone, Debug)] pub struct Statistics { @@ -56,94 +75,57 @@ pub struct DamageStats { pub add_damage: u64, } -// A support macro to introduce a new block. -// -// Doesn't really require a macro, but it's nicer to look at -// with! { foo = bar } -// rather than -// { let foo = bar; ... } -macro_rules! with { - ($name:ident = $expr:expr => $bl:block) => {{ - let $name = $expr; - $bl - }}; +/// Takes a bunch of trackers and runs them on the given log. +/// +/// This method returns "nothing", as the statistics are saved in the trackers. +/// It's the job of the caller to extract them appropriately. +pub fn run_trackers(log: &Log, trackers: &mut [&mut RunnableTracker]) -> StatResult<()> { + for event in log.events() { + for mut tracker in trackers.iter_mut() { + try_tracker!((*tracker).run_feed(event)); + } + } + Ok(()) } /// Calculate the statistics for the given log. pub fn calculate(log: &Log) -> StatResult { - use super::EventKind::*; - let mut agent_stats = HashMap::::new(); - let mut log_start_time = 0; - fn get_stats(map: &mut HashMap, source: u64, target: u64) -> &mut DamageStats { - map.entry(source) - .or_insert_with(Default::default) - .per_target_damage - .entry(target) - .or_insert_with(Default::default) + let mut damage_tracker = trackers::DamageTracker::new(log); + let mut log_start_tracker = trackers::LogStartTracker::new(); + let mut combat_time_tracker = trackers::CombatTimeTracker::new(); + + run_trackers( + log, + &mut [ + &mut damage_tracker, + &mut log_start_tracker, + &mut combat_time_tracker, + ], + )?; + + let log_start_time = try_tracker!(log_start_tracker.finalize()); + + let combat_times = try_tracker!(combat_time_tracker.finalize()); + for (agent_addr, &(enter_time, exit_time)) in &combat_times { + let agent = agent_stats + .entry(*agent_addr) + .or_insert_with(Default::default); + if enter_time != 0 { + agent.enter_combat = enter_time - log_start_time; + } + if exit_time != 0 { + agent.exit_combat = exit_time - log_start_time; + } } - for event in log.events() { - match event.kind { - LogStart { .. } => { - log_start_time = event.time; - } - - EnterCombat { agent_addr, .. } => { - agent_stats - .entry(agent_addr) - .or_insert_with(Default::default) - .enter_combat = event.time - log_start_time; - } - - ExitCombat { agent_addr } => { - agent_stats - .entry(agent_addr) - .or_insert_with(Default::default) - .exit_combat = event.time - log_start_time; - } - - Physical { - source_agent_addr, - destination_agent_addr, - damage, - .. - } => { - with! { stats = get_stats(&mut agent_stats, source_agent_addr, destination_agent_addr) => { - stats.total_damage += damage as u64; - stats.power_damage += damage as u64; - }} - - if let Some(master) = log.master_agent(source_agent_addr) { - let master_stats = - get_stats(&mut agent_stats, master.addr, destination_agent_addr); - master_stats.total_damage += damage as u64; - master_stats.add_damage += damage as u64; - } - } - - ConditionTick { - source_agent_addr, - destination_agent_addr, - damage, - .. - } => { - with! { stats = get_stats(&mut agent_stats, source_agent_addr, destination_agent_addr) => { - stats.total_damage += damage as u64; - stats.condition_damage += damage as u64; - }} - - if let Some(master) = log.master_agent(source_agent_addr) { - let master_stats = - get_stats(&mut agent_stats, master.addr, destination_agent_addr); - master_stats.total_damage += damage as u64; - master_stats.add_damage += damage as u64; - } - } - - _ => (), - } + 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(); diff --git a/src/statistics/trackers.rs b/src/statistics/trackers.rs new file mode 100644 index 0000000..4f2c871 --- /dev/null +++ b/src/statistics/trackers.rs @@ -0,0 +1,221 @@ +//! evtclib tracker definitions. +//! +//! The idea behind a "tracker" is to have one object taking care of one +//! specific thing. This makes it easier to organize the whole "statistic +//! gathering loop", and it keeps each tracker somewhat small. +//! +//! It's also easy to define your own trackers if there are any statistics that +//! you want to track. Just implement [`Tracker`](trait.Tracker.html). It +//! doesn't matter what you track, it doesn't matter how many trackers you +//! define. +//! +//! You can use [`run_trackers`](../fn.run_trackers.html) to run multiple +//! trackers on the same log. +use std::collections::HashMap; +use std::error::Error; + +use super::super::{Event, EventKind, Log}; +use super::DamageStats; + +// A support macro to introduce a new block. +// +// Doesn't really require a macro, but it's nicer to look at +// with! { foo = bar } +// rather than +// { let foo = bar; ... } +macro_rules! with { + ($name:ident = $expr:expr => $bl:block) => {{ + let $name = $expr; + $bl + }}; +} + +/// A tracker. +/// +/// A tracker should be responsible for tracking a single statistic. Each +/// tracker is fed each event. If an error is returned while feeding, the whole +/// calculation will be aborted. +pub trait Tracker { + /// The resulting statistic that this tracker will return. + type Stat; + /// The error that this tracker might return. + type Error: Error; + + /// Feed a single event into this tracker. + /// + /// The tracker will update its internal state. + fn feed(&mut self, event: &Event) -> Result<(), Self::Error>; + + /// Finalize this tracker and get the statistics out. + fn finalize(self) -> Result; +} + +/// A helper trait that erases the types from a tracker. +/// +/// This makes it able to use references like `&mut RunnableTracker` without +/// having to specify the generic types, allowing you to e.g. store a bunch of +/// them in a vector, regardless of their output type. Unless you want to do +/// that, you probably don't want to use this trait directly. +/// +/// Note that you do not need to implement this yourself. It is automatically +/// implemented for all types that implement `Tracker`. +/// +/// RunnableTrackers provide no way to extract their resources, and they wrap +/// all errors in `Box<_>`, so you should always keep a "real" reference around +/// if you plan on getting any data. +pub trait RunnableTracker { + /// See `Tracker.feed()`. Renamed to avoid conflicts. + fn run_feed(&mut self, event: &Event) -> Result<(), Box>; +} + +impl> RunnableTracker for T { + fn run_feed(&mut self, event: &Event) -> Result<(), Box> { + self.feed(event).map_err(|e| Box::new(e) as Box) + } +} + +/// A tracker that tracks per-target damage of all agents. +pub struct DamageTracker<'l> { + log: &'l Log, + // Source -> Target -> Damage + damages: HashMap>, +} + +impl<'t> DamageTracker<'t> { + /// Create a new damage tracker for the given log. + pub fn new(log: &Log) -> DamageTracker { + DamageTracker { + log, + damages: HashMap::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>; + type Error = !; + + fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { + match event.kind { + EventKind::Physical { + source_agent_addr, + destination_agent_addr, + damage, + .. + } => { + 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; + } + } + + EventKind::ConditionTick { + source_agent_addr, + destination_agent_addr, + damage, + .. + } => { + 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; + } + } + + _ => (), + } + Ok(()) + } + + fn finalize(self) -> Result { + Ok(self.damages) + } +} + +/// Tracks when the log has been started. +#[derive(Default)] +pub struct LogStartTracker { + event_time: u64, +} + +impl LogStartTracker { + /// Create a new log start tracker. + pub fn new() -> LogStartTracker { + LogStartTracker { event_time: 0 } + } +} + +impl Tracker for LogStartTracker { + type Stat = u64; + type Error = !; + + fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { + if let EventKind::LogStart { .. } = event.kind { + self.event_time = event.time; + } + Ok(()) + } + + fn finalize(self) -> Result { + Ok(self.event_time) + } +} + +/// A tracker that tracks the combat entry and exit times for each agent. +#[derive(Default)] +pub struct CombatTimeTracker { + times: HashMap, +} + +impl CombatTimeTracker { + /// Create a new combat time tracker. + pub fn new() -> CombatTimeTracker { + CombatTimeTracker { + times: HashMap::new(), + } + } +} + +impl Tracker for CombatTimeTracker { + // Maps from agent addr to (entry time, exit time) + type Stat = HashMap; + type Error = !; + + fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { + match event.kind { + EventKind::EnterCombat { agent_addr, .. } => { + self.times.entry(agent_addr).or_insert((0, 0)).0 = event.time; + } + + EventKind::ExitCombat { agent_addr } => { + self.times.entry(agent_addr).or_insert((0, 0)).1 = event.time; + } + + _ => (), + } + Ok(()) + } + + fn finalize(self) -> Result { + Ok(self.times) + } +} -- cgit v1.2.3