Can play full game

This commit is contained in:
Pierre HUBERT 2022-09-15 17:37:26 +02:00
parent 5b7a56d060
commit 2723724589
13 changed files with 339 additions and 22 deletions

View File

@ -77,6 +77,32 @@ pub async fn run_client(server: &str, rules: &GameRules) -> Result<(), Box<dyn E
)?)) )?))
.await?; .await?;
} }
ServerMessage::StrikeResult { pos, result } => {
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;
}
} }
} }

View File

@ -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_BOATS_CAN_TOUCH: bool = true;
pub const MULTI_PLAYER_PLAYER_CAN_CONTINUE_AFTER_HIT: 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 MULTI_PLAYER_PLAYER_BOATS: [usize; 5] = [2, 3, 3, 4, 5];
pub const ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

View File

@ -2,9 +2,10 @@ use std::io::ErrorKind;
use rand::{Rng, RngCore}; use rand::{Rng, RngCore};
use crate::consts::ALPHABET;
use crate::data::GameRules; 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 { pub enum BoatDirection {
Left, Left,
Right, Right,
@ -95,9 +96,20 @@ impl Coordinates {
&& self.x < rules.map_width as i32 && self.x < rules.map_width as i32
&& self.y < rules.map_height 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 { pub struct BoatPosition {
start: Coordinates, start: Coordinates,
len: usize, len: usize,
@ -256,6 +268,10 @@ impl BoatsLayout {
pub fn find_boat_at_position(&self, pos: Coordinates) -> Option<&BoatPosition> { pub fn find_boat_at_position(&self, pos: Coordinates) -> Option<&BoatPosition> {
self.0.iter().find(|f| f.all_coordinates().contains(&pos)) self.0.iter().find(|f| f.all_coordinates().contains(&pos))
} }
pub fn number_of_boats(&self) -> usize {
self.0.len()
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -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<Vec<MapCellContent>>,
}
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
}
}

View File

@ -1,22 +1,34 @@
use crate::data::boats_layout::{BoatsLayout, Coordinates}; 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 { pub enum MapCellContent {
Invalid, Invalid,
Nothing, Nothing,
TouchedBoat, TouchedBoat,
SunkBoat,
Boat, Boat,
FailedStrike, FailedStrike,
} }
impl MapCellContent { impl MapCellContent {
fn letter(&self) -> &'static str { pub fn letter(&self) -> &'static str {
match self { match self {
MapCellContent::Invalid => "!", MapCellContent::Invalid => "!",
MapCellContent::Nothing => ".", MapCellContent::Nothing => ".",
MapCellContent::TouchedBoat => "T", MapCellContent::TouchedBoat => "T",
MapCellContent::SunkBoat => "S",
MapCellContent::Boat => "B", MapCellContent::Boat => "B",
MapCellContent::FailedStrike => "X", MapCellContent::FailedStrike => "x",
} }
} }
} }
@ -24,6 +36,9 @@ impl MapCellContent {
pub struct GameMap { pub struct GameMap {
rules: GameRules, rules: GameRules,
boats_config: BoatsLayout, boats_config: BoatsLayout,
failed_strikes: Vec<Coordinates>,
successful_strikes: Vec<Coordinates>,
sunk_boats: Vec<BoatPosition>,
} }
impl GameMap { impl GameMap {
@ -31,19 +46,80 @@ impl GameMap {
Self { Self {
rules, rules,
boats_config, boats_config,
failed_strikes: vec![],
successful_strikes: vec![],
sunk_boats: vec![],
} }
} }
pub fn get_cell_content(&self, c: Coordinates) -> MapCellContent { 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() { 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; return MapCellContent::Boat;
} }
if self.sunk_boats.contains(b) {
return MapCellContent::SunkBoat;
}
return MapCellContent::TouchedBoat;
}
MapCellContent::Nothing 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) { pub fn print_map(&self) {
for y in 0..self.rules.map_height { for y in 0..self.rules.map_height {
for x in 0..self.rules.map_width { for x in 0..self.rules.map_width {
@ -56,4 +132,17 @@ impl GameMap {
println!(); 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::<Vec<_>>()
})
.collect::<Vec<_>>(),
}
}
} }

View File

@ -1,11 +1,13 @@
pub use boats_layout::*; pub use boats_layout::*;
pub use current_game_status::*; pub use current_game_status::*;
pub use end_game_map::*;
pub use game_map::*; pub use game_map::*;
pub use game_rules::*; pub use game_rules::*;
pub use play_config::*; pub use play_config::*;
mod boats_layout; mod boats_layout;
mod current_game_status; mod current_game_status;
mod end_game_map;
mod game_map; mod game_map;
mod game_rules; mod game_rules;
mod play_config; mod play_config;

View File

@ -23,6 +23,7 @@ pub struct PlayConfiguration {
pub min_boats_number: usize, pub min_boats_number: usize,
pub max_boats_number: usize, pub max_boats_number: usize,
pub bot_types: Vec<BotDescription>, pub bot_types: Vec<BotDescription>,
pub ordinate_alphabet: &'static str,
} }
impl Default for PlayConfiguration { impl Default for PlayConfiguration {
@ -40,6 +41,7 @@ impl Default for PlayConfiguration {
r#type: BotType::Random, r#type: BotType::Random,
description: "Random strike. All the time.".to_string(), description: "Random strike. All the time.".to_string(),
}], }],
ordinate_alphabet: ALPHABET,
} }
} }
} }

View File

@ -20,6 +20,14 @@ pub trait Player {
fn request_fire(&self, status: CurrentGameStatus); fn request_fire(&self, status: CurrentGameStatus);
fn other_player_must_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 { fn opponent(index: usize) -> usize {
@ -36,6 +44,7 @@ enum GameStatus {
Created, Created,
WaitingForBoatsDisposition, WaitingForBoatsDisposition,
Started, Started,
Finished,
} }
pub struct Game { pub struct Game {
@ -87,9 +96,65 @@ impl Game {
self.turn 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[self.turn].request_fire(self.get_game_status_for_player(self.turn));
self.players[opponent(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 /// Get current game status for a specific player
@ -164,6 +229,16 @@ impl Handler<Fire> for Game {
type Result = (); type Result = ();
fn handle(&mut self, msg: Fire, _ctx: &mut Self::Context) -> Self::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)
} }
} }

View File

@ -1,7 +1,7 @@
use actix::Addr; use actix::Addr;
use uuid::Uuid; 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::game::{Fire, Game, Player, SetBoatsLayout};
use crate::human_player_ws::{ClientMessage, HumanPlayerWS, ServerMessage}; use crate::human_player_ws::{ClientMessage, HumanPlayerWS, ServerMessage};
@ -43,15 +43,44 @@ impl Player for HumanPlayer {
self.player self.player
.do_send(ServerMessage::OtherPlayerMustFire { status }); .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 { impl HumanPlayer {
pub fn handle_client_message(&self, msg: ClientMessage) { pub fn handle_client_message(&self, msg: ClientMessage) {
match msg { match msg {
ClientMessage::StopGame => { ClientMessage::StopGame => {
// TODO : do something} // TODO : do something
} }
ClientMessage::BoatsLayout { layout } => { ClientMessage::BoatsLayout { layout } => {
// TODO : check boat layout validity
self.game.do_send(SetBoatsLayout(self.uuid, layout)) self.game.do_send(SetBoatsLayout(self.uuid, layout))
} }
ClientMessage::Fire { location } => self.game.do_send(Fire(self.uuid, location)), ClientMessage::Fire { location } => self.game.do_send(Fire(self.uuid, location)),

View File

@ -6,7 +6,9 @@ use actix_web_actors::ws;
use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext}; use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext};
use uuid::Uuid; 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::game::{AddPlayer, Game};
use crate::human_player::HumanPlayer; use crate::human_player::HumanPlayer;
use crate::random_bot::RandomBot; use crate::random_bot::RandomBot;
@ -33,12 +35,34 @@ pub enum ClientMessage {
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ServerMessage { pub enum ServerMessage {
WaitingForAnotherPlayer, WaitingForAnotherPlayer,
QueryBoatsLayout { rules: GameRules }, QueryBoatsLayout {
rules: GameRules,
},
WaitingForOtherPlayerConfiguration, WaitingForOtherPlayerConfiguration,
OtherPlayerReady, OtherPlayerReady,
GameStarting, GameStarting,
OtherPlayerMustFire { status: CurrentGameStatus }, OtherPlayerMustFire {
RequestFire { status: CurrentGameStatus }, 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)] #[derive(Default)]
@ -95,7 +119,7 @@ impl Actor for HumanPlayerWS {
} }
} }
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for HumanPlayerWS { impl StreamHandler<Result<ws::Message, ProtocolError>> for HumanPlayerWS {
fn handle(&mut self, msg: Result<Message, ProtocolError>, ctx: &mut Self::Context) { fn handle(&mut self, msg: Result<Message, ProtocolError>, ctx: &mut Self::Context) {
match msg { match msg {
Ok(Message::Ping(msg)) => ctx.pong(&msg), Ok(Message::Ping(msg)) => ctx.pong(&msg),

View File

@ -1,7 +1,7 @@
use actix::Addr; use actix::Addr;
use uuid::Uuid; 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}; use crate::game::{Fire, Game, Player, SetBoatsLayout};
#[derive(Clone)] #[derive(Clone)]
@ -49,4 +49,12 @@ impl Player for RandomBot {
} }
fn other_player_must_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) {}
} }

View File

@ -1,10 +1,11 @@
use tokio::task;
use crate::args::Args; use crate::args::Args;
use crate::bot_client; use crate::bot_client;
use crate::data::GameRules; use crate::data::GameRules;
use crate::server::start_server; use crate::server::start_server;
use crate::test::network_utils::wait_for_port; use crate::test::network_utils::wait_for_port;
use crate::test::TestPort; use crate::test::TestPort;
use tokio::task;
#[tokio::test] #[tokio::test]
async fn invalid_port() { async fn invalid_port() {
@ -46,10 +47,31 @@ async fn full_game() {
local_set local_set
.run_until(async move { .run_until(async move {
let rules = GameRules::random_players_rules(); let rules = GameRules::random_players_rules();
task::spawn_local(start_server(Args::for_test(TestPort::ClientInvalidRules))); task::spawn_local(start_server(Args::for_test(TestPort::FullGame)));
wait_for_port(TestPort::ClientInvalidRules.port()).await; 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 .await
.unwrap(); .unwrap();
}) })

View File

@ -4,6 +4,8 @@ use crate::args::Args;
enum TestPort { enum TestPort {
ClientInvalidPort = 20000, ClientInvalidPort = 20000,
ClientInvalidRules, ClientInvalidRules,
FullGame,
FullGameTouchingBoats,
} }
impl TestPort { impl TestPort {