remove tests
This commit is contained in:
@@ -389,66 +389,3 @@ impl<'a, T: IntoPerformance<'a>> From<T> for Performance<'a> {
|
||||
into.into_performance()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
any::DifficultyAttributes,
|
||||
catch::{CatchDifficultyAttributes, CatchPerformanceAttributes},
|
||||
mania::{ManiaDifficultyAttributes, ManiaPerformanceAttributes},
|
||||
osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||
taiko::{Taiko, TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn create() {
|
||||
let map = Beatmap::from_path("./resources/1028484.osu").unwrap();
|
||||
let converted = map.unchecked_as_converted::<Taiko>();
|
||||
|
||||
let _ = Performance::new(&converted);
|
||||
let _ = Performance::new(converted.as_owned());
|
||||
let _ = Performance::new(&map);
|
||||
let _ = Performance::new(map.clone());
|
||||
|
||||
let _ = Performance::new(OsuDifficultyAttributes::default());
|
||||
let _ = Performance::new(TaikoDifficultyAttributes::default());
|
||||
let _ = Performance::new(CatchDifficultyAttributes::default());
|
||||
let _ = Performance::new(ManiaDifficultyAttributes::default());
|
||||
|
||||
let _ = Performance::new(OsuPerformanceAttributes::default());
|
||||
let _ = Performance::new(TaikoPerformanceAttributes::default());
|
||||
let _ = Performance::new(CatchPerformanceAttributes::default());
|
||||
let _ = Performance::new(ManiaPerformanceAttributes::default());
|
||||
|
||||
let _ = Performance::new(DifficultyAttributes::Osu(OsuDifficultyAttributes::default()));
|
||||
let _ = Performance::new(PerformanceAttributes::Taiko(
|
||||
TaikoPerformanceAttributes::default(),
|
||||
));
|
||||
|
||||
let _ = Performance::from(&converted);
|
||||
let _ = Performance::from(converted);
|
||||
let _ = Performance::from(&map);
|
||||
let _ = Performance::from(map);
|
||||
|
||||
let _ = Performance::from(OsuDifficultyAttributes::default());
|
||||
let _ = Performance::from(TaikoDifficultyAttributes::default());
|
||||
let _ = Performance::from(CatchDifficultyAttributes::default());
|
||||
let _ = Performance::from(ManiaDifficultyAttributes::default());
|
||||
|
||||
let _ = Performance::from(OsuPerformanceAttributes::default());
|
||||
let _ = Performance::from(TaikoPerformanceAttributes::default());
|
||||
let _ = Performance::from(CatchPerformanceAttributes::default());
|
||||
let _ = Performance::from(ManiaPerformanceAttributes::default());
|
||||
|
||||
let _ = Performance::from(DifficultyAttributes::Osu(OsuDifficultyAttributes::default()));
|
||||
let _ = Performance::from(PerformanceAttributes::Taiko(
|
||||
TaikoPerformanceAttributes::default(),
|
||||
));
|
||||
|
||||
let _ = DifficultyAttributes::Osu(OsuDifficultyAttributes::default()).performance();
|
||||
let _ = PerformanceAttributes::Taiko(TaikoPerformanceAttributes::default()).performance();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,59 +158,3 @@ impl ExactSizeIterator for CatchGradualDifficulty {
|
||||
self.diff_objects.len() + 1 - self.idx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Beatmap;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let converted = Beatmap::from_bytes(&[]).unwrap().unchecked_into_converted();
|
||||
|
||||
let mut gradual = CatchGradualDifficulty::new(Difficulty::new(), &converted);
|
||||
|
||||
assert!(gradual.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/2118524.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted();
|
||||
|
||||
let difficulty = Difficulty::new();
|
||||
|
||||
let mut gradual = CatchGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
let mut gradual_2nd = CatchGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
let mut gradual_3rd = CatchGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
|
||||
for i in 1.. {
|
||||
let Some(next_gradual) = gradual.next() else {
|
||||
assert_eq!(i, 731);
|
||||
assert!(gradual_2nd.last().is_none()); // 730 % 2 == 0
|
||||
assert!(gradual_3rd.last().is_some()); // 730 % 3 == 1
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let expected = difficulty
|
||||
.clone()
|
||||
.passed_objects(i as u32)
|
||||
.with_mode()
|
||||
.calculate(&converted);
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,55 +135,3 @@ impl CatchGradualPerformance {
|
||||
self.difficulty.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{catch::CatchPerformance, Beatmap};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/2118524.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted();
|
||||
|
||||
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
||||
|
||||
let mut gradual = CatchGradualPerformance::new(difficulty.clone(), &converted);
|
||||
let mut gradual_2nd = CatchGradualPerformance::new(difficulty.clone(), &converted);
|
||||
let mut gradual_3rd = CatchGradualPerformance::new(difficulty.clone(), &converted);
|
||||
|
||||
let mut state = CatchScoreState::default();
|
||||
|
||||
for i in 1.. {
|
||||
state.misses += 1;
|
||||
|
||||
let Some(next_gradual) = gradual.next(state.clone()) else {
|
||||
assert_eq!(i, 731);
|
||||
assert!(gradual_2nd.last(state.clone()).is_none()); // 730 % 2 == 0
|
||||
assert!(gradual_3rd.last(state.clone()).is_some()); // 730 % 3 == 1
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let regular_calc = CatchPerformance::new(converted.as_owned())
|
||||
.difficulty(difficulty.clone())
|
||||
.passed_objects(i as u32)
|
||||
.state(state.clone());
|
||||
|
||||
let expected = regular_calc.calculate();
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,275 +595,3 @@ fn accuracy(
|
||||
|
||||
f64::from(numerator) / f64::from(denominator)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use proptest::prelude::*;
|
||||
use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::{
|
||||
any::{DifficultyAttributes, PerformanceAttributes},
|
||||
osu::{Osu, OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
static ATTRS: OnceLock<CatchDifficultyAttributes> = OnceLock::new();
|
||||
|
||||
const N_FRUITS: u32 = 728;
|
||||
const N_DROPLETS: u32 = 2;
|
||||
const N_TINY_DROPLETS: u32 = 291;
|
||||
|
||||
fn beatmap() -> Beatmap {
|
||||
Beatmap::from_path("./resources/2118524.osu").unwrap()
|
||||
}
|
||||
|
||||
fn attrs() -> CatchDifficultyAttributes {
|
||||
ATTRS
|
||||
.get_or_init(|| {
|
||||
let converted = beatmap().unchecked_into_converted::<Catch>();
|
||||
let attrs = Difficulty::new().with_mode().calculate(&converted);
|
||||
|
||||
assert_eq!(N_FRUITS, attrs.n_fruits);
|
||||
assert_eq!(N_DROPLETS, attrs.n_droplets);
|
||||
assert_eq!(N_TINY_DROPLETS, attrs.n_tiny_droplets);
|
||||
|
||||
attrs
|
||||
})
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Checks all remaining hitresult combinations w.r.t. the given parameters
|
||||
/// and returns the [`CatchScoreState`] that matches `acc` the best.
|
||||
///
|
||||
/// Very slow but accurate.
|
||||
fn brute_force_best(
|
||||
acc: f64,
|
||||
n_fruits: Option<u32>,
|
||||
n_droplets: Option<u32>,
|
||||
n_tiny_droplets: Option<u32>,
|
||||
n_tiny_droplet_misses: Option<u32>,
|
||||
misses: u32,
|
||||
) -> CatchScoreState {
|
||||
let misses = cmp::min(misses, N_FRUITS + N_DROPLETS);
|
||||
|
||||
let mut best_state = CatchScoreState {
|
||||
max_combo: N_FRUITS + N_DROPLETS - misses,
|
||||
misses,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut best_dist = f64::INFINITY;
|
||||
|
||||
let (new_fruits, new_droplets) = match (n_fruits, n_droplets) {
|
||||
(Some(mut n_fruits), Some(mut n_droplets)) => {
|
||||
let n_remaining =
|
||||
(N_FRUITS + N_DROPLETS).saturating_sub(n_fruits + n_droplets + misses);
|
||||
|
||||
let new_droplets = cmp::min(n_remaining, N_DROPLETS.saturating_sub(n_droplets));
|
||||
n_droplets += new_droplets;
|
||||
n_fruits += n_remaining - new_droplets;
|
||||
|
||||
n_fruits = cmp::min(
|
||||
n_fruits,
|
||||
(N_FRUITS + N_DROPLETS).saturating_sub(n_droplets + misses),
|
||||
);
|
||||
n_droplets = cmp::min(n_droplets, N_FRUITS + N_DROPLETS - n_fruits - misses);
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
(Some(mut n_fruits), None) => {
|
||||
let n_droplets = N_DROPLETS
|
||||
.saturating_sub(misses.saturating_sub(N_FRUITS.saturating_sub(n_fruits)));
|
||||
n_fruits = N_FRUITS + N_DROPLETS - misses - n_droplets;
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
(None, Some(mut n_droplets)) => {
|
||||
let n_fruits = N_FRUITS
|
||||
.saturating_sub(misses.saturating_sub(N_DROPLETS.saturating_sub(n_droplets)));
|
||||
n_droplets = N_FRUITS + N_DROPLETS - misses - n_fruits;
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
(None, None) => {
|
||||
let n_droplets = N_DROPLETS.saturating_sub(misses);
|
||||
let n_fruits = N_FRUITS - (misses - (N_DROPLETS.saturating_sub(n_droplets)));
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
};
|
||||
|
||||
best_state.fruits = new_fruits;
|
||||
best_state.droplets = new_droplets;
|
||||
|
||||
let (min_tiny_droplets, max_tiny_droplets) = match (n_tiny_droplets, n_tiny_droplet_misses)
|
||||
{
|
||||
(Some(n_tiny_droplets), Some(n_tiny_droplet_misses)) => {
|
||||
match (n_tiny_droplets + n_tiny_droplet_misses).cmp(&N_TINY_DROPLETS) {
|
||||
Ordering::Equal => (
|
||||
cmp::min(N_TINY_DROPLETS, n_tiny_droplets),
|
||||
cmp::min(N_TINY_DROPLETS, n_tiny_droplets),
|
||||
),
|
||||
Ordering::Less | Ordering::Greater => (0, N_TINY_DROPLETS),
|
||||
}
|
||||
}
|
||||
(Some(n_tiny_droplets), None) => (
|
||||
cmp::min(N_TINY_DROPLETS, n_tiny_droplets),
|
||||
cmp::min(N_TINY_DROPLETS, n_tiny_droplets),
|
||||
),
|
||||
(None, Some(n_tiny_droplet_misses)) => (
|
||||
N_TINY_DROPLETS.saturating_sub(n_tiny_droplet_misses),
|
||||
N_TINY_DROPLETS.saturating_sub(n_tiny_droplet_misses),
|
||||
),
|
||||
(None, None) => (0, N_TINY_DROPLETS),
|
||||
};
|
||||
|
||||
for new_tiny_droplets in min_tiny_droplets..=max_tiny_droplets {
|
||||
let new_tiny_droplet_misses = N_TINY_DROPLETS - new_tiny_droplets;
|
||||
|
||||
let curr_acc = accuracy(
|
||||
new_fruits,
|
||||
new_droplets,
|
||||
new_tiny_droplets,
|
||||
new_tiny_droplet_misses,
|
||||
misses,
|
||||
);
|
||||
|
||||
let curr_dist = (acc - curr_acc).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
best_state.tiny_droplets = new_tiny_droplets;
|
||||
best_state.tiny_droplet_misses = new_tiny_droplet_misses;
|
||||
}
|
||||
}
|
||||
|
||||
best_state
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(1000))]
|
||||
|
||||
#[test]
|
||||
fn hitresults(
|
||||
acc in 0.0..=1.0,
|
||||
n_fruits in prop::option::weighted(0.10, 0_u32..=N_FRUITS + 10),
|
||||
n_droplets in prop::option::weighted(0.10, 0_u32..=N_DROPLETS + 10),
|
||||
n_tiny_droplets in prop::option::weighted(0.10, 0_u32..=N_TINY_DROPLETS + 10),
|
||||
n_tiny_droplet_misses in prop::option::weighted(0.10, 0_u32..=N_TINY_DROPLETS + 10),
|
||||
n_misses in prop::option::weighted(0.15, 0_u32..=N_FRUITS + N_DROPLETS + 10),
|
||||
) {
|
||||
let mut state = CatchPerformance::from(attrs())
|
||||
.accuracy(acc * 100.0);
|
||||
|
||||
if let Some(n_fruits) = n_fruits {
|
||||
state = state.fruits(n_fruits);
|
||||
}
|
||||
|
||||
if let Some(n_droplets) = n_droplets {
|
||||
state = state.droplets(n_droplets);
|
||||
}
|
||||
|
||||
if let Some(n_tiny_droplets) = n_tiny_droplets {
|
||||
state = state.tiny_droplets(n_tiny_droplets);
|
||||
}
|
||||
|
||||
if let Some(n_tiny_droplet_misses) = n_tiny_droplet_misses {
|
||||
state = state.tiny_droplet_misses(n_tiny_droplet_misses);
|
||||
}
|
||||
|
||||
if let Some(misses) = n_misses {
|
||||
state = state.misses(misses);
|
||||
}
|
||||
|
||||
let first = state.generate_state();
|
||||
let state = state.generate_state();
|
||||
assert_eq!(first, state);
|
||||
|
||||
let expected = brute_force_best(
|
||||
acc,
|
||||
n_fruits,
|
||||
n_droplets,
|
||||
n_tiny_droplets,
|
||||
n_tiny_droplet_misses,
|
||||
n_misses.unwrap_or(0),
|
||||
);
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fruits_missing_objects() {
|
||||
let state = CatchPerformance::from(attrs())
|
||||
.fruits(N_FRUITS - 10)
|
||||
.droplets(N_DROPLETS - 1)
|
||||
.tiny_droplets(N_TINY_DROPLETS - 50)
|
||||
.tiny_droplet_misses(20)
|
||||
.misses(2)
|
||||
.generate_state();
|
||||
|
||||
let expected = CatchScoreState {
|
||||
max_combo: N_FRUITS + N_DROPLETS - 2,
|
||||
fruits: N_FRUITS - 2,
|
||||
droplets: N_DROPLETS,
|
||||
tiny_droplets: N_TINY_DROPLETS - 20,
|
||||
tiny_droplet_misses: 20,
|
||||
misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create() {
|
||||
let mut map = beatmap();
|
||||
let converted = map.unchecked_as_converted();
|
||||
|
||||
let _ = CatchPerformance::new(CatchDifficultyAttributes::default());
|
||||
let _ = CatchPerformance::new(CatchPerformanceAttributes::default());
|
||||
let _ = CatchPerformance::new(&converted);
|
||||
let _ = CatchPerformance::new(converted.as_owned());
|
||||
|
||||
let _ = CatchPerformance::try_new(CatchDifficultyAttributes::default()).unwrap();
|
||||
let _ = CatchPerformance::try_new(CatchPerformanceAttributes::default()).unwrap();
|
||||
let _ = CatchPerformance::try_new(DifficultyAttributes::Catch(
|
||||
CatchDifficultyAttributes::default(),
|
||||
))
|
||||
.unwrap();
|
||||
let _ = CatchPerformance::try_new(PerformanceAttributes::Catch(
|
||||
CatchPerformanceAttributes::default(),
|
||||
))
|
||||
.unwrap();
|
||||
let _ = CatchPerformance::try_new(&converted).unwrap();
|
||||
let _ = CatchPerformance::try_new(converted.as_owned()).unwrap();
|
||||
|
||||
let _ = CatchPerformance::from(CatchDifficultyAttributes::default());
|
||||
let _ = CatchPerformance::from(CatchPerformanceAttributes::default());
|
||||
let _ = CatchPerformance::from(&converted);
|
||||
let _ = CatchPerformance::from(converted);
|
||||
|
||||
let _ = CatchDifficultyAttributes::default().performance();
|
||||
let _ = CatchPerformanceAttributes::default().performance();
|
||||
|
||||
map.mode = GameMode::Osu;
|
||||
let converted = map.unchecked_as_converted::<Osu>();
|
||||
|
||||
assert!(CatchPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
|
||||
assert!(CatchPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
|
||||
assert!(CatchPerformance::try_new(DifficultyAttributes::Osu(
|
||||
OsuDifficultyAttributes::default()
|
||||
))
|
||||
.is_none());
|
||||
assert!(CatchPerformance::try_new(PerformanceAttributes::Osu(
|
||||
OsuPerformanceAttributes::default()
|
||||
))
|
||||
.is_none());
|
||||
assert!(CatchPerformance::try_new(&converted).is_none());
|
||||
assert!(CatchPerformance::try_new(converted).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,44 +214,3 @@ fn target_columns(map: &Beatmap) -> f32 {
|
||||
(rounded_od + 1.0).clamp(4.0, 7.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::util::float_ext::FloatExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn convert_mania() {
|
||||
let converted = Beatmap::from_path("./resources/2785319.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Mania>();
|
||||
|
||||
let map = &converted;
|
||||
assert!(map.is_convert);
|
||||
|
||||
assert_eq!(map.mode, GameMode::Mania);
|
||||
assert_eq!(map.version, 14);
|
||||
assert!(map.ar.eq(9.3), "{} != 9.3", map.ar);
|
||||
assert!(map.od.eq(8.8), "{} != 8.8", map.od);
|
||||
assert!(map.cs.eq(7.0), "{} != 7.0", map.cs);
|
||||
assert!(map.hp.eq(5.0), "{} != 5.0", map.hp);
|
||||
assert!(
|
||||
map.slider_multiplier.eq(1.7),
|
||||
"{} != 1.7",
|
||||
map.slider_multiplier
|
||||
);
|
||||
assert!(
|
||||
map.slider_tick_rate.eq(1.0),
|
||||
"{} != 1.0",
|
||||
map.slider_tick_rate
|
||||
);
|
||||
assert_eq!(map.hit_objects.len(), 1046);
|
||||
assert_eq!(map.hit_sounds.len(), 0);
|
||||
assert_eq!(map.timing_points.len(), 1);
|
||||
assert_eq!(map.difficulty_points.len(), 50);
|
||||
assert_eq!(map.effect_points.len(), 0);
|
||||
assert!(map.stack_leniency.eq(0.5), "{} != 0.5", map.stack_leniency);
|
||||
assert_eq!(map.breaks.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,63 +205,3 @@ fn increment_combo_raw(is_circle: bool, start_time: f64, end_time: f64, curr_com
|
||||
*curr_combo += 1 + ((end_time - start_time) / 100.0) as u32;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{mania::Mania, Beatmap};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let converted = Beatmap::from_bytes(&[])
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Mania>();
|
||||
|
||||
let mut gradual = ManiaGradualDifficulty::new(Difficulty::new(), &converted);
|
||||
|
||||
assert!(gradual.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/1638954.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Mania>();
|
||||
|
||||
let difficulty = Difficulty::new();
|
||||
|
||||
let mut gradual = ManiaGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
let mut gradual_2nd = ManiaGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
let mut gradual_3rd = ManiaGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
|
||||
let hit_objects_len = converted.hit_objects.len();
|
||||
|
||||
for i in 1.. {
|
||||
let Some(next_gradual) = gradual.next() else {
|
||||
assert_eq!(i, hit_objects_len + 1);
|
||||
assert!(gradual_2nd.last().is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last().is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let expected = difficulty
|
||||
.clone()
|
||||
.passed_objects(i as u32)
|
||||
.with_mode()
|
||||
.calculate(&converted);
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,63 +117,3 @@ impl ManiaGradualPerformance {
|
||||
self.difficulty.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
mania::{Mania, ManiaPerformance},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/1638954.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Mania>();
|
||||
|
||||
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
||||
|
||||
let mut gradual = ManiaGradualPerformance::new(difficulty.clone(), &converted);
|
||||
let mut gradual_2nd = ManiaGradualPerformance::new(difficulty.clone(), &converted);
|
||||
let mut gradual_3rd = ManiaGradualPerformance::new(difficulty.clone(), &converted);
|
||||
|
||||
let mut state = ManiaScoreState::default();
|
||||
|
||||
let hit_objects_len = converted.hit_objects.len();
|
||||
|
||||
for i in 1.. {
|
||||
state.misses += 1;
|
||||
|
||||
let Some(next_gradual) = gradual.next(state.clone()) else {
|
||||
assert_eq!(i, hit_objects_len + 1);
|
||||
assert!(gradual_2nd.last(state.clone()).is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last(state.clone()).is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let mut regular_calc = ManiaPerformance::new(converted.as_owned())
|
||||
.difficulty(difficulty.clone())
|
||||
.passed_objects(i as u32)
|
||||
.state(state.clone());
|
||||
|
||||
let regular_state = regular_calc.generate_state();
|
||||
assert_eq!(state, regular_state);
|
||||
|
||||
let expected = regular_calc.calculate();
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -931,417 +931,3 @@ fn accuracy(n320: u32, n300: u32, n200: u32, n100: u32, n50: u32, misses: u32) -
|
||||
|
||||
f64::from(numerator) / f64::from(denominator)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cmp::Ordering, sync::OnceLock};
|
||||
|
||||
use proptest::prelude::*;
|
||||
use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::{
|
||||
any::{DifficultyAttributes, PerformanceAttributes},
|
||||
osu::{Osu, OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
static ATTRS: OnceLock<ManiaDifficultyAttributes> = OnceLock::new();
|
||||
|
||||
const N_OBJECTS: u32 = 594;
|
||||
|
||||
fn beatmap() -> Beatmap {
|
||||
Beatmap::from_path("./resources/1638954.osu").unwrap()
|
||||
}
|
||||
|
||||
fn attrs() -> ManiaDifficultyAttributes {
|
||||
ATTRS
|
||||
.get_or_init(|| {
|
||||
let converted = beatmap().unchecked_into_converted::<Mania>();
|
||||
let attrs = Difficulty::new().with_mode().calculate(&converted);
|
||||
|
||||
assert_eq!(N_OBJECTS, converted.hit_objects.len() as u32);
|
||||
|
||||
attrs
|
||||
})
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Checks most remaining hitresult combinations w.r.t. the given parameters
|
||||
/// and returns the [`ManiaScoreState`] that matches `acc` the best.
|
||||
///
|
||||
/// Very slow but accurate. Only slight optimizations have been applied so
|
||||
/// that it doesn't run unreasonably long.
|
||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||
fn brute_force_best(
|
||||
acc: f64,
|
||||
n320: Option<u32>,
|
||||
n300: Option<u32>,
|
||||
n200: Option<u32>,
|
||||
n100: Option<u32>,
|
||||
n50: Option<u32>,
|
||||
misses: u32,
|
||||
best_case: bool,
|
||||
) -> ManiaScoreState {
|
||||
let misses = cmp::min(misses, N_OBJECTS);
|
||||
|
||||
let mut best_state = ManiaScoreState {
|
||||
misses,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut best_dist = f64::INFINITY;
|
||||
let mut best_custom_acc = 0.0;
|
||||
|
||||
let n_remaining = N_OBJECTS - misses;
|
||||
|
||||
let multiple_given = (usize::from(n320.is_some())
|
||||
+ usize::from(n300.is_some())
|
||||
+ usize::from(n200.is_some())
|
||||
+ usize::from(n100.is_some())
|
||||
+ usize::from(n50.is_some()))
|
||||
> 1;
|
||||
|
||||
let max_left = N_OBJECTS
|
||||
.saturating_sub(n200.unwrap_or(0) + n100.unwrap_or(0) + n50.unwrap_or(0) + misses);
|
||||
|
||||
let min_n3x0 = cmp::min(
|
||||
max_left,
|
||||
(acc * f64::from(3 * N_OBJECTS) - f64::from(2 * n_remaining)).floor() as u32,
|
||||
);
|
||||
|
||||
let max_n3x0 = cmp::min(
|
||||
max_left,
|
||||
((acc * f64::from(6 * N_OBJECTS) - f64::from(n_remaining)) / 5.0).ceil() as u32,
|
||||
);
|
||||
|
||||
let (min_n3x0, max_n3x0) = match (n320, n300) {
|
||||
(Some(n320), Some(n300)) => (
|
||||
cmp::min(n_remaining, n320 + n300),
|
||||
cmp::min(n_remaining, n320 + n300),
|
||||
),
|
||||
(Some(n320), None) => (
|
||||
cmp::max(cmp::min(n_remaining, n320), min_n3x0),
|
||||
cmp::max(max_n3x0, cmp::min(n320, n_remaining)),
|
||||
),
|
||||
(None, Some(n300)) => (
|
||||
cmp::max(cmp::min(n_remaining, n300), min_n3x0),
|
||||
cmp::max(max_n3x0, cmp::min(n300, n_remaining)),
|
||||
),
|
||||
(None, None) => (min_n3x0, max_n3x0),
|
||||
};
|
||||
|
||||
for new3x0 in min_n3x0..=max_n3x0 {
|
||||
let max_left =
|
||||
n_remaining.saturating_sub(new3x0 + n100.unwrap_or(0) + n50.unwrap_or(0));
|
||||
|
||||
let (min_n200, max_n200) = match (n200, n100, n50) {
|
||||
(Some(n200), ..) if multiple_given => {
|
||||
(cmp::min(n_remaining, n200), cmp::min(n_remaining, n200))
|
||||
}
|
||||
(Some(n200), ..) => (cmp::min(max_left, n200), cmp::min(max_left, n200)),
|
||||
(None, Some(_), Some(_)) => (max_left, max_left),
|
||||
_ => (0, max_left),
|
||||
};
|
||||
|
||||
for new200 in min_n200..=max_n200 {
|
||||
let max_left = n_remaining.saturating_sub(new3x0 + new200 + n50.unwrap_or(0));
|
||||
|
||||
let (min_n100, max_n100) = match (n100, n50) {
|
||||
(Some(n100), _) if multiple_given => {
|
||||
(cmp::min(n_remaining, n100), cmp::min(n_remaining, n100))
|
||||
}
|
||||
(Some(n100), _) => (cmp::min(max_left, n100), cmp::min(max_left, n100)),
|
||||
(None, Some(_)) => (max_left, max_left),
|
||||
(None, None) => (0, max_left),
|
||||
};
|
||||
|
||||
for new100 in min_n100..=max_n100 {
|
||||
let max_left = n_remaining.saturating_sub(new3x0 + new200 + new100);
|
||||
|
||||
let new50 = match n50 {
|
||||
Some(n50) if multiple_given => cmp::min(n_remaining, n50),
|
||||
Some(n50) => cmp::min(max_left, n50),
|
||||
None => max_left,
|
||||
};
|
||||
|
||||
let (new320, new300) = match (n320, n300) {
|
||||
(Some(n320), Some(n300)) => {
|
||||
(cmp::min(n_remaining, n320), cmp::min(n_remaining, n300))
|
||||
}
|
||||
(Some(n320), None) => (
|
||||
cmp::min(n320, n_remaining),
|
||||
new3x0 - cmp::min(n320, n_remaining),
|
||||
),
|
||||
(None, Some(n300)) => (
|
||||
new3x0 - cmp::min(n300, n_remaining),
|
||||
cmp::min(n300, n_remaining),
|
||||
),
|
||||
(None, None) if best_case => (new3x0, 0),
|
||||
(None, None) => (0, new3x0),
|
||||
};
|
||||
|
||||
let curr_acc = accuracy(new320, new300, new200, new100, new50, misses);
|
||||
let curr_dist = (acc - curr_acc).abs();
|
||||
|
||||
let curr_custom_acc =
|
||||
custom_accuracy(new320, new300, new200, new100, new50, N_OBJECTS);
|
||||
|
||||
match curr_dist.partial_cmp(&best_dist).expect("non-NaN") {
|
||||
Ordering::Less => {
|
||||
best_dist = curr_dist;
|
||||
best_custom_acc = curr_custom_acc;
|
||||
best_state.n320 = new320;
|
||||
best_state.n300 = new300;
|
||||
best_state.n200 = new200;
|
||||
best_state.n100 = new100;
|
||||
best_state.n50 = new50;
|
||||
}
|
||||
Ordering::Equal if curr_custom_acc < best_custom_acc => {
|
||||
best_custom_acc = curr_custom_acc;
|
||||
best_state.n320 = new320;
|
||||
best_state.n300 = new300;
|
||||
best_state.n200 = new200;
|
||||
best_state.n100 = new100;
|
||||
best_state.n50 = new50;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best_state.n320 + best_state.n300 + best_state.n200 + best_state.n100 + best_state.n50
|
||||
< n_remaining
|
||||
{
|
||||
let n_remaining = n_remaining
|
||||
- (best_state.n320
|
||||
+ best_state.n300
|
||||
+ best_state.n200
|
||||
+ best_state.n100
|
||||
+ best_state.n50);
|
||||
|
||||
if best_case {
|
||||
match (n320, n300, n200, n100, n50) {
|
||||
(None, ..) => best_state.n320 += n_remaining,
|
||||
(_, None, ..) => best_state.n300 += n_remaining,
|
||||
(_, _, None, ..) => best_state.n200 += n_remaining,
|
||||
(.., None, _) => best_state.n100 += n_remaining,
|
||||
(.., None) => best_state.n50 += n_remaining,
|
||||
_ => best_state.n320 += n_remaining,
|
||||
}
|
||||
} else {
|
||||
match (n50, n100, n200, n300, n320) {
|
||||
(None, ..) => best_state.n50 += n_remaining,
|
||||
(_, None, ..) => best_state.n100 += n_remaining,
|
||||
(_, _, None, ..) => best_state.n200 += n_remaining,
|
||||
(.., None, _) => best_state.n300 += n_remaining,
|
||||
(.., None) => best_state.n320 += n_remaining,
|
||||
_ => best_state.n50 += n_remaining,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best_case {
|
||||
if n320.is_none() && n200.is_none() && n100.is_none() {
|
||||
let n = best_state.n200 / 2;
|
||||
best_state.n320 += n;
|
||||
best_state.n200 -= 2 * n;
|
||||
best_state.n100 += n;
|
||||
}
|
||||
|
||||
if n100.is_none() && n50.is_none() {
|
||||
let n = if n320.is_none() && n300.is_none() {
|
||||
let n = cmp::min(best_state.n320 + best_state.n300, best_state.n50 / 4);
|
||||
|
||||
let removed320 = cmp::min(best_state.n320, n);
|
||||
let removed300 = n - removed320;
|
||||
|
||||
best_state.n320 -= removed320;
|
||||
best_state.n300 -= removed300;
|
||||
|
||||
n
|
||||
} else if n320.is_none() {
|
||||
let n = cmp::min(best_state.n320, best_state.n50 / 4);
|
||||
best_state.n320 -= n;
|
||||
|
||||
n
|
||||
} else if n300.is_none() {
|
||||
let n = cmp::min(best_state.n300, best_state.n50 / 4);
|
||||
best_state.n300 -= n;
|
||||
|
||||
n
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
best_state.n100 += 5 * n;
|
||||
best_state.n50 -= 4 * n;
|
||||
}
|
||||
} else if n320.is_none() && n200.is_none() && n100.is_none() {
|
||||
let n = cmp::min(best_state.n320, best_state.n100);
|
||||
best_state.n320 -= n;
|
||||
best_state.n200 += 2 * n;
|
||||
best_state.n100 -= n;
|
||||
}
|
||||
|
||||
best_state
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(50))]
|
||||
|
||||
#[test]
|
||||
fn mania_hitresults(
|
||||
acc in 0.0..=1.0,
|
||||
n320 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
|
||||
n300 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
|
||||
n200 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
|
||||
n100 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
|
||||
n50 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
|
||||
n_misses in prop::option::weighted(0.15, 0_u32..=N_OBJECTS + 10),
|
||||
best_case in prop::bool::ANY,
|
||||
) {
|
||||
let priority = if best_case {
|
||||
HitResultPriority::BestCase
|
||||
} else {
|
||||
HitResultPriority::WorstCase
|
||||
};
|
||||
|
||||
let mut state = ManiaPerformance::from(attrs())
|
||||
.accuracy(acc * 100.0)
|
||||
.hitresult_priority(priority);
|
||||
|
||||
if let Some(n320) = n320 {
|
||||
state = state.n320(n320);
|
||||
}
|
||||
|
||||
if let Some(n300) = n300 {
|
||||
state = state.n300(n300);
|
||||
}
|
||||
|
||||
if let Some(n200) = n200 {
|
||||
state = state.n200(n200);
|
||||
}
|
||||
|
||||
if let Some(n100) = n100 {
|
||||
state = state.n100(n100);
|
||||
}
|
||||
|
||||
if let Some(n50) = n50 {
|
||||
state = state.n50(n50);
|
||||
}
|
||||
|
||||
if let Some(misses) = n_misses {
|
||||
state = state.misses(misses);
|
||||
}
|
||||
|
||||
let first = state.generate_state();
|
||||
let state = state.generate_state();
|
||||
assert_eq!(first, state);
|
||||
|
||||
let expected = brute_force_best(
|
||||
acc,
|
||||
n320,
|
||||
n300,
|
||||
n200,
|
||||
n100,
|
||||
n50,
|
||||
n_misses.unwrap_or(0),
|
||||
best_case,
|
||||
);
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n320_misses_best() {
|
||||
let state = ManiaPerformance::from(attrs())
|
||||
.n320(500)
|
||||
.misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_state();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 500,
|
||||
n300: 92,
|
||||
n200: 0,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n100_n50_misses_worst() {
|
||||
let state = ManiaPerformance::from(attrs())
|
||||
.n100(200)
|
||||
.n50(50)
|
||||
.misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_state();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 0,
|
||||
n300: 0,
|
||||
n200: 342,
|
||||
n100: 200,
|
||||
n50: 50,
|
||||
misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create() {
|
||||
let mut map = beatmap();
|
||||
let converted = map.unchecked_as_converted();
|
||||
|
||||
let _ = ManiaPerformance::new(ManiaDifficultyAttributes::default());
|
||||
let _ = ManiaPerformance::new(ManiaPerformanceAttributes::default());
|
||||
let _ = ManiaPerformance::new(&converted);
|
||||
let _ = ManiaPerformance::new(converted.as_owned());
|
||||
|
||||
let _ = ManiaPerformance::try_new(ManiaDifficultyAttributes::default()).unwrap();
|
||||
let _ = ManiaPerformance::try_new(ManiaPerformanceAttributes::default()).unwrap();
|
||||
let _ = ManiaPerformance::try_new(DifficultyAttributes::Mania(
|
||||
ManiaDifficultyAttributes::default(),
|
||||
))
|
||||
.unwrap();
|
||||
let _ = ManiaPerformance::try_new(PerformanceAttributes::Mania(
|
||||
ManiaPerformanceAttributes::default(),
|
||||
))
|
||||
.unwrap();
|
||||
let _ = ManiaPerformance::try_new(&converted).unwrap();
|
||||
let _ = ManiaPerformance::try_new(converted.as_owned()).unwrap();
|
||||
|
||||
let _ = ManiaPerformance::from(ManiaDifficultyAttributes::default());
|
||||
let _ = ManiaPerformance::from(ManiaPerformanceAttributes::default());
|
||||
let _ = ManiaPerformance::from(&converted);
|
||||
let _ = ManiaPerformance::from(converted);
|
||||
|
||||
let _ = ManiaDifficultyAttributes::default().performance();
|
||||
let _ = ManiaPerformanceAttributes::default().performance();
|
||||
|
||||
map.mode = GameMode::Osu;
|
||||
let converted = map.unchecked_as_converted::<Osu>();
|
||||
|
||||
assert!(ManiaPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
|
||||
assert!(ManiaPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
|
||||
assert!(ManiaPerformance::try_new(DifficultyAttributes::Osu(
|
||||
OsuDifficultyAttributes::default()
|
||||
))
|
||||
.is_none());
|
||||
assert!(ManiaPerformance::try_new(PerformanceAttributes::Osu(
|
||||
OsuPerformanceAttributes::default()
|
||||
))
|
||||
.is_none());
|
||||
assert!(ManiaPerformance::try_new(&converted).is_none());
|
||||
assert!(ManiaPerformance::try_new(converted).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,87 +385,3 @@ impl ModsDependentKind {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rosu_mods::{generated_mods::DifficultyAdjustOsu, GameMod, GameMods};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_ar() {
|
||||
let gamemod = GameMod::HiddenOsu(Default::default());
|
||||
let diff = Difficulty::new().mods(GameMods::from(gamemod));
|
||||
let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build();
|
||||
|
||||
assert_eq!(attrs.ar, 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_ar_without_mods() {
|
||||
let gamemod = GameMod::DoubleTimeOsu(Default::default());
|
||||
let diff = Difficulty::new().mods(GameMods::from(gamemod));
|
||||
let attrs = BeatmapAttributesBuilder::new()
|
||||
.ar(8.5, false)
|
||||
.difficulty(&diff)
|
||||
.build();
|
||||
|
||||
assert_eq!(attrs.ar, 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_ar_with_mods() {
|
||||
let gamemod = GameMod::DoubleTimeOsu(Default::default());
|
||||
let diff = Difficulty::new().mods(GameMods::from(gamemod));
|
||||
let attrs = BeatmapAttributesBuilder::new()
|
||||
.ar(8.5, true)
|
||||
.difficulty(&diff)
|
||||
.build();
|
||||
|
||||
assert_eq!(attrs.ar, 8.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_mods_ar() {
|
||||
let mut mods = GameMods::new();
|
||||
mods.insert(GameMod::DoubleTimeCatch(Default::default()));
|
||||
mods.insert(GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu {
|
||||
approach_rate: Some(7.0),
|
||||
..Default::default()
|
||||
}));
|
||||
let diff = Difficulty::new().mods(mods);
|
||||
let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build();
|
||||
|
||||
assert_eq!(attrs.ar, 9.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_ar_custom_mods_ar_without_mods() {
|
||||
let mut mods = GameMods::new();
|
||||
mods.insert(GameMod::DoubleTimeCatch(Default::default()));
|
||||
mods.insert(GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu {
|
||||
approach_rate: Some(9.0),
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let diff = Difficulty::new().mods(mods).ar(8.5, false);
|
||||
let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build();
|
||||
|
||||
assert_eq!(attrs.ar, 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_ar_custom_mods_ar_with_mods() {
|
||||
let mut mods = GameMods::new();
|
||||
mods.insert(GameMod::DoubleTimeCatch(Default::default()));
|
||||
mods.insert(GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu {
|
||||
approach_rate: Some(9.0),
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let diff = Difficulty::new().mods(mods).ar(8.5, true);
|
||||
let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build();
|
||||
|
||||
assert_eq!(attrs.ar, 8.5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,63 +254,3 @@ mod osu_objects {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{osu::Osu, Beatmap};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let converted = Beatmap::from_bytes(&[])
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Osu>();
|
||||
|
||||
let mut gradual = OsuGradualDifficulty::new(Difficulty::new(), &converted);
|
||||
|
||||
assert!(gradual.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/2785319.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Osu>();
|
||||
|
||||
let difficulty = Difficulty::new();
|
||||
|
||||
let mut gradual = OsuGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
let mut gradual_2nd = OsuGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
let mut gradual_3rd = OsuGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
|
||||
let hit_objects_len = converted.hit_objects.len();
|
||||
|
||||
for i in 1.. {
|
||||
let Some(next_gradual) = gradual.next() else {
|
||||
assert_eq!(i, hit_objects_len + 1);
|
||||
assert!(gradual_2nd.last().is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last().is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let expected = difficulty
|
||||
.clone()
|
||||
.passed_objects(i as u32)
|
||||
.with_mode()
|
||||
.calculate(&converted);
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,63 +127,3 @@ impl OsuGradualPerformance {
|
||||
self.difficulty.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
osu::{Osu, OsuPerformance},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/2785319.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Osu>();
|
||||
|
||||
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
||||
|
||||
let mut gradual = OsuGradualPerformance::new(difficulty.clone(), &converted);
|
||||
let mut gradual_2nd = OsuGradualPerformance::new(difficulty.clone(), &converted);
|
||||
let mut gradual_3rd = OsuGradualPerformance::new(difficulty.clone(), &converted);
|
||||
|
||||
let mut state = OsuScoreState::default();
|
||||
|
||||
let hit_objects_len = converted.hit_objects.len();
|
||||
|
||||
for i in 1.. {
|
||||
state.misses += 1;
|
||||
|
||||
let Some(next_gradual) = gradual.next(state) else {
|
||||
assert_eq!(i, hit_objects_len + 1);
|
||||
assert!(gradual_2nd.last(state).is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last(state).is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(state, 1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(state, 2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let mut regular_calc = OsuPerformance::new(converted.as_owned())
|
||||
.difficulty(difficulty.clone())
|
||||
.passed_objects(i as u32)
|
||||
.state(state);
|
||||
|
||||
let regular_state = regular_calc.generate_state();
|
||||
assert_eq!(state, regular_state);
|
||||
|
||||
let expected = regular_calc.calculate();
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,326 +885,3 @@ fn accuracy(n300: u32, n100: u32, n50: u32, misses: u32) -> f64 {
|
||||
|
||||
f64::from(numerator) / f64::from(denominator)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::{
|
||||
any::{DifficultyAttributes, PerformanceAttributes},
|
||||
taiko::{Taiko, TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
static ATTRS: OnceLock<OsuDifficultyAttributes> = OnceLock::new();
|
||||
|
||||
const N_OBJECTS: u32 = 601;
|
||||
|
||||
fn beatmap() -> Beatmap {
|
||||
Beatmap::from_path("./resources/2785319.osu").unwrap()
|
||||
}
|
||||
|
||||
fn attrs() -> OsuDifficultyAttributes {
|
||||
ATTRS
|
||||
.get_or_init(|| {
|
||||
let converted = beatmap().unchecked_into_converted::<Osu>();
|
||||
let attrs = Difficulty::new().with_mode().calculate(&converted);
|
||||
|
||||
assert_eq!(
|
||||
(attrs.n_circles, attrs.n_sliders, attrs.n_spinners),
|
||||
(307, 293, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
attrs.n_circles + attrs.n_sliders + attrs.n_spinners,
|
||||
N_OBJECTS,
|
||||
);
|
||||
|
||||
attrs
|
||||
})
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Checks all remaining hitresult combinations w.r.t. the given parameters
|
||||
/// and returns the [`OsuScoreState`] that matches `acc` the best.
|
||||
///
|
||||
/// Very slow but accurate.
|
||||
fn brute_force_best(
|
||||
acc: f64,
|
||||
n300: Option<u32>,
|
||||
n100: Option<u32>,
|
||||
n50: Option<u32>,
|
||||
misses: u32,
|
||||
best_case: bool,
|
||||
) -> OsuScoreState {
|
||||
let misses = cmp::min(misses, N_OBJECTS);
|
||||
|
||||
let mut best_state = OsuScoreState {
|
||||
misses,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut best_dist = f64::INFINITY;
|
||||
|
||||
let n_remaining = N_OBJECTS - misses;
|
||||
|
||||
let (min_n300, max_n300) = match (n300, n100, n50) {
|
||||
(Some(n300), ..) => (cmp::min(n_remaining, n300), cmp::min(n_remaining, n300)),
|
||||
(None, Some(n100), Some(n50)) => (
|
||||
n_remaining.saturating_sub(n100 + n50),
|
||||
n_remaining.saturating_sub(n100 + n50),
|
||||
),
|
||||
(None, ..) => (
|
||||
0,
|
||||
n_remaining.saturating_sub(n100.unwrap_or(0) + n50.unwrap_or(0)),
|
||||
),
|
||||
};
|
||||
|
||||
for new300 in min_n300..=max_n300 {
|
||||
let (min_n100, max_n100) = match (n100, n50) {
|
||||
(Some(n100), _) => (cmp::min(n_remaining, n100), cmp::min(n_remaining, n100)),
|
||||
(None, Some(n50)) => (
|
||||
n_remaining.saturating_sub(new300 + n50),
|
||||
n_remaining.saturating_sub(new300 + n50),
|
||||
),
|
||||
(None, None) => (0, n_remaining - new300),
|
||||
};
|
||||
|
||||
for new100 in min_n100..=max_n100 {
|
||||
let new50 = match n50 {
|
||||
Some(n50) => cmp::min(n_remaining, n50),
|
||||
None => n_remaining.saturating_sub(new300 + new100),
|
||||
};
|
||||
|
||||
let curr_acc = accuracy(new300, new100, new50, misses);
|
||||
let curr_dist = (acc - curr_acc).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
best_state.n300 = new300;
|
||||
best_state.n100 = new100;
|
||||
best_state.n50 = new50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best_state.n300 + best_state.n100 + best_state.n50 < n_remaining {
|
||||
let remaining = n_remaining - (best_state.n300 + best_state.n100 + best_state.n50);
|
||||
|
||||
if best_case {
|
||||
best_state.n300 += remaining;
|
||||
} else {
|
||||
best_state.n50 += remaining;
|
||||
}
|
||||
}
|
||||
|
||||
if n300.is_none() && n100.is_none() && n50.is_none() {
|
||||
if best_case {
|
||||
let n = cmp::min(best_state.n300, best_state.n50 / 4);
|
||||
best_state.n300 -= n;
|
||||
best_state.n100 += 5 * n;
|
||||
best_state.n50 -= 4 * n;
|
||||
} else {
|
||||
let n = best_state.n100 / 5;
|
||||
best_state.n300 += n;
|
||||
best_state.n100 -= 5 * n;
|
||||
best_state.n50 += 4 * n;
|
||||
}
|
||||
}
|
||||
|
||||
best_state
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(1000))]
|
||||
|
||||
#[test]
|
||||
fn hitresults(
|
||||
acc in 0.0..=1.0,
|
||||
n300 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
|
||||
n100 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
|
||||
n50 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
|
||||
n_misses in prop::option::weighted(0.15, 0_u32..=N_OBJECTS + 10),
|
||||
best_case in prop::bool::ANY,
|
||||
) {
|
||||
let attrs = attrs();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let priority = if best_case {
|
||||
HitResultPriority::BestCase
|
||||
} else {
|
||||
HitResultPriority::WorstCase
|
||||
};
|
||||
|
||||
let mut state = OsuPerformance::from(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(n50) = n50 {
|
||||
state = state.n50(n50);
|
||||
}
|
||||
|
||||
if let Some(misses) = n_misses {
|
||||
state = state.misses(misses);
|
||||
}
|
||||
|
||||
let first = state.generate_state();
|
||||
let state = state.generate_state();
|
||||
assert_eq!(first, state);
|
||||
|
||||
let mut expected = brute_force_best(
|
||||
acc,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
n_misses.unwrap_or(0),
|
||||
best_case,
|
||||
);
|
||||
expected.max_combo = max_combo.saturating_sub(n_misses.map_or(0, |n| cmp::min(n, N_OBJECTS)));
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n300_n100_misses_best() {
|
||||
let state = OsuPerformance::from(attrs())
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
.n100(20)
|
||||
.misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_state();
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 300,
|
||||
n100: 20,
|
||||
n50: 279,
|
||||
misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n300_n50_misses_best() {
|
||||
let state = OsuPerformance::from(attrs())
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
.n50(10)
|
||||
.misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_state();
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 300,
|
||||
n100: 289,
|
||||
n50: 10,
|
||||
misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n50_misses_worst() {
|
||||
let state = OsuPerformance::from(attrs())
|
||||
.combo(500)
|
||||
.n50(10)
|
||||
.misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_state();
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 0,
|
||||
n100: 589,
|
||||
n50: 10,
|
||||
misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n300_n100_n50_misses_worst() {
|
||||
let state = OsuPerformance::from(attrs())
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
.n100(50)
|
||||
.n50(10)
|
||||
.misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_state();
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 300,
|
||||
n100: 50,
|
||||
n50: 249,
|
||||
misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create() {
|
||||
let mut map = beatmap();
|
||||
let converted = map.unchecked_as_converted();
|
||||
|
||||
let _ = OsuPerformance::new(OsuDifficultyAttributes::default());
|
||||
let _ = OsuPerformance::new(OsuPerformanceAttributes::default());
|
||||
let _ = OsuPerformance::new(&converted);
|
||||
let _ = OsuPerformance::new(converted.as_owned());
|
||||
|
||||
let _ = OsuPerformance::try_new(OsuDifficultyAttributes::default()).unwrap();
|
||||
let _ = OsuPerformance::try_new(OsuPerformanceAttributes::default()).unwrap();
|
||||
let _ =
|
||||
OsuPerformance::try_new(DifficultyAttributes::Osu(OsuDifficultyAttributes::default()))
|
||||
.unwrap();
|
||||
let _ = OsuPerformance::try_new(PerformanceAttributes::Osu(
|
||||
OsuPerformanceAttributes::default(),
|
||||
))
|
||||
.unwrap();
|
||||
let _ = OsuPerformance::try_new(&converted).unwrap();
|
||||
let _ = OsuPerformance::try_new(converted.as_owned()).unwrap();
|
||||
|
||||
let _ = OsuPerformance::from(OsuDifficultyAttributes::default());
|
||||
let _ = OsuPerformance::from(OsuPerformanceAttributes::default());
|
||||
let _ = OsuPerformance::from(&converted);
|
||||
let _ = OsuPerformance::from(converted);
|
||||
|
||||
let _ = OsuDifficultyAttributes::default().performance();
|
||||
let _ = OsuPerformanceAttributes::default().performance();
|
||||
|
||||
map.mode = GameMode::Taiko;
|
||||
let converted = map.unchecked_as_converted::<Taiko>();
|
||||
|
||||
assert!(OsuPerformance::try_new(TaikoDifficultyAttributes::default()).is_none());
|
||||
assert!(OsuPerformance::try_new(TaikoPerformanceAttributes::default()).is_none());
|
||||
assert!(OsuPerformance::try_new(DifficultyAttributes::Taiko(
|
||||
TaikoDifficultyAttributes::default()
|
||||
))
|
||||
.is_none());
|
||||
assert!(OsuPerformance::try_new(PerformanceAttributes::Taiko(
|
||||
TaikoPerformanceAttributes::default()
|
||||
))
|
||||
.is_none());
|
||||
assert!(OsuPerformance::try_new(&converted).is_none());
|
||||
assert!(OsuPerformance::try_new(converted).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,67 +251,3 @@ impl ExactSizeIterator for TaikoGradualDifficulty {
|
||||
self.total_hits - self.idx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Beatmap;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let converted = Beatmap::from_bytes(&[]).unwrap().unchecked_into_converted();
|
||||
|
||||
let mut gradual = TaikoGradualDifficulty::new(Difficulty::new(), &converted);
|
||||
|
||||
assert!(gradual.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/1028484.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted();
|
||||
|
||||
let difficulty = Difficulty::new();
|
||||
|
||||
let mut gradual = TaikoGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
let mut gradual_2nd = TaikoGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
let mut gradual_3rd = TaikoGradualDifficulty::new(difficulty.clone(), &converted);
|
||||
|
||||
let hit_objects_len = converted.hit_objects.len();
|
||||
|
||||
let n_hits = converted
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter(|h| h.is_circle())
|
||||
.count();
|
||||
|
||||
for i in 1.. {
|
||||
let Some(next_gradual) = gradual.next() else {
|
||||
assert_eq!(i, n_hits + 1);
|
||||
assert!(gradual_2nd.last().is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last().is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let expected = difficulty
|
||||
.clone()
|
||||
.passed_objects(i as u32)
|
||||
.with_mode()
|
||||
.calculate(&converted);
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,66 +126,3 @@ impl TaikoGradualPerformance {
|
||||
self.difficulty.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{taiko::TaikoPerformance, Beatmap};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/1028484.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted();
|
||||
|
||||
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
||||
|
||||
let mut gradual = TaikoGradualPerformance::new(difficulty.clone(), &converted);
|
||||
let mut gradual_2nd = TaikoGradualPerformance::new(difficulty.clone(), &converted);
|
||||
let mut gradual_3rd = TaikoGradualPerformance::new(difficulty.clone(), &converted);
|
||||
|
||||
let mut state = TaikoScoreState::default();
|
||||
|
||||
let hit_objects_len = converted.hit_objects.len();
|
||||
|
||||
let n_hits = converted
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter(|h| h.is_circle())
|
||||
.count();
|
||||
|
||||
for i in 1.. {
|
||||
state.misses += 1;
|
||||
|
||||
let Some(next_gradual) = gradual.next(state) else {
|
||||
assert_eq!(i, n_hits + 1);
|
||||
assert!(gradual_2nd.last(state).is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last(state).is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(state, 1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(state, 2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let mut regular_calc = TaikoPerformance::new(converted.as_owned())
|
||||
.difficulty(difficulty.clone())
|
||||
.passed_objects(i as u32)
|
||||
.state(state);
|
||||
|
||||
let regular_state = regular_calc.generate_state();
|
||||
assert_eq!(state, regular_state);
|
||||
|
||||
let expected = regular_calc.calculate();
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,236 +516,3 @@ fn accuracy(n300: u32, n100: u32, misses: u32) -> f64 {
|
||||
|
||||
f64::from(numerator) / f64::from(denominator)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use proptest::prelude::*;
|
||||
use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::{
|
||||
any::{DifficultyAttributes, PerformanceAttributes},
|
||||
osu::{Osu, OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
static ATTRS: OnceLock<TaikoDifficultyAttributes> = OnceLock::new();
|
||||
|
||||
const MAX_COMBO: u32 = 289;
|
||||
|
||||
fn beatmap() -> Beatmap {
|
||||
Beatmap::from_path("./resources/1028484.osu").unwrap()
|
||||
}
|
||||
|
||||
fn attrs() -> TaikoDifficultyAttributes {
|
||||
ATTRS
|
||||
.get_or_init(|| {
|
||||
let converted = beatmap().unchecked_into_converted::<Taiko>();
|
||||
let attrs = Difficulty::new().with_mode().calculate(&converted);
|
||||
|
||||
assert_eq!(MAX_COMBO, attrs.max_combo);
|
||||
|
||||
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<u32>,
|
||||
n100: Option<u32>,
|
||||
misses: u32,
|
||||
best_case: bool,
|
||||
) -> TaikoScoreState {
|
||||
let misses = cmp::min(misses, MAX_COMBO);
|
||||
|
||||
let mut best_state = TaikoScoreState {
|
||||
misses,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut best_dist = f64::INFINITY;
|
||||
|
||||
let n_objects = MAX_COMBO;
|
||||
let n_remaining = n_objects - misses;
|
||||
|
||||
let (min_n300, max_n300) = match (n300, n100) {
|
||||
(Some(n300), _) => (cmp::min(n_remaining, n300), cmp::min(n_remaining, n300)),
|
||||
(None, Some(n100)) => (
|
||||
n_remaining.saturating_sub(n100),
|
||||
n_remaining.saturating_sub(n100),
|
||||
),
|
||||
(None, None) => (0, n_remaining),
|
||||
};
|
||||
|
||||
for new300 in min_n300..=max_n300 {
|
||||
let new100 = match n100 {
|
||||
Some(n100) => cmp::min(n_remaining, n100),
|
||||
None => n_remaining - new300,
|
||||
};
|
||||
|
||||
let curr_acc = accuracy(new300, new100, misses);
|
||||
let curr_dist = (acc - curr_acc).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
best_state.n300 = new300;
|
||||
best_state.n100 = new100;
|
||||
}
|
||||
}
|
||||
|
||||
if best_state.n300 + best_state.n100 < n_remaining {
|
||||
let remaining = n_remaining - (best_state.n300 + best_state.n100);
|
||||
|
||||
if best_case {
|
||||
best_state.n300 += remaining;
|
||||
} else {
|
||||
best_state.n100 += remaining;
|
||||
}
|
||||
}
|
||||
|
||||
best_state
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(1000))]
|
||||
|
||||
#[test]
|
||||
fn hitresults(
|
||||
acc in 0.0..=1.0,
|
||||
n300 in prop::option::weighted(0.10, 0_u32..=MAX_COMBO + 10),
|
||||
n100 in prop::option::weighted(0.10, 0_u32..=MAX_COMBO + 10),
|
||||
n_misses in prop::option::weighted(0.15, 0_u32..=MAX_COMBO + 10),
|
||||
best_case in prop::bool::ANY,
|
||||
) {
|
||||
let priority = if best_case {
|
||||
HitResultPriority::BestCase
|
||||
} else {
|
||||
HitResultPriority::WorstCase
|
||||
};
|
||||
|
||||
let mut state = TaikoPerformance::from(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(misses) = n_misses {
|
||||
state = state.misses(misses);
|
||||
}
|
||||
|
||||
let first = state.generate_state();
|
||||
let state = state.generate_state();
|
||||
assert_eq!(first, state);
|
||||
|
||||
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!(state, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n300_misses_best() {
|
||||
let state = TaikoPerformance::from(attrs())
|
||||
.combo(100)
|
||||
.n300(150)
|
||||
.misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_state();
|
||||
|
||||
let expected = TaikoScoreState {
|
||||
max_combo: 100,
|
||||
n300: 150,
|
||||
n100: 137,
|
||||
misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_misses_best() {
|
||||
let state = TaikoPerformance::from(attrs())
|
||||
.combo(100)
|
||||
.misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_state();
|
||||
|
||||
let expected = TaikoScoreState {
|
||||
max_combo: 100,
|
||||
n300: 287,
|
||||
n100: 0,
|
||||
misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create() {
|
||||
let mut map = beatmap();
|
||||
let converted = map.unchecked_as_converted();
|
||||
|
||||
let _ = TaikoPerformance::new(TaikoDifficultyAttributes::default());
|
||||
let _ = TaikoPerformance::new(TaikoPerformanceAttributes::default());
|
||||
let _ = TaikoPerformance::new(&converted);
|
||||
let _ = TaikoPerformance::new(converted.as_owned());
|
||||
|
||||
let _ = TaikoPerformance::try_new(TaikoDifficultyAttributes::default()).unwrap();
|
||||
let _ = TaikoPerformance::try_new(TaikoPerformanceAttributes::default()).unwrap();
|
||||
let _ = TaikoPerformance::try_new(DifficultyAttributes::Taiko(
|
||||
TaikoDifficultyAttributes::default(),
|
||||
))
|
||||
.unwrap();
|
||||
let _ = TaikoPerformance::try_new(PerformanceAttributes::Taiko(
|
||||
TaikoPerformanceAttributes::default(),
|
||||
))
|
||||
.unwrap();
|
||||
let _ = TaikoPerformance::try_new(&converted).unwrap();
|
||||
let _ = TaikoPerformance::try_new(converted.as_owned()).unwrap();
|
||||
|
||||
let _ = TaikoPerformance::from(TaikoDifficultyAttributes::default());
|
||||
let _ = TaikoPerformance::from(TaikoPerformanceAttributes::default());
|
||||
let _ = TaikoPerformance::from(&converted);
|
||||
let _ = TaikoPerformance::from(converted);
|
||||
|
||||
let _ = TaikoDifficultyAttributes::default().performance();
|
||||
let _ = TaikoPerformanceAttributes::default().performance();
|
||||
|
||||
map.mode = GameMode::Osu;
|
||||
let converted = map.unchecked_as_converted::<Osu>();
|
||||
|
||||
assert!(TaikoPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
|
||||
assert!(TaikoPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
|
||||
assert!(TaikoPerformance::try_new(DifficultyAttributes::Osu(
|
||||
OsuDifficultyAttributes::default()
|
||||
))
|
||||
.is_none());
|
||||
assert!(TaikoPerformance::try_new(PerformanceAttributes::Osu(
|
||||
OsuPerformanceAttributes::default()
|
||||
))
|
||||
.is_none());
|
||||
assert!(TaikoPerformance::try_new(&converted).is_none());
|
||||
assert!(TaikoPerformance::try_new(converted).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,108 +154,3 @@ impl<T, const N: usize> Index<usize> for LimitedQueue<T, N> {
|
||||
&self.queue[idx]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::cmp;
|
||||
|
||||
use super::LimitedQueue;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let queue = LimitedQueue::<u8, 4>::default();
|
||||
assert!(queue.is_empty());
|
||||
assert_eq!(queue.last(), None);
|
||||
assert_eq!(queue.iter().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_push() {
|
||||
let mut queue = LimitedQueue::<u8, 4>::default();
|
||||
let elem = 42;
|
||||
queue.push(elem);
|
||||
assert!(!queue.is_empty());
|
||||
assert_eq!(queue.len(), 1);
|
||||
assert_eq!(queue.last(), Some(&elem));
|
||||
assert!(queue.iter().eq([elem].iter()));
|
||||
assert_eq!(queue[0], elem);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overfull() {
|
||||
let mut queue = LimitedQueue::<u8, 4>::default();
|
||||
|
||||
for i in 1..=5 {
|
||||
queue.push(i as u8);
|
||||
assert_eq!(cmp::min(i, 4), queue.len());
|
||||
}
|
||||
|
||||
assert_eq!(queue.last(), Some(&5));
|
||||
assert!(queue.iter().eq(&[2, 3, 4, 5]));
|
||||
assert_eq!(queue[0], 2);
|
||||
assert_eq!(queue[3], 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_slices() {
|
||||
let mut queue = LimitedQueue::<u8, 3>::default();
|
||||
assert_eq!(queue.as_slices(), ([].as_slice(), [].as_slice()));
|
||||
|
||||
// Start by filling the tail slice
|
||||
queue.push(1);
|
||||
assert_eq!(queue.as_slices(), ([].as_slice(), [1].as_slice()));
|
||||
queue.push(2);
|
||||
assert_eq!(queue.as_slices(), ([].as_slice(), [1, 2].as_slice()));
|
||||
queue.push(3);
|
||||
assert_eq!(queue.as_slices(), ([].as_slice(), [1, 2, 3].as_slice()));
|
||||
|
||||
// The buffer is full and wraps around so now it uses the head slice
|
||||
queue.push(4);
|
||||
assert_eq!(queue.as_slices(), ([2, 3].as_slice(), [4].as_slice()));
|
||||
queue.push(5);
|
||||
assert_eq!(queue.as_slices(), ([3].as_slice(), [4, 5].as_slice()));
|
||||
queue.push(6);
|
||||
assert_eq!(queue.as_slices(), ([].as_slice(), [4, 5, 6].as_slice()));
|
||||
|
||||
queue.push(7);
|
||||
assert_eq!(queue.as_slices(), ([5, 6].as_slice(), [7].as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_nth() {
|
||||
const CAPACITY: usize = 5;
|
||||
const LIMIT: usize = 11;
|
||||
|
||||
let mut queue = LimitedQueue::<u8, CAPACITY>::default();
|
||||
|
||||
// The queue is not yet full
|
||||
for n in 0..CAPACITY {
|
||||
queue.push(n as u8);
|
||||
|
||||
for i in 0..=n {
|
||||
assert_eq!(queue.iter().nth(i), Some(&(i as u8)));
|
||||
}
|
||||
|
||||
for i in n + 1..CAPACITY {
|
||||
assert_eq!(queue.iter().nth(i), None);
|
||||
}
|
||||
}
|
||||
|
||||
// The queue is full
|
||||
for n in CAPACITY..LIMIT {
|
||||
queue.push(n as u8);
|
||||
|
||||
let (head, tail) = queue.as_slices();
|
||||
|
||||
for (i, item) in head.iter().enumerate() {
|
||||
assert_eq!(queue.iter().nth(i), Some(item));
|
||||
}
|
||||
|
||||
for (i, item) in tail.iter().enumerate() {
|
||||
assert_eq!(queue.iter().nth(head.len() + i), Some(item));
|
||||
}
|
||||
|
||||
assert_eq!(queue.iter().nth(head.len() + tail.len() + 1), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,30 +104,3 @@ impl TandemSorter {
|
||||
idx ^ !(usize::MAX >> 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::TandemSorter;
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(1000))]
|
||||
|
||||
#[test]
|
||||
fn sort(mut actual in prop::collection::vec(0_u8..100, 0..100)) {
|
||||
let mut expected_sorted = actual.clone();
|
||||
expected_sorted.sort_unstable();
|
||||
|
||||
let expected_unsorted = actual.clone();
|
||||
|
||||
let mut sorter = TandemSorter::new_unstable(&actual, u8::cmp);
|
||||
|
||||
sorter.sort(&mut actual);
|
||||
assert_eq!(actual, expected_sorted);
|
||||
|
||||
sorter.unsort(&mut actual);
|
||||
assert_eq!(actual, expected_unsorted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,57 +265,6 @@ mod inner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::util::float_ext::FloatExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn expected(mut values in prop::collection::vec(prop::option::of(0.0..1_000.0), 0..1_000)) {
|
||||
let mut vec = StrainsVec::with_capacity(values.len());
|
||||
|
||||
let mut additional_zeros = 0;
|
||||
let mut prev_zero = false;
|
||||
let mut sum = 0.0;
|
||||
|
||||
for opt in values.iter().copied() {
|
||||
if let Some(value) = opt {
|
||||
vec.push(value);
|
||||
prev_zero = false;
|
||||
sum += value;
|
||||
} else {
|
||||
vec.push(0.0);
|
||||
|
||||
if prev_zero {
|
||||
additional_zeros += 1;
|
||||
}
|
||||
|
||||
prev_zero = true;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(vec.len(), values.len());
|
||||
assert_eq!(vec.inner.len(), values.len() - additional_zeros);
|
||||
assert!(vec.sum().eq(sum));
|
||||
assert!(vec.iter().eq(values.iter().copied().map(|opt| opt.unwrap_or(0.0))));
|
||||
|
||||
values.retain(Option::is_some);
|
||||
|
||||
values.sort_by(|a, b| {
|
||||
let (Some(a), Some(b)) = (a, b) else { unreachable!() };
|
||||
|
||||
b.total_cmp(a)
|
||||
});
|
||||
|
||||
assert!(vec.sorted_non_zero_iter().eq(values.into_iter().flatten()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "compact_strains"))]
|
||||
|
||||
@@ -144,19 +144,3 @@ impl<T: fmt::Debug> fmt::Debug for Weak<T> {
|
||||
/// ```
|
||||
#[cfg(not(feature = "sync"))]
|
||||
const fn _share_gradual_taiko() {}
|
||||
|
||||
#[cfg(all(test, feature = "sync"))]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn share_gradual_taiko() {
|
||||
use crate::{taiko::TaikoGradualDifficulty, Beatmap, Difficulty};
|
||||
|
||||
let converted = Beatmap::from_bytes(&[]).unwrap().unchecked_into_converted();
|
||||
let mut gradual = TaikoGradualDifficulty::new(Difficulty::new(), &converted);
|
||||
|
||||
// Arc<RwLock<_>> *can* be shared across threads so this should compile
|
||||
std::thread::spawn(move || {
|
||||
let _ = gradual.next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user