team04_server/lobby/game/
mod.rs1pub 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
32pub fn phases_of_round(round: u64) -> impl Iterator<Item = LobbyPhase> {
35 [
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}
46pub fn next_phases(
52 current_round: u64,
53 max_round: u64,
54 current_phase: LobbyPhase,
55) -> impl Iterator<Item = LobbyPhase> {
56 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 lobby.wait_for_game_start().await;
72
73 log::debug!("Lobby task entered game phase"; &pfx);
74
75 'game_rounds_loop: loop {
76 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 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 if round_has_lightsaber_shop_phase(round) {
94 lightsaber_purchase(&lobby, &pfx).await;
95 maybe_pause(&lobby).await;
96 }
97
98 if round_has_unit_shop_phase(round) {
100 unit_purchase(&lobby, &pfx, round).await;
101 maybe_pause(&lobby).await;
102 }
103
104 if round_has_placement_phase(round) {
106 placements(&lobby, &pfx).await;
107 maybe_pause(&lobby).await;
108 }
109
110 if round_has_fight_phase(round) {
112 fight(&lobby).await;
113 maybe_pause(&lobby).await;
114 }
115
116 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 if !matchups.iter().flat_map(|v| [v.0, v.1]).any(|p| p == p1) {
240 for ghost in [false, true] {
241 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 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 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 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 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}