pub mod bpm;
pub mod hash;
pub mod nps;
pub mod pattern;
pub mod pattern_recognition;
pub use bpm::{bpm_max, bpm_min, bpm_mode};
pub use hash::{hash, notes_hash, timings_hash};
pub use nps::{density, highest_drain_time, highest_nps, lowest_nps, nps};
pub use pattern::{lane_balance, polyphony};
pub use pattern_recognition::analyze as pattern_analysis;
use crate::model::RoxChart;
use std::collections::HashMap;
pub trait RoxAnalysis {
fn bpm_min(&self) -> f64;
fn bpm_max(&self) -> f64;
fn bpm_mode(&self) -> f64;
fn nps(&self) -> f64;
fn density(&self, segments: usize) -> Vec<f64>;
fn highest_nps(&self, window_size_s: f64) -> f64;
fn lowest_nps(&self, window_size_s: f64) -> f64;
fn highest_drain_time(&self) -> f64;
fn polyphony(&self) -> HashMap<u32, u32>;
fn lane_balance(&self) -> Vec<u32>;
fn hash(&self) -> String;
fn notes_hash(&self) -> String;
fn timings_hash(&self) -> String;
fn short_hash(&self) -> String;
fn pattern_analysis(&self) -> pattern_recognition::AnalysisResult;
}
impl RoxAnalysis for RoxChart {
fn bpm_min(&self) -> f64 {
bpm::bpm_min(self)
}
fn bpm_max(&self) -> f64 {
bpm::bpm_max(self)
}
fn bpm_mode(&self) -> f64 {
bpm::bpm_mode(self)
}
fn nps(&self) -> f64 {
nps::nps(self)
}
fn density(&self, segments: usize) -> Vec<f64> {
nps::density(self, segments)
}
fn highest_nps(&self, window_size_s: f64) -> f64 {
nps::highest_nps(self, window_size_s)
}
fn lowest_nps(&self, window_size_s: f64) -> f64 {
nps::lowest_nps(self, window_size_s)
}
fn highest_drain_time(&self) -> f64 {
nps::highest_drain_time(self)
}
fn polyphony(&self) -> HashMap<u32, u32> {
pattern::polyphony(self)
}
fn lane_balance(&self) -> Vec<u32> {
pattern::lane_balance(self)
}
fn hash(&self) -> String {
hash::hash(self)
}
fn notes_hash(&self) -> String {
hash::notes_hash(self)
}
fn timings_hash(&self) -> String {
hash::timings_hash(self)
}
fn short_hash(&self) -> String {
let h = self.hash();
if h.len() >= 16 {
h[..16].to_string()
} else {
h
}
}
fn pattern_analysis(&self) -> pattern_recognition::AnalysisResult {
pattern_recognition::analyze(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Note, RoxChart, TimingPoint};
#[test]
fn test_bpm_stats() {
let mut chart = RoxChart::new(4);
chart.timing_points.push(TimingPoint::bpm(0, 100.0));
chart
.timing_points
.push(TimingPoint::bpm(10_000_000, 200.0)); chart
.timing_points
.push(TimingPoint::bpm(20_000_000, 100.0));
chart.notes.push(Note::tap(30_000_000, 0));
assert_eq!(chart.bpm_min(), 100.0);
assert_eq!(chart.bpm_max(), 200.0);
assert_eq!(chart.bpm_mode(), 100.0);
}
#[test]
fn test_nps() {
let mut chart = RoxChart::new(4);
chart.notes.push(Note::tap(0, 0));
chart.notes.push(Note::tap(1_000_000, 0));
chart.notes.push(Note::tap(2_000_000, 0));
assert_eq!(chart.nps(), 1.5);
}
#[test]
fn test_density() {
let mut chart = RoxChart::new(4);
chart.notes.push(Note::tap(9_999_999, 0));
for i in 0..10 {
chart.notes.push(Note::tap(i * 500_000, 0));
}
let dens = chart.density(2);
assert_eq!(dens.len(), 2);
assert!((dens[0] - 2.0).abs() < 0.001);
assert!((dens[1] - 0.2).abs() < 0.001);
}
#[test]
fn test_highest_nps() {
let mut chart = RoxChart::new(4);
for i in 0..10 {
chart.notes.push(Note::tap(10_000_000 + i * 50_000, 0)); }
chart.notes.push(Note::tap(0, 0));
chart.notes.push(Note::tap(20_000_000, 0));
let peak = chart.highest_nps(1.0);
assert_eq!(peak, 10.0);
}
#[test]
fn test_lowest_nps() {
let mut chart = RoxChart::new(4);
chart.notes.push(Note::tap(0, 0));
chart.notes.push(Note::tap(1_000_000, 0));
chart.notes.push(Note::tap(10_000_000, 0));
assert_eq!(chart.lowest_nps(2.0), 0.0);
}
#[test]
fn test_highest_drain_time() {
let mut chart = RoxChart::new(4);
for i in 0..50 {
chart.notes.push(Note::tap(1_000_000 + i * 100_000, 0));
}
for i in 0..100 {
chart.notes.push(Note::tap(10_000_000 + i * 100_000, 0));
}
let drain = chart.highest_drain_time();
assert!(drain >= 9.0 && drain <= 10.5, "Drain time was {}", drain);
}
#[test]
fn test_hash_correctness() {
let mut chart = RoxChart::new(4);
chart.notes.push(Note::tap(1_000_000, 0));
chart.notes.push(Note::tap(2_000_000, 1));
chart.notes.push(Note::hold(3_000_000, 500_000, 2));
let hash = chart.hash();
assert_eq!(
hash, "2c0964de10711f16489a3db2796afe320ace9cbe2e28a142e226e28620d1e8d4",
"Hash verification failed"
);
assert_eq!(
chart.notes_hash(),
"dcdccda1c57c13043c67373c71bc17769a5d77e3c7eb2f258549125b87162ea5"
);
assert_eq!(chart.short_hash(), "2c0964de10711f16");
}
#[test]
fn test_hash_extension() {
let mut chart = RoxChart::new(4);
chart.notes.push(Note::tap(0, 0));
let h1 = chart.hash();
assert!(!h1.is_empty());
let nh1 = chart.notes_hash();
assert!(!nh1.is_empty());
}
#[test]
fn test_hash_determinism() {
let mut chart = RoxChart::new(4);
chart.metadata.title = "Test".into();
chart.notes.push(Note::tap(0, 0));
let hash1 = chart.hash();
let mut chart2 = RoxChart::new(4);
chart2.metadata.title = "Test".into();
chart2.notes.push(Note::tap(0, 0));
let hash2 = chart2.hash();
assert_eq!(hash1, hash2);
chart2.notes.push(Note::tap(1_000_000, 1));
let hash3 = chart2.hash();
assert_ne!(hash1, hash3);
}
}