use crate::event::Event;
use fforge_domain::{ClubId, Fixture, FixtureId, GameDate, Lineup, World};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub struct GameState {
pub seed: u64,
pub world: World,
pub player_club: ClubId,
pub schedule: Vec<Fixture>,
pub date: GameDate,
pub current_matchday: u8,
pub last_matchday: u8,
pub results: BTreeMap<FixtureId, (u8, u8)>,
pub pending_lineup: Option<Lineup>,
pub last_lineup: Option<Lineup>,
pub champion: Option<ClubId>,
}
impl GameState {
pub fn replay(events: &[Event]) -> GameState {
let mut iter = events.iter();
let first = iter.next().expect("event log is empty");
let mut state = match first {
Event::GameStarted {
seed,
start_date,
player_club,
world,
schedule,
} => {
let last_matchday = schedule.iter().map(|f| f.matchday).max().unwrap_or(0);
GameState {
seed: *seed,
world: world.clone(),
player_club: *player_club,
schedule: schedule.clone(),
date: *start_date,
current_matchday: 1,
last_matchday,
results: BTreeMap::new(),
pending_lineup: None,
last_lineup: None,
champion: None,
}
}
other => panic!("event log must start with GameStarted, found {other:?}"),
};
for event in iter {
state.apply(event);
}
state
}
pub fn apply(&mut self, event: &Event) {
match event {
Event::GameStarted { .. } => {
panic!("GameStarted may only appear as the first event")
}
Event::LineupSubmitted { lineup, .. } => {
self.pending_lineup = Some(lineup.clone());
}
Event::MatchPlayed {
fixture,
home_goals,
away_goals,
..
} => {
self.results.insert(*fixture, (*home_goals, *away_goals));
}
Event::MatchdayAdvanced { new_date, .. } => {
if let Some(lineup) = self.pending_lineup.take() {
self.last_lineup = Some(lineup);
}
self.date = *new_date;
self.current_matchday += 1;
}
Event::SeasonEnded { champion } => {
self.champion = Some(*champion);
}
}
}
pub fn season_over(&self) -> bool {
self.champion.is_some()
}
pub fn fixtures_of_matchday(&self, matchday: u8) -> impl Iterator<Item = &Fixture> {
self.schedule.iter().filter(move |f| f.matchday == matchday)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableRow {
pub club: ClubId,
pub played: u32,
pub won: u32,
pub drawn: u32,
pub lost: u32,
pub goals_for: u32,
pub goals_against: u32,
}
impl TableRow {
pub fn points(&self) -> u32 {
self.won * 3 + self.drawn
}
pub fn goal_diff(&self) -> i64 {
self.goals_for as i64 - self.goals_against as i64
}
}
pub fn league_table(
world: &World,
schedule: &[Fixture],
results: &BTreeMap<FixtureId, (u8, u8)>,
) -> Vec<TableRow> {
let mut rows: BTreeMap<ClubId, TableRow> = world
.competition
.clubs
.iter()
.map(|&c| {
(
c,
TableRow {
club: c,
played: 0,
won: 0,
drawn: 0,
lost: 0,
goals_for: 0,
goals_against: 0,
},
)
})
.collect();
for fixture in schedule {
let Some(&(hg, ag)) = results.get(&fixture.id) else {
continue;
};
{
let home = rows.get_mut(&fixture.home).expect("home club in table");
home.played += 1;
home.goals_for += hg as u32;
home.goals_against += ag as u32;
match hg.cmp(&ag) {
std::cmp::Ordering::Greater => home.won += 1,
std::cmp::Ordering::Equal => home.drawn += 1,
std::cmp::Ordering::Less => home.lost += 1,
}
}
{
let away = rows.get_mut(&fixture.away).expect("away club in table");
away.played += 1;
away.goals_for += ag as u32;
away.goals_against += hg as u32;
match ag.cmp(&hg) {
std::cmp::Ordering::Greater => away.won += 1,
std::cmp::Ordering::Equal => away.drawn += 1,
std::cmp::Ordering::Less => away.lost += 1,
}
}
}
let mut table: Vec<TableRow> = rows.into_values().collect();
table.sort_by(|a, b| {
b.points()
.cmp(&a.points())
.then(b.goal_diff().cmp(&a.goal_diff()))
.then(b.goals_for.cmp(&a.goals_for))
.then(world.club(a.club).name.cmp(&world.club(b.club).name))
});
table
}