diff --git a/README.adoc b/README.adoc index 11d4b87..2338ac5 100644 --- a/README.adoc +++ b/README.adoc @@ -14,7 +14,38 @@ 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 diff --git a/gamelog/Cargo.lock b/gamelog/Cargo.lock index a94f63e..93a3f7b 100644 --- a/gamelog/Cargo.lock +++ b/gamelog/Cargo.lock @@ -24,8 +24,15 @@ 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" diff --git a/gamelog/Cargo.toml b/gamelog/Cargo.toml index e80c272..ac307c4 100644 --- a/gamelog/Cargo.toml +++ b/gamelog/Cargo.toml @@ -6,6 +6,10 @@ edition = "2024" [dependencies] ron = "0.9" +[dependencies.strum] +version = "0.27" +features = ["derive"] + [dependencies.semver] version = "1.0" features = ["serde"] diff --git a/gamelog/src/action.rs b/gamelog/src/action.rs new file mode 100644 index 0000000..179e1d8 --- /dev/null +++ b/gamelog/src/action.rs @@ -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 { + 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 { + 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, +} diff --git a/gamelog/src/error.rs b/gamelog/src/error.rs index 7816ff0..c3e135f 100644 --- a/gamelog/src/error.rs +++ b/gamelog/src/error.rs @@ -4,32 +4,26 @@ 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), } } } diff --git a/gamelog/src/event.rs b/gamelog/src/event.rs new file mode 100644 index 0000000..bc385b8 --- /dev/null +++ b/gamelog/src/event.rs @@ -0,0 +1,14 @@ +use crate::{Play, ScoreChange, Team, TerrainState}; + +use serde::Deserialize; + +type Offence = Team; + +#[derive(Debug, Deserialize, Clone)] +pub enum Event { + Play(Play), + Kickoff(Offence), + Turnover(Offence), + Penalty(TerrainState), + ScoreChange(ScoreChange), +} diff --git a/gamelog/src/file.rs b/gamelog/src/file.rs index 3bda9da..71d4334 100644 --- a/gamelog/src/file.rs +++ b/gamelog/src/file.rs @@ -5,6 +5,25 @@ use std::{fs::File, path::PathBuf}; #[derive(Debug, Deserialize, Clone)] pub struct LogFile(Vec); +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(&super::MIN_VER).is_lt() + } +} + impl TryFrom for LogFile { type Error = ron::error::SpannedError; @@ -31,34 +50,3 @@ impl TryFrom 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 { - if self.is_compatible() { - Ok(self) - } else { - Err(error::LogFileError::CompatibilityCheck(self.get_min_ver())) - } - } -} diff --git a/gamelog/src/game.rs b/gamelog/src/game.rs new file mode 100644 index 0000000..2c99e3f --- /dev/null +++ b/gamelog/src/game.rs @@ -0,0 +1,44 @@ +use crate::{Event, Period, Play, PlayHandle, Team, error}; +use serde::Deserialize; + +#[deprecated(since = "0.2.0", note = "Migrated to Game")] +pub type GameRecord = Game; + +#[derive(Debug, Deserialize, Clone)] +pub struct Game { + pub version: semver::Version, + pub periods: Vec, +} + +impl Game { + /// Returns the teams of this game. + pub fn teams(&self) -> Result, error::TeamsError> { + let mut teams = vec![]; + + self.periods.iter().for_each(|period| { + period.events.iter().for_each(|event| { + if let Event::Kickoff(t) | Event::Turnover(t) = event { + if teams.contains(t) { + teams.push(t.to_owned()) + } + } + }) + }); + + if teams.len() == 2 { + Ok(teams) + } else { + Err(error::TeamsError::NumberFound(teams.len())) + } + } +} + +impl PlayHandle for Game { + fn plays(&self) -> Vec { + self.periods + .iter() + .map(|period| period.plays()) + .collect::>>() // Make compiler happy with turbofish. + .concat() + } +} diff --git a/gamelog/src/lib.rs b/gamelog/src/lib.rs index 8d1304f..9dd1de2 100644 --- a/gamelog/src/lib.rs +++ b/gamelog/src/lib.rs @@ -1,13 +1,23 @@ +#![allow(deprecated)] + +mod action; mod error; +mod event; mod file; +mod game; mod period; mod play; +mod score; mod terrain; #[allow(unused)] pub const MIN_VER: semver::Version = semver::Version::new(0, 3, 0); +pub use action::*; +pub use event::Event; pub use file::LogFile; +pub use game::{Game, GameRecord}; pub use period::*; pub use play::*; +pub use score::ScoreChange; pub use terrain::TerrainState; diff --git a/gamelog/src/period.rs b/gamelog/src/period.rs index b062687..f06e4bd 100644 --- a/gamelog/src/period.rs +++ b/gamelog/src/period.rs @@ -1,22 +1,29 @@ +use crate::{Event, PlayHandle}; 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>, -} - #[derive(Debug, Deserialize, Clone)] pub struct Period { - start: Quarter, - end: Option, - plays: Vec, + pub start: Quarter, + pub end: Option, + pub events: Vec, } -#[derive(Debug, Deserialize, Clone)] +impl PlayHandle for Period { + fn plays(&self) -> Vec { + self.events + .iter() + .filter_map(|event| { + if let Event::Play(play) = event { + Some(play.to_owned()) + } else { + None + } + }) + .collect() + } +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] pub enum Quarter { First, Second, diff --git a/gamelog/src/play.rs b/gamelog/src/play.rs index 0ab629d..f1a4eee 100644 --- a/gamelog/src/play.rs +++ b/gamelog/src/play.rs @@ -1,57 +1,35 @@ -use crate::{TerrainState, error}; +use crate::{Action, TerrainState}; use serde::Deserialize; -#[derive(Debug, Deserialize, Clone)] +pub trait PlayHandle { + /// Returns all plays within object's scope. + fn plays(&self) -> Vec; +} + +#[derive(Debug, Deserialize, Clone, Default, PartialEq)] pub struct Play { - down: Down, - terrain: TerrainState, + pub action: Action, + pub down: Down, + pub terrain: 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 PlayHandle for Play { + fn plays(&self) -> Vec { + vec![self.to_owned()] + } } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Default, PartialEq)] pub enum Down { + #[default] First, Second, Third, Fourth, - PointAfterTouchdown, + PointAfterTouchdown(Option), } -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.")] diff --git a/gamelog/src/score.rs b/gamelog/src/score.rs new file mode 100644 index 0000000..51d2d69 --- /dev/null +++ b/gamelog/src/score.rs @@ -0,0 +1,8 @@ +use crate::Team; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone, PartialEq)] +pub struct ScoreChange { + pub team: Team, + pub score: u8, +} diff --git a/gamelog/src/terrain.rs b/gamelog/src/terrain.rs index dcfe9ab..1fc8176 100644 --- a/gamelog/src/terrain.rs +++ b/gamelog/src/terrain.rs @@ -1,11 +1,12 @@ 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, } diff --git a/miller/src/main.rs b/miller/src/main.rs index e69f17b..dfba545 100644 --- a/miller/src/main.rs +++ b/miller/src/main.rs @@ -29,10 +29,5 @@ fn main() { 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), - } }; }