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
|
== Goals
|
||||||
|
|
||||||
|
=== Gamelog
|
||||||
* [*] Data Format
|
* [*] 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
|
* [ ] Mathematics
|
||||||
** [ ] Avg. Terrain Gain
|
** [*] Avg. Terrain Gain
|
||||||
** [ ] Avg. Terrain Loss
|
** [*] Avg. Terrain Loss
|
||||||
** [ ] Avg. Terrain Delta
|
** [*] Avg. Terrain Delta
|
||||||
** [ ] Avg. Offence Plays per quarter
|
** [*] Avg. Offence Plays per quarter
|
||||||
** [ ] Avg. Offence Plays per game
|
** [ ] Avg. Offence Plays per game
|
||||||
** [ ] Avg. Penalties per game
|
** [ ] Avg. Penalties per game
|
||||||
* [ ] Play Trend Analysis
|
* [ ] Play Trend Analysis
|
||||||
|
|||||||
1310
gamelog.ron
1310
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]]
|
[[package]]
|
||||||
name = "gamelog"
|
name = "gamelog"
|
||||||
version = "0.3.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ron",
|
"ron",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
"strum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.94"
|
version = "1.0.94"
|
||||||
@@ -57,6 +64,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
@@ -86,6 +99,28 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.100"
|
version = "2.0.100"
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gamelog"
|
name = "gamelog"
|
||||||
version = "0.3.0"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ron = "0.9"
|
ron = "0.9"
|
||||||
|
|
||||||
|
[dependencies.strum]
|
||||||
|
version = "0.27"
|
||||||
|
features = ["derive"]
|
||||||
|
|
||||||
[dependencies.semver]
|
[dependencies.semver]
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
features = ["serde"]
|
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 {
|
pub enum LogFileError {
|
||||||
FailedToOpen(io::Error),
|
FailedToOpen(io::Error),
|
||||||
RonSpannedError(ron::error::SpannedError),
|
RonSpannedError(ron::error::SpannedError),
|
||||||
CompatibilityCheck(semver::Version),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for LogFileError {
|
impl fmt::Display for LogFileError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::FailedToOpen(err) => write!(f, "{}", err),
|
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),
|
Self::RonSpannedError(err) => write!(f, "{}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum DownError {
|
#[derive(Debug)]
|
||||||
NotKickoff,
|
pub enum TeamsError {
|
||||||
|
NumberFound(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DownError {
|
impl fmt::Display for TeamsError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match self {
|
||||||
Self::NotKickoff => write!(f, "Variant was not Down::Kickoff."),
|
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};
|
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 {
|
||||||
|
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 {
|
impl TryFrom<File> for LogFile {
|
||||||
type Error = ron::error::SpannedError;
|
type Error = ron::error::SpannedError;
|
||||||
|
|
||||||
fn try_from(file: File) -> Result<Self, Self::Error> {
|
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 error;
|
||||||
|
mod event;
|
||||||
mod file;
|
mod file;
|
||||||
|
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, 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 period::*;
|
||||||
pub use play::*;
|
pub use play::*;
|
||||||
pub use terrain::TerrainState;
|
pub use terrain::*;
|
||||||
|
|||||||
@@ -1,22 +1,129 @@
|
|||||||
|
use crate::{Event, Play, Team, error};
|
||||||
use serde::Deserialize;
|
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)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct Period {
|
pub struct Period {
|
||||||
start: Quarter,
|
pub start: Quarter,
|
||||||
end: Option<Quarter>,
|
pub end: Option<Quarter>,
|
||||||
plays: Vec<super::Play>,
|
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 {
|
pub enum Quarter {
|
||||||
First,
|
First,
|
||||||
Second,
|
Second,
|
||||||
@@ -24,3 +131,159 @@ pub enum Quarter {
|
|||||||
Fourth,
|
Fourth,
|
||||||
Overtime(u8),
|
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;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone, PartialEq)]
|
||||||
pub struct Play {
|
pub struct Play {
|
||||||
down: Down,
|
pub action: Action,
|
||||||
terrain: TerrainState,
|
pub down: Option<Down>,
|
||||||
|
pub terrain: Option<TerrainState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Offence = Team;
|
impl Default for Play {
|
||||||
impl Offence {}
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
action: Action::default(),
|
||||||
pub enum Event {
|
down: Some(Down::First),
|
||||||
CrackStudentBodyRightTackle(Play),
|
terrain: Some(TerrainState::Yards(10)),
|
||||||
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)]
|
#[derive(Debug, Deserialize, Clone, Default, PartialEq)]
|
||||||
pub enum Down {
|
pub enum Down {
|
||||||
|
#[default]
|
||||||
First,
|
First,
|
||||||
Second,
|
Second,
|
||||||
Third,
|
Third,
|
||||||
Fourth,
|
Fourth,
|
||||||
PointAfterTouchdown,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Down {
|
#[derive(Debug, Deserialize, Clone, PartialEq)]
|
||||||
fn get_offence(&self) -> Result<&Team, error::DownError> {
|
|
||||||
match self {
|
|
||||||
Self::Kickoff { offence } => Ok(offence),
|
|
||||||
_ => Err(error::DownError::NotKickoff),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
pub enum Team {
|
pub enum Team {
|
||||||
ArizonaState,
|
ArizonaState,
|
||||||
#[deprecated(since = "0.2.0", note = "Team left the project.")]
|
#[deprecated(since = "0.2.0", note = "Team left the project.")]
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone, Default, PartialEq)]
|
||||||
pub enum TerrainState {
|
pub enum TerrainState {
|
||||||
#[deprecated(since = "0.2.0", note = "Replaced in favour of TerrainState::Yards")]
|
|
||||||
Distance(u8),
|
|
||||||
Yards(u8),
|
Yards(u8),
|
||||||
GoalLine,
|
GoalLine,
|
||||||
Inches,
|
Inches,
|
||||||
|
#[default]
|
||||||
Unknown,
|
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]]
|
[[package]]
|
||||||
name = "gamelog"
|
name = "gamelog"
|
||||||
version = "0.3.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ron",
|
"ron",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
"strum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -116,6 +117,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
@@ -151,6 +158,28 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.100"
|
version = "2.0.100"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,38 +1,140 @@
|
|||||||
mod calculator;
|
use clap::{ArgAction, Parser};
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
use core::panic;
|
use core::panic;
|
||||||
use gamelog::LogFile;
|
use gamelog::{Action, Flags, Key, LogFile, Team};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
|
#[clap(author, version, about)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Path to source file or block device
|
/// Path to source file or block device
|
||||||
#[arg(
|
#[arg(
|
||||||
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()
|
||||||
.unwrap())
|
.unwrap())
|
||||||
)]
|
)]
|
||||||
logfile_path: PathBuf,
|
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() {
|
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),
|
||||||
};
|
};
|
||||||
|
|
||||||
match file.ensure_compatible() {
|
let mut stats = vec![
|
||||||
Ok(f) => f,
|
TeamStats::new(Team::ArizonaState),
|
||||||
Err(err) => panic!("Error: Failed to ensure logfile compatibility: {:?}", err),
|
#[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)]
|
#![enable(unwrap_variant_newtypes)]
|
||||||
|
|
||||||
[
|
[
|
||||||
GameRecord(
|
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),
|
||||||
Play(
|
Play(
|
||||||
action: None,
|
action: Unknown,
|
||||||
down: Kickoff(
|
down: First,
|
||||||
offence: Nebraska
|
|
||||||
),
|
|
||||||
terrain: Yards(10)
|
terrain: Yards(10)
|
||||||
),
|
),
|
||||||
|
Score(FieldGoal),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user