Finish tasks to bring calculation capabilities in line with last interval. #9

Merged
Cutieguwu merged 19 commits from gamelog into main 2025-04-06 12:20:32 -04:00
19 changed files with 2421 additions and 421 deletions

View File

@@ -14,12 +14,43 @@ I figured, that since I already had to digitize every note, that I was required
== Goals
=== Gamelog
* [*] Data Format
** [*] Support recording multiple games
** [*] Periods
*** Quarters
*** Overtime
** [*] Teams
*** Iowa
*** Syracuse
*** Texas A&M
*** Colorado
*** Nebraska
*** Boise State [deprecated]
*** Arizona State
*** South Carolina
** [*] Downs
*** First - Fourth
*** Kickoff
*** PAT (Point After Touchdown)
**** One
**** Two
**** Failed
** [*] Score
** [*] Terrain Position
*** [*] Yards
*** [*] In (Inches)
*** [*] GL (Goal Line)
** [*] Penalty
** Out?
** [*] Plays
=== Miller:
* [ ] Mathematics
** [ ] Avg. Terrain Gain
** [ ] Avg. Terrain Loss
** [ ] Avg. Terrain Delta
** [ ] Avg. Offence Plays per quarter
** [*] Avg. Terrain Gain
** [*] Avg. Terrain Loss
** [*] Avg. Terrain Delta
** [*] Avg. Offence Plays per quarter
** [ ] Avg. Offence Plays per game
** [ ] Avg. Penalties per game
* [ ] Play Trend Analysis

File diff suppressed because it is too large Load Diff

37
gamelog/Cargo.lock generated
View File

@@ -19,13 +19,20 @@ dependencies = [
[[package]]
name = "gamelog"
version = "0.3.0"
version = "0.5.0"
dependencies = [
"ron",
"semver",
"serde",
"strum",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "proc-macro2"
version = "1.0.94"
@@ -57,6 +64,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "semver"
version = "1.0.26"
@@ -86,6 +99,28 @@ dependencies = [
"syn",
]
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.100"

View File

@@ -1,11 +1,15 @@
[package]
name = "gamelog"
version = "0.3.0"
version = "0.5.0"
edition = "2024"
[dependencies]
ron = "0.9"
[dependencies.strum]
version = "0.27"
features = ["derive"]
[dependencies.semver]
version = "1.0"
features = ["serde"]

138
gamelog/src/action.rs Normal file
View File

@@ -0,0 +1,138 @@
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone, Default, PartialEq)]
pub enum Action {
CrackStudentBodyRightTackle,
Curls,
FleaFlicker,
HalfbackSlam,
HalfbackSlipScreen,
HalfbackSweep,
Mesh,
PlayActionBoot,
PlayActionComebacks,
PlayActionPowerZero,
PowerZero,
SlantBubble,
SlotOut,
SpeedOption,
StrongFlood,
#[default]
Unknown,
}
impl Action {
// I'd love a better way of doing these
// Attributes are probably the way to go,
// but I'm not about to write procedural macros for this project.
/// Returns `true` if `self` is a play action.
pub fn is_play_action(&self) -> bool {
if let Self::PlayActionBoot | Self::PlayActionComebacks | Self::PlayActionPowerZero = self {
true
} else {
false
}
}
/// Returns `true` if `self` is a halfback.
pub fn is_halfback(&self) -> bool {
if let Self::HalfbackSlam | Self::HalfbackSlipScreen | Self::HalfbackSweep = self {
true
} else {
true
}
}
/// Returns `true` if `self` is a running play.
pub fn is_run(&self) -> bool {
if let Self::HalfbackSlam
| Self::SpeedOption
| Self::HalfbackSweep
| Self::PowerZero
| Self::CrackStudentBodyRightTackle = self
{
true
} else {
false
}
}
/// Returns `true` if `self` is a passing play.
pub fn is_pass(&self) -> bool {
!self.is_run()
}
/// Returns `true` if `self` is `Event::Unknown`.
pub fn is_unknown(&self) -> bool {
if let Self::Unknown = self {
true
} else {
false
}
}
/// Returns the `Playset` that this action belongs to.
/// Returns `None` if `Event::Unknown`
pub fn playset(&self) -> Option<Playset> {
if self.is_unknown() {
return None;
}
Some(match self {
Self::SlantBubble | Self::HalfbackSlam | Self::PlayActionBoot => Playset::PistolSpread,
Self::StrongFlood | Self::SpeedOption | Self::HalfbackSlipScreen => {
Playset::ShotgunTripleWingsOffset
}
Self::SlotOut | Self::HalfbackSweep | Self::PlayActionComebacks => {
Playset::ShotgunDoubleFlex
}
Self::Curls | Self::PowerZero | Self::PlayActionPowerZero => Playset::IFormNormal,
Self::Mesh | Self::CrackStudentBodyRightTackle | Self::FleaFlicker => {
Playset::IFormTight
}
_ => unreachable!(),
})
}
/// Returns the `Key` that this action belongs to.
/// Returns `None` if `Event::Unknown`
pub fn key(&self) -> Option<Key> {
if self.is_unknown() {
return None;
}
// All running plays are on `Key::X`
if self.is_run() {
return Some(Key::X);
}
Some(match self {
Self::SlantBubble | Self::StrongFlood | Self::SlotOut | Self::Curls | Self::Mesh => {
Key::Square
}
Self::PlayActionBoot
| Self::HalfbackSlipScreen
| Self::PlayActionComebacks
| Self::PlayActionPowerZero
| Self::FleaFlicker => Key::Triangle,
_ => unreachable!(),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Playset {
PistolSpread,
ShotgunTripleWingsOffset,
ShotgunDoubleFlex,
IFormNormal,
IFormTight,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Key {
Square,
X,
Triangle,
}

View File

@@ -4,32 +4,44 @@ use std::{fmt, io};
pub enum LogFileError {
FailedToOpen(io::Error),
RonSpannedError(ron::error::SpannedError),
CompatibilityCheck(semver::Version),
}
impl fmt::Display for LogFileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FailedToOpen(err) => write!(f, "{}", err),
Self::CompatibilityCheck(ver) => write!(
f,
"GameLogs cannot be older than {}, but {} was found in logfile.",
crate::MIN_VER.to_string(),
ver.to_string()
),
Self::RonSpannedError(err) => write!(f, "{}", err),
}
}
}
pub enum DownError {
NotKickoff,
#[derive(Debug)]
pub enum TeamsError {
NumberFound(usize),
}
impl fmt::Display for DownError {
impl fmt::Display for TeamsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::NotKickoff => write!(f, "Variant was not Down::Kickoff."),
match self {
Self::NumberFound(err) => write!(f, "Expected two, found: {:?}", err),
}
}
}
#[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.")
}
}

258
gamelog/src/event.rs Normal file
View File

@@ -0,0 +1,258 @@
use crate::{Down, Play, Team, TerrainState, error};
use serde::Deserialize;
type Offence = Team;
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum Event {
Kickoff(Offence),
Play(Play),
Turnover(Offence),
Penalty(TerrainState),
Score(ScorePoints),
}
impl Event {
pub fn delta(&self, following: &Self) -> Option<i8> {
// Clean this trash spaghetti code up.
fn make_play(event: &Event) -> Option<Play> {
match event {
Event::Kickoff(_) | Event::Turnover(_) => Some(Play::default()),
Event::Play(play) => {
let p = play.to_owned();
if p.down.is_none()
|| p.terrain.is_none()
|| p.terrain.as_ref()? == &TerrainState::Unknown
{
None
} else {
Some(p)
}
}
_ => None,
}
}
let preceeding = make_play(self)?;
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? {
Some(yrds as i8)
} else {
None
}
} else {
let a = if let TerrainState::Yards(yrds) = preceeding.terrain? {
yrds
} else {
0_u8
};
let b = if let TerrainState::Yards(yrds) = following.terrain? {
yrds
} else {
0_u8
};
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)]
pub enum ScorePoints {
#[default]
Touchdown,
FieldGoal,
Safety,
PatFail,
PatTouchdown,
PatFieldGoal,
PatSafety,
}
impl ScorePoints {
pub fn to_points(&self) -> u8 {
match &self {
Self::Touchdown => 6,
Self::FieldGoal => 3,
Self::Safety => 2,
Self::PatFail => 0,
Self::PatTouchdown => 2,
Self::PatFieldGoal => 1,
Self::PatSafety => 1,
}
}
}
#[cfg(test)]
mod tests {
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),
terrain: Some(TerrainState::Inches),
});
assert!(10_i8 == kickoff.delta(&first_down).unwrap());
assert!(0_i8 == kickoff.delta(&second_down).unwrap());
assert!(None == kickoff.delta(&penalty));
assert!(None == kickoff.delta(&score));
assert!(10_i8 == first_down.delta(&kickoff).unwrap());
assert!(10_i8 == first_down.delta(&first_down).unwrap());
assert!(0_i8 == first_down.delta(&second_down).unwrap());
assert!(None == first_down.delta(&turnover));
assert!(None == first_down.delta(&penalty));
assert!(None == first_down.delta(&score));
assert!(10_i8 == first_down.delta(&goal_line).unwrap());
assert!(10_i8 == first_down.delta(&inches).unwrap());
assert!(None == first_down.delta(&noned_down));
assert!(10_i8 == second_down.delta(&kickoff).unwrap());
assert!(10_i8 == second_down.delta(&first_down).unwrap());
assert!(-3_i8 == second_down.delta(&third_down).unwrap());
assert!(None == second_down.delta(&turnover));
assert!(None == second_down.delta(&penalty));
assert!(None == second_down.delta(&score));
assert!(10_i8 == second_down.delta(&goal_line).unwrap());
assert!(10_i8 == second_down.delta(&inches).unwrap());
assert!(None == second_down.delta(&noned_down));
assert!(13_i8 == third_down.delta(&kickoff).unwrap());
assert!(13_i8 == third_down.delta(&first_down).unwrap());
assert!(8_i8 == third_down.delta(&fourth_down).unwrap());
assert!(None == third_down.delta(&turnover));
assert!(None == third_down.delta(&penalty));
assert!(None == third_down.delta(&score));
assert!(13_i8 == third_down.delta(&goal_line).unwrap());
assert!(13_i8 == third_down.delta(&inches).unwrap());
assert!(None == third_down.delta(&noned_down));
assert!(5_i8 == fourth_down.delta(&kickoff).unwrap());
assert!(5_i8 == fourth_down.delta(&first_down).unwrap());
assert!(None == fourth_down.delta(&turnover));
assert!(None == fourth_down.delta(&penalty));
assert!(None == fourth_down.delta(&score));
assert!(5_i8 == fourth_down.delta(&goal_line).unwrap());
assert!(5_i8 == fourth_down.delta(&inches).unwrap());
assert!(None == fourth_down.delta(&noned_down));
assert!(10_i8 == turnover.delta(&first_down).unwrap());
assert!(0_i8 == turnover.delta(&second_down).unwrap());
assert!(None == turnover.delta(&turnover));
assert!(None == turnover.delta(&penalty));
assert!(None == turnover.delta(&score));
assert!(10_i8 == turnover.delta(&goal_line).unwrap());
assert!(10_i8 == turnover.delta(&inches).unwrap());
assert!(None == turnover.delta(&noned_down));
assert!(None == score.delta(&kickoff));
assert!(None == score.delta(&first_down));
assert!(None == score.delta(&second_down));
assert!(None == score.delta(&third_down));
assert!(None == score.delta(&fourth_down));
assert!(None == score.delta(&turnover));
assert!(None == score.delta(&penalty));
assert!(None == score.delta(&goal_line));
assert!(None == score.delta(&inches));
assert!(None == score.delta(&score));
assert!(None == goal_line.delta(&kickoff));
assert!(None == goal_line.delta(&first_down));
assert!(-10_i8 == goal_line.delta(&second_down).unwrap());
assert!(-13_i8 == goal_line.delta(&third_down).unwrap());
assert!(-5_i8 == goal_line.delta(&fourth_down).unwrap());
assert!(None == goal_line.delta(&turnover));
assert!(None == goal_line.delta(&penalty));
assert!(None == goal_line.delta(&goal_line));
assert!(None == goal_line.delta(&inches));
assert!(None == goal_line.delta(&score));
assert!(None == inches.delta(&kickoff));
assert!(None == inches.delta(&first_down));
assert!(-10_i8 == goal_line.delta(&second_down).unwrap());
assert!(-13_i8 == goal_line.delta(&third_down).unwrap());
assert!(-5_i8 == goal_line.delta(&fourth_down).unwrap());
assert!(None == inches.delta(&turnover));
assert!(None == inches.delta(&penalty));
assert!(None == inches.delta(&goal_line));
assert!(None == inches.delta(&inches));
assert!(None == inches.delta(&score));
assert!(None == noned_down.delta(&kickoff));
assert!(None == noned_down.delta(&first_down));
assert!(None == noned_down.delta(&second_down));
assert!(None == noned_down.delta(&third_down));
assert!(None == noned_down.delta(&fourth_down));
assert!(None == noned_down.delta(&turnover));
assert!(None == noned_down.delta(&penalty));
assert!(None == noned_down.delta(&goal_line));
assert!(None == noned_down.delta(&inches));
assert!(None == noned_down.delta(&score));
}
}

View File

@@ -3,13 +3,34 @@ use serde::Deserialize;
use std::{fs::File, path::PathBuf};
#[derive(Debug, Deserialize, Clone)]
pub struct LogFile(Vec<super::Game>);
pub struct LogFile(pub Vec<super::Game>);
impl LogFile {
pub fn min_ver(&self) -> semver::Version {
let mut lowest = semver::Version::new(u64::MAX, u64::MAX, u64::MAX);
self.0.iter().for_each(|x| {
if x.version.cmp_precedence(&lowest).is_lt() {
lowest = x.version.clone()
}
});
lowest
}
/// Returns if the LogFile min version is compatible.
pub fn is_compatible(&self) -> bool {
self.min_ver().cmp_precedence(&crate::MIN_VER).is_lt()
}
}
impl TryFrom<File> for LogFile {
type Error = ron::error::SpannedError;
fn try_from(file: File) -> Result<Self, Self::Error> {
ron::de::from_reader(file)
ron::Options::default()
.with_default_extension(ron::extensions::Extensions::EXPLICIT_STRUCT_NAMES)
.from_reader(file)
}
}
@@ -31,34 +52,3 @@ impl TryFrom<PathBuf> for LogFile {
}
}
}
impl LogFile {
pub fn get_min_ver(self) -> semver::Version {
let mut lowest = semver::Version::new(u64::MAX, u64::MAX, u64::MAX);
self.0.iter().for_each(|x| {
if x.version.cmp_precedence(&lowest).is_lt() {
lowest = x.version.clone()
}
});
lowest
}
/// Returns if the LogFile min version is compatible.
fn is_compatible(&self) -> bool {
self.clone()
.get_min_ver()
.cmp_precedence(&super::MIN_VER)
.is_lt()
}
/// Ensures that the returned gamefile is compatible, else returns Error.
pub fn ensure_compatible(self) -> Result<Self, error::LogFileError> {
if self.is_compatible() {
Ok(self)
} else {
Err(error::LogFileError::CompatibilityCheck(self.get_min_ver()))
}
}
}

409
gamelog/src/game.rs Normal file
View File

@@ -0,0 +1,409 @@
use crate::{Event, Period, Team, error};
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct Game {
pub version: semver::Version,
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| {
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 || ignore.len() != 0 {
Ok(teams)
} else {
Err(error::TeamsError::NumberFound(teams.len()))
}
}
pub fn deltas(&self, team: Team) -> Vec<i8> {
let events = self
.periods
.iter()
.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![];
while idx < len {
if let Some(value) = events[idx].delta(&events[idx + 1]) {
deltas.push(value);
}
idx += 1
}
deltas
}
pub fn team_plays(&self, team: Team) -> 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>>()
.iter()
.sum::<usize>()
}
/// 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>>();
quarterly_avgs.iter().sum::<f32>() / quarterly_avgs.len() as f32
}
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
}
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
}
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
}
pub fn penalties(&self, team: Team) -> usize {
self.periods
.iter()
.filter_map(|period| {
Some(
period
.team_events(team.to_owned(), None)
.ok()?
.iter()
.filter_map(|event| {
if let Event::Penalty(_) = event {
Some(event.to_owned())
} else {
None
}
})
.collect::<Vec<Event>>(),
)
})
.collect::<Vec<Vec<Event>>>()
.concat()
.len()
}
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum Flags {
IgnoreTeam(Team),
IgnoreScore,
}
#[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]);
}
}

View File

@@ -1,13 +1,21 @@
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, 3, 0);
pub const MIN_VER: semver::Version = semver::Version::new(0, 5, 0);
pub use file::LogFile;
// I'm lazy.
pub use action::*;
pub use event::*;
pub use file::*;
pub use game::*;
pub use period::*;
pub use play::*;
pub use terrain::TerrainState;
pub use terrain::*;

View File

@@ -1,22 +1,129 @@
use crate::{Event, Play, Team, error};
use serde::Deserialize;
#[deprecated(since = "0.2.0", note = "Migrated to Game")]
type GameRecord = Game;
#[derive(Debug, Deserialize, Clone)]
pub struct Game {
pub version: semver::Version,
periods: Vec<Option<Period>>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Period {
start: Quarter,
end: Option<Quarter>,
plays: Vec<super::Play>,
pub start: Quarter,
pub end: Option<Quarter>,
pub events: Vec<Event>,
}
#[derive(Debug, Deserialize, Clone)]
impl Period {
pub fn team_events(
&self,
team: Team,
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);
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;
}
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
}
}
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum Quarter {
First,
Second,
@@ -24,3 +131,159 @@ pub enum Quarter {
Fourth,
Overtime(u8),
}
impl Quarter {
pub fn is_overtime(&self) -> bool {
if let Self::Overtime(_) = self {
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
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::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::default()),
],
};
assert!(
period.team_plays(Team::Nebraska, None).unwrap()
== vec![Play::default(), Play::default(), Play::default()]
);
}
#[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,57 +1,33 @@
use crate::{TerrainState, error};
use crate::{Action, TerrainState};
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct Play {
down: Down,
terrain: TerrainState,
pub action: Action,
pub down: Option<Down>,
pub terrain: Option<TerrainState>,
}
type Offence = Team;
impl Offence {}
#[derive(Debug, Deserialize, Clone)]
pub enum Event {
CrackStudentBodyRightTackle(Play),
Curls(Play),
FleaFlicker(Play),
HalfbackSlam(Play),
HalfbackSlipScreen(Play),
HalfbackSweep(Play),
Mesh(Play),
PlayActionBoot(Play),
PlayActionComebacks(Play),
PlayActionPowerZero(Play),
PowerZero(Play),
SlantBubble(Play),
SlotOut(Play),
SpeedOption(Play),
StrongFlood(Play),
Unknown(Play),
Kickoff { offence: Team },
Turnover { offence: Team },
Penalty { terrain: TerrainState },
}
#[derive(Debug, Deserialize, Clone)]
pub enum Down {
First,
Second,
Third,
Fourth,
PointAfterTouchdown,
}
impl Down {
fn get_offence(&self) -> Result<&Team, error::DownError> {
match self {
Self::Kickoff { offence } => Ok(offence),
_ => Err(error::DownError::NotKickoff),
impl Default for Play {
fn default() -> Self {
Self {
action: Action::default(),
down: Some(Down::First),
terrain: Some(TerrainState::Yards(10)),
}
}
}
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Clone, Default, PartialEq)]
pub enum Down {
#[default]
First,
Second,
Third,
Fourth,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum Team {
ArizonaState,
#[deprecated(since = "0.2.0", note = "Team left the project.")]

View File

@@ -1,11 +1,10 @@
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Clone, Default, PartialEq)]
pub enum TerrainState {
#[deprecated(since = "0.2.0", note = "Replaced in favour of TerrainState::Yards")]
Distance(u8),
Yards(u8),
GoalLine,
Inches,
#[default]
Unknown,
}

View File

31
miller/Cargo.lock generated
View File

@@ -64,11 +64,12 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "gamelog"
version = "0.3.0"
version = "0.5.0"
dependencies = [
"ron",
"semver",
"serde",
"strum",
]
[[package]]
@@ -116,6 +117,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "semver"
version = "1.0.26"
@@ -151,6 +158,28 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.100"

View File

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

View File

@@ -1,38 +1,140 @@
mod calculator;
use clap::Parser;
use clap::{ArgAction, Parser};
use core::panic;
use gamelog::LogFile;
use gamelog::{Action, Flags, Key, LogFile, Team};
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[clap(author, version, about)]
struct Args {
/// Path to source file or block device
#[arg(
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()
.unwrap())
)]
logfile_path: PathBuf,
// Behaviour is backwards.
// ArgAction::SetFalse by default evaluates to true,
// ArgAction::SetTrue by default evaluates to false.
#[arg(short, long, action=ArgAction::SetFalse)]
display_results: bool,
}
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),
};
match file.ensure_compatible() {
Ok(f) => f,
Err(err) => panic!("Error: Failed to ensure logfile compatibility: {:?}", err),
}
let log: LogFile = match LogFile::try_from(config.logfile_path) {
Ok(f) => f,
Err(err) => panic!("Error: Failed to open logfile: {:?}", err),
};
let mut stats = vec![
TeamStats::new(Team::ArizonaState),
#[allow(deprecated)]
TeamStats::new(Team::BoiseState),
TeamStats::new(Team::Colorado),
TeamStats::new(Team::Iowa),
TeamStats::new(Team::Nebraska),
TeamStats::new(Team::Syracuse),
TeamStats::new(Team::SouthCarolina),
TeamStats::new(Team::TexasAnM),
];
// Work on knocking down the nesting here?
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())) {
// Team is to have their stats recorded this game of file.
let team_idx = stats
.iter()
.position(|stat| {
if stat.team == team.to_owned() {
true
} else {
false
}
})
.unwrap();
stats[team_idx]
.avg_terrain_gain
.push(game.avg_gain(team.to_owned()));
stats[team_idx]
.avg_terrain_loss
.push(game.avg_loss(team.to_owned()));
stats[team_idx]
.avg_terrain_delta
.push(game.avg_delta(team.to_owned()));
stats[team_idx]
.plays_per_quarter
.push(game.avg_plays_per_quarter(team.to_owned()));
stats[team_idx]
.plays_per_game
.push(game.team_plays(team.to_owned()));
stats[team_idx]
.penalties_per_game
.push(game.penalties(team.to_owned()));
}
}
}
}
if dbg!(config.display_results) {
// :#? for pretty-printing.
stats.iter().for_each(|team| println!("{:#?}", team));
}
}
#[derive(Debug)]
struct TeamStats {
team: gamelog::Team,
// Terrain
avg_terrain_gain: Vec<f32>,
avg_terrain_loss: Vec<f32>,
avg_terrain_delta: Vec<f32>,
// Play rate
plays_per_quarter: Vec<f32>,
plays_per_game: Vec<usize>,
// Penalties
penalties_per_game: Vec<usize>,
// Score
points_per_quarter: Vec<u8>,
points_per_game: Vec<u8>,
// Biases
most_common_play: Option<Action>,
least_common_play: Option<Action>,
most_common_key: Option<Key>,
least_common_key: Option<Key>,
}
impl TeamStats {
fn new(team: Team) -> Self {
TeamStats {
team,
avg_terrain_gain: vec![],
avg_terrain_loss: vec![],
avg_terrain_delta: vec![],
plays_per_quarter: vec![],
plays_per_game: vec![],
penalties_per_game: vec![],
points_per_quarter: vec![],
points_per_game: vec![],
most_common_play: None,
least_common_play: None,
most_common_key: None,
least_common_key: None,
}
}
}

View File

@@ -3,20 +3,21 @@
#![enable(unwrap_variant_newtypes)]
[
GameRecord(
version: "0.3.0",
Game(
version: "0.5.0",
flags: [],
periods: [
Period(
start: First,
end: Fourth,
plays: [
events: [
Kickoff(Nebraska),
Play(
action: None,
down: Kickoff(
offence: Nebraska
),
action: Unknown,
down: First,
terrain: Yards(10)
),
Score(FieldGoal),
]
)
]