diff options
Diffstat (limited to 'src/statistics/boon.rs')
-rw-r--r-- | src/statistics/boon.rs | 361 |
1 files changed, 0 insertions, 361 deletions
diff --git a/src/statistics/boon.rs b/src/statistics/boon.rs deleted file mode 100644 index 196bd1d..0000000 --- a/src/statistics/boon.rs +++ /dev/null @@ -1,361 +0,0 @@ -//! Module providing functions and structs to deal with boon related statistics. -use std::cmp; -use std::fmt; -use std::ops::Mul; - -use super::math::{Monoid, RecordFunc, Semigroup}; - -use fnv::FnvHashMap; - -/// The type of a boon. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum BoonType { - /// Boon stacks duration, e.g. Regeneration. - Duration, - /// Boon stacks intensity, e.g. Might. - Intensity, -} - -/// A struct that helps with simulating boon changes over time. -/// -/// This basically simulates a single boon-queue (for a single boon). -/// -/// # A quick word about how boon queues work -/// -/// For each boon, you have an internal *boon queue*, limited to a specific -/// capacity. When the current stack expires, the next one is taken from the -/// queue. -/// -/// The queue is sorted by boon strength. This means that "weak" boons are -/// always at the end (and as such, are the first ones to be deleted when the -/// queue is full). This prevents "bad" boons (e.g. the Quickness from Lightning -/// Hammer #2) to override the "good" boons (e.g. the Quickness from your -/// friendly neighborhood Chrono with 100% boon duration). -/// -/// This also means that boons can be "lost". If the queue is full, the boon -/// might not get applied, or it might replace another boon, thus wasting some -/// of the boon duration. -/// -/// Intensity-stacked boons (such as Might) work a bit differently: as time -/// passes, all stacks are decreased simultaneously! As soon as a stack reaches -/// 0, it is dropped. -/// -/// You can find more information and the size of some of the queues on the wiki: -/// https://wiki.guildwars2.com/wiki/Effect_stacking -#[derive(Clone, Debug)] -pub struct BoonQueue { - capacity: u32, - queue: Vec<u64>, - boon_type: BoonType, - next_update: u64, - backlog: u64, -} - -impl BoonQueue { - /// Create a new boon queue. - /// - /// * `capacity` - The capacity of the queue. - /// * `boon_type` - How the boons stack. - pub fn new(capacity: u32, boon_type: BoonType) -> BoonQueue { - BoonQueue { - capacity, - queue: Vec::new(), - boon_type, - next_update: 0, - backlog: 0, - } - } - - fn fix_queue(&mut self) { - // Sort reversed, so that the longest stack is at the front. - self.queue.sort_unstable_by(|a, b| b.cmp(a)); - // Truncate queue by cutting of the shortest stacks - if self.queue.len() > self.capacity as usize { - self.queue.drain(self.capacity as usize..); - } - } - - /// Get the type of this boon. - pub fn boon_type(&self) -> BoonType { - self.boon_type - } - - /// Add a boon stack to this queue. - /// - /// * `duration` - Duration (in milliseconds) of the added stack. - pub fn add_stack(&mut self, duration: u64) { - let backlog = self.backlog; - self.do_simulate(backlog); - self.queue.push(duration); - self.fix_queue(); - self.next_update = self.next_update(); - } - - /// Return the amount of current stacks. - /// - /// If the boon type is a duration boon, this will always return 0 or 1. - /// - /// If the boon type is an intensity boon, it will return the number of - /// stacks. - pub fn current_stacks(&self) -> u32 { - let result = match self.boon_type { - BoonType::Intensity => self.queue.len(), - BoonType::Duration => cmp::min(1, self.queue.len()), - }; - result as u32 - } - - /// Simulate time passing. - /// - /// This will decrease the remaining duration of the stacks accordingly. - /// - /// * `duration` - The amount of time (in milliseconds) to simulate. - pub fn simulate(&mut self, duration: u64) { - if duration == 0 { - return; - } - if duration < self.next_update { - self.next_update -= duration; - self.backlog += duration; - } else { - let total = self.backlog + duration; - self.do_simulate(total); - } - } - - /// Simulate the thing without using the backlog. - fn do_simulate(&mut self, duration: u64) { - let mut remaining = duration; - match self.boon_type { - BoonType::Duration => { - while remaining > 0 && !self.queue.is_empty() { - let next = self.queue.remove(0); - if next > remaining { - self.queue.push(next - remaining); - break; - } else { - remaining -= next; - } - } - self.fix_queue(); - } - - BoonType::Intensity => { - self.queue = self - .queue - .iter() - .cloned() - .filter(|v| *v > duration) - .map(|v| v - duration) - .collect(); - } - } - self.next_update = self.next_update(); - self.backlog = 0; - } - - /// Remove all stacks. - pub fn clear(&mut self) { - self.queue.clear(); - self.next_update = 0; - self.backlog = 0; - } - - /// Cleanse a single stack - pub fn drop_single(&mut self) { - if self.is_empty() { - return; - } - self.queue.pop(); - } - - /// Checks if any stacks are left. - pub fn is_empty(&self) -> bool { - self.queue.is_empty() - } - - /// Calculate when the stacks will have the next visible change. - /// - /// This assumes that the stacks will not be modified during this time. - /// - /// The return value is the duration in milliseconds. If the boon queue is - /// currently empty, 0 is returned. - pub fn next_change(&self) -> u64 { - match self.boon_type { - BoonType::Duration => self.queue.iter().sum(), - BoonType::Intensity => self.queue.last().cloned().unwrap_or(0), - } - } - - /// Calculate when the boon queue should be updated next. - /// - /// The next update always means that a stack runs out, even if it has no - /// visible effect. - /// - /// For each queue: `next_update() <= next_change()`. - /// - /// A return value of 0 means that there's no update awaiting. - pub fn next_update(&self) -> u64 { - self.queue.last().cloned().unwrap_or(0) - } -} - -/// Amount of stacks of a boon. -// Since this is also used to represent changes in stacks, we need access to -// negative numbers too, as stacks can drop. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Stacks(i32); - -impl Semigroup for Stacks { - #[inline] - fn combine(&self, other: &Self) -> Self { - Stacks(self.0 + other.0) - } -} - -impl Monoid for Stacks { - #[inline] - fn mempty() -> Self { - Stacks(0) - } -} - -// This shouldn't be negative, as total stacks are always greater than 0, thus -// the area below the curve will always be positive. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[doc(hidden)] -pub struct BoonArea(u64); - -impl Semigroup for BoonArea { - #[inline] - fn combine(&self, other: &Self) -> Self { - BoonArea(self.0 + other.0) - } -} - -impl Monoid for BoonArea { - #[inline] - fn mempty() -> Self { - BoonArea(0) - } -} - -impl Mul<u64> for Stacks { - type Output = BoonArea; - - #[inline] - fn mul(self, rhs: u64) -> BoonArea { - BoonArea(self.0 as u64 * rhs) - } -} - -/// A boon log for a specific player. -/// -/// This logs the amount of stacks of each boon a player had at any given time. -#[derive(Clone, Default)] -pub struct BoonLog { - // Keep a separate RecordFunc for each boon. - inner: FnvHashMap<u32, RecordFunc<u64, (), Stacks>>, -} - -impl BoonLog { - /// Create a new, empty boon log. - pub fn new() -> Self { - Default::default() - } - - /// Add an event to the boon log. - pub fn log(&mut self, time: u64, boon_id: u32, stacks: u32) { - let func = self.inner.entry(boon_id).or_insert_with(Default::default); - let current = func.tally(); - if current.0 == stacks as i32 { - return; - } - let diff = stacks as i32 - current.0; - func.insert(time, (), Stacks(diff)); - } - - /// Get the average amount of stacks between the two given time points. - /// - /// * `a` - Start time point. - /// * `b` - End time point. - /// * `boon_id` - ID of the boon that you want to get the average for. - pub fn average_stacks(&self, a: u64, b: u64, boon_id: u32) -> f32 { - assert!(b >= a, "timespan is negative?!"); - let func = if let Some(f) = self.inner.get(&boon_id) { - f - } else { - return 0.; - }; - let area = func.integral(&a, &b); - area.0 as f32 / (b - a) as f32 - } - - /// Get the amount of stacks at the given time point. - /// - /// * `x` - Time point. - /// * `boon_id` - ID of the boon that you want to get. - pub fn stacks_at(&self, x: u64, boon_id: u32) -> u32 { - self.inner - .get(&boon_id) - .map(|f| f.get(&x)) - .unwrap_or(Stacks(0)) - .0 as u32 - } -} - -impl fmt::Debug for BoonLog { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "BoonLog {{ .. }}") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_queue_capacity() { - let mut queue = BoonQueue::new(5, BoonType::Intensity); - assert_eq!(queue.current_stacks(), 0); - for _ in 0..10 { - queue.add_stack(10); - } - assert_eq!(queue.current_stacks(), 5); - } - - #[test] - fn test_simulate_duration() { - let mut queue = BoonQueue::new(10, BoonType::Duration); - queue.add_stack(10); - queue.add_stack(20); - assert_eq!(queue.current_stacks(), 1); - queue.simulate(30); - assert_eq!(queue.current_stacks(), 0); - - queue.add_stack(50); - queue.simulate(30); - assert_eq!(queue.current_stacks(), 1); - queue.simulate(10); - assert_eq!(queue.current_stacks(), 1); - queue.simulate(15); - assert_eq!(queue.current_stacks(), 0); - } - - #[test] - fn test_simulate_intensity() { - let mut queue = BoonQueue::new(5, BoonType::Intensity); - - queue.add_stack(10); - queue.add_stack(20); - assert_eq!(queue.current_stacks(), 2); - - queue.simulate(5); - assert_eq!(queue.current_stacks(), 2); - - queue.simulate(5); - assert_eq!(queue.current_stacks(), 1); - queue.simulate(15); - assert_eq!(queue.current_stacks(), 0); - } -} |