use std::sync::Arc; use std::time::Duration; use actix::prelude::*; use actix::{Actor, Context, Handler}; use uuid::Uuid; use crate::bot_player::BotPlayer; use crate::data::*; use crate::utils::time_utils::time; pub trait Player { fn get_name(&self) -> &str; fn get_uid(&self) -> Uuid; fn is_bot(&self) -> bool; fn opponent_connected(&self); fn set_other_player_name(&self, name: &str); fn query_boats_layout(&self, rules: &GameRules); fn rejected_boats_layout(&self, errors: Vec<&'static str>); fn waiting_for_opponent_boats_layout(&self); fn notify_other_player_ready(&self); fn notify_game_starting(&self); fn request_fire(&self, status: CurrentGameStatus); fn opponent_must_fire(&self, status: CurrentGameStatus); fn strike_result(&self, c: Coordinates, res: FireResult); fn other_player_strike_result(&self, c: Coordinates, res: FireResult); fn lost_game(&self, status: CurrentGameStatus); fn won_game(&self, status: CurrentGameStatus); fn opponent_requested_rematch(&self); fn opponent_rejected_rematch(&self); fn opponent_accepted_rematch(&self); fn opponent_left_game(&self); fn opponent_replaced_by_bot(&self); } /// How often strike timeout controller is run const STRIKE_TIMEOUT_CONTROL: Duration = Duration::from_secs(1); fn opponent(index: usize) -> usize { match index { 0 => 1, 1 => 0, _ => unreachable!(), } } #[derive(Default, Eq, PartialEq, Debug, Copy, Clone)] enum GameStatus { #[default] Created, WaitingForBoatsDisposition, Started, Finished, RematchRequested, RematchRejected, } impl GameStatus { pub fn can_game_continue_with_bot(&self) -> bool { *self != GameStatus::Finished && *self != GameStatus::RematchRejected && *self != GameStatus::RematchRequested } } pub struct Game { rules: GameRules, players: Vec>, status: GameStatus, map_0: Option, map_1: Option, turn: usize, curr_strike_request_started: u64, } impl Game { pub fn new(rules: GameRules) -> Self { Self { rules, players: vec![], status: GameStatus::Created, map_0: None, map_1: None, turn: 0, curr_strike_request_started: 0, } } /// Find the ID of a player from its UUID fn player_id_by_uuid(&self, uuid: Uuid) -> usize { self.players .iter() .enumerate() .find(|p| p.1.get_uid() == uuid) .expect("Player is not member of this game!") .0 } /// Once the two player has been registered, the game may start fn query_boats_disposition(&mut self) { self.players[0].set_other_player_name(self.players[1].get_name()); self.players[1].set_other_player_name(self.players[0].get_name()); log::debug!("Query boats disposition"); assert_eq!(self.status, GameStatus::Created); self.status = GameStatus::WaitingForBoatsDisposition; self.players[0].query_boats_layout(&self.rules); self.players[1].query_boats_layout(&self.rules); } /// Start fires exchange fn start_fire_exchanges(&mut self) { self.status = GameStatus::Started; log::debug!( "Start fire exchanges. Player {}#{} goes first", self.players[self.turn].get_name(), self.turn ); self.request_fire(true); } fn request_fire(&mut self, reset_counter: bool) { if reset_counter { self.curr_strike_request_started = time(); } self.players[self.turn].request_fire(self.get_game_status_for_player(self.turn)); self.players[opponent(self.turn)] .opponent_must_fire(self.get_game_status_for_player(opponent(self.turn))); } fn player_map(&self, id: usize) -> &GameMap { match id { 0 => self.map_0.as_ref(), 1 => self.map_1.as_ref(), _ => unreachable!(), } .unwrap() } /// Replace user for a fire in case of timeout fn force_fire_in_case_of_timeout(&mut self) { if self.status != GameStatus::Started || self.rules.strike_timeout.is_none() { return; } let timeout = self.rules.strike_timeout.unwrap_or_default(); if time() <= self.curr_strike_request_started + timeout { return; } // Determine target of fire let target = self .get_game_status_for_player(self.turn) .find_fire_coordinates_for_bot_type(self.rules.bot_type); // Fire as player self.handle_fire(target); } fn player_map_mut(&mut self, id: usize) -> &mut GameMap { match id { 0 => self.map_0.as_mut(), 1 => self.map_1.as_mut(), _ => unreachable!(), } .unwrap() } /// Handle fire attempts fn handle_fire(&mut self, c: Coordinates) { let result = self.player_map_mut(opponent(self.turn)).fire(c); self.players[self.turn].strike_result(c, result); self.players[opponent(self.turn)].other_player_strike_result(c, result); // Easiest case : player missed his fire if result == FireResult::Missed { self.turn = opponent(self.turn); self.request_fire(true); return; } if matches!(result, FireResult::Sunk(_)) && self.player_map(opponent(self.turn)).are_all_boat_sunk() { self.status = GameStatus::Finished; self.players[self.turn].won_game(self.get_game_status_for_player(self.turn)); self.players[opponent(self.turn)] .lost_game(self.get_game_status_for_player(opponent(self.turn))); return; } if matches!(result, FireResult::Sunk(_) | FireResult::Hit) && !self.rules.player_continue_on_hit { self.turn = opponent(self.turn); } self.request_fire( result != FireResult::AlreadyTargetedPosition && result != FireResult::Rejected, ); } fn handle_request_rematch(&mut self, player_id: Uuid) { self.status = GameStatus::RematchRequested; self.turn = opponent(self.player_id_by_uuid(player_id)); self.players[self.turn].opponent_requested_rematch(); } fn handle_request_rematch_response(&mut self, accepted: bool) { if !accepted { self.players[opponent(self.turn)].opponent_rejected_rematch(); self.status = GameStatus::RematchRejected; return; } self.players[opponent(self.turn)].opponent_accepted_rematch(); // Swap players let swap = self.players[1].clone(); self.players[1] = self.players[0].clone(); self.players[0] = swap; // "Forget everything" self.status = GameStatus::Created; self.map_0 = None; self.map_1 = None; self.query_boats_disposition(); } /// Get current game status for a specific player fn get_game_status_for_player(&self, id: usize) -> CurrentGameStatus { CurrentGameStatus { remaining_time_for_strike: self.rules.strike_timeout.map(|v| { ((self.curr_strike_request_started + v) as i64 - time() as i64).max(0) as u64 }), rules: self.rules.clone(), your_map: self.player_map(id).current_map_status(false), opponent_map: self .player_map(opponent(id)) .current_map_status(self.status != GameStatus::Finished), } } } impl Actor for Game { type Context = Context; fn started(&mut self, ctx: &mut Self::Context) { if self.rules.strike_timeout.is_some() { ctx.run_interval(STRIKE_TIMEOUT_CONTROL, |act, _ctx| { act.force_fire_in_case_of_timeout(); }); } } } #[derive(Message)] #[rtype(result = "()")] pub struct AddPlayer(pub E); impl Handler>> for Game where E: Player + 'static, { type Result = (); /// Add a new player to the game fn handle(&mut self, msg: AddPlayer>, _ctx: &mut Self::Context) -> Self::Result { assert!(self.players.len() < 2); self.players.push(msg.0); if self.players.len() == 2 { self.players[0].opponent_connected(); self.players[1].opponent_connected(); self.query_boats_disposition(); } } } #[derive(Message)] #[rtype(result = "()")] pub struct SetBoatsLayout(pub Uuid, pub BoatsLayout); impl Handler for Game { type Result = (); /// Receive game configuration of a player fn handle(&mut self, msg: SetBoatsLayout, _ctx: &mut Self::Context) -> Self::Result { if self.status != GameStatus::WaitingForBoatsDisposition { log::error!("Player attempted to set boat configuration on invalid step!"); return; } let player_index = self.player_id_by_uuid(msg.0); let errors = msg.1.errors(&self.rules); if !errors.is_empty() { log::error!("Got invalid boats layout!"); self.players[player_index].rejected_boats_layout(errors); self.players[player_index].query_boats_layout(&self.rules); return; } log::debug!("Got boat disposition for player {}", player_index); match player_index { 0 => self.map_0 = Some(GameMap::new(self.rules.clone(), msg.1)), 1 => self.map_1 = Some(GameMap::new(self.rules.clone(), msg.1)), _ => unreachable!(), } self.players[opponent(player_index)].notify_other_player_ready(); if self.map_0.is_some() && self.map_1.is_some() { self.players.iter().for_each(|p| p.notify_game_starting()); self.start_fire_exchanges(); } else { self.players[player_index].waiting_for_opponent_boats_layout(); } } } #[derive(Message, Debug)] #[rtype(result = "()")] pub struct Fire(pub Uuid, pub Coordinates); impl Handler for Game { type Result = (); fn handle(&mut self, msg: Fire, _ctx: &mut Self::Context) -> Self::Result { if self.status != GameStatus::Started { log::error!("Player attempted to fire on invalid step!"); return; } if msg.0 != self.players[self.turn].get_uid() { log::error!("Player attempted to fire when it was not its turn!"); return; } self.handle_fire(msg.1) } } #[derive(Message, Debug)] #[rtype(result = "()")] pub struct RequestRematch(pub Uuid); impl Handler for Game { type Result = (); fn handle(&mut self, msg: RequestRematch, _ctx: &mut Self::Context) -> Self::Result { if self.status != GameStatus::Finished { log::error!("Player attempted to request rematch on invalid step!"); return; } self.handle_request_rematch(msg.0); } } #[derive(Message, Debug)] #[rtype(result = "()")] pub struct RespondRequestRematch(pub Uuid, pub bool); impl Handler for Game { type Result = (); fn handle(&mut self, msg: RespondRequestRematch, _ctx: &mut Self::Context) -> Self::Result { if self.status != GameStatus::RematchRequested { log::error!("Player attempted to respond to request rematch on invalid step!"); return; } if self.player_id_by_uuid(msg.0) != self.turn { log::error!("Player can not respond to its own rematch request!"); return; } self.handle_request_rematch_response(msg.1); } } #[derive(Message, Debug)] #[rtype(result = "()")] pub struct PlayerLeftGame(pub Uuid); impl Handler for Game { type Result = (); fn handle(&mut self, msg: PlayerLeftGame, ctx: &mut Self::Context) -> Self::Result { let offline_player = self.player_id_by_uuid(msg.0); self.players[opponent(offline_player)].opponent_left_game(); // If the other player is a bot or if the game is not running, stop the game if !self.status.can_game_continue_with_bot() || self.players[opponent(offline_player)].is_bot() { ctx.stop(); } else { // Replace the player with a bot self.players[offline_player] = Arc::new(BotPlayer::new(self.rules.bot_type, ctx.address())); self.players[opponent(offline_player)].opponent_replaced_by_bot(); // Re-do current action if self.status == GameStatus::Started { self.request_fire(true); } else if self.status == GameStatus::WaitingForBoatsDisposition { self.players[offline_player].query_boats_layout(&self.rules); } } } }