From 27237245899833ef1a0bde9dd50b4620c5299f3c Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 15 Sep 2022 17:37:26 +0200 Subject: [PATCH] Can play full game --- sea_battle_backend/src/bot_client.rs | 26 +++++ sea_battle_backend/src/consts.rs | 2 + sea_battle_backend/src/data/boats_layout.rs | 20 +++- sea_battle_backend/src/data/end_game_map.rs | 20 ++++ sea_battle_backend/src/data/game_map.rs | 101 ++++++++++++++++++-- sea_battle_backend/src/data/mod.rs | 2 + sea_battle_backend/src/data/play_config.rs | 2 + sea_battle_backend/src/game.rs | 79 ++++++++++++++- sea_battle_backend/src/human_player.rs | 33 ++++++- sea_battle_backend/src/human_player_ws.rs | 34 ++++++- sea_battle_backend/src/random_bot.rs | 10 +- sea_battle_backend/src/test/client.rs | 30 +++++- sea_battle_backend/src/test/mod.rs | 2 + 13 files changed, 339 insertions(+), 22 deletions(-) create mode 100644 sea_battle_backend/src/data/end_game_map.rs diff --git a/sea_battle_backend/src/bot_client.rs b/sea_battle_backend/src/bot_client.rs index a5442ab..9449468 100644 --- a/sea_battle_backend/src/bot_client.rs +++ b/sea_battle_backend/src/bot_client.rs @@ -77,6 +77,32 @@ pub async fn run_client(server: &str, rules: &GameRules) -> Result<(), Box { + log::debug!("Strike at {} result: {:?}", pos.human_print(), result) + } + ServerMessage::OpponentStrikeResult { pos, result } => log::debug!( + "Opponent trike at {} result: {:?}", + pos.human_print(), + result + ), + ServerMessage::LostGame { + your_map, + opponent_map, + } => { + log::debug!("We lost game :("); + log::debug!("Other game:\n{}\n", opponent_map.get_map()); + log::debug!("Our game:\n{}\n", your_map.get_map()); + break; + } + ServerMessage::WonGame { + your_map, + opponent_map, + } => { + log::debug!("We won the game !!!!"); + log::debug!("Other game:\n{}\n", opponent_map.get_map()); + log::debug!("Our game:\n{}\n", your_map.get_map()); + break; + } } } diff --git a/sea_battle_backend/src/consts.rs b/sea_battle_backend/src/consts.rs index 737c803..5e08298 100644 --- a/sea_battle_backend/src/consts.rs +++ b/sea_battle_backend/src/consts.rs @@ -17,3 +17,5 @@ pub const MULTI_PLAYER_MAP_HEIGHT: usize = 10; pub const MULTI_PLAYER_BOATS_CAN_TOUCH: bool = true; pub const MULTI_PLAYER_PLAYER_CAN_CONTINUE_AFTER_HIT: bool = true; pub const MULTI_PLAYER_PLAYER_BOATS: [usize; 5] = [2, 3, 3, 4, 5]; + +pub const ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; diff --git a/sea_battle_backend/src/data/boats_layout.rs b/sea_battle_backend/src/data/boats_layout.rs index e4d7239..550173a 100644 --- a/sea_battle_backend/src/data/boats_layout.rs +++ b/sea_battle_backend/src/data/boats_layout.rs @@ -2,9 +2,10 @@ use std::io::ErrorKind; use rand::{Rng, RngCore}; +use crate::consts::ALPHABET; use crate::data::GameRules; -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] pub enum BoatDirection { Left, Right, @@ -95,9 +96,20 @@ impl Coordinates { && self.x < rules.map_width as i32 && self.y < rules.map_height as i32 } + + pub fn human_print(&self) -> String { + format!( + "{}:{}", + match self.y < 0 || self.y >= ALPHABET.len() as i32 { + true => self.y.to_string(), + false => ALPHABET.chars().nth(self.y as usize).unwrap().to_string(), + }, + self.x + ) + } } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] pub struct BoatPosition { start: Coordinates, len: usize, @@ -256,6 +268,10 @@ impl BoatsLayout { pub fn find_boat_at_position(&self, pos: Coordinates) -> Option<&BoatPosition> { self.0.iter().find(|f| f.all_coordinates().contains(&pos)) } + + pub fn number_of_boats(&self) -> usize { + self.0.len() + } } #[cfg(test)] diff --git a/sea_battle_backend/src/data/end_game_map.rs b/sea_battle_backend/src/data/end_game_map.rs new file mode 100644 index 0000000..4d9dc0d --- /dev/null +++ b/sea_battle_backend/src/data/end_game_map.rs @@ -0,0 +1,20 @@ +use crate::data::{BoatsLayout, MapCellContent}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct EndGameMap { + pub boats: BoatsLayout, + pub grid: Vec>, +} + +impl EndGameMap { + pub fn get_map(&self) -> String { + let mut s = String::new(); + for row in &self.grid { + for col in row { + s.push_str(&format!("{} ", col.letter())); + } + s.push('\n'); + } + s + } +} diff --git a/sea_battle_backend/src/data/game_map.rs b/sea_battle_backend/src/data/game_map.rs index 6fcb0d2..01ad39a 100644 --- a/sea_battle_backend/src/data/game_map.rs +++ b/sea_battle_backend/src/data/game_map.rs @@ -1,22 +1,34 @@ use crate::data::boats_layout::{BoatsLayout, Coordinates}; -use crate::data::GameRules; +use crate::data::{BoatPosition, EndGameMap, GameRules}; +#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum FireResult { + Missed, + Hit, + Sunk, + Rejected, + AlreadyTargetedPosition, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum MapCellContent { Invalid, Nothing, TouchedBoat, + SunkBoat, Boat, FailedStrike, } impl MapCellContent { - fn letter(&self) -> &'static str { + pub fn letter(&self) -> &'static str { match self { MapCellContent::Invalid => "!", MapCellContent::Nothing => ".", MapCellContent::TouchedBoat => "T", + MapCellContent::SunkBoat => "S", MapCellContent::Boat => "B", - MapCellContent::FailedStrike => "X", + MapCellContent::FailedStrike => "x", } } } @@ -24,6 +36,9 @@ impl MapCellContent { pub struct GameMap { rules: GameRules, boats_config: BoatsLayout, + failed_strikes: Vec, + successful_strikes: Vec, + sunk_boats: Vec, } impl GameMap { @@ -31,19 +46,80 @@ impl GameMap { Self { rules, boats_config, + failed_strikes: vec![], + successful_strikes: vec![], + sunk_boats: vec![], } } pub fn get_cell_content(&self, c: Coordinates) -> MapCellContent { - //TODO : improve this + if !c.is_valid(&self.rules) { + return MapCellContent::Invalid; + } - if self.boats_config.find_boat_at_position(c).is_some() { - return MapCellContent::Boat; + if self.failed_strikes.contains(&c) { + return MapCellContent::FailedStrike; + } + + if let Some(b) = self.boats_config.find_boat_at_position(c) { + if !self.successful_strikes.contains(&c) { + return MapCellContent::Boat; + } + + if self.sunk_boats.contains(b) { + return MapCellContent::SunkBoat; + } + + return MapCellContent::TouchedBoat; } MapCellContent::Nothing } + pub fn fire(&mut self, c: Coordinates) -> FireResult { + if !c.is_valid(&self.rules) { + return FireResult::Rejected; + } + + if self.failed_strikes.contains(&c) || self.successful_strikes.contains(&c) { + return FireResult::AlreadyTargetedPosition; + } + + match self.boats_config.find_boat_at_position(c) { + None => { + self.failed_strikes.push(c); + FireResult::Missed + } + Some(b) => { + self.successful_strikes.push(c); + + if !b + .all_coordinates() + .iter() + .all(|c| self.successful_strikes.contains(c)) + { + return FireResult::Hit; + } + + self.sunk_boats.push(*b); + + if !self.rules.boats_can_touch { + for c in b.neighbor_coordinates(&self.rules) { + if !self.failed_strikes.contains(&c) { + self.failed_strikes.push(c); + } + } + } + + FireResult::Sunk + } + } + } + + pub fn are_all_boat_sunk(&self) -> bool { + self.sunk_boats.len() == self.boats_config.number_of_boats() + } + pub fn print_map(&self) { for y in 0..self.rules.map_height { for x in 0..self.rules.map_width { @@ -56,4 +132,17 @@ impl GameMap { println!(); } } + + pub fn final_map(&self) -> EndGameMap { + EndGameMap { + boats: self.boats_config.clone(), + grid: (0..self.rules.map_height) + .map(|y| { + (0..self.rules.map_width) + .map(|x| self.get_cell_content(Coordinates::new(x as i32, y as i32))) + .collect::>() + }) + .collect::>(), + } + } } diff --git a/sea_battle_backend/src/data/mod.rs b/sea_battle_backend/src/data/mod.rs index 9d84c56..04c7de0 100644 --- a/sea_battle_backend/src/data/mod.rs +++ b/sea_battle_backend/src/data/mod.rs @@ -1,11 +1,13 @@ pub use boats_layout::*; pub use current_game_status::*; +pub use end_game_map::*; pub use game_map::*; pub use game_rules::*; pub use play_config::*; mod boats_layout; mod current_game_status; +mod end_game_map; mod game_map; mod game_rules; mod play_config; diff --git a/sea_battle_backend/src/data/play_config.rs b/sea_battle_backend/src/data/play_config.rs index c73e7a2..0edd806 100644 --- a/sea_battle_backend/src/data/play_config.rs +++ b/sea_battle_backend/src/data/play_config.rs @@ -23,6 +23,7 @@ pub struct PlayConfiguration { pub min_boats_number: usize, pub max_boats_number: usize, pub bot_types: Vec, + pub ordinate_alphabet: &'static str, } impl Default for PlayConfiguration { @@ -40,6 +41,7 @@ impl Default for PlayConfiguration { r#type: BotType::Random, description: "Random strike. All the time.".to_string(), }], + ordinate_alphabet: ALPHABET, } } } diff --git a/sea_battle_backend/src/game.rs b/sea_battle_backend/src/game.rs index 11ce9e5..481e637 100644 --- a/sea_battle_backend/src/game.rs +++ b/sea_battle_backend/src/game.rs @@ -20,6 +20,14 @@ pub trait Player { 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 { @@ -36,6 +44,7 @@ enum GameStatus { Created, WaitingForBoatsDisposition, Started, + Finished, } pub struct Game { @@ -87,9 +96,65 @@ impl Game { 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)] - .request_fire(self.get_game_status_for_player(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 @@ -164,6 +229,16 @@ impl Handler for Game { type Result = (); fn handle(&mut self, msg: Fire, _ctx: &mut Self::Context) -> Self::Result { - log::debug!("FIRE ===> {:?}", msg); + 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) } } diff --git a/sea_battle_backend/src/human_player.rs b/sea_battle_backend/src/human_player.rs index f6a7619..c17e210 100644 --- a/sea_battle_backend/src/human_player.rs +++ b/sea_battle_backend/src/human_player.rs @@ -1,7 +1,7 @@ use actix::Addr; use uuid::Uuid; -use crate::data::{CurrentGameStatus, GameRules}; +use crate::data::{Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules}; use crate::game::{Fire, Game, Player, SetBoatsLayout}; use crate::human_player_ws::{ClientMessage, HumanPlayerWS, ServerMessage}; @@ -43,15 +43,44 @@ impl Player for HumanPlayer { self.player .do_send(ServerMessage::OtherPlayerMustFire { status }); } + + fn strike_result(&self, c: Coordinates, res: FireResult) { + self.player.do_send(ServerMessage::StrikeResult { + pos: c, + result: res, + }); + } + + fn other_player_strike_result(&self, c: Coordinates, res: FireResult) { + self.player.do_send(ServerMessage::OpponentStrikeResult { + pos: c, + result: res, + }); + } + + fn lost_game(&self, your_map: EndGameMap, opponent_map: EndGameMap) { + self.player.do_send(ServerMessage::LostGame { + your_map, + opponent_map, + }); + } + + fn won_game(&self, your_map: EndGameMap, opponent_map: EndGameMap) { + self.player.do_send(ServerMessage::WonGame { + your_map, + opponent_map, + }); + } } impl HumanPlayer { pub fn handle_client_message(&self, msg: ClientMessage) { match msg { ClientMessage::StopGame => { - // TODO : do something} + // TODO : do something } ClientMessage::BoatsLayout { layout } => { + // TODO : check boat layout validity self.game.do_send(SetBoatsLayout(self.uuid, layout)) } ClientMessage::Fire { location } => self.game.do_send(Fire(self.uuid, location)), diff --git a/sea_battle_backend/src/human_player_ws.rs b/sea_battle_backend/src/human_player_ws.rs index 8a4400a..74328fa 100644 --- a/sea_battle_backend/src/human_player_ws.rs +++ b/sea_battle_backend/src/human_player_ws.rs @@ -6,7 +6,9 @@ use actix_web_actors::ws; use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext}; use uuid::Uuid; -use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, GameRules}; +use crate::data::{ + BoatsLayout, BotType, Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules, +}; use crate::game::{AddPlayer, Game}; use crate::human_player::HumanPlayer; use crate::random_bot::RandomBot; @@ -33,12 +35,34 @@ pub enum ClientMessage { #[serde(tag = "type")] pub enum ServerMessage { WaitingForAnotherPlayer, - QueryBoatsLayout { rules: GameRules }, + QueryBoatsLayout { + rules: GameRules, + }, WaitingForOtherPlayerConfiguration, OtherPlayerReady, GameStarting, - OtherPlayerMustFire { status: CurrentGameStatus }, - RequestFire { status: CurrentGameStatus }, + OtherPlayerMustFire { + status: CurrentGameStatus, + }, + RequestFire { + status: CurrentGameStatus, + }, + StrikeResult { + pos: Coordinates, + result: FireResult, + }, + OpponentStrikeResult { + pos: Coordinates, + result: FireResult, + }, + LostGame { + your_map: EndGameMap, + opponent_map: EndGameMap, + }, + WonGame { + your_map: EndGameMap, + opponent_map: EndGameMap, + }, } #[derive(Default)] @@ -95,7 +119,7 @@ impl Actor for HumanPlayerWS { } } -impl StreamHandler> for HumanPlayerWS { +impl StreamHandler> for HumanPlayerWS { fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { match msg { Ok(Message::Ping(msg)) => ctx.pong(&msg), diff --git a/sea_battle_backend/src/random_bot.rs b/sea_battle_backend/src/random_bot.rs index 9f1897a..57d7c77 100644 --- a/sea_battle_backend/src/random_bot.rs +++ b/sea_battle_backend/src/random_bot.rs @@ -1,7 +1,7 @@ use actix::Addr; use uuid::Uuid; -use crate::data::{BoatsLayout, CurrentGameStatus, GameRules}; +use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules}; use crate::game::{Fire, Game, Player, SetBoatsLayout}; #[derive(Clone)] @@ -49,4 +49,12 @@ impl Player for RandomBot { } 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) {} } diff --git a/sea_battle_backend/src/test/client.rs b/sea_battle_backend/src/test/client.rs index b86f48c..d12a6c8 100644 --- a/sea_battle_backend/src/test/client.rs +++ b/sea_battle_backend/src/test/client.rs @@ -1,10 +1,11 @@ +use tokio::task; + use crate::args::Args; use crate::bot_client; use crate::data::GameRules; use crate::server::start_server; use crate::test::network_utils::wait_for_port; use crate::test::TestPort; -use tokio::task; #[tokio::test] async fn invalid_port() { @@ -46,10 +47,31 @@ async fn full_game() { local_set .run_until(async move { let rules = GameRules::random_players_rules(); - task::spawn_local(start_server(Args::for_test(TestPort::ClientInvalidRules))); - wait_for_port(TestPort::ClientInvalidRules.port()).await; + task::spawn_local(start_server(Args::for_test(TestPort::FullGame))); + wait_for_port(TestPort::FullGame.port()).await; - bot_client::run_client(&TestPort::ClientInvalidRules.as_url(), &rules) + bot_client::run_client(&TestPort::FullGame.as_url(), &rules) + .await + .unwrap(); + }) + .await; +} + +#[tokio::test] +async fn full_game_no_touching_boats() { + let _ = env_logger::builder().is_test(true).try_init(); + + let local_set = task::LocalSet::new(); + local_set + .run_until(async move { + let mut rules = GameRules::random_players_rules(); + rules.boats_can_touch = false; + task::spawn_local(start_server(Args::for_test( + TestPort::FullGameTouchingBoats, + ))); + wait_for_port(TestPort::FullGameTouchingBoats.port()).await; + + bot_client::run_client(&TestPort::FullGameTouchingBoats.as_url(), &rules) .await .unwrap(); }) diff --git a/sea_battle_backend/src/test/mod.rs b/sea_battle_backend/src/test/mod.rs index e4a1952..bf09977 100644 --- a/sea_battle_backend/src/test/mod.rs +++ b/sea_battle_backend/src/test/mod.rs @@ -4,6 +4,8 @@ use crate::args::Args; enum TestPort { ClientInvalidPort = 20000, ClientInvalidRules, + FullGame, + FullGameTouchingBoats, } impl TestPort {