Fix .git location

This commit is contained in:
2022-09-14 18:34:10 +02:00
parent a0eae4c93d
commit 5b7a56d060
22 changed files with 0 additions and 0 deletions

2
sea_battle_backend/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.idea

1552
sea_battle_backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
[package]
name = "sea_battle_backend"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "3.2.17", features = ["derive"] }
log = "0.4.17"
env_logger = "0.9.0"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.85"
actix-web = "4.1.0"
actix-cors = "0.6.2"
actix = "0.13.0"
actix-web-actors = "4.1.0"
actix-rt = "2.7.0"
uuid = { version = "1.1.2", features = ["v4"] }
rand = "0.8.5"
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
#reqwest = { version = "0.11.11", default-features = false, features = ["json", "rustls-tls"] }
tokio-tungstenite = "0.17.2"
serde_urlencoded = "0.7.1"
futures = "0.3.23"

View File

@ -0,0 +1,14 @@
use clap::Parser;
/// Simple sea battle server
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
/// The address this server will listen to
#[clap(short, long, value_parser, default_value = "0.0.0.0:7000")]
pub listen_address: String,
/// CORS (allowed origin) set to '*' to allow all origins
#[clap(short, long, value_parser)]
pub cors: Option<String>,
}

View File

@ -0,0 +1,84 @@
use std::error::Error;
use futures::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::Message;
use crate::data::{BoatsLayout, GameRules};
use crate::human_player_ws::{ClientMessage, ServerMessage};
pub async fn run_client(server: &str, rules: &GameRules) -> Result<(), Box<dyn Error>> {
let url = format!(
"{}/play/bot?{}",
server.replace("http", "ws"),
serde_urlencoded::to_string(rules).unwrap()
);
log::debug!("Connecting to {}...", url);
let (mut socket, _) = match tokio_tungstenite::connect_async(url).await {
Ok(s) => s,
Err(e) => {
log::error!("Failed to establish WebSocket connection! {:?}", e);
return Err(Box::new(e));
}
};
while let Some(chunk) = socket.next().await {
let message = match chunk? {
Message::Text(message) => {
log::debug!("TEXT message from server: {}", message);
let msg: ServerMessage = serde_json::from_str(&message)?;
msg
}
Message::Binary(_) => {
log::debug!("BINARY message from server");
continue;
}
Message::Ping(_) => {
log::debug!("PING from server");
continue;
}
Message::Pong(_) => {
log::debug!("PONG from server");
continue;
}
Message::Close(_) => {
log::debug!("CLOSE message request from server");
break;
}
Message::Frame(_) => {
log::debug!("Frame from server");
continue;
}
};
match message {
ServerMessage::WaitingForAnotherPlayer => log::debug!("Waiting for other player..."),
ServerMessage::QueryBoatsLayout { rules } => {
log::debug!("Server requested boats layout");
let layout = BoatsLayout::gen_random_for_rules(&rules)?;
socket
.send(Message::Text(serde_json::to_string(
&ClientMessage::BoatsLayout { layout },
)?))
.await?;
}
ServerMessage::WaitingForOtherPlayerConfiguration => {
log::debug!("Waiting for other player configuration...")
}
ServerMessage::OtherPlayerReady => log::debug!("Other player is ready!"),
ServerMessage::GameStarting => log::debug!("The game is starting..."),
ServerMessage::OtherPlayerMustFire { .. } => log::debug!("Other player must fire!"),
ServerMessage::RequestFire { status } => {
let location = status.find_valid_random_fire_location();
log::debug!("Will fire at {:?}", location);
socket
.send(Message::Text(serde_json::to_string(
&ClientMessage::Fire { location },
)?))
.await?;
}
}
}
Ok(())
}

View File

@ -0,0 +1,19 @@
//! # Project constants
pub const MIN_BOATS_NUMBER: usize = 1;
pub const MAX_BOATS_NUMBER: usize = 10;
pub const MIN_BOATS_LENGTH: usize = 1;
pub const MAX_BOATS_LENGTH: usize = 6;
pub const MIN_MAP_WIDTH: usize = 5;
pub const MAX_MAP_WIDTH: usize = 26;
pub const MIN_MAP_HEIGHT: usize = 5;
pub const MAX_MAP_HEIGHT: usize = 26;
pub const MULTI_PLAYER_MAP_WIDTH: usize = 10;
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];

View File

@ -0,0 +1,399 @@
use std::io::ErrorKind;
use rand::{Rng, RngCore};
use crate::data::GameRules;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)]
pub enum BoatDirection {
Left,
Right,
Up,
Down,
UpLeft,
UpRight,
BottomLeft,
BottomRight,
}
const _BOATS_DIRECTION: [BoatDirection; 4] = [
BoatDirection::Left,
BoatDirection::Right,
BoatDirection::Up,
BoatDirection::Down,
];
const _ALL_DIRECTIONS: [BoatDirection; 8] = [
BoatDirection::Left,
BoatDirection::Right,
BoatDirection::Up,
BoatDirection::Down,
BoatDirection::UpLeft,
BoatDirection::UpRight,
BoatDirection::BottomLeft,
BoatDirection::BottomRight,
];
impl BoatDirection {
pub fn boat_directions() -> &'static [BoatDirection] {
&_BOATS_DIRECTION
}
pub fn all_directions() -> &'static [BoatDirection] {
&_ALL_DIRECTIONS
}
pub fn is_valid_boat_direction(&self) -> bool {
matches!(
self,
BoatDirection::Left | BoatDirection::Right | BoatDirection::Up | BoatDirection::Down
)
}
pub fn shift_coordinates(&self, coordinates: Coordinates, nth: usize) -> Coordinates {
let shift = match self {
BoatDirection::Left => (1, 0),
BoatDirection::Right => (-1, 0),
BoatDirection::Up => (0, -1),
BoatDirection::Down => (0, 1),
BoatDirection::UpLeft => (-1, -1),
BoatDirection::UpRight => (1, -1),
BoatDirection::BottomLeft => (-1, 1),
BoatDirection::BottomRight => (1, 1),
};
Coordinates::new(
coordinates.x + shift.0 * nth as i32,
coordinates.y + shift.1 * nth as i32,
)
}
}
#[derive(
serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd,
)]
pub struct Coordinates {
y: i32,
x: i32,
}
impl Coordinates {
pub fn new<E>(x: E, y: E) -> Self
where
E: Into<i32>,
{
Self {
x: x.into(),
y: y.into(),
}
}
pub fn is_valid(&self, rules: &GameRules) -> bool {
self.x >= 0
&& self.y >= 0
&& self.x < rules.map_width as i32
&& self.y < rules.map_height as i32
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)]
pub struct BoatPosition {
start: Coordinates,
len: usize,
direction: BoatDirection,
}
impl BoatPosition {
/// Get all the coordinates occupied by the boat
pub fn all_coordinates(&self) -> Vec<Coordinates> {
let mut positions = Vec::with_capacity(self.len);
for i in 0..self.len {
positions.push(self.direction.shift_coordinates(self.start, i));
}
positions
}
/// Get all the coordinates around the boats
pub fn neighbor_coordinates(&self, rules: &GameRules) -> Vec<Coordinates> {
let boat_positions = self.all_coordinates();
let mut neighbors = Vec::with_capacity(self.len);
for i in 0..self.len {
let boat_point = self.direction.shift_coordinates(self.start, i);
for dir in BoatDirection::all_directions() {
let curr_point = dir.shift_coordinates(boat_point, 1);
if !neighbors.contains(&curr_point)
&& !boat_positions.contains(&curr_point)
&& curr_point.is_valid(rules)
{
neighbors.push(curr_point);
}
}
}
neighbors
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct BoatsLayout(Vec<BoatPosition>);
impl BoatsLayout {
/// Generate random boats layout for given game rules
pub fn gen_random_for_rules(rules: &GameRules) -> std::io::Result<Self> {
let mut boats = Self(Vec::with_capacity(rules.boats_list().len()));
let mut rng = rand::thread_rng();
for boat in rules.boats_list() {
let mut attempt = 0;
loop {
attempt += 1;
let directions = BoatDirection::boat_directions();
let position = BoatPosition {
start: Coordinates::new(
(rng.next_u32() % rules.map_width as u32) as i32,
(rng.next_u32() % rules.map_height as u32) as i32,
),
len: boat,
direction: directions[rng.gen::<usize>() % directions.len()],
};
if boats.is_acceptable_boat_position(&position, rules) {
boats.0.push(position);
break;
}
if attempt >= rules.map_width * rules.map_height * 4 {
return Err(std::io::Error::new(
ErrorKind::Other,
"Un-usable game rules!",
));
}
}
}
Ok(boats)
}
fn is_acceptable_boat_position(&self, pos: &BoatPosition, rules: &GameRules) -> bool {
// Check if boat coordinates are valid
if pos.all_coordinates().iter().any(|c| !c.is_valid(rules)) {
return false;
}
// Check if the boat would cross another boat
if pos
.all_coordinates()
.iter()
.any(|c| self.find_boat_at_position(*c).is_some())
{
return false;
}
// Check if the boat touch another boat in a configuration where is it forbidden
if !rules.boats_can_touch
&& pos
.neighbor_coordinates(rules)
.iter()
.any(|c| self.find_boat_at_position(*c).is_some())
{
return false;
}
true
}
pub fn errors(&self, rules: &GameRules) -> Vec<&str> {
let mut errors = vec![];
// Check the number of boats
if self.0.len() != rules.boats_list().len() {
errors.push("The number of boats is invalid!");
}
// Check the length of the boats
let mut len = self.0.iter().map(|l| l.len).collect::<Vec<_>>();
len.sort();
let mut boats = rules.boats_list();
boats.sort();
if len != boats {
errors.push("Boats lens mismatch!");
}
for boat_i in 0..self.0.len() {
// Check boat coordinates
let boat_i_coordinates = self.0[boat_i].all_coordinates();
if boat_i_coordinates.iter().any(|c| !c.is_valid(rules)) {
errors.push("A boat goes outside the game map!");
}
for boat_j in 0..self.0.len() {
if boat_i == boat_j {
continue;
}
let boat_j_coords = self.0[boat_j].all_coordinates();
if boat_i_coordinates.iter().any(|c| boat_j_coords.contains(c)) {
errors.push("A collision between two boats has been detected!");
}
if !rules.boats_can_touch
&& self.0[boat_i]
.neighbor_coordinates(rules)
.iter()
.any(|c| boat_j_coords.contains(c))
{
errors.push("A collision between two boats has been detected!");
}
}
}
errors
}
pub fn find_boat_at_position(&self, pos: Coordinates) -> Option<&BoatPosition> {
self.0.iter().find(|f| f.all_coordinates().contains(&pos))
}
}
#[cfg(test)]
mod test {
use crate::data::boats_layout::{BoatDirection, BoatPosition, BoatsLayout, Coordinates};
use crate::data::game_map::GameMap;
use crate::data::{BotType, GameRules, PlayConfiguration};
#[test]
fn get_boat_coordinates() {
let position = BoatPosition {
start: Coordinates { x: 1, y: 1 },
len: 3,
direction: BoatDirection::Down,
};
let coordinates = position.all_coordinates();
assert_eq!(
coordinates,
vec![
Coordinates::new(1, 1),
Coordinates::new(1, 2),
Coordinates::new(1, 3),
]
)
}
#[test]
fn get_boat_neighbor_coordinates() {
let rules = GameRules::random_players_rules();
let position = BoatPosition {
start: Coordinates { x: 0, y: 1 },
len: 3,
direction: BoatDirection::Down,
};
println!("{:?}", position.all_coordinates());
let mut coordinates = position.neighbor_coordinates(&rules);
coordinates.sort();
assert_eq!(
coordinates,
vec![
Coordinates::new(0, 0),
Coordinates::new(1, 0),
Coordinates::new(1, 1),
Coordinates::new(1, 2),
Coordinates::new(1, 3),
Coordinates::new(0, 4),
Coordinates::new(1, 4),
]
)
}
#[test]
fn generate_random_boats_layout() {
let rules = GameRules::random_players_rules();
let boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
assert!(boats.errors(&rules).is_empty());
let game = GameMap::new(rules, boats);
game.print_map();
}
#[test]
fn generate_random_boats_layout_without_touching_boats() {
let mut rules = GameRules::random_players_rules();
rules.boats_can_touch = false;
let boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
assert!(boats.errors(&rules).is_empty());
let game = GameMap::new(rules, boats);
game.print_map();
}
#[test]
fn impossible_map() {
let mut rules = GameRules::random_players_rules();
rules.map_height = PlayConfiguration::default().min_map_height;
rules.map_width = PlayConfiguration::default().min_map_width;
let mut boats = Vec::new();
for _i in 0..PlayConfiguration::default().max_boats_number {
boats.push(PlayConfiguration::default().max_boat_len);
}
rules.set_boats_list(&boats);
BoatsLayout::gen_random_for_rules(&rules).unwrap_err();
}
#[test]
fn empty_boats_layout() {
let rules = GameRules::random_players_rules();
let boats = BoatsLayout(vec![]);
assert!(!boats.errors(&rules).is_empty());
}
#[test]
fn invalid_number_of_boats() {
let rules = GameRules::random_players_rules();
let mut boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
boats.0.remove(0);
assert!(!boats.errors(&rules).is_empty());
}
#[test]
fn boats_crossing() {
let rules = GameRules::random_players_rules();
let mut boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
boats.0[0].start = boats.0[1].start;
assert!(!boats.errors(&rules).is_empty());
}
#[test]
fn boats_touching_in_forbidden_configuration() {
let rules = GameRules {
map_width: 5,
map_height: 5,
boats_str: "1,1".to_string(),
boats_can_touch: false,
player_continue_on_hit: false,
bot_type: BotType::Random,
};
let mut boats = BoatsLayout(vec![
BoatPosition {
start: Coordinates::new(0, 0),
len: 1,
direction: BoatDirection::Left,
},
BoatPosition {
start: Coordinates::new(0, 1),
len: 1,
direction: BoatDirection::Left,
},
]);
boats.0[0].start = boats.0[1].start;
assert!(!boats.errors(&rules).is_empty());
}
#[test]
fn boats_outside_map() {
let rules = GameRules::random_players_rules();
let mut boats = BoatsLayout::gen_random_for_rules(&rules).unwrap();
boats.0[0].start = Coordinates::new(-1, -1);
assert!(!boats.errors(&rules).is_empty());
}
}

View File

@ -0,0 +1,30 @@
use crate::data::{Coordinates, GameRules};
use rand::RngCore;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct CurrentGameStatus {
pub rules: GameRules,
// TODO
}
impl CurrentGameStatus {
/// Check if user can fire at a given location
pub fn can_fire_at_location(&self, _location: Coordinates) -> bool {
// TODO
true
}
/// Find valid random fire location. Loop until one is found
pub fn find_valid_random_fire_location(&self) -> Coordinates {
let mut rng = rand::thread_rng();
loop {
let coordinates = Coordinates::new(
(rng.next_u32() % self.rules.map_width as u32) as i32,
(rng.next_u32() % self.rules.map_height as u32) as i32,
);
if coordinates.is_valid(&self.rules) && self.can_fire_at_location(coordinates) {
return coordinates;
}
}
}
}

View File

@ -0,0 +1,59 @@
use crate::data::boats_layout::{BoatsLayout, Coordinates};
use crate::data::GameRules;
pub enum MapCellContent {
Invalid,
Nothing,
TouchedBoat,
Boat,
FailedStrike,
}
impl MapCellContent {
fn letter(&self) -> &'static str {
match self {
MapCellContent::Invalid => "!",
MapCellContent::Nothing => ".",
MapCellContent::TouchedBoat => "T",
MapCellContent::Boat => "B",
MapCellContent::FailedStrike => "X",
}
}
}
pub struct GameMap {
rules: GameRules,
boats_config: BoatsLayout,
}
impl GameMap {
pub fn new(rules: GameRules, boats_config: BoatsLayout) -> Self {
Self {
rules,
boats_config,
}
}
pub fn get_cell_content(&self, c: Coordinates) -> MapCellContent {
//TODO : improve this
if self.boats_config.find_boat_at_position(c).is_some() {
return MapCellContent::Boat;
}
MapCellContent::Nothing
}
pub fn print_map(&self) {
for y in 0..self.rules.map_height {
for x in 0..self.rules.map_width {
print!(
"{} ",
self.get_cell_content(Coordinates::new(x as i32, y as i32))
.letter()
);
}
println!();
}
}
}

View File

@ -0,0 +1,84 @@
use crate::consts::*;
use crate::data::{BotType, PlayConfiguration};
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct GameRules {
pub map_width: usize,
pub map_height: usize,
pub boats_str: String,
pub boats_can_touch: bool,
pub player_continue_on_hit: bool,
pub bot_type: BotType,
}
impl GameRules {
pub fn random_players_rules() -> Self {
Self {
map_width: MULTI_PLAYER_MAP_WIDTH,
map_height: MULTI_PLAYER_MAP_HEIGHT,
boats_str: MULTI_PLAYER_PLAYER_BOATS
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(","),
boats_can_touch: MULTI_PLAYER_BOATS_CAN_TOUCH,
player_continue_on_hit: MULTI_PLAYER_PLAYER_CAN_CONTINUE_AFTER_HIT,
bot_type: BotType::Random,
}
}
/// Set the list of boats for this configuration
pub fn set_boats_list(&mut self, boats: &[usize]) {
self.boats_str = boats
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(",");
}
/// Get the list of boats for this configuration
pub fn boats_list(&self) -> Vec<usize> {
self.boats_str
.split(',')
.map(|s| s.parse::<usize>().unwrap_or_default())
.collect()
}
pub fn get_errors(&self) -> Vec<&str> {
let config = PlayConfiguration::default();
let mut errors = vec![];
if self.map_width < config.min_map_width || self.map_width > config.max_map_width {
errors.push("Map width is outside bounds!");
}
if self.map_height < config.min_map_height || self.map_height > config.max_map_height {
errors.push("Map height is outside bounds!");
}
if self.boats_list().len() < config.min_boats_number
|| self.boats_list().len() > config.max_boats_number
{
errors.push("Number of boats is invalid!");
}
for boat in self.boats_list() {
if boat < config.min_boat_len || boat > config.max_boat_len {
errors.push("A boat has an invalid length");
}
}
errors
}
}
#[cfg(test)]
mod test {
use crate::data::GameRules;
#[test]
fn multi_players_config() {
assert!(GameRules::random_players_rules().get_errors().is_empty());
}
}

View File

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

View File

@ -0,0 +1,45 @@
use crate::consts::*;
/// Specifies the kind of boat to use
#[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)]
pub enum BotType {
Random,
}
#[derive(serde::Serialize)]
pub struct BotDescription {
r#type: BotType,
description: String,
}
#[derive(serde::Serialize)]
pub struct PlayConfiguration {
pub min_boat_len: usize,
pub max_boat_len: usize,
pub min_map_width: usize,
pub max_map_width: usize,
pub min_map_height: usize,
pub max_map_height: usize,
pub min_boats_number: usize,
pub max_boats_number: usize,
pub bot_types: Vec<BotDescription>,
}
impl Default for PlayConfiguration {
fn default() -> Self {
Self {
min_boat_len: MIN_BOATS_LENGTH,
max_boat_len: MAX_BOATS_LENGTH,
min_map_width: MIN_MAP_WIDTH,
max_map_width: MAX_MAP_WIDTH,
min_map_height: MIN_MAP_HEIGHT,
max_map_height: MAX_MAP_HEIGHT,
min_boats_number: MIN_BOATS_NUMBER,
max_boats_number: MAX_BOATS_NUMBER,
bot_types: vec![BotDescription {
r#type: BotType::Random,
description: "Random strike. All the time.".to_string(),
}],
}
}
}

View File

@ -0,0 +1,169 @@
use std::sync::Arc;
use actix::prelude::*;
use actix::{Actor, Context, Handler};
use uuid::Uuid;
use crate::data::*;
pub trait Player {
fn get_name(&self) -> &str;
fn get_uid(&self) -> Uuid;
fn query_boats_layout(&self, rules: &GameRules);
fn notify_other_player_ready(&self);
fn notify_game_starting(&self);
fn request_fire(&self, status: CurrentGameStatus);
fn other_player_must_fire(&self, status: CurrentGameStatus);
}
fn opponent(index: usize) -> usize {
match index {
0 => 1,
1 => 0,
_ => unreachable!(),
}
}
#[derive(Default, Eq, PartialEq, Debug, Copy, Clone)]
enum GameStatus {
#[default]
Created,
WaitingForBoatsDisposition,
Started,
}
pub struct Game {
rules: GameRules,
players: Vec<Arc<dyn Player>>,
status: GameStatus,
map_0: Option<GameMap>,
map_1: Option<GameMap>,
turn: usize,
}
impl Game {
pub fn new(rules: GameRules) -> Self {
Self {
rules,
players: vec![],
status: GameStatus::Created,
map_0: None,
map_1: None,
turn: 0,
}
}
/// Find the ID of a player from its UUID
fn player_id_by_uuid(&self, uuid: Uuid) -> usize {
self.players
.iter()
.enumerate()
.find(|p| p.1.get_uid() == uuid)
.expect("Player is not member of this game!")
.0
}
/// Once the two player has been registered, the game may start
fn query_boats_disposition(&mut self) {
log::debug!("Query boats disposition");
assert_eq!(self.status, GameStatus::Created);
self.status = GameStatus::WaitingForBoatsDisposition;
self.players[0].query_boats_layout(&self.rules);
self.players[1].query_boats_layout(&self.rules);
}
/// Start fires exchange
fn start_fire_exchanges(&mut self) {
self.status = GameStatus::Started;
log::debug!(
"Start fire exchanges. Player {}#{} goes first",
self.players[self.turn].get_name(),
self.turn
);
self.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)));
}
/// Get current game status for a specific player
fn get_game_status_for_player(&self, _id: usize) -> CurrentGameStatus {
CurrentGameStatus {
rules: self.rules.clone(),
}
}
}
impl Actor for Game {
type Context = Context<Self>;
}
#[derive(Message)]
#[rtype(result = "()")]
pub struct AddPlayer<E>(pub E);
impl<E> Handler<AddPlayer<Arc<E>>> for Game
where
E: Player + 'static,
{
type Result = ();
/// Add a new player to the game
fn handle(&mut self, msg: AddPlayer<Arc<E>>, _ctx: &mut Self::Context) -> Self::Result {
assert!(self.players.len() < 2);
self.players.push(msg.0);
if self.players.len() == 2 {
self.query_boats_disposition();
}
}
}
#[derive(Message)]
#[rtype(result = "()")]
pub struct SetBoatsLayout(pub Uuid, pub BoatsLayout);
impl Handler<SetBoatsLayout> for Game {
type Result = ();
/// Receive game configuration of a player
fn handle(&mut self, msg: SetBoatsLayout, _ctx: &mut Self::Context) -> Self::Result {
if self.status != GameStatus::WaitingForBoatsDisposition {
log::error!("Player attempted to set boat configuration on invalid step!");
return;
}
let player_index = self.player_id_by_uuid(msg.0);
log::debug!("Got boat disposition for player {}", player_index);
match player_index {
0 => self.map_0 = Some(GameMap::new(self.rules.clone(), msg.1)),
1 => self.map_1 = Some(GameMap::new(self.rules.clone(), msg.1)),
_ => unreachable!(),
}
self.players[opponent(player_index)].notify_other_player_ready();
if self.map_0.is_some() && self.map_1.is_some() {
self.players.iter().for_each(|p| p.notify_game_starting());
self.start_fire_exchanges();
}
}
}
#[derive(Message, Debug)]
#[rtype(result = "()")]
pub struct Fire(pub Uuid, pub Coordinates);
impl Handler<Fire> for Game {
type Result = ();
fn handle(&mut self, msg: Fire, _ctx: &mut Self::Context) -> Self::Result {
log::debug!("FIRE ===> {:?}", msg);
}
}

View File

@ -0,0 +1,60 @@
use actix::Addr;
use uuid::Uuid;
use crate::data::{CurrentGameStatus, GameRules};
use crate::game::{Fire, Game, Player, SetBoatsLayout};
use crate::human_player_ws::{ClientMessage, HumanPlayerWS, ServerMessage};
pub struct HumanPlayer {
pub name: String,
pub game: Addr<Game>,
pub player: Addr<HumanPlayerWS>,
pub uuid: Uuid,
}
impl Player for HumanPlayer {
fn get_name(&self) -> &str {
&self.name
}
fn get_uid(&self) -> Uuid {
self.uuid
}
fn query_boats_layout(&self, rules: &GameRules) {
self.player.do_send(ServerMessage::QueryBoatsLayout {
rules: rules.clone(),
});
}
fn notify_other_player_ready(&self) {
self.player.do_send(ServerMessage::OtherPlayerReady);
}
fn notify_game_starting(&self) {
self.player.do_send(ServerMessage::GameStarting);
}
fn request_fire(&self, status: CurrentGameStatus) {
self.player.do_send(ServerMessage::RequestFire { status });
}
fn other_player_must_fire(&self, status: CurrentGameStatus) {
self.player
.do_send(ServerMessage::OtherPlayerMustFire { status });
}
}
impl HumanPlayer {
pub fn handle_client_message(&self, msg: ClientMessage) {
match msg {
ClientMessage::StopGame => {
// TODO : do something}
}
ClientMessage::BoatsLayout { layout } => {
self.game.do_send(SetBoatsLayout(self.uuid, layout))
}
ClientMessage::Fire { location } => self.game.do_send(Fire(self.uuid, location)),
}
}
}

View File

@ -0,0 +1,137 @@
use std::sync::Arc;
use actix::prelude::*;
use actix::{Actor, Handler, StreamHandler};
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::game::{AddPlayer, Game};
use crate::human_player::HumanPlayer;
use crate::random_bot::RandomBot;
#[derive(Default, Debug)]
pub enum StartMode {
Bot(GameRules),
#[default]
AgainstHuman,
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(tag = "type")]
pub enum ClientMessage {
StopGame,
BoatsLayout { layout: BoatsLayout },
Fire { location: Coordinates },
}
#[derive(Message)]
#[rtype(result = "()")]
#[derive(serde::Serialize, serde::Deserialize, Debug)]
#[serde(tag = "type")]
pub enum ServerMessage {
WaitingForAnotherPlayer,
QueryBoatsLayout { rules: GameRules },
WaitingForOtherPlayerConfiguration,
OtherPlayerReady,
GameStarting,
OtherPlayerMustFire { status: CurrentGameStatus },
RequestFire { status: CurrentGameStatus },
}
#[derive(Default)]
pub struct HumanPlayerWS {
inner: Option<Arc<HumanPlayer>>,
pub start_mode: StartMode,
// TODO : add heartbeat stuff
}
impl HumanPlayerWS {
fn send_message(&self, msg: ServerMessage, ctx: &mut <HumanPlayerWS as Actor>::Context) {
ctx.text(serde_json::to_string(&msg).unwrap());
}
}
impl Actor for HumanPlayerWS {
type Context = WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
self.send_message(ServerMessage::WaitingForAnotherPlayer, ctx);
// Start game, according to appropriate start mode
match &self.start_mode {
StartMode::Bot(rules) => {
log::debug!("Start play with a bot");
let game = Game::new(rules.clone()).start();
match rules.bot_type {
BotType::Random => {
game.do_send(AddPlayer(Arc::new(RandomBot::new(game.clone()))));
}
};
let player = Arc::new(HumanPlayer {
name: "Human".to_string(),
game: game.clone(),
player: ctx.address(),
uuid: Uuid::new_v4(),
});
self.inner = Some(player.clone());
game.do_send(AddPlayer(player));
}
StartMode::AgainstHuman => {
unimplemented!();
}
}
}
fn stopped(&mut self, _ctx: &mut Self::Context) {
if let Some(player) = &self.inner {
player.handle_client_message(ClientMessage::StopGame);
}
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for HumanPlayerWS {
fn handle(&mut self, msg: Result<Message, ProtocolError>, ctx: &mut Self::Context) {
match msg {
Ok(Message::Ping(msg)) => ctx.pong(&msg),
Ok(Message::Binary(_bin)) => log::warn!("Got unsupported binary message!"),
Ok(Message::Text(msg)) => match serde_json::from_str::<ClientMessage>(&msg) {
Ok(msg) => match &self.inner {
None => {
log::error!("Client tried to send message without game!");
ctx.text("No game yet!");
}
Some(p) => p.handle_client_message(msg),
},
Err(e) => log::warn!("Got invalid message from client! {:?}", e),
},
Ok(Message::Nop) => log::warn!("Got WS nop"),
Ok(Message::Continuation(_)) => {
log::warn!("Got unsupported continuation message!");
}
Ok(Message::Pong(_)) => {
log::info!("Got pong message");
// TODO : handle pong message
}
Ok(Message::Close(reason)) => {
log::info!("Client asked to close this socket! reason={:?}", reason);
ctx.close(Some(CloseReason::from(CloseCode::Away)));
}
Err(e) => log::warn!("Websocket protocol error! {:?}", e),
}
}
}
impl Handler<ServerMessage> for HumanPlayerWS {
type Result = ();
fn handle(&mut self, msg: ServerMessage, ctx: &mut Self::Context) -> Self::Result {
ctx.text(serde_json::to_string(&msg).unwrap());
}
}

View File

@ -0,0 +1,14 @@
extern crate core;
pub mod args;
#[cfg(test)]
pub mod bot_client;
pub mod consts;
pub mod data;
pub mod game;
pub mod human_player;
pub mod human_player_ws;
pub mod random_bot;
pub mod server;
#[cfg(test)]
mod test;

View File

@ -0,0 +1,12 @@
use clap::Parser;
use env_logger::Env;
use sea_battle_backend::args::Args;
use sea_battle_backend::server::start_server;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
start_server(Args::parse()).await
}

View File

@ -0,0 +1,52 @@
use actix::Addr;
use uuid::Uuid;
use crate::data::{BoatsLayout, CurrentGameStatus, GameRules};
use crate::game::{Fire, Game, Player, 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 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 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 other_player_must_fire(&self, _status: CurrentGameStatus) {}
}

View File

@ -0,0 +1,63 @@
use crate::args::Args;
use crate::data::{GameRules, PlayConfiguration};
use crate::human_player_ws::{HumanPlayerWS, StartMode};
use actix_cors::Cors;
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use actix_web_actors::ws;
/// The default '/' route
async fn index() -> impl Responder {
HttpResponse::Ok().json("Sea battle backend")
}
/// The default 404 route
async fn not_found() -> impl Responder {
HttpResponse::NotFound().json("You missed your strike lol")
}
/// Get bot configuration
async fn game_configuration() -> impl Responder {
HttpResponse::Ok().json(PlayConfiguration::default())
}
/// Start bot game
async fn start_bot_play(
req: HttpRequest,
stream: web::Payload,
query: web::Query<GameRules>,
) -> Result<HttpResponse, actix_web::Error> {
let errors = query.0.get_errors();
if !errors.is_empty() {
return Ok(HttpResponse::BadRequest().json(errors));
}
let mut player_ws = HumanPlayerWS::default();
player_ws.start_mode = StartMode::Bot(query.0.clone());
let resp = ws::start(player_ws, &req, stream);
log::info!("New bot play with configuration: {:?}", &query.0);
resp
}
pub async fn start_server(args: Args) -> std::io::Result<()> {
let args_clone = args.clone();
HttpServer::new(move || {
let mut cors = Cors::default();
match args_clone.cors.as_deref() {
Some("*") => cors = cors.allow_any_origin(),
Some(orig) => cors = cors.allowed_origin(orig),
None => {}
}
App::new()
.wrap(cors)
.route("/config", web::get().to(game_configuration))
.route("/play/bot", web::get().to(start_bot_play))
.route("/", web::get().to(index))
.route("{tail:.*}", web::get().to(not_found))
})
.bind(&args.listen_address)?
.run()
.await
}

View File

@ -0,0 +1,57 @@
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() {
let _ = env_logger::builder().is_test(true).try_init();
bot_client::run_client(
&TestPort::ClientInvalidPort.as_url(),
&GameRules::random_players_rules(),
)
.await
.unwrap_err();
}
#[tokio::test]
async fn invalid_rules() {
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.map_width = 0;
task::spawn_local(start_server(Args::for_test(TestPort::ClientInvalidRules)));
wait_for_port(TestPort::ClientInvalidRules.port()).await;
bot_client::run_client(&TestPort::ClientInvalidRules.as_url(), &rules)
.await
.unwrap_err();
})
.await;
}
#[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 {
let rules = GameRules::random_players_rules();
task::spawn_local(start_server(Args::for_test(TestPort::ClientInvalidRules)));
wait_for_port(TestPort::ClientInvalidRules.port()).await;
bot_client::run_client(&TestPort::ClientInvalidRules.as_url(), &rules)
.await
.unwrap();
})
.await;
}

View File

@ -0,0 +1,29 @@
use crate::args::Args;
#[derive(Copy, Clone)]
enum TestPort {
ClientInvalidPort = 20000,
ClientInvalidRules,
}
impl TestPort {
pub fn port(&self) -> u16 {
(*self as u32) as u16
}
pub fn as_url(&self) -> String {
format!("http://127.0.0.1:{}", self.port())
}
}
impl Args {
fn for_test(port: TestPort) -> Self {
Self {
listen_address: format!("127.0.0.1:{}", port.port()),
cors: None,
}
}
}
mod client;
mod network_utils;

View File

@ -0,0 +1,25 @@
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time;
/// Check whether a given port is open or not
pub async fn is_port_open(port: u16) -> bool {
match TcpStream::connect(("127.0.0.1", port)).await {
Ok(_) => true,
Err(_) => false,
}
}
/// Wait for a port to become available
pub async fn wait_for_port(port: u16) {
for _ in 0..50 {
if is_port_open(port).await {
return;
}
time::sleep(Duration::from_millis(10)).await;
}
eprintln!("Port {} did not open in time!", port);
std::process::exit(2);
}