Live standings/bracket updates + possession stats
This commit is contained in:
parent
e3242583b6
commit
39ea6c0c0a
6 changed files with 249 additions and 85 deletions
13
DEVLOG.md
13
DEVLOG.md
|
|
@ -54,3 +54,16 @@
|
||||||
- Updated `quick` command to support optional `--home` / `--away`:
|
- Updated `quick` command to support optional `--home` / `--away`:
|
||||||
- If missing, CPU auto-fills from remaining teams.
|
- If missing, CPU auto-fills from remaining teams.
|
||||||
- Same seed yields the same auto-filled teams and outcomes.
|
- Same seed yields the same auto-filled teams and outcomes.
|
||||||
|
|
||||||
|
## 2026-02-10 - Live standings/bracket updates + possession surfacing
|
||||||
|
|
||||||
|
### Scope completed
|
||||||
|
- Added explicit possession fields to match results and surfaced them in quick mode output.
|
||||||
|
- Added possession row to single-match CSV export output.
|
||||||
|
- Implemented live detail updates driven by simulation frames:
|
||||||
|
- per-frame stats updates
|
||||||
|
- per-frame competition panel updates
|
||||||
|
- incremental history updates
|
||||||
|
- Added live league standings snapshots after each fixture.
|
||||||
|
- Added ASCII knockout tree bracket snapshots and updates after each semifinal/final.
|
||||||
|
- Tuned detail view layout to allocate more space to stats/competition/history panels.
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,14 @@ pub fn simulation_to_csv_bytes(sim: &PreparedSimulation) -> io::Result<Vec<u8>>
|
||||||
format!("{:.2}", m.stats.away.xg),
|
format!("{:.2}", m.stats.away.xg),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
write_row(
|
||||||
|
&mut out,
|
||||||
|
&[
|
||||||
|
"Possession".to_string(),
|
||||||
|
format!("{}%", m.home_possession),
|
||||||
|
format!("{}%", m.away_possession),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
write_row(
|
write_row(
|
||||||
&mut out,
|
&mut out,
|
||||||
&[
|
&[
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,9 @@ impl SimulationInstance {
|
||||||
let mut rng = Rng::new(self.seed);
|
let mut rng = Rng::new(self.seed);
|
||||||
let prepared = run_simulation(self.sim_type, &self.teams, &mut rng);
|
let prepared = run_simulation(self.sim_type, &self.teams, &mut rng);
|
||||||
let total_frames = prepared.frames.len();
|
let total_frames = prepared.frames.len();
|
||||||
self.stats_lines = prepared.stats_lines.clone();
|
self.stats_lines.clear();
|
||||||
self.competition_lines = prepared.competition_lines.clone();
|
self.competition_lines.clear();
|
||||||
self.history_lines = prepared.history_lines.clone();
|
self.history_lines.clear();
|
||||||
self.status = SimStatus::Running {
|
self.status = SimStatus::Running {
|
||||||
frame_index: 0,
|
frame_index: 0,
|
||||||
total_frames,
|
total_frames,
|
||||||
|
|
@ -101,6 +101,15 @@ impl SimulationInstance {
|
||||||
for line in frame.logs {
|
for line in frame.logs {
|
||||||
self.push_log(line);
|
self.push_log(line);
|
||||||
}
|
}
|
||||||
|
if let Some(stats) = frame.stats_lines {
|
||||||
|
self.stats_lines = stats;
|
||||||
|
}
|
||||||
|
if let Some(comp) = frame.competition_lines {
|
||||||
|
self.competition_lines = comp;
|
||||||
|
}
|
||||||
|
for item in frame.history_append {
|
||||||
|
self.history_lines.push(item);
|
||||||
|
}
|
||||||
frame_index += 1;
|
frame_index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ fn quick_mode(home: Option<String>, away: Option<String>, base_seed: u64) -> io:
|
||||||
if let sim::SimOutcome::Single(m) = prepared.outcome {
|
if let sim::SimOutcome::Single(m) = prepared.outcome {
|
||||||
println!("{} {}-{} {}", m.home, m.home_goals, m.away_goals, m.away);
|
println!("{} {}-{} {}", m.home, m.home_goals, m.away_goals, m.away);
|
||||||
println!("xG {:.2} - {:.2}", m.stats.home.xg, m.stats.away.xg);
|
println!("xG {:.2} - {:.2}", m.stats.home.xg, m.stats.away.xg);
|
||||||
|
println!("Possession {}% - {}%", m.home_possession, m.away_possession);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("-- log --");
|
println!("-- log --");
|
||||||
|
|
|
||||||
293
src/sim.rs
293
src/sim.rs
|
|
@ -49,6 +49,8 @@ pub struct MatchResult {
|
||||||
pub home_profile: TeamProfile,
|
pub home_profile: TeamProfile,
|
||||||
pub away_profile: TeamProfile,
|
pub away_profile: TeamProfile,
|
||||||
pub stats: MatchStats,
|
pub stats: MatchStats,
|
||||||
|
pub home_possession: u8,
|
||||||
|
pub away_possession: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -68,6 +70,9 @@ pub struct StandingsRow {
|
||||||
pub struct SimFrame {
|
pub struct SimFrame {
|
||||||
pub scoreboard: String,
|
pub scoreboard: String,
|
||||||
pub logs: Vec<String>,
|
pub logs: Vec<String>,
|
||||||
|
pub stats_lines: Option<Vec<String>>,
|
||||||
|
pub competition_lines: Option<Vec<String>>,
|
||||||
|
pub history_append: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -86,8 +91,6 @@ pub enum SimOutcome {
|
||||||
pub struct PreparedSimulation {
|
pub struct PreparedSimulation {
|
||||||
pub frames: Vec<SimFrame>,
|
pub frames: Vec<SimFrame>,
|
||||||
pub outcome: SimOutcome,
|
pub outcome: SimOutcome,
|
||||||
pub stats_lines: Vec<String>,
|
|
||||||
pub competition_lines: Vec<String>,
|
|
||||||
pub history_lines: Vec<String>,
|
pub history_lines: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +98,38 @@ fn chance(rng: &mut Rng, p: f64) -> bool {
|
||||||
rng.chance(p)
|
rng.chance(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn possession_pct(result: &MatchResult) -> (u8, u8) {
|
||||||
|
let home_poss_base = (result.stats.home.attacks as f64)
|
||||||
|
* if result.home_profile.tactic == "possession" {
|
||||||
|
1.15
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let away_poss_base = (result.stats.away.attacks as f64)
|
||||||
|
* if result.away_profile.tactic == "possession" {
|
||||||
|
1.15
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let home_poss = if (home_poss_base + away_poss_base) > 0.0 {
|
||||||
|
((home_poss_base / (home_poss_base + away_poss_base)) * 100.0).round() as u8
|
||||||
|
} else {
|
||||||
|
50
|
||||||
|
};
|
||||||
|
let away_poss = 100 - home_poss;
|
||||||
|
(home_poss, away_poss)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_frame(scoreboard: String, logs: Vec<String>) -> SimFrame {
|
||||||
|
SimFrame {
|
||||||
|
scoreboard,
|
||||||
|
logs,
|
||||||
|
stats_lines: None,
|
||||||
|
competition_lines: None,
|
||||||
|
history_append: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn penalties(rng: &mut Rng) -> (u8, u8, bool) {
|
pub fn penalties(rng: &mut Rng) -> (u8, u8, bool) {
|
||||||
let mut h = 0u8;
|
let mut h = 0u8;
|
||||||
let mut a = 0u8;
|
let mut a = 0u8;
|
||||||
|
|
@ -145,8 +180,8 @@ pub fn simulate_match(home: &str, away: &str, rng: &mut Rng) -> (MatchResult, Ve
|
||||||
away_tactic.label
|
away_tactic.label
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut frames = vec![SimFrame {
|
let mut frames = vec![empty_frame(
|
||||||
scoreboard: format!(
|
format!(
|
||||||
"{} ({}) {} - {} {} ({}) | {}'",
|
"{} ({}) {} - {} {} ({}) | {}'",
|
||||||
display_name(home),
|
display_name(home),
|
||||||
home_profile.formation,
|
home_profile.formation,
|
||||||
|
|
@ -156,8 +191,8 @@ pub fn simulate_match(home: &str, away: &str, rng: &mut Rng) -> (MatchResult, Ve
|
||||||
away_profile.formation,
|
away_profile.formation,
|
||||||
pad2(minute)
|
pad2(minute)
|
||||||
),
|
),
|
||||||
logs: vec![kickoff],
|
vec![kickoff],
|
||||||
}];
|
)];
|
||||||
|
|
||||||
while minute < 90 {
|
while minute < 90 {
|
||||||
minute += 1;
|
minute += 1;
|
||||||
|
|
@ -296,8 +331,8 @@ pub fn simulate_match(home: &str, away: &str, rng: &mut Rng) -> (MatchResult, Ve
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
frames.push(SimFrame {
|
frames.push(empty_frame(
|
||||||
scoreboard: format!(
|
format!(
|
||||||
"{} ({}) {} - {} {} ({}) | {}'",
|
"{} ({}) {} - {} {} ({}) | {}'",
|
||||||
display_name(home),
|
display_name(home),
|
||||||
home_profile.formation,
|
home_profile.formation,
|
||||||
|
|
@ -308,9 +343,22 @@ pub fn simulate_match(home: &str, away: &str, rng: &mut Rng) -> (MatchResult, Ve
|
||||||
pad2(minute)
|
pad2(minute)
|
||||||
),
|
),
|
||||||
logs,
|
logs,
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let preview_result = MatchResult {
|
||||||
|
home: home.to_string(),
|
||||||
|
away: away.to_string(),
|
||||||
|
home_goals,
|
||||||
|
away_goals,
|
||||||
|
home_profile,
|
||||||
|
away_profile,
|
||||||
|
stats: stats.clone(),
|
||||||
|
home_possession: 50,
|
||||||
|
away_possession: 50,
|
||||||
|
};
|
||||||
|
let (home_poss, away_poss) = possession_pct(&preview_result);
|
||||||
|
|
||||||
(
|
(
|
||||||
MatchResult {
|
MatchResult {
|
||||||
home: home.to_string(),
|
home: home.to_string(),
|
||||||
|
|
@ -320,6 +368,8 @@ pub fn simulate_match(home: &str, away: &str, rng: &mut Rng) -> (MatchResult, Ve
|
||||||
home_profile,
|
home_profile,
|
||||||
away_profile,
|
away_profile,
|
||||||
stats,
|
stats,
|
||||||
|
home_possession: home_poss,
|
||||||
|
away_possession: away_poss,
|
||||||
},
|
},
|
||||||
frames,
|
frames,
|
||||||
)
|
)
|
||||||
|
|
@ -328,24 +378,6 @@ pub fn simulate_match(home: &str, away: &str, rng: &mut Rng) -> (MatchResult, Ve
|
||||||
pub fn match_stats_lines(result: &MatchResult) -> Vec<String> {
|
pub fn match_stats_lines(result: &MatchResult) -> Vec<String> {
|
||||||
let home_tactic = tactic_by_key(result.home_profile.tactic);
|
let home_tactic = tactic_by_key(result.home_profile.tactic);
|
||||||
let away_tactic = tactic_by_key(result.away_profile.tactic);
|
let away_tactic = tactic_by_key(result.away_profile.tactic);
|
||||||
let home_poss_base = (result.stats.home.attacks as f64)
|
|
||||||
* if result.home_profile.tactic == "possession" {
|
|
||||||
1.15
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let away_poss_base = (result.stats.away.attacks as f64)
|
|
||||||
* if result.away_profile.tactic == "possession" {
|
|
||||||
1.15
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let home_poss = if (home_poss_base + away_poss_base) > 0.0 {
|
|
||||||
((home_poss_base / (home_poss_base + away_poss_base)) * 100.0).round() as u8
|
|
||||||
} else {
|
|
||||||
50
|
|
||||||
};
|
|
||||||
let away_poss = 100 - home_poss;
|
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
format!(
|
format!(
|
||||||
|
|
@ -385,7 +417,10 @@ pub fn match_stats_lines(result: &MatchResult) -> Vec<String> {
|
||||||
"Saves: {} vs {}",
|
"Saves: {} vs {}",
|
||||||
result.stats.home.saves, result.stats.away.saves
|
result.stats.home.saves, result.stats.away.saves
|
||||||
),
|
),
|
||||||
format!("Possession: {}% vs {}%", home_poss, away_poss),
|
format!(
|
||||||
|
"Possession: {}% vs {}%",
|
||||||
|
result.home_possession, result.away_possession
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,6 +432,36 @@ fn standings_cmp(a: &StandingsRow, b: &StandingsRow) -> Ordering {
|
||||||
.then(a.team.cmp(&b.team))
|
.then(a.team.cmp(&b.team))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn league_table_lines(rows: &[StandingsRow]) -> Vec<String> {
|
||||||
|
let mut out = Vec::with_capacity(rows.len() + 2);
|
||||||
|
out.push("TEAM P W D L GF GA GD PTS".to_string());
|
||||||
|
out.push("--------------------------------------------------------".to_string());
|
||||||
|
for r in rows {
|
||||||
|
out.push(format!(
|
||||||
|
"{:<28} {:>2} {:>2} {:>2} {:>2} {:>3} {:>2} {:>3} {:>3}",
|
||||||
|
r.team, r.p, r.w, r.d, r.l, r.gf, r.ga, r.gd, r.pts
|
||||||
|
));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn knockout_bracket_lines(
|
||||||
|
semi1: Option<&str>,
|
||||||
|
semi2: Option<&str>,
|
||||||
|
final_line: Option<&str>,
|
||||||
|
champion: Option<&str>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"Knockout Bracket".to_string(),
|
||||||
|
format!("Semi 1: {}", semi1.unwrap_or("TBD")),
|
||||||
|
" \\".to_string(),
|
||||||
|
format!(" +-- Final: {}", final_line.unwrap_or("TBD")),
|
||||||
|
" /".to_string(),
|
||||||
|
format!("Semi 2: {}", semi2.unwrap_or("TBD")),
|
||||||
|
format!("Champion: {}", champion.unwrap_or("TBD")),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn init_table(teams: &[String]) -> BTreeMap<String, StandingsRow> {
|
fn init_table(teams: &[String]) -> BTreeMap<String, StandingsRow> {
|
||||||
let mut map = BTreeMap::new();
|
let mut map = BTreeMap::new();
|
||||||
for team in teams {
|
for team in teams {
|
||||||
|
|
@ -423,11 +488,23 @@ pub fn run_single(teams: &[String], rng: &mut Rng) -> PreparedSimulation {
|
||||||
let away = teams[1].clone();
|
let away = teams[1].clone();
|
||||||
let (result, frames) = simulate_match(&home, &away, rng);
|
let (result, frames) = simulate_match(&home, &away, rng);
|
||||||
let stats_lines = match_stats_lines(&result);
|
let stats_lines = match_stats_lines(&result);
|
||||||
|
let mut frames = frames;
|
||||||
|
frames.push(SimFrame {
|
||||||
|
scoreboard: format!(
|
||||||
|
"{} {}-{} {} | FT",
|
||||||
|
display_name(&result.home),
|
||||||
|
result.home_goals,
|
||||||
|
result.away_goals,
|
||||||
|
display_name(&result.away)
|
||||||
|
),
|
||||||
|
logs: Vec::new(),
|
||||||
|
stats_lines: Some(stats_lines.clone()),
|
||||||
|
competition_lines: None,
|
||||||
|
history_append: Vec::new(),
|
||||||
|
});
|
||||||
PreparedSimulation {
|
PreparedSimulation {
|
||||||
frames,
|
frames,
|
||||||
outcome: SimOutcome::Single(result),
|
outcome: SimOutcome::Single(result),
|
||||||
stats_lines,
|
|
||||||
competition_lines: vec![],
|
|
||||||
history_lines: vec![],
|
history_lines: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -447,17 +524,27 @@ pub fn run_league4(teams: &[String], rng: &mut Rng) -> PreparedSimulation {
|
||||||
let mut history = Vec::new();
|
let mut history = Vec::new();
|
||||||
let mut last_stats = Vec::new();
|
let mut last_stats = Vec::new();
|
||||||
|
|
||||||
for (idx, (home, away)) in fixtures.iter().enumerate() {
|
let mut initial_table: Vec<StandingsRow> = table.values().cloned().collect();
|
||||||
|
initial_table.sort_by(standings_cmp);
|
||||||
frames.push(SimFrame {
|
frames.push(SimFrame {
|
||||||
scoreboard: format!("Running League Match {}/{}", idx + 1, fixtures.len()),
|
scoreboard: "League created - waiting for Matchday 1".to_string(),
|
||||||
logs: vec![format!(
|
logs: vec!["League table initialized".to_string()],
|
||||||
|
stats_lines: None,
|
||||||
|
competition_lines: Some(league_table_lines(&initial_table)),
|
||||||
|
history_append: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (idx, (home, away)) in fixtures.iter().enumerate() {
|
||||||
|
frames.push(empty_frame(
|
||||||
|
format!("Running League Match {}/{}", idx + 1, fixtures.len()),
|
||||||
|
vec![format!(
|
||||||
"League fixture {}/{}: {} vs {}",
|
"League fixture {}/{}: {} vs {}",
|
||||||
idx + 1,
|
idx + 1,
|
||||||
fixtures.len(),
|
fixtures.len(),
|
||||||
display_name(home),
|
display_name(home),
|
||||||
display_name(away)
|
display_name(away)
|
||||||
)],
|
)],
|
||||||
});
|
));
|
||||||
|
|
||||||
let (res, mut match_frames) = simulate_match(home, away, rng);
|
let (res, mut match_frames) = simulate_match(home, away, rng);
|
||||||
frames.append(&mut match_frames);
|
frames.append(&mut match_frames);
|
||||||
|
|
@ -504,6 +591,16 @@ pub fn run_league4(teams: &[String], rng: &mut Rng) -> PreparedSimulation {
|
||||||
res.away_goals,
|
res.away_goals,
|
||||||
display_name(away)
|
display_name(away)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let mut snapshot: Vec<StandingsRow> = table.values().cloned().collect();
|
||||||
|
snapshot.sort_by(standings_cmp);
|
||||||
|
frames.push(SimFrame {
|
||||||
|
scoreboard: format!("League table updated after Match {}", idx + 1),
|
||||||
|
logs: vec!["Standings updated".to_string()],
|
||||||
|
stats_lines: Some(last_stats.clone()),
|
||||||
|
competition_lines: Some(league_table_lines(&snapshot)),
|
||||||
|
history_append: vec![history.last().cloned().unwrap_or_default()],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut final_table: Vec<StandingsRow> = table.into_values().collect();
|
let mut final_table: Vec<StandingsRow> = table.into_values().collect();
|
||||||
|
|
@ -515,23 +612,15 @@ pub fn run_league4(teams: &[String], rng: &mut Rng) -> PreparedSimulation {
|
||||||
final_table[0].pts
|
final_table[0].pts
|
||||||
));
|
));
|
||||||
|
|
||||||
let competition = final_table
|
let competition = league_table_lines(&final_table);
|
||||||
.iter()
|
|
||||||
.map(|r| {
|
frames.push(SimFrame {
|
||||||
format!(
|
scoreboard: format!("League complete - Champion {}", display_name(&champion)),
|
||||||
"{} | P:{} W:{} D:{} L:{} GF:{} GA:{} GD:{} Pts:{}",
|
logs: vec!["League finished".to_string()],
|
||||||
display_name(&r.team),
|
stats_lines: Some(last_stats.clone()),
|
||||||
r.p,
|
competition_lines: Some(competition.clone()),
|
||||||
r.w,
|
history_append: vec![history.last().cloned().unwrap_or_default()],
|
||||||
r.d,
|
});
|
||||||
r.l,
|
|
||||||
r.gf,
|
|
||||||
r.ga,
|
|
||||||
r.gd,
|
|
||||||
r.pts
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
PreparedSimulation {
|
PreparedSimulation {
|
||||||
frames,
|
frames,
|
||||||
|
|
@ -539,8 +628,6 @@ pub fn run_league4(teams: &[String], rng: &mut Rng) -> PreparedSimulation {
|
||||||
champion,
|
champion,
|
||||||
final_table,
|
final_table,
|
||||||
},
|
},
|
||||||
stats_lines: last_stats,
|
|
||||||
competition_lines: competition,
|
|
||||||
history_lines: history,
|
history_lines: history,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -553,24 +640,36 @@ pub fn run_knockout4(teams: &[String], rng: &mut Rng) -> PreparedSimulation {
|
||||||
let mut winners = Vec::new();
|
let mut winners = Vec::new();
|
||||||
let mut history = Vec::new();
|
let mut history = Vec::new();
|
||||||
let mut frames = Vec::new();
|
let mut frames = Vec::new();
|
||||||
|
let mut semi1_line: Option<String> = None;
|
||||||
|
let mut semi2_line: Option<String> = None;
|
||||||
|
|
||||||
|
frames.push(SimFrame {
|
||||||
|
scoreboard: "Knockout bracket initialized".to_string(),
|
||||||
|
logs: vec!["Semi-finals ready".to_string()],
|
||||||
|
stats_lines: None,
|
||||||
|
competition_lines: Some(knockout_bracket_lines(None, None, None, None)),
|
||||||
|
history_append: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
for (idx, (home, away)) in semis.iter().enumerate() {
|
for (idx, (home, away)) in semis.iter().enumerate() {
|
||||||
frames.push(SimFrame {
|
frames.push(empty_frame(
|
||||||
scoreboard: format!("Running Semi-final {}/2", idx + 1),
|
format!("Running Semi-final {}/2", idx + 1),
|
||||||
logs: vec![format!(
|
vec![format!(
|
||||||
"Semi {}: {} vs {}",
|
"Semi {}: {} vs {}",
|
||||||
idx + 1,
|
idx + 1,
|
||||||
display_name(home),
|
display_name(home),
|
||||||
display_name(away)
|
display_name(away)
|
||||||
)],
|
)],
|
||||||
});
|
));
|
||||||
|
|
||||||
let (res, mut semi_frames) = simulate_match(home, away, rng);
|
let (res, mut semi_frames) = simulate_match(home, away, rng);
|
||||||
frames.append(&mut semi_frames);
|
frames.append(&mut semi_frames);
|
||||||
|
|
||||||
|
let line_text;
|
||||||
|
|
||||||
let winner = if res.home_goals == res.away_goals {
|
let winner = if res.home_goals == res.away_goals {
|
||||||
let (ph, pa, home_wins) = penalties(rng);
|
let (ph, pa, home_wins) = penalties(rng);
|
||||||
history.push(format!(
|
line_text = format!(
|
||||||
"Semi {}: {} {}-{} {} (pens {}-{})",
|
"Semi {}: {} {}-{} {} (pens {}-{})",
|
||||||
idx + 1,
|
idx + 1,
|
||||||
display_name(home),
|
display_name(home),
|
||||||
|
|
@ -579,52 +678,75 @@ pub fn run_knockout4(teams: &[String], rng: &mut Rng) -> PreparedSimulation {
|
||||||
display_name(away),
|
display_name(away),
|
||||||
ph,
|
ph,
|
||||||
pa
|
pa
|
||||||
));
|
);
|
||||||
|
history.push(line_text.clone());
|
||||||
if home_wins {
|
if home_wins {
|
||||||
home.clone()
|
home.clone()
|
||||||
} else {
|
} else {
|
||||||
away.clone()
|
away.clone()
|
||||||
}
|
}
|
||||||
} else if res.home_goals > res.away_goals {
|
} else if res.home_goals > res.away_goals {
|
||||||
history.push(format!(
|
line_text = format!(
|
||||||
"Semi {}: {} {}-{} {}",
|
"Semi {}: {} {}-{} {}",
|
||||||
idx + 1,
|
idx + 1,
|
||||||
display_name(home),
|
display_name(home),
|
||||||
res.home_goals,
|
res.home_goals,
|
||||||
res.away_goals,
|
res.away_goals,
|
||||||
display_name(away)
|
display_name(away)
|
||||||
));
|
);
|
||||||
|
history.push(line_text.clone());
|
||||||
home.clone()
|
home.clone()
|
||||||
} else {
|
} else {
|
||||||
history.push(format!(
|
line_text = format!(
|
||||||
"Semi {}: {} {}-{} {}",
|
"Semi {}: {} {}-{} {}",
|
||||||
idx + 1,
|
idx + 1,
|
||||||
display_name(home),
|
display_name(home),
|
||||||
res.home_goals,
|
res.home_goals,
|
||||||
res.away_goals,
|
res.away_goals,
|
||||||
display_name(away)
|
display_name(away)
|
||||||
));
|
);
|
||||||
|
history.push(line_text.clone());
|
||||||
away.clone()
|
away.clone()
|
||||||
};
|
};
|
||||||
winners.push(winner);
|
|
||||||
|
if idx == 0 {
|
||||||
|
semi1_line = Some(line_text.clone());
|
||||||
|
} else {
|
||||||
|
semi2_line = Some(line_text.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
frames.push(SimFrame {
|
frames.push(SimFrame {
|
||||||
scoreboard: "Running Final".to_string(),
|
scoreboard: format!("Semi-final {} complete", idx + 1),
|
||||||
logs: vec![format!(
|
logs: vec!["Bracket updated".to_string()],
|
||||||
|
stats_lines: Some(match_stats_lines(&res)),
|
||||||
|
competition_lines: Some(knockout_bracket_lines(
|
||||||
|
semi1_line.as_deref(),
|
||||||
|
semi2_line.as_deref(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)),
|
||||||
|
history_append: vec![line_text],
|
||||||
|
});
|
||||||
|
winners.push(winner);
|
||||||
|
}
|
||||||
|
|
||||||
|
frames.push(empty_frame(
|
||||||
|
"Running Final".to_string(),
|
||||||
|
vec![format!(
|
||||||
"Final: {} vs {}",
|
"Final: {} vs {}",
|
||||||
display_name(&winners[0]),
|
display_name(&winners[0]),
|
||||||
display_name(&winners[1])
|
display_name(&winners[1])
|
||||||
)],
|
)],
|
||||||
});
|
));
|
||||||
|
|
||||||
let (final_res, mut final_frames) = simulate_match(&winners[0], &winners[1], rng);
|
let (final_res, mut final_frames) = simulate_match(&winners[0], &winners[1], rng);
|
||||||
frames.append(&mut final_frames);
|
frames.append(&mut final_frames);
|
||||||
let last_stats = match_stats_lines(&final_res);
|
let last_stats = match_stats_lines(&final_res);
|
||||||
|
|
||||||
|
let final_line;
|
||||||
let champion = if final_res.home_goals == final_res.away_goals {
|
let champion = if final_res.home_goals == final_res.away_goals {
|
||||||
let (ph, pa, home_wins) = penalties(rng);
|
let (ph, pa, home_wins) = penalties(rng);
|
||||||
history.push(format!(
|
final_line = format!(
|
||||||
"Final: {} {}-{} {} (pens {}-{})",
|
"Final: {} {}-{} {} (pens {}-{})",
|
||||||
display_name(&winners[0]),
|
display_name(&winners[0]),
|
||||||
final_res.home_goals,
|
final_res.home_goals,
|
||||||
|
|
@ -632,45 +754,56 @@ pub fn run_knockout4(teams: &[String], rng: &mut Rng) -> PreparedSimulation {
|
||||||
display_name(&winners[1]),
|
display_name(&winners[1]),
|
||||||
ph,
|
ph,
|
||||||
pa
|
pa
|
||||||
));
|
);
|
||||||
|
history.push(final_line.clone());
|
||||||
if home_wins {
|
if home_wins {
|
||||||
winners[0].clone()
|
winners[0].clone()
|
||||||
} else {
|
} else {
|
||||||
winners[1].clone()
|
winners[1].clone()
|
||||||
}
|
}
|
||||||
} else if final_res.home_goals > final_res.away_goals {
|
} else if final_res.home_goals > final_res.away_goals {
|
||||||
history.push(format!(
|
final_line = format!(
|
||||||
"Final: {} {}-{} {}",
|
"Final: {} {}-{} {}",
|
||||||
display_name(&winners[0]),
|
display_name(&winners[0]),
|
||||||
final_res.home_goals,
|
final_res.home_goals,
|
||||||
final_res.away_goals,
|
final_res.away_goals,
|
||||||
display_name(&winners[1])
|
display_name(&winners[1])
|
||||||
));
|
);
|
||||||
|
history.push(final_line.clone());
|
||||||
winners[0].clone()
|
winners[0].clone()
|
||||||
} else {
|
} else {
|
||||||
history.push(format!(
|
final_line = format!(
|
||||||
"Final: {} {}-{} {}",
|
"Final: {} {}-{} {}",
|
||||||
display_name(&winners[0]),
|
display_name(&winners[0]),
|
||||||
final_res.home_goals,
|
final_res.home_goals,
|
||||||
final_res.away_goals,
|
final_res.away_goals,
|
||||||
display_name(&winners[1])
|
display_name(&winners[1])
|
||||||
));
|
);
|
||||||
|
history.push(final_line.clone());
|
||||||
winners[1].clone()
|
winners[1].clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
history.push(format!("Champion: {} 🏆", display_name(&champion)));
|
let champion_line = format!("Champion: {} 🏆", display_name(&champion));
|
||||||
|
history.push(champion_line.clone());
|
||||||
|
|
||||||
|
frames.push(SimFrame {
|
||||||
|
scoreboard: format!("Knockout complete - {}", display_name(&champion)),
|
||||||
|
logs: vec!["Final complete".to_string()],
|
||||||
|
stats_lines: Some(last_stats.clone()),
|
||||||
|
competition_lines: Some(knockout_bracket_lines(
|
||||||
|
semi1_line.as_deref(),
|
||||||
|
semi2_line.as_deref(),
|
||||||
|
Some(&final_line),
|
||||||
|
Some(&champion_line),
|
||||||
|
)),
|
||||||
|
history_append: vec![final_line.clone(), champion_line.clone()],
|
||||||
|
});
|
||||||
|
|
||||||
PreparedSimulation {
|
PreparedSimulation {
|
||||||
frames,
|
frames,
|
||||||
outcome: SimOutcome::Knockout {
|
outcome: SimOutcome::Knockout {
|
||||||
champion: champion.clone(),
|
champion: champion.clone(),
|
||||||
},
|
},
|
||||||
stats_lines: last_stats,
|
|
||||||
competition_lines: vec![
|
|
||||||
"Bracket: Semi 1 = Team1 vs Team4".to_string(),
|
|
||||||
"Bracket: Semi 2 = Team2 vs Team3".to_string(),
|
|
||||||
format!("Champion: {}", display_name(&champion)),
|
|
||||||
],
|
|
||||||
history_lines: history,
|
history_lines: history,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ pub fn render(f: &mut Frame<'_>, area: Rect, app: &App) {
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
Constraint::Min(8),
|
Constraint::Min(6),
|
||||||
Constraint::Length(8),
|
Constraint::Length(12),
|
||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue