From 5d1ca4a96d160b6bbde4df2bdc91ee8380f3b8da Mon Sep 17 00:00:00 2001 From: Adam <24621027+adoyle0@users.noreply.github.com> Date: Tue, 20 Aug 2024 22:25:39 -0400 Subject: [PATCH] add submission and judging --- client/src/components/game.rs | 87 +++++++++++--- client/src/components/websocket.rs | 11 +- lib/src/lib.rs | 17 ++- server/src/game_handler.rs | 183 +++++++++++++++++++++++++---- server/src/lib.rs | 7 +- server/src/main.rs | 9 +- server/src/message_handler.rs | 10 ++ server/src/user_handler.rs | 44 +++++++ 8 files changed, 314 insertions(+), 54 deletions(-) diff --git a/client/src/components/game.rs b/client/src/components/game.rs index 34032ac..c69f712 100644 --- a/client/src/components/game.rs +++ b/client/src/components/game.rs @@ -3,12 +3,13 @@ use crate::components::websocket::WebSocketContext; use leptos::*; use lib::*; use serde_json::to_string; -use std::collections::{BTreeSet, HashMap}; +use std::collections::HashMap; #[component] pub fn Game() -> impl IntoView { let websocket = expect_context::(); let game_meta = expect_context::>>(); + let judge_round = expect_context::>>(); let (game_id, set_game_id) = create_signal("".to_string()); let (game_name, set_game_name) = create_signal("".to_string()); @@ -16,14 +17,47 @@ pub fn Game() -> impl IntoView { let (game_players, set_game_players) = create_signal(vec![]); let (game_czar, set_game_czar) = create_signal("".to_string()); let (game_black, set_game_black) = create_signal(("".to_string(), 0u8)); - let (game_white, set_game_white) = create_signal(vec![]); - let (player_white, set_player_white) = - create_signal::>(HashMap::new()); + let (_game_white, set_game_white) = create_signal(vec![]); let (selected_cards_ordered, set_selected_cards_ordered) = create_signal::>(vec![]); let (player_hand, set_player_hand) = create_signal::>(vec![]); + let (player_white, set_player_white) = + create_signal::>(HashMap::new()); let (card_clicked, set_card_clicked) = create_signal::(String::new()); provide_context::>(set_card_clicked); + // Handle incoming judge message + create_effect(move |_| { + judge_round.with(move |judge_round| { + set_player_hand.update(|list| { + list.clear(); + }); + set_player_white.update(|list| { + list.clear(); + }); + set_card_clicked.update(|list| { + list.clear(); + }); + set_selected_cards_ordered.update(|list| { + list.clear(); + }); + + // Load hand + if let Some(judge) = judge_round { + for cards in judge.cards_to_judge.clone() { + for card in cards { + set_player_white.update(|hand| { + hand.insert(card.uuid.clone(), card.clone()); + }); + set_player_hand.update(|hand| { + hand.push(card.uuid.clone()); + }); + } + } + } + }); + }); + + // Handle incoming state update create_effect(move |_| { if let Some(game) = game_meta() { // Clear in case of (re-)joining game @@ -61,7 +95,7 @@ pub fn Game() -> impl IntoView { } }); - // Move cards back and forth from hand to selected when clicked + // Move cards back and forth between hand and selected when clicked create_effect(move |_| { if let Some(card_index) = player_hand() .iter() @@ -88,16 +122,34 @@ pub fn Game() -> impl IntoView { } }); - // create_effect(move |_| { - // logging::log!("{:#?}", selected_cards()); - // websocket.send( - // &to_string(&PlayerMoveRequest { - // game_id: game_id(), - // card_ids: selected_cards(), - // }) - // .unwrap(), - // ) - // }); + let submit_cards = move |_| { + let tx = websocket.clone(); + judge_round.with(move |judge| { + let mut _message: Option = None; + + if judge.is_some() { + _message = Some( + to_string(&JudgeDecisionRequest { + game_id: game_id(), + winning_cards: selected_cards_ordered(), + }) + .unwrap(), + ); + } else { + _message = Some( + to_string(&PlayerMoveRequest { + game_id: game_id(), + card_ids: selected_cards_ordered(), + }) + .unwrap(), + ); + } + + if let Some(msg) = _message { + tx.send(&msg); + } + }) + }; view! {
@@ -136,6 +188,11 @@ pub fn Game() -> impl IntoView { } />
+
+ +
// Player cards
diff --git a/client/src/components/websocket.rs b/client/src/components/websocket.rs index 0d56f3d..87923f7 100644 --- a/client/src/components/websocket.rs +++ b/client/src/components/websocket.rs @@ -54,26 +54,29 @@ pub fn Websocket() -> impl IntoView { Rc::new(close.clone()), )); + // TODO: This context stuff can probably be done better + // Contexts for message handler let (state_summary, set_state_summary) = create_signal::>(Option::None); + let (active_games, set_active_games) = create_signal::>(vec![]); let (user_update, set_user_update) = create_signal::>(Option::None); let (chat_update, set_chat_update) = create_signal::>(Option::None); + let (judge_round, set_judge_round) = create_signal::>(Option::None); let (chat_message, set_chat_message) = create_signal::>(Option::None); - let (active_games, set_active_games) = create_signal::>(vec![]); let (current_game, set_current_game) = create_signal::>(Option::None); let (card_packs_meta, set_card_packs_meta) = create_signal::(CardPacksMeta { official_meta: vec![], unofficial_meta: vec![], }); - // provide_context::>>(game_object); + provide_context::>(card_packs_meta); provide_context::>>(user_update); provide_context::>>(chat_update); + provide_context::>>(judge_round); provide_context::>>(chat_message); provide_context::>>(active_games); provide_context::>>(current_game); - provide_context::>(card_packs_meta); provide_context::>>(state_summary); // Message handler @@ -94,6 +97,8 @@ pub fn Websocket() -> impl IntoView { set_current_game(Some(game_update)); } else if let Ok(packs_meta_update) = from_str::(message) { set_card_packs_meta(packs_meta_update); + } else if let Ok(judge_update) = from_str::(message) { + set_judge_round(Some(judge_update)); } else { logging::log!("Unhandled message: {}", message); } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f62f24f..5d99664 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,7 +1,18 @@ -use std::collections::BTreeSet; - use serde::{Deserialize, Serialize}; +/// Judge decision +#[derive(Debug, Serialize, Deserialize)] +pub struct JudgeDecisionRequest { + pub game_id: String, + pub winning_cards: Vec, +} + +/// Judge round +#[derive(Debug, Serialize, Deserialize)] +pub struct JudgeRound { + pub cards_to_judge: Vec>, +} + /// Delete game request #[derive(Debug, Serialize, Deserialize)] pub struct GameDeleteRequest { @@ -18,7 +29,7 @@ pub struct GameJoinRequest { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PlayerMoveRequest { pub game_id: String, - pub card_ids: BTreeSet, + pub card_ids: Vec, } /// White Card Meta diff --git a/server/src/game_handler.rs b/server/src/game_handler.rs index 11c00cf..be59f86 100644 --- a/server/src/game_handler.rs +++ b/server/src/game_handler.rs @@ -28,6 +28,7 @@ pub enum GameHandlerMessage { id: String, }, MoveRequest(PlayerMoveRequest, SocketAddr), + JudgeDecision(JudgeDecisionRequest, SocketAddr), } /// Handles game stuff @@ -48,12 +49,14 @@ impl GameHandler { NewGame { addr, new_game } => self.create_new_game(addr, new_game).await, JoinGame { addr, id } => self.join_game(addr, id).await, MoveRequest(request, addr) => self.handle_player_move(request, addr).await, + JudgeDecision(request, addr) => self.handle_judging(request, addr).await, } } - /// Process player move request - async fn handle_player_move(&self, request: PlayerMoveRequest, addr: SocketAddr) { - let this_player_id = self + /// Process judging + async fn handle_judging(&self, request: JudgeDecisionRequest, addr: SocketAddr) { + // Get pointers + let this_user_id = self .state .online_users .read() @@ -74,6 +77,8 @@ impl GameHandler { .unwrap() .clone(); + // Check if this player is czar + // Check if player is currently Czar if this_game .read() .unwrap() @@ -82,19 +87,129 @@ impl GameHandler { .unwrap() .uuid .to_string() - == this_player_id + == this_user_id { - tracing::error!("No! User id is same as current czar"); - } else { - tracing::error!("Ok, but i have nothing to do"); - } + // Find user who submitted the card + let winning_user_id = this_game + .read() + .unwrap() + .judge_pool + .get(&request.winning_cards) + .unwrap() + .clone(); - tracing::debug!( - "Player move received:\nCards: {:#?}\nGame: {}\nPlayer: {}", - request.card_ids, - request.game_id, - this_player_id, - ); + tracing::debug!("{:#?} Won the round!", winning_user_id); + } + } + + /// Process player move request + async fn handle_player_move(&self, request: PlayerMoveRequest, addr: SocketAddr) { + // Get pointers + let this_user_id = self + .state + .online_users + .read() + .unwrap() + .get(&addr) + .unwrap() + .read() + .unwrap() + .uuid + .to_string(); + + let this_game = self + .state + .games + .read() + .unwrap() + .get(&request.game_id) + .unwrap() + .clone(); + + // Do the stuff + + // Check if player is currently Czar + if this_game + .read() + .unwrap() + .czar + .read() + .unwrap() + .uuid + .to_string() + == this_user_id + { + // Tell player no + let _ = self + .state + .users_tx + .send(DmUserAddr { + addr, + message: SendChatMessage(ChatMessage { + text: "You can't submit cards to judge, you ARE the judge!".to_string(), + }), + }) + .await; + } else { + // Ignore extra cards + let current_round_pick: usize = this_game.read().unwrap().current_black.pick.into(); + // TODO: handle not enough cards submitted + let trimmed = &request.card_ids[..current_round_pick]; + + // Put cards into game judge pool + this_game + .write() + .unwrap() + .judge_pool + .insert(trimmed.to_vec(), this_user_id.clone()); + + // Check if this is the last player to submit + if this_game.read().unwrap().judge_pool.len() + == this_game.read().unwrap().players.len() - 1 + { + let message = SendJudgeRound(JudgeRound { + cards_to_judge: this_game + .read() + .unwrap() + .judge_pool + .keys() + .into_iter() + .map(|group| { + group + .iter() + .map(|id| WhiteCardMeta { + uuid: self + .state + .white_cards_by_id + .get(id) + .unwrap() + .uuid + .to_string(), + text: self + .state + .white_cards_by_id + .get(id) + .unwrap() + .text + .clone(), + }) + .collect() + }) + .collect(), + }); + + tracing::debug!("send for judging"); + let czar_id = this_game.read().unwrap().czar.read().unwrap().uuid.clone(); + let _ = self + .state + .users_tx + .send(DmUserId { + id: czar_id, + message, + }) + .await; + } + } } /// Puts a user in a game @@ -304,11 +419,11 @@ struct CardBlackFromJSON { /// A processed white card for use server-side #[derive(Debug)] -struct CardWhiteWithID { +pub struct CardWhiteWithID { /// Unique identifier - uuid: Uuid, + pub uuid: Uuid, /// Card text - text: String, + pub text: String, } /// A processed black card for use server-side @@ -366,16 +481,19 @@ pub struct Game { pub name: String, /// The host user of the game pub host: Arc>, - /// White draw pile - white: Vec>, /// Current card czar pub czar: Arc>, + /// Packs selected for this game + pub packs: Vec, + /// White draw pile + white: Vec>, /// Black draw pile black: Vec>, - pub players: HashMap, + pub players: HashMap, /// Black card for the current round current_black: Arc, - pub packs: Vec, + /// Judge pool + judge_pool: HashMap, String>, } impl Game { @@ -424,6 +542,7 @@ impl Game { players: HashMap::new(), current_black, packs: request.packs.clone(), + judge_pool: HashMap::new(), } } @@ -463,7 +582,13 @@ impl Game { } /// Parse json for card data -pub fn load_cards_from_json(path: &str) -> Result<(CardPacks, CardPacksMeta)> { +pub fn load_cards_from_json( + path: &str, +) -> Result<( + CardPacks, + CardPacksMeta, + HashMap>, +)> { // Load in json let data: String = read_to_string(path).with_context(|| format!("Invalid JSON path: \"{}\"", path))?; @@ -475,6 +600,7 @@ pub fn load_cards_from_json(path: &str) -> Result<(CardPacks, CardPacksMeta)> { let mut unofficial: HashMap = HashMap::new(); let mut official_meta: Vec = vec![]; let mut unofficial_meta: Vec = vec![]; + let mut white_cards_by_id = HashMap::>::new(); // Unpack the json for sets in jayson { @@ -496,10 +622,15 @@ pub fn load_cards_from_json(path: &str) -> Result<(CardPacks, CardPacksMeta)> { pack = Some(white[0].pack); let mut white_buf = vec![]; for card in sets.white.unwrap() { - white_buf.push(Arc::new(CardWhiteWithID { - uuid: Uuid::now_v7(), + let uuid = Uuid::now_v7(); + + let new_card = Arc::new(CardWhiteWithID { + uuid, text: card.text, - })); + }); + + white_cards_by_id.insert(uuid.to_string(), new_card.clone()); + white_buf.push(new_card); } newset.white = Some(white_buf); } @@ -555,5 +686,5 @@ pub fn load_cards_from_json(path: &str) -> Result<(CardPacks, CardPacksMeta)> { unofficial_meta, }; - Ok((packs, packs_meta)) + Ok((packs, packs_meta, white_cards_by_id)) } diff --git a/server/src/lib.rs b/server/src/lib.rs index f1c7d7e..1a3352e 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -24,7 +24,7 @@ pub mod websocket; /// User #[derive(Debug)] pub struct User { - pub uuid: Uuid, + pub uuid: String, pub name: String, pub tx: Sender, } @@ -33,7 +33,7 @@ impl User { /// Create a new user object from incoming data pub fn new(name: String, tx: Sender) -> User { User { - uuid: Uuid::now_v7(), + uuid: Uuid::now_v7().to_string(), name, tx, } @@ -60,6 +60,7 @@ pub fn load_names(path: &str) -> Result> { // Our shared state pub struct AppState { + pub white_cards_by_id: HashMap>, pub broadcast_tx: broadcast::Sender, pub users_tx: mpsc::Sender, pub messages_tx: mpsc::Sender<(SocketAddr, Message)>, @@ -67,7 +68,7 @@ pub struct AppState { pub first_names: Vec, pub last_names: Vec, pub reserved_names: RwLock>, - pub users_by_id: RwLock>>>, + pub users_by_id: RwLock>>>, pub online_users: RwLock>>>, pub offline_users: RwLock>>>, pub packs: CardPacks, diff --git a/server/src/main.rs b/server/src/main.rs index 4848368..690f854 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -14,7 +14,6 @@ use tower::ServiceBuilder; use tower_http::{compression::CompressionLayer, services::ServeDir}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use user_handler::UserHandler; -use uuid::Uuid; #[tokio::main] async fn main() -> Result<()> { @@ -35,13 +34,14 @@ async fn main() -> Result<()> { let first_names = load_names("data/first.txt")?; let last_names = load_names("data/last.txt")?; let reserved_names = RwLock::new(HashSet::::new()); - let users_by_id = RwLock::new(HashMap::>>::new()); + let users_by_id = RwLock::new(HashMap::>>::new()); let online_users = RwLock::new(HashMap::>>::new()); let offline_users = RwLock::new(HashMap::>>::new()); - let (packs, packs_meta) = load_cards_from_json("data/cah-cards-full.json")?; + let (packs, packs_meta, white_cards_by_id) = load_cards_from_json("data/cah-cards-full.json")?; let games = RwLock::new(HashMap::new()); let app_state = Arc::new(AppState { + white_cards_by_id, broadcast_tx, users_tx, messages_tx, @@ -65,7 +65,8 @@ async fn main() -> Result<()> { } }); - // TODO: Make an outgoing message handler handler + // TODO: Restart handler threads if they crash + // TODO: Make an outgoing message handler handler? // Spawn task to handle User things let user_handler = UserHandler::new(app_state.clone()); diff --git a/server/src/message_handler.rs b/server/src/message_handler.rs index 0f7db09..70cb2b8 100644 --- a/server/src/message_handler.rs +++ b/server/src/message_handler.rs @@ -83,6 +83,16 @@ impl MessageHandler { } } + _judge_decision + if let Ok(judge_request) = from_str::(&text) => + { + self.state + .games_tx + .send(JudgeDecision(judge_request, addr)) + .await + .unwrap(); + } + _ => tracing::debug!("Unhandled text from {}", addr), }, diff --git a/server/src/user_handler.rs b/server/src/user_handler.rs index bff23a8..fedad2b 100644 --- a/server/src/user_handler.rs +++ b/server/src/user_handler.rs @@ -27,12 +27,18 @@ pub enum UserHandlerMessage { addr: SocketAddr, message: SendUserMessage, }, + DmUserId { + id: String, + message: SendUserMessage, + }, } /// Types of messages that can be sent to a user as a DM +// TODO: try to eliminate this extra step pub enum SendUserMessage { SendUserUpdate(UserUpdate), SendChatMessage(ChatMessage), + SendJudgeRound(JudgeRound), } impl UserHandler { @@ -50,9 +56,43 @@ impl UserHandler { } UserLogIn { username, addr } => self.login(username, addr).await, DmUserAddr { addr, message } => self.send_message_addr(addr, message).await, + DmUserId { id, message } => self.send_message_id(id, message).await, } } + /// Send message direct to a single user via user id + async fn send_message_id(&self, id: String, message: SendUserMessage) { + let tx = self + .state + .users_by_id + .read() + .unwrap() + .get(&id) + .unwrap() + .read() + .unwrap() + .tx + .clone(); + + // TODO: this feels messy + match message { + SendUserUpdate(message) => { + let msg = to_string::(&message).unwrap(); + tx.send(msg).await.unwrap() + } + SendChatMessage(message) => { + let msg = to_string::(&message).unwrap(); + tx.send(msg).await.unwrap() + } + SendJudgeRound(message) => { + let msg = to_string::(&message).unwrap(); + tx.send(msg).await.unwrap() + } + } + } + + // TODO: Combine ^v these + /// Send message direct to a single user via addr async fn send_message_addr(&self, addr: SocketAddr, message: SendUserMessage) { let tx = self @@ -75,6 +115,10 @@ impl UserHandler { let msg = to_string::(&message).unwrap(); tx.send(msg).await.unwrap() } + SendJudgeRound(message) => { + let msg = to_string::(&message).unwrap(); + tx.send(msg).await.unwrap() + } } }