From c30c3ca2a23bbb2a580d9aabc376c736be6764c4 Mon Sep 17 00:00:00 2001 From: whatsacomputertho Date: Tue, 27 Jan 2026 09:46:11 -0500 Subject: [PATCH 1/7] feat(league): add conferences and divisions --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cli/league/season.rs | 9 + src/cli/league/season/conference.rs | 64 +++++ src/cli/league/season/conference/division.rs | 76 ++++++ src/cli/league/season/playoffs.rs | 29 ++- src/cli/league/season/playoffs/round.rs | 10 + src/cli/league/season/schedule.rs | 12 + src/cli/league/season/standings.rs | 33 +++ src/cli/league/season/team.rs | 25 ++ src/league/season.rs | 2 + src/league/season/conference.rs | 4 + src/league/season/conference/add.rs | 51 ++++ src/league/season/conference/division.rs | 3 + src/league/season/conference/division/add.rs | 54 +++++ src/league/season/conference/division/get.rs | 74 ++++++ src/league/season/conference/division/list.rs | 60 +++++ src/league/season/conference/get.rs | 83 +++++++ src/league/season/conference/list.rs | 55 +++++ src/league/season/playoffs/gen.rs | 37 ++- src/league/season/playoffs/picture.rs | 198 +++++++++++++++- src/league/season/playoffs/round/get.rs | 114 ++++++++- src/league/season/playoffs/round/sim.rs | 138 +++++++++-- src/league/season/playoffs/sim.rs | 2 +- src/league/season/schedule.rs | 21 ++ src/league/season/standings.rs | 222 ++++++++++++++++++ src/league/season/team.rs | 1 + src/league/season/team/assign.rs | 66 ++++++ src/main.rs | 22 ++ 29 files changed, 1431 insertions(+), 38 deletions(-) create mode 100644 src/cli/league/season/conference.rs create mode 100644 src/cli/league/season/conference/division.rs create mode 100644 src/cli/league/season/standings.rs create mode 100644 src/league/season/conference.rs create mode 100644 src/league/season/conference/add.rs create mode 100644 src/league/season/conference/division.rs create mode 100644 src/league/season/conference/division/add.rs create mode 100644 src/league/season/conference/division/get.rs create mode 100644 src/league/season/conference/division/list.rs create mode 100644 src/league/season/conference/get.rs create mode 100644 src/league/season/conference/list.rs create mode 100644 src/league/season/standings.rs create mode 100644 src/league/season/team/assign.rs diff --git a/Cargo.lock b/Cargo.lock index 6a567b4..0291ceb 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=conferences-divisions#3c3b533f57ec5dd04dc9769515dee959e3b44b4b" dependencies = [ "chrono", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 6385731..699f407 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.23", features = ["derive"] } -fbsim-core = { git = "https://github.com/whatsacomputertho/fbsim-core.git", branch = "main" } +fbsim-core = { git = "https://github.com/whatsacomputertho/fbsim-core.git", branch = "conferences-divisions" } crossterm = "0.27.0" indicatif = "0.17.11" rand = "0.8.5" 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..aad58e7 --- /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 index + #[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..3b55aca --- /dev/null +++ b/src/cli/league/season/conference/division.rs @@ -0,0 +1,76 @@ +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 division ID + #[arg(short='i')] + #[arg(long="id")] + pub id: 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..7c31b02 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, + + /// Treat num_teams as per-conference instead of total + #[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, + + /// Treat num_playoff_teams as per-conference instead of total + #[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..ec3c511 100644 --- a/src/cli/league/season/playoffs/round.rs +++ b/src/cli/league/season/playoffs/round.rs @@ -21,6 +21,11 @@ pub struct FbsimLeagueSeasonPlayoffsRoundGetArgs { #[arg(short='r')] #[arg(long="round")] pub round: usize, + + /// Get conference-specific round (optional, for conference playoffs) + #[arg(short='c')] + #[arg(long="conference")] + pub conference: Option, } /// Simulate a playoff round @@ -35,6 +40,11 @@ pub struct FbsimLeagueSeasonPlayoffsRoundSimArgs { #[arg(short='r')] #[arg(long="round")] pub round: usize, + + /// Simulate conference-specific round (optional, for conference playoffs) + #[arg(short='c')] + #[arg(long="conference")] + pub conference: Option, } /// Manage rounds in the playoffs 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..caa53a5 --- /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 output by conference + #[arg(long="by-conference")] + pub by_conference: bool, + + /// Group output 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..296f026 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 index + #[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..592d79e --- /dev/null +++ b/src/league/season/conference/add.rs @@ -0,0 +1,51 @@ +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 index before adding + let conf_index = 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 with index {}", args.name, conf_index); + 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..b87e6b3 --- /dev/null +++ b/src/league/season/conference/division/add.rs @@ -0,0 +1,54 @@ +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 + let season = match league.current_season_mut() { + Some(s) => s, + None => return Err(String::from("No current season found")), + }; + + // Get the conference + let conference = match season.conference_mut(args.conference) { + Some(c) => c, + None => return Err(format!("No conference found with index: {}", args.conference)), + }; + + // Add the division + let division = LeagueDivision::with_name(&args.name); + conference.add_division(args.id, 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, args.conference, args.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..a8367de --- /dev/null +++ b/src/league/season/conference/division/get.rs @@ -0,0 +1,74 @@ +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 + let season = match league.season(args.year) { + Some(season) => season, + None => return Err(format!("No season found with year: {}", args.year)), + }; + + // Get the conference + let conferences = season.conferences(); + let conference = match conferences.get(args.conference) { + Some(c) => c, + None => return Err(format!("No conference found with index: {}", args.conference)), + }; + + // Get the division + 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, "ID:\t{}", args.division).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..c481ae4 --- /dev/null +++ b/src/league/season/conference/division/list.rs @@ -0,0 +1,60 @@ +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 + let season = match league.season(args.year) { + Some(season) => season, + None => return Err(format!("No season found with year: {}", args.year)), + }; + + // Get the conference + let conferences = season.conferences(); + let conference = match conferences.get(args.conference) { + Some(c) => c, + None => return Err(format!("No conference found with index: {}", args.conference)), + }; + + // Get divisions + 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 in {} Conference", 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 { + 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..65b52f1 --- /dev/null +++ b/src/league/season/conference/get.rs @@ -0,0 +1,83 @@ +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 + let season = match league.season(args.year) { + Some(season) => season, + None => return Err(format!("No season found with year: {}", args.year)), + }; + + // Get the conference + let conferences = season.conferences(); + let conference = match conferences.get(args.conference) { + Some(c) => c, + None => return Err(format!("No conference found with index: {}", args.conference)), + }; + + // Display conference info + let mut tw = TabWriter::new(stdout()); + writeln!(&mut tw, "Conference:\t{}", conference.name()).map_err(|e| e.to_string())?; + writeln!(&mut tw, "Index:\t{}", args.conference).map_err(|e| e.to_string())?; + writeln!(&mut tw).map_err(|e| e.to_string())?; + + // Display divisions + 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() { + 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").map_err(|e| e.to_string())?; + for (div_id, division) in conference.divisions() { + for team_id in division.teams() { + if let Some(team) = season.team(*team_id) { + writeln!( + &mut tw, "{}\t{}\t{}", + team_id, + team.name(), + division.name() + ).map_err(|e| e.to_string())?; + } + } + let _ = div_id; // suppress unused warning + } + } + + 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..744711f --- /dev/null +++ b/src/league/season/conference/list.rs @@ -0,0 +1,55 @@ +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 + let season = match league.season(args.year) { + Some(season) => season, + None => return Err(format!("No season found with year: {}", args.year)), + }; + + // Get conferences + 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, "Index\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/playoffs/gen.rs b/src/league/season/playoffs/gen.rs index 2ad14ef..5c5a642 100644 --- a/src/league/season/playoffs/gen.rs +++ b/src/league/season/playoffs/gen.rs @@ -27,9 +27,38 @@ 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 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 + if let Err(e) = season.generate_playoffs_with_conferences( + args.num_teams, + args.division_winners, + &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 + if let Err(e) = season.generate_playoffs(args.num_teams, &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); @@ -44,6 +73,6 @@ pub fn gen_playoffs(args: FbsimLeagueSeasonPlayoffsGenArgs) -> Result<(), String 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/picture.rs b/src/league/season/playoffs/picture.rs index b307b57..9bd1bd1 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, PlayoffStatus}; use crate::cli::league::season::playoffs::FbsimLeagueSeasonPlayoffsPictureArgs; @@ -43,6 +44,34 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul |w| !w.complete() ).count(); + // 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_idx = args.conference.unwrap(); + display_single_conference_picture(season, conf_idx, &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 let picture = season.playoff_picture(args.num_playoff_teams)?; @@ -52,6 +81,167 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul 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-aware playoff picture + let picture = PlayoffPicture::from_season_with_conferences( + season, + args.num_playoff_teams, + args.division_winners + )?; + + for (conf_idx, conference) in conferences.iter().enumerate() { + // Skip if filtering to specific conference + if let Some(filter_conf) = args.conference { + if filter_conf != conf_idx { + continue; + } + } + + println!("=== {} Playoff Picture ===", conference.name()); + + // Filter the picture entries by conference + display_conference_playoff_picture_sections(&picture, season, conf_idx)?; + } + + display_legend(); + Ok(()) +} + +fn display_conference_playoff_picture_sections( + picture: &PlayoffPicture, + season: &LeagueSeason, + conf_idx: usize +) -> Result<(), String> { + let conference = match season.conferences().get(conf_idx) { + Some(c) => c, + None => return Err(format!("No conference found with index: {}", conf_idx)), + }; + 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_idx: usize, + args: &FbsimLeagueSeasonPlayoffsPictureArgs, + weeks_remaining: usize +) -> Result<(), String> { + let conferences = season.conferences(); + let conference = match conferences.get(conf_idx) { + Some(c) => c, + None => return Err(format!("No conference found with index: {}", conf_idx)), + }; + + // 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 picture = season.playoff_picture(args.num_playoff_teams)?; + 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() { @@ -121,13 +311,15 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul println!(); } - // Display legend + Ok(()) +} + +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/get.rs b/src/league/season/playoffs/round/get.rs index 9e7f914..4eada61 100644 --- a/src/league/season/playoffs/round/get.rs +++ b/src/league/season/playoffs/round/get.rs @@ -29,20 +29,36 @@ pub fn get_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundGetArgs) -> Result // Get the playoffs let playoffs = season.playoffs(); + + if playoffs.is_conference_playoff() { + display_conference_round(season, args.round, args.conference, args.year)?; + } else { + display_traditional_round(season, args.round, args.year)?; + } + + Ok(()) +} + +fn display_traditional_round( + season: &fbsim_core::league::season::LeagueSeason, + round_idx: usize, + year: usize +) -> Result<(), String> { + 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)); + return Err(format!("Playoffs have not been generated for the {} season", year)); } // Get the round - let round = match rounds.get(args.round) { + let round = match rounds.get(round_idx) { Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", args.round)), + None => return Err(format!("No playoff round found with index: {}", round_idx)), }; // Display the round - println!("Playoff Round {} - {} Season", args.round, args.year); + println!("Playoff Round {} - {} Season", round_idx, 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() { @@ -68,3 +84,93 @@ pub fn get_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundGetArgs) -> Result Ok(()) } + +fn display_conference_round( + season: &fbsim_core::league::season::LeagueSeason, + round_idx: usize, + filter_conference: Option, + year: usize +) -> Result<(), String> { + let playoffs = season.playoffs(); + let conference_rounds = playoffs.conference_rounds(); + let conferences = season.conferences(); + + if conference_rounds.is_empty() { + return Err(format!("Conference playoffs have not been generated for the {} season", year)); + } + + for (conf_idx, conf_rounds) in conference_rounds.iter() { + // Skip if filtering to specific conference + if let Some(filter) = filter_conference { + if filter != *conf_idx { + continue; + } + } + + let conf_name = conferences.get(*conf_idx) + .map(|c| c.name().to_string()) + .unwrap_or_else(|| format!("Conference {}", conf_idx)); + + if let Some(round) = conf_rounds.get(round_idx) { + println!("=== {} Round {} - {} Season ===", conf_name, round_idx, 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" + } 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!(); + } else if filter_conference.is_some() { + return Err(format!("No round {} found for conference {}", round_idx, conf_name)); + } + } + + // Also display winners bracket if it exists + let winners_bracket = playoffs.winners_bracket(); + if !winners_bracket.is_empty() { + println!("=== Championship Bracket ==="); + for (bracket_round_idx, round) in winners_bracket.iter().enumerate() { + println!("Championship Round {}", bracket_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())?; + } + } + + Ok(()) +} diff --git a/src/league/season/playoffs/round/sim.rs b/src/league/season/playoffs/round/sim.rs index 2256a48..633e90c 100644 --- a/src/league/season/playoffs/round/sim.rs +++ b/src/league/season/playoffs/round/sim.rs @@ -29,18 +29,35 @@ pub fn sim_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundSimArgs) -> Result // Simulate the playoff round let mut rng = rand::thread_rng(); + let is_conference_playoff = season.playoffs().is_conference_playoff(); + + // For conference playoffs, simulate the round (the core library handles conference-specific logic) if let Err(e) = season.sim_playoff_round(args.round, &mut rng) { return Err(format!("Failed to simulate playoff round: {}", e)); } // 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 round_complete = if is_conference_playoff { + // For conference playoffs, check all conference rounds + playoffs.conference_rounds().iter().all(|(_, rounds)| { + rounds.get(args.round).map(|r| r.complete()).unwrap_or(true) + }) + } else { + playoffs.rounds().get(args.round).map(|r| r.complete()).unwrap_or(false) + }; let playoffs_complete = playoffs.complete(); 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)); + if is_conference_playoff { + if let Err(e) = season.generate_next_conference_playoff_round(&mut rng) { + // It's okay if this fails - might mean we need to transition to winners bracket + let _ = e; + } + } else { + if let Err(e) = season.generate_next_playoff_round(&mut rng) { + return Err(format!("Failed to generate next playoff round: {}", e)); + } } } @@ -48,26 +65,11 @@ pub fn sim_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundSimArgs) -> Result 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)), - }; - - 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())?; + if is_conference_playoff { + display_conference_round_results(season, args.round, args.conference)?; + } else { + display_traditional_round_results(season, args.round)?; } - tw.flush().map_err(|e| e.to_string())?; // Display champion if playoffs are complete if season.playoffs().complete() { @@ -93,3 +95,95 @@ pub fn sim_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundSimArgs) -> Result let _ = year; // suppress unused warning Ok(()) } + +fn display_traditional_round_results( + season: &fbsim_core::league::season::LeagueSeason, + round_idx: usize +) -> Result<(), String> { + let round = match season.playoffs().rounds().get(round_idx) { + Some(r) => r, + None => return Err(format!("No playoff round found with index: {}", round_idx)), + }; + + println!("Playoff Round {} Results", round_idx); + 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())?; + } + tw.flush().map_err(|e| e.to_string())?; + Ok(()) +} + +fn display_conference_round_results( + season: &fbsim_core::league::season::LeagueSeason, + round_idx: usize, + filter_conference: Option +) -> Result<(), String> { + let conferences = season.conferences(); + let conference_rounds = season.playoffs().conference_rounds(); + + for (conf_idx, conf_rounds) in conference_rounds.iter() { + // Skip if filtering to specific conference + if let Some(filter) = filter_conference { + if filter != *conf_idx { + continue; + } + } + + let conf_name = conferences.get(*conf_idx) + .map(|c| c.name().to_string()) + .unwrap_or_else(|| format!("Conference {}", conf_idx)); + + if let Some(round) = conf_rounds.get(round_idx) { + println!("=== {} Round {} Results ===", conf_name, round_idx); + 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())?; + } + tw.flush().map_err(|e| e.to_string())?; + println!(); + } + } + + // Also display winners bracket if it exists + let winners_bracket = season.playoffs().winners_bracket(); + if !winners_bracket.is_empty() { + println!("=== Championship Round ==="); + for round in winners_bracket.iter() { + 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())?; + } + tw.flush().map_err(|e| e.to_string())?; + } + } + + Ok(()) +} diff --git a/src/league/season/playoffs/sim.rs b/src/league/season/playoffs/sim.rs index b2b2bf8..426e00b 100644 --- a/src/league/season/playoffs/sim.rs +++ b/src/league/season/playoffs/sim.rs @@ -25,7 +25,7 @@ pub fn sim_playoffs(args: FbsimLeagueSeasonPlayoffsSimArgs) -> Result<(), String None => return Err(String::from("No current season found")), }; - // Simulate the playoffs + // Simulate the playoffs (handles both traditional and conference playoffs) let mut rng = rand::thread_rng(); if let Err(e) = season.sim_playoffs(&mut rng) { return Err(format!("Failed to simulate playoffs: {}", e)); 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..2afb408 --- /dev/null +++ b/src/league/season/standings.rs @@ -0,0 +1,222 @@ +use std::fs; +use std::io::{Write, stdout}; + +use fbsim_core::league::League; + +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(); + + // Handle different display modes + 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_idx) = args.conference { + // Filter by specific conference + if let Some(div_id) = args.division { + display_division_standings(season, conf_idx, div_id)?; + } else { + display_conference_standings(season, conf_idx)?; + } + } else { + // Display overall standings + display_overall_standings(season)?; + } + + Ok(()) +} + +fn display_overall_standings(season: &fbsim_core::league::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: &fbsim_core::league::season::LeagueSeason +) -> Result<(), String> { + let conferences = season.conferences(); + + for (conf_idx, conference) in conferences.iter().enumerate() { + println!("=== {} ===", conference.name()); + + let standings = season.conference_standings(conf_idx)?; + + 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())?; + println!(); + } + Ok(()) +} + +fn display_standings_by_division( + season: &fbsim_core::league::season::LeagueSeason +) -> Result<(), String> { + let conferences = season.conferences(); + + for (conf_idx, conference) in conferences.iter().enumerate() { + println!("=== {} ===", conference.name()); + + for (div_id, division) in conference.divisions() { + println!("--- {} ---", division.name()); + + let standings = season.division_standings(conf_idx, *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())?; + println!(); + } + } + Ok(()) +} + +fn display_conference_standings( + season: &fbsim_core::league::season::LeagueSeason, + conf_idx: usize +) -> Result<(), String> { + let conferences = season.conferences(); + let conference = match conferences.get(conf_idx) { + Some(c) => c, + None => return Err(format!("No conference found with index: {}", conf_idx)), + }; + + println!("=== {} ===", conference.name()); + + let standings = season.conference_standings(conf_idx)?; + + 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: &fbsim_core::league::season::LeagueSeason, + conf_idx: usize, + div_id: usize +) -> Result<(), String> { + let conferences = season.conferences(); + let conference = match conferences.get(conf_idx) { + Some(c) => c, + None => return Err(format!("No conference found with index: {}", conf_idx)), + }; + + 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_idx, 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..66ecdbc --- /dev/null +++ b/src/league/season/team/assign.rs @@ -0,0 +1,66 @@ +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)), + }; + + // 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/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 { From 5a208debe8dee6cf3ee3a526938c4b113d75b875 Mon Sep 17 00:00:00 2001 From: whatsacomputertho Date: Sat, 31 Jan 2026 17:32:20 -0500 Subject: [PATCH 2/7] feat(league): account for fbsim-core refactoring in league subcommands --- Cargo.lock | 2 +- .../league/season/playoffs/round/matchup.rs | 21 ++++ src/league/season/conference/division/add.rs | 4 +- src/league/season/conference/division/list.rs | 2 +- src/league/season/conference/get.rs | 5 +- src/league/season/get.rs | 41 ++++++-- src/league/season/playoffs/gen.rs | 15 +-- src/league/season/playoffs/get.rs | 95 +++++++++++++------ src/league/season/playoffs/picture.rs | 32 ++++--- src/league/season/playoffs/round/get.rs | 56 ++++++----- .../season/playoffs/round/matchup/get.rs | 26 +++-- .../season/playoffs/round/matchup/sim.rs | 74 +++++++++------ src/league/season/playoffs/round/sim.rs | 70 ++++++-------- src/league/season/standings.rs | 32 +++---- src/league/season/team/get.rs | 58 ++++++++--- 15 files changed, 334 insertions(+), 199 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0291ceb..92ae47f 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=conferences-divisions#3c3b533f57ec5dd04dc9769515dee959e3b44b4b" +source = "git+https://github.com/whatsacomputertho/fbsim-core.git?branch=conferences-divisions#015ed1d680aa83fe0474ca4807d38386b334b036" dependencies = [ "chrono", "lazy_static", 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/league/season/conference/division/add.rs b/src/league/season/conference/division/add.rs index b87e6b3..f4b696c 100644 --- a/src/league/season/conference/division/add.rs +++ b/src/league/season/conference/division/add.rs @@ -34,7 +34,7 @@ pub fn add_division(args: FbsimLeagueSeasonConferenceDivisionAddArgs) -> Result< // Add the division let division = LeagueDivision::with_name(&args.name); - conference.add_division(args.id, division); + conference.add_division(division); // Serialize the league as JSON let league_res = serde_json::to_string_pretty(&league); @@ -49,6 +49,6 @@ pub fn add_division(args: FbsimLeagueSeasonConferenceDivisionAddArgs) -> Result< return Err(format!("Error writing league file: {}", e)); } - println!("Division '{}' added to conference {} with ID {}", args.name, args.conference, args.id); + println!("Division '{}' added to conference {}", args.name, args.conference); Ok(()) } diff --git a/src/league/season/conference/division/list.rs b/src/league/season/conference/division/list.rs index c481ae4..b8e5fd0 100644 --- a/src/league/season/conference/division/list.rs +++ b/src/league/season/conference/division/list.rs @@ -47,7 +47,7 @@ pub fn list_divisions(args: FbsimLeagueSeasonConferenceDivisionListArgs) -> Resu let mut tw = TabWriter::new(stdout()); writeln!(&mut tw, "ID\tName\tTeams").map_err(|e| e.to_string())?; - for (div_id, division) in divisions { + for (div_id, division) in divisions.iter().enumerate() { writeln!( &mut tw, "{}\t{}\t{}", div_id, diff --git a/src/league/season/conference/get.rs b/src/league/season/conference/get.rs index 65b52f1..c1bec61 100644 --- a/src/league/season/conference/get.rs +++ b/src/league/season/conference/get.rs @@ -46,7 +46,7 @@ pub fn get_conference(args: FbsimLeagueSeasonConferenceGetArgs) -> Result<(), St } 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() { + for (div_id, division) in conference.divisions().iter().enumerate() { writeln!( &mut tw, "{}\t{}\t{}", div_id, @@ -63,7 +63,7 @@ pub fn get_conference(args: FbsimLeagueSeasonConferenceGetArgs) -> Result<(), St 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").map_err(|e| e.to_string())?; - for (div_id, division) in conference.divisions() { + for division in conference.divisions() { for team_id in division.teams() { if let Some(team) = season.team(*team_id) { writeln!( @@ -74,7 +74,6 @@ pub fn get_conference(args: FbsimLeagueSeasonConferenceGetArgs) -> Result<(), St ).map_err(|e| e.to_string())?; } } - let _ = div_id; // suppress unused warning } } diff --git a/src/league/season/get.rs b/src/league/season/get.rs index 8c3d2a7..76fefbf 100644 --- a/src/league/season/get.rs +++ b/src/league/season/get.rs @@ -58,16 +58,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 +97,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/gen.rs b/src/league/season/playoffs/gen.rs index 5c5a642..df14c0d 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; @@ -28,6 +29,8 @@ pub fn gen_playoffs(args: FbsimLeagueSeasonPlayoffsGenArgs) -> Result<(), String // Generate the playoffs let mut rng = rand::thread_rng(); + let mut options = LeagueSeasonPlayoffOptions::new(); + let result_msg = if args.per_conference { // Validate conferences exist if season.conferences().is_empty() { @@ -38,11 +41,10 @@ pub fn gen_playoffs(args: FbsimLeagueSeasonPlayoffsGenArgs) -> Result<(), String } // Generate conference playoffs - if let Err(e) = season.generate_playoffs_with_conferences( - args.num_teams, - args.division_winners, - &mut rng - ) { + 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)); } @@ -53,7 +55,8 @@ pub fn gen_playoffs(args: FbsimLeagueSeasonPlayoffsGenArgs) -> Result<(), String ) } else { // Generate traditional playoffs - if let Err(e) = season.generate_playoffs(args.num_teams, &mut rng) { + 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)); } diff --git a/src/league/season/playoffs/get.rs b/src/league/season/playoffs/get.rs index 8b6b22d..5593571 100644 --- a/src/league/season/playoffs/get.rs +++ b/src/league/season/playoffs/get.rs @@ -29,43 +29,76 @@ pub fn get_playoffs(args: FbsimLeagueSeasonPlayoffsGetArgs) -> Result<(), String // Get the playoffs 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())?; + // 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!("=== {} ===", conf_name); + + 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!(); } - tw.flush().map_err(|e| e.to_string())?; - println!(); } // Display champion if playoffs are complete diff --git a/src/league/season/playoffs/picture.rs b/src/league/season/playoffs/picture.rs index 9bd1bd1..b2678a6 100644 --- a/src/league/season/playoffs/picture.rs +++ b/src/league/season/playoffs/picture.rs @@ -3,7 +3,7 @@ use std::io::{Write, stdout}; use fbsim_core::league::League; use fbsim_core::league::season::LeagueSeason; -use fbsim_core::league::season::playoffs::picture::{PlayoffPicture, PlayoffStatus}; +use fbsim_core::league::season::playoffs::picture::{PlayoffPicture, PlayoffPictureOptions, PlayoffStatus}; use crate::cli::league::season::playoffs::FbsimLeagueSeasonPlayoffsPictureArgs; @@ -57,8 +57,8 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul display_conference_playoff_picture(season, &args, weeks_remaining)?; } else if has_conferences && args.conference.is_some() { // Display single conference - let conf_idx = args.conference.unwrap(); - display_single_conference_picture(season, conf_idx, &args, weeks_remaining)?; + 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)?; @@ -100,16 +100,20 @@ fn display_conference_playoff_picture( println!(); // Get the conference-aware playoff picture - let picture = PlayoffPicture::from_season_with_conferences( + let options = PlayoffPictureOptions { + by_conference: Some(true), + division_winners_guaranteed: args.division_winners, + }; + let picture = PlayoffPicture::from_season( season, args.num_playoff_teams, - args.division_winners + Some(options) )?; - for (conf_idx, conference) in conferences.iter().enumerate() { + 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_idx { + if filter_conf != conf_index { continue; } } @@ -117,7 +121,7 @@ fn display_conference_playoff_picture( println!("=== {} Playoff Picture ===", conference.name()); // Filter the picture entries by conference - display_conference_playoff_picture_sections(&picture, season, conf_idx)?; + display_conference_playoff_picture_sections(&picture, season, conf_index)?; } display_legend(); @@ -127,11 +131,11 @@ fn display_conference_playoff_picture( fn display_conference_playoff_picture_sections( picture: &PlayoffPicture, season: &LeagueSeason, - conf_idx: usize + conf_index: usize ) -> Result<(), String> { - let conference = match season.conferences().get(conf_idx) { + let conference = match season.conferences().get(conf_index) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", conf_idx)), + None => return Err(format!("No conference found with index: {}", conf_index)), }; let conf_teams: Vec = conference.all_teams(); @@ -218,14 +222,14 @@ fn display_conference_playoff_picture_sections( fn display_single_conference_picture( season: &LeagueSeason, - conf_idx: usize, + conf_index: usize, args: &FbsimLeagueSeasonPlayoffsPictureArgs, weeks_remaining: usize ) -> Result<(), String> { let conferences = season.conferences(); - let conference = match conferences.get(conf_idx) { + let conference = match conferences.get(conf_index) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", conf_idx)), + None => return Err(format!("No conference found with index: {}", conf_index)), }; // Display header diff --git a/src/league/season/playoffs/round/get.rs b/src/league/season/playoffs/round/get.rs index 4eada61..a5acc0f 100644 --- a/src/league/season/playoffs/round/get.rs +++ b/src/league/season/playoffs/round/get.rs @@ -41,27 +41,33 @@ pub fn get_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundGetArgs) -> Result fn display_traditional_round( season: &fbsim_core::league::season::LeagueSeason, - round_idx: usize, + round_index: usize, year: usize ) -> Result<(), String> { let playoffs = season.playoffs(); - let rounds = playoffs.rounds(); + let brackets = playoffs.conference_brackets(); - if rounds.is_empty() { + if brackets.is_empty() { return Err(format!("Playoffs have not been generated for the {} season", year)); } + // For traditional (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)), + }; + // Get the round - let round = match rounds.get(round_idx) { + let round = match rounds.get(round_index) { Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", round_idx)), + None => return Err(format!("No playoff round found with index: {}", round_index)), }; // Display the round - println!("Playoff Round {} - {} Season", round_idx, year); + println!("Playoff Round {} - {} Season", round_index, 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() { + 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(); @@ -74,7 +80,7 @@ fn display_traditional_round( }; writeln!( &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", - matchup_idx, + matchup_index, away_team, context.away_score(), home_team, context.home_score(), status @@ -87,35 +93,35 @@ fn display_traditional_round( fn display_conference_round( season: &fbsim_core::league::season::LeagueSeason, - round_idx: usize, + round_index: usize, filter_conference: Option, year: usize ) -> Result<(), String> { let playoffs = season.playoffs(); - let conference_rounds = playoffs.conference_rounds(); + let conference_brackets = playoffs.conference_brackets(); let conferences = season.conferences(); - if conference_rounds.is_empty() { + if conference_brackets.is_empty() { return Err(format!("Conference playoffs have not been generated for the {} season", year)); } - for (conf_idx, conf_rounds) in conference_rounds.iter() { + for (conf_index, conf_rounds) in conference_brackets.iter() { // Skip if filtering to specific conference if let Some(filter) = filter_conference { - if filter != *conf_idx { + if filter != *conf_index { continue; } } - let conf_name = conferences.get(*conf_idx) + let conf_name = conferences.get(*conf_index) .map(|c| c.name().to_string()) - .unwrap_or_else(|| format!("Conference {}", conf_idx)); + .unwrap_or_else(|| format!("Conference {}", conf_index)); - if let Some(round) = conf_rounds.get(round_idx) { - println!("=== {} Round {} - {} Season ===", conf_name, round_idx, year); + if let Some(round) = conf_rounds.get(round_index) { + println!("=== {} Round {} - {} Season ===", conf_name, round_index, 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() { + 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(); @@ -128,28 +134,28 @@ fn display_conference_round( }; writeln!( &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", - matchup_idx, + 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!(); } else if filter_conference.is_some() { - return Err(format!("No round {} found for conference {}", round_idx, conf_name)); + return Err(format!("No round {} found for conference {}", round_index, conf_name)); } } // Also display winners bracket if it exists let winners_bracket = playoffs.winners_bracket(); if !winners_bracket.is_empty() { + println!(); println!("=== Championship Bracket ==="); - for (bracket_round_idx, round) in winners_bracket.iter().enumerate() { - println!("Championship Round {}", bracket_round_idx); + for (bracket_round_index, round) in winners_bracket.iter().enumerate() { + println!("Championship Round {}", bracket_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_idx, matchup) in round.matchups().iter().enumerate() { + 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(); @@ -162,7 +168,7 @@ fn display_conference_round( }; writeln!( &mut tw, "{}\t{}\t{}\t{}\t{}\t{}", - matchup_idx, + matchup_index, away_team, context.away_score(), home_team, context.home_score(), status diff --git a/src/league/season/playoffs/round/matchup/get.rs b/src/league/season/playoffs/round/matchup/get.rs index 877c2e5..8c8043d 100644 --- a/src/league/season/playoffs/round/matchup/get.rs +++ b/src/league/season/playoffs/round/matchup/get.rs @@ -27,16 +27,24 @@ 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 index: {}", 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 index: {}", conf_index)), + }; + match bracket.get(args.round) { + Some(r) => r, + None => return Err(format!("No playoff round found with index: {}", args.round)), + } }; // Get the matchup diff --git a/src/league/season/playoffs/round/matchup/sim.rs b/src/league/season/playoffs/round/matchup/sim.rs index 46d83d8..debd96c 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,15 @@ 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)); + if season.generate_next_playoff_round(&mut rng).is_ok() { + println!("\nNext playoff round generated!"); } - println!("\nNext playoff round generated!"); } // Serialize the league as JSON @@ -182,3 +172,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 index: {}", args.round)), + } + } else { + let bracket = match playoffs.conference_bracket(args.conference) { + Some(b) => b, + None => return Err(format!("No conference bracket found with index: {}", args.conference)), + }; + match bracket.get(args.round) { + Some(r) => r, + None => return Err(format!("No playoff round found with index: {}", args.round)), + } + }; + match round.matchups().get(args.matchup) { + Some(m) => Ok(m), + None => Err(format!("No matchup found with index: {}", args.matchup)), + } +} diff --git a/src/league/season/playoffs/round/sim.rs b/src/league/season/playoffs/round/sim.rs index 633e90c..f7498f2 100644 --- a/src/league/season/playoffs/round/sim.rs +++ b/src/league/season/playoffs/round/sim.rs @@ -36,29 +36,12 @@ pub fn sim_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundSimArgs) -> Result return Err(format!("Failed to simulate playoff round: {}", e)); } - // Generate the next round if the current round is complete and playoffs are not done - let playoffs = season.playoffs(); - let round_complete = if is_conference_playoff { - // For conference playoffs, check all conference rounds - playoffs.conference_rounds().iter().all(|(_, rounds)| { - rounds.get(args.round).map(|r| r.complete()).unwrap_or(true) - }) - } else { - playoffs.rounds().get(args.round).map(|r| r.complete()).unwrap_or(false) - }; - let playoffs_complete = playoffs.complete(); - - if round_complete && !playoffs_complete { - if is_conference_playoff { - if let Err(e) = season.generate_next_conference_playoff_round(&mut rng) { - // It's okay if this fails - might mean we need to transition to winners bracket - let _ = e; - } - } else { - if let Err(e) = season.generate_next_playoff_round(&mut rng) { - return Err(format!("Failed to generate next playoff round: {}", e)); - } - } + // Try to generate the next round if playoffs are not yet complete. + // generate_next_playoff_round handles all transitions (next conference round, + // winners bracket generation, next winners bracket round) and will return an + // error if the current round is not yet complete, which we can safely ignore. + if !season.playoffs().complete() { + let _ = season.generate_next_playoff_round(&mut rng); } // Get the year for display @@ -98,23 +81,28 @@ pub fn sim_playoffs_round(args: FbsimLeagueSeasonPlayoffsRoundSimArgs) -> Result fn display_traditional_round_results( season: &fbsim_core::league::season::LeagueSeason, - round_idx: usize + round_index: usize ) -> Result<(), String> { - let round = match season.playoffs().rounds().get(round_idx) { + let brackets = season.playoffs().conference_brackets(); + let rounds = match brackets.values().next() { + Some(r) => r, + None => return Err(String::from("No playoff bracket found")), + }; + let round = match rounds.get(round_index) { Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", round_idx)), + None => return Err(format!("No playoff round found with index: {}", round_index)), }; - println!("Playoff Round {} Results", round_idx); + println!("Playoff Round {} Results", round_index); 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() { + 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(); writeln!( &mut tw, "{}\t{}\t{}\t{}\t{}", - matchup_idx, + matchup_index, away_team, context.away_score(), home_team, context.home_score() ).map_err(|e| e.to_string())?; @@ -125,35 +113,35 @@ fn display_traditional_round_results( fn display_conference_round_results( season: &fbsim_core::league::season::LeagueSeason, - round_idx: usize, + round_index: usize, filter_conference: Option ) -> Result<(), String> { let conferences = season.conferences(); - let conference_rounds = season.playoffs().conference_rounds(); + let conference_brackets = season.playoffs().conference_brackets(); - for (conf_idx, conf_rounds) in conference_rounds.iter() { + for (conf_index, conf_rounds) in conference_brackets.iter() { // Skip if filtering to specific conference if let Some(filter) = filter_conference { - if filter != *conf_idx { + if filter != *conf_index { continue; } } - let conf_name = conferences.get(*conf_idx) + let conf_name = conferences.get(*conf_index) .map(|c| c.name().to_string()) - .unwrap_or_else(|| format!("Conference {}", conf_idx)); + .unwrap_or_else(|| format!("Conference {}", conf_index)); - if let Some(round) = conf_rounds.get(round_idx) { - println!("=== {} Round {} Results ===", conf_name, round_idx); + if let Some(round) = conf_rounds.get(round_index) { + println!("=== {} Round {} Results ===", conf_name, round_index); 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() { + 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(); writeln!( &mut tw, "{}\t{}\t{}\t{}\t{}", - matchup_idx, + matchup_index, away_team, context.away_score(), home_team, context.home_score() ).map_err(|e| e.to_string())?; @@ -170,13 +158,13 @@ fn display_conference_round_results( for round in winners_bracket.iter() { 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() { + 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(); writeln!( &mut tw, "{}\t{}\t{}\t{}\t{}", - matchup_idx, + matchup_index, away_team, context.away_score(), home_team, context.home_score() ).map_err(|e| e.to_string())?; diff --git a/src/league/season/standings.rs b/src/league/season/standings.rs index 2afb408..2786656 100644 --- a/src/league/season/standings.rs +++ b/src/league/season/standings.rs @@ -46,12 +46,12 @@ pub fn get_standings(args: FbsimLeagueSeasonStandingsArgs) -> Result<(), String> return Err(String::from("No conferences defined for this season")); } display_standings_by_conference(season)?; - } else if let Some(conf_idx) = args.conference { + } else if let Some(conf_index) = args.conference { // Filter by specific conference if let Some(div_id) = args.division { - display_division_standings(season, conf_idx, div_id)?; + display_division_standings(season, conf_index, div_id)?; } else { - display_conference_standings(season, conf_idx)?; + display_conference_standings(season, conf_index)?; } } else { // Display overall standings @@ -88,10 +88,10 @@ fn display_standings_by_conference( ) -> Result<(), String> { let conferences = season.conferences(); - for (conf_idx, conference) in conferences.iter().enumerate() { + for (conf_index, conference) in conferences.iter().enumerate() { println!("=== {} ===", conference.name()); - let standings = season.conference_standings(conf_idx)?; + 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())?; @@ -119,13 +119,13 @@ fn display_standings_by_division( ) -> Result<(), String> { let conferences = season.conferences(); - for (conf_idx, conference) in conferences.iter().enumerate() { + for (conf_index, conference) in conferences.iter().enumerate() { println!("=== {} ===", conference.name()); - for (div_id, division) in conference.divisions() { + for (div_id, division) in conference.divisions().iter().enumerate() { println!("--- {} ---", division.name()); - let standings = season.division_standings(conf_idx, *div_id)?; + 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())?; @@ -151,17 +151,17 @@ fn display_standings_by_division( fn display_conference_standings( season: &fbsim_core::league::season::LeagueSeason, - conf_idx: usize + conf_index: usize ) -> Result<(), String> { let conferences = season.conferences(); - let conference = match conferences.get(conf_idx) { + let conference = match conferences.get(conf_index) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", conf_idx)), + None => return Err(format!("No conference found with index: {}", conf_index)), }; println!("=== {} ===", conference.name()); - let standings = season.conference_standings(conf_idx)?; + 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())?; @@ -184,13 +184,13 @@ fn display_conference_standings( fn display_division_standings( season: &fbsim_core::league::season::LeagueSeason, - conf_idx: usize, + conf_index: usize, div_id: usize ) -> Result<(), String> { let conferences = season.conferences(); - let conference = match conferences.get(conf_idx) { + let conference = match conferences.get(conf_index) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", conf_idx)), + None => return Err(format!("No conference found with index: {}", conf_index)), }; let division = match conference.division(div_id) { @@ -200,7 +200,7 @@ fn display_division_standings( println!("=== {} - {} ===", conference.name(), division.name()); - let standings = season.division_standings(conf_idx, div_id)?; + 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())?; diff --git a/src/league/season/team/get.rs b/src/league/season/team/get.rs index 24f109d..40036bb 100644 --- a/src/league/season/team/get.rs +++ b/src/league/season/team/get.rs @@ -128,17 +128,32 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), 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; + + // Check conference brackets + 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; } } } @@ -149,15 +164,36 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), String> "Round\tHome Team\tHome Score\tAway Team\tAway Score" ).map_err(|e| e.to_string())?; - for (round_id, round) in rounds.iter().enumerate() { + 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())?; + } + } + } + } + + 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())?; From 9575059689e7ebe6611ba91ca5b3dd783eff9002 Mon Sep 17 00:00:00 2001 From: whatsacomputertho Date: Sun, 1 Feb 2026 14:53:02 -0500 Subject: [PATCH 3/7] feat(league): review and revise initial conferences & divisions impl --- src/cli/league/season/conference/division.rs | 5 - src/cli/league/season/playoffs/round.rs | 19 +-- src/league/season/conference/add.rs | 6 +- src/league/season/conference/division/add.rs | 6 +- src/league/season/conference/division/get.rs | 10 +- src/league/season/conference/division/list.rs | 14 +- src/league/season/conference/get.rs | 26 ++- src/league/season/conference/list.rs | 8 +- src/league/season/get.rs | 47 +++++- src/league/season/playoffs.rs | 1 + src/league/season/playoffs/display.rs | 87 ++++++++++ src/league/season/playoffs/get.rs | 86 +--------- src/league/season/playoffs/picture.rs | 19 ++- src/league/season/playoffs/round.rs | 1 + src/league/season/playoffs/round/display.rs | 157 +++++++++++++++++ src/league/season/playoffs/round/get.rs | 158 +----------------- .../season/playoffs/round/matchup/get.rs | 38 ++++- .../season/playoffs/round/matchup/sim.rs | 8 +- src/league/season/playoffs/round/sim.rs | 146 +++++----------- src/league/season/playoffs/sim.rs | 11 +- src/league/season/standings.rs | 22 ++- src/league/season/team/get.rs | 21 +-- src/league/season/team/list.rs | 67 ++++++-- src/league/season/week/matchup/get.rs | 157 +++-------------- 24 files changed, 533 insertions(+), 587 deletions(-) create mode 100644 src/league/season/playoffs/display.rs create mode 100644 src/league/season/playoffs/round/display.rs diff --git a/src/cli/league/season/conference/division.rs b/src/cli/league/season/conference/division.rs index 3b55aca..8c2e418 100644 --- a/src/cli/league/season/conference/division.rs +++ b/src/cli/league/season/conference/division.rs @@ -13,11 +13,6 @@ pub struct FbsimLeagueSeasonConferenceDivisionAddArgs { #[arg(long="conference")] pub conference: usize, - /// The division ID - #[arg(short='i')] - #[arg(long="id")] - pub id: usize, - /// The name of the division #[arg(short='n')] #[arg(long="name")] diff --git a/src/cli/league/season/playoffs/round.rs b/src/cli/league/season/playoffs/round.rs index ec3c511..16cfd5c 100644 --- a/src/cli/league/season/playoffs/round.rs +++ b/src/cli/league/season/playoffs/round.rs @@ -17,15 +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 conference playoffs) + /// 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 @@ -35,16 +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, - - /// Simulate conference-specific round (optional, for conference playoffs) - #[arg(short='c')] - #[arg(long="conference")] - pub conference: Option, } /// Manage rounds in the playoffs diff --git a/src/league/season/conference/add.rs b/src/league/season/conference/add.rs index 592d79e..77fb39e 100644 --- a/src/league/season/conference/add.rs +++ b/src/league/season/conference/add.rs @@ -26,8 +26,8 @@ pub fn add_conference(args: FbsimLeagueSeasonConferenceAddArgs) -> Result<(), St None => return Err(String::from("No current season found")), }; - // Get the index before adding - let conf_index = season.conferences().len(); + // Get the conference ID before adding + let conf_id = season.conferences().len(); // Add the conference let conference = LeagueConference::with_name(&args.name); @@ -46,6 +46,6 @@ pub fn add_conference(args: FbsimLeagueSeasonConferenceAddArgs) -> Result<(), St return Err(format!("Error writing league file: {}", e)); } - println!("Conference '{}' added with index {}", args.name, conf_index); + println!("Conference {} added to season with ID {}", args.name, conf_id); Ok(()) } diff --git a/src/league/season/conference/division/add.rs b/src/league/season/conference/division/add.rs index f4b696c..faeb227 100644 --- a/src/league/season/conference/division/add.rs +++ b/src/league/season/conference/division/add.rs @@ -32,6 +32,10 @@ pub fn add_division(args: FbsimLeagueSeasonConferenceDivisionAddArgs) -> Result< 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); @@ -49,6 +53,6 @@ pub fn add_division(args: FbsimLeagueSeasonConferenceDivisionAddArgs) -> Result< return Err(format!("Error writing league file: {}", e)); } - println!("Division '{}' added to conference {}", args.name, args.conference); + 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 index a8367de..46b39dc 100644 --- a/src/league/season/conference/division/get.rs +++ b/src/league/season/conference/division/get.rs @@ -21,20 +21,16 @@ pub fn get_division(args: FbsimLeagueSeasonConferenceDivisionGetArgs) -> Result< Err(error) => return Err(format!("Error loading league from file: {}", error)), }; - // Get the season + // 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)), }; - - // Get the conference let conferences = season.conferences(); let conference = match conferences.get(args.conference) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", args.conference)), + None => return Err(format!("No conference found with ID: {}", args.conference)), }; - - // Get the division let division = match conference.division(args.division) { Some(d) => d, None => return Err(format!("No division found with ID: {}", args.division)), @@ -43,7 +39,6 @@ pub fn get_division(args: FbsimLeagueSeasonConferenceDivisionGetArgs) -> Result< // 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, "ID:\t{}", args.division).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())?; @@ -68,7 +63,6 @@ pub fn get_division(args: FbsimLeagueSeasonConferenceDivisionGetArgs) -> Result< } } } - 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 index b8e5fd0..d595d2d 100644 --- a/src/league/season/conference/division/list.rs +++ b/src/league/season/conference/division/list.rs @@ -21,32 +21,26 @@ pub fn list_divisions(args: FbsimLeagueSeasonConferenceDivisionListArgs) -> Resu Err(error) => return Err(format!("Error loading league from file: {}", error)), }; - // Get the season + // 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)), }; - - // Get the conference let conferences = season.conferences(); let conference = match conferences.get(args.conference) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", args.conference)), + None => return Err(format!("No conference found with ID: {}", args.conference)), }; - - // Get divisions let divisions = conference.divisions(); - if divisions.is_empty() { - println!("No divisions found in conference '{}'", conference.name()); + println!("No divisions found in conference {}", conference.name()); return Ok(()); } // Display divisions in a table - println!("Divisions in {} Conference", conference.name()); + 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{}", diff --git a/src/league/season/conference/get.rs b/src/league/season/conference/get.rs index c1bec61..5697980 100644 --- a/src/league/season/conference/get.rs +++ b/src/league/season/conference/get.rs @@ -21,26 +21,20 @@ pub fn get_conference(args: FbsimLeagueSeasonConferenceGetArgs) -> Result<(), St Err(error) => return Err(format!("Error loading league from file: {}", error)), }; - // Get the season + // 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)), }; - - // Get the conference let conferences = season.conferences(); let conference = match conferences.get(args.conference) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", args.conference)), + None => return Err(format!("No conference found with ID: {}", args.conference)), }; - // Display conference info + // Display conference divisions in a table + println!("=== {} ===", conference.name()); let mut tw = TabWriter::new(stdout()); - writeln!(&mut tw, "Conference:\t{}", conference.name()).map_err(|e| e.to_string())?; - writeln!(&mut tw, "Index:\t{}", args.conference).map_err(|e| e.to_string())?; - writeln!(&mut tw).map_err(|e| e.to_string())?; - - // Display divisions if conference.divisions().is_empty() { writeln!(&mut tw, "No divisions").map_err(|e| e.to_string())?; } else { @@ -58,25 +52,27 @@ pub fn get_conference(args: FbsimLeagueSeasonConferenceGetArgs) -> Result<(), St // 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").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{}", + &mut tw, "{}\t{}\t{}\t{}", team_id, team.name(), - division.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 index 744711f..c20792a 100644 --- a/src/league/season/conference/list.rs +++ b/src/league/season/conference/list.rs @@ -21,15 +21,12 @@ pub fn list_conferences(args: FbsimLeagueSeasonConferenceListArgs) -> Result<(), Err(error) => return Err(format!("Error loading league from file: {}", error)), }; - // Get the season + // 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)), }; - - // Get conferences let conferences = season.conferences(); - if conferences.is_empty() { println!("No conferences found for the {} season", args.year); return Ok(()); @@ -37,8 +34,7 @@ pub fn list_conferences(args: FbsimLeagueSeasonConferenceListArgs) -> Result<(), // Display conferences in a table let mut tw = TabWriter::new(stdout()); - writeln!(&mut tw, "Index\tName\tDivisions\tTeams").map_err(|e| e.to_string())?; - + 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(); diff --git a/src/league/season/get.rs b/src/league/season/get.rs index 76fefbf..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())?; } 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..b00ad01 --- /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); + + 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/get.rs b/src/league/season/playoffs/get.rs index 5593571..061c21d 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,89 +26,10 @@ 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(); - - // Display playoff status println!("Playoffs for {} season ({} teams)", args.year, playoffs.num_teams()); println!(); - // 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!("=== {} ===", conf_name); - - 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(()) + display::display_playoffs(season) } diff --git a/src/league/season/playoffs/picture.rs b/src/league/season/playoffs/picture.rs index b2678a6..7475633 100644 --- a/src/league/season/playoffs/picture.rs +++ b/src/league/season/playoffs/picture.rs @@ -46,7 +46,6 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul // 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( @@ -72,8 +71,12 @@ fn display_traditional_playoff_picture( args: &FbsimLeagueSeasonPlayoffsPictureArgs, weeks_remaining: usize ) -> Result<(), String> { - // Get the playoff picture - let picture = season.playoff_picture(args.num_playoff_teams)?; + // 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 header println!("Playoff Picture for {} Season", args.year); @@ -135,7 +138,7 @@ fn display_conference_playoff_picture_sections( ) -> Result<(), String> { let conference = match season.conferences().get(conf_index) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", conf_index)), + None => return Err(format!("No conference found with ID: {}", conf_index)), }; let conf_teams: Vec = conference.all_teams(); @@ -229,7 +232,7 @@ fn display_single_conference_picture( let conferences = season.conferences(); let conference = match conferences.get(conf_index) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", conf_index)), + None => return Err(format!("No conference found with ID: {}", conf_index)), }; // Display header @@ -239,7 +242,11 @@ fn display_single_conference_picture( println!(); // Use regular playoff picture but filtered to conference teams - let picture = season.playoff_picture(args.num_playoff_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(()) 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..0ac1a25 --- /dev/null +++ b/src/league/season/playoffs/round/display.rs @@ -0,0 +1,157 @@ +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 a5acc0f..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,162 +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(); + // Display the playoff round if playoffs.is_conference_playoff() { - display_conference_round(season, args.round, args.conference, args.year)?; - } else { - display_traditional_round(season, args.round, args.year)?; - } - - Ok(()) -} - -fn display_traditional_round( - season: &fbsim_core::league::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 traditional (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)), - }; - - // Get the round - let round = match rounds.get(round_index) { - Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", round_index)), - }; - - // Display the round - println!("Playoff Round {} - {} Season", round_index, 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_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" + 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_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(()) -} - -fn display_conference_round( - season: &fbsim_core::league::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(); - - 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; - } - } - - 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!("=== {} Round {} - {} Season ===", conf_name, round_index, 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_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)); - } - } - - // Also display winners bracket if it exists - let winners_bracket = playoffs.winners_bracket(); - if !winners_bracket.is_empty() { - println!(); - println!("=== Championship Bracket ==="); - for (bracket_round_index, round) in winners_bracket.iter().enumerate() { - println!("Championship Round {}", bracket_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())?; + display::display_conference_round(season, args.round, args.conference, args.year) } + } else { + display::display_traditional_round(season, args.round, args.year) } - - Ok(()) } diff --git a/src/league/season/playoffs/round/matchup/get.rs b/src/league/season/playoffs/round/matchup/get.rs index 8c8043d..418c6a2 100644 --- a/src/league/season/playoffs/round/matchup/get.rs +++ b/src/league/season/playoffs/round/matchup/get.rs @@ -33,24 +33,24 @@ pub fn get_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupGetArgs) let winners = playoffs.winners_bracket(); match winners.get(args.round) { Some(r) => r, - None => return Err(format!("No winners bracket round found with index: {}", args.round)), + 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 index: {}", conf_index)), + 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 index: {}", args.round)), + 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 @@ -58,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); @@ -75,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 debd96c..defa177 100644 --- a/src/league/season/playoffs/round/matchup/sim.rs +++ b/src/league/season/playoffs/round/matchup/sim.rs @@ -181,20 +181,20 @@ fn get_matchup<'a>( let winners = playoffs.winners_bracket(); match winners.get(args.round) { Some(r) => r, - None => return Err(format!("No winners bracket round found with index: {}", args.round)), + 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 index: {}", args.conference)), + 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 index: {}", args.round)), + 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 index: {}", args.matchup)), + 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 f7498f2..fe6dd4e 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,31 +27,34 @@ 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(); let is_conference_playoff = season.playoffs().is_conference_playoff(); + let year = *season.year(); + + // Determine the current round and whether it's a winners bracket round + let (round_index, is_winners_bracket) = find_current_round(season)?; - // For conference playoffs, simulate the round (the core library handles conference-specific logic) - if let Err(e) = season.sim_playoff_round(args.round, &mut rng) { + // 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)); } // Try to generate the next round if playoffs are not yet complete. - // generate_next_playoff_round handles all transitions (next conference round, - // winners bracket generation, next winners bracket round) and will return an - // error if the current round is not yet complete, which we can safely ignore. if !season.playoffs().complete() { let _ = season.generate_next_playoff_round(&mut rng); } - // Get the year for display - let year = *season.year(); - - // Display results - if is_conference_playoff { - display_conference_round_results(season, args.round, args.conference)?; + // 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_traditional_round_results(season, args.round)?; + display::display_traditional_round(season, round_index, year)?; } // Display champion if playoffs are complete @@ -74,104 +77,41 @@ 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(()) } -fn display_traditional_round_results( - season: &fbsim_core::league::season::LeagueSeason, - round_index: usize -) -> Result<(), String> { - let brackets = season.playoffs().conference_brackets(); - let rounds = match brackets.values().next() { - Some(r) => r, - None => return Err(String::from("No playoff bracket found")), - }; - let round = match rounds.get(round_index) { - Some(r) => r, - None => return Err(format!("No playoff round found with index: {}", round_index)), - }; +/// 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(); - println!("Playoff Round {} Results", round_index); - 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_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(); - writeln!( - &mut tw, "{}\t{}\t{}\t{}\t{}", - matchup_index, - away_team, context.away_score(), - home_team, context.home_score() - ).map_err(|e| e.to_string())?; + if playoffs.complete() { + return Err(String::from("Playoffs are already complete")); } - tw.flush().map_err(|e| e.to_string())?; - Ok(()) -} - -fn display_conference_round_results( - season: &fbsim_core::league::season::LeagueSeason, - round_index: usize, - filter_conference: Option -) -> Result<(), String> { - let conferences = season.conferences(); - let conference_brackets = season.playoffs().conference_brackets(); - - 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; - } - } - 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!("=== {} Round {} Results ===", conf_name, round_index); - 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_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(); - writeln!( - &mut tw, "{}\t{}\t{}\t{}\t{}", - matchup_index, - away_team, context.away_score(), - home_team, context.home_score() - ).map_err(|e| e.to_string())?; + // 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)); + } } - tw.flush().map_err(|e| e.to_string())?; - println!(); } + return Err(String::from("All conference playoff rounds are complete")); } - // Also display winners bracket if it exists - let winners_bracket = season.playoffs().winners_bracket(); - if !winners_bracket.is_empty() { - println!("=== Championship Round ==="); - for round in winners_bracket.iter() { - 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_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(); - writeln!( - &mut tw, "{}\t{}\t{}\t{}\t{}", - matchup_index, - away_team, context.away_score(), - home_team, context.home_score() - ).map_err(|e| e.to_string())?; - } - tw.flush().map_err(|e| e.to_string())?; + // 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)); } } - - Ok(()) + 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 426e00b..74734ea 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); @@ -52,6 +48,5 @@ pub fn sim_playoffs(args: FbsimLeagueSeasonPlayoffsSimArgs) -> Result<(), String return Err(format!("Error writing league file: {}", e)); } - println!("{}", champion_msg); Ok(()) } diff --git a/src/league/season/standings.rs b/src/league/season/standings.rs index 2786656..7fb82f9 100644 --- a/src/league/season/standings.rs +++ b/src/league/season/standings.rs @@ -87,6 +87,7 @@ fn display_standings_by_conference( season: &fbsim_core::league::season::LeagueSeason ) -> Result<(), String> { let conferences = season.conferences(); + let num_conferences = conferences.len(); for (conf_index, conference) in conferences.iter().enumerate() { println!("=== {} ===", conference.name()); @@ -109,7 +110,9 @@ fn display_standings_by_conference( ).map_err(|e| e.to_string())?; } tw.flush().map_err(|e| e.to_string())?; - println!(); + if conf_index != (num_conferences - 1) { + println!(); + } } Ok(()) } @@ -118,11 +121,14 @@ fn display_standings_by_division( season: &fbsim_core::league::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 conference.divisions().iter().enumerate() { + for (div_id, division) in divisions.iter().enumerate() { println!("--- {} ---", division.name()); let standings = season.division_standings(conf_index, div_id)?; @@ -143,6 +149,12 @@ fn display_standings_by_division( ).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!(); } } @@ -156,7 +168,7 @@ fn display_conference_standings( let conferences = season.conferences(); let conference = match conferences.get(conf_index) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", conf_index)), + None => return Err(format!("No conference found with ID: {}", conf_index)), }; println!("=== {} ===", conference.name()); @@ -190,7 +202,7 @@ fn display_division_standings( let conferences = season.conferences(); let conference = match conferences.get(conf_index) { Some(c) => c, - None => return Err(format!("No conference found with index: {}", conf_index)), + None => return Err(format!("No conference found with ID: {}", conf_index)), }; let division = match conference.division(div_id) { @@ -198,7 +210,7 @@ fn display_division_standings( None => return Err(format!("No division found with ID: {}", div_id)), }; - println!("=== {} - {} ===", conference.name(), division.name()); + println!("=== {} {} ===", conference.name(), division.name()); let standings = season.division_standings(conf_index, div_id)?; diff --git a/src/league/season/team/get.rs b/src/league/season/team/get.rs index 40036bb..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,7 +115,7 @@ 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())?; }, } @@ -129,8 +123,6 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), String> // Display playoff matchups if the team participated let mut has_playoff_matchups = false; - - // Check conference brackets for rounds in playoffs.conference_brackets().values() { for round in rounds.iter() { for matchup in round.matchups().iter() { @@ -156,14 +148,15 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), String> 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())?; + // 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()) @@ -185,6 +178,7 @@ pub fn get_season_team(args: FbsimLeagueSeasonTeamGetArgs) -> Result<(), 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 { @@ -201,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..e9ebc9d 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,9 +19,6 @@ 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 let season = match league.season(args.year) { Some(season) => season, @@ -54,137 +49,35 @@ pub fn get_matchup(args: FbsimLeagueSeasonWeekMatchupGetArgs) -> Result<(), Stri // 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())?; - - // Get the home and away stats - let home_stats_opt = matchup.home_stats(); - let away_stats_opt = matchup.away_stats(); + // Display based on game state + println!("{} season week {} matchup {}", args.year, args.week, args.matchup); + println!(); + println!("{} @ {}", away_team, home_team); + println!(); - // Get the game - let game_opt = matchup.game(); + if context.game_over() { + println!("{} Final", context); - // 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())?; + 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())?; - // 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())?; - } 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())?; Ok(()) } From 807a29d2e533bf8935a74013db3d0947c99df717 Mon Sep 17 00:00:00 2001 From: whatsacomputertho Date: Sun, 1 Feb 2026 15:36:54 -0500 Subject: [PATCH 4/7] fix(format): remove newlines, proper imports and comments --- src/cli/league/season/conference.rs | 2 +- src/cli/league/season/playoffs.rs | 4 +- src/cli/league/season/standings.rs | 4 +- src/cli/league/season/team.rs | 2 +- src/league/season/conference/add.rs | 1 - src/league/season/conference/division/add.rs | 5 +-- src/league/season/playoffs/display.rs | 2 +- src/league/season/playoffs/gen.rs | 5 --- src/league/season/playoffs/get.rs | 1 - src/league/season/playoffs/picture.rs | 19 ++-------- src/league/season/playoffs/round/display.rs | 1 - .../season/playoffs/round/matchup/sim.rs | 4 +- src/league/season/playoffs/round/sim.rs | 1 - src/league/season/playoffs/sim.rs | 3 +- src/league/season/standings.rs | 38 ++++--------------- src/league/season/team/assign.rs | 1 - src/league/season/week/matchup/get.rs | 12 +----- 17 files changed, 22 insertions(+), 83 deletions(-) diff --git a/src/cli/league/season/conference.rs b/src/cli/league/season/conference.rs index aad58e7..5c44cde 100644 --- a/src/cli/league/season/conference.rs +++ b/src/cli/league/season/conference.rs @@ -45,7 +45,7 @@ pub struct FbsimLeagueSeasonConferenceGetArgs { #[arg(long="year")] pub year: usize, - /// The conference index + /// The conference ID #[arg(short='c')] #[arg(long="conference")] pub conference: usize, diff --git a/src/cli/league/season/playoffs.rs b/src/cli/league/season/playoffs.rs index 7c31b02..94f90d7 100644 --- a/src/cli/league/season/playoffs.rs +++ b/src/cli/league/season/playoffs.rs @@ -17,7 +17,7 @@ pub struct FbsimLeagueSeasonPlayoffsGenArgs { #[arg(long="num-teams")] pub num_teams: usize, - /// Treat num_teams as per-conference instead of total + /// Enable multi-conference playoffs, where number of teams is per-conference #[arg(short='p')] #[arg(long="per-conference")] pub per_conference: bool, @@ -70,7 +70,7 @@ pub struct FbsimLeagueSeasonPlayoffsPictureArgs { #[arg(default_value="4")] pub num_playoff_teams: usize, - /// Treat num_playoff_teams as per-conference instead of total + /// Calculate multi-conference playoff picture, where number of teams is per-conference #[arg(short='p')] #[arg(long="per-conference")] pub per_conference: bool, diff --git a/src/cli/league/season/standings.rs b/src/cli/league/season/standings.rs index caa53a5..eb825e1 100644 --- a/src/cli/league/season/standings.rs +++ b/src/cli/league/season/standings.rs @@ -23,11 +23,11 @@ pub struct FbsimLeagueSeasonStandingsArgs { #[arg(long="division")] pub division: Option, - /// Group output by conference + /// Group standings by conference #[arg(long="by-conference")] pub by_conference: bool, - /// Group output by division + /// 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 296f026..44b491c 100644 --- a/src/cli/league/season/team.rs +++ b/src/cli/league/season/team.rs @@ -36,7 +36,7 @@ pub struct FbsimLeagueSeasonTeamAssignArgs { #[arg(long="team")] pub team: usize, - /// The conference index + /// The conference ID #[arg(short='c')] #[arg(long="conference")] pub conference: usize, diff --git a/src/league/season/conference/add.rs b/src/league/season/conference/add.rs index 77fb39e..c0448ec 100644 --- a/src/league/season/conference/add.rs +++ b/src/league/season/conference/add.rs @@ -45,7 +45,6 @@ pub fn add_conference(args: FbsimLeagueSeasonConferenceAddArgs) -> Result<(), St 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/add.rs b/src/league/season/conference/division/add.rs index faeb227..68d2a36 100644 --- a/src/league/season/conference/division/add.rs +++ b/src/league/season/conference/division/add.rs @@ -20,13 +20,11 @@ pub fn add_division(args: FbsimLeagueSeasonConferenceDivisionAddArgs) -> Result< Err(error) => return Err(format!("Error loading league from file: {}", error)), }; - // Get the current season + // 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")), }; - - // Get the conference let conference = match season.conference_mut(args.conference) { Some(c) => c, None => return Err(format!("No conference found with index: {}", args.conference)), @@ -52,7 +50,6 @@ pub fn add_division(args: FbsimLeagueSeasonConferenceDivisionAddArgs) -> Result< 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/playoffs/display.rs b/src/league/season/playoffs/display.rs index b00ad01..5c91014 100644 --- a/src/league/season/playoffs/display.rs +++ b/src/league/season/playoffs/display.rs @@ -13,6 +13,7 @@ pub fn display_playoffs(season: &LeagueSeason) -> Result<(), 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()); @@ -82,6 +83,5 @@ pub fn display_playoffs(season: &LeagueSeason) -> Result<(), String> { } else { println!("Playoffs in progress"); } - Ok(()) } diff --git a/src/league/season/playoffs/gen.rs b/src/league/season/playoffs/gen.rs index df14c0d..2eb7b60 100644 --- a/src/league/season/playoffs/gen.rs +++ b/src/league/season/playoffs/gen.rs @@ -28,9 +28,7 @@ pub fn gen_playoffs(args: FbsimLeagueSeasonPlayoffsGenArgs) -> Result<(), String // Generate the playoffs let mut rng = rand::thread_rng(); - let mut options = LeagueSeasonPlayoffOptions::new(); - let result_msg = if args.per_conference { // Validate conferences exist if season.conferences().is_empty() { @@ -47,7 +45,6 @@ pub fn gen_playoffs(args: FbsimLeagueSeasonPlayoffsGenArgs) -> Result<(), String 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)", @@ -59,7 +56,6 @@ pub fn gen_playoffs(args: FbsimLeagueSeasonPlayoffsGenArgs) -> Result<(), String 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) }; @@ -75,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!("{}", result_msg); Ok(()) } diff --git a/src/league/season/playoffs/get.rs b/src/league/season/playoffs/get.rs index 061c21d..1f2bb3c 100644 --- a/src/league/season/playoffs/get.rs +++ b/src/league/season/playoffs/get.rs @@ -30,6 +30,5 @@ pub fn get_playoffs(args: FbsimLeagueSeasonPlayoffsGetArgs) -> Result<(), String let playoffs = season.playoffs(); println!("Playoffs for {} season ({} teams)", args.year, playoffs.num_teams()); println!(); - display::display_playoffs(season) } diff --git a/src/league/season/playoffs/picture.rs b/src/league/season/playoffs/picture.rs index 7475633..f66f622 100644 --- a/src/league/season/playoffs/picture.rs +++ b/src/league/season/playoffs/picture.rs @@ -62,7 +62,6 @@ pub fn get_playoffs_picture(args: FbsimLeagueSeasonPlayoffsPictureArgs) -> Resul // Display traditional playoff picture display_traditional_playoff_picture(season, &args, weeks_remaining)?; } - Ok(()) } @@ -78,12 +77,11 @@ fn display_traditional_playoff_picture( }; let picture = PlayoffPicture::from_season(season, args.num_playoff_teams, Some(options))?; - // Display header + // 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(()) @@ -102,7 +100,7 @@ fn display_conference_playoff_picture( println!("Weeks remaining in season: {}", weeks_remaining); println!(); - // Get the conference-aware playoff picture + // Get the conference-based playoff picture let options = PlayoffPictureOptions { by_conference: Some(true), division_winners_guaranteed: args.division_winners, @@ -112,7 +110,6 @@ fn display_conference_playoff_picture( 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 { @@ -121,12 +118,10 @@ fn display_conference_playoff_picture( } } + // Display the playoff-picture println!("=== {} Playoff Picture ===", conference.name()); - - // Filter the picture entries by conference display_conference_playoff_picture_sections(&picture, season, conf_index)?; } - display_legend(); Ok(()) } @@ -151,7 +146,6 @@ fn display_conference_playoff_picture_sections( 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() { @@ -182,7 +176,6 @@ fn display_conference_playoff_picture_sections( 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, @@ -206,7 +199,6 @@ fn display_conference_playoff_picture_sections( 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, @@ -219,7 +211,6 @@ fn display_conference_playoff_picture_sections( tw.flush().map_err(|e| e.to_string())?; println!(); } - Ok(()) } @@ -259,7 +250,6 @@ fn display_playoff_picture_sections(picture: &PlayoffPicture) -> Result<(), Stri 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() { @@ -287,7 +277,6 @@ fn display_playoff_picture_sections(picture: &PlayoffPicture) -> Result<(), Stri 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, @@ -308,7 +297,6 @@ fn display_playoff_picture_sections(picture: &PlayoffPicture) -> Result<(), Stri 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, @@ -321,7 +309,6 @@ fn display_playoff_picture_sections(picture: &PlayoffPicture) -> Result<(), Stri tw.flush().map_err(|e| e.to_string())?; println!(); } - Ok(()) } diff --git a/src/league/season/playoffs/round/display.rs b/src/league/season/playoffs/round/display.rs index 0ac1a25..f9bcb63 100644 --- a/src/league/season/playoffs/round/display.rs +++ b/src/league/season/playoffs/round/display.rs @@ -10,7 +10,6 @@ pub fn display_traditional_round( ) -> 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)); } diff --git a/src/league/season/playoffs/round/matchup/sim.rs b/src/league/season/playoffs/round/matchup/sim.rs index defa177..1560739 100644 --- a/src/league/season/playoffs/round/matchup/sim.rs +++ b/src/league/season/playoffs/round/matchup/sim.rs @@ -153,9 +153,7 @@ pub fn sim_playoffs_matchup(args: FbsimLeagueSeasonPlayoffsRoundMatchupSimArgs) Some(s) => s, None => return Err(String::from("No current season found for generating next round")), }; - if season.generate_next_playoff_round(&mut rng).is_ok() { - println!("\nNext playoff round generated!"); - } + season.generate_next_playoff_round(&mut rng)?; } // Serialize the league as JSON diff --git a/src/league/season/playoffs/round/sim.rs b/src/league/season/playoffs/round/sim.rs index fe6dd4e..e5806c9 100644 --- a/src/league/season/playoffs/round/sim.rs +++ b/src/league/season/playoffs/round/sim.rs @@ -85,7 +85,6 @@ 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")); } diff --git a/src/league/season/playoffs/sim.rs b/src/league/season/playoffs/sim.rs index 74734ea..b58b61f 100644 --- a/src/league/season/playoffs/sim.rs +++ b/src/league/season/playoffs/sim.rs @@ -26,7 +26,7 @@ pub fn sim_playoffs(args: FbsimLeagueSeasonPlayoffsSimArgs) -> Result<(), String None => return Err(String::from("No current season found")), }; - // Simulate the playoffs (handles both traditional and conference playoffs) + // Simulate the playoffs let mut rng = rand::thread_rng(); if let Err(e) = season.sim_playoffs(&mut rng) { return Err(format!("Failed to simulate playoffs: {}", e)); @@ -47,6 +47,5 @@ pub fn sim_playoffs(args: FbsimLeagueSeasonPlayoffsSimArgs) -> Result<(), String if let Err(e) = write_res { return Err(format!("Error writing league file: {}", e)); } - Ok(()) } diff --git a/src/league/season/standings.rs b/src/league/season/standings.rs index 7fb82f9..b5b3ad2 100644 --- a/src/league/season/standings.rs +++ b/src/league/season/standings.rs @@ -2,6 +2,7 @@ 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; @@ -31,11 +32,10 @@ pub fn get_standings(args: FbsimLeagueSeasonStandingsArgs) -> Result<(), String> Some(season) => season, None => return Err(format!("No season found with year: {}", args.year)), }; - let conferences = season.conferences(); let has_conferences = !conferences.is_empty(); - // Handle different display modes + // 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")); @@ -47,26 +47,21 @@ pub fn get_standings(args: FbsimLeagueSeasonStandingsArgs) -> Result<(), String> } display_standings_by_conference(season)?; } else if let Some(conf_index) = args.conference { - // Filter by specific 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 display_overall_standings(season)?; } - Ok(()) } -fn display_overall_standings(season: &fbsim_core::league::season::LeagueSeason) -> Result<(), String> { +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) @@ -84,19 +79,15 @@ fn display_overall_standings(season: &fbsim_core::league::season::LeagueSeason) } fn display_standings_by_conference( - season: &fbsim_core::league::season::LeagueSeason + 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) @@ -118,24 +109,19 @@ fn display_standings_by_conference( } fn display_standings_by_division( - season: &fbsim_core::league::season::LeagueSeason + 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) @@ -153,7 +139,6 @@ fn display_standings_by_division( println!(); } } - if conf_index != (num_conferences - 1) { println!(); } @@ -162,7 +147,7 @@ fn display_standings_by_division( } fn display_conference_standings( - season: &fbsim_core::league::season::LeagueSeason, + season: &LeagueSeason, conf_index: usize ) -> Result<(), String> { let conferences = season.conferences(); @@ -170,14 +155,10 @@ fn display_conference_standings( 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) @@ -195,7 +176,7 @@ fn display_conference_standings( } fn display_division_standings( - season: &fbsim_core::league::season::LeagueSeason, + season: &LeagueSeason, conf_index: usize, div_id: usize ) -> Result<(), String> { @@ -204,19 +185,14 @@ fn display_division_standings( 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) diff --git a/src/league/season/team/assign.rs b/src/league/season/team/assign.rs index 66ecdbc..32a7f8e 100644 --- a/src/league/season/team/assign.rs +++ b/src/league/season/team/assign.rs @@ -60,7 +60,6 @@ pub fn assign_team(args: FbsimLeagueSeasonTeamAssignArgs) -> Result<(), String> 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/week/matchup/get.rs b/src/league/season/week/matchup/get.rs index e9ebc9d..5831a47 100644 --- a/src/league/season/week/matchup/get.rs +++ b/src/league/season/week/matchup/get.rs @@ -19,19 +19,15 @@ pub fn get_matchup(args: FbsimLeagueSeasonWeekMatchupGetArgs) -> Result<(), Stri Err(error) => return Err(format!("Error loading league from file: {}", error)), }; - // 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( @@ -42,11 +38,9 @@ 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 based on game state @@ -54,7 +48,6 @@ pub fn get_matchup(args: FbsimLeagueSeasonWeekMatchupGetArgs) -> Result<(), Stri println!(); println!("{} @ {}", away_team, home_team); println!(); - if context.game_over() { println!("{} Final", context); @@ -78,6 +71,5 @@ pub fn get_matchup(args: FbsimLeagueSeasonWeekMatchupGetArgs) -> Result<(), Stri } else { println!("{} Pending", context); } - Ok(()) } From f657956ee79ac82fff63584da7e9f14179339901 Mon Sep 17 00:00:00 2001 From: whatsacomputertho Date: Sun, 1 Feb 2026 16:18:29 -0500 Subject: [PATCH 5/7] fix(duplicate): check for duplicate conf and div teams before adding --- Cargo.lock | 1 - Cargo.toml | 3 ++- src/league/season/conference/add.rs | 2 +- src/league/season/conference/division/add.rs | 2 +- src/league/season/team/assign.rs | 14 +++++++++++++- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92ae47f..802958f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,7 +240,6 @@ dependencies = [ [[package]] name = "fbsim-core" version = "1.0.0-beta.1" -source = "git+https://github.com/whatsacomputertho/fbsim-core.git?branch=conferences-divisions#015ed1d680aa83fe0474ca4807d38386b334b036" dependencies = [ "chrono", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 699f407..a48f08c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.23", features = ["derive"] } -fbsim-core = { git = "https://github.com/whatsacomputertho/fbsim-core.git", branch = "conferences-divisions" } +fbsim-core = { path = "../fbsim-core" } +#fbsim-core = { git = "https://github.com/whatsacomputertho/fbsim-core.git", branch = "conferences-divisions" } crossterm = "0.27.0" indicatif = "0.17.11" rand = "0.8.5" diff --git a/src/league/season/conference/add.rs b/src/league/season/conference/add.rs index c0448ec..d53aca0 100644 --- a/src/league/season/conference/add.rs +++ b/src/league/season/conference/add.rs @@ -31,7 +31,7 @@ pub fn add_conference(args: FbsimLeagueSeasonConferenceAddArgs) -> Result<(), St // Add the conference let conference = LeagueConference::with_name(&args.name); - season.add_conference(conference); + season.add_conference(conference)?; // Serialize the league as JSON let league_res = serde_json::to_string_pretty(&league); diff --git a/src/league/season/conference/division/add.rs b/src/league/season/conference/division/add.rs index 68d2a36..bd0318f 100644 --- a/src/league/season/conference/division/add.rs +++ b/src/league/season/conference/division/add.rs @@ -36,7 +36,7 @@ pub fn add_division(args: FbsimLeagueSeasonConferenceDivisionAddArgs) -> Result< // Add the division let division = LeagueDivision::with_name(&args.name); - conference.add_division(division); + conference.add_division(division)?; // Serialize the league as JSON let league_res = serde_json::to_string_pretty(&league); diff --git a/src/league/season/team/assign.rs b/src/league/season/team/assign.rs index 32a7f8e..4a4e377 100644 --- a/src/league/season/team/assign.rs +++ b/src/league/season/team/assign.rs @@ -43,10 +43,22 @@ pub fn assign_team(args: FbsimLeagueSeasonTeamAssignArgs) -> Result<(), 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); + division.add_team(args.team)?; // Serialize the league as JSON let league_res = serde_json::to_string_pretty(&league); From 49c8b0aa36e5a19cb168ad7c8a632bd26eb6aec4 Mon Sep 17 00:00:00 2001 From: whatsacomputertho Date: Sun, 1 Feb 2026 16:21:34 -0500 Subject: [PATCH 6/7] fix(build): rebuild and regenerate manifest and lock --- Cargo.lock | 1 + Cargo.toml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 802958f..d897639 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,7 @@ dependencies = [ [[package]] name = "fbsim-core" version = "1.0.0-beta.1" +source = "git+https://github.com/whatsacomputertho/fbsim-core.git?branch=conferences-divisions#cf47167136eb782b53d0c771fa5b1f342b164137" dependencies = [ "chrono", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index a48f08c..699f407 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.23", features = ["derive"] } -fbsim-core = { path = "../fbsim-core" } -#fbsim-core = { git = "https://github.com/whatsacomputertho/fbsim-core.git", branch = "conferences-divisions" } +fbsim-core = { git = "https://github.com/whatsacomputertho/fbsim-core.git", branch = "conferences-divisions" } crossterm = "0.27.0" indicatif = "0.17.11" rand = "0.8.5" From d0691e79ae6723e842a6ff66deb1953611c56ba0 Mon Sep 17 00:00:00 2001 From: whatsacomputertho Date: Sun, 1 Feb 2026 16:30:12 -0500 Subject: [PATCH 7/7] chore(deps): use fbsim-core main branch --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d897639..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=conferences-divisions#cf47167136eb782b53d0c771fa5b1f342b164137" +source = "git+https://github.com/whatsacomputertho/fbsim-core.git?branch=main#b615b83e6085abbe74116361ea6c1e7ac0ee9a79" dependencies = [ "chrono", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 699f407..6385731 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.23", features = ["derive"] } -fbsim-core = { git = "https://github.com/whatsacomputertho/fbsim-core.git", branch = "conferences-divisions" } +fbsim-core = { git = "https://github.com/whatsacomputertho/fbsim-core.git", branch = "main" } crossterm = "0.27.0" indicatif = "0.17.11" rand = "0.8.5"