Implement PPQ Calculation [per team per game] and housekeeping.

This commit is contained in:
Cutieguwu
2025-04-05 16:21:31 -04:00
parent 589dbd55d0
commit e72cdbf4b7
12 changed files with 688 additions and 137 deletions

View File

@@ -1,58 +1,356 @@
use crate::{Event, Period, Play, PlayHandle, Team, error};
use crate::{Event, Period, Team, error};
use serde::Deserialize;
#[deprecated(since = "0.2.0", note = "Migrated to Game")]
pub type GameRecord = Game;
#[derive(Debug, Deserialize, Clone)]
pub struct Game {
pub version: semver::Version,
pub flags: Vec<FeatureFlags>,
pub flags: Vec<Flags>,
pub periods: Vec<Period>,
}
impl Game {
/// Returns the teams of this game.
pub fn teams(&self) -> Result<Vec<Team>, error::TeamsError> {
let ignore: Vec<Team> = self
.flags
.iter()
.filter_map(|flag| {
if let Flags::IgnoreTeam(team) = flag {
Some(team.to_owned())
} else {
None
}
})
.collect();
let mut teams = vec![];
self.periods.iter().for_each(|period| {
period.events.iter().for_each(|event| {
if let Event::Kickoff(t) | Event::Turnover(t) = event {
if teams.contains(t) {
teams.push(t.to_owned())
for event in period.events.iter() {
if let Ok(team) = event.team() {
if !ignore.contains(&team) && !teams.contains(&team) {
teams.push(team)
}
}
})
}
});
if teams.len() == 2 {
if teams.len() == 2 || ignore.len() != 0 {
Ok(teams)
} else {
Err(error::TeamsError::NumberFound(teams.len()))
}
}
pub fn deltas(&self) -> Vec<i8> {
self.periods
pub fn deltas(&self, team: Team) -> Vec<i8> {
let events = self
.periods
.iter()
.map(|period| period.deltas())
.collect::<Vec<Vec<i8>>>()
.concat()
.filter_map(|period| Some(period.team_events(team.to_owned(), None).ok().unwrap()))
.collect::<Vec<Vec<Event>>>()
.concat();
let len = events.len() - 1;
let mut idx: usize = 0;
let mut deltas: Vec<i8> = vec![];
dbg!(&events);
while idx < len {
if let Some(value) = events[idx].delta(&events[idx + 1]) {
deltas.push(value);
}
idx += 1
}
deltas
}
/// The average number of plays in a quarter.
/// Does not include OT plays or quarters where team indeterminate.
pub fn avg_plays_per_quarter(&self, team: Team) -> f32 {
// Handle if teams known at start or not override via index calculation of all game events.
let quarterly_avgs: Vec<f32> = self
.periods
.iter()
.filter_map(|period| {
if !period.is_overtime() {
let plays = period.team_plays(team.to_owned(), None);
Some(plays.unwrap().len() as f32 / period.quarters().len() as f32)
} else {
None
}
})
.collect::<Vec<f32>>();
let mut summation = 0_f32;
quarterly_avgs.iter().for_each(|float| summation += float);
summation / quarterly_avgs.len() as f32
}
pub fn team_plays(&self, team: Team) -> usize {
let quarterly_plays: Vec<usize> = self
.periods
.iter()
.filter_map(|period| {
if !period.is_overtime() {
let plays = period.team_plays(team.to_owned(), None);
Some(plays.unwrap().len())
} else {
None
}
})
.collect::<Vec<usize>>();
let mut summation = 0_usize;
quarterly_plays.iter().for_each(|value| summation += value);
summation
}
}
impl PlayHandle for Game {
fn plays(&self) -> Vec<Play> {
self.periods
.iter()
.map(|period| period.plays())
.collect::<Vec<Vec<Play>>>() // Make compiler happy with turbofish.
.concat()
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum Flags {
IgnoreTeam(Team),
IgnoreScore,
}
#[derive(Debug, Deserialize, Clone)]
pub enum FeatureFlags {
Ignore(Team),
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn avg_plays_per_quarter() {
let a = Game {
version: crate::MIN_VER,
flags: vec![],
periods: vec![
Period {
start: Quarter::First,
end: None,
events: vec![
Event::Kickoff(Team::Nebraska),
Event::Play(Play::default()),
Event::Turnover(Team::ArizonaState),
],
},
Period {
start: Quarter::Second,
end: Some(Quarter::Fourth),
events: vec![
Event::Turnover(Team::Nebraska),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Turnover(Team::ArizonaState),
],
},
],
};
let b = Game {
version: crate::MIN_VER,
flags: vec![],
periods: vec![Period {
start: Quarter::Second,
end: Some(Quarter::Fourth),
events: vec![
Event::Turnover(Team::Nebraska),
Event::Play(Play::default()),
Event::Turnover(Team::ArizonaState),
],
}],
};
assert!(a.avg_plays_per_quarter(Team::Nebraska) == ((1_f32 + 2_f32) / 2_f32));
assert!(b.avg_plays_per_quarter(Team::Nebraska) == (1_f32 / 3_f32))
}
#[test]
fn team_plays() {
let a = Game {
version: crate::MIN_VER,
flags: vec![],
periods: vec![
Period {
start: Quarter::First,
end: None,
events: vec![
Event::Kickoff(Team::Nebraska),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
],
},
Period {
start: Quarter::Second,
end: Some(Quarter::Fourth),
events: vec![
Event::Turnover(Team::Nebraska),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Play(Play::default()),
],
},
],
};
assert!(a.team_plays(Team::Nebraska) == 12_usize)
}
#[test]
#[allow(deprecated)]
fn teams() {
let a = Game {
version: crate::MIN_VER,
flags: vec![],
periods: vec![
Period {
start: Quarter::First,
end: None,
events: vec![Event::Kickoff(Team::Nebraska)],
},
Period {
start: Quarter::Second,
end: Some(Quarter::Fourth),
events: vec![
Event::Turnover(Team::ArizonaState),
Event::Kickoff(Team::Nebraska),
],
},
],
};
let b = Game {
version: crate::MIN_VER,
flags: vec![],
periods: vec![
Period {
start: Quarter::First,
end: None,
events: vec![Event::Kickoff(Team::Nebraska)],
},
Period {
start: Quarter::Second,
end: Some(Quarter::Fourth),
events: vec![
Event::Turnover(Team::ArizonaState),
Event::Kickoff(Team::BoiseState),
],
},
],
};
let c = Game {
version: crate::MIN_VER,
flags: vec![Flags::IgnoreTeam(Team::Nebraska)],
periods: vec![
Period {
start: Quarter::First,
end: None,
events: vec![Event::Kickoff(Team::Nebraska)],
},
Period {
start: Quarter::Second,
end: Some(Quarter::Fourth),
events: vec![
Event::Turnover(Team::ArizonaState),
Event::Kickoff(Team::Nebraska),
],
},
],
};
let d = Game {
version: crate::MIN_VER,
flags: vec![Flags::IgnoreTeam(Team::Nebraska)],
periods: vec![Period {
start: Quarter::First,
end: None,
events: vec![Event::Kickoff(Team::Nebraska)],
}],
};
assert!(a.teams().unwrap() == vec![Team::Nebraska, Team::ArizonaState]);
assert!(b.teams().is_err() == true);
assert!(c.teams().unwrap() == vec![Team::ArizonaState]);
assert!(d.teams().unwrap() == vec![]);
}
#[test]
fn deltas() {
let game = Game {
version: crate::MIN_VER,
flags: vec![],
periods: vec![
Period {
start: Quarter::First,
end: None,
events: vec![
Event::Kickoff(Team::Nebraska),
Event::Play(Play {
action: Action::Unknown,
down: Some(Down::First),
terrain: Some(TerrainState::Yards(10)),
}),
Event::Play(Play {
action: Action::Unknown,
down: Some(Down::Second),
terrain: Some(TerrainState::Yards(13)),
}),
Event::Play(Play {
action: Action::Unknown,
down: Some(Down::Third),
terrain: Some(TerrainState::Yards(8)),
}),
Event::Turnover(Team::ArizonaState),
Event::Play(Play {
action: Action::Unknown,
down: Some(Down::First),
terrain: Some(TerrainState::Yards(10)),
}),
Event::Play(Play {
action: Action::Unknown,
down: Some(Down::Second),
terrain: Some(TerrainState::Yards(10)),
}),
Event::Turnover(Team::Nebraska),
Event::Play(Play {
action: Action::Unknown,
down: Some(Down::Second),
terrain: Some(TerrainState::Yards(12)),
}),
],
},
Period {
start: Quarter::Second,
end: None,
events: vec![
Event::Play(Play {
action: Action::Unknown,
down: Some(Down::First),
terrain: Some(TerrainState::Yards(10)),
}),
Event::Turnover(Team::ArizonaState),
],
},
],
};
assert!(dbg!(game.deltas(Team::Nebraska)) == vec![10_i8, -3_i8, 5_i8, -2_i8, 12_i8]);
assert!(dbg!(game.deltas(Team::ArizonaState)) == vec![10_i8, 0_i8]);
}
}