diff --git a/.gitignore b/.gitignore index 5af1954..a5643ea 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,7 @@ Cargo.lock # Build generated by trunk serve **/*/dist/ +.idea/ + # Node modules used for tailwind setup node_modules/ diff --git a/server/Cargo.toml b/server/Cargo.toml index 9911232..d650982 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -15,3 +15,4 @@ dotenv = "0.15.0" env_logger = "0.9.0" mongodb = "2.1.0" futures = "0.3.19" +rand = "0.8.4" diff --git a/server/src/main.rs b/server/src/main.rs index 3bd61a4..348ef3c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -8,6 +8,7 @@ use tokio::sync::Mutex; mod components; mod models; mod types; +mod utils; use models::app_data::AppData; diff --git a/server/src/models/color.rs b/server/src/models/color.rs index a313a1b..9d455b4 100644 --- a/server/src/models/color.rs +++ b/server/src/models/color.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] pub enum Color { Red, Green, diff --git a/server/src/models/game.rs b/server/src/models/game.rs index a2b1bc2..7eb67fa 100644 --- a/server/src/models/game.rs +++ b/server/src/models/game.rs @@ -3,12 +3,185 @@ use mongodb::bson::DateTime; use serde::{Deserialize, Serialize}; use super::player::Player; +use crate::models::color::Color; #[derive(Debug, Serialize, Deserialize)] pub struct Game { - pub id: String, - pub started_at: DateTime, - pub finished_at: Option, - pub fields: Vec, - pub players: Vec, + pub id: String, + pub started_at: DateTime, + pub finished_at: Option, + pub fields: Vec, + pub players: Vec, + pub current_player: Color +} + + +impl Game { + + pub fn update_current_player(&mut self) { + self.current_player = match self.current_player { + Color::Yellow => Color::Blue, + Color::Blue => Color::Red, + Color::Red => Color::Green, + Color::Green => Color::Yellow + } + } + + // how many steps we need to make to reach the first field of player's home + // for yellow starting at offset = 0, we need to make 40 - position steps + // position = index in the Vec fields [0 ; 39] + // - if yellow piece is at position 39, it is right in front of its home + pub fn distance_from_home(&self, position: usize, color: Color) -> usize { + self.fields.len() - position + get_offset(color) + } + + // returns size of the home column (finish) + pub fn get_home_size(&self) -> usize { + match self.players.get(0) { + Some(player) => player.home.len(), + None => 4 + } + } + + // we assume home_offset is valid + pub fn get_home_field(&self, player_color: Color, home_offset: usize) -> &Field { + let player = self.players.iter().filter(|&player| player.color == player_color).next().unwrap(); + &player.home[home_offset] + } + + // we can use the following (simplest) board as an example: + // https://www.vectorstock.com/royalty-free-vector/ludo-board-game-vector-8703408 + // there is a clock-wise ordering: Yellow, Blue, Red, Green + // there are 40 fields in the main 'board' (if we change the board - 56 fields e.g.), + // we have to adjust the constants + pub fn get_offset(&self, color: Color) -> usize { + let offset = (self.fields.len() / 4) as usize; + match color { + Color::Yellow => 0, + Color::Blue => offset, + Color::Red => offset * 2, + Color::Green => offset * 3 + } + } + + pub fn is_a_valid_move(&self, position: usize, dice_value: usize) -> MoveResultType { + + if dice_value == 6 { + ... + } + + // if player threw 6 and decided to move his piece from the start, there are two options [*]: + // a) if the field at offset is empty: + // 1. place our piece at offset + // 2. decrease pieces_at_start by one for a specific player (color) + // b) if the field is occupied by: + // 1) our own piece - we can't move there, invalid move + // 2) opponent's piece - we can move there and remove his piece + // - the same actions as for a) + increase pieces_at_start for the player whose piece + // we have just removed (sent to start) + + // [*] another thing is: + // is player able to send a message to move a piece from his start, even if he has + // no pieces at the start anymore (i.e. we might have add a check for the situation, + // when pieces_at_start = 0) + + + // do we always obtain Game (and fields ...) from DB for every turn ? + // and do we update the DB after every move as well (when the game/board changed) ? + // adding bonus throws after getting 6 is not solved yet + + + let distance_from_home = distance_from_home(position, self.current_player); + + match dice_value < distance_from_home { + // is within main 'board' + true => { + new_position = position + dice_value; + match self.is_field_empty(position) { + true => MoveResultType::Success, + false => match self.is_players_piece(position, self.current_player) { + true => MoveResultType::Error(String::from("Our piece already occupies this position")), + false => MoveResultType::Success + } + } + + // if piece at position: + // a) is empty, we can go there: + // 1. clear field at 'position' + // 2. update field at 'new_position' + // b) is occupied by: + // 1) our own piece - we can't go there, not a valid move + // 2) opponent's piece - we can move there (clear field at 'position', update field + // + }, + + // reaches home - validity of move is based on home + false => { + // first we check a situation where we overshoot home + if dice_value >= distance_from_home + self.get_home_size() { + return MoveResultType::Error(String::from("Would overshoot home.")) + } + + // offset in player's home column (if piece is right in front of home - distance = 1, + // and we throw a 1, we would reach the first home field + let home_offset = dice_value - distance_from_home; + match self.get_home_field(self.current_player, home_offset) { + Some(_) => MoveResultType::Error(String::from("Home field is already occupied.")), + None => MoveResultType::Success + } + + // if field at home[home_offset] is occupied, invalid move + // otherwise we move our piece to that position: + // place our piece at home[home_offset] and remove original piece from fields[position] + } + } + } + + + // // returns whether a field specified by is is occupied by a piece with + // pub fn is_players_piece(&self, position: usize, player_color: Color) -> bool { + // match self.fields.get(position) { + // Some(field) => match field { + // Some(color) => color == player_color, + // None => false + // } + // None => false + // } + // } + + // returns whether a field specified by is is occupied by a piece with + pub fn is_players_piece(&self, position: usize) -> bool { + match self.fields.get(position) { + Some(field) => match field { + Some(color) => color == self.current_player, + None => false + } + None => false + } + } + + // returns whether a field is empty + pub fn is_field_empty(&self, position: usize) -> bool { + match self.fields.get(position) { + Some(field) => match field { + Some(_) => false, + None => true + } + None => false + } + } + + pub fn get_player(&self) -> &Player { + self.players.iter().filter(|&player| player.color == self.current_player).next().unwrap() + } + + pub fn is_current_play_AI(&self) -> bool { + self.AI_players.contains + } +} + + +pub enum MoveResultType { + Success, + Error(String) } diff --git a/server/src/models/player.rs b/server/src/models/player.rs index 1485b3b..bc4512f 100644 --- a/server/src/models/player.rs +++ b/server/src/models/player.rs @@ -10,4 +10,5 @@ pub struct Player { pub color: Color, pub pawns_at_start: u32, pub home: Vec, + pub is_bot: bool } diff --git a/server/src/utils/mod.rs b/server/src/utils/mod.rs index e69de29..112d815 100644 --- a/server/src/utils/mod.rs +++ b/server/src/utils/mod.rs @@ -0,0 +1,172 @@ + + +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum ClientMessage { + // CreateRoom(...), + // JoinRoom(...), + ThrowDice, + MoveFigure(i32), + PlaceFigure +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum ServerMessage { + JoinedRoom { + room_name: String, + players: Vec<(String, u32, bool)>, + active_player: usize, + player_index: usize, + board: Vec<((i32, i32), Piece)>, + pieces: Vec, + }, + JoinFailed(String), + Chat { + from: String, + message: String, + }, + Information(String), + NewPlayer(String), + PlayerReconnected(usize), + PlayerDisconnected(usize), + PlayerTurn(usize), + Played(Vec<(Piece, i32, i32)>), + Swapped(usize), + MoveAccepted(Vec), + MoveRejected, + PlayerScore { + delta: u32, + total: u32, + }, + PiecesRemaining(usize), + ItsOver(usize), +} + +use crate::models::color::Color; +use crate::types::Field; +use crate::models::game::{Game, MoveResultType}; +use rand::Rng; + + +// +// // could be a method of Game +// // returns player's home Vec based on their color +// pub fn get_home(color: Color) -> Vec { +// +// } + + +pub fn get_dice_value() -> usize { + let mut rng = rand::thread_rng(); + rng.gen_range(1..7) +} + +pub fn throw_dice() -> usize { + let mut dice_value: usize = 0; + // player/client sends MessageType::ThrowDice + << message exchange >> + match get_dice_value() { + 6 => { + dice_value += 6; + << message exchange >>; + match get_dice_value() { + 6 => { + dice_value += 6; + << message exchange >>; + match get_dice_value() { + // if we throw 6 three times, it gets reset + 6 => { + dice_value = 0; + << message exchange >> + }, + n => dice_value += n; + } + }, + n => dice_value += n; + } + }, + n => dice_value += n + } + + dice_value +} + +pub fn make_a_move() { + + let mut game: Game = find_game(id); + + let mut player = game.get_player(); + + if player.is_bot { + return make_a_move_bot() + } + + // dice_value + let dice_value = throw_dice(); + let position: usize = message_from_client/player(); + + + // throw a dice and get position (which piece to move) from player + // different behaviour for AI (special attribute in Game? AI_player? + // - if AI_players contains game.current_player => it is AI + + match game.is_a_valid_move(position, dice_value) { + MoveResultType::Success => { + game.execute_move(); + game.update_current_player(); + update_db(...) + }, + MoveResultType::Error(err) => send/broadcast_error_message(err) + } + + // throw dice (generate 1-6, and inform the player(s) - send a message) + // wait for a message from player (his choice of figure for example) + + // je zalozene na loopoch? vzdy cakame na urcity typ spravy od klienta: + + // vzdy ked obdrzime message - deserializovat, a podla typu message nieco spravit + // MessageType::ThrowDice + // MessageType::MoveFigure(position) + // napr. ak klient posle ThrowDice message, tak musi nasledovat MoveFigure message s poziciou figurky + + // ak klient posle zlu poziciu (napr. field je empty alebo figurka patri superovi - ak to umozni frontend), + // tak posleme klientovi spravu o 'chybe' - 'You can only move your own pieces.' + + + // loop kym nedostaneme ThrowDice message (cez match MessageType) { + // ThrowDice => 1. vygenerujeme hodnotu 1-6 + // 2. checkneme, ci ma hrac valid moves: + // - ak nie, posleme NoMoves message, nastavime dalsieho hraca a return + // - ak ano, len breakneme loop a cakame na dalsiu spravu od klienta + // _ => 1. odosleme message, ze najskor treba hodit kostkou? stale sme v loope + // } + // + // << mame dice_value >> + // + // loop kym nedostaneme validnu MovePiece message {} - ci position oznacuje policko s nasou figurkou + // + + // co ak klienta nema ziadne volne tahy? automaticky by sme ho mali skipnut + // (t.j. message pre klienta A / broadcast pre vsetkych klientov, ze: + // > 'Player A has no available moves, skipping.' + // > 'Next player - Player B.' + + // player chooses a piece to move (might choose figure at start) + // - special coordinate (-1), or a specific message? + // - if the player doesn't throw a 6, should the choice for getting a piece into a field be + // grayed out? + // >>> Use a special MessageType (PlaceFigure) + + // if a player throws a 6, he can: + // a) get a piece from start to field - doesn't get a bonus throw + // b) decides to move one of his pieces in the field - gets an extra throw (applies to the same figure ??) + + + // ako ukladat aktualneho / nasledujuceho hraca? v DB + // pri ukonceni tahu by sa mal vo frontende prepnut dalsi hrac (napr. podla svojej farby vs. current_player + // po aktualizacii) - a napr. 'zasednut' tlacitko, ktore normalne umozni hodit kostkou + // zasleme spravu nasledujucemu hracovi, ze je na rade (napr. CurrentPlayer) + // a hraci, ktory skoncil tah teraz posleme spravu, ze nie je na rade (NotCurrentPlayer) + + + // pridat .idea do gitignore +}