Set up lib.rs for integration test access, add 50 tests covering calc engine (weights, standardization), model helpers (Player positions, Lineup JSON roundtrips), and full query layer (in-memory SQLite). Update PHASE1_PROJECT_PLAN.json to reflect all 12 tasks completed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
4.0 KiB
Rust
132 lines
4.0 KiB
Rust
use super::league_stats::StatDistribution;
|
|
use super::weights::StatWeight;
|
|
|
|
/// Convert a raw stat value to a standardized score (-3 to +3).
|
|
///
|
|
/// Uses standard deviation thresholds from the league mean.
|
|
/// A value of 0 always returns +3 (best possible).
|
|
pub fn standardize_value(value: f64, distribution: &StatDistribution, high_is_better: bool) -> i32 {
|
|
if value == 0.0 {
|
|
return 3;
|
|
}
|
|
|
|
let avg = distribution.avg;
|
|
let stdev = distribution.stdev;
|
|
|
|
let base_score = if value > avg + 2.0 * stdev {
|
|
-3
|
|
} else if value > avg + stdev {
|
|
-2
|
|
} else if value > avg + 0.33 * stdev {
|
|
-1
|
|
} else if value > avg - 0.33 * stdev {
|
|
0
|
|
} else if value > avg - stdev {
|
|
1
|
|
} else if value > avg - 2.0 * stdev {
|
|
2
|
|
} else {
|
|
3
|
|
};
|
|
|
|
if high_is_better { -base_score } else { base_score }
|
|
}
|
|
|
|
/// Calculate weighted score for a single stat.
|
|
pub fn calculate_weighted_score(
|
|
value: f64,
|
|
distribution: &StatDistribution,
|
|
stat_weight: &StatWeight,
|
|
) -> f64 {
|
|
let std_score = standardize_value(value, distribution, stat_weight.high_is_better);
|
|
std_score as f64 * stat_weight.weight as f64
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn dist(avg: f64, stdev: f64) -> StatDistribution {
|
|
StatDistribution { avg, stdev }
|
|
}
|
|
|
|
// --- standardize_value tests ---
|
|
|
|
#[test]
|
|
fn zero_value_always_returns_3() {
|
|
let d = dist(5.0, 1.0);
|
|
assert_eq!(standardize_value(0.0, &d, true), 3);
|
|
assert_eq!(standardize_value(0.0, &d, false), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn value_far_above_mean_is_minus3_base() {
|
|
// value > avg + 2*stdev => base_score = -3
|
|
let d = dist(5.0, 1.0);
|
|
// high_is_better=true: -(-3) = 3
|
|
assert_eq!(standardize_value(7.5, &d, true), 3);
|
|
// high_is_better=false: -3
|
|
assert_eq!(standardize_value(7.5, &d, false), -3);
|
|
}
|
|
|
|
#[test]
|
|
fn value_far_below_mean_is_plus3_base() {
|
|
// value <= avg - 2*stdev => base_score = 3
|
|
let d = dist(5.0, 1.0);
|
|
// high_is_better=true: -(3) = -3
|
|
assert_eq!(standardize_value(2.5, &d, true), -3);
|
|
// high_is_better=false: 3
|
|
assert_eq!(standardize_value(2.5, &d, false), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn value_at_mean_returns_zero_base() {
|
|
// avg - 0.33*stdev < value < avg + 0.33*stdev => base_score = 0
|
|
let d = dist(5.0, 1.0);
|
|
assert_eq!(standardize_value(5.0, &d, true), 0);
|
|
assert_eq!(standardize_value(5.0, &d, false), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn score_ranges_are_symmetric() {
|
|
// Walk through all 7 buckets with a clean distribution
|
|
let d = dist(10.0, 2.0);
|
|
|
|
// base_score mapping (raw, not flipped):
|
|
// > 14.0 (avg+2*std) => -3
|
|
// > 12.0 (avg+std) => -2
|
|
// > 10.66 (avg+0.33*std) => -1
|
|
// > 9.34 (avg-0.33*std) => 0
|
|
// > 8.0 (avg-std) => 1
|
|
// > 6.0 (avg-2*std) => 2
|
|
// <= 6.0 => 3
|
|
|
|
// Test with high_is_better=false (no flip)
|
|
assert_eq!(standardize_value(15.0, &d, false), -3);
|
|
assert_eq!(standardize_value(13.0, &d, false), -2);
|
|
assert_eq!(standardize_value(11.0, &d, false), -1);
|
|
assert_eq!(standardize_value(10.0, &d, false), 0);
|
|
assert_eq!(standardize_value(9.0, &d, false), 1);
|
|
assert_eq!(standardize_value(7.0, &d, false), 2);
|
|
assert_eq!(standardize_value(5.0, &d, false), 3);
|
|
}
|
|
|
|
// --- calculate_weighted_score tests ---
|
|
|
|
#[test]
|
|
fn weighted_score_multiplies_std_by_weight() {
|
|
let d = dist(10.0, 2.0);
|
|
let w = StatWeight { weight: 5, high_is_better: true };
|
|
// value=15.0 => base=-3, flipped=3 => 3*5=15.0
|
|
assert!((calculate_weighted_score(15.0, &d, &w) - 15.0).abs() < f64::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn weighted_score_zero_value() {
|
|
let d = dist(10.0, 2.0);
|
|
let w = StatWeight { weight: 3, high_is_better: false };
|
|
// value=0.0 => always 3 => 3*3=9.0
|
|
assert!((calculate_weighted_score(0.0, &d, &w) - 9.0).abs() < f64::EPSILON);
|
|
}
|
|
}
|