osu & fruits: added unit tests for acc in pp calculators

This commit is contained in:
MaxOhn
2021-01-22 14:44:55 +01:00
parent 907edb4eaa
commit 3b599d649e
4 changed files with 333 additions and 61 deletions
+180 -18
View File
@@ -6,12 +6,14 @@ pub trait FruitsAttributeProvider {
}
impl FruitsAttributeProvider for DifficultyAttributes {
#[inline]
fn attributes(self) -> Option<DifficultyAttributes> {
Some(self)
}
}
impl FruitsAttributeProvider for StarResult {
#[inline]
fn attributes(self) -> Option<DifficultyAttributes> {
#[allow(irrefutable_let_patterns)]
if let Self::Fruits(attributes) = self {
@@ -23,12 +25,14 @@ impl FruitsAttributeProvider for StarResult {
}
impl FruitsAttributeProvider for PpResult {
#[inline]
fn attributes(self) -> Option<DifficultyAttributes> {
self.attributes.attributes()
}
}
/// Calculator for pp on osu!ctb maps.
#[derive(Clone, Debug)]
pub struct FruitsPP<'m> {
map: &'m Beatmap,
attributes: Option<DifficultyAttributes>,
@@ -109,7 +113,7 @@ impl<'m> FruitsPP<'m> {
self
}
/// Specify the amount of tiny droplets of a play.
/// Specify the amount of tiny droplets of a play i.e. n50.
#[inline]
pub fn tiny_droplets(mut self, n_tiny_droplets: usize) -> Self {
self.n_tiny_droplets.replace(n_tiny_droplets);
@@ -117,7 +121,7 @@ impl<'m> FruitsPP<'m> {
self
}
/// Specify the amount of tiny droplet misses of a play.
/// Specify the amount of tiny droplet misses of a play i.e. n_katu.
#[inline]
pub fn tiny_droplet_misses(mut self, n_tiny_droplet_misses: usize) -> Self {
self.n_tiny_droplet_misses.replace(n_tiny_droplet_misses);
@@ -144,7 +148,7 @@ impl<'m> FruitsPP<'m> {
/// Generate the hit results with respect to the given accuracy between `0` and `100`.
///
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
pub fn accuracy(mut self, acc: f32) -> Self {
pub fn accuracy(mut self, mut acc: f32) -> Self {
if self.attributes.is_none() {
self.attributes.replace(
stars(self.map, self.mods, self.passed_objects)
@@ -162,10 +166,12 @@ impl<'m> FruitsPP<'m> {
let n_fruits = self.n_fruits.unwrap_or_else(|| {
attributes
.max_combo
.saturating_sub(self.n_misses.saturating_sub(n_droplets))
.saturating_sub(self.n_misses)
.saturating_sub(n_droplets)
});
let max_tiny_droplets = attributes.n_tiny_droplets;
acc /= 100.0;
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
((acc * (attributes.max_combo + max_tiny_droplets) as f32).round() as usize)
@@ -183,21 +189,29 @@ impl<'m> FruitsPP<'m> {
self
}
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::fruits::DifficultyAttributes)
/// containing stars and other attributes.
pub fn calculate(mut self) -> PpResult {
let attributes = self.attributes.take().unwrap_or_else(|| {
stars(self.map, self.mods, self.passed_objects)
.attributes()
.unwrap()
});
// Make sure all objects are set
if self
fn assert_hitresults(&mut self, attributes: &DifficultyAttributes) {
let correct_combo_hits = self
.n_fruits
.and(self.n_droplets)
.and(self.n_tiny_droplets)
.and(self.n_tiny_droplet_misses)
.and_then(|f| self.n_droplets.map(|d| f + d + self.n_misses))
.filter(|h| *h == attributes.max_combo);
let correct_fruits = self
.n_fruits
.filter(|f| *f >= attributes.n_fruits.saturating_sub(self.n_misses));
let correct_droplets = self
.n_droplets
.filter(|d| *d >= attributes.n_droplets.saturating_sub(self.n_misses));
let correct_tinies = self
.n_tiny_droplets
.and_then(|t| self.n_tiny_droplet_misses.map(|m| t + m))
.filter(|h| *h == attributes.n_tiny_droplets);
if correct_combo_hits
.and(correct_fruits)
.and(correct_droplets)
.and(correct_tinies)
.is_none()
{
let mut n_fruits = self.n_fruits.unwrap_or(0);
@@ -226,6 +240,19 @@ impl<'m> FruitsPP<'m> {
self.n_tiny_droplets.replace(n_tiny_droplets);
self.n_tiny_droplet_misses.replace(n_tiny_droplet_misses);
}
}
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::fruits::DifficultyAttributes)
/// containing stars and other attributes.
pub fn calculate(mut self) -> PpResult {
let attributes = self.attributes.take().unwrap_or_else(|| {
stars(self.map, self.mods, self.passed_objects)
.attributes()
.unwrap()
});
// Make sure all objects are set
self.assert_hitresults(&attributes);
let stars = attributes.stars;
@@ -322,3 +349,138 @@ impl<'m> FruitsPP<'m> {
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Beatmap;
fn attributes() -> DifficultyAttributes {
DifficultyAttributes {
n_fruits: 1234,
n_droplets: 567,
n_tiny_droplets: 2345,
max_combo: 1234 + 567,
..Default::default()
}
}
#[test]
fn fruits_only_accuracy() {
let map = Beatmap::default();
let attributes = attributes();
let total_objects = attributes.n_fruits + attributes.n_droplets;
let target_acc = 97.5;
let calculator = FruitsPP::new(&map)
.attributes(attributes)
.passed_objects(total_objects)
.accuracy(target_acc);
let numerator = calculator.n_fruits.unwrap_or(0)
+ calculator.n_droplets.unwrap_or(0)
+ calculator.n_tiny_droplets.unwrap_or(0);
let denominator =
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
let acc = 100.0 * numerator as f32 / denominator as f32;
assert!(
(target_acc - acc).abs() < 1.0,
"Expected: {} | Actual: {}",
target_acc,
acc
);
}
#[test]
fn fruits_accuracy_droplets_and_tiny_droplets() {
let map = Beatmap::default();
let attributes = attributes();
let total_objects = attributes.n_fruits + attributes.n_droplets;
let target_acc = 97.5;
let n_droplets = 550;
let n_tiny_droplets = 2222;
let calculator = FruitsPP::new(&map)
.attributes(attributes)
.passed_objects(total_objects)
.droplets(n_droplets)
.tiny_droplets(n_tiny_droplets)
.accuracy(target_acc);
assert_eq!(
n_droplets,
calculator.n_droplets.unwrap(),
"Expected: {} | Actual: {}",
n_droplets,
calculator.n_droplets.unwrap()
);
let numerator = calculator.n_fruits.unwrap_or(0)
+ calculator.n_droplets.unwrap_or(0)
+ calculator.n_tiny_droplets.unwrap_or(0);
let denominator =
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
let acc = 100.0 * numerator as f32 / denominator as f32;
assert!(
(target_acc - acc).abs() < 1.0,
"Expected: {} | Actual: {}",
target_acc,
acc
);
}
#[test]
fn fruits_missing_objects() {
let map = Beatmap::default();
let attributes = attributes();
let total_objects = attributes.n_fruits + attributes.n_droplets;
let n_fruits = attributes.n_fruits - 10;
let n_droplets = attributes.n_droplets - 5;
let n_tiny_droplets = attributes.n_tiny_droplets - 50;
let n_tiny_droplet_misses = 20;
let n_misses = 2;
let mut calculator = FruitsPP::new(&map)
.attributes(attributes.clone())
.passed_objects(total_objects)
.fruits(n_fruits)
.droplets(n_droplets)
.tiny_droplets(n_tiny_droplets)
.tiny_droplet_misses(n_tiny_droplet_misses)
.misses(n_misses);
calculator.assert_hitresults(&attributes);
assert!(
(attributes.n_fruits as i32 - calculator.n_fruits.unwrap() as i32).abs()
<= n_misses as i32,
"Expected: {} | Actual: {} [+/- {} misses]",
attributes.n_fruits,
calculator.n_fruits.unwrap(),
n_misses
);
assert_eq!(
attributes.n_droplets,
calculator.n_droplets.unwrap()
- (n_misses - (attributes.n_fruits - calculator.n_fruits.unwrap())),
"Expected: {} | Actual: {}",
attributes.n_droplets,
calculator.n_droplets.unwrap()
- (n_misses - (attributes.n_fruits - calculator.n_fruits.unwrap())),
);
assert_eq!(
attributes.n_tiny_droplets,
calculator.n_tiny_droplets.unwrap() + calculator.n_tiny_droplet_misses.unwrap(),
"Expected: {} | Actual: {}",
attributes.n_tiny_droplets,
calculator.n_tiny_droplets.unwrap() + calculator.n_tiny_droplet_misses.unwrap(),
);
}
}