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

@@ -4,12 +4,13 @@
[ [
Game( Game(
version: "0.3.0", version: "0.5.0",
flags: [IgnoreScore],
periods: [ periods: [
Period( Period(
start: First, start: First,
end: Third, end: Third,
plays: [ events: [
Kickoff(ArizonaState), Kickoff(ArizonaState),
Play( Play(
action: Unknown, action: Unknown,
@@ -47,7 +48,7 @@
terrain: Yards(7), terrain: Yards(7),
), ),
Play( Play(
action: HalfbackSweep action: HalfbackSweep,
down: Fourth, down: Fourth,
terrain: Yards(11), terrain: Yards(11),
), ),
@@ -130,7 +131,7 @@
Period( Period(
start: Fourth, start: Fourth,
end: None, end: None,
plays: [ events: [
Play( Play(
action: PlayActionComebacks, // Original note: Dupe Spike Centre action: PlayActionComebacks, // Original note: Dupe Spike Centre
down: Fourth, down: Fourth,
@@ -182,12 +183,13 @@
] ]
), ),
Game( Game(
version: "0.3.1", version: "0.5.0",
flags: [IgnoreScore],
periods: [ periods: [
Period( Period(
start: First, start: First,
end: None, end: None,
plays: [ events: [
Kickoff(Syracuse), Kickoff(Syracuse),
Play( Play(
action: Unknown, action: Unknown,
@@ -224,7 +226,7 @@
Period( Period(
start: Second, start: Second,
end: None, end: None,
plays: [ events: [
Play( Play(
action: Unknown, // Original note: PA Throw Centre action: Unknown, // Original note: PA Throw Centre
down: Second, down: Second,
@@ -254,7 +256,7 @@
action: Unknown, // HalfbackSlam? Original note: PA Rush Centre action: Unknown, // HalfbackSlam? Original note: PA Rush Centre
down: None, down: None,
terrain: None, terrain: None,
) ),
Turnover(Colorado), Turnover(Colorado),
Play( Play(
action: PlayActionComebacks, action: PlayActionComebacks,
@@ -281,7 +283,7 @@
Period( Period(
start: Third, start: Third,
end: None, end: None,
plays: [ events: [
Kickoff(Colorado), Kickoff(Colorado),
Play( Play(
action: Unknown, // Original note: Throw Right action: Unknown, // Original note: Throw Right
@@ -308,7 +310,7 @@
Period( Period(
start: Fourth, start: Fourth,
end: None, end: None,
plays: [ events: [
Play( Play(
action: CrackStudentBodyRightTackle, action: CrackStudentBodyRightTackle,
down: Third, down: Third,
@@ -319,7 +321,6 @@
down: Fourth, down: Fourth,
terrain: Yards(3), terrain: Yards(3),
), ),
Score(0),
Kickoff(Syracuse), Kickoff(Syracuse),
Play( Play(
action: Unknown, // Original note: PA Throw Centre action: Unknown, // Original note: PA Throw Centre
@@ -336,18 +337,19 @@
] ]
), ),
Game( Game(
version: "0.3.1", version: "0.5.0",
flags: [],
periods: [ periods: [
Period( Period(
start: First, start: First,
end: None, end: None,
plays: [ events: [
Kickoff(Nebraska), Kickoff(Nebraska),
Play( Play(
action: Curls, action: Curls,
down: First, down: First,
terrain: Yards(10) terrain: Yards(10)
) ),
Play( Play(
action: Unknown, action: Unknown,
down: Second, down: Second,
@@ -379,14 +381,14 @@
Period( Period(
start: Second, start: Second,
end: None, end: None,
plays: [ events: [
Play( Play(
action: Unknown, // Original note: Throw Centre action: Unknown, // Original note: Throw Centre
down: First, down: First,
terrain: GoalLine, terrain: GoalLine,
), ),
Pat(Fail), Score(Touchdown),
Score(6), Score(PatFail),
Kickoff(Nebraska), Kickoff(Nebraska),
Play( Play(
action: StrongFlood, action: StrongFlood,
@@ -429,7 +431,7 @@
Period( Period(
start: Third, start: Third,
end: None, end: None,
plays: [ events: [
Kickoff(SouthCarolina), Kickoff(SouthCarolina),
Play( Play(
action: Unknown, action: Unknown,
@@ -452,7 +454,7 @@
down: None, down: None,
terrain: None, terrain: None,
), ),
Score(3), Score(FieldGoal),
Kickoff(SouthCarolina), Kickoff(SouthCarolina),
Play( Play(
action: Unknown, action: Unknown,
@@ -469,7 +471,7 @@
Period( Period(
start: Fourth, start: Fourth,
end: None, end: None,
plays: [ events: [
Play( Play(
action: Unknown, // Original note: Throw Centre action: Unknown, // Original note: Throw Centre
down: Fourth, down: Fourth,
@@ -490,8 +492,8 @@
down: Third, down: Third,
terrain: Yards(5), terrain: Yards(5),
), ),
Pat(), // Original note: Dupe, Throw Centre Score(Touchdown),
Score(12), Score(PatFail),
Kickoff(Nebraska), Kickoff(Nebraska),
Play( Play(
action: PowerZero, action: PowerZero,
@@ -536,7 +538,7 @@
), ),
Turnover(Nebraska), Turnover(Nebraska),
// Field Goal 41 yrds // Field Goal 41 yrds
Score(6), Score(FieldGoal),
Kickoff(SouthCarolina), Kickoff(SouthCarolina),
Play( Play(
action: Unknown, action: Unknown,
@@ -548,7 +550,8 @@
] ]
), ),
Game( Game(
version: "0.4.0", version: "0.5.0",
flags: [],
periods: [ periods: [
Period( Period(
start: First, start: First,
@@ -617,8 +620,8 @@
terrain: GoalLine, terrain: GoalLine,
), ),
// Touchdown // Touchdown
Pat(One), // Throw Score(Touchdown),
Score(7), Score(PatSafety),
Kickoff(Iowa), Kickoff(Iowa),
Penalty(Yards(15)), Penalty(Yards(15)),
Play( Play(
@@ -656,7 +659,7 @@
down: First, down: First,
terrain: GoalLine, terrain: GoalLine,
), ),
Score(3), Score(FieldGoal),
] ]
), ),
Period( Period(
@@ -716,8 +719,8 @@
terrain: Yards(4), terrain: Yards(4),
), ),
// Touchdown // Touchdown
Pat(One) Score(Touchdown),
Score(7), Score(PatSafety),
Kickoff(Colorado), Kickoff(Colorado),
Play( Play(
action: Unknown, // Original note: Dupe, Throw Left action: Unknown, // Original note: Dupe, Throw Left
@@ -752,13 +755,14 @@
terrain: Yards(15), terrain: Yards(15),
), ),
//Field Goal //Field Goal
Score(10) Score(FieldGoal)
] ]
) )
] ]
), ),
Game( Game(
version: "0.5.0", version: "0.5.0",
flags: [],
periods: [ periods: [
Period( Period(
start: First, start: First,
@@ -878,7 +882,7 @@
terrain:Yards(10), terrain:Yards(10),
), ),
Play( Play(
action: //IForm Normal, Thrown action: Unknown,//IForm Normal, Thrown
down: None, down: None,
terrain: None terrain: None
), ),
@@ -985,6 +989,7 @@
// TexasAnM were opponents, but not recorded as // TexasAnM were opponents, but not recorded as
// they were not present; Miller played in place. // they were not present; Miller played in place.
version: "0.5.0", version: "0.5.0",
flags: [IgnoreTeam(TexasAnM)],
periods: [ periods: [
Period( Period(
start: First, start: First,
@@ -1053,7 +1058,7 @@
terrain: Yards(13) terrain: Yards(13)
), ),
Play( Play(
action: First, action: Unknown,
down: First, down: First,
terrain: Yards(10) terrain: Yards(10)
), ),

View File

@@ -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.")
}
}

View File

@@ -1,9 +1,9 @@
use crate::{Down, Play, Team, TerrainState}; use crate::{Down, Play, Team, TerrainState, error};
use serde::Deserialize; use serde::Deserialize;
type Offence = Team; type Offence = Team;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum Event { pub enum Event {
Play(Play), Play(Play),
Kickoff(Offence), Kickoff(Offence),
@@ -18,7 +18,7 @@ impl Event {
fn make_play(event: &Event) -> Option<Play> { fn make_play(event: &Event) -> Option<Play> {
match event { match event {
Event::Kickoff(_) => Some(Play::default()), Event::Kickoff(_) | Event::Turnover(_) => Some(Play::default()),
Event::Play(play) => { Event::Play(play) => {
let p = play.to_owned(); let p = play.to_owned();
@@ -36,7 +36,13 @@ impl Event {
} }
let preceeding = make_play(self)?; 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 following.down? == Down::First {
if let TerrainState::Yards(yrds) = preceeding.terrain? { if let TerrainState::Yards(yrds) = preceeding.terrain? {
@@ -60,6 +66,14 @@ impl Event {
Some(a as i8 - b as i8) Some(a as i8 - b as i8)
} }
} }
pub fn team(&self) -> Result<Team, error::NoTeamAttribute> {
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)] #[derive(Debug, Deserialize, Clone, PartialEq, Default)]
@@ -90,45 +104,54 @@ impl ScorePoints {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use crate::*;
use crate::{Action, Down, Team, TerrainState};
#[test] #[test]
fn delta() { fn delta() {
let kickoff = Event::Kickoff(Team::Nebraska); let kickoff = Event::Kickoff(Team::Nebraska);
let first_down = Event::Play(Play { let first_down = Event::Play(Play {
action: Action::Unknown, action: Action::Unknown,
down: Some(Down::First), down: Some(Down::First),
terrain: Some(TerrainState::Yards(10)), terrain: Some(TerrainState::Yards(10)),
}); });
let second_down = Event::Play(Play { let second_down = Event::Play(Play {
action: Action::Unknown, action: Action::Unknown,
down: Some(Down::Second), down: Some(Down::Second),
terrain: Some(TerrainState::Yards(10)), terrain: Some(TerrainState::Yards(10)),
}); });
let third_down = Event::Play(Play { let third_down = Event::Play(Play {
action: Action::Unknown, action: Action::Unknown,
down: Some(Down::Third), down: Some(Down::Third),
terrain: Some(TerrainState::Yards(13)), terrain: Some(TerrainState::Yards(13)),
}); });
let fourth_down = Event::Play(Play { let fourth_down = Event::Play(Play {
action: Action::Unknown, action: Action::Unknown,
down: Some(Down::Fourth), down: Some(Down::Fourth),
terrain: Some(TerrainState::Yards(5)), terrain: Some(TerrainState::Yards(5)),
}); });
let penalty = Event::Penalty(TerrainState::Yards(15)); let penalty = Event::Penalty(TerrainState::Yards(15));
let turnover = Event::Turnover(Team::Nebraska); let turnover = Event::Turnover(Team::Nebraska);
let noned_down = Event::Play(Play { let noned_down = Event::Play(Play {
action: Action::Unknown, action: Action::Unknown,
down: None, down: None,
terrain: None, terrain: None,
}); });
let score = Event::Score(ScorePoints::default()); let score = Event::Score(ScorePoints::default());
let goal_line = Event::Play(Play { let goal_line = Event::Play(Play {
action: Action::Unknown, action: Action::Unknown,
down: Some(Down::First), down: Some(Down::First),
terrain: Some(TerrainState::GoalLine), terrain: Some(TerrainState::GoalLine),
}); });
let inches = Event::Play(Play { let inches = Event::Play(Play {
action: Action::Unknown, action: Action::Unknown,
down: Some(Down::First), down: Some(Down::First),
@@ -155,5 +178,8 @@ mod tests {
assert!(None == goal_line.delta(&first_down)); assert!(None == goal_line.delta(&first_down));
assert!(None == inches.delta(&first_down)); assert!(None == inches.delta(&first_down));
assert!(None == goal_line.delta(&inches)); 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());
} }
} }

View File

@@ -3,7 +3,7 @@ use serde::Deserialize;
use std::{fs::File, path::PathBuf}; use std::{fs::File, path::PathBuf};
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct LogFile(Vec<super::Game>); pub struct LogFile(pub Vec<super::Game>);
impl LogFile { impl LogFile {
pub fn min_ver(&self) -> semver::Version { pub fn min_ver(&self) -> semver::Version {

View File

@@ -1,58 +1,356 @@
use crate::{Event, Period, Play, PlayHandle, Team, error}; use crate::{Event, Period, Team, error};
use serde::Deserialize; use serde::Deserialize;
#[deprecated(since = "0.2.0", note = "Migrated to Game")]
pub type GameRecord = Game;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct Game { pub struct Game {
pub version: semver::Version, pub version: semver::Version,
pub flags: Vec<FeatureFlags>, pub flags: Vec<Flags>,
pub periods: Vec<Period>, pub periods: Vec<Period>,
} }
impl Game { impl Game {
/// Returns the teams of this game. /// Returns the teams of this game.
pub fn teams(&self) -> Result<Vec<Team>, error::TeamsError> { 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![]; let mut teams = vec![];
self.periods.iter().for_each(|period| { self.periods.iter().for_each(|period| {
period.events.iter().for_each(|event| { for event in period.events.iter() {
if let Event::Kickoff(t) | Event::Turnover(t) = event { if let Ok(team) = event.team() {
if teams.contains(t) { if !ignore.contains(&team) && !teams.contains(&team) {
teams.push(t.to_owned()) teams.push(team)
}
} }
} }
})
}); });
if teams.len() == 2 { if teams.len() == 2 || ignore.len() != 0 {
Ok(teams) Ok(teams)
} else { } else {
Err(error::TeamsError::NumberFound(teams.len())) Err(error::TeamsError::NumberFound(teams.len()))
} }
} }
pub fn deltas(&self) -> Vec<i8> { pub fn deltas(&self, team: Team) -> Vec<i8> {
self.periods let events = self
.periods
.iter() .iter()
.map(|period| period.deltas()) .filter_map(|period| Some(period.team_events(team.to_owned(), None).ok().unwrap()))
.collect::<Vec<Vec<i8>>>() .collect::<Vec<Vec<Event>>>()
.concat() .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 { #[derive(Debug, Deserialize, Clone, PartialEq)]
fn plays(&self) -> Vec<Play> { pub enum Flags {
self.periods IgnoreTeam(Team),
.iter() IgnoreScore,
.map(|period| period.plays())
.collect::<Vec<Vec<Play>>>() // Make compiler happy with turbofish.
.concat()
}
} }
#[derive(Debug, Deserialize, Clone)] #[cfg(test)]
pub enum FeatureFlags { mod tests {
Ignore(Team), 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]);
}
} }

View File

@@ -1,21 +1,21 @@
#![allow(deprecated)]
mod action; mod action;
mod error; mod error;
mod event; mod event;
mod file; mod file;
mod game; mod game;
mod period; mod period;
#[allow(deprecated)]
mod play; mod play;
mod terrain; mod terrain;
#[allow(unused)] #[allow(unused)]
pub const MIN_VER: semver::Version = semver::Version::new(0, 5, 0); pub const MIN_VER: semver::Version = semver::Version::new(0, 5, 0);
// I'm lazy.
pub use action::*; pub use action::*;
pub use event::Event; pub use event::*;
pub use file::LogFile; pub use file::*;
pub use game::{Game, GameRecord}; pub use game::*;
pub use period::*; pub use period::*;
pub use play::*; pub use play::*;
pub use terrain::TerrainState; pub use terrain::*;

View File

@@ -1,4 +1,4 @@
use crate::{Event, Play, PlayHandle}; use crate::{Event, Play, PlayHandle, Team, error};
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@@ -24,20 +24,117 @@ impl PlayHandle for Period {
} }
impl Period { impl Period {
pub fn deltas(&self) -> Vec<i8> { pub fn team_events(
let len = self.events.len() - 1; &self,
let mut idx: usize = 0; team: Team,
let mut deltas: Vec<i8> = vec![]; assume_team_known: Option<bool>,
) -> Result<Vec<Event>, error::CannotDetermineTeams> {
let mut events: Vec<Event> = vec![];
let mut first = true;
let mut record: bool = true;
let assume_team_known = assume_team_known.unwrap_or(false);
while idx < len { for event in self.events.iter() {
if let Some(value) = self.events[idx].delta(&self.events[idx + 1]) { if let Event::Kickoff(_) | Event::Turnover(_) = event {
deltas.push(value); record = {
if team == event.team().unwrap() {
// Wipe events vec if the start of quarter was opposition
// on offence.
if first {
events = vec![];
} }
idx += 1 true
} else {
events.push(event.to_owned());
false
}
};
first = false;
} }
deltas if record {
events.push(event.to_owned());
}
}
// 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<bool>,
) -> Result<Vec<Play>, 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<Quarter> {
let mut quarters: Vec<Quarter> = 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<usize> = ((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), Overtime(u8),
} }
impl Quarter {
pub fn is_overtime(&self) -> bool {
if let Self::Overtime(_) = self {
true
} else {
false
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use crate::*;
use crate::{Action, Down, Team, TerrainState};
#[test] #[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 { let period = Period {
start: Quarter::First, start: Quarter::First,
end: None, end: None,
events: vec![ events: vec![
Event::Kickoff(Team::Nebraska), Event::Kickoff(Team::Nebraska),
Event::Play(Play { Event::Play(Play::default()),
action: Action::Unknown, Event::Turnover(Team::ArizonaState),
down: Some(Down::First), Event::Play(Play::default()),
terrain: Some(TerrainState::Yards(10)), Event::Play(Play::default()),
}), Event::Kickoff(Team::Nebraska),
Event::Play(Play { Event::Play(Play::default()),
action: Action::Unknown, Event::Score(ScorePoints::default()),
down: Some(Down::Second), Event::Kickoff(Team::SouthCarolina),
terrain: Some(TerrainState::Yards(13)), Event::Play(Play::default()),
}),
Event::Play(Play {
action: Action::Unknown,
down: Some(Down::Third),
terrain: Some(TerrainState::Yards(8)),
}),
Event::Turnover(Team::Nebraska), Event::Turnover(Team::Nebraska),
Event::Play(Play { Event::Play(Play::default()),
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)),
}),
], ],
}; };
let expected: Vec<i8> = 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)
]
)
} }
} }

View File

@@ -1,11 +1,6 @@
use crate::{Action, TerrainState}; use crate::{Action, TerrainState};
use serde::Deserialize; use serde::Deserialize;
pub trait PlayHandle {
/// Returns all plays within object's scope.
fn plays(&self) -> Vec<Play>;
}
#[derive(Debug, Deserialize, Clone, PartialEq)] #[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct Play { pub struct Play {
pub action: Action, pub action: Action,
@@ -13,12 +8,6 @@ pub struct Play {
pub terrain: Option<TerrainState>, pub terrain: Option<TerrainState>,
} }
impl PlayHandle for Play {
fn plays(&self) -> Vec<Play> {
vec![self.to_owned()]
}
}
impl Default for Play { impl Default for Play {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@@ -3,7 +3,6 @@ name = "miller"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
license-file = "LICENSE"
[dependencies.clap] [dependencies.clap]
version = "4.5" version = "4.5"

View File

@@ -1,8 +1,6 @@
mod calculator;
use clap::Parser; use clap::Parser;
use core::panic; use core::panic;
use gamelog::LogFile; use gamelog::{Flags, LogFile};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@@ -12,7 +10,7 @@ struct Args {
short, short,
long, long,
value_hint = clap::ValueHint::DirPath, 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.") .expect("Failed to get current working dir.")
.into_os_string() .into_os_string()
.to_str() .to_str()
@@ -24,10 +22,22 @@ struct Args {
fn main() { fn main() {
let config = Args::parse(); let config = Args::parse();
let log: LogFile = { let log: LogFile = match LogFile::try_from(config.logfile_path) {
let file = match LogFile::try_from(config.logfile_path) {
Ok(f) => f, Ok(f) => f,
Err(err) => panic!("Error: Failed to open logfile: {:?}", err), 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())
)
}
}
}
}
} }

View File

@@ -4,20 +4,20 @@
[ [
Game( Game(
version: "0.3.0", version: "0.5.0",
flags: [],
periods: [ periods: [
Period( Period(
start: First, start: First,
end: Fourth, end: Fourth,
plays: [ events: [
Kickoff(Nebraska), Kickoff(Nebraska),
Play( Play(
action: Unknown, action: Unknown,
down: First, down: First,
terrain: Yards(10) terrain: Yards(10)
), ),
Score(3), Score(FieldGoal),
Pat(Fail)
] ]
) )
] ]