diff --git a/README.adoc b/README.adoc index 11d4b87..295222c 100644 --- a/README.adoc +++ b/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 diff --git a/gamelog.ron b/gamelog.ron index f92dc86..517a1dd 100644 --- a/gamelog.ron +++ b/gamelog.ron @@ -3,360 +3,1106 @@ #![enable(unwrap_variant_newtypes)] [ - GameRecord( - version: "0.2.0", + Game( + version: "0.5.0", + flags: [IgnoreScore], periods: [ Period( start: First, - end: Third, - plays: [ + end: Second, + events: [ + Kickoff(ArizonaState), Play( - action: None, - down: Kickoff( - offence: ArizonaState - ), - terrain: Distance(10) - ), - Play( - action: None, + action: Unknown, down: First, - terrain: 10 + terrain: Yards(10), ), Play( - action: None, + action: Unknown, down: Second, - terrain: 2 + terrain: Yards(2), ), Play( - action: ThrowLeft, + action: Unknown, // Throw Left down: First, - terrain: 10 + terrain: Yards(10), ), Play( - action: ThrowRight, + action: Unknown, // Throw Right down: Second, - terrain: 10 + terrain: Yards(10), ), Play( - action: None, + action: Unknown, down: First, - terrain: 10 + terrain: Yards(10), ), Play( - action: PlayAction ThrowRight, + action: Unknown, // PA Throw Right down: Second, - terrain: 2 + terrain: Yards(2), ), Play( - action: PlayAction RunRight, + action: Unknown, // PA Run Right down: Third, - terrain: 7 + terrain: Yards(7), ), Play( - action: HandoffRushLeft, + action: HalfbackSweep, down: Fourth, - terrain: 11 + terrain: Yards(11), ), Play( - action: None, + action: Unknown, down: First, - terrain: 10 + terrain: Yards(10), ), - TURNOVER, + Turnover(TexasAnM), Play( - action: PlayAction ThrowRight, + action: Unknown, // PA Throw Right down: Second, - terrain: 11 + terrain: Yards(11), ), Play( - action: None, + action: Unknown, down: Third, - terrain: 9 + terrain: Yards(9), ), Play( - action: PassCentre, + action: Unknown, // Throw Centre down: First, - terrain: 10 + terrain: Yards(10), ), Play( - action: TossRunRight, + action: Unknown, // Throw Run Right down: Second, - terrain: 3 + terrain: Yards(3), ), Play( - action: FailedThrow, + action: Unknown, // Throw that failed miserably. down: First, - terrain: 10 + terrain: Yards(10), ), - Play( - action: Kickoff( offence: TexasAnM ), - down: First, - terrain: 10 - ), - Play( - action: SpikeCentre, - down: Second, - terrain: 6 - ), - Play( - action: None, - down: Third, - terrain: 13 - ), - Play( - action: ThrowRight, - down: Fourth, - terrain: 13 - ), - TURNOVER - Play( - action: ShotgunDoubleFlex Throw, - down: Second, - terrain: 10 - ), - Play( - action: SpikeCentre, - down: First, - terrain: 10 - ), - Play( - action: Dupe SpikeCentre, - down: First, - terrain: 10 - ), - Play( - action: FailedThrow, - down: Second, - terrain: 10 - ), - Play( - action: TransferRushRight, - down: Third, - terrain: 15 - ), - ] - ), - //Texas offence - Period( - start: Fourth, - end: None, - plays: [ - Play( - action: None, - down: Fourth, - terrain 17 - ), - Play( - action: Punt, - down: First, - terrain: 10 - ), - Play( - action: ThrowLeft, - down: First, - terrain: 10 - ), - Play( - action: None, - down: First, - terrain: 10 - ), - Play( - action: ThrowCentre, - down: First, - terrain: 10 - ), - Play( - action: Transfer RunLeft, - down: Second, - terrain: 12 - ), - Play( - action: ThrowCentre, - down: First, - terrain: 10 - ), - Play( - action: TransferRushRight, - down: Second, - terrain: 8 - ), - Play( - action: , - down: First, - terrain: 10 - ), - Play( - action: Kickoff(offence: ArizonaState), - down: First, - terrain: 10 - ), - ] - ) - ] - ), - GameRecord( - version: "0.2.0", - periods: [ - Period( - start: First, - end: None, - plays: [ - Play( - action: None - down: Kickoff(offence: Syracuse), - terrain: Distance(10) - - ), - Play( - action: None, - down: Second, - terrain: 3 - ), - Play( - action: SpikeCentre, - down: Third, - terrain: 3, - ), - Play( - action: Curls, - down: Fourth, - terrain: 3 - ), - Play( - action: Mesh || PassCentre, - down: First, - terrain: 10 - ), - Play( - action: None, - down: Second, - terrain: 15 - ), - Play( - action: PlayAction ThrowLeft, - down: First, - terrain: 10 - ) - ] - ), - Period( - start: Second, - end: None, - plays: [ - Play( - action: PA ThrowCentre, - down: Second, - terrain: 10 - ), - Play( - action: Transfer Rush Centre/Left, - down: First, - terrain: 10 - ), - Play( - action: Shotgun ThrowRight, - down: Second, - terrain: 8 - ), - Play( - action: PlayAction RushRight, - down: Third, - terrain: 1 - ), - Play( - action: HB? Rush Centre/Right, - down: Fourth, - terrain: 6 - ), - Play( - action: PlayAction RushCentre, - ), - //Turnover, Colorado Offence - Play( - action: ShotgunDoubleFlex PlayActionComebacks, //Big Throw Right - down: First, - terrain: 10 - ), - Play( - action: Transfer HB? RushCentre, - down: Second, - terrain: 9 - ), - Play( - action: FailedThrow, - down: Third, - terrain: 17 - ), - Play( - action: Same FailedThrow, - ) ] ), Period( start: Third, end: None, - plays: [ + events: [ + Kickoff(TexasAnM), Play( - action: Kickoff(offence:Colorado), - down: First, - terrain: 10 - ), - Play( - action: ThrowRight, - down: First, - terrain: 10 - ), - Play( - action: ShotgunDoubleFlex ThrowRight, - down: First, - terrain: 10 - ), - Play( - action: Pass RushCentre, - down: First, - terrain: 10 - ), - Play( - action: PowerZero, + action: Unknown, // PA Comebacks or Curls? Original note: Spike Centre down: Second, - terrain: 6 - ) + terrain: Yards(6), + ), + Play( + action: Unknown, // Halfback? Original note: None + down: Third, + terrain: Yards(13), + ), + Play( + action: Unknown, // HB Slip Screen? Original note: Throw Right + down: First, + terrain: Yards(10), + ), + Turnover(ArizonaState), + Play( + action: SlotOut, // Slot Out or PA Comebacks. Original note: ShotgunDoubleFlex Throw + down: Second, + terrain: Yards(10), + ), + Play( + action: PlayActionComebacks, // Curls or PA Comebacks. Original note: Spike Centre + down: First, + terrain: Yards(10), + ), + Turnover(TexasAnM), + Play( + action: PlayActionComebacks, // Original note: Dupe Spike Centre + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Failed Throw + down: Second, + terrain: Yards(10), + ), + Play( + action: PowerZero, // Power 0 or Crack Student Body RT. Original note: Transfer Rush Right + down: Third, + terrain: Yards(15), + ), ] ), Period( start: Fourth, end: None, - plays: [ + events: [ + Play( + action: PlayActionComebacks, // Original note: Dupe Spike Centre + down: Fourth, + terrain: Yards(17), + ), + Play( + action: Unknown, // Punt + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Throw Left + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Throw Centre + down: First, + terrain: Yards(10), + ), + Play( + action: HalfbackSweep, // Original note: Transfer Run Left + down: Second, + terrain: Yards(12), + ), + Play( + action: Unknown, // Original note: Throw Centre + down: First, + terrain: Yards(10), + ), + Play( + action: PowerZero, // Original note: Transfer Run Right + down: Second, + terrain: Yards(8), + ), + Play( + action: Unknown, + down: First, + terrain: Yards(10), + ), + Kickoff(ArizonaState), + ] + ), + ] + ), + Game( + version: "0.5.0", + flags: [IgnoreScore], + periods: [ + Period( + start: First, + end: None, + events: [ + Kickoff(Syracuse), + Play( + action: Unknown, + down: Second, + terrain: Yards(3), + ), + Play( + action: Unknown, // Original note: Spike Centre + down: Third, + terrain: Yards(3), + ), + Play( + action: Curls, + down: Fourth, + terrain: Yards(3), + ), + Play( + action: Mesh, + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, + down: Second, + terrain: Yards(15), + ), + Play( + action: Unknown, // Original note: PA Throw Left + down: First, + terrain: Yards(10), + ), + ] + ), + Period( + start: Second, + end: None, + events: [ + Play( + action: Unknown, // Original note: PA Throw Centre + down: Second, + terrain: Yards(10), + ), + Play( + action: HalfbackSlam, // Original note: Transfer Rush Centre/Left + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Shotgun Throw Right + down: Second, + terrain: Yards(8), + ), + Play( + action: PlayActionPowerZero, // Original note: PA Rush Right + down: Third, + terrain: Yards(1), + ), + Play( + action: Unknown, // PowerZero or CSB RT? Original note: "HB? Rush Centre/Right" + down: Fourth, + terrain: Yards(6), + ), + Play( + action: Unknown, // HalfbackSlam? Original note: PA Rush Centre + down: None, + terrain: None, + ), + Turnover(Colorado), + Play( + action: PlayActionComebacks, + down: First, + terrain: Yards(10), + ), + Play( + action: HalfbackSlam, // Original note: "Transfer HB? Rush Centre" + down: Second, + terrain: Yards(9), + ), + Play( + action: Unknown, // Original note: Failed Throw + down: Third, + terrain: Yards(17), + ), + Play( + action: Unknown, // Original note: Same Failed Throw + down: Fourth, + terrain: Unknown, + ), + ] + ), + Period( + start: Third, + end: None, + events: [ + Kickoff(Colorado), + Play( + action: Unknown, // Original note: Throw Right + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: ShotgunDoubleFlex Throw Right + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Pass Run Centre + down: First, + terrain: Yards(10), + ), + Play( + action: PowerZero, + down: Second, + terrain: Yards(6), + ), + ] + ), + Period( + start: Fourth, + end: None, + events: [ Play( action: CrackStudentBodyRightTackle, down: Third, - terrain: 11 + terrain: Yards(11), ), Play( - action: SpikeCentre, + action: Unknown, // Original note: Spike Centre down: Fourth, - terrain: 3 + terrain: Yards(3), ), + Kickoff(Syracuse), Play( - action: SpecialTeams... PuntIntoNet, - ), - Play( - action: Kickoff(offence: Syracuse), + action: Unknown, // Original note: PA Throw Centre down: First, - terrain: 10 - ), - Play( - action: PlayAction ThrowCentre, - down: First, - terrain: 10 + terrain: Yards(10), ), Play( action: SpeedOption, down: Second, - terrain: 3 + terrain: Yards(3), + ), + ] + ), + ] + ), + Game( + version: "0.5.0", + flags: [], + periods: [ + Period( + start: First, + end: None, + events: [ + Kickoff(Nebraska), + Play( + action: Curls, + down: First, + terrain: Yards(10) + ), + Play( + action: Unknown, + down: Second, + terrain: Yards(7), + ), + Play( + action: Curls, + down: Third, + terrain: Yards(9), + ), + Play( + action: CrackStudentBodyRightTackle, + down: Fourth, + terrain: Yards(9), + ), + Play( + action: FleaFlicker, + down: None, + terrain: None, + ), + Turnover(SouthCarolina), + Play( + action: Unknown, // Original note: Throw Right Dash + down: First, + terrain: Yards(10), + ), + ] + ), + Period( + start: Second, + end: None, + events: [ + Play( + action: Unknown, // Original note: Throw Centre + down: First, + terrain: GoalLine, + ), + Score(Touchdown), + Score(PatFail), + Kickoff(Nebraska), + Play( + action: StrongFlood, + down: First, + terrain: Yards(10), + ), + Play( + action: HalfbackSweep, + down: Second, + terrain: Yards(15), + ), + Play( + action: SlantBubble, + down: Third, + terrain: Yards(4), + ), + Play( + action: Unknown, // Original note: Halfback Slant Dummy + down: Fourth, + terrain: Yards(6), + ), + Play( + action: SlantBubble, + down: None, + terrain: None, + ), + Turnover(SouthCarolina), + Play( + action: Unknown, + down: First, + terrain: Yards(10) + ), + Play( + action: Unknown, + down: Second, + terrain: Yards(10) + ), + ] + ), + Period( + start: Third, + end: None, + events: [ + Kickoff(SouthCarolina), + Play( + action: Unknown, + down: Second, + terrain: Yards(18), + ), + Play( + action: Unknown, + down: Third, + terrain: Yards(14), + ), + Play( + action: Unknown, // Original note: Spike Centre + down: Fourth, + terrain: Yards(16), + ), + Turnover(Nebraska), + Play( + action: Unknown, // Original note: Fake Field Goal 44yrds + down: None, + terrain: None, + ), + Score(FieldGoal), + Kickoff(SouthCarolina), + Play( + action: Unknown, + down: Second, + terrain: Yards(3), + ), + Play( + action: Unknown, + down: Third, + terrain: Yards(3), + ), + ] + ), + Period( + start: Fourth, + end: None, + events: [ + Play( + action: Unknown, // Original note: Throw Centre + down: Fourth, + terrain: Yards(3), + ), + Play( + action: Unknown, // Original note: Throw, Run Left + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Throw Left + down: Second, + terrain: Yards(10), + ), + Play( + action: Unknown, + down: Third, + terrain: Yards(5), + ), + Score(Touchdown), + Score(PatFail), + Kickoff(Nebraska), + Play( + action: PowerZero, + down: First, + terrain: Yards(10), + ), + Play( + action: StrongFlood, + down: First, + terrain: Yards(10), + ), + Play( + action: PlayActionComebacks, + down: Second, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Punt + down: First, + terrain: Yards(10), + ), + Turnover(SouthCarolina), + Play( + action: Unknown, + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, + down: Second, + terrain: Yards(10), + ), + Play( + action: Unknown, + down: Third, + terrain: Yards(10), + ), + Play( + action: Unknown, + down: Fourth, + terrain: Yards(6), + ), + Turnover(Nebraska), + // Field Goal 41 yrds + Score(FieldGoal), + Kickoff(SouthCarolina), + Play( + action: Unknown, + down: Second, + terrain: Yards(18), ) ] + ), + ] + ), + Game( + version: "0.5.0", + flags: [], + periods: [ + Period( + start: First, + end: None, + events: [ + Kickoff(Colorado), + Play( + action: Unknown, + down: Second, + terrain: Yards(3), + ), + Play( + action: Unknown, // Original note: Pass, Dash Centre + down: Third, + terrain: Yards(3), + ), + Play( + action: Unknown, // Original note: Throw, Dash Right + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Dash Centre + down: Second, + terrain: Yards(12), + ), + Play( + action: Unknown, + down: First, + terrain: Yards(10), + ), + ] + ), + Period( + start: Second, + end: None, + events: [ + Play( + action: Unknown, // Original note: Pass Back, Dash + down: Second, + terrain: Yards(15), + ), + Play( // Golden Play + action: Unknown, // Original note: Throw, Dash Right + down: Third, + terrain: Yards(2), + ), + Play( + action: Unknown, // Original note: Same as last. + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Pass Back, Dash Right + down: Second, + terrain: Yards(7), + ), + Play( + action: Unknown, // Original note: Throw Right; Went Out + down: Third, + terrain: Yards(7), + ), + Play( + action: Unknown, // HB? Original note: Walk Back Throw Centre, + down: First, + terrain: GoalLine, + ), + Score(Touchdown), + Score(PatSafety), + Kickoff(Iowa), + Penalty(Yards(15)), + Play( + action: Unknown, // Original note: PA Throw Right and Dash + down: Second, + terrain: Yards(6), + ), + Play( + action: Unknown, // Original note: Throw Right + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, + down: Second, + terrain: Yards(1), + ), + Play( + action: Unknown, // Original note: Spike Left + down: Third, + terrain: Yards(1), + ), + Play( + action: Unknown, // Original note: PA Throw Right and Dash + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Throw Right + down: First, + terrain: GoalLine, + ), + Score(FieldGoal), + ] + ), + Period( + start: Third, + end: None, + events: [ + Kickoff(Iowa), + Play( + action: Unknown, // HB? Original note: Run Back, Throw Centre, Run Centre + down: Second, + terrain: Yards(4), + ), + Play( + action: Unknown, // PA? Original note: Dupe, Throw Right + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Orignal note: Run Left + down: Second, + terrain: Yards(13), + ), + Play( + action: Unknown, // PA? Original note: Dupe, Throw Centre, Run Centre + down: First, + terrain: Yards(10), + ), + Play( + action: Unknown, // Orignal note: Throw Right + down: Second, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Throw Left + down: Third, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Dupe, Throw Centre, Run Centre + down: First, + terrain: Yards(10), + ), + ] + ), + Period( + start: Fourth, + end: None, + events: [ + Play( + action: Unknown, // Original note: Throw Left, Run Left + down: Second, + terrain: Yards(8), + ), + Play( + action: Unknown, // Original note: Dupe, Throw Centre, Run Centre + down: Third, + terrain: Yards(4), + ), + // Touchdown + Score(Touchdown), + Score(PatSafety), + Kickoff(Colorado), + Play( + action: Unknown, // Original note: Dupe, Throw Left + down: Second, + terrain: Yards(10), + ), + Play( + action: Unknown, // Original note: Throw Centre, Run Centre + down: Third, + terrain: Yards(1), + ), + Play( + action: Unknown, // Original note: Run Left + down: Fourth, + terrain: Yards(4) + ), + Play( + action: Unknown, // Original note: Dupe, Run Right, + down: None, + terrain: None, + ), + Turnover(Iowa), + Penalty(Yards(15)), + Play( + action: Unknown, // Original note: Run Left + down: Second, + terrain: Yards(11), + ), + Play( + action: Unknown, // Original note: Pass Back, Run Left + down: Third, + terrain: Yards(15), + ), + //Field Goal + Score(FieldGoal) + ] + ) + ] + ), + Game( + version: "0.5.0", + flags: [], + periods: [ + Period( + start: First, + end: None, + events: [ + Kickoff(Nebraska), + Play( + action: CrackStudentBodyRightTackle, + down:Second, + terrain:Yards(13) + ), + Play( + action:Mesh, + down:Third, + terrain:Yards(14) + ), + Play( + action:StrongFlood, + down:Fourth, + terrain:Yards(14) + ), + Play( + action: PlayActionComebacks, + down:First, + terrain:Yards(10) + ), + Play( + action:SlotOut, + down:Second, + terrain:Yards(1) + ) + ] + ), + Period( + start:Second, + end:None, + events:[ + Play( + action:PlayActionComebacks, + down: First, + terrain:Yards(10) + ), + Play( + action:SpeedOption, + down:Second, + terrain:Yards(7) + ), + Play( + action:SlotOut, + down:First, + terrain:Yards(10) + ), + Play( + action:PlayActionComebacks, + down:First, + terrain:Yards(10) + ), + Play( + action:StrongFlood, + down:First, + terrain:GoalLine + ), + Play( + action:StrongFlood, + down:Second, + terrain:GoalLine + ), + Play( + action:HalfbackSlipScreen, + down:Third, + terrain: GoalLine + ), + Play( + action:PlayActionComebacks, + down:None, + terrain:None, + ), + Score(Touchdown), + Score(PatFail), + Kickoff(Iowa), + Play( + action: Unknown, + down:Second, + terrain:Yards(10) + ), + Play( + action: Unknown, + down:Third, + terrain:Yards(10) + ), + Play( + action: Unknown, //Shotgun double flex Shoot Centre + down:Fourth, + terrain:Yards(10) + ), + Turnover(Nebraska), + Play( + action:SlantBubble, + down: First, + terrain: GoalLine, + ) + ] + ), + Period( + start:Third, + end: Fourth, + events: [ + Kickoff(Iowa), + Play( + action:Unknown, + down: Second, + terrain:Yards(4) + ), + Play( + action:Unknown, //PA? + down: First, + terrain:Yards(10), + ), + Play( + action: Unknown,//IForm Normal, Thrown + down: None, + terrain: None + ), + Score(Touchdown), + Score(PatSafety), + Kickoff(Nebraska), + Play( + action: PlayActionBoot, + down: First, + terrain: Yards(10) + ), + Play( + action: SlantBubble, + down: First, + terrain: Yards(10) + ), + Play( + action: FleaFlicker, + down: Second, + terrain: Yards(10) + ), + Play( + action:PlayActionComebacks, + down: Third, + terrain: Yards(10) + ), + Play( + action: SlotOut, + down: First, + terrain: GoalLine + ), + Play( + action: HalfbackSlipScreen, + down: Second, + terrain: GoalLine, + ), + Play( + action: SpeedOption, + down: Third, + terrain: GoalLine, + ), + Play( + action: StrongFlood, + down: Fourth, + terrain: GoalLine + ), + Play( + action: Curls, + down: None, + terrain: None + ), + Turnover(Iowa), + Play( + action: Unknown, + down: Second, + terrain: Yards(10) + ), + Play( + action: Unknown, + down: Third, + terrain: Yards(10) + ), + Play( + action: Unknown,// Run + down: Fourth, + terrain: Yards(7) + ), + Play( + action:Unknown, + down: First, + terrain: Yards(10) + ), + Turnover(Nebraska), + Play( + action:SlantBubble, + down: Second, + terrain: Yards(2) + ), + Play( + action: PlayActionPowerZero, + down: Third, + terrain: Yards(2) + ), + Play( + action: CrackStudentBodyRightTackle, + down: First, + terrain: Yards(10) + ), + Play( + action: StrongFlood, + down: Second, + terrain: Yards(10) + ), + Play( + action: PlayActionComebacks, + down: None, + terrain: None + ) + ] + ) + ] + ), + Game( + // TexasAnM were opponents, but not recorded as + // they were not present; Miller played in place. + version: "0.5.0", + flags: [IgnoreTeam(TexasAnM)], + periods: [ + Period( + start: First, + end: None, + events: [ + Turnover(SouthCarolina), + Play( + action: FleaFlicker, + down: None, + terrain: None + ), + Turnover(TexasAnM) + ] + ), + Period( + start: Second, + end: None, + events:[ + Turnover(SouthCarolina), + Play( + action: SpeedOption, + down: Second, + terrain: Yards(9) + ), + Play( + action: Unknown, + down: None, + terrain: None, + ), + Turnover(TexasAnM), + Score(FieldGoal) + ] + ), + Period( + start: Third, + end: Fourth, + events: [ + Kickoff(SouthCarolina), + Play( + action: Unknown, // Rush + down: Second, + terrain: Yards(7) + ), + Play( + action: Unknown, + down: Third, + terrain: Yards(16) + ), + Play( + action: HalfbackSweep, + down: Fourth, + terrain: Yards(17) + ), + Play( + action: Unknown, + down: None, + terrain: None, + ), + Turnover(TexasAnM), + Score(Touchdown), + Score(PatSafety), + Kickoff(SouthCarolina), + Play( + action: HalfbackSweep, + down: Second, + terrain: Yards(13) + ), + Play( + action: Unknown, + down: First, + terrain: Yards(10) + ), + Play( + action: Unknown,//DoubleFlex Throw + down: Second, + terrain: Yards(10) + ), + Play( + action: Unknown, + down: Third, + terrain: Yards(10) + ), + Play( + action: Unknown, + down: Fourth, + terrain: Yards(10) + ), + Play( + action: Unknown, // Rush + down: None, + terrain: None + ), + Turnover(TexasAnM), + Score(Touchdown), + Score(PatSafety), + Kickoff(SouthCarolina), + Play( + action: Unknown, + down: Second, + terrain: Yards(13) + ), + Play( + action: HalfbackSweep, + down: None, + terrain: None, + ) + //Texas 17, SouthCarolina 0 + ] ) ] ) diff --git a/gamelog/Cargo.lock b/gamelog/Cargo.lock index a94f63e..b1bfb5c 100644 --- a/gamelog/Cargo.lock +++ b/gamelog/Cargo.lock @@ -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" diff --git a/gamelog/Cargo.toml b/gamelog/Cargo.toml index e80c272..9bc0a05 100644 --- a/gamelog/Cargo.toml +++ b/gamelog/Cargo.toml @@ -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"] 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..709783d 100644 --- a/gamelog/src/error.rs +++ b/gamelog/src/error.rs @@ -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.") + } +} diff --git a/gamelog/src/event.rs b/gamelog/src/event.rs new file mode 100644 index 0000000..caceb1a --- /dev/null +++ b/gamelog/src/event.rs @@ -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 { + // Clean this trash spaghetti code up. + + fn make_play(event: &Event) -> Option { + 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 { + 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)); + } +} diff --git a/gamelog/src/file.rs b/gamelog/src/file.rs index 3bda9da..b21a035 100644 --- a/gamelog/src/file.rs +++ b/gamelog/src/file.rs @@ -3,13 +3,34 @@ use serde::Deserialize; use std::{fs::File, path::PathBuf}; #[derive(Debug, Deserialize, Clone)] -pub struct LogFile(Vec); +pub struct LogFile(pub 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(&crate::MIN_VER).is_lt() + } +} impl TryFrom for LogFile { type Error = ron::error::SpannedError; fn try_from(file: File) -> Result { - 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 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..efea30a --- /dev/null +++ b/gamelog/src/game.rs @@ -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, + pub periods: Vec, +} + +impl Game { + /// Returns the teams of this game. + pub fn teams(&self) -> Result, error::TeamsError> { + let ignore: Vec = 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 { + let events = self + .periods + .iter() + .filter_map(|period| Some(period.team_events(team.to_owned(), None).ok().unwrap())) + .collect::>>() + .concat(); + let len = events.len() - 1; + let mut idx: usize = 0; + let mut deltas: Vec = 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::>() + .iter() + .sum::() + } + + /// 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 = 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::>(); + + quarterly_avgs.iter().sum::() / 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::() as f32 / deltas.len() as f32 + } + + pub fn avg_gain(&self, team: Team) -> f32 { + let deltas: Vec = 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::() as f32 / deltas.len() as f32 + } + + pub fn avg_loss(&self, team: Team) -> f32 { + let deltas: Vec = self + .deltas(team) + .iter() + .filter_map(|value| { + if value.is_negative() { + Some(value.to_owned()) + } else { + None + } + }) + .collect(); + + deltas.iter().sum::() 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::>(), + ) + }) + .collect::>>() + .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]); + } +} diff --git a/gamelog/src/lib.rs b/gamelog/src/lib.rs index 8d1304f..81f722f 100644 --- a/gamelog/src/lib.rs +++ b/gamelog/src/lib.rs @@ -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::*; diff --git a/gamelog/src/period.rs b/gamelog/src/period.rs index b062687..654d878 100644 --- a/gamelog/src/period.rs +++ b/gamelog/src/period.rs @@ -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>, -} - #[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 Period { + pub fn team_events( + &self, + team: Team, + assume_team_known: Option, + ) -> Result, error::CannotDetermineTeams> { + let mut events: Vec = 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, + ) -> Result, 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 { + let mut quarters: Vec = 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 = ((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) + ] + ) + } +} diff --git a/gamelog/src/play.rs b/gamelog/src/play.rs index 0ab629d..3c683a4 100644 --- a/gamelog/src/play.rs +++ b/gamelog/src/play.rs @@ -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, + pub terrain: Option, } -type Offence = Team; -impl Offence {} - -#[derive(Debug, Deserialize, Clone)] -pub enum Event { - CrackStudentBodyRightTackle(Play), - Curls(Play), - FleaFlicker(Play), - HalfbackSlam(Play), - HalfbackSlipScreen(Play), - HalfbackSweep(Play), - Mesh(Play), - PlayActionBoot(Play), - PlayActionComebacks(Play), - PlayActionPowerZero(Play), - PowerZero(Play), - SlantBubble(Play), - SlotOut(Play), - SpeedOption(Play), - StrongFlood(Play), - Unknown(Play), - Kickoff { offence: Team }, - Turnover { offence: Team }, - Penalty { terrain: TerrainState }, -} - -#[derive(Debug, Deserialize, Clone)] -pub enum Down { - First, - Second, - Third, - Fourth, - PointAfterTouchdown, -} - -impl Down { - fn get_offence(&self) -> Result<&Team, error::DownError> { - match self { - Self::Kickoff { offence } => Ok(offence), - _ => Err(error::DownError::NotKickoff), +impl Default for Play { + fn default() -> Self { + Self { + action: Action::default(), + down: Some(Down::First), + terrain: Some(TerrainState::Yards(10)), } } } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Default, PartialEq)] +pub enum Down { + #[default] + First, + Second, + Third, + Fourth, +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] pub enum Team { ArizonaState, #[deprecated(since = "0.2.0", note = "Team left the project.")] diff --git a/gamelog/src/terrain.rs b/gamelog/src/terrain.rs index dcfe9ab..fd1ded6 100644 --- a/gamelog/src/terrain.rs +++ b/gamelog/src/terrain.rs @@ -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, } diff --git a/.gitignore b/miller/.gitignore similarity index 100% rename from .gitignore rename to miller/.gitignore diff --git a/miller/Cargo.lock b/miller/Cargo.lock index 1916530..3d598a4 100644 --- a/miller/Cargo.lock +++ b/miller/Cargo.lock @@ -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" diff --git a/miller/Cargo.toml b/miller/Cargo.toml index 1d5f2b4..8805ca2 100644 --- a/miller/Cargo.toml +++ b/miller/Cargo.toml @@ -3,7 +3,6 @@ name = "miller" version = "0.1.0" edition = "2024" license = "MIT" -license-file = "LICENSE" [dependencies.clap] version = "4.5" diff --git a/miller/src/calculator.rs b/miller/src/calculator.rs deleted file mode 100644 index e69de29..0000000 diff --git a/miller/src/main.rs b/miller/src/main.rs index e69f17b..58667aa 100644 --- a/miller/src/main.rs +++ b/miller/src/main.rs @@ -1,38 +1,140 @@ -mod calculator; - -use clap::Parser; +use clap::{ArgAction, Parser}; use core::panic; -use gamelog::LogFile; +use gamelog::{Action, Flags, Key, LogFile, Team}; use std::path::PathBuf; #[derive(Debug, Parser)] +#[clap(author, version, about)] struct Args { /// Path to source file or block device #[arg( short, long, value_hint = clap::ValueHint::DirPath, - default_value = format!("{}/templates/logfile.ron", std::env::current_dir() + default_value = format!("{}/../templates/logfile.ron", std::env::current_dir() .expect("Failed to get current working dir.") .into_os_string() .to_str() .unwrap()) )] logfile_path: PathBuf, + + // Behaviour is backwards. + // ArgAction::SetFalse by default evaluates to true, + // ArgAction::SetTrue by default evaluates to false. + #[arg(short, long, action=ArgAction::SetFalse)] + display_results: bool, } fn main() { let config = Args::parse(); - let log: LogFile = { - let file = match LogFile::try_from(config.logfile_path) { - Ok(f) => f, - Err(err) => panic!("Error: Failed to open logfile: {:?}", err), - }; - - match file.ensure_compatible() { - Ok(f) => f, - Err(err) => panic!("Error: Failed to ensure logfile compatibility: {:?}", err), - } + let log: LogFile = match LogFile::try_from(config.logfile_path) { + Ok(f) => f, + Err(err) => panic!("Error: Failed to open logfile: {:?}", err), }; + + let mut stats = vec![ + TeamStats::new(Team::ArizonaState), + #[allow(deprecated)] + TeamStats::new(Team::BoiseState), + TeamStats::new(Team::Colorado), + TeamStats::new(Team::Iowa), + TeamStats::new(Team::Nebraska), + TeamStats::new(Team::Syracuse), + TeamStats::new(Team::SouthCarolina), + TeamStats::new(Team::TexasAnM), + ]; + + // Work on knocking down the nesting here? + for game in log.0.iter() { + if let Ok(teams) = game.teams() { + for team in teams { + if !game.flags.contains(&Flags::IgnoreTeam(team.to_owned())) { + // Team is to have their stats recorded this game of file. + let team_idx = stats + .iter() + .position(|stat| { + if stat.team == team.to_owned() { + true + } else { + false + } + }) + .unwrap(); + + stats[team_idx] + .avg_terrain_gain + .push(game.avg_gain(team.to_owned())); + + stats[team_idx] + .avg_terrain_loss + .push(game.avg_loss(team.to_owned())); + + stats[team_idx] + .avg_terrain_delta + .push(game.avg_delta(team.to_owned())); + + stats[team_idx] + .plays_per_quarter + .push(game.avg_plays_per_quarter(team.to_owned())); + + stats[team_idx] + .plays_per_game + .push(game.team_plays(team.to_owned())); + + stats[team_idx] + .penalties_per_game + .push(game.penalties(team.to_owned())); + } + } + } + } + + if dbg!(config.display_results) { + // :#? for pretty-printing. + stats.iter().for_each(|team| println!("{:#?}", team)); + } +} + +#[derive(Debug)] +struct TeamStats { + team: gamelog::Team, + // Terrain + avg_terrain_gain: Vec, + avg_terrain_loss: Vec, + avg_terrain_delta: Vec, + // Play rate + plays_per_quarter: Vec, + plays_per_game: Vec, + // Penalties + penalties_per_game: Vec, + // Score + points_per_quarter: Vec, + points_per_game: Vec, + // Biases + most_common_play: Option, + least_common_play: Option, + most_common_key: Option, + least_common_key: Option, +} + +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, + } + } } diff --git a/templates/logfile.ron b/templates/logfile.ron index ffc050e..bd43e30 100644 --- a/templates/logfile.ron +++ b/templates/logfile.ron @@ -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), ] ) ]