fix: improve taiko hitresult generation

This commit is contained in:
MaxOhn
2023-10-29 20:28:29 +01:00
parent 8527f2f391
commit f8246dbf71
3 changed files with 205 additions and 56 deletions
+9
View File
@@ -0,0 +1,9 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 1883e1e612026e7f7c8035803830ff21b6d98c946cfb92c5c144e682a5d72ad2 # shrinks to acc = 0.0, n300 = None, n100 = None, n_misses = None, best_case = false
cc 84792fb0ec8971122d0eb2e61355a637e9326ac2b7469226bba06a2defdc44dd # shrinks to acc = 0.659020825478002, n300 = None, n100 = None, n_misses = Some(99), best_case = false
cc 94fb95e246e580e010115334e5fd7de6160b3a24a735e4cb4ab4341f5f7b769e # shrinks to acc = 0.0, n300 = Some(0), n100 = Some(1), n_misses = None, best_case = false
+2
View File
@@ -725,6 +725,8 @@ fn calculate_effective_misses(attrs: &OsuDifficultyAttributes, state: &OsuScoreS
}
fn accuracy(n300: usize, n100: usize, n50: usize, n_misses: usize) -> f64 {
debug_assert_ne!(n300 + n100 + n50 + n_misses, 0);
let numerator = 6 * n300 + 2 * n100 + n50;
let denominator = 6 * (n300 + n100 + n50 + n_misses);
+194 -56
View File
@@ -233,9 +233,11 @@ impl<'map> TaikoPP<'map> {
let priority = self.hitresult_priority.unwrap_or_default();
let mut n300 = self.n300.unwrap_or(0);
let mut n100 = self.n100.unwrap_or(0);
let n_misses = self.n_misses.unwrap_or(0);
let n_misses = self.n_misses.map_or(0, |n| n.min(total_result_count));
let n_remaining = total_result_count - n_misses;
let mut n300 = self.n300.map_or(0, |n| n.min(n_remaining));
let mut n100 = self.n100.map_or(0, |n| n.min(n_remaining));
if let Some(acc) = self.acc {
match (self.n300, self.n100) {
@@ -250,9 +252,24 @@ impl<'map> TaikoPP<'map> {
(Some(_), None) => n100 += total_result_count.saturating_sub(n300 + n_misses),
(None, Some(_)) => n300 += total_result_count.saturating_sub(n100 + n_misses),
(None, None) => {
let target_total = (acc * (total_result_count * 2) as f64).round() as usize;
n300 = target_total - (total_result_count.saturating_sub(n_misses));
n100 = total_result_count.saturating_sub(n300 + n_misses);
let target_total = acc * (2 * total_result_count) as f64;
let mut best_dist = f64::MAX;
let raw_n300 = target_total - n_remaining as f64;
let min_n300 = n_remaining.min(raw_n300.floor() as usize);
let max_n300 = n_remaining.min(raw_n300.ceil() as usize);
for new300 in min_n300..=max_n300 {
let new100 = n_remaining - new300;
let dist = (acc - accuracy(new300, new100, n_misses)).abs();
if dist < best_dist {
best_dist = dist;
n300 = new300;
n100 = new100;
}
}
}
}
} else {
@@ -260,19 +277,23 @@ impl<'map> TaikoPP<'map> {
match priority {
HitResultPriority::BestCase => match (self.n300, self.n100) {
(Some(_), None) => n100 = remaining,
(Some(_), Some(_)) => n300 += remaining,
(None, _) => n300 = remaining,
},
HitResultPriority::WorstCase => match (self.n300, self.n100) {
(None, Some(_)) => n300 = remaining,
(Some(_), Some(_)) => n100 += remaining,
(_, None) => n100 = remaining,
_ => n300 += remaining,
},
HitResultPriority::WorstCase => match (self.n100, self.n300) {
(None, _) => n100 = remaining,
(_, None) => n300 = remaining,
_ => n100 += remaining,
},
}
}
let max_combo = self.combo.map_or(max_combo, |combo| combo.min(max_combo));
let max_possible_combo = max_combo.saturating_sub(n_misses);
let max_combo = self
.combo
.map_or(max_possible_combo, |combo| combo.min(max_possible_combo));
TaikoScoreState {
max_combo,
@@ -437,6 +458,15 @@ impl<'map> From<OsuPP<'map>> for TaikoPP<'map> {
}
}
fn accuracy(n300: usize, n100: usize, n_misses: usize) -> f64 {
debug_assert_ne!(n300 + n100 + n_misses, 0);
let numerator = 2 * n300 + n100;
let denominator = 2 * (n300 + n100 + n_misses);
numerator as f64 / denominator as f64
}
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
pub trait TaikoAttributeProvider {
/// Provide the actual difficulty attributes.
@@ -486,22 +516,159 @@ impl TaikoAttributeProvider for PerformanceAttributes {
mod test {
use super::*;
use crate::Beatmap;
use proptest::{option, prelude::*};
use std::sync::OnceLock;
fn test_data() -> (Beatmap, TaikoDifficultyAttributes) {
let path = "./maps/1028484.osu";
let map = Beatmap::from_path(path).unwrap();
static DATA: OnceLock<(Beatmap, TaikoDifficultyAttributes)> = OnceLock::new();
let attrs = TaikoDifficultyAttributes {
stamina: 1.4528845068865617,
rhythm: 0.20130047251681948,
colour: 1.0487315549761433,
peak: 1.8881824429738323,
hit_window: 35.0,
stars: 2.9778030386845606,
max_combo: 289,
const MAX_COMBO: usize = 289;
fn test_data() -> (&'static Beatmap, TaikoDifficultyAttributes) {
let (map, attrs) = DATA.get_or_init(|| {
let path = "./maps/1028484.osu";
let map = Beatmap::from_path(path).unwrap();
let attrs = TaikoDifficultyAttributes {
stamina: 1.4528845068865617,
rhythm: 0.20130047251681948,
colour: 1.0487315549761433,
peak: 1.8881824429738323,
hit_window: 35.0,
stars: 2.9778030386845606,
max_combo: 289,
};
assert_eq!(MAX_COMBO, attrs.max_combo);
(map, attrs)
});
(map, attrs.to_owned())
}
/// Checks all remaining hitresult combinations w.r.t. the given parameters
/// and returns the [`TaikoScoreState`] that matches `acc` the best.
///
/// Very slow but accurate.
fn brute_force_best(
acc: f64,
n300: Option<usize>,
n100: Option<usize>,
n_misses: usize,
best_case: bool,
) -> TaikoScoreState {
let n_objects = MAX_COMBO;
let n_misses = n_misses.min(n_objects);
let mut best_dist = f64::MAX;
let mut best_state = TaikoScoreState {
n_misses,
..Default::default()
};
(map, attrs)
let mut bf_with_100 = |n300: usize, n100: usize| {
let dist = (acc - accuracy(n300, n100, n_misses)).abs();
if dist < best_dist {
best_dist = dist;
best_state.n300 = n300;
best_state.n100 = n100;
}
};
let mut bf_with_300 = |n300: usize| match n100 {
Some(mut n100) => {
n100 = n100
.min(n_objects - n_misses)
.max(n_objects.saturating_sub(n300 + n_misses));
bf_with_100(n300, n100);
}
None => {
let n100 = n_objects.saturating_sub(n300 + n_misses);
bf_with_100(n300, n100);
}
};
match (n300, n100) {
(Some(mut n300), Some(n100)) => {
n300 = n300.min(n_objects - n_misses);
if best_case {
n300 = n300.max(n_objects.saturating_sub(n100 + n_misses));
}
bf_with_300(n300);
}
(Some(mut n300), None) => {
n300 = n300.min(n_objects - n_misses);
bf_with_300(n300);
}
(None, Some(n100)) => {
let n300 = n_objects.saturating_sub(n100 + n_misses);
bf_with_300(n300);
}
(None, None) => {
let n_remaining = n_objects - n_misses;
for n300 in 0..=n_remaining {
bf_with_300(n300);
}
}
}
best_state
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10_000))]
#[test]
fn taiko_hitresults(
acc in 0.0..=1.0,
n300 in option::weighted(0.10, 0_usize..=MAX_COMBO),
n100 in option::weighted(0.10, 0_usize..=MAX_COMBO),
n_misses in option::weighted(0.15, 0_usize..=MAX_COMBO),
best_case in prop::bool::ANY,
) {
let (map, attrs) = test_data();
let priority = if best_case {
HitResultPriority::BestCase
} else {
HitResultPriority::WorstCase
};
let mut state = TaikoPP::new(map)
.attributes(attrs)
.accuracy(acc * 100.0)
.hitresult_priority(priority);
if let Some(n300) = n300 {
state = state.n300(n300);
}
if let Some(n100) = n100 {
state = state.n100(n100);
}
if let Some(n_misses) = n_misses {
state = state.n_misses(n_misses);
}
let hitresults = state.generate_hitresults(MAX_COMBO);
let mut expected = brute_force_best(
acc,
n300,
n100,
n_misses.unwrap_or(0),
best_case,
);
expected.max_combo = MAX_COMBO.saturating_sub(n_misses.unwrap_or(0));
assert_eq!(hitresults, expected);
}
}
#[test]
@@ -509,7 +676,7 @@ mod test {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = TaikoPP::new(&map)
let state = TaikoPP::new(map)
.attributes(attrs)
.combo(100)
.n300(150)
@@ -532,7 +699,7 @@ mod test {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = TaikoPP::new(&map)
let state = TaikoPP::new(map)
.attributes(attrs)
.combo(100)
.n_misses(2)
@@ -548,33 +715,4 @@ mod test {
assert_eq!(state, expected);
}
#[test]
fn hitresults_acc_n_misses_worst() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = TaikoPP::new(&map)
.attributes(attrs)
.combo(100)
.accuracy(97.2)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults(max_combo);
let expected = TaikoScoreState {
max_combo: 100,
n300: 275,
n100: 12,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
}
}