osu: fixed & tested pp
This commit is contained in:
@@ -13,7 +13,7 @@ A standalone crate to calculate star ratings and performance points for all [osu
|
|||||||
- [x] ctb sr
|
- [x] ctb sr
|
||||||
- [x] mania sr
|
- [x] mania sr
|
||||||
---
|
---
|
||||||
- [x] osu pp (need testing!)
|
- [x] osu pp
|
||||||
- [x] taiko pp
|
- [x] taiko pp
|
||||||
- [x] ctb pp
|
- [x] ctb pp
|
||||||
- [x] mania pp
|
- [x] mania pp
|
||||||
|
|||||||
@@ -15,21 +15,3 @@ pub struct DifficultyAttributes {
|
|||||||
pub n_circles: usize,
|
pub n_circles: usize,
|
||||||
pub n_spinners: usize,
|
pub n_spinners: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
const HITWINDOW_OD_MIN: f32 = 80.0;
|
|
||||||
const HITWINDOW_OD_AVG: f32 = 50.0;
|
|
||||||
const HITWINDOW_OD_MAX: f32 = 20.0;
|
|
||||||
|
|
||||||
const HITWINDOW_AR_MIN: f32 = 1800.0;
|
|
||||||
const HITWINDOW_AR_AVG: f32 = 1200.0;
|
|
||||||
const HITWINDOW_AR_MAX: f32 = 450.0;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn difficulty_range_od(od: f32) -> f32 {
|
|
||||||
crate::difficulty_range(od, HITWINDOW_OD_MAX, HITWINDOW_OD_AVG, HITWINDOW_OD_MIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn difficulty_range_ar(ar: f32) -> f32 {
|
|
||||||
crate::difficulty_range(ar, HITWINDOW_AR_MAX, HITWINDOW_AR_AVG, HITWINDOW_AR_MIN)
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-86
@@ -117,14 +117,6 @@ impl<'m> PpCalculator<'m> {
|
|||||||
self.n300.replace(delta / 5);
|
self.n300.replace(delta / 5);
|
||||||
self.n100.replace(delta % 5);
|
self.n100.replace(delta % 5);
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "{} - {} - {} - {}",
|
|
||||||
// n_objects,
|
|
||||||
// self.n300.unwrap(),
|
|
||||||
// self.n100.unwrap(),
|
|
||||||
// self.n_misses
|
|
||||||
// );
|
|
||||||
|
|
||||||
self.n50
|
self.n50
|
||||||
.replace(n_objects - self.n300.unwrap() - self.n100.unwrap() - self.n_misses);
|
.replace(n_objects - self.n300.unwrap() - self.n100.unwrap() - self.n_misses);
|
||||||
}
|
}
|
||||||
@@ -134,11 +126,6 @@ impl<'m> PpCalculator<'m> {
|
|||||||
|
|
||||||
self.acc.replace(acc);
|
self.acc.replace(acc);
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "n300: {:?} | n100: {:?} | n50: {:?} | nMiss: {:?} => {}",
|
|
||||||
// self.n300, self.n100, self.n50, self.n_misses, acc
|
|
||||||
// );
|
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,11 +162,6 @@ impl<'m> PpCalculator<'m> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "n300: {:?} | n100: {:?} | n50: {:?} | nMiss: {:?}",
|
|
||||||
// self.n300, self.n100, self.n50, self.n_misses
|
|
||||||
// );
|
|
||||||
|
|
||||||
let numerator = self.n50.unwrap() + self.n100.unwrap() * 2 + self.n300.unwrap() * 6;
|
let numerator = self.n50.unwrap() + self.n100.unwrap() * 2 + self.n300.unwrap() * 6;
|
||||||
self.acc.replace(numerator as f32 / n_objects as f32 / 6.0);
|
self.acc.replace(numerator as f32 / n_objects as f32 / 6.0);
|
||||||
}
|
}
|
||||||
@@ -200,11 +182,6 @@ impl<'m> PpCalculator<'m> {
|
|||||||
let speed_value = self.compute_speed_value(total_hits as f32);
|
let speed_value = self.compute_speed_value(total_hits as f32);
|
||||||
let acc_value = self.compute_accuracy_value(total_hits);
|
let acc_value = self.compute_accuracy_value(total_hits);
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "aim={} | speed={} | acc={}",
|
|
||||||
// aim_value, speed_value, acc_value
|
|
||||||
// );
|
|
||||||
|
|
||||||
let pp = (aim_value.powf(1.1) + speed_value.powf(1.1) + acc_value.powf(1.1))
|
let pp = (aim_value.powf(1.1) + speed_value.powf(1.1) + acc_value.powf(1.1))
|
||||||
.powf(1.0 / 1.1)
|
.powf(1.0 / 1.1)
|
||||||
* multiplier;
|
* multiplier;
|
||||||
@@ -218,8 +195,6 @@ impl<'m> PpCalculator<'m> {
|
|||||||
fn compute_aim_value(&self, total_hits: f32) -> f32 {
|
fn compute_aim_value(&self, total_hits: f32) -> f32 {
|
||||||
let attributes = self.attributes.as_ref().unwrap();
|
let attributes = self.attributes.as_ref().unwrap();
|
||||||
|
|
||||||
// println!("aim_strain={}", attributes.aim_strain);
|
|
||||||
|
|
||||||
// TD penalty
|
// TD penalty
|
||||||
let raw_aim = if self.mods.td() {
|
let raw_aim = if self.mods.td() {
|
||||||
attributes.aim_strain.powf(0.8)
|
attributes.aim_strain.powf(0.8)
|
||||||
@@ -227,20 +202,14 @@ impl<'m> PpCalculator<'m> {
|
|||||||
attributes.aim_strain
|
attributes.aim_strain
|
||||||
};
|
};
|
||||||
|
|
||||||
// println!("raw={}", raw_aim);
|
|
||||||
|
|
||||||
let mut aim_value = (5.0 * (raw_aim / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
let mut aim_value = (5.0 * (raw_aim / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
||||||
|
|
||||||
// println!("init: {}", aim_value);
|
|
||||||
|
|
||||||
// Longer maps are worth more
|
// Longer maps are worth more
|
||||||
let len_bonus = 0.95
|
let len_bonus = 0.95
|
||||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||||
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
||||||
aim_value *= len_bonus;
|
aim_value *= len_bonus;
|
||||||
|
|
||||||
// println!("len bonus: {} => {}", len_bonus, aim_value);
|
|
||||||
|
|
||||||
// Penalize misses
|
// Penalize misses
|
||||||
if self.n_misses > 0 {
|
if self.n_misses > 0 {
|
||||||
aim_value *= 0.97
|
aim_value *= 0.97
|
||||||
@@ -248,38 +217,25 @@ impl<'m> PpCalculator<'m> {
|
|||||||
.powi(self.n_misses as i32);
|
.powi(self.n_misses as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!("miss penalty: {}", aim_value);
|
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "combo={:?} | max_combo={}",
|
|
||||||
// self.combo, attributes.max_combo
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||||
aim_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
aim_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!("combo scaling: {}", aim_value);
|
|
||||||
|
|
||||||
// AR bonus
|
// AR bonus
|
||||||
let mut ar_factor = 0.0;
|
let mut ar_factor = 0.0;
|
||||||
if attributes.ar > 10.33 {
|
if attributes.ar > 10.33 {
|
||||||
ar_factor += 0.4 * (attributes.ar - 10.33);
|
ar_factor += 0.4 * (attributes.ar - 10.33);
|
||||||
} else if attributes.ar < 8.0 {
|
} else if attributes.ar < 8.0 {
|
||||||
ar_factor += 0.1 * (8.0 - attributes.ar);
|
ar_factor += 0.01 * (8.0 - attributes.ar);
|
||||||
}
|
}
|
||||||
aim_value *= 1.0 + ar_factor.min(ar_factor * total_hits / 1000.0);
|
aim_value *= 1.0 + ar_factor.min(ar_factor * total_hits / 1000.0);
|
||||||
|
|
||||||
// println!("ar bonus: {} => {}", ar_factor, aim_value);
|
|
||||||
|
|
||||||
// HD bonus
|
// HD bonus
|
||||||
if self.mods.hd() {
|
if self.mods.hd() {
|
||||||
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!("hd bonus: {}", aim_value);
|
|
||||||
|
|
||||||
// FL bonus
|
// FL bonus
|
||||||
if self.mods.fl() {
|
if self.mods.fl() {
|
||||||
aim_value *= 1.0
|
aim_value *= 1.0
|
||||||
@@ -288,43 +244,25 @@ impl<'m> PpCalculator<'m> {
|
|||||||
+ (total_hits > 500.0) as u8 as f32 * (total_hits - 500.0) / 1200.0;
|
+ (total_hits > 500.0) as u8 as f32 * (total_hits - 500.0) / 1200.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!("fl bonus: {}", aim_value);
|
|
||||||
|
|
||||||
// Scale with accuracy
|
// Scale with accuracy
|
||||||
aim_value *= 0.5 + self.acc.unwrap() / 2.0;
|
aim_value *= 0.5 + self.acc.unwrap() / 2.0;
|
||||||
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
||||||
|
|
||||||
// println!("> acc: {:?}", self.acc);
|
|
||||||
|
|
||||||
// println!("final: {}", aim_value);
|
|
||||||
|
|
||||||
aim_value
|
aim_value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_speed_value(&self, total_hits: f32) -> f32 {
|
fn compute_speed_value(&self, total_hits: f32) -> f32 {
|
||||||
let attributes = self.attributes.as_ref().unwrap();
|
let attributes = self.attributes.as_ref().unwrap();
|
||||||
|
|
||||||
// println!("speed_strain={}", attributes.speed_strain);
|
|
||||||
|
|
||||||
let mut speed_value =
|
let mut speed_value =
|
||||||
(5.0 * (attributes.speed_strain / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
(5.0 * (attributes.speed_strain / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "curr={} | modified={}",
|
|
||||||
// speed_value,
|
|
||||||
// (5.0 * (2.0994549379474163_f32 / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0
|
|
||||||
// );
|
|
||||||
|
|
||||||
// println!("init: {}", speed_value);
|
|
||||||
|
|
||||||
// Longer maps are worth more
|
// Longer maps are worth more
|
||||||
let len_bonus = 0.95
|
let len_bonus = 0.95
|
||||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||||
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
||||||
speed_value *= len_bonus;
|
speed_value *= len_bonus;
|
||||||
|
|
||||||
// println!("len bonus: {} => {}", len_bonus, speed_value);
|
|
||||||
|
|
||||||
// Penalize misses
|
// Penalize misses
|
||||||
if self.n_misses > 0 {
|
if self.n_misses > 0 {
|
||||||
speed_value *= 0.97
|
speed_value *= 0.97
|
||||||
@@ -332,30 +270,22 @@ impl<'m> PpCalculator<'m> {
|
|||||||
.powf((self.n_misses as f32).powf(0.875));
|
.powf((self.n_misses as f32).powf(0.875));
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!("miss penalty: {}", speed_value);
|
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||||
speed_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
speed_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!("combo scaling: {}", speed_value);
|
|
||||||
|
|
||||||
// AR bonus
|
// AR bonus
|
||||||
if attributes.ar > 10.33 {
|
if attributes.ar > 10.33 {
|
||||||
let ar_factor = 0.4 * (attributes.ar - 10.33);
|
let ar_factor = 0.4 * (attributes.ar - 10.33);
|
||||||
speed_value *= 1.0 + ar_factor.min(ar_factor * total_hits / 1000.0);
|
speed_value *= 1.0 + ar_factor.min(ar_factor * total_hits / 1000.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!("ar bonus: {}", speed_value);
|
|
||||||
|
|
||||||
// HD bonus
|
// HD bonus
|
||||||
if self.mods.hd() {
|
if self.mods.hd() {
|
||||||
speed_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
speed_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!("hidden bonus: {}", speed_value);
|
|
||||||
|
|
||||||
// Scaling the speed value with accuracy and OD
|
// Scaling the speed value with accuracy and OD
|
||||||
let od_factor = 0.95 + attributes.od * attributes.od / 750.0;
|
let od_factor = 0.95 + attributes.od * attributes.od / 750.0;
|
||||||
let acc_factor = self
|
let acc_factor = self
|
||||||
@@ -364,16 +294,12 @@ impl<'m> PpCalculator<'m> {
|
|||||||
.powf((14.5 - attributes.od.max(8.0)) / 2.0);
|
.powf((14.5 - attributes.od.max(8.0)) / 2.0);
|
||||||
speed_value *= od_factor * acc_factor;
|
speed_value *= od_factor * acc_factor;
|
||||||
|
|
||||||
// println!("acc & od scaling: {}", speed_value);
|
|
||||||
|
|
||||||
// Penalize n50s
|
// Penalize n50s
|
||||||
speed_value *= 0.98_f32.powf(
|
speed_value *= 0.98_f32.powf(
|
||||||
(self.n50.unwrap_or(0) as f32 >= total_hits / 500.0) as u8 as f32
|
(self.n50.unwrap_or(0) as f32 >= total_hits / 500.0) as u8 as f32
|
||||||
* (self.n50.unwrap_or(0) as f32 - total_hits / 500.0),
|
* (self.n50.unwrap_or(0) as f32 - total_hits / 500.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// println!("final: {}", speed_value);
|
|
||||||
|
|
||||||
speed_value
|
speed_value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +307,6 @@ impl<'m> PpCalculator<'m> {
|
|||||||
let attributes = self.attributes.as_ref().unwrap();
|
let attributes = self.attributes.as_ref().unwrap();
|
||||||
let n_circles = attributes.n_circles;
|
let n_circles = attributes.n_circles;
|
||||||
|
|
||||||
// println!("n_circles={}", n_circles);
|
|
||||||
|
|
||||||
let better_acc_percentage = (n_circles > 0) as u8 as f32
|
let better_acc_percentage = (n_circles > 0) as u8 as f32
|
||||||
* (((self.n300.unwrap() - (total_hits - n_circles)) * 6
|
* (((self.n300.unwrap() - (total_hits - n_circles)) * 6
|
||||||
+ self.n100.unwrap_or(0) * 2
|
+ self.n100.unwrap_or(0) * 2
|
||||||
@@ -390,19 +314,10 @@ impl<'m> PpCalculator<'m> {
|
|||||||
/ (n_circles * 6) as f32)
|
/ (n_circles * 6) as f32)
|
||||||
.max(0.0);
|
.max(0.0);
|
||||||
|
|
||||||
// println!("better_acc_percentage={}", better_acc_percentage);
|
|
||||||
|
|
||||||
let attributes = self.attributes.as_ref().unwrap();
|
let attributes = self.attributes.as_ref().unwrap();
|
||||||
|
|
||||||
let mut acc_value = 1.52163_f32.powf(attributes.od) * better_acc_percentage.powi(24) * 2.83;
|
let mut acc_value = 1.52163_f32.powf(attributes.od) * better_acc_percentage.powi(24) * 2.83;
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "1.52163^{} * {}^24 * 2.83 = {}",
|
|
||||||
// attributes.od, better_acc_percentage, acc_value
|
|
||||||
// );
|
|
||||||
|
|
||||||
// println!("init: {}", acc_value);
|
|
||||||
|
|
||||||
// Bonus for many hitcircles
|
// Bonus for many hitcircles
|
||||||
acc_value *= ((n_circles as f32 / 1000.0).powf(0.3)).min(1.15);
|
acc_value *= ((n_circles as f32 / 1000.0).powf(0.3)).min(1.15);
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
|||||||
/// Star calculation for osu!standard maps
|
/// Star calculation for osu!standard maps
|
||||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||||
let attributes = map.attributes().mods(mods);
|
let attributes = map.attributes().mods(mods);
|
||||||
|
let hitwindow = super::difficulty_range(attributes.od).floor() / attributes.clock_rate;
|
||||||
|
let od = (80.0 - hitwindow) / 6.0;
|
||||||
|
|
||||||
if map.hit_objects.len() < 2 {
|
if map.hit_objects.len() < 2 {
|
||||||
return DifficultyAttributes {
|
return DifficultyAttributes {
|
||||||
ar: attributes.ar,
|
ar: attributes.ar,
|
||||||
od: attributes.od,
|
od,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -108,7 +110,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
|||||||
DifficultyAttributes {
|
DifficultyAttributes {
|
||||||
stars,
|
stars,
|
||||||
ar: attributes.ar,
|
ar: attributes.ar,
|
||||||
od: attributes.od,
|
od,
|
||||||
speed_strain: speed_rating,
|
speed_strain: speed_rating,
|
||||||
aim_strain: aim_rating,
|
aim_strain: aim_rating,
|
||||||
max_combo: 0, // TODO
|
max_combo: 0, // TODO
|
||||||
@@ -124,93 +126,10 @@ mod tests {
|
|||||||
use crate::Beatmap;
|
use crate::Beatmap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn all_included_single_stars() {
|
|
||||||
// let file = match File::open("E:/Games/osu!/beatmaps/1851299.osu") {
|
|
||||||
// Ok(file) => file,
|
|
||||||
// Err(why) => panic!("Could not open file: {}", why),
|
|
||||||
// };
|
|
||||||
let file = match File::open("C:/Users/Max/Desktop/2578801.osu") {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(why) => panic!("Could not open file: {}", why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let map = match Beatmap::parse(file) {
|
|
||||||
Ok(map) => map,
|
|
||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let stars = stars(&map, 0).stars;
|
|
||||||
|
|
||||||
println!("Stars: {}", stars);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn all_included_stars() {
|
fn all_included_single() {
|
||||||
let margin = 0.005;
|
let file = match File::open("./maps/70090.osu") {
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
// TODO: More mods
|
|
||||||
let data = vec![
|
|
||||||
(1851299, 1 << 8, 4.23514130038547), // HT
|
|
||||||
(1851299, 0, 5.356786475158158), // NM
|
|
||||||
(1851299, 1 << 6, 7.450616908751305), // DT
|
|
||||||
(1851299, 1 << 4, 5.6834681957637665),// HR
|
|
||||||
(1851299, 1 << 1, 4.937817303399699), // EZ
|
|
||||||
|
|
||||||
(70090, 1 << 8, 2.2929922580201803), // HT
|
|
||||||
(70090, 0, 2.8322940761833983), // NM
|
|
||||||
(70090, 1 << 6, 3.8338563325375485), // DT
|
|
||||||
(70090, 1 << 4, 3.0617492228478174), // HR
|
|
||||||
(70090, 1 << 1, 2.698823231324141), // EZ
|
|
||||||
|
|
||||||
(1241370, 1 << 8, 5.662809600985943), // HT
|
|
||||||
(1241370, 0, 7.0367002127481975), // NM
|
|
||||||
(1241370, 1 << 6, 11.144720506574934),// DT
|
|
||||||
(1241370, 1 << 4, 7.641688110458715), // HR
|
|
||||||
(1241370, 1 << 1, 6.316288616688052), // EZ
|
|
||||||
|
|
||||||
// Slider fiesta
|
|
||||||
// (1657535, 1 << 8, 4.1727975286379895),// HT
|
|
||||||
// (1657535, 0, 5.16048239944917), // NM
|
|
||||||
// (1657535, 1 << 6, 7.125936779100417), // DT
|
|
||||||
// (1657535, 1 << 4, 5.545877027713307), // HR
|
|
||||||
// (1657535, 1 << 1, 4.66015083361088), // EZ
|
|
||||||
];
|
|
||||||
|
|
||||||
for (map_id, mods, expected_stars) in data {
|
|
||||||
let file = match File::open(format!("./test/{}.osu", map_id)) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let map = match Beatmap::parse(file) {
|
|
||||||
Ok(map) => map,
|
|
||||||
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let stars = stars(&map, mods).stars;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
(stars - expected_stars).abs() < margin,
|
|
||||||
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
|
|
||||||
stars,
|
|
||||||
expected_stars,
|
|
||||||
(stars - expected_stars).abs(),
|
|
||||||
map_id,
|
|
||||||
mods
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn all_included_single_pp() {
|
|
||||||
// let file = match File::open("E:/Games/osu!/beatmaps/1851299.osu") {
|
|
||||||
// Ok(file) => file,
|
|
||||||
// Err(why) => panic!("Could not open file: {}", why),
|
|
||||||
// };
|
|
||||||
let file = match File::open("C:/Users/Max/Desktop/2578801.osu") {
|
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(why) => panic!("Could not open file: {}", why),
|
Err(why) => panic!("Could not open file: {}", why),
|
||||||
};
|
};
|
||||||
@@ -220,9 +139,9 @@ mod tests {
|
|||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
Err(why) => panic!("Error while parsing map: {}", why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let calculator = PpCalculator::new(&map).mods(0);
|
let result = PpCalculator::new(&map).mods(64).calculate(stars);
|
||||||
let result = calculator.calculate(stars);
|
|
||||||
|
|
||||||
|
println!("Stars: {}", result.attributes.stars);
|
||||||
println!("PP: {}", result.pp);
|
println!("PP: {}", result.pp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
pub mod all_included;
|
pub mod all_included;
|
||||||
pub mod no_leniency;
|
pub mod no_leniency;
|
||||||
pub mod no_sliders_no_leniency;
|
pub mod no_sliders_no_leniency;
|
||||||
|
|
||||||
|
const OSU_OD_MAX: f32 = 20.0;
|
||||||
|
const OSU_OD_AVG: f32 = 50.0;
|
||||||
|
const OSU_OD_MIN: f32 = 80.0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn difficulty_range(od: f32) -> f32 {
|
||||||
|
super::super::difficulty_range(od, OSU_OD_MAX, OSU_OD_AVG, OSU_OD_MIN)
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ impl<'h> DifficultyObject<'h> {
|
|||||||
let jump_dist = if base.is_spinner() {
|
let jump_dist = if base.is_spinner() {
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
(pos * scaling_factor - prev_cursor_pos * scaling_factor).length()
|
((pos - prev_cursor_pos) * scaling_factor).length()
|
||||||
};
|
};
|
||||||
|
|
||||||
let angle = prev_prev.map(|prev_prev| {
|
let angle = prev_prev.map(|prev_prev| {
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ const NORMALIZED_RADIUS: f32 = 52.0;
|
|||||||
/// processing stack leniency is relatively expensive.
|
/// processing stack leniency is relatively expensive.
|
||||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||||
let map_attributes = map.attributes().mods(mods);
|
let map_attributes = map.attributes().mods(mods);
|
||||||
|
let hitwindow = super::difficulty_range(map_attributes.od).floor() / map_attributes.clock_rate;
|
||||||
|
let od = (80.0 - hitwindow) / 6.0;
|
||||||
|
|
||||||
let mut diff_attributes = DifficultyAttributes {
|
let mut diff_attributes = DifficultyAttributes {
|
||||||
ar: map_attributes.ar,
|
ar: map_attributes.ar,
|
||||||
od: map_attributes.od,
|
od,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,109 +150,18 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn no_leniency_single_stars() {
|
fn no_leniency_single() {
|
||||||
let file = match File::open("./test/70090.osu") {
|
let file = match File::open("./maps/1851299.osu") {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(why) => panic!("Could not open file: {}", why),
|
Err(why) => panic!("Could not open file: {}", why),
|
||||||
};
|
};
|
||||||
// let file = match File::open("C:/Users/Max/Desktop/2578801.osu") {
|
|
||||||
// Ok(file) => file,
|
|
||||||
// Err(why) => panic!("Could not open file: {}", why),
|
|
||||||
// };
|
|
||||||
|
|
||||||
let map = match Beatmap::parse(file) {
|
let map = match Beatmap::parse(file) {
|
||||||
Ok(map) => map,
|
Ok(map) => map,
|
||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
Err(why) => panic!("Error while parsing map: {}", why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let stars = stars(&map, 0).stars;
|
let result = PpCalculator::new(&map).mods(256).calculate(stars);
|
||||||
|
|
||||||
println!("Stars: {}", stars);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn no_leniency_stars() {
|
|
||||||
let margin = 0.005;
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
// TODO: More mods
|
|
||||||
let data = vec![
|
|
||||||
(1851299, 1 << 8, 4.23514130038547), // HT
|
|
||||||
(1851299, 0, 5.356786475158158), // NM
|
|
||||||
(1851299, 1 << 6, 7.450616908751305), // DT
|
|
||||||
(1851299, 1 << 4, 5.6834681957637665),// HR
|
|
||||||
(1851299, 1 << 1, 4.937817303399699), // EZ
|
|
||||||
|
|
||||||
(70090, 1 << 8, 2.2929922580201803), // HT
|
|
||||||
(70090, 0, 2.8322940761833983), // NM
|
|
||||||
(70090, 1 << 6, 3.8338563325375485), // DT
|
|
||||||
(70090, 1 << 4, 3.0617492228478174), // HR
|
|
||||||
(70090, 1 << 1, 2.698823231324141), // EZ
|
|
||||||
|
|
||||||
(1241370, 1 << 8, 5.662809600985943), // HT
|
|
||||||
(1241370, 0, 7.0367002127481975), // NM
|
|
||||||
(1241370, 1 << 6, 11.144720506574934),// DT
|
|
||||||
(1241370, 1 << 4, 7.641688110458715), // HR
|
|
||||||
(1241370, 1 << 1, 6.316288616688052), // EZ
|
|
||||||
|
|
||||||
// Slider fiesta
|
|
||||||
// (1657535, 1 << 8, 4.1727975286379895),// HT
|
|
||||||
// (1657535, 0, 5.16048239944917), // NM
|
|
||||||
// (1657535, 1 << 6, 7.125936779100417), // DT
|
|
||||||
// (1657535, 1 << 4, 5.545877027713307), // HR
|
|
||||||
// (1657535, 1 << 1, 4.66015083361088), // EZ
|
|
||||||
];
|
|
||||||
|
|
||||||
for (map_id, mods, expected_stars) in data {
|
|
||||||
let file = match File::open(format!("./test/{}.osu", map_id)) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let map = match Beatmap::parse(file) {
|
|
||||||
Ok(map) => map,
|
|
||||||
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let stars = stars(&map, mods).stars;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
(stars - expected_stars).abs() < margin,
|
|
||||||
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
|
|
||||||
stars,
|
|
||||||
expected_stars,
|
|
||||||
(stars - expected_stars).abs(),
|
|
||||||
map_id,
|
|
||||||
mods
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_leniency_single_pp() {
|
|
||||||
let file = match File::open("E:/Games/osu!/beatmaps/1241370.osu") {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(why) => panic!("Could not open file: {}", why),
|
|
||||||
};
|
|
||||||
// let file = match File::open("C:/Users/Max/Desktop/2578801.osu") {
|
|
||||||
// Ok(file) => file,
|
|
||||||
// Err(why) => panic!("Could not open file: {}", why),
|
|
||||||
// };
|
|
||||||
|
|
||||||
let map = match Beatmap::parse(file) {
|
|
||||||
Ok(map) => map,
|
|
||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let calculator = PpCalculator::new(&map)
|
|
||||||
// .misses(2)
|
|
||||||
// .accuracy(96.78)
|
|
||||||
// .combo(1876)
|
|
||||||
// .n100(0)
|
|
||||||
.mods(8 + 16);
|
|
||||||
|
|
||||||
let result = calculator.calculate(stars);
|
|
||||||
|
|
||||||
println!("Stars: {}", result.attributes.stars);
|
println!("Stars: {}", result.attributes.stars);
|
||||||
println!("PP: {}", result.pp);
|
println!("PP: {}", result.pp);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl<'h> DifficultyObject<'h> {
|
|||||||
let jump_dist = if base.is_spinner() {
|
let jump_dist = if base.is_spinner() {
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
(base.pos * scaling_factor - prev.pos * scaling_factor).length()
|
((base.pos - prev.pos) * scaling_factor).length()
|
||||||
};
|
};
|
||||||
|
|
||||||
let angle = prev_prev.map(|prev_prev| {
|
let angle = prev_prev.map(|prev_prev| {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::super::{difficulty_range_ar, difficulty_range_od, DifficultyAttributes};
|
use super::super::DifficultyAttributes;
|
||||||
|
|
||||||
mod control_point_iter;
|
mod control_point_iter;
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
@@ -27,11 +27,13 @@ const NORMALIZED_RADIUS: f32 = 52.0;
|
|||||||
/// However, this is the most efficient one.
|
/// However, this is the most efficient one.
|
||||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||||
let attributes = map.attributes().mods(mods);
|
let attributes = map.attributes().mods(mods);
|
||||||
|
let hitwindow = super::difficulty_range(attributes.od).floor() / attributes.clock_rate;
|
||||||
|
let od = (80.0 - hitwindow) / 6.0;
|
||||||
|
|
||||||
if map.hit_objects.len() < 2 {
|
if map.hit_objects.len() < 2 {
|
||||||
return DifficultyAttributes {
|
return DifficultyAttributes {
|
||||||
ar: attributes.ar,
|
ar: attributes.ar,
|
||||||
od: attributes.od,
|
od,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -147,19 +149,9 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
|||||||
|
|
||||||
let stars = aim_strain + speed_strain + (aim_strain - speed_strain).abs() / 2.0;
|
let stars = aim_strain + speed_strain + (aim_strain - speed_strain).abs() / 2.0;
|
||||||
|
|
||||||
let hit_window_od = difficulty_range_od(attributes.od) as i32 as f32 / attributes.clock_rate;
|
|
||||||
let hit_window_ar = difficulty_range_ar(attributes.ar) as i32 as f32 / attributes.clock_rate;
|
|
||||||
|
|
||||||
let od = (80.0 - hit_window_od) / 6.0;
|
|
||||||
let ar = if hit_window_ar > 1200.0 {
|
|
||||||
(1800.0 - hit_window_ar) / 120.0
|
|
||||||
} else {
|
|
||||||
(1200.0 - hit_window_ar) / 150.0 + 5.0
|
|
||||||
};
|
|
||||||
|
|
||||||
DifficultyAttributes {
|
DifficultyAttributes {
|
||||||
stars,
|
stars,
|
||||||
ar,
|
ar: attributes.ar,
|
||||||
od,
|
od,
|
||||||
speed_strain,
|
speed_strain,
|
||||||
aim_strain,
|
aim_strain,
|
||||||
@@ -178,110 +170,18 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn no_sliders_single_stars() {
|
fn no_sliders_single() {
|
||||||
let file = match File::open("E:/Games/osu!/beatmaps/70090.osu") {
|
let file = match File::open("./maps/1851299.osu") {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(why) => panic!("Could not open file: {}", why),
|
Err(why) => panic!("Could not open file: {}", why),
|
||||||
};
|
};
|
||||||
// let file = match File::open("C:/Users/Max/Desktop/1241370.osu") {
|
|
||||||
// Ok(file) => file,
|
|
||||||
// Err(why) => panic!("Could not open file: {}", why),
|
|
||||||
// };
|
|
||||||
|
|
||||||
let map = match Beatmap::parse(file) {
|
let map = match Beatmap::parse(file) {
|
||||||
Ok(map) => map,
|
Ok(map) => map,
|
||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
Err(why) => panic!("Error while parsing map: {}", why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let stars = stars(&map, 1024 + 8 + 64 + 16).stars;
|
let result = PpCalculator::new(&map).mods(2).calculate(stars);
|
||||||
|
|
||||||
println!("Stars: {}", stars);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn no_sliders_stars() {
|
|
||||||
let margin = 0.5;
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
// TODO: More mods
|
|
||||||
let data = vec![
|
|
||||||
(1851299, 1 << 8, 4.23514130038547), // HT
|
|
||||||
(1851299, 0, 5.356786475158158), // NM
|
|
||||||
(1851299, 1 << 6, 7.450616908751305), // DT
|
|
||||||
(1851299, 1 << 4, 5.6834681957637665),// HR
|
|
||||||
(1851299, 1 << 1, 4.937817303399699), // EZ
|
|
||||||
|
|
||||||
(70090, 1 << 8, 2.2929922580201803), // HT
|
|
||||||
(70090, 0, 2.8322940761833983), // NM
|
|
||||||
(70090, 1 << 6, 3.8338563325375485), // DT
|
|
||||||
(70090, 1 << 4, 3.0617492228478174), // HR
|
|
||||||
(70090, 1 << 1, 2.698823231324141), // EZ
|
|
||||||
|
|
||||||
(1241370, 1 << 8, 5.662809600985943), // HT
|
|
||||||
(1241370, 0, 7.0367002127481975), // NM
|
|
||||||
(1241370, 1 << 6, 11.144720506574934),// DT
|
|
||||||
(1241370, 1 << 4, 7.641688110458715), // HR
|
|
||||||
(1241370, 1 << 1, 6.316288616688052), // EZ
|
|
||||||
|
|
||||||
// Slider fiesta
|
|
||||||
// (1657535, 1 << 8, 4.1727975286379895),// HT
|
|
||||||
// (1657535, 0, 5.16048239944917), // NM
|
|
||||||
// (1657535, 1 << 6, 7.125936779100417), // DT
|
|
||||||
// (1657535, 1 << 4, 5.545877027713307), // HR
|
|
||||||
// (1657535, 1 << 1, 4.66015083361088), // EZ
|
|
||||||
];
|
|
||||||
|
|
||||||
for (map_id, mods, expected_stars) in data {
|
|
||||||
let file = match File::open(format!("./test/{}.osu", map_id)) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let map = match Beatmap::parse(file) {
|
|
||||||
Ok(map) => map,
|
|
||||||
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let stars = stars(&map, mods).stars;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
(stars - expected_stars).abs() < margin,
|
|
||||||
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
|
|
||||||
stars,
|
|
||||||
expected_stars,
|
|
||||||
(stars - expected_stars).abs(),
|
|
||||||
map_id,
|
|
||||||
mods
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn no_sliders_single_pp() {
|
|
||||||
let file = match File::open("E:/Games/osu!/beatmaps/1241370.osu") {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(why) => panic!("Could not open file: {}", why),
|
|
||||||
};
|
|
||||||
// let file = match File::open("C:/Users/Max/Desktop/1851299_base.osu") {
|
|
||||||
// Ok(file) => file,
|
|
||||||
// Err(why) => panic!("Could not open file: {}", why),
|
|
||||||
// };
|
|
||||||
|
|
||||||
let map = match Beatmap::parse(file) {
|
|
||||||
Ok(map) => map,
|
|
||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let calculator = PpCalculator::new(&map)
|
|
||||||
.misses(2)
|
|
||||||
.accuracy(96.78)
|
|
||||||
.combo(1876)
|
|
||||||
// .n100(0)
|
|
||||||
.mods(8 + 16);
|
|
||||||
|
|
||||||
let result = calculator.calculate(stars);
|
|
||||||
|
|
||||||
println!("Stars: {}", result.attributes.stars);
|
println!("Stars: {}", result.attributes.stars);
|
||||||
println!("PP: {}", result.pp);
|
println!("PP: {}", result.pp);
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ impl BeatmapAttributes {
|
|||||||
const AR_MS_STEP_1: f32 = (Self::AR0_MS - Self::AR5_MS) / 5.0;
|
const AR_MS_STEP_1: f32 = (Self::AR0_MS - Self::AR5_MS) / 5.0;
|
||||||
const AR_MS_STEP_2: f32 = (Self::AR5_MS - Self::AR10_MS) / 5.0;
|
const AR_MS_STEP_2: f32 = (Self::AR5_MS - Self::AR10_MS) / 5.0;
|
||||||
|
|
||||||
const OD0_MS: f32 = 80.0;
|
|
||||||
const OD10_MS: f32 = 20.0;
|
|
||||||
const OD_MS_STEP: f32 = (Self::OD0_MS - Self::OD10_MS) / 10.0;
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn new(ar: f32, od: f32, cs: f32, hp: f32) -> Self {
|
pub(crate) fn new(ar: f32, od: f32, cs: f32, hp: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -57,11 +53,7 @@ impl BeatmapAttributes {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// OD
|
// OD
|
||||||
let mut od = self.od * multiplier;
|
let od = (self.od * multiplier).min(10.0);
|
||||||
let mut od_ms = Self::OD0_MS - (Self::OD_MS_STEP * od).ceil();
|
|
||||||
od_ms = od_ms.max(Self::OD10_MS).min(Self::OD0_MS);
|
|
||||||
od_ms /= clock_rate;
|
|
||||||
od = (Self::OD0_MS - od_ms) / Self::OD_MS_STEP;
|
|
||||||
|
|
||||||
// CS
|
// CS
|
||||||
let mut cs = self.cs;
|
let mut cs = self.cs;
|
||||||
|
|||||||
+24
-16
@@ -12,8 +12,8 @@ struct MapResult {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fruits() {
|
fn fruits() {
|
||||||
let star_margin = 0.005;
|
let star_margin = 0.0001;
|
||||||
let pp_margin = 0.015;
|
let pp_margin = 0.0001;
|
||||||
|
|
||||||
for result in RESULTS {
|
for result in RESULTS {
|
||||||
let MapResult {
|
let MapResult {
|
||||||
@@ -38,23 +38,31 @@ fn fruits() {
|
|||||||
.calculate();
|
.calculate();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(result.stars - stars).abs() < star_margin,
|
(result.stars - stars).abs() < star_margin * stars,
|
||||||
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
|
"\nStars:\n\
|
||||||
result.stars,
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
stars,
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
(result.stars - stars).abs(),
|
[map {map} | mods {mods}]\n",
|
||||||
map_id,
|
calculated = result.stars,
|
||||||
mods
|
expected = stars,
|
||||||
|
margin = (result.stars - stars).abs(),
|
||||||
|
allowed = star_margin * stars,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(result.pp - pp).abs() < pp_margin,
|
(result.pp - pp).abs() < pp_margin * pp,
|
||||||
"PP: {} | Expected: {} => {} margin [map {} | mods {}]",
|
"\nPP:\n\
|
||||||
result.pp,
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
pp,
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
(result.pp - pp).abs(),
|
[map {map} | mods {mods}]\n",
|
||||||
map_id,
|
calculated = result.pp,
|
||||||
mods
|
expected = pp,
|
||||||
|
margin = (result.pp - pp).abs(),
|
||||||
|
allowed = pp_margin * pp,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-16
@@ -12,8 +12,8 @@ struct MapResult {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mania() {
|
fn mania() {
|
||||||
let star_margin = 0.001;
|
let star_margin = 0.00001;
|
||||||
let pp_margin = 0.001;
|
let pp_margin = 0.00001;
|
||||||
|
|
||||||
for result in RESULTS {
|
for result in RESULTS {
|
||||||
let MapResult {
|
let MapResult {
|
||||||
@@ -38,23 +38,31 @@ fn mania() {
|
|||||||
.calculate();
|
.calculate();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(result.stars - stars).abs() < star_margin,
|
(result.stars - stars).abs() < star_margin * stars,
|
||||||
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
|
"\nStars:\n\
|
||||||
result.stars,
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
stars,
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
(result.stars - stars).abs(),
|
[map {map} | mods {mods}]\n",
|
||||||
map_id,
|
calculated = result.stars,
|
||||||
mods
|
expected = stars,
|
||||||
|
margin = (result.stars - stars).abs(),
|
||||||
|
allowed = star_margin * stars,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(result.pp - pp).abs() < pp_margin,
|
(result.pp - pp).abs() < pp_margin * pp,
|
||||||
"PP: {} | Expected: {} => {} margin [map {} | mods {}]",
|
"\nPP:\n\
|
||||||
result.pp,
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
pp,
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
(result.pp - pp).abs(),
|
[map {map} | mods {mods}]\n",
|
||||||
map_id,
|
calculated = result.pp,
|
||||||
mods
|
expected = pp,
|
||||||
|
margin = (result.pp - pp).abs(),
|
||||||
|
allowed = pp_margin * pp,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+238
-1
@@ -1 +1,238 @@
|
|||||||
// TODO
|
extern crate rosu_pp;
|
||||||
|
|
||||||
|
use rosu_pp::{osu, Beatmap};
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
struct MapResult {
|
||||||
|
map_id: u32,
|
||||||
|
mods: u32,
|
||||||
|
stars: f32,
|
||||||
|
pp: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osu_no_sliders() {
|
||||||
|
let star_margin = 0.0075;
|
||||||
|
let pp_margin = 0.0075;
|
||||||
|
|
||||||
|
for result in RESULTS {
|
||||||
|
let MapResult {
|
||||||
|
map_id,
|
||||||
|
mods,
|
||||||
|
stars,
|
||||||
|
pp,
|
||||||
|
} = result;
|
||||||
|
|
||||||
|
let file = match File::open(format!("./maps/{}.osu", map_id)) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
|
||||||
|
};
|
||||||
|
|
||||||
|
let map = match Beatmap::parse(file) {
|
||||||
|
Ok(map) => map,
|
||||||
|
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = rosu_pp::osu::PpCalculator::new(&map)
|
||||||
|
.mods(*mods)
|
||||||
|
.calculate(osu::no_sliders_no_leniency::stars);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(result.attributes.stars - stars).abs() < star_margin * stars,
|
||||||
|
"\nStars:\n\
|
||||||
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
|
[map {map} | mods {mods}]\n",
|
||||||
|
calculated = result.attributes.stars,
|
||||||
|
expected = stars,
|
||||||
|
margin = (result.attributes.stars - stars).abs(),
|
||||||
|
allowed = star_margin * stars,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(result.pp - pp).abs() < pp_margin * pp,
|
||||||
|
"\nPP:\n\
|
||||||
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
|
[map {map} | mods {mods}]\n",
|
||||||
|
calculated = result.pp,
|
||||||
|
expected = pp,
|
||||||
|
margin = (result.pp - pp).abs(),
|
||||||
|
allowed = pp_margin * pp,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osu_no_leniency() {
|
||||||
|
let star_margin = 0.0025;
|
||||||
|
let pp_margin = 0.0025;
|
||||||
|
|
||||||
|
for result in RESULTS {
|
||||||
|
let MapResult {
|
||||||
|
map_id,
|
||||||
|
mods,
|
||||||
|
stars,
|
||||||
|
pp,
|
||||||
|
} = result;
|
||||||
|
|
||||||
|
let file = match File::open(format!("./maps/{}.osu", map_id)) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
|
||||||
|
};
|
||||||
|
|
||||||
|
let map = match Beatmap::parse(file) {
|
||||||
|
Ok(map) => map,
|
||||||
|
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = rosu_pp::osu::PpCalculator::new(&map)
|
||||||
|
.mods(*mods)
|
||||||
|
.calculate(osu::no_leniency::stars);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(result.attributes.stars - stars).abs() < star_margin * stars,
|
||||||
|
"\nStars:\n\
|
||||||
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
|
[map {map} | mods {mods}]\n",
|
||||||
|
calculated = result.attributes.stars,
|
||||||
|
expected = stars,
|
||||||
|
margin = (result.attributes.stars - stars).abs(),
|
||||||
|
allowed = star_margin * stars,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(result.pp - pp).abs() < pp_margin * pp,
|
||||||
|
"\nPP:\n\
|
||||||
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
|
[map {map} | mods {mods}]\n",
|
||||||
|
calculated = result.pp,
|
||||||
|
expected = pp,
|
||||||
|
margin = (result.pp - pp).abs(),
|
||||||
|
allowed = pp_margin * pp,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RESULTS: &[MapResult] = &[
|
||||||
|
MapResult {
|
||||||
|
map_id: 1851299,
|
||||||
|
mods: 256,
|
||||||
|
stars: 4.23514130038547,
|
||||||
|
pp: 96.59532540603362,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1851299,
|
||||||
|
mods: 0,
|
||||||
|
stars: 5.356786475158158,
|
||||||
|
pp: 191.72186087468594,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1851299,
|
||||||
|
mods: 8,
|
||||||
|
stars: 5.356786475158158,
|
||||||
|
pp: 211.17333597663404,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1851299,
|
||||||
|
mods: 64,
|
||||||
|
stars: 7.450616908751305,
|
||||||
|
pp: 476.39199443787675,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1851299,
|
||||||
|
mods: 16,
|
||||||
|
stars: 5.6834681957637665,
|
||||||
|
pp: 243.32730989490153,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1851299,
|
||||||
|
mods: 2,
|
||||||
|
stars: 4.937817303399699,
|
||||||
|
pp: 110.44350158714633,
|
||||||
|
},
|
||||||
|
// -----
|
||||||
|
MapResult {
|
||||||
|
map_id: 70090,
|
||||||
|
mods: 256,
|
||||||
|
stars: 2.2929922580201803,
|
||||||
|
pp: 17.530571082228978,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 70090,
|
||||||
|
mods: 0,
|
||||||
|
stars: 2.8322940761833983,
|
||||||
|
pp: 40.611760049886534,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 70090,
|
||||||
|
mods: 8,
|
||||||
|
stars: 2.8322940761833983,
|
||||||
|
pp: 46.252172598153074,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 70090,
|
||||||
|
mods: 64,
|
||||||
|
stars: 3.8338563325375485,
|
||||||
|
pp: 110.32389105793393,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 70090,
|
||||||
|
mods: 16,
|
||||||
|
stars: 3.0617492228478174,
|
||||||
|
pp: 84.67846960014381,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 70090,
|
||||||
|
mods: 2,
|
||||||
|
stars: 2.698823231324141,
|
||||||
|
pp: 21.861918597227252,
|
||||||
|
},
|
||||||
|
// -----
|
||||||
|
MapResult {
|
||||||
|
map_id: 1241370,
|
||||||
|
mods: 256,
|
||||||
|
stars: 5.662809600985943,
|
||||||
|
pp: 346.1069865511771,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1241370,
|
||||||
|
mods: 0,
|
||||||
|
stars: 7.0367002127481975,
|
||||||
|
pp: 658.4944314112954,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1241370,
|
||||||
|
mods: 8,
|
||||||
|
stars: 7.0367002127481975,
|
||||||
|
pp: 720.8614284211293,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1241370,
|
||||||
|
mods: 64,
|
||||||
|
stars: 11.144720506574934,
|
||||||
|
pp: 2414.665180655108,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1241370,
|
||||||
|
mods: 16,
|
||||||
|
stars: 7.641688110458715,
|
||||||
|
pp: 853.7411405318841,
|
||||||
|
},
|
||||||
|
MapResult {
|
||||||
|
map_id: 1241370,
|
||||||
|
mods: 2,
|
||||||
|
stars: 6.316288616688052,
|
||||||
|
pp: 357.3089261481221,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
+24
-16
@@ -12,8 +12,8 @@ struct MapResult {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn taiko() {
|
fn taiko() {
|
||||||
let star_margin = 0.001;
|
let star_margin = 0.0001;
|
||||||
let pp_margin = 0.0075;
|
let pp_margin = 0.0001;
|
||||||
|
|
||||||
for result in RESULTS {
|
for result in RESULTS {
|
||||||
let MapResult {
|
let MapResult {
|
||||||
@@ -38,23 +38,31 @@ fn taiko() {
|
|||||||
.calculate();
|
.calculate();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(result.stars - stars).abs() < star_margin,
|
(result.stars - stars).abs() < star_margin * stars,
|
||||||
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
|
"\nStars:\n\
|
||||||
result.stars,
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
stars,
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
(result.stars - stars).abs(),
|
[map {map} | mods {mods}]\n",
|
||||||
map_id,
|
calculated = result.stars,
|
||||||
mods
|
expected = stars,
|
||||||
|
margin = (result.stars - stars).abs(),
|
||||||
|
allowed = star_margin * stars,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(result.pp - pp).abs() < pp_margin,
|
(result.pp - pp).abs() < pp_margin * pp,
|
||||||
"PP: {} | Expected: {} => {} margin [map {} | mods {}]",
|
"\nPP:\n\
|
||||||
result.pp,
|
Calculated: {calculated} | Expected: {expected}\n \
|
||||||
pp,
|
=> {margin} margin ({allowed} allowed)\n\
|
||||||
(result.pp - pp).abs(),
|
[map {map} | mods {mods}]\n",
|
||||||
map_id,
|
calculated = result.pp,
|
||||||
mods
|
expected = pp,
|
||||||
|
margin = (result.pp - pp).abs(),
|
||||||
|
allowed = pp_margin * pp,
|
||||||
|
map = map_id,
|
||||||
|
mods = mods
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user