//Four Line Dropper Frontend/Backend - A webapp that allows one to play a game of Four Line Dropper //Copyright (C) 2022 Stephen Seo // //This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // //This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // //You should have received a copy of the GNU General Public License along with this program. If not, see . use crate::ai::AIDifficulty; use crate::constants::{COLS, ROWS}; use crate::game_logic::{check_win_draw, WinType}; use serde::{Deserialize, Serialize}; use std::cell::{Cell, RefCell}; use std::collections::hash_set::HashSet; use std::fmt::Display; use std::rc::Rc; #[derive(Clone, Debug, PartialEq, Eq)] pub enum GameState { MainMenu, SinglePlayer(Turn, AIDifficulty), LocalMultiplayer, NetworkedMultiplayer { paired: bool, current_side: Option, current_turn: Turn, phrase: Option, }, PostGameResults(BoardState), } impl GameState { pub fn is_networked_multiplayer(&self) -> bool { matches!( *self, GameState::NetworkedMultiplayer { paired: _, current_side: _, current_turn: _, phrase: _, } ) } pub fn set_networked_paired(&mut self) { if let GameState::NetworkedMultiplayer { ref mut paired, current_side: _, current_turn: _, phrase: _, } = self { *paired = true; } } pub fn get_networked_current_side(&self) -> Option { if let GameState::NetworkedMultiplayer { paired: _, current_side, current_turn: _, phrase: _, } = *self { current_side } else { None } } pub fn set_networked_current_side(&mut self, side: Option) { if let GameState::NetworkedMultiplayer { paired: _, ref mut current_side, current_turn: _, phrase: _, } = self { *current_side = side; } } pub fn get_current_turn(&self) -> Turn { if let GameState::SinglePlayer(turn, _) = *self { turn } else if let GameState::NetworkedMultiplayer { paired: _, current_side: _, current_turn, phrase: _, } = *self { current_turn } else { Turn::CyanPlayer } } pub fn set_networked_current_turn(&mut self, turn: Turn) { if let GameState::NetworkedMultiplayer { paired: _, current_side: _, ref mut current_turn, phrase: _, } = self { *current_turn = turn; } } pub fn get_phrase(&self) -> Option { if let GameState::NetworkedMultiplayer { paired: _, current_side: _, current_turn: _, phrase, } = self { phrase.clone() } else { None } } pub fn get_singleplayer_current_side(&self) -> Option { if let GameState::SinglePlayer(turn, _) = *self { Some(turn) } else { None } } } impl Default for GameState { fn default() -> Self { GameState::MainMenu } } impl From for GameState { fn from(msg: MainMenuMessage) -> Self { match msg { MainMenuMessage::SinglePlayer(t, ai) => GameState::SinglePlayer(t, ai), MainMenuMessage::LocalMultiplayer => GameState::LocalMultiplayer, MainMenuMessage::NetworkedMultiplayer(phrase) => GameState::NetworkedMultiplayer { paired: false, current_side: None, current_turn: Turn::CyanPlayer, phrase, }, } } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum BoardState { Empty, Cyan, Magenta, CyanWin, MagentaWin, } impl Default for BoardState { fn default() -> Self { Self::Empty } } impl Display for BoardState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { BoardState::Empty => f.write_str("open"), BoardState::Cyan => f.write_str("cyan"), BoardState::CyanWin => f.write_str("cyan win"), BoardState::Magenta => f.write_str("magenta"), BoardState::MagentaWin => f.write_str("magenta win"), } } } impl From for BoardState { fn from(t: Turn) -> Self { match t { Turn::CyanPlayer => BoardState::Cyan, Turn::MagentaPlayer => BoardState::Magenta, } } } impl BoardState { pub fn is_empty(&self) -> bool { *self == BoardState::Empty } pub fn is_win(self) -> bool { match self { BoardState::Empty | BoardState::Cyan | BoardState::Magenta => false, BoardState::CyanWin | BoardState::MagentaWin => true, } } pub fn into_win(self) -> Self { match self { BoardState::Empty => BoardState::Empty, BoardState::Cyan | BoardState::CyanWin => BoardState::CyanWin, BoardState::Magenta | BoardState::MagentaWin => BoardState::MagentaWin, } } pub fn from_win(&self) -> Self { match *self { BoardState::Empty => BoardState::Empty, BoardState::Cyan | BoardState::CyanWin => BoardState::Cyan, BoardState::Magenta | BoardState::MagentaWin => BoardState::Magenta, } } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Turn { CyanPlayer, MagentaPlayer, } impl Display for Turn { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { Turn::CyanPlayer => f.write_str("CyanPlayer"), Turn::MagentaPlayer => f.write_str("MagentaPlayer"), } } } impl From for Turn { fn from(board_state: BoardState) -> Self { match board_state { BoardState::Empty | BoardState::Cyan | BoardState::CyanWin => Turn::CyanPlayer, BoardState::Magenta | BoardState::MagentaWin => Turn::MagentaPlayer, } } } impl Turn { pub fn get_color(&self) -> &str { match *self { Turn::CyanPlayer => "cyan", Turn::MagentaPlayer => "magenta", } } pub fn get_opposite(&self) -> Self { match *self { Turn::CyanPlayer => Turn::MagentaPlayer, Turn::MagentaPlayer => Turn::CyanPlayer, } } } pub type BoardType = [Rc>; 56]; pub fn new_empty_board() -> BoardType { [ Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())), ] } pub fn board_deep_clone(board: &BoardType) -> BoardType { let cloned_board = new_empty_board(); for i in 0..board.len() { cloned_board[i].replace(board[i].get()); } cloned_board } pub type PlacedType = [Rc>; 56]; pub fn new_placed() -> PlacedType { [ Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), Rc::new(Cell::new(false)), ] } #[derive(Clone, Debug, PartialEq)] pub struct SharedState { pub board: BoardType, pub game_state: Rc>, pub turn: Rc>, pub placed: PlacedType, } impl Default for SharedState { fn default() -> Self { Self { // cannot use [; 56] because Rc does not impl Copy board: new_empty_board(), game_state: Rc::new(RefCell::new(GameState::default())), turn: Rc::new(Cell::new(Turn::CyanPlayer)), placed: new_placed(), } } } // This enum moved from yew_components module so that this module would have no // dependencies on the yew_components module #[derive(Clone, Debug, PartialEq, Eq)] pub enum MainMenuMessage { SinglePlayer(Turn, AIDifficulty), LocalMultiplayer, NetworkedMultiplayer(Option), } pub fn new_string_board() -> String { let mut board = String::with_capacity(56); for _i in 0..56 { board.push('a'); } board } pub fn board_from_string(board_string: String) -> BoardType { let board = new_empty_board(); for (idx, c) in board_string.chars().enumerate() { match c { 'a' => board[idx].replace(BoardState::Empty), 'b' | 'f' => board[idx].replace(BoardState::Cyan), 'd' | 'h' => board[idx].replace(BoardState::CyanWin), 'c' | 'g' => board[idx].replace(BoardState::Magenta), 'e' | 'i' => board[idx].replace(BoardState::MagentaWin), _ => BoardState::Empty, }; } board } /// Returns the board as a String, and None if game has not ended, Empty if game /// ended in a draw, or a player if that player has won pub fn string_from_board(board: &BoardType, placed: usize) -> (String, Option) { let mut board_string = String::with_capacity(56); // check for winning pieces let mut win_set: HashSet = HashSet::new(); let win_opt = check_win_draw(board); if let Some((_board_state, win_type)) = win_opt { match win_type { WinType::Horizontal(pos) => { for i in pos..(pos + 4) { win_set.insert(i); } } WinType::Vertical(pos) => { for i in 0..4 { win_set.insert(pos + i * COLS as usize); } } WinType::DiagonalUp(pos) => { for i in 0..4 { win_set.insert(pos + i - i * COLS as usize); } } WinType::DiagonalDown(pos) => { for i in 0..4 { win_set.insert(pos + i + i * COLS as usize); } } WinType::None => (), } } // set values to String let mut is_full = true; for (idx, board_state) in board.iter().enumerate().take((COLS * ROWS) as usize) { board_string.push(match board_state.get() { BoardState::Empty => { is_full = false; 'a' } BoardState::Cyan | BoardState::CyanWin => { if win_set.contains(&idx) { if idx == placed { 'h' } else { 'd' } } else if idx == placed { 'f' } else { 'b' } } BoardState::Magenta | BoardState::MagentaWin => { if win_set.contains(&idx) { if idx == placed { 'i' } else { 'e' } } else if idx == placed { 'g' } else { 'c' } } }); } if is_full && win_set.is_empty() { (board_string, Some(BoardState::Empty)) } else if !win_set.is_empty() { let winning_char: char = board_string.chars().collect::>()[*win_set.iter().next().unwrap()]; ( board_string.clone(), if winning_char == 'd' || winning_char == 'h' { Some(BoardState::CyanWin) } else { Some(BoardState::MagentaWin) }, ) } else { (board_string, None) } } #[derive(Debug, Serialize, Deserialize)] pub struct PairingRequestResponse { pub r#type: String, pub id: u32, pub status: String, pub color: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct PairingStatusResponse { pub r#type: String, pub status: String, pub color: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct GameStateResponse { pub r#type: String, pub status: String, pub board: Option, pub peer_emote: Option, pub updated_time: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct PlaceTokenResponse { pub r#type: String, pub status: String, pub board: String, } #[derive(Debug, Serialize, Deserialize)] pub struct SendEmoteRequestResponse { pub r#type: String, pub status: String, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum NetworkedGameState { CyanTurn, MagentaTurn, CyanWon, MagentaWon, Draw, Disconnected, InternalError, NotPaired, UnknownID, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PlacedEnum { Accepted, Illegal, NotYourTurn, Other(NetworkedGameState), } #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum EmoteEnum { Smile, Neutral, Frown, Think, } impl Display for EmoteEnum { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { EmoteEnum::Smile => f.write_str("smile"), EmoteEnum::Neutral => f.write_str("neutral"), EmoteEnum::Frown => f.write_str("frown"), EmoteEnum::Think => f.write_str("think"), } } } impl TryFrom<&str> for EmoteEnum { type Error = (); fn try_from(value: &str) -> Result { match value.to_lowercase().as_str() { "smile" => Ok(Self::Smile), "neutral" => Ok(Self::Neutral), "frown" => Ok(Self::Frown), "think" => Ok(Self::Think), _ => Err(()), } } } impl From for String { fn from(e: EmoteEnum) -> Self { match e { EmoteEnum::Smile => "smile".into(), EmoteEnum::Neutral => "neutral".into(), EmoteEnum::Frown => "frown".into(), EmoteEnum::Think => "think".into(), } } } impl EmoteEnum { pub fn get_unicode(&self) -> char { match *self { EmoteEnum::Smile => '🙂', EmoteEnum::Neutral => '😐', EmoteEnum::Frown => '🙁', EmoteEnum::Think => '🤔', } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_networked_multiplayer_enum() { let state = GameState::MainMenu; assert!(!state.is_networked_multiplayer()); let state = GameState::LocalMultiplayer; assert!(!state.is_networked_multiplayer()); let state = GameState::NetworkedMultiplayer { paired: false, current_side: None, current_turn: Turn::CyanPlayer, phrase: None, }; assert!(state.is_networked_multiplayer()); let state = GameState::NetworkedMultiplayer { paired: true, current_side: Some(Turn::CyanPlayer), current_turn: Turn::MagentaPlayer, phrase: None, }; assert!(state.is_networked_multiplayer()); } #[test] fn test_board_string() { let board = new_empty_board(); board[49].set(BoardState::Cyan); board[51].set(BoardState::Cyan); board[52].set(BoardState::Cyan); board[53].set(BoardState::Cyan); board[54].set(BoardState::Cyan); board[55].set(BoardState::Magenta); let (board_string, state_opt) = string_from_board(&board, 51); let board_chars: Vec = board_string.chars().collect(); assert_eq!(board_chars[49], 'b'); assert_eq!(board_chars[50], 'a'); assert_eq!(board_chars[51], 'h'); assert_eq!(board_chars[52], 'd'); assert_eq!(board_chars[53], 'd'); assert_eq!(board_chars[54], 'd'); assert_eq!(board_chars[55], 'c'); assert_eq!(state_opt, Some(BoardState::CyanWin)); board[49].set(BoardState::Magenta); board[51].set(BoardState::Magenta); board[52].set(BoardState::Magenta); board[53].set(BoardState::Magenta); board[54].set(BoardState::Magenta); board[55].set(BoardState::Cyan); let (board_string, state_opt) = string_from_board(&board, 51); let board_chars: Vec = board_string.chars().collect(); assert_eq!(board_chars[49], 'c'); assert_eq!(board_chars[50], 'a'); assert_eq!(board_chars[51], 'i'); assert_eq!(board_chars[52], 'e'); assert_eq!(board_chars[53], 'e'); assert_eq!(board_chars[54], 'e'); assert_eq!(board_chars[55], 'b'); assert_eq!(state_opt, Some(BoardState::MagentaWin)); } }