From cd548eaa01be13cd5eab4e466761a447e444961f Mon Sep 17 00:00:00 2001 From: Simon Bernier St-Pierre Date: Wed, 6 Mar 2019 23:31:03 -0500 Subject: [PATCH] continue work on simulation --- .gitignore | 2 + Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 203 ++++++++++++++++++++++++++++++++++++++++++++-- src/nhlapi.rs | 143 +++++++++++++++++++++++++++----- src/simulation.rs | 34 ++++++++ 6 files changed, 357 insertions(+), 27 deletions(-) create mode 100644 .gitignore create mode 100644 src/simulation.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0e3bca --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 79c737b..90bed4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,6 +579,7 @@ name = "playoffsbot" version = "0.1.0" dependencies = [ "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Cargo.toml b/Cargo.toml index a26d79a..5d82940 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,4 @@ edition = "2018" chrono = { version = "0.4", features = ["serde"] } reqwest = "0.9" serde = { version = "1", features = ["derive"] } +rand = "0.6" diff --git a/src/main.rs b/src/main.rs index 731560b..858dbe6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,200 @@ -mod nhlapi; +use std::collections::BTreeSet; -fn main() { - println!("Hello, world!"); - let s = nhlapi::schedule::today().unwrap(); - println!("{:#?}", s); +use nhlapi::schedule::Game; +use nhlapi::standings::TeamRecord; +use nhlapi::teams::Team; + +mod nhlapi; +mod simulation; + +pub struct Api { + teams: Vec, + past_standings: Vec, + standings: Vec, + results: nhlapi::schedule::Date, + games: nhlapi::schedule::Date, +} + +impl Api { + pub fn get_team_by_abbrev(&self, abbrev: &str) -> &Team { + let abbrev = abbrev.to_ascii_uppercase(); + self.teams + .iter() + .find(|t| t.abbrev == abbrev) + .expect("team abbrev not found") + } + + pub fn get_points(&self, team_id: u32, past: bool) -> u32 { + if !past { + self.standings + .iter() + .find(|t| t.team.id == team_id) + .expect("team id not found") + .points + } else { + self.past_standings + .iter() + .find(|t| t.team.id == team_id) + .expect("team id not found") + .points + } + } +} + +struct Analyzer<'a> { + api: &'a Api, + my_team: &'a Team, + own_conference_team_ids: BTreeSet, +} + +impl Analyzer<'_> { + pub fn new<'a>(api: &'a Api, my_team: &'a Team) -> Analyzer<'a> { + let mut own_conference_team_ids = BTreeSet::new(); + for team in &api.teams { + if team.conference.id == my_team.conference.id { + own_conference_team_ids.insert(team.id); + } + } + Analyzer { + api, + my_team, + own_conference_team_ids, + } + } + + pub fn perform(&self) -> Analysis { + let mut my_game = None; + let mut games = vec![]; + let mut my_result = None; + let mut results = vec![]; + + for game in &self.api.games.games { + let m = MatchupPre::create(self, game, false); + if m.is_relevant(self) { + if m.is_my_team_involed { + my_game = Some(m.pick_winner(self)); + } else { + games.push(m.pick_winner(self)); + } + } + } + + for game in &self.api.results.games { + let m = MatchupPre::create(self, game, true); + if m.is_relevant(self) { + if m.is_my_team_involed { + my_result = Some(m.pick_winner(self)); + } else { + results.push(m.pick_winner(self)); + } + } + } + + Analysis { + my_team: self.my_team, + my_game: my_game, + games: games, + my_result: my_result, + results: results, + } + } +} + +#[derive(Debug)] +struct Analysis<'a> { + pub my_team: &'a Team, + pub my_result: Option>, + pub results: Vec>, + pub my_game: Option>, + pub games: Vec>, +} + +#[derive(Debug)] +struct Matchup<'a> { + pub game: &'a Game, + pub is_result: bool, + pub is_my_team_involed: bool, + pub ideal_winner: &'a nhlapi::Team, +} + +struct MatchupPre<'a> { + pub game: &'a Game, + pub is_result: bool, + pub is_my_team_involed: bool, +} + +impl<'m> MatchupPre<'m> { + pub fn create<'a>(a: &'a Analyzer, game: &'a Game, is_result: bool) -> MatchupPre<'a> { + let is_my_team_involed = + game.teams.away.team.id == a.my_team.id || game.teams.home.team.id == a.my_team.id; + MatchupPre { + game, + is_result, + is_my_team_involed, + } + } + + pub fn is_relevant(&self, a: &Analyzer) -> bool { + self.is_my_team_involed + || a.own_conference_team_ids + .contains(&self.game.home_team().id) + || a.own_conference_team_ids + .contains(&self.game.away_team().id) + } + + pub fn pick_winner(self, a: &'m Analyzer) -> Matchup<'m> { + let home_team = self.game.home_team(); + let away_team = self.game.away_team(); + + let ideal_winner = if self.is_my_team_involed { + if a.my_team.id == home_team.id { + home_team + } else if a.my_team.id == away_team.id { + away_team + } else { + panic!("unexpected case in pick_winner"); + } + } else if a.own_conference_team_ids.contains(&home_team.id) + && !a.own_conference_team_ids.contains(&away_team.id) + { + away_team + } else if a.own_conference_team_ids.contains(&away_team.id) + && !a.own_conference_team_ids.contains(&home_team.id) + { + home_team + } else { + // TODO: simulation + home_team + }; + + Matchup { + game: self.game, + is_result: self.is_result, + is_my_team_involed: self.is_my_team_involed, + ideal_winner: ideal_winner, + } + } +} + +fn main() -> reqwest::Result<()> { + let teams = nhlapi::teams::get()?; + let past_standings = nhlapi::standings::yesterday()?; + let standings = nhlapi::standings::today()?; + let results = nhlapi::schedule::yesterday()?; + let games = nhlapi::schedule::today()?; + + let api = Api { + teams, + past_standings, + standings, + results, + games, + }; + + let a = Analyzer::new(&api, api.get_team_by_abbrev("mtl")); + let xd = a.perform(); + + println!("{:#?}", xd); + + Ok(()) } diff --git a/src/nhlapi.rs b/src/nhlapi.rs index 3e51616..7449038 100644 --- a/src/nhlapi.rs +++ b/src/nhlapi.rs @@ -48,19 +48,19 @@ impl Serialize for Season { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct LeagueRecord { - wins: u32, - losses: u32, - ot: u32, + pub wins: u32, + pub losses: u32, + pub ot: u32, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Team { - id: u32, - name: String, + pub id: u32, + pub name: String, } pub mod schedule { - use chrono::{DateTime, NaiveDate, Utc}; + use chrono::{DateTime, Local, NaiveDate, Utc}; use serde::{Deserialize, Serialize}; use super::{LeagueRecord, Season, Team}; @@ -88,6 +88,15 @@ pub mod schedule { pub teams: Teams, } + impl Game { + pub fn home_team(&self) -> &Team { + &self.teams.home.team + } + pub fn away_team(&self) -> &Team { + &self.teams.away.team + } + } + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Teams { pub away: TeamRecord, @@ -99,15 +108,45 @@ pub mod schedule { pub team: Team, #[serde(rename = "leagueRecord")] pub league_record: LeagueRecord, + pub score: u32, } - pub fn today() -> reqwest::Result> { - let root: Root = reqwest::get("https://statsapi.web.nhl.com/api/v1/schedule")?.json()?; + pub fn get(date: &NaiveDate) -> reqwest::Result { + let date = format!("{}", date.format("%Y-%m-%d")); + + let client = reqwest::Client::new(); + let mut root: Root = client + .get("https://statsapi.web.nhl.com/api/v1/schedule") + .query(&[("date", date)]) + .send()? + .json()?; + Ok(root.dates.remove(0)) + } + + pub fn get_range(begin: &NaiveDate, end: &NaiveDate) -> reqwest::Result> { + let begin = format!("{}", begin.format("%Y-%m-%d")); + let end = format!("{}", end.format("%Y-%m-%d")); + + let client = reqwest::Client::new(); + let root: Root = client + .get("https://statsapi.web.nhl.com/api/v1/schedule") + .query(&[("startDate", begin), ("endDate", end)]) + .send()? + .json()?; Ok(root.dates) } + + pub fn today() -> reqwest::Result { + get(&Local::today().naive_local()) + } + + pub fn yesterday() -> reqwest::Result { + get(&Local::today().naive_local().pred()) + } } pub mod standings { + use chrono::{Local, NaiveDate}; use serde::{Deserialize, Serialize}; use super::{from_str, LeagueRecord, Team}; @@ -125,32 +164,92 @@ pub mod standings { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TeamRecord { - team: Team, + pub team: Team, #[serde(rename = "leagueRecord")] - league_record: LeagueRecord, + pub league_record: LeagueRecord, #[serde(rename = "goalsAgainst")] - goals_against: u32, + pub goals_against: u32, #[serde(rename = "goalsScored")] - goals_scored: u32, - points: u32, - row: u32, + pub goals_scored: u32, + pub points: u32, + pub row: u32, #[serde(rename = "gamesPlayed")] - games_played: u32, + pub games_played: u32, #[serde(rename = "divisionRank", deserialize_with = "from_str")] - division_rank: u32, + pub division_rank: u32, #[serde(rename = "conferenceRank", deserialize_with = "from_str")] - conference_rank: u32, + pub conference_rank: u32, #[serde(rename = "leagueRank", deserialize_with = "from_str")] - league_rank: u32, + pub league_rank: u32, #[serde(rename = "wildCardRank", deserialize_with = "from_str")] - wildcard_rank: u32, + pub wildcard_rank: u32, + } + + pub fn get(date: &NaiveDate) -> reqwest::Result> { + let date = format!("{}", date.format("%Y-%m-%d")); + let client = reqwest::Client::new(); + let mut root: Root = client + .get("https://statsapi.web.nhl.com/api/v1/standings/byLeague") + .query(&[("date", date)]) + .send()? + .json()?; + Ok(root.records.remove(0).team_records) } pub fn today() -> reqwest::Result> { - let mut root: Root = - reqwest::get("https://statsapi.web.nhl.com/api/v1/standings/byLeague")?.json()?; - Ok(root.records.remove(0).team_records) + get(&Local::today().naive_local()) + } + + pub fn yesterday() -> reqwest::Result> { + get(&Local::today().naive_local().pred()) + } +} + +pub mod teams { + use std::cmp; + + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, Deserialize, Serialize)] + struct Root { + teams: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct Team { + pub id: u32, + #[serde(rename = "name")] + pub full_name: String, + #[serde(rename = "abbreviation")] + pub abbrev: String, + #[serde(rename = "teamName")] + pub name: String, + #[serde(rename = "locationName")] + pub location: String, + pub division: Division, + pub conference: Conference, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct Division { + pub id: u32, + pub name: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct Conference { + pub id: u32, + pub name: String, + } + + pub fn get() -> reqwest::Result> { + let client = reqwest::Client::new(); + let root: Root = client + .get("https://statsapi.web.nhl.com/api/v1/teams") + .send()? + .json()?; + Ok(root.teams) } } diff --git a/src/simulation.rs b/src/simulation.rs new file mode 100644 index 0000000..65afd73 --- /dev/null +++ b/src/simulation.rs @@ -0,0 +1,34 @@ +use rand::seq::SliceRandom; + +use crate::nhlapi::LeagueRecord; + +#[derive(Debug, Copy, Clone)] +struct Entry { + team_id: u32, + division_id: u32, + conference_id: u32, + wins: u32, + losses: u32, + ot: u32, + points: u32, +} + +#[derive(Debug, Copy, Clone)] +enum Event { + Win, + Loss, + Ot, +} + +fn random_event(rec: &LeagueRecord) -> Event { + [ + (Event::Win, rec.wins), + (Event::Loss, rec.losses), + (Event::Ot, rec.ot), + ] + .choose_weighted(&mut rand::thread_rng(), |x| x.1) + .unwrap() + .0 +} + +pub struct Simulation {}