Can play full game
This commit is contained in:
parent
5b7a56d060
commit
2723724589
@ -77,6 +77,32 @@ pub async fn run_client(server: &str, rules: &GameRules) -> Result<(), Box<dyn E
|
||||
)?))
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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)]
|
||||
|
20
sea_battle_backend/src/data/end_game_map.rs
Normal file
20
sea_battle_backend/src/data/end_game_map.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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<Coordinates>,
|
||||
successful_strikes: Vec<Coordinates>,
|
||||
sunk_boats: Vec<BoatPosition>,
|
||||
}
|
||||
|
||||
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::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -23,6 +23,7 @@ pub struct PlayConfiguration {
|
||||
pub min_boats_number: usize,
|
||||
pub max_boats_number: usize,
|
||||
pub bot_types: Vec<BotDescription>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Fire> 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)
|
||||
}
|
||||
}
|
||||
|
@ -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)),
|
||||
|
@ -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<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) {
|
||||
match msg {
|
||||
Ok(Message::Ping(msg)) => ctx.pong(&msg),
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
@ -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();
|
||||
})
|
||||
|
@ -4,6 +4,8 @@ use crate::args::Args;
|
||||
enum TestPort {
|
||||
ClientInvalidPort = 20000,
|
||||
ClientInvalidRules,
|
||||
FullGame,
|
||||
FullGameTouchingBoats,
|
||||
}
|
||||
|
||||
impl TestPort {
|
||||
|
Loading…
Reference in New Issue
Block a user