This repository has been archived on 2025-07-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
miller/gamelog/src/game.rs

529 lines
16 KiB
Rust

use crate::{Event, Play, Quarter, Team, error};
use serde::Deserialize;
use strum::IntoEnumIterator;
#[derive(Debug, Deserialize, Clone)]
pub struct Game {
pub version: semver::Version,
pub flags: Vec<Flags>,
pub events: Vec<Event>,
}
impl Game {
/// Returns the teams that played.
pub fn teams(&self) -> Result<Vec<Team>, error::LogFileError> {
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.events.iter().for_each(|event| {
if let Some(team) = event.team() {
if !ignore.contains(&team) && !teams.contains(&team) {
teams.push(team)
}
}
});
if teams.len() == 2 || ignore.len() != 0 {
Ok(teams)
} else {
Err(error::LogFileError::TooManyTeams(teams.len()))
}
}
/// Returns all of the terrain deltas of a team.
pub fn deltas(&self, team: Team) -> Vec<i8> {
let events: Vec<Event> = self
.team_events(team)
.unwrap_or(TeamEvents(vec![]))
.0
.iter()
.filter_map(|event| {
if let Event::Quarter(_) = event {
None
} else {
Some(event.to_owned())
}
})
.collect();
let len = events.len() - 1;
let mut idx: usize = 0;
let mut deltas: Vec<i8> = vec![];
while idx < len {
if let Some(value) = events[idx].delta(&events[idx + 1]) {
deltas.push(value);
}
idx += 1
}
deltas
}
/// Returns all of the plays of a team.
pub fn team_plays(&self, team: Team) -> TeamPlays {
TeamPlays(
self.team_events(team)
.unwrap_or(TeamEvents(vec![]))
.0
.iter()
.filter_map(|event| {
if let Event::Play(play) = event {
Some(play.to_owned())
} else {
None
}
})
.collect::<Vec<Play>>(),
)
}
/// The average number of plays in a quarter.
pub fn avg_plays_per_quarter(&self, team: Team) -> f32 {
let periods: Vec<Period> = Quarter::iter()
.filter_map(|quarter| self.get_period(quarter.to_owned()))
.to_owned()
.collect();
let quarterly_avgs: Vec<f32> = periods
.iter()
.filter_map(|period| {
if !period.is_overtime() {
Some(period.team_plays(team.to_owned()) as f32)
} else {
None
}
})
.collect::<Vec<f32>>();
quarterly_avgs.iter().sum::<f32>() / quarterly_avgs.len() as f32
}
/// Returns the average delta of a team.
pub fn avg_delta(&self, team: Team) -> f32 {
let deltas = self.deltas(team);
// Summation doesn't like directly returning f32 from i8.
deltas.iter().sum::<i8>() as f32 / deltas.len() as f32
}
/// Returns the average delta for a team's positive deltas.
pub fn avg_gain(&self, team: Team) -> f32 {
let deltas: Vec<u8> = self
.deltas(team)
.iter()
.filter_map(|value| {
if value.is_positive() {
Some(value.to_owned() as u8)
} else {
None
}
})
.collect();
// Summation doesn't like directly returning f32 from u8.
deltas.iter().sum::<u8>() as f32 / deltas.len() as f32
}
/// Returns the average delta for a team's negative deltas.
pub fn avg_loss(&self, team: Team) -> f32 {
let deltas: Vec<i8> = self
.deltas(team)
.iter()
.filter_map(|value| {
if value.is_negative() {
Some(value.to_owned())
} else {
None
}
})
.collect();
deltas.iter().sum::<i8>() as f32 / deltas.len() as f32
}
/// Returns the number of penalties that a team experienced.
pub fn penalties(&self, team: Team) -> usize {
self.team_events(team)
.unwrap_or(TeamEvents(vec![]))
.0
.iter()
.filter_map(|event| {
if let Event::Penalty(_) = event {
Some(event.to_owned())
} else {
None
}
})
.collect::<Vec<Event>>()
.len()
}
/// Returns the requested quarter.
/// If there is no history logged for the requested quarter, returns `None`.
/// For example, if requesting an OT that doesn't exist.
pub fn get_period(&self, quarter: Quarter) -> Option<Period> {
let mut record = false;
let period = Period {
period: quarter.to_owned(),
events: self
.events
.iter()
.filter_map(|event| {
if let Event::Quarter(_) = event {
record = Event::Quarter(quarter.to_owned()) == *event;
}
if record {
return Some(event.to_owned());
}
None
})
.collect::<Vec<Event>>(),
};
if period.events.len() != 0 {
Some(period)
} else {
None
}
}
/// Returns all events relevent to a team's deltas and score.
pub fn team_events(&self, team: Team) -> Option<TeamEvents> {
if !self.teams().is_ok_and(|ok| ok.contains(&team)) {
return None;
}
let mut events: Vec<Event> = vec![];
let mut first = true;
let mut record: bool = true;
self.events.iter().for_each(|event| {
if let Event::Kickoff(_) | Event::Turnover(_) = event {
record = {
if team == event.team().unwrap() {
// Wipe events vec if the start of quarter was opposition
// on offence.
if first {
events = vec![];
}
true
} else {
events.push(event.to_owned());
false
}
};
first = false;
}
if record {
events.push(event.to_owned());
}
});
// If already handled or assumption override applicable
Some(TeamEvents(events))
}
}
#[derive(Debug)]
pub struct TeamEvents(pub Vec<Event>);
#[derive(Debug)]
pub struct TeamPlays(pub Vec<Play>);
#[derive(Debug, Clone)]
pub struct Period {
period: Quarter,
events: Vec<Event>,
}
impl Period {
/// Returns all events relevent to a team's deltas and score.
pub fn team_events(&self, team: Team) -> Vec<Event> {
let mut events: Vec<Event> = vec![];
let mut first = true;
let mut record: bool = true;
self.events.iter().for_each(|event| {
if let Event::Kickoff(_) | Event::Turnover(_) = event {
record = {
if team == event.team().unwrap() {
// Wipe events vec if the start of quarter was opposition
// on offence.
if first {
events = vec![];
}
true
} else {
events.push(event.to_owned());
false
}
};
first = false;
}
if record {
events.push(event.to_owned());
}
});
events
}
/// Returns all of the plays of a team.
pub fn team_plays(&self, team: Team) -> usize {
self.team_events(team)
.iter()
.filter_map(|event| {
if let Event::Play(_) = event {
Some(event)
} else {
None
}
})
.collect::<Vec<&Event>>()
.len()
}
/// Returns true if the current period is overtime.
pub fn is_overtime(&self) -> bool {
if let Quarter::Overtime(_) = self.period {
true
} else {
false
}
}
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum Flags {
ClockBleeding(Team),
IgnoreActions,
IgnoreTeam(Team),
IgnoreScore,
Interval(u8),
LowDataCredibility,
SheerDumbFuckingLuck,
Stub,
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn avg_plays_per_quarter() {
let a = Game {
version: crate::MIN_VER,
flags: vec![],
events: vec![
Event::Quarter(Quarter::First),
Event::Kickoff(Team::Nebraska),
Event::Play(Play::default()),
Event::Turnover(Team::ArizonaState),
Event::Quarter(Quarter::Second),
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![],
events: vec![
Event::Quarter(Quarter::First),
Event::Turnover(Team::Nebraska),
Event::Play(Play::default()),
Event::Turnover(Team::ArizonaState),
],
};
dbg!(a.avg_plays_per_quarter(Team::Nebraska));
dbg!(b.avg_plays_per_quarter(Team::Nebraska));
assert!(a.avg_plays_per_quarter(Team::Nebraska) == ((1_f32 + 6_f32) / 2_f32));
assert!(b.avg_plays_per_quarter(Team::Nebraska) == 1_f32)
}
#[test]
#[allow(deprecated)]
fn teams() {
let a = Game {
version: crate::MIN_VER,
flags: vec![],
events: vec![
Event::Kickoff(Team::Nebraska),
Event::Turnover(Team::ArizonaState),
Event::Kickoff(Team::Nebraska),
],
};
let b = Game {
version: crate::MIN_VER,
flags: vec![],
events: vec![
Event::Kickoff(Team::Nebraska),
Event::Turnover(Team::ArizonaState),
Event::Kickoff(Team::BoiseState),
],
};
let c = Game {
version: crate::MIN_VER,
flags: vec![Flags::IgnoreTeam(Team::Nebraska)],
events: vec![
Event::Kickoff(Team::Nebraska),
Event::Turnover(Team::ArizonaState),
Event::Kickoff(Team::Nebraska),
],
};
let d = Game {
version: crate::MIN_VER,
flags: vec![Flags::IgnoreTeam(Team::Nebraska)],
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![],
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)),
}),
Event::Play(Play {
action: Action::Unknown,
down: Some(Down::First),
terrain: Some(TerrainState::Yards(10)),
}),
Event::Turnover(Team::ArizonaState),
],
};
assert!(game.deltas(Team::Nebraska) == vec![10_i8, -3_i8, 5_i8, -2_i8, 12_i8]);
assert!(game.deltas(Team::ArizonaState) == vec![10_i8, 0_i8]);
}
#[test]
#[allow(deprecated)]
fn team_events() {
let a = Game {
version: crate::MIN_VER,
flags: vec![],
events: vec![
Event::Kickoff(Team::Nebraska),
Event::Play(Play::default()),
Event::Turnover(Team::SouthCarolina),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Kickoff(Team::Nebraska),
Event::Score(ScorePoints::Touchdown),
Event::Kickoff(Team::SouthCarolina),
],
};
assert!(
a.team_events(Team::Nebraska).unwrap().0
== vec![
Event::Kickoff(Team::Nebraska),
Event::Play(Play::default()),
Event::Turnover(Team::SouthCarolina),
Event::Kickoff(Team::Nebraska),
Event::Score(ScorePoints::Touchdown),
Event::Kickoff(Team::SouthCarolina),
]
);
assert!(a.team_events(Team::BoiseState).is_none());
}
#[test]
fn team_plays() {
let game = Game {
version: crate::MIN_VER,
flags: vec![],
events: vec![
Event::Kickoff(Team::Nebraska),
Event::Play(Play::default()),
Event::Turnover(Team::ArizonaState),
Event::Play(Play::default()),
Event::Play(Play::default()),
Event::Kickoff(Team::Nebraska),
Event::Play(Play::default()),
Event::Score(ScorePoints::default()),
Event::Kickoff(Team::ArizonaState),
Event::Play(Play::default()),
Event::Turnover(Team::Nebraska),
Event::Play(Play::default()),
],
};
assert!(
game.team_plays(Team::Nebraska).0
== vec![Play::default(), Play::default(), Play::default()]
);
}
}