diff --git a/Cargo.lock b/Cargo.lock index 6a567b4..bff2a36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,7 +240,7 @@ dependencies = [ [[package]] name = "fbsim-core" version = "1.0.0-beta.1" -source = "git+https://github.com/whatsacomputertho/fbsim-core.git?branch=main#67541c7f7a7af1fd15bfac8d6d64869990b97ab5" +source = "git+https://github.com/whatsacomputertho/fbsim-core.git?branch=main#b615b83e6085abbe74116361ea6c1e7ac0ee9a79" dependencies = [ "chrono", "lazy_static", diff --git a/src/cli/league/season.rs b/src/cli/league/season.rs index ce71cde..45c1512 100644 --- a/src/cli/league/season.rs +++ b/src/cli/league/season.rs @@ -1,11 +1,15 @@ +pub mod conference; pub mod playoffs; pub mod schedule; +pub mod standings; pub mod team; pub mod week; use clap::{Subcommand, Args}; +use crate::cli::league::season::conference::FbsimLeagueSeasonConferenceSubcommand; use crate::cli::league::season::playoffs::FbsimLeagueSeasonPlayoffsSubcommand; +use crate::cli::league::season::standings::FbsimLeagueSeasonStandingsArgs; use crate::cli::league::season::team::FbsimLeagueSeasonTeamSubcommand; use crate::cli::league::season::schedule::FbsimLeagueSeasonScheduleSubcommand; use crate::cli::league::season::week::FbsimLeagueSeasonWeekSubcommand; @@ -58,6 +62,11 @@ pub enum FbsimLeagueSeasonSubcommand { Get(FbsimLeagueSeasonGetArgs), List(FbsimLeagueSeasonListArgs), Sim(FbsimLeagueSeasonSimArgs), + Standings(FbsimLeagueSeasonStandingsArgs), + Conference { + #[command(subcommand)] + command: FbsimLeagueSeasonConferenceSubcommand + }, Playoffs { #[command(subcommand)] command: FbsimLeagueSeasonPlayoffsSubcommand diff --git a/src/cli/league/season/conference.rs b/src/cli/league/season/conference.rs new file mode 100644 index 0000000..5c44cde --- /dev/null +++ b/src/cli/league/season/conference.rs @@ -0,0 +1,64 @@ +pub mod division; + +use clap::{Subcommand, Args}; + +use crate::cli::league::season::conference::division::FbsimLeagueSeasonConferenceDivisionSubcommand; + +/// Add a conference to the current season +#[derive(Args, Clone)] +pub struct FbsimLeagueSeasonConferenceAddArgs { + /// The input filepath for the league + #[arg(short='l')] + #[arg(long="league")] + pub league: String, + + /// The name of the conference + #[arg(short='n')] + #[arg(long="name")] + pub name: String, +} + +/// List conferences in a season +#[derive(Args, Clone)] +pub struct FbsimLeagueSeasonConferenceListArgs { + /// The input filepath for the league + #[arg(short='l')] + #[arg(long="league")] + pub league: String, + + /// The year of the season + #[arg(short='y')] + #[arg(long="year")] + pub year: usize, +} + +/// Get a conference from a season +#[derive(Args, Clone)] +pub struct FbsimLeagueSeasonConferenceGetArgs { + /// The input filepath for the league + #[arg(short='l')] + #[arg(long="league")] + pub league: String, + + /// The year of the season + #[arg(short='y')] + #[arg(long="year")] + pub year: usize, + + /// The conference ID + #[arg(short='c')] + #[arg(long="conference")] + pub conference: usize, +} + +/// Manage conferences for a season +#[derive(Subcommand, Clone)] +pub enum FbsimLeagueSeasonConferenceSubcommand { + Add(FbsimLeagueSeasonConferenceAddArgs), + List(FbsimLeagueSeasonConferenceListArgs), + Get(FbsimLeagueSeasonConferenceGetArgs), + Division { + #[command(subcommand)] + command: FbsimLeagueSeasonConferenceDivisionSubcommand + } +} diff --git a/src/cli/league/season/conference/division.rs b/src/cli/league/season/conference/division.rs new file mode 100644 index 0000000..8c2e418 --- /dev/null +++ b/src/cli/league/season/conference/division.rs @@ -0,0 +1,71 @@ +use clap::{Subcommand, Args}; + +/// Add a division to a conference +#[derive(Args, Clone)] +pub struct FbsimLeagueSeasonConferenceDivisionAddArgs { + /// The input filepath for the league + #[arg(short='l')] + #[arg(long="league")] + pub league: String, + + /// The conference index + #[arg(short='c')] + #[arg(long="conference")] + pub conference: usize, + + /// The name of the division + #[arg(short='n')] + #[arg(long="name")] + pub name: String, +} + +/// List divisions in a conference +#[derive(Args, Clone)] +pub struct FbsimLeagueSeasonConferenceDivisionListArgs { + /// The input filepath for the league + #[arg(short='l')] + #[arg(long="league")] + pub league: String, + + /// The year of the season + #[arg(short='y')] + #[arg(long="year")] + pub year: usize, + + /// The conference index + #[arg(short='c')] + #[arg(long="conference")] + pub conference: usize, +} + +/// Get a division from a conference +#[derive(Args, Clone)] +pub struct FbsimLeagueSeasonConferenceDivisionGetArgs { + /// The input filepath for the league + #[arg(short='l')] + #[arg(long="league")] + pub league: String, + + /// The year of the season + #[arg(short='y')] + #[arg(long="year")] + pub year: usize, + + /// The conference index + #[arg(short='c')] + #[arg(long="conference")] + pub conference: usize, + + /// The division ID + #[arg(short='d')] + #[arg(long="division")] + pub division: usize, +} + +/// Manage divisions in a conference +#[derive(Subcommand, Clone)] +pub enum FbsimLeagueSeasonConferenceDivisionSubcommand { + Add(FbsimLeagueSeasonConferenceDivisionAddArgs), + List(FbsimLeagueSeasonConferenceDivisionListArgs), + Get(FbsimLeagueSeasonConferenceDivisionGetArgs), +} diff --git a/src/cli/league/season/playoffs.rs b/src/cli/league/season/playoffs.rs index e9ff60e..94f90d7 100644 --- a/src/cli/league/season/playoffs.rs +++ b/src/cli/league/season/playoffs.rs @@ -12,10 +12,20 @@ pub struct FbsimLeagueSeasonPlayoffsGenArgs { #[arg(long="league")] pub league: String, - /// The number of teams in the playoffs + /// The number of teams in the playoffs (total, or per conference with -p flag) #[arg(short='n')] #[arg(long="num-teams")] pub num_teams: usize, + + /// Enable multi-conference playoffs, where number of teams is per-conference + #[arg(short='p')] + #[arg(long="per-conference")] + pub per_conference: bool, + + /// Guarantee division winners get playoff berths + #[arg(short='d')] + #[arg(long="division-winners")] + pub division_winners: bool, } /// Display the playoffs for a season @@ -54,11 +64,26 @@ pub struct FbsimLeagueSeasonPlayoffsPictureArgs { #[arg(long="year")] pub year: usize, - /// Number of playoff teams + /// Number of playoff teams (total, or per conference with -p flag) #[arg(short='n')] #[arg(long="num-playoff-teams")] #[arg(default_value="4")] pub num_playoff_teams: usize, + + /// Calculate multi-conference playoff picture, where number of teams is per-conference + #[arg(short='p')] + #[arg(long="per-conference")] + pub per_conference: bool, + + /// Account for division winner guaranteed berths + #[arg(short='d')] + #[arg(long="division-winners")] + pub division_winners: bool, + + /// Show only this conference (optional) + #[arg(short='c')] + #[arg(long="conference")] + pub conference: Option, } /// Manage playoffs for a season diff --git a/src/cli/league/season/playoffs/round.rs b/src/cli/league/season/playoffs/round.rs index c12e099..16cfd5c 100644 --- a/src/cli/league/season/playoffs/round.rs +++ b/src/cli/league/season/playoffs/round.rs @@ -17,10 +17,20 @@ pub struct FbsimLeagueSeasonPlayoffsRoundGetArgs { #[arg(long="year")] pub year: usize, - /// The playoff round index + /// The playoff round ID #[arg(short='r')] #[arg(long="round")] pub round: usize, + + /// Get conference-specific round (optional, for multi-conference playoffs) + #[arg(short='c')] + #[arg(long="conference")] + pub conference: Option, + + /// Get winners bracket round (optional, for multi-conference playoffs) + #[arg(short='w')] + #[arg(long="winners-bracket")] + pub winners_bracket: bool, } /// Simulate a playoff round @@ -30,11 +40,6 @@ pub struct FbsimLeagueSeasonPlayoffsRoundSimArgs { #[arg(short='l')] #[arg(long="league")] pub league: String, - - /// The playoff round index - #[arg(short='r')] - #[arg(long="round")] - pub round: usize, } /// Manage rounds in the playoffs diff --git a/src/cli/league/season/playoffs/round/matchup.rs b/src/cli/league/season/playoffs/round/matchup.rs index eaacfda..136cf7c 100644 --- a/src/cli/league/season/playoffs/round/matchup.rs +++ b/src/cli/league/season/playoffs/round/matchup.rs @@ -22,6 +22,16 @@ pub struct FbsimLeagueSeasonPlayoffsRoundMatchupGetArgs { #[arg(short='m')] #[arg(long="matchup")] pub matchup: usize, + + /// The conference bracket index (optional, for conference playoffs) + #[arg(short='c')] + #[arg(long="conference")] + pub conference: Option, + + /// Get a matchup from the winners bracket instead of a conference bracket + #[arg(short='w')] + #[arg(long="winners-bracket")] + pub winners_bracket: bool, } /// Simulate a matchup from a playoff round @@ -46,6 +56,17 @@ pub struct FbsimLeagueSeasonPlayoffsRoundMatchupSimArgs { #[arg(short='m')] #[arg(long="matchup")] pub matchup: usize, + + /// The conference bracket index (defaults to 0) + #[arg(short='c')] + #[arg(long="conference")] + #[arg(default_value_t = 0)] + pub conference: usize, + + /// Simulate a matchup from the winners bracket instead of a conference bracket + #[arg(short='w')] + #[arg(long="winners-bracket")] + pub winners_bracket: bool, } /// Manage matchups for a playoff round diff --git a/src/cli/league/season/schedule.rs b/src/cli/league/season/schedule.rs index a1a6d62..8232fc9 100644 --- a/src/cli/league/season/schedule.rs +++ b/src/cli/league/season/schedule.rs @@ -26,6 +26,18 @@ pub struct FbsimLeagueSeasonScheduleGenArgs { #[arg(short='p')] #[arg(long="permute")] pub permute: Option, + + /// Number of games per division opponent + #[arg(long="division-games")] + pub division_games: Option, + + /// Number of games per non-division conference opponent + #[arg(long="conference-games")] + pub conference_games: Option, + + /// Total number of cross-conference games + #[arg(long="cross-conference-games")] + pub cross_conference_games: Option, } /// Manage the schedule for the current season of a FootballSim league diff --git a/src/cli/league/season/standings.rs b/src/cli/league/season/standings.rs new file mode 100644 index 0000000..eb825e1 --- /dev/null +++ b/src/cli/league/season/standings.rs @@ -0,0 +1,33 @@ +use clap::Args; + +/// Display standings for a season +#[derive(Args, Clone)] +pub struct FbsimLeagueSeasonStandingsArgs { + /// The input filepath for the league + #[arg(short='l')] + #[arg(long="league")] + pub league: String, + + /// The year of the season + #[arg(short='y')] + #[arg(long="year")] + pub year: usize, + + /// Filter by conference index (optional) + #[arg(short='c')] + #[arg(long="conference")] + pub conference: Option, + + /// Filter by division ID (requires -c/--conference) + #[arg(short='d')] + #[arg(long="division")] + pub division: Option, + + /// Group standings by conference + #[arg(long="by-conference")] + pub by_conference: bool, + + /// Group standings by division + #[arg(long="by-division")] + pub by_division: bool, +} diff --git a/src/cli/league/season/team.rs b/src/cli/league/season/team.rs index 915bee8..44b491c 100644 --- a/src/cli/league/season/team.rs +++ b/src/cli/league/season/team.rs @@ -23,6 +23,30 @@ pub struct FbsimLeagueSeasonTeamAddArgs { pub id: usize } +/// Assign a team to a division +#[derive(Args, Clone)] +pub struct FbsimLeagueSeasonTeamAssignArgs { + /// The input filepath for the league + #[arg(short='l')] + #[arg(long="league")] + pub league: String, + + /// The ID of the team to assign + #[arg(short='t')] + #[arg(long="team")] + pub team: usize, + + /// The conference ID + #[arg(short='c')] + #[arg(long="conference")] + pub conference: usize, + + /// The division ID + #[arg(short='d')] + #[arg(long="division")] + pub division: usize, +} + /// Display a team from a FootballSim season #[derive(Args, Clone)] pub struct FbsimLeagueSeasonTeamGetArgs { @@ -72,6 +96,7 @@ pub struct FbsimLeagueSeasonTeamListArgs { #[derive(Subcommand, Clone)] pub enum FbsimLeagueSeasonTeamSubcommand { Add(FbsimLeagueSeasonTeamAddArgs), + Assign(FbsimLeagueSeasonTeamAssignArgs), Get(FbsimLeagueSeasonTeamGetArgs), List(FbsimLeagueSeasonTeamListArgs), Stats { diff --git a/src/league/season.rs b/src/league/season.rs index bc4e7ff..bfc9a1e 100644 --- a/src/league/season.rs +++ b/src/league/season.rs @@ -1,8 +1,10 @@ pub mod add; +pub mod conference; pub mod get; pub mod list; pub mod playoffs; pub mod schedule; pub mod sim; +pub mod standings; pub mod team; pub mod week; \ No newline at end of file diff --git a/src/league/season/conference.rs b/src/league/season/conference.rs new file mode 100644 index 0000000..62d0381 --- /dev/null +++ b/src/league/season/conference.rs @@ -0,0 +1,4 @@ +pub mod add; +pub mod get; +pub mod list; +pub mod division; diff --git a/src/league/season/conference/add.rs b/src/league/season/conference/add.rs new file mode 100644 index 0000000..d53aca0 --- /dev/null +++ b/src/league/season/conference/add.rs @@ -0,0 +1,50 @@ +use std::fs; + +use fbsim_core::league::League; +use fbsim_core::league::season::conference::LeagueConference; + +use crate::cli::league::season::conference::FbsimLeagueSeasonConferenceAddArgs; + +use serde_json; + +pub fn add_conference(args: FbsimLeagueSeasonConferenceAddArgs) -> Result<(), String> { + // Load the league from its file as mutable + let file_res = &fs::read_to_string(&args.league); + let file = match file_res { + Ok(file) => file, + Err(error) => return Err(format!("Error loading league file: {}", error)), + }; + let league_res = serde_json::from_str(file); + let mut league: League = match league_res { + Ok(league) => league, + Err(error) => return Err(format!("Error loading league from file: {}", error)), + }; + + // Get the current season + let season = match league.current_season_mut() { + Some(s) => s, + None => return Err(String::from("No current season found")), + }; + + // Get the conference ID before adding + let conf_id = season.conferences().len(); + + // Add the conference + let conference = LeagueConference::with_name(&args.name); + season.add_conference(conference)?; + + // Serialize the league as JSON + let league_res = serde_json::to_string_pretty(&league); + let league_str: String = match league_res { + Ok(league_str) => league_str, + Err(error) => return Err(format!("Error serializing league: {}", error)), + }; + + // Write the league back to its file + let write_res = fs::write(&args.league, league_str); + if let Err(e) = write_res { + return Err(format!("Error writing league file: {}", e)); + } + println!("Conference {} added to season with ID {}", args.name, conf_id); + Ok(()) +} diff --git a/src/league/season/conference/division.rs b/src/league/season/conference/division.rs new file mode 100644 index 0000000..c489319 --- /dev/null +++ b/src/league/season/conference/division.rs @@ -0,0 +1,3 @@ +pub mod add; +pub mod get; +pub mod list; diff --git a/src/league/season/conference/division/add.rs b/src/league/season/conference/division/add.rs new file mode 100644 index 0000000..bd0318f --- /dev/null +++ b/src/league/season/conference/division/add.rs @@ -0,0 +1,55 @@ +use std::fs; + +use fbsim_core::league::League; +use fbsim_core::league::season::conference::LeagueDivision; + +use crate::cli::league::season::conference::division::FbsimLeagueSeasonConferenceDivisionAddArgs; + +use serde_json; + +pub fn add_division(args: FbsimLeagueSeasonConferenceDivisionAddArgs) -> Result<(), String> { + // Load the league from its file as mutable + let file_res = &fs::read_to_string(&args.league); + let file = match file_res { + Ok(file) => file, + Err(error) => return Err(format!("Error loading league file: {}", error)), + }; + let league_res = serde_json::from_str(file); + let mut league: League = match league_res { + Ok(league) => league, + Err(error) => return Err(format!("Error loading league from file: {}", error)), + }; + + // Get the current season and conference + let season = match league.current_season_mut() { + Some(s) => s, + None => return Err(String::from("No current season found")), + }; + let conference = match season.conference_mut(args.conference) { + Some(c) => c, + None => return Err(format!("No conference found with index: {}", args.conference)), + }; + + // Get the division ID and conference name before adding + let div_id = conference.divisions().len(); + let conf_name = conference.name_mut().clone(); + + // Add the division + let division = LeagueDivision::with_name(&args.name); + conference.add_division(division)?; + + // Serialize the league as JSON + let league_res = serde_json::to_string_pretty(&league); + let league_str: String = match league_res { + Ok(league_str) => league_str, + Err(error) => return Err(format!("Error serializing league: {}", error)), + }; + + // Write the league back to its file + let write_res = fs::write(&args.league, league_str); + if let Err(e) = write_res { + return Err(format!("Error writing league file: {}", e)); + } + println!("Division {} added to conference {} with ID {}", args.name, conf_name, div_id); + Ok(()) +} diff --git a/src/league/season/conference/division/get.rs b/src/league/season/conference/division/get.rs new file mode 100644 index 0000000..46b39dc --- /dev/null +++ b/src/league/season/conference/division/get.rs @@ -0,0 +1,68 @@ +use std::fs; +use std::io::{Write, stdout}; + +use fbsim_core::league::League; + +use crate::cli::league::season::conference::division::FbsimLeagueSeasonConferenceDivisionGetArgs; + +use serde_json; +use tabwriter::TabWriter; + +pub fn get_division(args: FbsimLeagueSeasonConferenceDivisionGetArgs) -> Result<(), String> { + // Load the league from its file + let file_res = &fs::read_to_string(&args.league); + let file = match file_res { + Ok(file) => file, + Err(error) => return Err(format!("Error loading league file: {}", error)), + }; + let league_res = serde_json::from_str(file); + let league: League = match league_res { + Ok(league) => league, + Err(error) => return Err(format!("Error loading league from file: {}", error)), + }; + + // Get the season, conference, and division + let season = match league.season(args.year) { + Some(season) => season, + None => return Err(format!("No season found with year: {}", args.year)), + }; + let conferences = season.conferences(); + let conference = match conferences.get(args.conference) { + Some(c) => c, + None => return Err(format!("No conference found with ID: {}", args.conference)), + }; + let division = match conference.division(args.division) { + Some(d) => d, + None => return Err(format!("No division found with ID: {}", args.division)), + }; + + // Display division info + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Division:\t{}", division.name()).map_err(|e| e.to_string())?; + writeln!(&mut tw, "Conference:\t{}", conference.name()).map_err(|e| e.to_string())?; + writeln!(&mut tw).map_err(|e| e.to_string())?; + + // Display teams in division + let team_ids = division.teams(); + if team_ids.is_empty() { + writeln!(&mut tw, "No teams assigned to this division").map_err(|e| e.to_string())?; + } else { + writeln!(&mut tw, "Teams:").map_err(|e| e.to_string())?; + writeln!(&mut tw, "ID\tName\tRecord").map_err(|e| e.to_string())?; + for team_id in team_ids { + if let Some(team) = season.team(*team_id) { + let record = season.team_matchups(*team_id) + .map(|m| m.record().to_string()) + .unwrap_or_else(|_| String::from("-")); + writeln!( + &mut tw, "{}\t{}\t{}", + team_id, + team.name(), + record + ).map_err(|e| e.to_string())?; + } + } + } + tw.flush().map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src/league/season/conference/division/list.rs b/src/league/season/conference/division/list.rs new file mode 100644 index 0000000..d595d2d --- /dev/null +++ b/src/league/season/conference/division/list.rs @@ -0,0 +1,54 @@ +use std::fs; +use std::io::{Write, stdout}; + +use fbsim_core::league::League; + +use crate::cli::league::season::conference::division::FbsimLeagueSeasonConferenceDivisionListArgs; + +use serde_json; +use tabwriter::TabWriter; + +pub fn list_divisions(args: FbsimLeagueSeasonConferenceDivisionListArgs) -> Result<(), String> { + // Load the league from its file + let file_res = &fs::read_to_string(&args.league); + let file = match file_res { + Ok(file) => file, + Err(error) => return Err(format!("Error loading league file: {}", error)), + }; + let league_res = serde_json::from_str(file); + let league: League = match league_res { + Ok(league) => league, + Err(error) => return Err(format!("Error loading league from file: {}", error)), + }; + + // Get the season, conference, and divisions + let season = match league.season(args.year) { + Some(season) => season, + None => return Err(format!("No season found with year: {}", args.year)), + }; + let conferences = season.conferences(); + let conference = match conferences.get(args.conference) { + Some(c) => c, + None => return Err(format!("No conference found with ID: {}", args.conference)), + }; + let divisions = conference.divisions(); + if divisions.is_empty() { + println!("No divisions found in conference {}", conference.name()); + return Ok(()); + } + + // Display divisions in a table + println!("=== {} Divisions ===", conference.name()); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "ID\tName\tTeams").map_err(|e| e.to_string())?; + for (div_id, division) in divisions.iter().enumerate() { + writeln!( + &mut tw, "{}\t{}\t{}", + div_id, + division.name(), + division.teams().len() + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src/league/season/conference/get.rs b/src/league/season/conference/get.rs new file mode 100644 index 0000000..5697980 --- /dev/null +++ b/src/league/season/conference/get.rs @@ -0,0 +1,78 @@ +use std::fs; +use std::io::{Write, stdout}; + +use fbsim_core::league::League; + +use crate::cli::league::season::conference::FbsimLeagueSeasonConferenceGetArgs; + +use serde_json; +use tabwriter::TabWriter; + +pub fn get_conference(args: FbsimLeagueSeasonConferenceGetArgs) -> Result<(), String> { + // Load the league from its file + let file_res = &fs::read_to_string(&args.league); + let file = match file_res { + Ok(file) => file, + Err(error) => return Err(format!("Error loading league file: {}", error)), + }; + let league_res = serde_json::from_str(file); + let league: League = match league_res { + Ok(league) => league, + Err(error) => return Err(format!("Error loading league from file: {}", error)), + }; + + // Get the season and conference + let season = match league.season(args.year) { + Some(season) => season, + None => return Err(format!("No season found with year: {}", args.year)), + }; + let conferences = season.conferences(); + let conference = match conferences.get(args.conference) { + Some(c) => c, + None => return Err(format!("No conference found with ID: {}", args.conference)), + }; + + // Display conference divisions in a table + println!("=== {} ===", conference.name()); + let mut tw = TabWriter::new(stdout()); + if conference.divisions().is_empty() { + writeln!(&mut tw, "No divisions").map_err(|e| e.to_string())?; + } else { + writeln!(&mut tw, "Divisions:").map_err(|e| e.to_string())?; + writeln!(&mut tw, "ID\tName\tTeams").map_err(|e| e.to_string())?; + for (div_id, division) in conference.divisions().iter().enumerate() { + writeln!( + &mut tw, "{}\t{}\t{}", + div_id, + division.name(), + division.teams().len() + ).map_err(|e| e.to_string())?; + } + } + + // Display teams in conference + let team_ids = conference.all_teams(); + if !team_ids.is_empty() { + writeln!(&mut tw).map_err(|e| e.to_string())?; + writeln!(&mut tw, "Teams:").map_err(|e| e.to_string())?; + writeln!(&mut tw, "ID\tName\tDivision\tRecord").map_err(|e| e.to_string())?; + for division in conference.divisions() { + for team_id in division.teams() { + if let Some(team) = season.team(*team_id) { + let record = season.team_matchups(*team_id) + .map(|m| m.record().to_string()) + .unwrap_or_else(|_| String::from("-")); + writeln!( + &mut tw, "{}\t{}\t{}\t{}", + team_id, + team.name(), + division.name(), + record + ).map_err(|e| e.to_string())?; + } + } + } + } + tw.flush().map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src/league/season/conference/list.rs b/src/league/season/conference/list.rs new file mode 100644 index 0000000..c20792a --- /dev/null +++ b/src/league/season/conference/list.rs @@ -0,0 +1,51 @@ +use std::fs; +use std::io::{Write, stdout}; + +use fbsim_core::league::League; + +use crate::cli::league::season::conference::FbsimLeagueSeasonConferenceListArgs; + +use serde_json; +use tabwriter::TabWriter; + +pub fn list_conferences(args: FbsimLeagueSeasonConferenceListArgs) -> Result<(), String> { + // Load the league from its file + let file_res = &fs::read_to_string(&args.league); + let file = match file_res { + Ok(file) => file, + Err(error) => return Err(format!("Error loading league file: {}", error)), + }; + let league_res = serde_json::from_str(file); + let league: League = match league_res { + Ok(league) => league, + Err(error) => return Err(format!("Error loading league from file: {}", error)), + }; + + // Get the season and conferences + let season = match league.season(args.year) { + Some(season) => season, + None => return Err(format!("No season found with year: {}", args.year)), + }; + let conferences = season.conferences(); + if conferences.is_empty() { + println!("No conferences found for the {} season", args.year); + return Ok(()); + } + + // Display conferences in a table + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "ID\tName\tDivisions\tTeams").map_err(|e| e.to_string())?; + for (index, conference) in conferences.iter().enumerate() { + let num_divisions = conference.divisions().len(); + let num_teams = conference.num_teams(); + writeln!( + &mut tw, "{}\t{}\t{}\t{}", + index, + conference.name(), + num_divisions, + num_teams + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src/league/season/get.rs b/src/league/season/get.rs index 8c3d2a7..971efdc 100644 --- a/src/league/season/get.rs +++ b/src/league/season/get.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fs; use std::io::{Write, stdout}; @@ -33,14 +34,56 @@ pub fn get_season(args: FbsimLeagueSeasonGetArgs) -> Result<(), String> { return Err(format!("No teams have been added to the {} season yet", args.year)); } + // Determine if we need conference/division columns + let conferences = season.conferences(); + let show_conference = conferences.len() > 1; + let show_division = conferences.iter().any(|c| c.divisions().len() > 1); + + // Build team -> conference/division lookup maps + let mut team_conference: HashMap = HashMap::new(); + let mut team_division: HashMap = HashMap::new(); + if show_conference || show_division { + for conference in conferences.iter() { + for division in conference.divisions().iter() { + for team_id in division.teams().iter() { + if show_conference { + team_conference.insert(*team_id, conference.name().to_string()); + } + if show_division { + let div_name = division.name(); + let div_str = if div_name.is_empty() { + "-".to_string() + } else { + div_name.to_string() + }; + team_division.insert(*team_id, div_str); + } + } + } + } + } + // Display the season teams in a table let mut tw = TabWriter::new(stdout()); - writeln!(&mut tw,"Team\tRecord").map_err(|e| e.to_string())?; + let mut header = String::from("Team"); + if show_conference { header.push_str("\tConference"); } + if show_division { header.push_str("\tDivision"); } + header.push_str("\tRecord"); + writeln!(&mut tw, "{}", header).map_err(|e| e.to_string())?; for (id, team) in season.teams().iter() { let matchups = season.team_matchups(*id)?; + let mut prefix = team.name().to_string(); + if show_conference { + let conf = team_conference.get(id).map(|s| s.as_str()).unwrap_or("-"); + prefix.push_str(&format!("\t{}", conf)); + } + if show_division { + let div = team_division.get(id).map(|s| s.as_str()).unwrap_or("-"); + prefix.push_str(&format!("\t{}", div)); + } writeln!( &mut tw, "{}\t{}", - team.name(), matchups.record() + prefix, matchups.record() ).map_err(|e| e.to_string())?; } @@ -58,16 +101,37 @@ pub fn get_season(args: FbsimLeagueSeasonGetArgs) -> Result<(), String> { // Display playoff information let playoffs = season.playoffs(); - let rounds = playoffs.rounds(); - if !rounds.is_empty() { + if playoffs.started() { writeln!(&mut tw, "\nPlayoffs ({} teams)", playoffs.num_teams()).map_err(|e| e.to_string())?; - writeln!(&mut tw, "Round\tMatchups\tSimulated").map_err(|e| e.to_string())?; - for (i, round) in rounds.iter().enumerate() { - let simulated = round.matchups().iter().filter(|m| m.context().game_over()).count(); - writeln!( - &mut tw, "{}\t{}\t{}", - i, round.matchups().len(), simulated - ).map_err(|e| e.to_string())?; + + // Display conference brackets + for (conf_index, rounds) in playoffs.conference_brackets() { + let conf_name = season.conferences().get(*conf_index) + .map(|c| c.name().to_string()) + .unwrap_or_else(|| format!("Conference {}", conf_index)); + writeln!(&mut tw, "\n{}", conf_name).map_err(|e| e.to_string())?; + writeln!(&mut tw, "Round\tMatchups\tSimulated").map_err(|e| e.to_string())?; + for (i, round) in rounds.iter().enumerate() { + let simulated = round.matchups().iter().filter(|m| m.context().game_over()).count(); + writeln!( + &mut tw, "{}\t{}\t{}", + i, round.matchups().len(), simulated + ).map_err(|e| e.to_string())?; + } + } + + // Display winners bracket + let winners = playoffs.winners_bracket(); + if !winners.is_empty() { + writeln!(&mut tw, "\nChampionship Bracket").map_err(|e| e.to_string())?; + writeln!(&mut tw, "Round\tMatchups\tSimulated").map_err(|e| e.to_string())?; + for (i, round) in winners.iter().enumerate() { + let simulated = round.matchups().iter().filter(|m| m.context().game_over()).count(); + writeln!( + &mut tw, "{}\t{}\t{}", + i, round.matchups().len(), simulated + ).map_err(|e| e.to_string())?; + } } // Display champion if playoffs are complete @@ -76,7 +140,7 @@ pub fn get_season(args: FbsimLeagueSeasonGetArgs) -> Result<(), String> { let champion = season.team(champion_id).unwrap(); writeln!(&mut tw, "\nChampion: {}", champion.name()).map_err(|e| e.to_string())?; } - } else if playoffs.started() { + } else { writeln!(&mut tw, "\nPlayoffs in progress").map_err(|e| e.to_string())?; } } diff --git a/src/league/season/playoffs.rs b/src/league/season/playoffs.rs index 58fcb00..8ede8b0 100644 --- a/src/league/season/playoffs.rs +++ b/src/league/season/playoffs.rs @@ -1,3 +1,4 @@ +pub mod display; pub mod gen; pub mod get; pub mod picture; diff --git a/src/league/season/playoffs/display.rs b/src/league/season/playoffs/display.rs new file mode 100644 index 0000000..5c91014 --- /dev/null +++ b/src/league/season/playoffs/display.rs @@ -0,0 +1,87 @@ +use std::io::{Write, stdout}; + +use fbsim_core::league::season::LeagueSeason; +use tabwriter::TabWriter; + +pub fn display_playoffs(season: &LeagueSeason) -> Result<(), String> { + let playoffs = season.playoffs(); + + // Display conference brackets + for (conf_index, rounds) in playoffs.conference_brackets() { + let conf_name = season.conferences().get(*conf_index) + .map(|c| c.name().to_string()) + .unwrap_or_else(|| format!("Conference {}", conf_index)); + println!("=== {} Conference Playoffs ===", conf_name); + + // Display conference bracket rounds + for (round_index, round) in rounds.iter().enumerate() { + println!("--- Round {} ---", round_index); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Matchup\tAway Team\tAway Score\tHome Team\tHome Score\tStatus").map_err(|e| e.to_string())?; + for (matchup_index, matchup) in round.matchups().iter().enumerate() { + let away_team = season.team(*matchup.away_team()).unwrap().name(); + let home_team = season.team(*matchup.home_team()).unwrap().name(); + let context = matchup.context(); + let status = if context.game_over() { + "Final" + } else if context.started() { + "In Progress" + } else { + "Pending" + }; + writeln!( + &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", + matchup_index, + away_team, context.away_score(), + home_team, context.home_score(), + status + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + println!(); + } + } + + // Display winners bracket + let winners = playoffs.winners_bracket(); + if !winners.is_empty() { + println!("=== Championship Bracket ==="); + for (round_index, round) in winners.iter().enumerate() { + println!("--- Round {} ---", round_index); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Matchup\tAway Team\tAway Score\tHome Team\tHome Score\tStatus").map_err(|e| e.to_string())?; + for (matchup_index, matchup) in round.matchups().iter().enumerate() { + let away_team = season.team(*matchup.away_team()).unwrap().name(); + let home_team = season.team(*matchup.home_team()).unwrap().name(); + let context = matchup.context(); + let status = if context.game_over() { + "Final" + } else if context.started() { + "In Progress" + } else { + "Pending" + }; + writeln!( + &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", + matchup_index, + away_team, context.away_score(), + home_team, context.home_score(), + status + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + println!(); + } + } + + // Display champion if playoffs are complete + if playoffs.complete() { + if let Some(champion_id) = playoffs.champion() { + let champion = season.team(champion_id).unwrap(); + println!("Champion: {}", champion.name()); + } + } else { + println!("Playoffs in progress"); + } + Ok(()) +} diff --git a/src/league/season/playoffs/gen.rs b/src/league/season/playoffs/gen.rs index 2ad14ef..2eb7b60 100644 --- a/src/league/season/playoffs/gen.rs +++ b/src/league/season/playoffs/gen.rs @@ -1,6 +1,7 @@ use std::fs; use fbsim_core::league::League; +use fbsim_core::league::season::LeagueSeasonPlayoffOptions; use crate::cli::league::season::playoffs::FbsimLeagueSeasonPlayoffsGenArgs; @@ -27,9 +28,36 @@ pub fn gen_playoffs(args: FbsimLeagueSeasonPlayoffsGenArgs) -> Result<(), String // Generate the playoffs let mut rng = rand::thread_rng(); - if let Err(e) = season.generate_playoffs(args.num_teams, &mut rng) { - return Err(format!("Failed to generate playoffs: {}", e)); - } + let mut options = LeagueSeasonPlayoffOptions::new(); + let result_msg = if args.per_conference { + // Validate conferences exist + if season.conferences().is_empty() { + return Err(String::from( + "Per-conference playoffs (-p) require conferences to be defined. \ + Use 'league season conference add' first." + )); + } + + // Generate conference playoffs + options.use_conference_brackets = true; + options.playoff_teams_per_conference = args.num_teams; + options.division_winners_guaranteed = args.division_winners; + if let Err(e) = season.generate_playoffs(options, &mut rng) { + return Err(format!("Failed to generate playoffs: {}", e)); + } + let num_conferences = season.conferences().len(); + format!( + "Conference playoffs generated with {} teams per conference ({} conferences)", + args.num_teams, num_conferences + ) + } else { + // Generate traditional playoffs + options.num_playoff_teams = args.num_teams; + if let Err(e) = season.generate_playoffs(options, &mut rng) { + return Err(format!("Failed to generate playoffs: {}", e)); + } + format!("Playoffs generated with {} teams", args.num_teams) + }; // Serialize the league as JSON let league_res = serde_json::to_string_pretty(&league); @@ -43,7 +71,6 @@ pub fn gen_playoffs(args: FbsimLeagueSeasonPlayoffsGenArgs) -> Result<(), String if let Err(e) = write_res { return Err(format!("Error writing league file: {}", e)); } - - println!("Playoffs generated with {} teams", args.num_teams); + println!("{}", result_msg); Ok(()) } diff --git a/src/league/season/playoffs/get.rs b/src/league/season/playoffs/get.rs index 8b6b22d..1f2bb3c 100644 --- a/src/league/season/playoffs/get.rs +++ b/src/league/season/playoffs/get.rs @@ -1,12 +1,11 @@ use std::fs; -use std::io::{Write, stdout}; use fbsim_core::league::League; use crate::cli::league::season::playoffs::FbsimLeagueSeasonPlayoffsGetArgs; +use crate::league::season::playoffs::display; use serde_json; -use tabwriter::TabWriter; pub fn get_playoffs(args: FbsimLeagueSeasonPlayoffsGetArgs) -> Result<(), String> { // Load the league from its file @@ -27,56 +26,9 @@ pub fn get_playoffs(args: FbsimLeagueSeasonPlayoffsGetArgs) -> Result<(), String None => return Err(format!("No season found with year: {}", args.year)), }; - // Get the playoffs + // Display general playoff info let playoffs = season.playoffs(); - let rounds = playoffs.rounds(); - - if rounds.is_empty() { - println!("Playoffs have not been generated for the {} season", args.year); - return Ok(()); - } - - // Display playoff status println!("Playoffs for {} season ({} teams)", args.year, playoffs.num_teams()); println!(); - - // Display each round - for (round_idx, round) in rounds.iter().enumerate() { - println!("Round {}", round_idx); - let mut tw = TabWriter::new(stdout()); - writeln!(&mut tw, "Matchup\tAway Team\tAway Score\tHome Team\tHome Score\tStatus").map_err(|e| e.to_string())?; - for (matchup_idx, matchup) in round.matchups().iter().enumerate() { - let away_team = season.team(*matchup.away_team()).unwrap().name(); - let home_team = season.team(*matchup.home_team()).unwrap().name(); - let context = matchup.context(); - let status = if context.game_over() { - "Final" - } else if context.started() { - "In Progress" - } else { - "Pending" - }; - writeln!( - &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", - matchup_idx, - away_team, context.away_score(), - home_team, context.home_score(), - status - ).map_err(|e| e.to_string())?; - } - tw.flush().map_err(|e| e.to_string())?; - println!(); - } - - // Display champion if playoffs are complete - if playoffs.complete() { - if let Some(champion_id) = playoffs.champion() { - let champion = season.team(champion_id).unwrap(); - println!("Champion: {}", champion.name()); - } - } else { - println!("Playoffs in progress"); - } - - Ok(()) + display::display_playoffs(season) } diff --git a/src/league/season/playoffs/picture.rs b/src/league/season/playoffs/picture.rs index b307b57..f66f622 100644 --- a/src/league/season/playoffs/picture.rs +++ b/src/league/season/playoffs/picture.rs @@ -2,7 +2,8 @@ use std::fs; use std::io::{Write, stdout}; use fbsim_core::league::League; -use fbsim_core::league::season::playoffs::picture::PlayoffStatus; +use fbsim_core::league::season::LeagueSeason; +use fbsim_core::league::season::playoffs::picture::{PlayoffPicture, PlayoffPictureOptions, PlayoffStatus}; use crate::cli::league::season::playoffs::FbsimLeagueSeasonPlayoffsPictureArgs; @@ -43,22 +44,212 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul |w| !w.complete() ).count(); - // Get the playoff picture - let picture = season.playoff_picture(args.num_playoff_teams)?; + // Determine if we should use conference-based playoff picture + let has_conferences = !season.conferences().is_empty(); + if args.per_conference { + if !has_conferences { + return Err(String::from( + "Per-conference playoff picture (-p) requires conferences to be defined. \ + Use 'league season conference add' first." + )); + } + display_conference_playoff_picture(season, &args, weeks_remaining)?; + } else if has_conferences && args.conference.is_some() { + // Display single conference + let conf_index = args.conference.unwrap(); + display_single_conference_picture(season, conf_index, &args, weeks_remaining)?; + } else { + // Display traditional playoff picture + display_traditional_playoff_picture(season, &args, weeks_remaining)?; + } + Ok(()) +} + +fn display_traditional_playoff_picture( + season: &LeagueSeason, + args: &FbsimLeagueSeasonPlayoffsPictureArgs, + weeks_remaining: usize +) -> Result<(), String> { + // Get the playoff picture (explicitly non-conference) + let options = PlayoffPictureOptions { + by_conference: Some(false), + division_winners_guaranteed: args.division_winners, + }; + let picture = PlayoffPicture::from_season(season, args.num_playoff_teams, Some(options))?; + + // Display playoff picture + println!("Playoff Picture for {} Season", args.year); + println!("Top {} teams make the playoffs", args.num_playoff_teams); + println!("Weeks remaining in season: {}", weeks_remaining); + println!(); + display_playoff_picture_sections(&picture)?; + display_legend(); + Ok(()) +} + +fn display_conference_playoff_picture( + season: &LeagueSeason, + args: &FbsimLeagueSeasonPlayoffsPictureArgs, + weeks_remaining: usize +) -> Result<(), String> { + let conferences = season.conferences(); // Display header println!("Playoff Picture for {} Season", args.year); + println!("{} teams per conference make the playoffs", args.num_playoff_teams); + println!("Weeks remaining in season: {}", weeks_remaining); + println!(); + + // Get the conference-based playoff picture + let options = PlayoffPictureOptions { + by_conference: Some(true), + division_winners_guaranteed: args.division_winners, + }; + let picture = PlayoffPicture::from_season( + season, + args.num_playoff_teams, + Some(options) + )?; + for (conf_index, conference) in conferences.iter().enumerate() { + // Skip if filtering to specific conference + if let Some(filter_conf) = args.conference { + if filter_conf != conf_index { + continue; + } + } + + // Display the playoff-picture + println!("=== {} Playoff Picture ===", conference.name()); + display_conference_playoff_picture_sections(&picture, season, conf_index)?; + } + display_legend(); + Ok(()) +} + +fn display_conference_playoff_picture_sections( + picture: &PlayoffPicture, + season: &LeagueSeason, + conf_index: usize +) -> Result<(), String> { + let conference = match season.conferences().get(conf_index) { + Some(c) => c, + None => return Err(format!("No conference found with ID: {}", conf_index)), + }; + let conf_teams: Vec = conference.all_teams(); + + // Display teams in playoff position that are in this conference + let binding_playoff = picture.playoff_teams(); + let playoff_teams: Vec<_> = binding_playoff.iter() + .filter(|e| conf_teams.contains(&e.team_id())) + .collect(); + if !playoff_teams.is_empty() { + println!("IN PLAYOFF POSITION"); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Seed\tTeam\tRecord\tStatus\tMagic #").map_err(|e| e.to_string())?; + for (i, entry) in playoff_teams.iter().enumerate() { + let status_str = format_status_indicator(entry.status()); + let magic_str = match entry.magic_number() { + Some(0) => "X".to_string(), + Some(m) => m.to_string(), + None => "-".to_string(), + }; + writeln!( + &mut tw, + "{}\t{}\t{}\t{}\t{}", + i + 1, + entry.team_name(), + entry.current_record(), + status_str, + magic_str + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + println!(); + } + + // Display teams in the hunt that are in this conference + let binding_hunt = picture.in_the_hunt(); + let in_the_hunt: Vec<_> = binding_hunt.iter() + .filter(|e| conf_teams.contains(&e.team_id())) + .collect(); + if !in_the_hunt.is_empty() { + println!("IN THE HUNT"); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Team\tRecord\tGB\tRemaining").map_err(|e| e.to_string())?; + for entry in in_the_hunt.iter() { + writeln!( + &mut tw, + "{}\t{}\t{:.1}\t{}", + entry.team_name(), + entry.current_record(), + entry.games_back(), + entry.remaining_games() + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + println!(); + } + + // Display eliminated teams that are in this conference + let binding_elim = picture.eliminated_teams(); + let eliminated: Vec<_> = binding_elim.iter() + .filter(|e| conf_teams.contains(&e.team_id())) + .collect(); + if !eliminated.is_empty() { + println!("ELIMINATED"); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Team\tRecord\tGB").map_err(|e| e.to_string())?; + for entry in eliminated.iter() { + writeln!( + &mut tw, + "{}\t{}\t{:.1}", + entry.team_name(), + entry.current_record(), + entry.games_back() + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + println!(); + } + Ok(()) +} + +fn display_single_conference_picture( + season: &LeagueSeason, + conf_index: usize, + args: &FbsimLeagueSeasonPlayoffsPictureArgs, + weeks_remaining: usize +) -> Result<(), String> { + let conferences = season.conferences(); + let conference = match conferences.get(conf_index) { + Some(c) => c, + None => return Err(format!("No conference found with ID: {}", conf_index)), + }; + + // Display header + println!("{} Playoff Picture for {} Season", conference.name(), args.year); println!("Top {} teams make the playoffs", args.num_playoff_teams); println!("Weeks remaining in season: {}", weeks_remaining); println!(); + // Use regular playoff picture but filtered to conference teams + let options = PlayoffPictureOptions { + by_conference: Some(false), + division_winners_guaranteed: args.division_winners, + }; + let picture = PlayoffPicture::from_season(season, args.num_playoff_teams, Some(options))?; + display_playoff_picture_sections(&picture)?; + display_legend(); + Ok(()) +} + +fn display_playoff_picture_sections(picture: &PlayoffPicture) -> Result<(), String> { // Display teams in playoff position let playoff_teams = picture.playoff_teams(); if !playoff_teams.is_empty() { println!("IN PLAYOFF POSITION"); let mut tw = TabWriter::new(stdout()); writeln!(&mut tw, "Seed\tTeam\tRecord\tStatus\tMagic #").map_err(|e| e.to_string())?; - for (i, entry) in playoff_teams.iter().enumerate() { let status_str = format_status_indicator(entry.status()); let magic_str = match entry.magic_number() { @@ -86,7 +277,6 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul println!("IN THE HUNT"); let mut tw = TabWriter::new(stdout()); writeln!(&mut tw, "Team\tRecord\tGB\tRemaining").map_err(|e| e.to_string())?; - for entry in in_the_hunt.iter() { writeln!( &mut tw, @@ -107,7 +297,6 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul println!("ELIMINATED"); let mut tw = TabWriter::new(stdout()); writeln!(&mut tw, "Team\tRecord\tGB").map_err(|e| e.to_string())?; - for entry in eliminated.iter() { writeln!( &mut tw, @@ -120,14 +309,15 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul tw.flush().map_err(|e| e.to_string())?; println!(); } + Ok(()) +} - // Display legend +fn display_legend() { println!("Legend:"); println!(" z- = Clinched #1 seed"); println!(" x- = Clinched playoff berth"); println!(" GB = Games behind playoff cutoff"); println!(" Magic # = Wins needed to clinch (X = clinched)"); - Ok(()) } fn format_status_indicator(status: &PlayoffStatus) -> String { diff --git a/src/league/season/playoffs/round.rs b/src/league/season/playoffs/round.rs index 6abd444..45ae322 100644 --- a/src/league/season/playoffs/round.rs +++ b/src/league/season/playoffs/round.rs @@ -1,3 +1,4 @@ +pub mod display; pub mod get; pub mod matchup; pub mod sim; diff --git a/src/league/season/playoffs/round/display.rs b/src/league/season/playoffs/round/display.rs new file mode 100644 index 0000000..f9bcb63 --- /dev/null +++ b/src/league/season/playoffs/round/display.rs @@ -0,0 +1,156 @@ +use std::io::{Write, stdout}; + +use fbsim_core::league::season::LeagueSeason; +use tabwriter::TabWriter; + +pub fn display_traditional_round( + season: &LeagueSeason, + round_index: usize, + year: usize +) -> Result<(), String> { + let playoffs = season.playoffs(); + let brackets = playoffs.conference_brackets(); + if brackets.is_empty() { + return Err(format!("Playoffs have not been generated for the {} season", year)); + } + + // For non-conference playoffs, use the first (only) bracket + let rounds = match brackets.values().next() { + Some(r) => r, + None => return Err(format!("No playoff bracket found for the {} season", year)), + }; + let round = match rounds.get(round_index) { + Some(r) => r, + None => return Err(format!("No playoff round found with ID: {}", round_index)), + }; + + // Display the round + println!("=== Playoff Round {} ===", round_index); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Matchup\tAway Team\tAway Score\tHome Team\tHome Score\tStatus").map_err(|e| e.to_string())?; + for (matchup_index, matchup) in round.matchups().iter().enumerate() { + let away_team = season.team(*matchup.away_team()).unwrap().name(); + let home_team = season.team(*matchup.home_team()).unwrap().name(); + let context = matchup.context(); + let status = if context.game_over() { + "Final" + } else if context.started() { + "In Progress" + } else { + "Pending" + }; + writeln!( + &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", + matchup_index, + away_team, context.away_score(), + home_team, context.home_score(), + status + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + Ok(()) +} + +pub fn display_conference_round( + season: &LeagueSeason, + round_index: usize, + filter_conference: Option, + year: usize +) -> Result<(), String> { + let playoffs = season.playoffs(); + let conference_brackets = playoffs.conference_brackets(); + let conferences = season.conferences(); + let num_conferences = conferences.len(); + if conference_brackets.is_empty() { + return Err(format!("Conference playoffs have not been generated for the {} season", year)); + } + + for (conf_index, conf_rounds) in conference_brackets.iter() { + // Skip if filtering to specific conference + if let Some(filter) = filter_conference { + if filter != *conf_index { + continue; + } + } + + // Get the number of conference rounds + let conf_name = conferences.get(*conf_index) + .map(|c| c.name().to_string()) + .unwrap_or_else(|| format!("Conference {}", conf_index)); + if let Some(round) = conf_rounds.get(round_index) { + println!("=== {} Conference Playoffs ===", conf_name); + println!("--- Round {} ---", round_index); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Matchup\tAway Team\tAway Score\tHome Team\tHome Score\tStatus").map_err(|e| e.to_string())?; + for (matchup_index, matchup) in round.matchups().iter().enumerate() { + let away_team = season.team(*matchup.away_team()).unwrap().name(); + let home_team = season.team(*matchup.home_team()).unwrap().name(); + let context = matchup.context(); + let status = if context.game_over() { + "Final" + } else if context.started() { + "In Progress" + } else { + "Pending" + }; + writeln!( + &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", + matchup_index, + away_team, context.away_score(), + home_team, context.home_score(), + status + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + } else if filter_conference.is_some() { + return Err(format!("No round {} found for conference {}", round_index, conf_name)); + } else { + return Err(format!("No round {} found in conference playoffs", round_index)); + } + if *conf_index != (num_conferences - 1) { + println!(); + } + } + Ok(()) +} + +pub fn display_winners_bracket_round( + season: &LeagueSeason, + round_index: usize, + year: usize +) -> Result<(), String> { + let playoffs = season.playoffs(); + let winners_bracket = playoffs.winners_bracket(); + if winners_bracket.is_empty() { + return Err(format!("Winners' bracket has not been generated for the {} season", year)); + } + if let Some(round) = winners_bracket.get(round_index) { + println!("=== Championship Bracket ==="); + println!("--- Round {} ---", round_index); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Matchup\tAway Team\tAway Score\tHome Team\tHome Score\tStatus").map_err(|e| e.to_string())?; + for (matchup_index, matchup) in round.matchups().iter().enumerate() { + let away_team = season.team(*matchup.away_team()).unwrap().name(); + let home_team = season.team(*matchup.home_team()).unwrap().name(); + let context = matchup.context(); + let status = if context.game_over() { + "Final" + } else if context.started() { + "In Progress" + } else { + "Pending" + }; + writeln!( + &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", + matchup_index, + away_team, context.away_score(), + home_team, context.home_score(), + status + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + } else { + return Err(format!("No round {} found in winners' bracket", round_index)); + } + Ok(()) +} diff --git a/src/league/season/playoffs/round/get.rs b/src/league/season/playoffs/round/get.rs index 9e7f914..6b93c7f 100644 --- a/src/league/season/playoffs/round/get.rs +++ b/src/league/season/playoffs/round/get.rs @@ -1,12 +1,11 @@ use std::fs; -use std::io::{Write, stdout}; use fbsim_core::league::League; use crate::cli::league::season::playoffs::round::FbsimLeagueSeasonPlayoffsRoundGetArgs; +use crate::league::season::playoffs::round::display; use serde_json; -use tabwriter::TabWriter; pub fn get_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundGetArgs) -> Result<(), String> { // Load the league from its file @@ -21,50 +20,21 @@ pub fn get_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundGetArgs) -> Result Err(error) => return Err(format!("Error loading league from file: {}", error)), }; - // Get the season + // Get the season and playoffs let season = match league.season(args.year) { Some(season) => season, None => return Err(format!("No season found with year: {}", args.year)), }; - - // Get the playoffs let playoffs = season.playoffs(); - let rounds = playoffs.rounds(); - - if rounds.is_empty() { - return Err(format!("Playoffs have not been generated for the {} season", args.year)); - } - // Get the round - let round = match rounds.get(args.round) { - Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", args.round)), - }; - - // Display the round - println!("Playoff Round {} - {} Season", args.round, args.year); - let mut tw = TabWriter::new(stdout()); - writeln!(&mut tw, "Matchup\tAway Team\tAway Score\tHome Team\tHome Score\tStatus").map_err(|e| e.to_string())?; - for (matchup_idx, matchup) in round.matchups().iter().enumerate() { - let away_team = season.team(*matchup.away_team()).unwrap().name(); - let home_team = season.team(*matchup.home_team()).unwrap().name(); - let context = matchup.context(); - let status = if context.game_over() { - "Final" - } else if context.started() { - "In Progress" + // Display the playoff round + if playoffs.is_conference_playoff() { + if args.winners_bracket { + display::display_winners_bracket_round(season, args.round, args.year) } else { - "Pending" - }; - writeln!( - &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", - matchup_idx, - away_team, context.away_score(), - home_team, context.home_score(), - status - ).map_err(|e| e.to_string())?; + display::display_conference_round(season, args.round, args.conference, args.year) + } + } else { + display::display_traditional_round(season, args.round, args.year) } - tw.flush().map_err(|e| e.to_string())?; - - Ok(()) } diff --git a/src/league/season/playoffs/round/matchup/get.rs b/src/league/season/playoffs/round/matchup/get.rs index 877c2e5..418c6a2 100644 --- a/src/league/season/playoffs/round/matchup/get.rs +++ b/src/league/season/playoffs/round/matchup/get.rs @@ -27,22 +27,30 @@ pub fn get_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupGetArgs) // Get the playoffs let playoffs = season.playoffs(); - let rounds = playoffs.rounds(); - if rounds.is_empty() { - return Err(format!("Playoffs have not been generated for the {} season", args.year)); - } - - // Get the round - let round = match rounds.get(args.round) { - Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", args.round)), + // Get the round from the appropriate bracket + let round = if args.winners_bracket { + let winners = playoffs.winners_bracket(); + match winners.get(args.round) { + Some(r) => r, + None => return Err(format!("No winners bracket round found with ID: {}", args.round)), + } + } else { + let conf_index = args.conference.unwrap_or(0); + let bracket = match playoffs.conference_bracket(conf_index) { + Some(b) => b, + None => return Err(format!("No conference bracket found with ID: {}", conf_index)), + }; + match bracket.get(args.round) { + Some(r) => r, + None => return Err(format!("No playoff round found with ID: {}", args.round)), + } }; // Get the matchup let matchup = match round.matchups().get(args.matchup) { Some(m) => m, - None => return Err(format!("No matchup found with index: {}", args.matchup)), + None => return Err(format!("No matchup found with ID: {}", args.matchup)), }; // Get team names @@ -50,15 +58,26 @@ pub fn get_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupGetArgs) let home_team = season.team(*matchup.home_team()).unwrap(); let context = matchup.context(); - // Display matchup info - println!("Playoff Round {} Matchup {}", args.round, args.matchup); + // Display matchup header based on bracket type + let header = if args.winners_bracket { + format!("Championship round {} matchup {}", args.round, args.matchup) + } else if playoffs.is_conference_playoff() { + let conf_index = args.conference.unwrap_or(0); + let conf_name = season.conferences().get(conf_index) + .map(|c| c.name().to_string()) + .unwrap_or_else(|| format!("Conference {}", conf_index)); + format!("{} conference playoff round {} matchup {}", conf_name, args.round, args.matchup) + } else { + format!("Playoff round {} matchup {}", args.round, args.matchup) + }; + println!("{}", header); println!(); println!("{} @ {}", away_team.name(), home_team.name()); println!(); - println!("{}", context); - // Display stats if game is complete if context.game_over() { + println!("{} Final", context); + if let Some(home_stats) = matchup.home_stats() { println!(); println!("{} stats\n{}", context.home_team_short(), home_stats); @@ -67,6 +86,17 @@ pub fn get_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupGetArgs) println!(); println!("{} stats\n{}", context.away_team_short(), away_stats); } + } else if context.started() { + // Display play-by-play log up to this point + if let Some(game) = matchup.game() { + for drive in game.drives().iter() { + println!("{}\n", drive); + } + } else { + println!("{}", context); + } + } else { + println!("{} Pending", context); } Ok(()) diff --git a/src/league/season/playoffs/round/matchup/sim.rs b/src/league/season/playoffs/round/matchup/sim.rs index 46d83d8..1560739 100644 --- a/src/league/season/playoffs/round/matchup/sim.rs +++ b/src/league/season/playoffs/round/matchup/sim.rs @@ -5,7 +5,8 @@ use std::{thread, time}; use crossterm::{cursor, terminal, QueueableCommand}; use fbsim_core::league::League; -use fbsim_core::game::play::Game; +use fbsim_core::league::season::matchup::LeagueSeasonMatchup; +use fbsim_core::game::play::{Drive, Game}; use fbsim_core::game::play::result::{PlayResult, PlayTypeResult}; use crate::cli::league::season::playoffs::round::matchup::FbsimLeagueSeasonPlayoffsRoundMatchupSimArgs; @@ -38,9 +39,16 @@ pub fn sim_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupSimArgs) Some(s) => s, None => return Err(String::from("No current season found")), }; - match season.sim_playoff_play(args.round, args.matchup, &mut rng) { - Ok(game_opt) => game_opt, - Err(error) => return Err(format!("Error simulating next play for playoff matchup: {}", error)), + if args.winners_bracket { + match season.sim_winners_bracket_play(args.round, args.matchup, &mut rng) { + Ok(game_opt) => game_opt, + Err(error) => return Err(format!("Error simulating next play for winners bracket matchup: {}", error)), + } + } else { + match season.sim_playoff_play(args.conference, args.round, args.matchup, &mut rng) { + Ok(game_opt) => game_opt, + Err(error) => return Err(format!("Error simulating next play for playoff matchup: {}", error)), + } } }; @@ -49,15 +57,8 @@ pub fn sim_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupSimArgs) Some(s) => s, None => return Err(String::from("No current season found after simulating play")) }; - let round = match season.playoffs().rounds().get(args.round) { - Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", args.round)), - }; - let matchup = match round.matchups().get(args.matchup) { - Some(m) => m, - None => return Err(format!("No matchup found with index: {}", args.matchup)), - }; - let drive_opt = if let Some(g) = game_opt.as_ref() { + let matchup = get_matchup(season.playoffs(), &args)?; + let drive_opt: Option<&Drive> = if let Some(g) = game_opt.as_ref() { g.drives().last() } else { match matchup.game() { @@ -122,14 +123,7 @@ pub fn sim_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupSimArgs) Some(s) => s, None => return Err(String::from("No current season found after simulating game")) }; - let round = match season.playoffs().rounds().get(args.round) { - Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", args.round)), - }; - let matchup = match round.matchups().get(args.matchup) { - Some(m) => m, - None => return Err(format!("No matchup found with index: {}", args.matchup)), - }; + let matchup = get_matchup(season.playoffs(), &args)?; let home_stats = match matchup.home_stats() { Some(s) => s, None => return Err(String::from("Failed to get home stats after simulating game")) @@ -153,19 +147,13 @@ pub fn sim_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupSimArgs) away_stats ); - // Generate the next round if this matchup completed the round and playoffs are not done - let round_complete = round.complete(); - let playoffs_complete = season.playoffs().complete(); - - if round_complete && !playoffs_complete { + // Try to generate the next round if playoffs are not yet complete + if !season.playoffs().complete() { let season = match league.current_season_mut() { Some(s) => s, None => return Err(String::from("No current season found for generating next round")), }; - if let Err(e) = season.generate_next_playoff_round(&mut rng) { - return Err(format!("Failed to generate next playoff round: {}", e)); - } - println!("\nNext playoff round generated!"); + season.generate_next_playoff_round(&mut rng)?; } // Serialize the league as JSON @@ -182,3 +170,29 @@ pub fn sim_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupSimArgs) } Ok(()) } + +fn get_matchup<'a>( + playoffs: &'a fbsim_core::league::season::playoffs::LeagueSeasonPlayoffs, + args: &FbsimLeagueSeasonPlayoffsRoundMatchupSimArgs +) -> Result<&'a LeagueSeasonMatchup, String> { + let round = if args.winners_bracket { + let winners = playoffs.winners_bracket(); + match winners.get(args.round) { + Some(r) => r, + None => return Err(format!("No winners bracket round found with ID: {}", args.round)), + } + } else { + let bracket = match playoffs.conference_bracket(args.conference) { + Some(b) => b, + None => return Err(format!("No conference bracket found with ID: {}", args.conference)), + }; + match bracket.get(args.round) { + Some(r) => r, + None => return Err(format!("No playoff round found with ID: {}", args.round)), + } + }; + match round.matchups().get(args.matchup) { + Some(m) => Ok(m), + None => Err(format!("No matchup found with ID: {}", args.matchup)), + } +} diff --git a/src/league/season/playoffs/round/sim.rs b/src/league/season/playoffs/round/sim.rs index 2256a48..e5806c9 100644 --- a/src/league/season/playoffs/round/sim.rs +++ b/src/league/season/playoffs/round/sim.rs @@ -1,12 +1,12 @@ use std::fs; -use std::io::{Write, stdout}; use fbsim_core::league::League; +use fbsim_core::league::season::LeagueSeason; use crate::cli::league::season::playoffs::round::FbsimLeagueSeasonPlayoffsRoundSimArgs; +use crate::league::season::playoffs::round::display; use serde_json; -use tabwriter::TabWriter; pub fn sim_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundSimArgs) -> Result<(), String> { // Load the league from its file as mutable @@ -27,47 +27,35 @@ pub fn sim_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundSimArgs) -> Result None => return Err(String::from("No current season found")), }; - // Simulate the playoff round let mut rng = rand::thread_rng(); - if let Err(e) = season.sim_playoff_round(args.round, &mut rng) { - return Err(format!("Failed to simulate playoff round: {}", e)); - } + let is_conference_playoff = season.playoffs().is_conference_playoff(); + let year = *season.year(); - // Generate the next round if the current round is complete and playoffs are not done - let playoffs = season.playoffs(); - let round_complete = playoffs.rounds().get(args.round).map(|r| r.complete()).unwrap_or(false); - let playoffs_complete = playoffs.complete(); + // Determine the current round and whether it's a winners bracket round + let (round_index, is_winners_bracket) = find_current_round(season)?; - if round_complete && !playoffs_complete { - if let Err(e) = season.generate_next_playoff_round(&mut rng) { - return Err(format!("Failed to generate next playoff round: {}", e)); + // Simulate the round + if is_winners_bracket { + if let Err(e) = season.sim_winners_bracket_round(round_index, &mut rng) { + return Err(format!("Failed to simulate winners bracket round: {}", e)); } + } else if let Err(e) = season.sim_playoff_round(round_index, &mut rng) { + return Err(format!("Failed to simulate playoff round: {}", e)); } - // Get the year for display - let year = *season.year(); - - // Display results - let round = match season.playoffs().rounds().get(args.round) { - Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", args.round)), - }; + // Try to generate the next round if playoffs are not yet complete. + if !season.playoffs().complete() { + let _ = season.generate_next_playoff_round(&mut rng); + } - println!("Playoff Round {} Results", args.round); - let mut tw = TabWriter::new(stdout()); - writeln!(&mut tw, "Matchup\tAway Team\tAway Score\tHome Team\tHome Score").map_err(|e| e.to_string())?; - for (matchup_idx, matchup) in round.matchups().iter().enumerate() { - let away_team = season.team(*matchup.away_team()).unwrap().name(); - let home_team = season.team(*matchup.home_team()).unwrap().name(); - let context = matchup.context(); - writeln!( - &mut tw, "{}\t{}\t{}\t{}\t{}", - matchup_idx, - away_team, context.away_score(), - home_team, context.home_score() - ).map_err(|e| e.to_string())?; + // Display results using the same format as the get command + if is_winners_bracket { + display::display_winners_bracket_round(season, round_index, year)?; + } else if is_conference_playoff { + display::display_conference_round(season, round_index, None, year)?; + } else { + display::display_traditional_round(season, round_index, year)?; } - tw.flush().map_err(|e| e.to_string())?; // Display champion if playoffs are complete if season.playoffs().complete() { @@ -89,7 +77,40 @@ pub fn sim_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundSimArgs) -> Result if let Err(e) = write_res { return Err(format!("Error writing league file: {}", e)); } - - let _ = year; // suppress unused warning Ok(()) } + +/// Find the current incomplete round. Returns (round_index, is_winners_bracket). +fn find_current_round( + season: &LeagueSeason +) -> Result<(usize, bool), String> { + let playoffs = season.playoffs(); + if playoffs.complete() { + return Err(String::from("Playoffs are already complete")); + } + + // Check conference brackets first + if !playoffs.conference_brackets_complete() { + // Find the first incomplete round across conference brackets + for (_conf_index, rounds) in playoffs.conference_brackets().iter() { + for (round_index, round) in rounds.iter().enumerate() { + if !round.complete() { + return Ok((round_index, false)); + } + } + } + return Err(String::from("All conference playoff rounds are complete")); + } + + // Conference brackets are complete, check winners bracket + let winners_bracket = playoffs.winners_bracket(); + if winners_bracket.is_empty() { + return Err(String::from("Winners bracket has not been generated yet")); + } + for (round_index, round) in winners_bracket.iter().enumerate() { + if !round.complete() { + return Ok((round_index, true)); + } + } + Err(String::from("All playoff rounds are complete")) +} diff --git a/src/league/season/playoffs/sim.rs b/src/league/season/playoffs/sim.rs index b2b2bf8..b58b61f 100644 --- a/src/league/season/playoffs/sim.rs +++ b/src/league/season/playoffs/sim.rs @@ -3,6 +3,7 @@ use std::fs; use fbsim_core::league::League; use crate::cli::league::season::playoffs::FbsimLeagueSeasonPlayoffsSimArgs; +use crate::league::season::playoffs::display; use serde_json; @@ -31,13 +32,8 @@ pub fn sim_playoffs(args: FbsimLeagueSeasonPlayoffsSimArgs) -> Result<(), String return Err(format!("Failed to simulate playoffs: {}", e)); } - // Get the champion - let champion_msg = if let Some(champion_id) = season.playoffs().champion() { - let champion = season.team(champion_id).unwrap(); - format!("Champion: {}", champion.name()) - } else { - String::from("Playoffs complete") - }; + // Display the full playoff results + display::display_playoffs(season)?; // Serialize the league as JSON let league_res = serde_json::to_string_pretty(&league); @@ -51,7 +47,5 @@ pub fn sim_playoffs(args: FbsimLeagueSeasonPlayoffsSimArgs) -> Result<(), String if let Err(e) = write_res { return Err(format!("Error writing league file: {}", e)); } - - println!("{}", champion_msg); Ok(()) } diff --git a/src/league/season/schedule.rs b/src/league/season/schedule.rs index 3ed76cf..886dbb1 100644 --- a/src/league/season/schedule.rs +++ b/src/league/season/schedule.rs @@ -21,11 +21,32 @@ pub fn generate_schedule(args: FbsimLeagueSeasonScheduleGenArgs) -> Result<(), S Err(error) => return Err(format!("Error loading league from file: {}", error)), }; + // Validate conference-based options + let has_conference_options = args.division_games.is_some() + || args.conference_games.is_some() + || args.cross_conference_games.is_some(); + + if has_conference_options { + let season = match league.current_season() { + Some(s) => s, + None => return Err(String::from("No current season found")), + }; + if season.conferences().is_empty() { + return Err(String::from( + "Conference-based schedule options (--division-games, --conference-games, --cross-conference-games) \ + require conferences to be defined. Use 'league season conference add' first." + )); + } + } + // Initialize schedule gen options let options = LeagueSeasonScheduleOptions{ weeks: args.weeks, shift: args.shift, permute: args.permute, + division_games: args.division_games, + conference_games: args.conference_games, + cross_conference_games: args.cross_conference_games, }; // Attempt to generate a schedule for the league diff --git a/src/league/season/standings.rs b/src/league/season/standings.rs new file mode 100644 index 0000000..b5b3ad2 --- /dev/null +++ b/src/league/season/standings.rs @@ -0,0 +1,210 @@ +use std::fs; +use std::io::{Write, stdout}; + +use fbsim_core::league::League; +use fbsim_core::league::season::LeagueSeason; + +use crate::cli::league::season::standings::FbsimLeagueSeasonStandingsArgs; + +use serde_json; +use tabwriter::TabWriter; + +pub fn get_standings(args: FbsimLeagueSeasonStandingsArgs) -> Result<(), String> { + // Validate args: division requires conference + if args.division.is_some() && args.conference.is_none() { + return Err(String::from("Division filter requires conference filter (-c/--conference)")); + } + + // Load the league from its file + let file_res = &fs::read_to_string(&args.league); + let file = match file_res { + Ok(file) => file, + Err(error) => return Err(format!("Error loading league file: {}", error)), + }; + let league_res = serde_json::from_str(file); + let league: League = match league_res { + Ok(league) => league, + Err(error) => return Err(format!("Error loading league from file: {}", error)), + }; + + // Get the season + let season = match league.season(args.year) { + Some(season) => season, + None => return Err(format!("No season found with year: {}", args.year)), + }; + let conferences = season.conferences(); + let has_conferences = !conferences.is_empty(); + + // Display the standings based on args and season structure + if args.by_division { + if !has_conferences { + return Err(String::from("No conferences/divisions defined for this season")); + } + display_standings_by_division(season)?; + } else if args.by_conference { + if !has_conferences { + return Err(String::from("No conferences defined for this season")); + } + display_standings_by_conference(season)?; + } else if let Some(conf_index) = args.conference { + if let Some(div_id) = args.division { + display_division_standings(season, conf_index, div_id)?; + } else { + display_conference_standings(season, conf_index)?; + } + } else { + display_overall_standings(season)?; + } + Ok(()) +} + +fn display_overall_standings(season: &LeagueSeason) -> Result<(), String> { + let standings = season.standings(); + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Rank\tTeam\tRecord").map_err(|e| e.to_string())?; + for (rank, (id, _)) in standings.iter().enumerate() { + let team = season.team(*id).unwrap(); + let record = season.team_matchups(*id) + .map(|m| m.record().to_string()) + .unwrap_or_else(|_| String::from("-")); + writeln!( + &mut tw, "{}\t{}\t{}", + rank + 1, + team.name(), + record + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + Ok(()) +} + +fn display_standings_by_conference( + season: &LeagueSeason +) -> Result<(), String> { + let conferences = season.conferences(); + let num_conferences = conferences.len(); + for (conf_index, conference) in conferences.iter().enumerate() { + println!("=== {} ===", conference.name()); + let standings = season.conference_standings(conf_index)?; + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Rank\tTeam\tRecord").map_err(|e| e.to_string())?; + for (rank, (id, _)) in standings.iter().enumerate() { + let team = season.team(*id).unwrap(); + let record = season.team_matchups(*id) + .map(|m| m.record().to_string()) + .unwrap_or_else(|_| String::from("-")); + writeln!( + &mut tw, "{}\t{}\t{}", + rank + 1, + team.name(), + record + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + if conf_index != (num_conferences - 1) { + println!(); + } + } + Ok(()) +} + +fn display_standings_by_division( + season: &LeagueSeason +) -> Result<(), String> { + let conferences = season.conferences(); + let num_conferences = conferences.len(); + for (conf_index, conference) in conferences.iter().enumerate() { + println!("=== {} ===", conference.name()); + let divisions = conference.divisions(); + let num_divisions = divisions.len(); + for (div_id, division) in divisions.iter().enumerate() { + println!("--- {} ---", division.name()); + let standings = season.division_standings(conf_index, div_id)?; + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Rank\tTeam\tRecord").map_err(|e| e.to_string())?; + for (rank, (id, _)) in standings.iter().enumerate() { + let team = season.team(*id).unwrap(); + let record = season.team_matchups(*id) + .map(|m| m.record().to_string()) + .unwrap_or_else(|_| String::from("-")); + writeln!( + &mut tw, "{}\t{}\t{}", + rank + 1, + team.name(), + record + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + if div_id != (num_divisions - 1) { + println!(); + } + } + if conf_index != (num_conferences - 1) { + println!(); + } + } + Ok(()) +} + +fn display_conference_standings( + season: &LeagueSeason, + conf_index: usize +) -> Result<(), String> { + let conferences = season.conferences(); + let conference = match conferences.get(conf_index) { + Some(c) => c, + None => return Err(format!("No conference found with ID: {}", conf_index)), + }; + println!("=== {} ===", conference.name()); + let standings = season.conference_standings(conf_index)?; + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Rank\tTeam\tRecord").map_err(|e| e.to_string())?; + for (rank, (id, _)) in standings.iter().enumerate() { + let team = season.team(*id).unwrap(); + let record = season.team_matchups(*id) + .map(|m| m.record().to_string()) + .unwrap_or_else(|_| String::from("-")); + writeln!( + &mut tw, "{}\t{}\t{}", + rank + 1, + team.name(), + record + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + Ok(()) +} + +fn display_division_standings( + season: &LeagueSeason, + conf_index: usize, + div_id: usize +) -> Result<(), String> { + let conferences = season.conferences(); + let conference = match conferences.get(conf_index) { + Some(c) => c, + None => return Err(format!("No conference found with ID: {}", conf_index)), + }; + let division = match conference.division(div_id) { + Some(d) => d, + None => return Err(format!("No division found with ID: {}", div_id)), + }; + println!("=== {} {} ===", conference.name(), division.name()); + let standings = season.division_standings(conf_index, div_id)?; + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Rank\tTeam\tRecord").map_err(|e| e.to_string())?; + for (rank, (id, _)) in standings.iter().enumerate() { + let team = season.team(*id).unwrap(); + let record = season.team_matchups(*id) + .map(|m| m.record().to_string()) + .unwrap_or_else(|_| String::from("-")); + writeln!( + &mut tw, "{}\t{}\t{}", + rank + 1, + team.name(), + record + ).map_err(|e| e.to_string())?; + } + tw.flush().map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src/league/season/team.rs b/src/league/season/team.rs index 609bb6b..a890c49 100644 --- a/src/league/season/team.rs +++ b/src/league/season/team.rs @@ -1,4 +1,5 @@ pub mod add; +pub mod assign; pub mod get; pub mod list; pub mod stats; \ No newline at end of file diff --git a/src/league/season/team/assign.rs b/src/league/season/team/assign.rs new file mode 100644 index 0000000..4a4e377 --- /dev/null +++ b/src/league/season/team/assign.rs @@ -0,0 +1,77 @@ +use std::fs; + +use fbsim_core::league::League; + +use crate::cli::league::season::team::FbsimLeagueSeasonTeamAssignArgs; + +use serde_json; + +pub fn assign_team(args: FbsimLeagueSeasonTeamAssignArgs) -> Result<(), String> { + // Load the league from its file as mutable + let file_res = &fs::read_to_string(&args.league); + let file = match file_res { + Ok(file) => file, + Err(error) => return Err(format!("Error loading league file: {}", error)), + }; + let league_res = serde_json::from_str(file); + let mut league: League = match league_res { + Ok(league) => league, + Err(error) => return Err(format!("Error loading league from file: {}", error)), + }; + + // Get the current season + let season = match league.current_season_mut() { + Some(s) => s, + None => return Err(String::from("No current season found")), + }; + + // Verify team exists + let team_name = match season.team(args.team) { + Some(t) => t.name().to_string(), + None => return Err(format!("No team found with ID: {}", args.team)), + }; + + // Verify conference exists and get info + let conf_name = match season.conference(args.conference) { + Some(c) => c.name().to_string(), + None => return Err(format!("No conference found with index: {}", args.conference)), + }; + + // Verify division exists and get info + let div_name = match season.conference(args.conference).and_then(|c| c.division(args.division)) { + Some(d) => d.name().to_string(), + None => return Err(format!("No division found with ID: {}", args.division)), + }; + + // Verify the team is not already assigned to a division in any conference + for (ci, conference) in season.conferences().iter().enumerate() { + for (di, division) in conference.divisions().iter().enumerate() { + if division.teams().contains(&args.team) { + return Err(format!( + "{} is already assigned to {} {} (conference {}, division {})", + team_name, conference.name(), division.name(), ci, di + )); + } + } + } + + // Assign the team to the division + let conference = season.conference_mut(args.conference).unwrap(); + let division = conference.division_mut(args.division).unwrap(); + division.add_team(args.team)?; + + // Serialize the league as JSON + let league_res = serde_json::to_string_pretty(&league); + let league_str: String = match league_res { + Ok(league_str) => league_str, + Err(error) => return Err(format!("Error serializing league: {}", error)), + }; + + // Write the league back to its file + let write_res = fs::write(&args.league, league_str); + if let Err(e) = write_res { + return Err(format!("Error writing league file: {}", e)); + } + println!("{} assigned to {} {}", team_name, conf_name, div_name); + Ok(()) +} diff --git a/src/league/season/team/get.rs b/src/league/season/team/get.rs index 24f109d..93c9ab9 100644 --- a/src/league/season/team/get.rs +++ b/src/league/season/team/get.rs @@ -23,19 +23,17 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), String> Err(error) => return Err(format!("Error loading league from file: {}", error)), }; - // Get the league season + // Get the league season and team let season = match league.season(args.year) { Some(season) => season, None => return Err(format!("No season found with year: {}", args.year)), }; - - // Get the league season team from the league season let team = match season.team(args.id) { Some(team) => team, None => return Err(format!("No team found in season {} with id: {}", args.year, args.id)), }; - // Get the league season team's matchups from the league season + // Get the team's matchups from the league season let matchups = season.team_matchups(args.id)?; // Get playoff information @@ -57,7 +55,7 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), String> total_record.increment_ties(*pr.ties()); } - // Display the results in a table + // Display team information for the season let mut tw = TabWriter::new(stdout()); writeln!(&mut tw, "Team:\t{}", team.name()).map_err(|e| e.to_string())?; writeln!(&mut tw, "Record:\t{}", total_record).map_err(|e| e.to_string())?; @@ -84,22 +82,18 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), String> if let Some(entry) = picture.team_status(args.id) { let status_str = format_playoff_status(entry.status()); writeln!(&mut tw, "Playoff Status:\t{}", status_str).map_err(|e| e.to_string())?; - if entry.games_back() > 0.0 { writeln!(&mut tw, "Games Back:\t{:.1}", entry.games_back()).map_err(|e| e.to_string())?; } - if let Some(magic) = entry.magic_number() { if magic > 0 { writeln!(&mut tw, "Magic Number:\t{}", magic).map_err(|e| e.to_string())?; } } - writeln!(&mut tw, "Remaining Games:\t{}", entry.remaining_games()).map_err(|e| e.to_string())?; } } } - writeln!(&mut tw).map_err(|e| e.to_string())?; // Display each regular season matchup @@ -121,43 +115,79 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), String> }, None => { writeln!( - &mut tw, "{}\tBYE", i+1 + &mut tw, "{}\tBYE\t-\tBYE\t-", i+1 ).map_err(|e| e.to_string())?; }, } } // Display playoff matchups if the team participated - let rounds = playoffs.rounds(); let mut has_playoff_matchups = false; - for round in rounds.iter() { - for matchup in round.matchups().iter() { - if *matchup.home_team() == args.id || *matchup.away_team() == args.id { - has_playoff_matchups = true; - break; + for rounds in playoffs.conference_brackets().values() { + for round in rounds.iter() { + for matchup in round.matchups().iter() { + if *matchup.home_team() == args.id || *matchup.away_team() == args.id { + has_playoff_matchups = true; + break; + } } + if has_playoff_matchups { break; } } - if has_playoff_matchups { - break; - } + if has_playoff_matchups { break; } } + // Check winners bracket + if !has_playoff_matchups { + for round in playoffs.winners_bracket().iter() { + for matchup in round.matchups().iter() { + if *matchup.home_team() == args.id || *matchup.away_team() == args.id { + has_playoff_matchups = true; + break; + } + } + if has_playoff_matchups { break; } + } + } if has_playoff_matchups { + // Playoffs header writeln!(&mut tw, "\nPlayoffs").map_err(|e| e.to_string())?; writeln!( &mut tw, "Round\tHome Team\tHome Score\tAway Team\tAway Score" ).map_err(|e| e.to_string())?; - for (round_id, round) in rounds.iter().enumerate() { + // Display conference playoff matchups + for (conf_index, rounds) in playoffs.conference_brackets() { + let conf_label = season.conferences().get(*conf_index) + .map(|c| c.name().to_string()) + .unwrap_or_else(|| format!("Conference {}", conf_index)); + for (round_index, round) in rounds.iter().enumerate() { + for matchup in round.matchups().iter() { + if *matchup.home_team() == args.id || *matchup.away_team() == args.id { + let context = matchup.context(); + let away_team = season.team(*matchup.away_team()).unwrap().name(); + let home_team = season.team(*matchup.home_team()).unwrap().name(); + writeln!( + &mut tw, "{} Round {}\t{}\t{}\t{}\t{}", + conf_label, round_index, + home_team, context.home_score(), + away_team, context.away_score() + ).map_err(|e| e.to_string())?; + } + } + } + } + + // Display winners bracket matchups + for (round_index, round) in playoffs.winners_bracket().iter().enumerate() { for matchup in round.matchups().iter() { if *matchup.home_team() == args.id || *matchup.away_team() == args.id { let context = matchup.context(); let away_team = season.team(*matchup.away_team()).unwrap().name(); let home_team = season.team(*matchup.home_team()).unwrap().name(); writeln!( - &mut tw, "{}\t{}\t{}\t{}\t{}", - round_id, + &mut tw, "Championship Round {}\t{}\t{}\t{}\t{}", + round_index, home_team, context.home_score(), away_team, context.away_score() ).map_err(|e| e.to_string())?; @@ -165,7 +195,6 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), String> } } } - tw.flush().map_err(|e| e.to_string())?; Ok(()) } diff --git a/src/league/season/team/list.rs b/src/league/season/team/list.rs index 7838309..c1532fe 100644 --- a/src/league/season/team/list.rs +++ b/src/league/season/team/list.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fs; use std::io::{Write, stdout}; @@ -41,28 +42,70 @@ pub fn list_season_teams(args: FbsimLeagueSeasonTeamListArgs) -> Result<(), Stri None }; + // Determine if we need conference/division columns + let conferences = season.conferences(); + let show_conference = conferences.len() > 1; + let show_division = conferences.iter().any(|c| c.divisions().len() > 1); + + // Build team -> conference/division lookup maps + let mut team_conference: HashMap = HashMap::new(); + let mut team_division: HashMap = HashMap::new(); + if show_conference || show_division { + for conference in conferences.iter() { + for division in conference.divisions().iter() { + for team_id in division.teams().iter() { + if show_conference { + team_conference.insert(*team_id, conference.name().to_string()); + } + if show_division { + let div_name = division.name(); + let div_str = if div_name.is_empty() { + "-".to_string() + } else { + div_name.to_string() + }; + team_division.insert(*team_id, div_str); + } + } + } + } + } + // Get standings for proper ordering let standings = season.standings(); // Display the results in a table let mut tw = TabWriter::new(stdout()); - // Determine header based on season state + // Build header + let mut header = String::from("Team"); + if show_conference { header.push_str("\tConference"); } + if show_division { header.push_str("\tDivision"); } + header.push_str("\tRecord"); if playoffs_complete { - writeln!(&mut tw, "Team\tRecord\tPlayoffs\tChampion").map_err(|e| e.to_string())?; + header.push_str("\tPlayoffs\tChampion"); } else if playoffs_started { - writeln!(&mut tw, "Team\tRecord\tPlayoffs").map_err(|e| e.to_string())?; + header.push_str("\tPlayoffs"); } else if playoff_picture.is_some() { - // During regular season, show playoff picture columns - writeln!(&mut tw, "Team\tRecord\tStatus\tGB\tMagic #").map_err(|e| e.to_string())?; - } else { - writeln!(&mut tw, "Team\tRecord").map_err(|e| e.to_string())?; + header.push_str("\tStatus\tGB\tMagic #"); } + writeln!(&mut tw, "{}", header).map_err(|e| e.to_string())?; for (id, _) in standings.iter() { let team = season.team(*id).unwrap(); let matchups: LeagueSeasonMatchups = season.team_matchups(*id)?; + // Build conference/division prefix + let mut prefix = team.name().to_string(); + if show_conference { + let conf = team_conference.get(id).map(|s| s.as_str()).unwrap_or("-"); + prefix.push_str(&format!("\t{}", conf)); + } + if show_division { + let div = team_division.get(id).map(|s| s.as_str()).unwrap_or("-"); + prefix.push_str(&format!("\t{}", div)); + } + if playoffs_complete { let playoff_record_str = match playoffs.record(*id) { Ok(playoff_record) => playoff_record.to_string(), @@ -71,7 +114,7 @@ pub fn list_season_teams(args: FbsimLeagueSeasonTeamListArgs) -> Result<(), Stri let champion_str = if champion_id == Some(*id) { "X" } else { "" }; writeln!( &mut tw, "{}\t{}\t{}\t{}", - team.name(), matchups.record(), playoff_record_str, champion_str + prefix, matchups.record(), playoff_record_str, champion_str ).map_err(|e| e.to_string())?; } else if playoffs_started { let playoff_record_str = match playoffs.record(*id) { @@ -80,7 +123,7 @@ pub fn list_season_teams(args: FbsimLeagueSeasonTeamListArgs) -> Result<(), Stri }; writeln!( &mut tw, "{}\t{}\t{}", - team.name(), matchups.record(), playoff_record_str + prefix, matchups.record(), playoff_record_str ).map_err(|e| e.to_string())?; } else if let Some(ref picture) = playoff_picture { if let Some(entry) = picture.team_status(*id) { @@ -97,18 +140,18 @@ pub fn list_season_teams(args: FbsimLeagueSeasonTeamListArgs) -> Result<(), Stri }; writeln!( &mut tw, "{}\t{}\t{}\t{}\t{}", - team.name(), matchups.record(), status_str, gb_str, magic_str + prefix, matchups.record(), status_str, gb_str, magic_str ).map_err(|e| e.to_string())?; } else { writeln!( &mut tw, "{}\t{}\t-\t-\t-", - team.name(), matchups.record() + prefix, matchups.record() ).map_err(|e| e.to_string())?; } } else { writeln!( &mut tw, "{}\t{}", - team.name(), matchups.record() + prefix, matchups.record() ).map_err(|e| e.to_string())?; } } diff --git a/src/league/season/week/matchup/get.rs b/src/league/season/week/matchup/get.rs index 2f87fed..5831a47 100644 --- a/src/league/season/week/matchup/get.rs +++ b/src/league/season/week/matchup/get.rs @@ -1,12 +1,10 @@ use std::fs; -use std::io::{Write, stdout}; use fbsim_core::league::League; use crate::cli::league::season::week::matchup::FbsimLeagueSeasonWeekMatchupGetArgs; use serde_json; -use tabwriter::TabWriter; pub fn get_matchup(args: FbsimLeagueSeasonWeekMatchupGetArgs) -> Result<(), String> { // Load the league from its file @@ -21,22 +19,15 @@ pub fn get_matchup(args: FbsimLeagueSeasonWeekMatchupGetArgs) -> Result<(), Stri Err(error) => return Err(format!("Error loading league from file: {}", error)), }; - // TODO: Calculate a summary of the matchup - // TODO: Display the summary in a nice looking way (not JSON) - - // Get the league season + // Get the league season matchup let season = match league.season(args.year) { Some(season) => season, None => return Err(format!("No season found with year: {}", args.year)), }; - - // Get the league season week from the league season let week = match season.weeks().get(args.week) { Some(week) => week, None => return Err(format!("No week found in season {} with id: {}", args.year, args.week)), }; - - // Get the league season matchup from the league week let matchup = match week.matchups().get(args.matchup) { Some(matchup) => matchup, None => return Err( @@ -47,144 +38,38 @@ pub fn get_matchup(args: FbsimLeagueSeasonWeekMatchupGetArgs) -> Result<(), Stri ), }; - // Get the team names + // Get the team names and matchup context let away_team = season.team(*matchup.away_team()).unwrap().name(); let home_team = season.team(*matchup.home_team()).unwrap().name(); - - // Get the game context and stats let context = matchup.context(); - // Display the matchup in a table - let mut tw = TabWriter::new(stdout()); - writeln!(&mut tw,"Away Team\tAway Score\tHome Team\tHome Score").map_err(|e| e.to_string())?; - writeln!( - &mut tw,"{}\t{}\t{}\t{}", - away_team, context.away_score(), - home_team, context.home_score() - ).map_err(|e| e.to_string())?; - tw.flush().map_err(|e| e.to_string())?; + // Display based on game state + println!("{} season week {} matchup {}", args.year, args.week, args.matchup); + println!(); + println!("{} @ {}", away_team, home_team); + println!(); + if context.game_over() { + println!("{} Final", context); - // Get the home and away stats - let home_stats_opt = matchup.home_stats(); - let away_stats_opt = matchup.away_stats(); - - // Get the game - let game_opt = matchup.game(); - - // Display the passing stats if the game is complete or in-progress - writeln!(&mut tw).map_err(|e| e.to_string())?; - writeln!( - &mut tw, - "Team\tCompletions\tComp %\tYards\tTouchdowns\tInterceptions" - ).map_err(|e| e.to_string())?; - if let Some(home_stats) = home_stats_opt { - let passing = home_stats.passing(); - let completions = passing.completions(); - let attempts = passing.attempts(); - let percent: f64 = completions as f64 / attempts as f64; - writeln!( - &mut tw, "{}\t{}/{}\t{:.2}%\t{}\t{}\t{}", - home_team, completions, attempts, percent * 100.0, - passing.yards(), passing.touchdowns(), passing.interceptions() - ).map_err(|e| e.to_string())?; - } else if let Some(game) = game_opt { - let home_stats = game.home_stats(); - let passing = home_stats.passing(); - let completions = passing.completions(); - let attempts = passing.attempts(); - let percent: f64 = completions as f64 / attempts as f64; - writeln!( - &mut tw, "{}\t{}/{}\t{:.2}%\t{}\t{}\t{}", - home_team, completions, attempts, percent * 100.0, - passing.yards(), passing.touchdowns(), passing.interceptions() - ).map_err(|e| e.to_string())?; - } else { - let status = if context.started() { "In Progress" } else { "Pending" }; - writeln!(&mut tw, "{}\t{}", home_team, status).map_err(|e| e.to_string())?; - } - if let Some(away_stats) = away_stats_opt { - let passing = away_stats.passing(); - let completions = passing.completions(); - let attempts = passing.attempts(); - let percent: f64 = completions as f64 / attempts as f64; - writeln!( - &mut tw, "{}\t{}/{}\t{:.2}%\t{}\t{}\t{}", - away_team, completions, attempts, percent * 100.0, - passing.yards(), passing.touchdowns(), passing.interceptions() - ).map_err(|e| e.to_string())?; - } else if let Some(game) = game_opt { - let away_stats = game.away_stats(); - let passing = away_stats.passing(); - let completions = passing.completions(); - let attempts = passing.attempts(); - let percent: f64 = completions as f64 / attempts as f64; - writeln!( - &mut tw, "{}\t{}/{}\t{:.2}%\t{}\t{}\t{}", - away_team, completions, attempts, percent * 100.0, - passing.yards(), passing.touchdowns(), passing.interceptions() - ).map_err(|e| e.to_string())?; - } else { - let status = if context.started() { "In Progress" } else { "Pending" }; - writeln!(&mut tw, "{}\t{}", away_team, status).map_err(|e| e.to_string())?; - } - tw.flush().map_err(|e| e.to_string())?; - - // Display the rushing stats - writeln!(&mut tw).map_err(|e| e.to_string())?; - writeln!( - &mut tw, - "Team\tRushes\tYards\tYPC\tTouchdowns\tFumbles" - ).map_err(|e| e.to_string())?; - if let Some(home_stats) = home_stats_opt { - let rushing = home_stats.rushing(); - let rushes = rushing.rushes(); - let yards = rushing.yards(); - let ypc: f64 = yards as f64 / rushes as f64; - writeln!( - &mut tw, "{}\t{}\t{}\t{:.2}\t{}\t{}", - home_team, rushes, yards, ypc, - rushing.touchdowns(), rushing.fumbles() - ).map_err(|e| e.to_string())?; - } else if let Some(game) = game_opt { - let home_stats = game.home_stats(); - let rushing = home_stats.rushing(); - let rushes = rushing.rushes(); - let yards = rushing.yards(); - let ypc: f64 = yards as f64 / rushes as f64; - writeln!( - &mut tw, "{}\t{}\t{}\t{:.2}\t{}\t{}", - home_team, rushes, yards, ypc, - rushing.touchdowns(), rushing.fumbles() - ).map_err(|e| e.to_string())?; - } else { - let status = if context.started() { "In Progress" } else { "Pending" }; - writeln!(&mut tw, "{}\t{}", home_team, status).map_err(|e| e.to_string())?; - } - if let Some(away_stats) = away_stats_opt { - let rushing = away_stats.rushing(); - let rushes = rushing.rushes(); - let yards = rushing.yards(); - let ypc: f64 = yards as f64 / rushes as f64; - writeln!( - &mut tw, "{}\t{}\t{}\t{:.2}\t{}\t{}", - away_team, rushes, yards, ypc, - rushing.touchdowns(), rushing.fumbles() - ).map_err(|e| e.to_string())?; - } else if let Some(game) = game_opt { - let away_stats = game.away_stats(); - let rushing = away_stats.rushing(); - let rushes = rushing.rushes(); - let yards = rushing.yards(); - let ypc: f64 = yards as f64 / rushes as f64; - writeln!( - &mut tw, "{}\t{}\t{}\t{:.2}\t{}\t{}", - away_team, rushes, yards, ypc, - rushing.touchdowns(), rushing.fumbles() - ).map_err(|e| e.to_string())?; + if let Some(home_stats) = matchup.home_stats() { + println!(); + println!("{} stats\n{}", context.home_team_short(), home_stats); + } + if let Some(away_stats) = matchup.away_stats() { + println!(); + println!("{} stats\n{}", context.away_team_short(), away_stats); + } + } else if context.started() { + // Display play-by-play log up to this point + if let Some(game) = matchup.game() { + for drive in game.drives().iter() { + println!("{}\n", drive); + } + } else { + println!("{}", context); + } } else { - let status = if context.started() { "In Progress" } else { "Pending" }; - writeln!(&mut tw, "{}\t{}", away_team, status).map_err(|e| e.to_string())?; + println!("{} Pending", context); } - tw.flush().map_err(|e| e.to_string())?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 21f1ea5..e8749a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,8 @@ use crate::cli::league::FbsimLeagueSubcommand; use crate::cli::league::team::FbsimLeagueTeamSubcommand; use crate::cli::league::team::stats::FbsimLeagueTeamStatsSubcommand; use crate::cli::league::season::FbsimLeagueSeasonSubcommand; +use crate::cli::league::season::conference::FbsimLeagueSeasonConferenceSubcommand; +use crate::cli::league::season::conference::division::FbsimLeagueSeasonConferenceDivisionSubcommand; use crate::cli::league::season::schedule::FbsimLeagueSeasonScheduleSubcommand; use crate::cli::league::season::team::FbsimLeagueSeasonTeamSubcommand; use crate::cli::league::season::team::stats::FbsimLeagueSeasonTeamStatsSubcommand; @@ -44,8 +46,16 @@ use crate::league::season::add::add_season; use crate::league::season::get::get_season; use crate::league::season::list::list_seasons; use crate::league::season::sim::sim_season; +use crate::league::season::standings::get_standings; +use crate::league::season::conference::add::add_conference; +use crate::league::season::conference::get::get_conference; +use crate::league::season::conference::list::list_conferences; +use crate::league::season::conference::division::add::add_division; +use crate::league::season::conference::division::get::get_division; +use crate::league::season::conference::division::list::list_divisions; use crate::league::season::schedule::generate_schedule; use crate::league::season::team::add::add_season_team; +use crate::league::season::team::assign::assign_team; use crate::league::season::team::get::get_season_team; use crate::league::season::team::list::list_season_teams; use crate::league::season::team::stats::passing::list_season_passing; @@ -107,8 +117,20 @@ fn main() { FbsimLeagueSeasonSubcommand::Get(args) => get_season(args.clone()), FbsimLeagueSeasonSubcommand::List(args) => list_seasons(args.clone()), FbsimLeagueSeasonSubcommand::Sim(args) => sim_season(args.clone()), + FbsimLeagueSeasonSubcommand::Standings(args) => get_standings(args.clone()), + FbsimLeagueSeasonSubcommand::Conference{ command } => match command { + FbsimLeagueSeasonConferenceSubcommand::Add(args) => add_conference(args.clone()), + FbsimLeagueSeasonConferenceSubcommand::Get(args) => get_conference(args.clone()), + FbsimLeagueSeasonConferenceSubcommand::List(args) => list_conferences(args.clone()), + FbsimLeagueSeasonConferenceSubcommand::Division{ command } => match command { + FbsimLeagueSeasonConferenceDivisionSubcommand::Add(args) => add_division(args.clone()), + FbsimLeagueSeasonConferenceDivisionSubcommand::Get(args) => get_division(args.clone()), + FbsimLeagueSeasonConferenceDivisionSubcommand::List(args) => list_divisions(args.clone()) + } + }, FbsimLeagueSeasonSubcommand::Team{ command } => match command { FbsimLeagueSeasonTeamSubcommand::Add(args) => add_season_team(args.clone()), + FbsimLeagueSeasonTeamSubcommand::Assign(args) => assign_team(args.clone()), FbsimLeagueSeasonTeamSubcommand::Get(args) => get_season_team(args.clone()), FbsimLeagueSeasonTeamSubcommand::List(args) => list_season_teams(args.clone()), FbsimLeagueSeasonTeamSubcommand::Stats{ command } => match command {