team04_server/lobby/game/
mod.rs

1pub mod completion;
2pub mod fight;
3pub mod lightsaber_purchase;
4pub mod placement;
5pub mod start;
6pub mod unit_purchase;
7
8use completion::completion;
9use fight::fight;
10use rand::seq::{IndexedRandom, SliceRandom};
11
12use crate::{lobby::state::players::PlayerInMatchup, log, unit::LightsaberType};
13
14use super::state::{LobbyPhase, LockedLobbyState, SharedLobbyState};
15
16pub fn round_has_lightsaber_shop_phase(round: u64) -> bool {
17    round % 2 == 1 && round > 2 && round < 9
18}
19pub fn round_has_unit_shop_phase(round: u64) -> bool {
20    round < 9
21}
22pub fn round_has_placement_phase(round: u64) -> bool {
23    round > 1
24}
25pub fn round_has_fight_phase(round: u64) -> bool {
26    round_has_placement_phase(round)
27}
28pub fn round_has_completion_phase(round: u64) -> bool {
29    round_has_fight_phase(round)
30}
31
32/// Creates an iterator over the phases of this round, where `round = 1`
33/// is the first round of the game.
34pub fn phases_of_round(round: u64) -> impl Iterator<Item = LobbyPhase> {
35    // lightsaber purchase phase
36    [
37        round_has_lightsaber_shop_phase(round).then_some(LobbyPhase::LightsaberShopPhase),
38        round_has_unit_shop_phase(round).then_some(LobbyPhase::UnitShopPhase),
39        round_has_placement_phase(round).then_some(LobbyPhase::PlacementPhase),
40        round_has_fight_phase(round).then_some(LobbyPhase::FightPhase),
41        round_has_completion_phase(round).then_some(LobbyPhase::CompletionPhase),
42    ]
43    .into_iter()
44    .flatten()
45}
46/// Creates an iterator over all future phases in the rounds from now up to `max_round`.
47/// If you set `max_rounds` very large, this iterator will (almost) never end,
48/// so it may be a good idea to `.take(n)` only the first `n` elements.
49///
50/// `current_round` should not be greater than `max_round`.
51pub fn next_phases(
52    current_round: u64,
53    max_round: u64,
54    current_phase: LobbyPhase,
55) -> impl Iterator<Item = LobbyPhase> {
56    // get the phases of the current round, and skip
57    // all phases until (including) the `current_phase`.
58    let mut current = phases_of_round(current_round);
59    while let Some(phase) = current.next() {
60        if phase == current_phase {
61            break;
62        }
63    }
64    current.chain((current_round + 1..=max_round).flat_map(|round| phases_of_round(round)))
65}
66
67pub async fn run(lobby: SharedLobbyState) {
68    let mut pfx = log::pfx();
69    pfx.lobby(lobby.id());
70    // wait for players to connect to this lobby and for the game to start
71    lobby.wait_for_game_start().await;
72
73    log::debug!("Lobby task entered game phase"; &pfx);
74
75    'game_rounds_loop: loop {
76        // go to next (or first) round
77        let round = {
78            let mut lock = lobby.lock().await;
79            lock.round += 1;
80            if lock.round > lock.configs.game_config.max_rounds {
81                break 'game_rounds_loop;
82            }
83            log::debug!("Begin round {}!", lock.round; &pfx);
84
85            // Find matchups
86            generate_matchups(&mut lock, &pfx);
87            log::info!("matchups for this round (round #{}):{}", lock.round, lock.matchups.iter().map(|(p1, p2, ghost)| format!("\n    {p1} vs. {p2}{}", if *ghost { " (ghost)" } else { "" })).collect::<String>(); &pfx);
88
89            lock.round
90        };
91
92        // lightsaber purchase phase
93        if round_has_lightsaber_shop_phase(round) {
94            lightsaber_purchase(&lobby, &pfx).await;
95            maybe_pause(&lobby).await;
96        }
97
98        // unit purchase phase
99        if round_has_unit_shop_phase(round) {
100            unit_purchase(&lobby, &pfx, round).await;
101            maybe_pause(&lobby).await;
102        }
103
104        // placement phase
105        if round_has_placement_phase(round) {
106            placements(&lobby, &pfx).await;
107            maybe_pause(&lobby).await;
108        }
109
110        // fight phase
111        if round_has_fight_phase(round) {
112            fight(&lobby).await;
113            maybe_pause(&lobby).await;
114        }
115
116        // completion phase
117        if round_has_completion_phase(round) {
118            if completion(&lobby).await {
119                break 'game_rounds_loop;
120            }
121            maybe_pause(&lobby).await;
122        }
123    }
124
125    log::debug!("Lobby task ended"; &pfx);
126
127    lobby.remove_lobby_from_server().await;
128}
129
130pub async fn maybe_pause(lobby: &SharedLobbyState) {
131    let mut lock = lobby.lock().await;
132    if lock.pause_requested {
133        let mut watcher: tokio::sync::watch::Receiver<bool> = lock.paused.subscribe();
134        if lock.paused.send(true).is_err() {
135            log::warning!("pause could not start due to internal synchronization error (pause watcher errored)"; log::pfx().lobby(lobby.id()));
136        }
137        lock.broadcast_gamestate().await;
138        drop(lock);
139        if watcher.wait_for(|paused| !*paused).await.is_err() {
140            log::warning!("pause ended early due to internal synchronization error (pause watcher errored)"; log::pfx().lobby(lobby.id()));
141        }
142    }
143}
144
145pub async fn lightsaber_purchase(lobby: &SharedLobbyState, pfx: &log::Prefix) {
146    let mut lock = lobby.lightsaber_purchase().await;
147    let options = *lock.hist_lightsaber_options.last().unwrap();
148    let (base_value_red, base_value_green, base_value_blue) = (
149        lock.configs.game_config.lightsaber_modifier_red.base_value,
150        lock.configs
151            .game_config
152            .lightsaber_modifier_green
153            .base_value,
154        lock.configs.game_config.lightsaber_modifier_blue.base_value,
155    );
156    let max_player_lives = lock.configs.game_config.player_lives;
157    for (player_id, player) in lock.clients.players.iter_alive_mut() {
158        let choice = player.lightsaber_choice.as_ref().unwrap();
159        log::debug!("purchased a {choice:?} lightsaber"; pfx.clone().player(*player_id));
160        match choice {
161            LightsaberType::Red => player.red_lightsabers.push(
162                (base_value_red * (2.0 - player.lives as f64 / max_player_lives as f64))
163                    * options.red,
164            ),
165            LightsaberType::Green => player.green_lightsabers.push(
166                (base_value_green * (2.0 - player.lives as f64 / max_player_lives as f64))
167                    * options.green,
168            ),
169            LightsaberType::Blue => player.blue_lightsabers.push(
170                (base_value_blue * (2.0 - player.lives as f64 / max_player_lives as f64))
171                    * options.blue,
172            ),
173        }
174    }
175}
176
177pub async fn unit_purchase(lobby: &SharedLobbyState, pfx: &log::Prefix, round: u64) {
178    let mut lock = lobby.unit_purchase(round).await;
179    for (player_id, player) in lock.clients.players.iter_alive_mut() {
180        let choice = player.unit_choice.as_ref().unwrap();
181        player.unit_bank.push(*choice);
182        log::debug!("purchased a {choice:?} unit"; pfx.clone().player(*player_id));
183    }
184}
185
186pub async fn placements(lobby: &SharedLobbyState, pfx: &log::Prefix) {
187    let mut lock = lobby.placements().await;
188    let configs = lock.configs.clone();
189    for (player_id, player) in lock.clients.players.iter_alive_mut() {
190        let (new_placed, new_bank) =
191            std::mem::replace(&mut player.new_placement, Err(None)).unwrap();
192        player.unit_placement = new_placed;
193        player.unit_bank = new_bank;
194        if log::logger().level(log::LogLevel::Debug) {
195            let placements_string = player
196                .unit_placement
197                .iter()
198                .map(|placement| {
199                    format!(
200                        "\n    - {:?} on {} ({})",
201                        placement.unit_type,
202                        &configs
203                            .board_config
204                            .board
205                            .get(placement.position)
206                            .map(|v| format!("{v:?}"))
207                            .unwrap_or_else(|| "<OutOfBounds>".to_owned()),
208                        placement.position,
209                    )
210                })
211                .collect::<String>();
212            let bank_string = player
213                .unit_bank
214                .iter()
215                .enumerate()
216                .map(|(i, unit)| format!("{}{:?}", if i == 0 { "" } else { ", " }, unit))
217                .collect::<String>();
218            log::debug!(
219                "Unit placement:{placements_string}\nBank: {bank_string}";
220                &pfx.clone().player(*player_id)
221            );
222        }
223    }
224}
225
226fn generate_matchups(lock: &mut LockedLobbyState, pfx: &log::Prefix) {
227    let mut p1s = lock
228        .clients
229        .players
230        .players_all()
231        .enumerate()
232        .filter(|(_, p)| p.lives > 0)
233        .map(|(i, _)| i)
234        .collect::<Vec<_>>();
235    p1s.shuffle(&mut rand::rng());
236    let mut matchups = Vec::<(usize, usize, bool)>::new();
237    for p1 in p1s {
238        // check if player is in another matchup already
239        if !matchups.iter().flat_map(|v| [v.0, v.1]).any(|p| p == p1) {
240            for ghost in [false, true] {
241                // find min-frequency matchups for this player
242                let mut min = u32::MAX;
243                let mut mins = Vec::new();
244                for p2 in lock
245                    .clients
246                    .players
247                    .players_all()
248                    .enumerate()
249                    .filter(|(_, p)| p.lives > 0)
250                    .map(|(i, _)| i)
251                {
252                    // check that other player is not in any matchup (unless looking for a ghost)
253                    if p1 != p2
254                        && (ghost || !matchups.iter().flat_map(|v| [v.0, v.1]).any(|p| p == p2))
255                    {
256                        let freq = lock.prev_matchups.get_frequency(p1, p2);
257                        if freq < min {
258                            min = freq;
259                            mins.clear();
260                        }
261                        if freq == min {
262                            mins.push(p2);
263                        }
264                    }
265                }
266                if let Some(p2) = mins.choose(&mut rand::rng()) {
267                    matchups.push((p1, *p2, ghost));
268                    break;
269                } else {
270                    // ghost enemy
271                    log::debug!("adding a ghost enemy"; &pfx);
272                    continue;
273                }
274            }
275        }
276    }
277    lock.matchups = matchups
278        .into_iter()
279        .map(|(p1, p2, ghost)| {
280            let size = lock.configs.board_config.board.get_size();
281            let p1 = lock.clients.players.iter_all_mut().nth(p1).unwrap();
282            p1.1.player_in_matchup = PlayerInMatchup::Player1;
283            for placement in &mut p1.1.unit_placement {
284                // correct units placed in Player2's half to the other half.
285                // relevant for GAMESTATEs after a matchup changes player1 to player2 before a placement phase.
286                if placement.position.y >= size.height / 2 {
287                    placement.position.x = size.width - 1 - placement.position.x;
288                    placement.position.y = size.height - 1 - placement.position.y;
289                }
290            }
291            let p1 = *p1.0;
292            let p2 = lock.clients.players.iter_all_mut().nth(p2).unwrap();
293            p2.1.player_in_matchup = PlayerInMatchup::Player2;
294            for placement in &mut p2.1.unit_placement {
295                // correct units placed in Player1's half to the other half.
296                // relevant for GAMESTATEs after a matchup changes player1 to player2 before a placement phase.
297                if placement.position.y < size.height / 2 {
298                    placement.position.x = size.width - 1 - placement.position.x;
299                    placement.position.y = size.height - 1 - placement.position.y;
300                }
301            }
302            let p2 = *p2.0;
303            (p1, p2, ghost)
304        })
305        .collect();
306}
307
308#[cfg(test)]
309mod test {
310    use tokio::spawn;
311
312    use crate::lobby::test::{config_set_modified, get_server_and_lobby_with_config, player_join};
313
314    #[tokio::test]
315    async fn test() {
316        for _ in 0..10 {
317            let (server, lobby) = get_server_and_lobby_with_config(config_set_modified(
318                |game| {
319                    game.timeout_lobby = 0;
320                    game.timeout_lightsaber_shop_phase = 0;
321                    game.timeout_unit_shop_phase = 0;
322                    game.timeout_placement_phase = 0;
323                    game.timeout_fight_phase = 0;
324                },
325                |_| {},
326                |_| {},
327            ));
328            let lobby_task = spawn(super::run(lobby.clone()));
329            let (_p1id, _, _p1con) = player_join(&server, &lobby).await;
330            let (_p2id, _, _p2con) = player_join(&server, &lobby).await;
331            lobby.lock().await.start_game_now().await;
332            lobby_task.await.unwrap();
333        }
334    }
335}