Compare commits
5 Commits
e71b724278
...
ef9d2ce112
| Author | SHA1 | Date | |
|---|---|---|---|
| ef9d2ce112 | |||
| 625cc88950 | |||
| dfdf6d9952 | |||
| 45b6a24eda | |||
| 663a9c2d71 |
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# SeaBattle
|
||||||
|
|
||||||
|
Full stack sea battle game.
|
||||||
|
|
||||||
|
Current status: working on backend, and then building web ui...
|
||||||
@@ -1,25 +1,27 @@
|
|||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
||||||
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
|
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct IntermediateBot {
|
pub struct BotPlayer {
|
||||||
game: Addr<Game>,
|
game: Addr<Game>,
|
||||||
|
kind: BotType,
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntermediateBot {
|
impl BotPlayer {
|
||||||
pub fn new(game: Addr<Game>) -> Self {
|
pub fn new(kind: BotType, game: Addr<Game>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
game,
|
game,
|
||||||
|
kind,
|
||||||
uuid: Uuid::new_v4(),
|
uuid: Uuid::new_v4(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player for IntermediateBot {
|
impl Player for BotPlayer {
|
||||||
fn get_name(&self) -> &str {
|
fn get_name(&self) -> &str {
|
||||||
"Intermediate Bot"
|
"Intermediate Bot"
|
||||||
}
|
}
|
||||||
@@ -54,11 +56,10 @@ impl Player for IntermediateBot {
|
|||||||
fn notify_game_starting(&self) {}
|
fn notify_game_starting(&self) {}
|
||||||
|
|
||||||
fn request_fire(&self, status: CurrentGameStatus) {
|
fn request_fire(&self, status: CurrentGameStatus) {
|
||||||
let coordinates = status
|
self.game.do_send(Fire(
|
||||||
.continue_attack_boat()
|
self.uuid,
|
||||||
.unwrap_or_else(|| status.find_valid_random_fire_location());
|
status.find_fire_coordinates_for_bot_type(self.kind),
|
||||||
|
));
|
||||||
self.game.do_send(Fire(self.uuid, coordinates));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn opponent_must_fire(&self, _status: CurrentGameStatus) {}
|
fn opponent_must_fire(&self, _status: CurrentGameStatus) {}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
use actix::Addr;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
|
||||||
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct LinearBot {
|
|
||||||
game: Addr<Game>,
|
|
||||||
uuid: Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinearBot {
|
|
||||||
pub fn new(game: Addr<Game>) -> Self {
|
|
||||||
Self {
|
|
||||||
game,
|
|
||||||
uuid: Uuid::new_v4(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Player for LinearBot {
|
|
||||||
fn get_name(&self) -> &str {
|
|
||||||
"Linear Bot"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_uid(&self) -> Uuid {
|
|
||||||
self.uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_bot(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_other_player_name(&self, _name: &str) {}
|
|
||||||
|
|
||||||
fn query_boats_layout(&self, rules: &GameRules) {
|
|
||||||
match BoatsLayout::gen_random_for_rules(rules) {
|
|
||||||
Ok(layout) => self.game.do_send(SetBoatsLayout(self.uuid, layout)),
|
|
||||||
|
|
||||||
Err(e) => log::error!(
|
|
||||||
"Failed to use game rules to construct boats layout: {:?}",
|
|
||||||
e
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rejected_boats_layout(&self, _errors: Vec<&'static str>) {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_other_player_ready(&self) {}
|
|
||||||
|
|
||||||
fn notify_game_starting(&self) {}
|
|
||||||
|
|
||||||
fn request_fire(&self, status: CurrentGameStatus) {
|
|
||||||
self.game
|
|
||||||
.do_send(Fire(self.uuid, status.find_first_valid_fire_location()));
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
self.game.do_send(RespondRequestRematch(self.uuid, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn opponent_rejected_rematch(&self) {}
|
|
||||||
|
|
||||||
fn opponent_accepted_rematch(&self) {}
|
|
||||||
|
|
||||||
fn opponent_left_game(&self) {
|
|
||||||
// Human are not reliable lol
|
|
||||||
}
|
|
||||||
|
|
||||||
fn opponent_replaced_by_bot(&self) {
|
|
||||||
// Not such a good idea. will panic, just in case
|
|
||||||
panic!("Bot shall not play against each other (it is completely useless)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub mod intermediate_bot;
|
|
||||||
pub mod linear_bot;
|
|
||||||
pub mod random_bot;
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
use actix::Addr;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
|
||||||
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RandomBot {
|
|
||||||
game: Addr<Game>,
|
|
||||||
uuid: Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RandomBot {
|
|
||||||
pub fn new(game: Addr<Game>) -> Self {
|
|
||||||
Self {
|
|
||||||
game,
|
|
||||||
uuid: Uuid::new_v4(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Player for RandomBot {
|
|
||||||
fn get_name(&self) -> &str {
|
|
||||||
"Random Bot"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_uid(&self) -> Uuid {
|
|
||||||
self.uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_bot(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_other_player_name(&self, _name: &str) {}
|
|
||||||
|
|
||||||
fn query_boats_layout(&self, rules: &GameRules) {
|
|
||||||
match BoatsLayout::gen_random_for_rules(rules) {
|
|
||||||
Ok(layout) => self.game.do_send(SetBoatsLayout(self.uuid, layout)),
|
|
||||||
|
|
||||||
Err(e) => log::error!(
|
|
||||||
"Failed to use game rules to construct boats layout: {:?}",
|
|
||||||
e
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rejected_boats_layout(&self, _errors: Vec<&'static str>) {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_other_player_ready(&self) {}
|
|
||||||
|
|
||||||
fn notify_game_starting(&self) {}
|
|
||||||
|
|
||||||
fn request_fire(&self, status: CurrentGameStatus) {
|
|
||||||
self.game
|
|
||||||
.do_send(Fire(self.uuid, status.find_valid_random_fire_location()));
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
self.game.do_send(RespondRequestRematch(self.uuid, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn opponent_rejected_rematch(&self) {}
|
|
||||||
|
|
||||||
fn opponent_accepted_rematch(&self) {}
|
|
||||||
|
|
||||||
fn opponent_left_game(&self) {
|
|
||||||
// Human are not reliable lol
|
|
||||||
}
|
|
||||||
|
|
||||||
fn opponent_replaced_by_bot(&self) {
|
|
||||||
// Not such a good idea. will panic, just in case
|
|
||||||
panic!("Bot shall not play against each other (it is completely useless)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,3 +19,5 @@ 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";
|
pub const ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
|
||||||
|
pub const INVITE_CODE_LENGTH: usize = 5;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
|
||||||
use crate::data::{
|
use crate::data::{
|
||||||
BoatPosition, BoatsLayout, Coordinates, GameRules, MapCellContent, PrintableMap,
|
BoatPosition, BoatsLayout, BotType, Coordinates, GameRules, MapCellContent, PrintableMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)]
|
||||||
@@ -274,6 +274,32 @@ impl CurrentGameStatus {
|
|||||||
pub fn print_opponent_map(&self) {
|
pub fn print_opponent_map(&self) {
|
||||||
print!("{}", self.get_opponent_map());
|
print!("{}", self.get_opponent_map());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_intermediate_bot_fire_location(&self) -> Coordinates {
|
||||||
|
self.continue_attack_boat()
|
||||||
|
.unwrap_or_else(|| self.find_valid_random_fire_location())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_smart_bot_fire_location(&self) -> Coordinates {
|
||||||
|
self.continue_attack_boat().unwrap_or_else(|| {
|
||||||
|
let coordinates = self.get_relevant_grid_locations();
|
||||||
|
if !coordinates.is_empty() {
|
||||||
|
let pos = rand::thread_rng().next_u32() as usize;
|
||||||
|
coordinates[pos % coordinates.len()]
|
||||||
|
} else {
|
||||||
|
self.find_valid_random_fire_location()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_fire_coordinates_for_bot_type(&self, t: BotType) -> Coordinates {
|
||||||
|
match t {
|
||||||
|
BotType::Random => self.find_valid_random_fire_location(),
|
||||||
|
BotType::Linear => self.find_first_valid_fire_location(),
|
||||||
|
BotType::Intermediate => self.find_intermediate_bot_fire_location(),
|
||||||
|
BotType::Smart => self.find_smart_bot_fire_location(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ pub enum BotType {
|
|||||||
Random,
|
Random,
|
||||||
Linear,
|
Linear,
|
||||||
Intermediate,
|
Intermediate,
|
||||||
// TODO : SmartBot
|
Smart,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct BotDescription {
|
pub struct BotDescription {
|
||||||
r#type: BotType,
|
r#type: BotType,
|
||||||
description: String,
|
name: &'static str,
|
||||||
|
description: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@@ -43,15 +44,23 @@ impl Default for PlayConfiguration {
|
|||||||
bot_types: vec![
|
bot_types: vec![
|
||||||
BotDescription {
|
BotDescription {
|
||||||
r#type: BotType::Linear,
|
r#type: BotType::Linear,
|
||||||
description: "Linear strike. Shoot A1, A2, A3, ..., B1, B2, ...".to_string(),
|
name: "Linear",
|
||||||
|
description: "Linear strike. Shoot A1, A2, A3, ..., B1, B2, ...",
|
||||||
},
|
},
|
||||||
BotDescription {
|
BotDescription {
|
||||||
r#type: BotType::Random,
|
r#type: BotType::Random,
|
||||||
description: "Random search. Random strike.".to_string(),
|
name: "Ranom",
|
||||||
|
description: "Random search. Random strike.",
|
||||||
},
|
},
|
||||||
BotDescription {
|
BotDescription {
|
||||||
r#type: BotType::Intermediate,
|
r#type: BotType::Intermediate,
|
||||||
description: "Randome search. Intelligent strike.".to_string(),
|
name: "Intermediate",
|
||||||
|
description: "Random search. Intelligent strike.",
|
||||||
|
},
|
||||||
|
BotDescription {
|
||||||
|
r#type: BotType::Smart,
|
||||||
|
name: "Smart",
|
||||||
|
description: "Smart search. Smart strike.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
ordinate_alphabet: ALPHABET,
|
ordinate_alphabet: ALPHABET,
|
||||||
|
|||||||
100
sea_battle_backend/src/dispatcher_actor.rs
Normal file
100
sea_battle_backend/src/dispatcher_actor.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//! # Dispatcher actors
|
||||||
|
//!
|
||||||
|
//! Allows to establish connections between human players
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use actix::{Actor, Addr, AsyncContext, Context, Handler, Message};
|
||||||
|
|
||||||
|
use crate::consts::INVITE_CODE_LENGTH;
|
||||||
|
use crate::data::GameRules;
|
||||||
|
use crate::game::Game;
|
||||||
|
use crate::human_player_ws::{CloseConnection, HumanPlayerWS, ServerMessage, SetGame};
|
||||||
|
use crate::utils::rand_str;
|
||||||
|
|
||||||
|
/// How often garbage collector is run
|
||||||
|
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct CreateInvite(pub GameRules, pub Addr<HumanPlayerWS>);
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct AcceptInvite(pub String, pub Addr<HumanPlayerWS>);
|
||||||
|
|
||||||
|
struct PendingPlayer {
|
||||||
|
player: Addr<HumanPlayerWS>,
|
||||||
|
rules: GameRules,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DispatcherActor {
|
||||||
|
with_invite: HashMap<String, PendingPlayer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for DispatcherActor {
|
||||||
|
type Context = Context<Self>;
|
||||||
|
|
||||||
|
fn started(&mut self, ctx: &mut Self::Context) {
|
||||||
|
ctx.run_interval(HEARTBEAT_INTERVAL, |act, _ctx| {
|
||||||
|
// Garbage collect invites
|
||||||
|
let ids = act
|
||||||
|
.with_invite
|
||||||
|
.iter()
|
||||||
|
.filter(|p| !p.1.player.connected())
|
||||||
|
.map(|p| p.0.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for id in ids {
|
||||||
|
log::debug!("Remove dead invite: {}", id);
|
||||||
|
act.with_invite.remove(&id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<CreateInvite> for DispatcherActor {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: CreateInvite, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
let mut invite_code = rand_str(INVITE_CODE_LENGTH).to_uppercase();
|
||||||
|
|
||||||
|
while self.with_invite.contains_key(&invite_code) {
|
||||||
|
invite_code = rand_str(INVITE_CODE_LENGTH).to_uppercase();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("Insert new invitation: {}", invite_code);
|
||||||
|
msg.1.do_send(ServerMessage::SetInviteCode {
|
||||||
|
code: invite_code.clone(),
|
||||||
|
});
|
||||||
|
msg.1.do_send(ServerMessage::WaitingForAnotherPlayer);
|
||||||
|
self.with_invite.insert(
|
||||||
|
invite_code,
|
||||||
|
PendingPlayer {
|
||||||
|
player: msg.1,
|
||||||
|
rules: msg.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<AcceptInvite> for DispatcherActor {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: AcceptInvite, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
let entry = match self.with_invite.remove(&msg.0) {
|
||||||
|
None => {
|
||||||
|
msg.1.do_send(ServerMessage::InvalidInviteCode);
|
||||||
|
msg.1.do_send(CloseConnection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(e) => e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let game = Game::new(entry.rules).start();
|
||||||
|
entry.player.do_send(SetGame(game.clone()));
|
||||||
|
msg.1.do_send(SetGame(game));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ use actix::prelude::*;
|
|||||||
use actix::{Actor, Context, Handler};
|
use actix::{Actor, Context, Handler};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::bots::random_bot::RandomBot;
|
use crate::bot_player::BotPlayer;
|
||||||
use crate::data::*;
|
use crate::data::*;
|
||||||
|
|
||||||
pub trait Player {
|
pub trait Player {
|
||||||
@@ -362,7 +362,8 @@ impl Handler<PlayerLeftGame> for Game {
|
|||||||
ctx.stop();
|
ctx.stop();
|
||||||
} else {
|
} else {
|
||||||
// Replace the player with a bot
|
// Replace the player with a bot
|
||||||
self.players[offline_player] = Arc::new(RandomBot::new(ctx.address()));
|
self.players[offline_player] =
|
||||||
|
Arc::new(BotPlayer::new(self.rules.bot_type, ctx.address()));
|
||||||
self.players[opponent(offline_player)].opponent_replaced_by_bot();
|
self.players[opponent(offline_player)].opponent_replaced_by_bot();
|
||||||
|
|
||||||
if self.turn == offline_player {
|
if self.turn == offline_player {
|
||||||
|
|||||||
@@ -7,10 +7,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::bots::intermediate_bot::IntermediateBot;
|
use crate::bot_player::BotPlayer;
|
||||||
use crate::bots::linear_bot::LinearBot;
|
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
||||||
use crate::bots::random_bot::RandomBot;
|
use crate::dispatcher_actor::{AcceptInvite, CreateInvite, DispatcherActor};
|
||||||
use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
|
||||||
use crate::game::{AddPlayer, Game};
|
use crate::game::{AddPlayer, Game};
|
||||||
use crate::human_player::HumanPlayer;
|
use crate::human_player::HumanPlayer;
|
||||||
|
|
||||||
@@ -19,13 +18,12 @@ const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(10);
|
|||||||
/// How long before lack of client response causes a timeout
|
/// How long before lack of client response causes a timeout
|
||||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(120);
|
const CLIENT_TIMEOUT: Duration = Duration::from_secs(120);
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Debug)]
|
||||||
pub enum StartMode {
|
pub enum StartMode {
|
||||||
Bot(GameRules),
|
Bot(GameRules),
|
||||||
|
CreateInvite(GameRules),
|
||||||
#[default]
|
AcceptInvite { code: String },
|
||||||
RandomHuman,
|
// TODO : random human
|
||||||
//TODO : create invite
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||||
@@ -44,6 +42,10 @@ pub enum ClientMessage {
|
|||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum ServerMessage {
|
pub enum ServerMessage {
|
||||||
|
SetInviteCode {
|
||||||
|
code: String,
|
||||||
|
},
|
||||||
|
InvalidInviteCode,
|
||||||
WaitingForAnotherPlayer,
|
WaitingForAnotherPlayer,
|
||||||
SetOpponentName {
|
SetOpponentName {
|
||||||
name: String,
|
name: String,
|
||||||
@@ -84,23 +86,33 @@ pub enum ServerMessage {
|
|||||||
OpponentReplacedByBot,
|
OpponentReplacedByBot,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct SetGame(pub Addr<Game>);
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct CloseConnection;
|
||||||
|
|
||||||
pub struct HumanPlayerWS {
|
pub struct HumanPlayerWS {
|
||||||
inner: Option<Arc<HumanPlayer>>,
|
inner: Option<Arc<HumanPlayer>>,
|
||||||
pub start_mode: StartMode,
|
pub start_mode: StartMode,
|
||||||
hb: Instant,
|
hb: Instant,
|
||||||
}
|
dispatcher: Addr<DispatcherActor>,
|
||||||
|
name: String,
|
||||||
impl Default for HumanPlayerWS {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: None,
|
|
||||||
start_mode: Default::default(),
|
|
||||||
hb: Instant::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HumanPlayerWS {
|
impl HumanPlayerWS {
|
||||||
|
pub fn new(start_mode: StartMode, dispatcher: &Addr<DispatcherActor>, name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: None,
|
||||||
|
start_mode,
|
||||||
|
hb: Instant::now(),
|
||||||
|
dispatcher: dispatcher.clone(),
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// helper method that sends ping to client every second.
|
/// helper method that sends ping to client every second.
|
||||||
///
|
///
|
||||||
/// also this method checks heartbeats from client
|
/// also this method checks heartbeats from client
|
||||||
@@ -141,20 +153,13 @@ impl Actor for HumanPlayerWS {
|
|||||||
log::debug!("Start play with a bot");
|
log::debug!("Start play with a bot");
|
||||||
let game = Game::new(rules.clone()).start();
|
let game = Game::new(rules.clone()).start();
|
||||||
|
|
||||||
match rules.bot_type {
|
game.do_send(AddPlayer(Arc::new(BotPlayer::new(
|
||||||
BotType::Random => {
|
rules.bot_type,
|
||||||
game.do_send(AddPlayer(Arc::new(RandomBot::new(game.clone()))));
|
game.clone(),
|
||||||
}
|
))));
|
||||||
BotType::Linear => {
|
|
||||||
game.do_send(AddPlayer(Arc::new(LinearBot::new(game.clone()))));
|
|
||||||
}
|
|
||||||
BotType::Intermediate => {
|
|
||||||
game.do_send(AddPlayer(Arc::new(IntermediateBot::new(game.clone()))));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let player = Arc::new(HumanPlayer {
|
let player = Arc::new(HumanPlayer {
|
||||||
name: "Human".to_string(),
|
name: self.name.to_string(),
|
||||||
game: game.clone(),
|
game: game.clone(),
|
||||||
player: ctx.address(),
|
player: ctx.address(),
|
||||||
uuid: Uuid::new_v4(),
|
uuid: Uuid::new_v4(),
|
||||||
@@ -163,8 +168,15 @@ impl Actor for HumanPlayerWS {
|
|||||||
game.do_send(AddPlayer(player));
|
game.do_send(AddPlayer(player));
|
||||||
}
|
}
|
||||||
|
|
||||||
StartMode::RandomHuman => {
|
StartMode::CreateInvite(rules) => {
|
||||||
unimplemented!();
|
log::info!("Create new play invite");
|
||||||
|
self.dispatcher
|
||||||
|
.do_send(CreateInvite(rules.clone(), ctx.address()));
|
||||||
|
}
|
||||||
|
StartMode::AcceptInvite { code } => {
|
||||||
|
log::info!("Accept play invite {}", code);
|
||||||
|
self.dispatcher
|
||||||
|
.do_send(AcceptInvite(code.clone(), ctx.address()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,3 +231,27 @@ impl Handler<ServerMessage> for HumanPlayerWS {
|
|||||||
ctx.text(serde_json::to_string(&msg).unwrap());
|
ctx.text(serde_json::to_string(&msg).unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Handler<SetGame> for HumanPlayerWS {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: SetGame, ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
let game = msg.0;
|
||||||
|
let player = Arc::new(HumanPlayer {
|
||||||
|
name: self.name.clone(),
|
||||||
|
game: game.clone(),
|
||||||
|
player: ctx.address(),
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
|
});
|
||||||
|
self.inner = Some(player.clone());
|
||||||
|
game.do_send(AddPlayer(player));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<CloseConnection> for HumanPlayerWS {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, _msg: CloseConnection, ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
ctx.close(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
extern crate core;
|
extern crate core;
|
||||||
|
|
||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod bots;
|
pub mod bot_player;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
|
pub mod dispatcher_actor;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
pub mod human_player;
|
pub mod human_player;
|
||||||
pub mod human_player_ws;
|
pub mod human_player_ws;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
pub mod utils;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
use actix::{Actor, Addr};
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder};
|
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder};
|
||||||
use actix_web_actors::ws;
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::data::{GameRules, PlayConfiguration};
|
use crate::data::{GameRules, PlayConfiguration};
|
||||||
|
use crate::dispatcher_actor::DispatcherActor;
|
||||||
use crate::human_player_ws::{HumanPlayerWS, StartMode};
|
use crate::human_player_ws::{HumanPlayerWS, StartMode};
|
||||||
|
|
||||||
/// The default '/' route
|
/// The default '/' route
|
||||||
@@ -26,23 +28,78 @@ async fn start_bot_play(
|
|||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
stream: web::Payload,
|
stream: web::Payload,
|
||||||
query: web::Query<GameRules>,
|
query: web::Query<GameRules>,
|
||||||
|
dispatcher: web::Data<Addr<DispatcherActor>>,
|
||||||
) -> Result<HttpResponse, actix_web::Error> {
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
let errors = query.0.get_errors();
|
let errors = query.0.get_errors();
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
return Ok(HttpResponse::BadRequest().json(errors));
|
return Ok(HttpResponse::BadRequest().json(errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut player_ws = HumanPlayerWS::default();
|
let player_ws = HumanPlayerWS::new(
|
||||||
player_ws.start_mode = StartMode::Bot(query.0.clone());
|
StartMode::Bot(query.0.clone()),
|
||||||
|
&dispatcher,
|
||||||
|
"Human".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
let resp = ws::start(player_ws, &req, stream);
|
let resp = ws::start(player_ws, &req, stream);
|
||||||
log::info!("New bot play with configuration: {:?}", &query.0);
|
log::info!("New bot play with configuration: {:?}", &query.0);
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start game by creating invite
|
||||||
|
async fn start_create_invite(
|
||||||
|
req: HttpRequest,
|
||||||
|
stream: web::Payload,
|
||||||
|
query: web::Query<GameRules>, // TODO : add player name to query
|
||||||
|
dispatcher: web::Data<Addr<DispatcherActor>>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let errors = query.0.get_errors();
|
||||||
|
if !errors.is_empty() {
|
||||||
|
return Ok(HttpResponse::BadRequest().json(errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
let player_ws = HumanPlayerWS::new(
|
||||||
|
StartMode::CreateInvite(query.0.clone()),
|
||||||
|
&dispatcher,
|
||||||
|
"Human".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = ws::start(player_ws, &req, stream);
|
||||||
|
log::info!("New create invite play with configuration: {:?}", &query.0);
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct AcceptInviteQuery {
|
||||||
|
pub code: String,
|
||||||
|
// TODO : add player name to query
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start game by creating invite
|
||||||
|
async fn start_accept_invite(
|
||||||
|
req: HttpRequest,
|
||||||
|
stream: web::Payload,
|
||||||
|
query: web::Query<AcceptInviteQuery>,
|
||||||
|
dispatcher: web::Data<Addr<DispatcherActor>>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let player_ws = HumanPlayerWS::new(
|
||||||
|
StartMode::AcceptInvite {
|
||||||
|
code: query.code.clone(),
|
||||||
|
},
|
||||||
|
&dispatcher,
|
||||||
|
"Human".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = ws::start(player_ws, &req, stream);
|
||||||
|
log::info!("New accept invite: {:?}", &query.0.code);
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start_server(args: Args) -> std::io::Result<()> {
|
pub async fn start_server(args: Args) -> std::io::Result<()> {
|
||||||
let args_clone = args.clone();
|
let args_clone = args.clone();
|
||||||
|
|
||||||
|
let dispatcher_actor = DispatcherActor::default().start();
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let mut cors = Cors::default();
|
let mut cors = Cors::default();
|
||||||
match args_clone.cors.as_deref() {
|
match args_clone.cors.as_deref() {
|
||||||
@@ -52,10 +109,12 @@ pub async fn start_server(args: Args) -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
|
.app_data(web::Data::new(dispatcher_actor.clone()))
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.route("/config", web::get().to(game_configuration))
|
.route("/config", web::get().to(game_configuration))
|
||||||
.route("/play/bot", web::get().to(start_bot_play))
|
.route("/play/bot", web::get().to(start_bot_play))
|
||||||
// TODO : create & accept invite
|
.route("/play/create_invite", web::get().to(start_create_invite))
|
||||||
|
.route("/play/accept_invite", web::get().to(start_accept_invite))
|
||||||
// TODO : join random
|
// TODO : join random
|
||||||
.route("/", web::get().to(index))
|
.route("/", web::get().to(index))
|
||||||
.route("{tail:.*}", web::get().to(not_found))
|
.route("{tail:.*}", web::get().to(not_found))
|
||||||
|
|||||||
@@ -4,23 +4,40 @@ use std::fmt::Display;
|
|||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
|
||||||
use crate::data::{BoatsLayout, GameRules};
|
use crate::data::{BoatsLayout, BotType, GameRules};
|
||||||
use crate::human_player_ws::{ClientMessage, ServerMessage};
|
use crate::human_player_ws::{ClientMessage, ServerMessage};
|
||||||
|
use crate::server::AcceptInviteQuery;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum RunMode {
|
||||||
|
#[default]
|
||||||
|
AgainstBot,
|
||||||
|
CreateInvite,
|
||||||
|
AcceptInvite {
|
||||||
|
code: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum ClientEndResult {
|
pub enum ClientEndResult {
|
||||||
Finished,
|
Finished {
|
||||||
|
number_victories: usize,
|
||||||
|
number_defeats: usize,
|
||||||
|
},
|
||||||
InvalidBoatsLayout,
|
InvalidBoatsLayout,
|
||||||
OpponentRejectedRematch,
|
OpponentRejectedRematch,
|
||||||
OpponentLeftGame,
|
OpponentLeftGame,
|
||||||
|
InvalidInviteCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BotClient {
|
pub struct BotClient {
|
||||||
server: String,
|
server: String,
|
||||||
|
run_mode: RunMode,
|
||||||
requested_rules: GameRules,
|
requested_rules: GameRules,
|
||||||
layout: Option<BoatsLayout>,
|
layout: Option<BoatsLayout>,
|
||||||
number_plays: usize,
|
number_plays: usize,
|
||||||
server_msg_callback: Option<Box<dyn FnMut(&ServerMessage)>>,
|
server_msg_callback: Option<Box<dyn FnMut(&ServerMessage)>>,
|
||||||
|
play_as_bot_type: BotType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotClient {
|
impl BotClient {
|
||||||
@@ -30,13 +47,20 @@ impl BotClient {
|
|||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
server: server.to_string(),
|
server: server.to_string(),
|
||||||
|
run_mode: RunMode::default(),
|
||||||
requested_rules: GameRules::random_players_rules(),
|
requested_rules: GameRules::random_players_rules(),
|
||||||
layout: None,
|
layout: None,
|
||||||
number_plays: 1,
|
number_plays: 1,
|
||||||
server_msg_callback: None,
|
server_msg_callback: None,
|
||||||
|
play_as_bot_type: BotType::Random,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_run_mode(mut self, mode: RunMode) -> Self {
|
||||||
|
self.run_mode = mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_rules(mut self, rules: GameRules) -> Self {
|
pub fn with_rules(mut self, rules: GameRules) -> Self {
|
||||||
self.requested_rules = rules;
|
self.requested_rules = rules;
|
||||||
self
|
self
|
||||||
@@ -60,14 +84,42 @@ impl BotClient {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_play_as_bot_type(mut self, t: BotType) -> Self {
|
||||||
|
self.play_as_bot_type = t;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_client(&mut self) -> Result<ClientEndResult, Box<dyn Error>> {
|
pub async fn run_client(&mut self) -> Result<ClientEndResult, Box<dyn Error>> {
|
||||||
let mut remaining_games = self.number_plays;
|
let mut remaining_games = self.number_plays;
|
||||||
|
let mut number_victories = 0;
|
||||||
|
let mut number_defeats = 0;
|
||||||
|
|
||||||
let url = format!(
|
let url = match &self.run_mode {
|
||||||
|
RunMode::AgainstBot => {
|
||||||
|
format!(
|
||||||
"{}/play/bot?{}",
|
"{}/play/bot?{}",
|
||||||
self.server.replace("http", "ws"),
|
self.server.replace("http", "ws"),
|
||||||
serde_urlencoded::to_string(&self.requested_rules).unwrap()
|
serde_urlencoded::to_string(&self.requested_rules).unwrap()
|
||||||
);
|
)
|
||||||
|
}
|
||||||
|
RunMode::CreateInvite => {
|
||||||
|
format!(
|
||||||
|
"{}/play/create_invite?{}",
|
||||||
|
self.server.replace("http", "ws"),
|
||||||
|
serde_urlencoded::to_string(&self.requested_rules).unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RunMode::AcceptInvite { code } => {
|
||||||
|
format!(
|
||||||
|
"{}/play/accept_invite?{}",
|
||||||
|
self.server.replace("http", "ws"),
|
||||||
|
serde_urlencoded::to_string(&AcceptInviteQuery {
|
||||||
|
code: code.to_string()
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
log::debug!("Connecting to {}...", url);
|
log::debug!("Connecting to {}...", url);
|
||||||
let (mut socket, _) = match tokio_tungstenite::connect_async(url).await {
|
let (mut socket, _) = match tokio_tungstenite::connect_async(url).await {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
@@ -115,6 +167,13 @@ impl BotClient {
|
|||||||
ServerMessage::WaitingForAnotherPlayer => {
|
ServerMessage::WaitingForAnotherPlayer => {
|
||||||
log::debug!("Waiting for other player...")
|
log::debug!("Waiting for other player...")
|
||||||
}
|
}
|
||||||
|
ServerMessage::SetInviteCode { code } => {
|
||||||
|
log::debug!("Got invite code: {}", code);
|
||||||
|
}
|
||||||
|
ServerMessage::InvalidInviteCode => {
|
||||||
|
log::debug!("Got invalid invite code!");
|
||||||
|
return Ok(ClientEndResult::InvalidInviteCode);
|
||||||
|
}
|
||||||
ServerMessage::QueryBoatsLayout { rules } => {
|
ServerMessage::QueryBoatsLayout { rules } => {
|
||||||
assert_eq!(&rules, &self.requested_rules);
|
assert_eq!(&rules, &self.requested_rules);
|
||||||
log::debug!("Server requested boats layout");
|
log::debug!("Server requested boats layout");
|
||||||
@@ -144,7 +203,7 @@ impl BotClient {
|
|||||||
ServerMessage::RequestFire { status } => {
|
ServerMessage::RequestFire { status } => {
|
||||||
assert_eq!(status.opponent_map.boats.number_of_boats(), 0);
|
assert_eq!(status.opponent_map.boats.number_of_boats(), 0);
|
||||||
|
|
||||||
let location = status.find_valid_random_fire_location();
|
let location = status.find_fire_coordinates_for_bot_type(self.play_as_bot_type);
|
||||||
log::debug!("Will fire at {:?}", location);
|
log::debug!("Will fire at {:?}", location);
|
||||||
socket
|
socket
|
||||||
.send(Message::Text(serde_json::to_string(
|
.send(Message::Text(serde_json::to_string(
|
||||||
@@ -161,6 +220,8 @@ impl BotClient {
|
|||||||
result
|
result
|
||||||
),
|
),
|
||||||
ServerMessage::LostGame { status } => {
|
ServerMessage::LostGame { status } => {
|
||||||
|
number_defeats += 1;
|
||||||
|
|
||||||
log::debug!("We lost game :(");
|
log::debug!("We lost game :(");
|
||||||
log::debug!("Opponent map:\n{}", status.get_opponent_map());
|
log::debug!("Opponent map:\n{}", status.get_opponent_map());
|
||||||
log::debug!("Our map:\n{}\n", status.get_your_map());
|
log::debug!("Our map:\n{}\n", status.get_your_map());
|
||||||
@@ -176,6 +237,8 @@ impl BotClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ServerMessage::WonGame { status } => {
|
ServerMessage::WonGame { status } => {
|
||||||
|
number_victories += 1;
|
||||||
|
|
||||||
log::debug!("We won the game !!!!");
|
log::debug!("We won the game !!!!");
|
||||||
log::debug!("Opponent map:\n{}\n", status.get_opponent_map());
|
log::debug!("Opponent map:\n{}\n", status.get_opponent_map());
|
||||||
log::debug!("Our map:\n{}\n", status.get_your_map());
|
log::debug!("Our map:\n{}\n", status.get_your_map());
|
||||||
@@ -197,7 +260,12 @@ impl BotClient {
|
|||||||
ServerMessage::SetOpponentName { name } => log::debug!("Opponent name: {}", name),
|
ServerMessage::SetOpponentName { name } => log::debug!("Opponent name: {}", name),
|
||||||
|
|
||||||
ServerMessage::OpponentRequestedRematch => {
|
ServerMessage::OpponentRequestedRematch => {
|
||||||
log::debug!("Opponent rejected rematch.");
|
log::debug!("Opponent requested rematch.");
|
||||||
|
socket
|
||||||
|
.send(Message::Text(serde_json::to_string(
|
||||||
|
&ClientMessage::AcceptRematch,
|
||||||
|
)?))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
ServerMessage::OpponentAcceptedRematch => {
|
ServerMessage::OpponentAcceptedRematch => {
|
||||||
log::debug!("Opponent accepted rematch");
|
log::debug!("Opponent accepted rematch");
|
||||||
@@ -216,6 +284,9 @@ impl BotClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ClientEndResult::Finished)
|
Ok(ClientEndResult::Finished {
|
||||||
|
number_victories,
|
||||||
|
number_defeats,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ async fn full_game() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ async fn full_game() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ async fn full_game_no_replay_on_hit() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ async fn full_game_no_replay_on_hit_two() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ async fn full_game_with_replay_on_hit() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ async fn full_game() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ async fn full_game_no_touching_boats() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ async fn full_game_multiple_rematches() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ async fn full_game_no_replay_on_hit() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
96
sea_battle_backend/src/test/bot_client_bot_smart_play.rs
Normal file
96
sea_battle_backend/src/test/bot_client_bot_smart_play.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use tokio::task;
|
||||||
|
|
||||||
|
use crate::args::Args;
|
||||||
|
use crate::data::{BotType, CurrentGameStatus, GameRules};
|
||||||
|
use crate::human_player_ws::ServerMessage;
|
||||||
|
use crate::server::start_server;
|
||||||
|
use crate::test::bot_client::ClientEndResult;
|
||||||
|
use crate::test::network_utils::wait_for_port;
|
||||||
|
use crate::test::{bot_client, TestPort};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn full_game() {
|
||||||
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
|
||||||
|
let local_set = task::LocalSet::new();
|
||||||
|
local_set
|
||||||
|
.run_until(async move {
|
||||||
|
task::spawn_local(start_server(Args::for_test(
|
||||||
|
TestPort::IntermediateBotFullGame,
|
||||||
|
)));
|
||||||
|
wait_for_port(TestPort::IntermediateBotFullGame.port()).await;
|
||||||
|
|
||||||
|
let mut curr_status = CurrentGameStatus::default();
|
||||||
|
|
||||||
|
let res = bot_client::BotClient::new(TestPort::IntermediateBotFullGame.as_url())
|
||||||
|
.with_rules(GameRules::random_players_rules().with_bot_type(BotType::Smart))
|
||||||
|
.with_server_msg_callback(move |msg| match msg {
|
||||||
|
ServerMessage::OpponentMustFire { status } => {
|
||||||
|
curr_status = status.clone();
|
||||||
|
}
|
||||||
|
ServerMessage::OpponentFireResult { pos, .. } => {
|
||||||
|
let pending_sunk_loc =
|
||||||
|
curr_status.your_map.get_successful_but_un_sunk_locations();
|
||||||
|
|
||||||
|
if !pending_sunk_loc.is_empty() {
|
||||||
|
log::debug!("Check if fire was smart...");
|
||||||
|
assert!(pending_sunk_loc.iter().any(|l| pos.dist_with(l) <= 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.run_client()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn full_game_multiple_rematches() {
|
||||||
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
|
||||||
|
let local_set = task::LocalSet::new();
|
||||||
|
local_set
|
||||||
|
.run_until(async move {
|
||||||
|
task::spawn_local(start_server(Args::for_test(
|
||||||
|
TestPort::IntermediateBotFullGame,
|
||||||
|
)));
|
||||||
|
wait_for_port(TestPort::IntermediateBotFullGame.port()).await;
|
||||||
|
|
||||||
|
let mut curr_status = CurrentGameStatus::default();
|
||||||
|
|
||||||
|
let res = bot_client::BotClient::new(TestPort::IntermediateBotFullGame.as_url())
|
||||||
|
.with_rules(GameRules::random_players_rules().with_bot_type(BotType::Smart))
|
||||||
|
.with_server_msg_callback(move |msg| match msg {
|
||||||
|
ServerMessage::OpponentMustFire { status } => {
|
||||||
|
curr_status = status.clone();
|
||||||
|
}
|
||||||
|
ServerMessage::OpponentFireResult { pos, .. } => {
|
||||||
|
let pending_sunk_loc =
|
||||||
|
curr_status.your_map.get_successful_but_un_sunk_locations();
|
||||||
|
|
||||||
|
if !pending_sunk_loc.is_empty() {
|
||||||
|
log::debug!("Check if fire was smart...");
|
||||||
|
assert!(pending_sunk_loc.iter().any(|l| pos.dist_with(l) <= 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.with_number_plays(20)
|
||||||
|
.run_client()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if let ClientEndResult::Finished { number_defeats, .. } = res {
|
||||||
|
assert!(
|
||||||
|
number_defeats > 15,
|
||||||
|
"number of defeats = {} which is < 15",
|
||||||
|
number_defeats
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
assert_eq!(0, 1, "Client did not finish correctly");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
215
sea_battle_backend/src/test/invite_mode.rs
Normal file
215
sea_battle_backend/src/test/invite_mode.rs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
use tokio::task;
|
||||||
|
|
||||||
|
use crate::args::Args;
|
||||||
|
use crate::data::BotType;
|
||||||
|
use crate::human_player_ws::ServerMessage;
|
||||||
|
use crate::server::start_server;
|
||||||
|
use crate::test::bot_client::{ClientEndResult, RunMode};
|
||||||
|
use crate::test::network_utils::wait_for_port;
|
||||||
|
use crate::test::{bot_client, TestPort};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn invalid_accept_code() {
|
||||||
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
|
||||||
|
let local_set = task::LocalSet::new();
|
||||||
|
local_set
|
||||||
|
.run_until(async move {
|
||||||
|
task::spawn_local(start_server(Args::for_test(
|
||||||
|
TestPort::InviteModeInvalidCode,
|
||||||
|
)));
|
||||||
|
wait_for_port(TestPort::InviteModeInvalidCode.port()).await;
|
||||||
|
|
||||||
|
let res = bot_client::BotClient::new(TestPort::InviteModeInvalidCode.as_url())
|
||||||
|
.with_run_mode(RunMode::AcceptInvite {
|
||||||
|
code: "BadCode".to_string(),
|
||||||
|
})
|
||||||
|
.run_client()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(res, ClientEndResult::InvalidInviteCode)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_other_invite_side(
|
||||||
|
sender: Sender<Result<ClientEndResult, Box<dyn Error>>>,
|
||||||
|
port: TestPort,
|
||||||
|
code: String,
|
||||||
|
play_as_bot_type: BotType,
|
||||||
|
number_plays: usize,
|
||||||
|
) {
|
||||||
|
let res = bot_client::BotClient::new(port.as_url())
|
||||||
|
.with_run_mode(RunMode::AcceptInvite { code })
|
||||||
|
.with_play_as_bot_type(play_as_bot_type)
|
||||||
|
.with_number_plays(number_plays)
|
||||||
|
.run_client()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
sender.send(res).await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn full_game() {
|
||||||
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
|
||||||
|
let local_set = task::LocalSet::new();
|
||||||
|
local_set
|
||||||
|
.run_until(async move {
|
||||||
|
task::spawn_local(start_server(Args::for_test(TestPort::InviteModeFullGame)));
|
||||||
|
wait_for_port(TestPort::InviteModeFullGame.port()).await;
|
||||||
|
|
||||||
|
let (sender, mut receiver) = mpsc::channel(1);
|
||||||
|
|
||||||
|
let res = bot_client::BotClient::new(TestPort::InviteModeFullGame.as_url())
|
||||||
|
.with_run_mode(RunMode::CreateInvite)
|
||||||
|
.with_server_msg_callback(move |msg| {
|
||||||
|
if let ServerMessage::SetInviteCode { code } = msg {
|
||||||
|
task::spawn_local(run_other_invite_side(
|
||||||
|
sender.clone(),
|
||||||
|
TestPort::InviteModeFullGame,
|
||||||
|
code.clone(),
|
||||||
|
BotType::Random,
|
||||||
|
1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.run_client()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
|
let other_side_res = receiver.recv().await.unwrap().unwrap();
|
||||||
|
assert!(matches!(other_side_res, ClientEndResult::Finished { .. }));
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn first_player_win() {
|
||||||
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
|
||||||
|
let local_set = task::LocalSet::new();
|
||||||
|
local_set
|
||||||
|
.run_until(async move {
|
||||||
|
task::spawn_local(start_server(Args::for_test(
|
||||||
|
TestPort::InviteModeFirstPlayerWin,
|
||||||
|
)));
|
||||||
|
wait_for_port(TestPort::InviteModeFirstPlayerWin.port()).await;
|
||||||
|
|
||||||
|
let (sender, mut receiver) = mpsc::channel(1);
|
||||||
|
|
||||||
|
let res = bot_client::BotClient::new(TestPort::InviteModeFirstPlayerWin.as_url())
|
||||||
|
.with_run_mode(RunMode::CreateInvite)
|
||||||
|
.with_play_as_bot_type(BotType::Smart)
|
||||||
|
.with_number_plays(3)
|
||||||
|
.with_server_msg_callback(move |msg| {
|
||||||
|
if let ServerMessage::SetInviteCode { code } = msg {
|
||||||
|
task::spawn_local(run_other_invite_side(
|
||||||
|
sender.clone(),
|
||||||
|
TestPort::InviteModeFirstPlayerWin,
|
||||||
|
code.clone(),
|
||||||
|
BotType::Linear,
|
||||||
|
3,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.run_client()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let other_side_res = receiver.recv().await.unwrap().unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
|
|
||||||
|
assert!(matches!(other_side_res, ClientEndResult::Finished { .. }));
|
||||||
|
|
||||||
|
match (res, other_side_res) {
|
||||||
|
(
|
||||||
|
ClientEndResult::Finished {
|
||||||
|
number_defeats: d1,
|
||||||
|
number_victories: v1,
|
||||||
|
},
|
||||||
|
ClientEndResult::Finished {
|
||||||
|
number_defeats: d2,
|
||||||
|
number_victories: v2,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
assert_eq!(d1, v2);
|
||||||
|
assert_eq!(v1, d2);
|
||||||
|
|
||||||
|
assert!(v1 > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
(_, _) => unreachable!(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn second_player_win() {
|
||||||
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
|
||||||
|
let local_set = task::LocalSet::new();
|
||||||
|
local_set
|
||||||
|
.run_until(async move {
|
||||||
|
task::spawn_local(start_server(Args::for_test(
|
||||||
|
TestPort::InviteModeSecondPlayerWin,
|
||||||
|
)));
|
||||||
|
wait_for_port(TestPort::InviteModeSecondPlayerWin.port()).await;
|
||||||
|
|
||||||
|
let (sender, mut receiver) = mpsc::channel(1);
|
||||||
|
|
||||||
|
let res = bot_client::BotClient::new(TestPort::InviteModeSecondPlayerWin.as_url())
|
||||||
|
.with_run_mode(RunMode::CreateInvite)
|
||||||
|
.with_play_as_bot_type(BotType::Linear)
|
||||||
|
.with_number_plays(3)
|
||||||
|
.with_server_msg_callback(move |msg| {
|
||||||
|
if let ServerMessage::SetInviteCode { code } = msg {
|
||||||
|
task::spawn_local(run_other_invite_side(
|
||||||
|
sender.clone(),
|
||||||
|
TestPort::InviteModeSecondPlayerWin,
|
||||||
|
code.clone(),
|
||||||
|
BotType::Smart,
|
||||||
|
3,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.run_client()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let other_side_res = receiver.recv().await.unwrap().unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
|
|
||||||
|
assert!(matches!(other_side_res, ClientEndResult::Finished { .. }));
|
||||||
|
|
||||||
|
match (res, other_side_res) {
|
||||||
|
(
|
||||||
|
ClientEndResult::Finished {
|
||||||
|
number_defeats: d1,
|
||||||
|
number_victories: v1,
|
||||||
|
},
|
||||||
|
ClientEndResult::Finished {
|
||||||
|
number_defeats: d2,
|
||||||
|
number_victories: v2,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
assert_eq!(d1, v2);
|
||||||
|
assert_eq!(v1, d2);
|
||||||
|
|
||||||
|
assert!(v2 > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
(_, _) => unreachable!(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@ enum TestPort {
|
|||||||
LinearBotFullGame,
|
LinearBotFullGame,
|
||||||
LinearBotNoReplayOnHit,
|
LinearBotNoReplayOnHit,
|
||||||
IntermediateBotFullGame,
|
IntermediateBotFullGame,
|
||||||
|
InviteModeInvalidCode,
|
||||||
|
InviteModeFullGame,
|
||||||
|
InviteModeFirstPlayerWin,
|
||||||
|
InviteModeSecondPlayerWin,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestPort {
|
impl TestPort {
|
||||||
@@ -39,5 +43,7 @@ pub mod bot_client;
|
|||||||
mod bot_client_bot_intermediate_play;
|
mod bot_client_bot_intermediate_play;
|
||||||
mod bot_client_bot_linear_play;
|
mod bot_client_bot_linear_play;
|
||||||
mod bot_client_bot_random_play;
|
mod bot_client_bot_random_play;
|
||||||
|
mod bot_client_bot_smart_play;
|
||||||
|
mod invite_mode;
|
||||||
mod network_utils;
|
mod network_utils;
|
||||||
mod play_utils;
|
mod play_utils;
|
||||||
|
|||||||
23
sea_battle_backend/src/utils.rs
Normal file
23
sea_battle_backend/src/utils.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use rand::distributions::Alphanumeric;
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
|
/// Generate a random string of a given size
|
||||||
|
pub fn rand_str(len: usize) -> String {
|
||||||
|
thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.map(char::from)
|
||||||
|
.take(len)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::utils::rand_str;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rand_str() {
|
||||||
|
let size = 10;
|
||||||
|
let rand = rand_str(size);
|
||||||
|
assert_eq!(size, rand.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user