1use crate::event::Event;
9use fforge_domain::{ClubId, Fixture, FixtureId, GameDate, Lineup, World};
10use std::collections::BTreeMap;
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct GameState {
14 pub seed: u64,
15 pub world: World,
16 pub player_club: ClubId,
17 pub schedule: Vec<Fixture>,
18 pub date: GameDate,
19 pub current_matchday: u8,
21 pub last_matchday: u8,
22 pub results: BTreeMap<FixtureId, (u8, u8)>,
23 pub pending_lineup: Option<Lineup>,
25 pub last_lineup: Option<Lineup>,
27 pub champion: Option<ClubId>,
28}
29
30impl GameState {
31 pub fn replay(events: &[Event]) -> GameState {
35 let mut iter = events.iter();
36 let first = iter.next().expect("event log is empty");
37 let mut state = match first {
38 Event::GameStarted {
39 seed,
40 start_date,
41 player_club,
42 world,
43 schedule,
44 } => {
45 let last_matchday = schedule.iter().map(|f| f.matchday).max().unwrap_or(0);
46 GameState {
47 seed: *seed,
48 world: world.clone(),
49 player_club: *player_club,
50 schedule: schedule.clone(),
51 date: *start_date,
52 current_matchday: 1,
53 last_matchday,
54 results: BTreeMap::new(),
55 pending_lineup: None,
56 last_lineup: None,
57 champion: None,
58 }
59 }
60 other => panic!("event log must start with GameStarted, found {other:?}"),
61 };
62 for event in iter {
63 state.apply(event);
64 }
65 state
66 }
67
68 pub fn apply(&mut self, event: &Event) {
70 match event {
71 Event::GameStarted { .. } => {
72 panic!("GameStarted may only appear as the first event")
73 }
74 Event::LineupSubmitted { lineup, .. } => {
75 self.pending_lineup = Some(lineup.clone());
76 }
77 Event::MatchPlayed {
78 fixture,
79 home_goals,
80 away_goals,
81 ..
82 } => {
83 self.results.insert(*fixture, (*home_goals, *away_goals));
84 }
85 Event::MatchdayAdvanced { new_date, .. } => {
86 if let Some(lineup) = self.pending_lineup.take() {
87 self.last_lineup = Some(lineup);
88 }
89 self.date = *new_date;
90 self.current_matchday += 1;
91 }
92 Event::SeasonEnded { champion } => {
93 self.champion = Some(*champion);
94 }
95 }
96 }
97
98 pub fn season_over(&self) -> bool {
99 self.champion.is_some()
100 }
101
102 pub fn fixtures_of_matchday(&self, matchday: u8) -> impl Iterator<Item = &Fixture> {
103 self.schedule.iter().filter(move |f| f.matchday == matchday)
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct TableRow {
112 pub club: ClubId,
113 pub played: u32,
114 pub won: u32,
115 pub drawn: u32,
116 pub lost: u32,
117 pub goals_for: u32,
118 pub goals_against: u32,
119}
120
121impl TableRow {
122 pub fn points(&self) -> u32 {
123 self.won * 3 + self.drawn
124 }
125 pub fn goal_diff(&self) -> i64 {
126 self.goals_for as i64 - self.goals_against as i64
127 }
128}
129
130pub fn league_table(
134 world: &World,
135 schedule: &[Fixture],
136 results: &BTreeMap<FixtureId, (u8, u8)>,
137) -> Vec<TableRow> {
138 let mut rows: BTreeMap<ClubId, TableRow> = world
139 .competition
140 .clubs
141 .iter()
142 .map(|&c| {
143 (
144 c,
145 TableRow {
146 club: c,
147 played: 0,
148 won: 0,
149 drawn: 0,
150 lost: 0,
151 goals_for: 0,
152 goals_against: 0,
153 },
154 )
155 })
156 .collect();
157
158 for fixture in schedule {
159 let Some(&(hg, ag)) = results.get(&fixture.id) else {
160 continue;
161 };
162 {
163 let home = rows.get_mut(&fixture.home).expect("home club in table");
164 home.played += 1;
165 home.goals_for += hg as u32;
166 home.goals_against += ag as u32;
167 match hg.cmp(&ag) {
168 std::cmp::Ordering::Greater => home.won += 1,
169 std::cmp::Ordering::Equal => home.drawn += 1,
170 std::cmp::Ordering::Less => home.lost += 1,
171 }
172 }
173 {
174 let away = rows.get_mut(&fixture.away).expect("away club in table");
175 away.played += 1;
176 away.goals_for += ag as u32;
177 away.goals_against += hg as u32;
178 match ag.cmp(&hg) {
179 std::cmp::Ordering::Greater => away.won += 1,
180 std::cmp::Ordering::Equal => away.drawn += 1,
181 std::cmp::Ordering::Less => away.lost += 1,
182 }
183 }
184 }
185
186 let mut table: Vec<TableRow> = rows.into_values().collect();
187 table.sort_by(|a, b| {
188 b.points()
189 .cmp(&a.points())
190 .then(b.goal_diff().cmp(&a.goal_diff()))
191 .then(b.goals_for.cmp(&a.goals_for))
192 .then(world.club(a.club).name.cmp(&world.club(b.club).name))
193 });
194 table
195}