osu: added pp computation

This commit is contained in:
MaxOhn
2021-01-09 17:24:00 +01:00
parent 3c51dc3ba5
commit 2d975ff21c
4 changed files with 518 additions and 8 deletions
+152
View File
@@ -151,6 +151,158 @@ fn norm(p: f32, a: f32, b: f32, c: f32) -> f32 {
(a.powf(p) + b.powf(p) + c.powf(p)).powf(p.recip())
}
pub struct PpResult {
pub pp: f32,
pub stars: f32,
}
pub trait PpProvider {
fn pp(&self) -> PpCalculator;
}
impl PpProvider for Beatmap {
#[inline]
fn pp(&self) -> PpCalculator {
PpCalculator::new(self)
}
}
// TODO: Allow partial plays
pub struct PpCalculator<'m> {
map: &'m Beatmap,
stars: Option<f32>,
mods: u32,
max_combo: usize,
combo: Option<usize>,
acc: f32,
n_misses: usize,
}
impl<'m> PpCalculator<'m> {
#[inline]
pub fn new(map: &'m Beatmap) -> Self {
let max_combo = map.hit_objects.iter().filter(|h| h.is_circle()).count();
Self {
map,
stars: None,
mods: 0,
max_combo,
combo: None,
acc: 100.0,
n_misses: 0,
}
}
#[inline]
pub fn stars(mut self, stars: f32) -> Self {
self.stars.replace(stars);
self
}
#[inline]
pub fn mods(mut self, mods: u32) -> Self {
self.mods = mods;
self
}
#[inline]
pub fn combo(mut self, combo: usize) -> Self {
self.combo.replace(combo);
self
}
#[inline]
pub fn misses(mut self, n_misses: usize) -> Self {
self.n_misses = n_misses;
self
}
/// Set the accuracy between 0.0 and 100.0;
#[inline]
pub fn accuracy(mut self, acc: f32) -> Self {
self.acc = acc / 100.0;
self
}
pub fn calculate(self) -> PpResult {
let stars = self.stars.unwrap_or_else(|| stars(self.map, self.mods));
let mut multiplier = 1.1;
if self.mods.nf() {
multiplier *= 0.9;
}
if self.mods.hd() {
multiplier *= 1.1;
}
// TODO: Consider HR & co?
let hit_window = difficulty_range(self.map.od) as i32 as f32 / self.mods.speed();
let strain_value = self.compute_strain_value(stars);
let acc_value = self.compute_accuracy_value(hit_window);
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
PpResult { stars, pp }
}
fn compute_strain_value(&self, stars: f32) -> f32 {
let exp_base = 5.0 * (stars / 0.0075).max(1.0) - 4.0;
let mut strain = exp_base * exp_base / 100_000.0;
// Longer maps are worth more
let len_bonus = 1.0 + 0.1 * (self.max_combo as f32 / 1500.0).min(1.0);
strain *= len_bonus;
// Penalize misses exponentially
strain *= 0.985_f32.powi(self.n_misses as i32);
// HD bonus
if self.mods.hd() {
strain *= 1.025;
}
// FL bonus
if self.mods.fl() {
strain *= 1.05 * len_bonus;
}
// Scale with accuracy
strain * self.acc
}
#[inline]
fn compute_accuracy_value(&self, hit_window: f32) -> f32 {
(150.0 / hit_window).powf(1.1)
* self.acc.powi(15)
* 22.0
* (self.max_combo as f32 / 1500.0).powf(0.3).min(1.15)
}
}
const HITWINDOW_MIN: f32 = 50.0;
const HITWINDOW_AVG: f32 = 35.0;
const HITWINDOW_MAX: f32 = 20.0;
#[inline]
fn difficulty_range(od: f32) -> f32 {
if od > 5.0 {
HITWINDOW_AVG + (HITWINDOW_MAX - HITWINDOW_AVG) * (od - 5.0) / 5.0
} else if od < 5.0 {
HITWINDOW_AVG - (HITWINDOW_AVG - HITWINDOW_MIN) * (5.0 - od) / 5.0
} else {
HITWINDOW_AVG
}
}
#[cfg(test)]
mod tests {
use super::*;