1use crate::event::Event;
9use crate::match_engine::{ai_pick_lineup, lineup_strength, simulate_match};
10use crate::rng::derive_stream;
11use crate::state::{league_table, GameState};
12use fforge_domain::{Lineup, PlayerId, FORMATIONS, XI};
13use std::collections::BTreeSet;
14use std::fmt;
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum Command {
18 SubmitLineup(Lineup),
20 AdvanceMatchday,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum CommandError {
26 SeasonOver,
27 UnknownFormation(u8),
28 DuplicatePlayers,
29 NotInSquad(PlayerId),
30}
31
32impl fmt::Display for CommandError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 CommandError::SeasonOver => write!(f, "the season is over"),
36 CommandError::UnknownFormation(i) => write!(f, "unknown formation index {i}"),
37 CommandError::DuplicatePlayers => write!(f, "a player appears twice in the lineup"),
38 CommandError::NotInSquad(p) => write!(f, "player {p} is not in your squad"),
39 }
40 }
41}
42
43const FIXTURE_STREAM_NS: u64 = 0x4D41_5443_0000_0000; pub fn step(state: &GameState, command: Command) -> Result<Vec<Event>, CommandError> {
47 if state.season_over() {
48 return Err(CommandError::SeasonOver);
49 }
50 match command {
51 Command::SubmitLineup(lineup) => {
52 validate_lineup(state, &lineup)?;
53 Ok(vec![Event::LineupSubmitted {
54 matchday: state.current_matchday,
55 lineup,
56 }])
57 }
58 Command::AdvanceMatchday => Ok(advance_matchday(state)),
59 }
60}
61
62fn validate_lineup(state: &GameState, lineup: &Lineup) -> Result<(), CommandError> {
63 if lineup.formation as usize >= FORMATIONS.len() {
64 return Err(CommandError::UnknownFormation(lineup.formation));
65 }
66 let mut seen = BTreeSet::new();
67 for &pid in &lineup.players {
68 if !seen.insert(pid) {
69 return Err(CommandError::DuplicatePlayers);
70 }
71 }
72 let squad = &state.world.club(state.player_club).players;
73 for &pid in &lineup.players {
74 if !squad.contains(&pid) {
75 return Err(CommandError::NotInSquad(pid));
76 }
77 }
78 debug_assert_eq!(lineup.players.len(), XI);
79 Ok(())
80}
81
82fn effective_player_lineup(state: &GameState) -> Lineup {
86 if let Some(lineup) = &state.pending_lineup {
87 return lineup.clone();
88 }
89 if let Some(lineup) = &state.last_lineup {
90 return lineup.clone();
91 }
92 ai_pick_lineup(&state.world, state.player_club)
93}
94
95fn advance_matchday(state: &GameState) -> Vec<Event> {
96 let md = state.current_matchday;
97 let mut events = Vec::new();
98 let mut new_results = state.results.clone();
99
100 for fixture in state.fixtures_of_matchday(md) {
101 let home_lineup = if fixture.home == state.player_club {
102 effective_player_lineup(state)
103 } else {
104 ai_pick_lineup(&state.world, fixture.home)
105 };
106 let away_lineup = if fixture.away == state.player_club {
107 effective_player_lineup(state)
108 } else {
109 ai_pick_lineup(&state.world, fixture.away)
110 };
111 let hs = lineup_strength(&state.world, &home_lineup);
112 let as_ = lineup_strength(&state.world, &away_lineup);
113 let mut rng = derive_stream(state.seed, FIXTURE_STREAM_NS | fixture.id.0 as u64);
114 let (hg, ag) = simulate_match(hs, as_, &mut rng);
115 new_results.insert(fixture.id, (hg, ag));
116 events.push(Event::MatchPlayed {
117 fixture: fixture.id,
118 matchday: md,
119 home_goals: hg,
120 away_goals: ag,
121 });
122 }
123
124 events.push(Event::MatchdayAdvanced {
125 matchday: md,
126 new_date: state.date.add_days(7),
127 });
128
129 if md == state.last_matchday {
130 let table = league_table(&state.world, &state.schedule, &new_results);
131 let champion = table.first().expect("non-empty league").club;
132 events.push(Event::SeasonEnded { champion });
133 }
134
135 events
136}