SeaBattle/sea_battle_backend/src/game.rs

263 lines
7.3 KiB
Rust

use std::sync::Arc;
use actix::prelude::*;
use actix::{Actor, Context, Handler};
use uuid::Uuid;
use crate::data::*;
pub trait Player {
fn get_name(&self) -> &str;
fn get_uid(&self) -> Uuid;
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 notify_other_player_ready(&self);
fn notify_game_starting(&self);
fn request_fire(&self, status: CurrentGameStatus);
fn other_player_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, your_map: EndGameMap, opponent_map: EndGameMap);
fn won_game(&self, your_map: EndGameMap, opponent_map: EndGameMap);
}
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,
}
pub struct Game {
rules: GameRules,
players: Vec<Arc<dyn Player>>,
status: GameStatus,
map_0: Option<GameMap>,
map_1: Option<GameMap>,
turn: usize,
}
impl Game {
pub fn new(rules: GameRules) -> Self {
Self {
rules,
players: vec![],
status: GameStatus::Created,
map_0: None,
map_1: None,
turn: 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();
}
fn request_fire(&self) {
self.players[self.turn].request_fire(self.get_game_status_for_player(self.turn));
self.players[opponent(self.turn)]
.other_player_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()
}
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)].strike_result(c, result);
// Easiest case : player missed his fire
if result == FireResult::Missed {
self.turn = opponent(self.turn);
self.request_fire();
return;
}
if result == FireResult::Sunk && self.player_map(opponent(self.turn)).are_all_boat_sunk() {
self.status = GameStatus::Finished;
let winner_map = self.player_map(self.turn).final_map();
let looser_map = self.player_map(opponent(self.turn)).final_map();
self.players[self.turn].won_game(winner_map.clone(), looser_map.clone());
self.players[opponent(self.turn)].lost_game(looser_map, winner_map);
return;
}
if (result == FireResult::Sunk || result == FireResult::Hit)
&& !self.rules.player_continue_on_hit
{
self.turn = opponent(self.turn);
}
self.request_fire();
}
/// Get current game status for a specific player
fn get_game_status_for_player(&self, id: usize) -> CurrentGameStatus {
CurrentGameStatus {
rules: self.rules.clone(),
your_map: self.player_map(id).current_map_status(false),
opponent_map: self.player_map(opponent(id)).current_map_status(true),
}
}
}
impl Actor for Game {
type Context = Context<Self>;
}
#[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.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();
}
}
}
#[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)
}
}