442 lines
13 KiB
Rust
442 lines
13 KiB
Rust
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<Arc<dyn Player>>,
|
|
status: GameStatus,
|
|
map_0: Option<GameMap>,
|
|
map_1: Option<GameMap>,
|
|
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<Self>;
|
|
|
|
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<E>(pub E);
|
|
|
|
impl<E> Handler<AddPlayer<Arc<E>>> for Game
|
|
where
|
|
E: Player + 'static,
|
|
{
|
|
type Result = ();
|
|
|
|
/// Add a new player to the game
|
|
fn handle(&mut self, msg: AddPlayer<Arc<E>>, _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<SetBoatsLayout> 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<Fire> 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<RequestRematch> 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<RespondRequestRematch> 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<PlayerLeftGame> 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);
|
|
}
|
|
}
|
|
}
|
|
}
|