From e72cdbf4b79d9cc07e526a6100c69114ef9ede3d Mon Sep 17 00:00:00 2001 From: Cutieguwu Date: Sat, 5 Apr 2025 16:21:31 -0400 Subject: [PATCH] Implement PPQ Calculation [per team per game] and housekeeping. --- gamelog.ron | 69 ++++---- gamelog/src/error.rs | 18 ++ gamelog/src/event.rs | 38 ++++- gamelog/src/file.rs | 2 +- gamelog/src/game.rs | 352 ++++++++++++++++++++++++++++++++++++--- gamelog/src/lib.rs | 12 +- gamelog/src/period.rs | 286 ++++++++++++++++++++++++++----- gamelog/src/play.rs | 11 -- miller/Cargo.toml | 1 - miller/src/calculator.rs | 0 miller/src/main.rs | 28 +++- templates/logfile.ron | 8 +- 12 files changed, 688 insertions(+), 137 deletions(-) delete mode 100644 miller/src/calculator.rs diff --git a/gamelog.ron b/gamelog.ron index 7197cb0..9d8e492 100644 --- a/gamelog.ron +++ b/gamelog.ron @@ -4,12 +4,13 @@ [ Game( - version: "0.3.0", + version: "0.5.0", + flags: [IgnoreScore], periods: [ Period( start: First, end: Third, - plays: [ + events: [ Kickoff(ArizonaState), Play( action: Unknown, @@ -47,7 +48,7 @@ terrain: Yards(7), ), Play( - action: HalfbackSweep + action: HalfbackSweep, down: Fourth, terrain: Yards(11), ), @@ -130,7 +131,7 @@ Period( start: Fourth, end: None, - plays: [ + events: [ Play( action: PlayActionComebacks, // Original note: Dupe Spike Centre down: Fourth, @@ -182,12 +183,13 @@ ] ), Game( - version: "0.3.1", + version: "0.5.0", + flags: [IgnoreScore], periods: [ Period( start: First, end: None, - plays: [ + events: [ Kickoff(Syracuse), Play( action: Unknown, @@ -224,7 +226,7 @@ Period( start: Second, end: None, - plays: [ + events: [ Play( action: Unknown, // Original note: PA Throw Centre down: Second, @@ -254,7 +256,7 @@ action: Unknown, // HalfbackSlam? Original note: PA Rush Centre down: None, terrain: None, - ) + ), Turnover(Colorado), Play( action: PlayActionComebacks, @@ -281,7 +283,7 @@ Period( start: Third, end: None, - plays: [ + events: [ Kickoff(Colorado), Play( action: Unknown, // Original note: Throw Right @@ -308,7 +310,7 @@ Period( start: Fourth, end: None, - plays: [ + events: [ Play( action: CrackStudentBodyRightTackle, down: Third, @@ -319,7 +321,6 @@ down: Fourth, terrain: Yards(3), ), - Score(0), Kickoff(Syracuse), Play( action: Unknown, // Original note: PA Throw Centre @@ -336,18 +337,19 @@ ] ), Game( - version: "0.3.1", + version: "0.5.0", + flags: [], periods: [ Period( start: First, end: None, - plays: [ + events: [ Kickoff(Nebraska), Play( action: Curls, down: First, terrain: Yards(10) - ) + ), Play( action: Unknown, down: Second, @@ -379,14 +381,14 @@ Period( start: Second, end: None, - plays: [ + events: [ Play( action: Unknown, // Original note: Throw Centre down: First, terrain: GoalLine, ), - Pat(Fail), - Score(6), + Score(Touchdown), + Score(PatFail), Kickoff(Nebraska), Play( action: StrongFlood, @@ -429,7 +431,7 @@ Period( start: Third, end: None, - plays: [ + events: [ Kickoff(SouthCarolina), Play( action: Unknown, @@ -452,7 +454,7 @@ down: None, terrain: None, ), - Score(3), + Score(FieldGoal), Kickoff(SouthCarolina), Play( action: Unknown, @@ -469,7 +471,7 @@ Period( start: Fourth, end: None, - plays: [ + events: [ Play( action: Unknown, // Original note: Throw Centre down: Fourth, @@ -490,8 +492,8 @@ down: Third, terrain: Yards(5), ), - Pat(), // Original note: Dupe, Throw Centre - Score(12), + Score(Touchdown), + Score(PatFail), Kickoff(Nebraska), Play( action: PowerZero, @@ -536,7 +538,7 @@ ), Turnover(Nebraska), // Field Goal 41 yrds - Score(6), + Score(FieldGoal), Kickoff(SouthCarolina), Play( action: Unknown, @@ -548,7 +550,8 @@ ] ), Game( - version: "0.4.0", + version: "0.5.0", + flags: [], periods: [ Period( start: First, @@ -617,8 +620,8 @@ terrain: GoalLine, ), // Touchdown - Pat(One), // Throw - Score(7), + Score(Touchdown), + Score(PatSafety), Kickoff(Iowa), Penalty(Yards(15)), Play( @@ -656,7 +659,7 @@ down: First, terrain: GoalLine, ), - Score(3), + Score(FieldGoal), ] ), Period( @@ -716,8 +719,8 @@ terrain: Yards(4), ), // Touchdown - Pat(One) - Score(7), + Score(Touchdown), + Score(PatSafety), Kickoff(Colorado), Play( action: Unknown, // Original note: Dupe, Throw Left @@ -752,13 +755,14 @@ terrain: Yards(15), ), //Field Goal - Score(10) + Score(FieldGoal) ] ) ] ), Game( version: "0.5.0", + flags: [], periods: [ Period( start: First, @@ -878,7 +882,7 @@ terrain:Yards(10), ), Play( - action: //IForm Normal, Thrown + action: Unknown,//IForm Normal, Thrown down: None, terrain: None ), @@ -985,6 +989,7 @@ // TexasAnM were opponents, but not recorded as // they were not present; Miller played in place. version: "0.5.0", + flags: [IgnoreTeam(TexasAnM)], periods: [ Period( start: First, @@ -1053,7 +1058,7 @@ terrain: Yards(13) ), Play( - action: First, + action: Unknown, down: First, terrain: Yards(10) ), diff --git a/gamelog/src/error.rs b/gamelog/src/error.rs index c3e135f..709783d 100644 --- a/gamelog/src/error.rs +++ b/gamelog/src/error.rs @@ -27,3 +27,21 @@ impl fmt::Display for TeamsError { } } } + +#[derive(Debug)] +pub struct NoTeamAttribute; + +impl fmt::Display for NoTeamAttribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Object has no team definition.") + } +} + +#[derive(Debug)] +pub struct CannotDetermineTeams; + +impl fmt::Display for CannotDetermineTeams { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Cannot determine teams present.") + } +} diff --git a/gamelog/src/event.rs b/gamelog/src/event.rs index e2d8fa0..622c59d 100644 --- a/gamelog/src/event.rs +++ b/gamelog/src/event.rs @@ -1,9 +1,9 @@ -use crate::{Down, Play, Team, TerrainState}; +use crate::{Down, Play, Team, TerrainState, error}; use serde::Deserialize; type Offence = Team; -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq)] pub enum Event { Play(Play), Kickoff(Offence), @@ -18,7 +18,7 @@ impl Event { fn make_play(event: &Event) -> Option { match event { - Event::Kickoff(_) => Some(Play::default()), + Event::Kickoff(_) | Event::Turnover(_) => Some(Play::default()), Event::Play(play) => { let p = play.to_owned(); @@ -36,7 +36,13 @@ impl Event { } let preceeding = make_play(self)?; - let following = make_play(following)?; + let following = if let Event::Turnover(_) = following { + // I should really just early return + // but this is too funny to look at. + None? + } else { + make_play(following)? + }; if following.down? == Down::First { if let TerrainState::Yards(yrds) = preceeding.terrain? { @@ -60,6 +66,14 @@ impl Event { Some(a as i8 - b as i8) } } + + pub fn team(&self) -> Result { + match self { + Self::Kickoff(team) => Ok(team.to_owned()), + Self::Turnover(team) => Ok(team.to_owned()), + _ => Err(error::NoTeamAttribute), + } + } } #[derive(Debug, Deserialize, Clone, PartialEq, Default)] @@ -90,45 +104,54 @@ impl ScorePoints { #[cfg(test)] mod tests { - use super::*; - use crate::{Action, Down, Team, TerrainState}; + use crate::*; #[test] fn delta() { let kickoff = Event::Kickoff(Team::Nebraska); + let first_down = Event::Play(Play { action: Action::Unknown, down: Some(Down::First), terrain: Some(TerrainState::Yards(10)), }); + let second_down = Event::Play(Play { action: Action::Unknown, down: Some(Down::Second), terrain: Some(TerrainState::Yards(10)), }); + let third_down = Event::Play(Play { action: Action::Unknown, down: Some(Down::Third), terrain: Some(TerrainState::Yards(13)), }); + let fourth_down = Event::Play(Play { action: Action::Unknown, down: Some(Down::Fourth), terrain: Some(TerrainState::Yards(5)), }); + let penalty = Event::Penalty(TerrainState::Yards(15)); + let turnover = Event::Turnover(Team::Nebraska); + let noned_down = Event::Play(Play { action: Action::Unknown, down: None, terrain: None, }); + let score = Event::Score(ScorePoints::default()); + let goal_line = Event::Play(Play { action: Action::Unknown, down: Some(Down::First), terrain: Some(TerrainState::GoalLine), }); + let inches = Event::Play(Play { action: Action::Unknown, down: Some(Down::First), @@ -155,5 +178,8 @@ mod tests { assert!(None == goal_line.delta(&first_down)); assert!(None == inches.delta(&first_down)); assert!(None == goal_line.delta(&inches)); + assert!(10_i8 == turnover.delta(&first_down).unwrap()); + assert!(0_i8 == turnover.delta(&second_down).unwrap()); + assert!(-3_i8 == turnover.delta(&third_down).unwrap()); } } diff --git a/gamelog/src/file.rs b/gamelog/src/file.rs index 48f5413..b21a035 100644 --- a/gamelog/src/file.rs +++ b/gamelog/src/file.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use std::{fs::File, path::PathBuf}; #[derive(Debug, Deserialize, Clone)] -pub struct LogFile(Vec); +pub struct LogFile(pub Vec); impl LogFile { pub fn min_ver(&self) -> semver::Version { diff --git a/gamelog/src/game.rs b/gamelog/src/game.rs index d99ccd8..2be7546 100644 --- a/gamelog/src/game.rs +++ b/gamelog/src/game.rs @@ -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, + pub flags: Vec, pub periods: Vec, } impl Game { /// Returns the teams of this game. pub fn teams(&self) -> Result, error::TeamsError> { + let ignore: Vec = 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 { - self.periods + pub fn deltas(&self, team: Team) -> Vec { + let events = self + .periods .iter() - .map(|period| period.deltas()) - .collect::>>() - .concat() + .filter_map(|period| Some(period.team_events(team.to_owned(), None).ok().unwrap())) + .collect::>>() + .concat(); + let len = events.len() - 1; + let mut idx: usize = 0; + let mut deltas: Vec = 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 = 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::>(); + + 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 = 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::>(); + + let mut summation = 0_usize; + + quarterly_plays.iter().for_each(|value| summation += value); + + summation } } -impl PlayHandle for Game { - fn plays(&self) -> Vec { - self.periods - .iter() - .map(|period| period.plays()) - .collect::>>() // 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]); + } } diff --git a/gamelog/src/lib.rs b/gamelog/src/lib.rs index 007edd5..81f722f 100644 --- a/gamelog/src/lib.rs +++ b/gamelog/src/lib.rs @@ -1,21 +1,21 @@ -#![allow(deprecated)] - mod action; mod error; mod event; mod file; mod game; mod period; +#[allow(deprecated)] mod play; mod terrain; #[allow(unused)] pub const MIN_VER: semver::Version = semver::Version::new(0, 5, 0); +// I'm lazy. pub use action::*; -pub use event::Event; -pub use file::LogFile; -pub use game::{Game, GameRecord}; +pub use event::*; +pub use file::*; +pub use game::*; pub use period::*; pub use play::*; -pub use terrain::TerrainState; +pub use terrain::*; diff --git a/gamelog/src/period.rs b/gamelog/src/period.rs index 5412afd..927045f 100644 --- a/gamelog/src/period.rs +++ b/gamelog/src/period.rs @@ -1,4 +1,4 @@ -use crate::{Event, Play, PlayHandle}; +use crate::{Event, Play, PlayHandle, Team, error}; use serde::Deserialize; #[derive(Debug, Deserialize, Clone)] @@ -24,20 +24,117 @@ impl PlayHandle for Period { } impl Period { - pub fn deltas(&self) -> Vec { - let len = self.events.len() - 1; - let mut idx: usize = 0; - let mut deltas: Vec = vec![]; + pub fn team_events( + &self, + team: Team, + assume_team_known: Option, + ) -> Result, error::CannotDetermineTeams> { + let mut events: Vec = vec![]; + let mut first = true; + let mut record: bool = true; + let assume_team_known = assume_team_known.unwrap_or(false); - while idx < len { - if let Some(value) = self.events[idx].delta(&self.events[idx + 1]) { - deltas.push(value); + for event in self.events.iter() { + 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; } - idx += 1 + if record { + events.push(event.to_owned()); + } } - deltas + // If already handled or assumption override applicable + if !first || (first && assume_team_known) { + Ok(events) + } else { + Err(error::CannotDetermineTeams) + } + } + + pub fn team_plays( + &self, + team: Team, + assume_team_known: Option, + ) -> Result, error::CannotDetermineTeams> { + Ok(self + .team_events(team, assume_team_known)? + .iter() + .filter_map(|event| { + if let Event::Play(play) = event { + Some(play.to_owned()) + } else { + None + } + }) + .collect()) + } + + pub fn quarters(&self) -> Vec { + let mut quarters: Vec = vec![self.start.to_owned()]; + + if self.end.is_none() { + return quarters; + } + + let order = vec![ + Quarter::First, + Quarter::Second, + Quarter::Third, + Quarter::Fourth, + ]; + + let start = if let Quarter::Overtime(x) = self.start { + (3 + x) as usize + } else { + order.iter().position(|q| q == &self.start).unwrap() + }; + + let end = if let Quarter::Overtime(x) = self.end.as_ref().unwrap() { + (3 + x) as usize + } else { + order + .iter() + .position(|q| q == self.end.as_ref().unwrap()) + .unwrap() + }; + + let range: Vec = ((start + 1)..=end).collect(); + + for i in range { + quarters.push(match i { + 0 => Quarter::First, + 1 => Quarter::Second, + 2 => Quarter::Third, + 3 => Quarter::Fourth, + _ => Quarter::Overtime((i - 3) as u8), + }); + } + + quarters + } + + pub fn is_overtime(&self) -> bool { + if self.start.is_overtime() || self.end.as_ref().is_some_and(|some| some.is_overtime()) { + true + } else { + false + } } } @@ -50,49 +147,158 @@ pub enum Quarter { Overtime(u8), } +impl Quarter { + pub fn is_overtime(&self) -> bool { + if let Self::Overtime(_) = self { + true + } else { + false + } + } +} + #[cfg(test)] mod tests { - use super::*; - use crate::{Action, Down, Team, TerrainState}; + use crate::*; #[test] - fn deltas() { + fn team_events() { + let a = Period { + start: Quarter::First, + end: None, + 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::Score(ScorePoints::Touchdown), + Event::Kickoff(Team::SouthCarolina), + ], + }; + + let b = Period { + start: Quarter::Second, + end: None, + events: vec![ + Event::Play(Play::default()), + Event::Turnover(Team::SouthCarolina), + ], + }; + + let c = Period { + start: Quarter::Second, + end: None, + events: vec![ + Event::Play(Play::default()), + Event::Turnover(Team::Nebraska), + ], + }; + + let d = Period { + start: Quarter::Second, + end: None, + events: vec![Event::Play(Play::default())], + }; + + assert!( + a.team_events(Team::Nebraska, None).unwrap() + == vec![ + Event::Kickoff(Team::Nebraska), + Event::Play(Play::default()), + Event::Turnover(Team::ArizonaState), + Event::Kickoff(Team::Nebraska), + Event::Score(ScorePoints::Touchdown), + Event::Kickoff(Team::SouthCarolina), + ] + ); + assert!( + b.team_events(Team::Nebraska, None).unwrap() + == vec![ + Event::Play(Play::default()), + Event::Turnover(Team::SouthCarolina) + ] + ); + assert!( + c.team_events(Team::Nebraska, None).unwrap() == vec![Event::Turnover(Team::Nebraska)] + ); + assert!(true == d.team_events(Team::Nebraska, None).is_err()); + assert!(false == d.team_events(Team::Nebraska, Some(true)).is_err()) + } + + #[test] + fn team_plays() { let period = 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::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::SouthCarolina), + Event::Play(Play::default()), Event::Turnover(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::First), - terrain: Some(TerrainState::Yards(10)), - }), + Event::Play(Play::default()), ], }; - let expected: Vec = vec![10, -3, 5, 10]; + assert!( + period.team_plays(Team::Nebraska, None).unwrap() + == vec![Play::default(), Play::default(), Play::default()] + ); + } - assert!(period.deltas() == expected) + #[test] + fn quarters() { + let first = Period { + start: Quarter::First, + end: None, + events: vec![], + }; + + let second_fourth = Period { + start: Quarter::Second, + end: Some(Quarter::Fourth), + events: vec![], + }; + + let third_ot_three = Period { + start: Quarter::Third, + end: Some(Quarter::Overtime(3)), + events: vec![], + }; + + let ot_one_three = Period { + start: Quarter::Overtime(1), + end: Some(Quarter::Overtime(3)), + events: vec![], + }; + + assert!(first.quarters() == vec![Quarter::First]); + assert!(second_fourth.quarters() == vec![Quarter::Second, Quarter::Third, Quarter::Fourth]); + assert!( + third_ot_three.quarters() + == vec![ + Quarter::Third, + Quarter::Fourth, + Quarter::Overtime(1), + Quarter::Overtime(2), + Quarter::Overtime(3) + ] + ); + assert!( + ot_one_three.quarters() + == vec![ + Quarter::Overtime(1), + Quarter::Overtime(2), + Quarter::Overtime(3) + ] + ) } } diff --git a/gamelog/src/play.rs b/gamelog/src/play.rs index 0f1610d..3c683a4 100644 --- a/gamelog/src/play.rs +++ b/gamelog/src/play.rs @@ -1,11 +1,6 @@ use crate::{Action, TerrainState}; use serde::Deserialize; -pub trait PlayHandle { - /// Returns all plays within object's scope. - fn plays(&self) -> Vec; -} - #[derive(Debug, Deserialize, Clone, PartialEq)] pub struct Play { pub action: Action, @@ -13,12 +8,6 @@ pub struct Play { pub terrain: Option, } -impl PlayHandle for Play { - fn plays(&self) -> Vec { - vec![self.to_owned()] - } -} - impl Default for Play { fn default() -> Self { Self { diff --git a/miller/Cargo.toml b/miller/Cargo.toml index 1d5f2b4..8805ca2 100644 --- a/miller/Cargo.toml +++ b/miller/Cargo.toml @@ -3,7 +3,6 @@ name = "miller" version = "0.1.0" edition = "2024" license = "MIT" -license-file = "LICENSE" [dependencies.clap] version = "4.5" diff --git a/miller/src/calculator.rs b/miller/src/calculator.rs deleted file mode 100644 index e69de29..0000000 diff --git a/miller/src/main.rs b/miller/src/main.rs index dfba545..2c55b8e 100644 --- a/miller/src/main.rs +++ b/miller/src/main.rs @@ -1,8 +1,6 @@ -mod calculator; - use clap::Parser; use core::panic; -use gamelog::LogFile; +use gamelog::{Flags, LogFile}; use std::path::PathBuf; #[derive(Debug, Parser)] @@ -12,7 +10,7 @@ struct Args { short, long, value_hint = clap::ValueHint::DirPath, - default_value = format!("{}/templates/logfile.ron", std::env::current_dir() + default_value = format!("{}/../templates/logfile.ron", std::env::current_dir() .expect("Failed to get current working dir.") .into_os_string() .to_str() @@ -24,10 +22,22 @@ struct Args { fn main() { let config = Args::parse(); - let log: LogFile = { - let file = match LogFile::try_from(config.logfile_path) { - Ok(f) => f, - Err(err) => panic!("Error: Failed to open logfile: {:?}", err), - }; + let log: LogFile = match LogFile::try_from(config.logfile_path) { + Ok(f) => f, + Err(err) => panic!("Error: Failed to open logfile: {:?}", err), }; + + for game in log.0.iter() { + if let Ok(teams) = game.teams() { + for team in teams { + if !game.flags.contains(&Flags::IgnoreTeam(team.to_owned())) { + println!( + "{:?}: {:?}", + &team, + game.avg_plays_per_quarter(team.to_owned()) + ) + } + } + } + } } diff --git a/templates/logfile.ron b/templates/logfile.ron index a12964a..bd43e30 100644 --- a/templates/logfile.ron +++ b/templates/logfile.ron @@ -4,20 +4,20 @@ [ Game( - version: "0.3.0", + version: "0.5.0", + flags: [], periods: [ Period( start: First, end: Fourth, - plays: [ + events: [ Kickoff(Nebraska), Play( action: Unknown, down: First, terrain: Yards(10) ), - Score(3), - Pat(Fail) + Score(FieldGoal), ] ) ]