Finish tasks to bring calculation capabilities in line with last interval. #9
39
README.adoc
39
README.adoc
@@ -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
|
||||
|
||||
1170
gamelog.ron
1170
gamelog.ron
File diff suppressed because it is too large
Load Diff
37
gamelog/Cargo.lock
generated
37
gamelog/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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
138
gamelog/src/action.rs
Normal 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,
|
||||
}
|
||||
@@ -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
258
gamelog/src/event.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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
409
gamelog/src/game.rs
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
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,
|
||||
PointAfterTouchdown,
|
||||
}
|
||||
|
||||
impl Down {
|
||||
fn get_offence(&self) -> Result<&Team, error::DownError> {
|
||||
match self {
|
||||
Self::Kickoff { offence } => Ok(offence),
|
||||
_ => Err(error::DownError::NotKickoff),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[derive(Debug, Deserialize, Clone, PartialEq)]
|
||||
pub enum Team {
|
||||
ArizonaState,
|
||||
#[deprecated(since = "0.2.0", note = "Team left the project.")]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
0
.gitignore → miller/.gitignore
vendored
0
.gitignore → miller/.gitignore
vendored
31
miller/Cargo.lock
generated
31
miller/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -3,7 +3,6 @@ name = "miller"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
license-file = "LICENSE"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.5"
|
||||
|
||||
@@ -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) {
|
||||
let log: LogFile = 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 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,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user