From d5705f76489b23c31d9b6247a118fc39810c0269 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 19 Apr 2025 15:33:50 -0400 Subject: [PATCH] feat+fix: more emojis to break up walls of text move the impersonation/missed/authentic reveal to the bottom of the embed in order to maintain top to bottom reading order hide awards / guesses when no one did that scale guessing and awarding time to the number of responses make error embeds red so they're obviously an error make action confirmation embeds cyan fix crash when a player late joins (code forgot to fetch a topic for them) fix crash when subject didn't respond add more pauses to allow for reading --- discord-bot/src/discord_formatting.rs | 326 +++++++++++--------------- discord-bot/src/game.rs | 164 +++++++------ discord-bot/src/main.rs | 2 +- discord-bot/src/topic_picker.rs | 21 +- 4 files changed, 230 insertions(+), 283 deletions(-) diff --git a/discord-bot/src/discord_formatting.rs b/discord-bot/src/discord_formatting.rs index ecd777a..a15afb7 100644 --- a/discord-bot/src/discord_formatting.rs +++ b/discord-bot/src/discord_formatting.rs @@ -16,6 +16,10 @@ use twilight_util::builder::embed::{EmbedBuilder, EmbedFieldBuilder, EmbedFooter use crate::game::GameTopic; +const COLOR_INFO: u32 = 0x06b6d4; +const COLOR_WARN: u32 = 0xeab308; +const COLOR_ERROR: u32 = 0xef4444; + const NO_LOBBY_FOR_PRESS_HEADER: &str = "No Lobby for That"; const NO_LOBBY_FOR_PRESS_DESCRIPTION: &str = "You tried to press a button for a game but there isn't a running lobby for it.\nOr am I malfunctioning? Can you please let J Navith know about this bug if so?"; @@ -89,6 +93,8 @@ const REMAINING_TIME_HEADER: &str = "Time Remaining"; const RESPONSE_RECORDED_HEADER: &str = "Response Recorded"; const RESPONSE_RECORDED_DESCRIPTION: &str = "I've got your response. Hang tight while everyone submits theirs.\n(Oh, and you can still change your response while waiting!)"; +const YOUR_SUBMISSION_HEADER: &str = "Your Submission"; + const ALL_RESPONSES_IN_HEADER: &str = "All Responses In"; const ALL_RESPONSES_IN_DESCRIPTION: &str = "Amazing! Everyone submitted a response before time ran out!"; @@ -126,8 +132,6 @@ const ALL_GUESSES_AND_AWARDS_IN_HEADER: &str = "All Guesses & Awards In"; const ALL_GUESSES_AND_AWARDS_IN_DESCRIPTION: &str = "Hurray! Everyone's guessed and awarded a response!"; -const REJECTED_RESPONSE_HEADER: &str = "Your Submission"; - const TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_HEADER: &str = "Guessing & Awarding Time Over"; const TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_DESCRIPTION: &str = "Time ran out before everyone submitted a guess *and* award. It's their loss; let's just move on."; @@ -137,22 +141,22 @@ const NOT_TIME_TO_GUESS_DESCRIPTION: &str = "It's not the guessing phase anymore const NOT_TIME_TO_AWARD_HEADER: &str = "Can't Award Now"; const NOT_TIME_TO_AWARD_DESCRIPTION: &str = "It's not the guessing phase anymore (or yet), so you can't give out an award. In fact, I'm surprised you've even encountered this error!"; -const AWARD_FROM_HEADER: &str = "Award from"; -const GUESSED_BY_HEADER: &str = "Guessed by"; -const FOUND_BY_HEADER: &str = "Found by"; -const WRITTEN_BY_HEADER: &str = "Written by"; +const AWARD_FROM_HEADER: &str = "🏆 Award from"; +const GUESSED_BY_HEADER: &str = "🔘 Guessed by"; +const FOUND_BY_HEADER: &str = "🔎 Found by"; +const WRITTEN_BY_HEADER: &str = "✏️ Written by"; -const IMPERSONATION_TITLE: &str = "Impersonation!"; -const AUTHENTIC_TITLE: &str = "The Authentic Response!"; -const MISSED_TITLE: &str = "Missed!"; +const IMPERSONATION_TITLE: &str = "👹 Impersonation!"; +const MISSED_TITLE: &str = "😞 Missed!"; +const AUTHENTIC_TITLE: &str = "❇️ The Authentic Response!"; const COLOR_WRONG: u32 = 0xef4444; const COLOR_MISSED: u32 = 0xeab308; const COLOR_CORRECT: u32 = 0x10b981; -const MIDGAME_LEADERBOARD_TITLE: &str = "Current Standings"; +const MIDGAME_LEADERBOARD_TITLE: &str = "📈 Current Standings"; -const PREFIX_ENDGAME_HEADER: &str = "Final Placements"; +const PREFIX_ENDGAME_HEADER: &str = "📊 Final Placements"; const PREFIX_ENDGAME_DESCRIPTION: &str = "Let's all see everyone's final score and place since this is the end of the game!"; @@ -190,40 +194,48 @@ fn format_names( } } -pub fn embed_for_no_lobby_for_press() -> Embed { +fn basic_embed_with_color(color: u32, title: &str, description: &str) -> EmbedBuilder { EmbedBuilder::new() - .title(NO_LOBBY_FOR_PRESS_HEADER) - .description(NO_LOBBY_FOR_PRESS_DESCRIPTION) + .color(color) + .title(title) + .description(description) .validate() .unwrap() - .build() } + +fn info_embed(title: &str, description: &str) -> EmbedBuilder { + basic_embed_with_color(COLOR_INFO, title, description) +} +fn warn_embed(title: &str, description: &str) -> EmbedBuilder { + basic_embed_with_color(COLOR_WARN, title, description) +} +fn error_embed(title: &str, description: &str) -> EmbedBuilder { + basic_embed_with_color(COLOR_ERROR, title, description) +} + +pub fn embed_for_no_lobby_for_press() -> Embed { + error_embed(NO_LOBBY_FOR_PRESS_HEADER, NO_LOBBY_FOR_PRESS_DESCRIPTION).build() +} + pub fn embed_for_no_lobby_for_response(response: String) -> Embed { - EmbedBuilder::new() - .title(NO_LOBBY_FOR_RESPONSE_HEADER) - .description(NO_LOBBY_FOR_RESPONSE_DESCRIPTION) - .field(EmbedFieldBuilder::new(REJECTED_RESPONSE_HEADER, response).build()) - .validate() - .unwrap() - .build() + error_embed( + NO_LOBBY_FOR_RESPONSE_HEADER, + NO_LOBBY_FOR_RESPONSE_DESCRIPTION, + ) + .field(EmbedFieldBuilder::new(YOUR_SUBMISSION_HEADER, response).build()) + .build() } pub fn embed_for_unrecognized_button() -> Embed { - EmbedBuilder::new() - .title(UNRECOGNIZED_BUTTON_HEADER) - .description(UNRECOGNIZED_BUTTON_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(UNRECOGNIZED_BUTTON_HEADER, UNRECOGNIZED_BUTTON_DESCRIPTION).build() } pub fn embed_for_not_meeting_minimum_necessary_players() -> Embed { - EmbedBuilder::new() - .title(INSUFFICIENT_PLAYERS_HEADER) - .description(INSUFFICIENT_PLAYERS_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed( + INSUFFICIENT_PLAYERS_HEADER, + INSUFFICIENT_PLAYERS_DESCRIPTION, + ) + .build() } pub fn embed_for_private_lobby_controls() -> Embed { @@ -264,81 +276,44 @@ pub fn embed_for_main_lobby_message( } pub fn embed_for_autoclosing_lobby() -> Embed { - EmbedBuilder::new() - .title(AUTOCLOSING_LOBBY_HEADER) - .description(AUTOCLOSING_LOBBY_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(AUTOCLOSING_LOBBY_HEADER, AUTOCLOSING_LOBBY_DESCRIPTION).build() } pub fn embed_for_successful_abort() -> Embed { - EmbedBuilder::new() - .title(SUCCESSFUL_ABORT_HEADER) - .description(SUCCESSFUL_ABORT_DESCRIPTION) - .validate() - .unwrap() - .build() + info_embed(SUCCESSFUL_ABORT_HEADER, SUCCESSFUL_ABORT_DESCRIPTION).build() } pub fn embed_for_already_existing_game() -> Embed { - EmbedBuilder::new() - .title(ALREADY_EXISTING_GAME_HEADER) - .description(ALREADY_EXISTING_GAME_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed( + ALREADY_EXISTING_GAME_HEADER, + ALREADY_EXISTING_GAME_DESCRIPTION, + ) + .build() } pub fn embed_for_game_already_running() -> Embed { - EmbedBuilder::new() - .title(GAME_ALREADY_RUNNING_HEADER) - .description(GAME_ALREADY_RUNNING_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed( + GAME_ALREADY_RUNNING_HEADER, + GAME_ALREADY_RUNNING_DESCRIPTION, + ) + .build() } pub fn embed_to_say_hello_into_game() -> Embed { - EmbedBuilder::new() - .title(HELLO_TO_GAME_HEADER) - .description(HELLO_TO_GAME_DESCRIPTION) - .validate() - .unwrap() - .build() + info_embed(HELLO_TO_GAME_HEADER, HELLO_TO_GAME_DESCRIPTION).build() } pub fn embed_to_say_goodbye_from_game() -> Embed { - EmbedBuilder::new() - .title(GOODBYE_FROM_GAME_HEADER) - .description(GOODBYE_FROM_GAME_DESCRIPTION) - .validate() - .unwrap() - .build() + info_embed(GOODBYE_FROM_GAME_HEADER, GOODBYE_FROM_GAME_DESCRIPTION).build() } pub fn embed_to_say_already_in_game() -> Embed { - EmbedBuilder::new() - .title(ALREADY_IN_GAME_HEADER) - .description(ALREADY_IN_GAME_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(ALREADY_IN_GAME_HEADER, ALREADY_IN_GAME_DESCRIPTION).build() } pub fn embed_to_say_already_not_in_game() -> Embed { - EmbedBuilder::new() - .title(ALREADY_NOT_IN_GAME_HEADER) - .description(ALREADY_NOT_IN_GAME_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(ALREADY_NOT_IN_GAME_HEADER, ALREADY_NOT_IN_GAME_DESCRIPTION).build() } pub fn embed_for_may_not_abort() -> Embed { - EmbedBuilder::new() - .title(MAY_NOT_ABORT_HEADER) - .description(MAY_NOT_ABORT_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(MAY_NOT_ABORT_HEADER, MAY_NOT_ABORT_DESCRIPTION).build() } fn format_description_for_new_turn(subject_name: &str) -> String { @@ -358,11 +333,11 @@ pub fn embed_for_new_turn_about_player_and_topic(subject_name: &str) -> Embed { fn format_title_for_writing_phase(subject_name: &str, game_topic: &GameTopic) -> String { match game_topic { - GameTopic::Text(topic) => format!("{subject_name}: on the topic of {topic}"), + GameTopic::Text(topic) => format!("✏️ {subject_name}: on the topic of {topic}"), GameTopic::Player(other_player_name) => { - format!("{subject_name}: writing about fellow player {other_player_name}") + format!("✏️ {subject_name}: writing about fellow player {other_player_name}") } - GameTopic::None => format!("{subject_name}... without a topic?!"), + GameTopic::None => format!("✏️ {subject_name}... without a topic?!"), } } @@ -421,10 +396,9 @@ pub fn embed_for_writing_responses_phase( .build() } -pub fn embed_for_response_recorded() -> Embed { - EmbedBuilder::new() - .title(RESPONSE_RECORDED_HEADER) - .description(RESPONSE_RECORDED_DESCRIPTION) +pub fn embed_for_response_recorded(response: &str) -> Embed { + info_embed(RESPONSE_RECORDED_HEADER, RESPONSE_RECORDED_DESCRIPTION) + .field(EmbedFieldBuilder::new(YOUR_SUBMISSION_HEADER, response)) .validate() .unwrap() .build() @@ -439,21 +413,15 @@ pub fn embed_for_all_responses_in() -> Embed { .build() } pub fn embed_for_time_up_not_all_responses_in() -> Embed { - EmbedBuilder::new() - .title(TIME_UP_NOT_ALL_RESPONSES_IN_HEADER) - .description(TIME_UP_NOT_ALL_RESPONSES_IN_DESCRIPTION) - .validate() - .unwrap() - .build() + warn_embed( + TIME_UP_NOT_ALL_RESPONSES_IN_HEADER, + TIME_UP_NOT_ALL_RESPONSES_IN_DESCRIPTION, + ) + .build() } pub fn embed_for_no_responses() -> Embed { - EmbedBuilder::new() - .title(NO_RESPONSES_HEADER) - .description(NO_RESPONSES_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(NO_RESPONSES_HEADER, NO_RESPONSES_DESCRIPTION).build() } fn format_description_for_prefix_guessing_phase( @@ -519,52 +487,25 @@ pub fn embed_for_postfix_guessing_phase( } pub fn embed_for_guess_recorded() -> Embed { - EmbedBuilder::new() - .title(GUESS_RECORDED_HEADER) - .description(GUESS_RECORDED_DESCRIPTION) - .validate() - .unwrap() - .build() + info_embed(GUESS_RECORDED_HEADER, GUESS_RECORDED_DESCRIPTION).build() } pub fn embed_for_award_recorded() -> Embed { - EmbedBuilder::new() - .title(AWARD_RECORDED_HEADER) - .description(AWARD_RECORDED_DESCRIPTION) - .validate() - .unwrap() - .build() + info_embed(AWARD_RECORDED_HEADER, AWARD_RECORDED_DESCRIPTION).build() } pub fn embed_for_no_self_awarding() -> Embed { - EmbedBuilder::new() - .title(NO_SELF_AWARDING_HEADER) - .description(NO_SELF_AWARDING_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(NO_SELF_AWARDING_HEADER, NO_SELF_AWARDING_DESCRIPTION).build() } pub fn embed_for_no_self_guessing() -> Embed { - EmbedBuilder::new() - .title(NO_SELF_GUESSING_HEADER) - .description(NO_SELF_GUESSING_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(NO_SELF_GUESSING_HEADER, NO_SELF_GUESSING_DESCRIPTION).build() } pub fn embed_for_no_subject_guessing() -> Embed { - EmbedBuilder::new() - .title(NO_SUBJECT_GUESSING_HEADER) - .description(NO_SUBJECT_GUESSING_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(NO_SUBJECT_GUESSING_HEADER, NO_SUBJECT_GUESSING_DESCRIPTION).build() } pub fn embed_for_not_time_to_respond(response: String) -> Embed { - EmbedBuilder::new() - .title(NOT_TIME_TO_RESPOND_HEADER) - .description(NOT_TIME_TO_RESPOND_DESCRIPTION) - .field(EmbedFieldBuilder::new(REJECTED_RESPONSE_HEADER, response)) + error_embed(NOT_TIME_TO_RESPOND_HEADER, NOT_TIME_TO_RESPOND_DESCRIPTION) + .field(EmbedFieldBuilder::new(YOUR_SUBMISSION_HEADER, response)) .validate() .unwrap() .build() @@ -579,29 +520,18 @@ pub fn embed_for_all_guesses_and_awards_in() -> Embed { .build() } pub fn embed_for_time_up_not_all_guesses_or_awards_in() -> Embed { - EmbedBuilder::new() - .title(TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_HEADER) - .description(TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_DESCRIPTION) - .validate() - .unwrap() - .build() + warn_embed( + TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_HEADER, + TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_DESCRIPTION, + ) + .build() } pub fn embed_for_not_time_to_guess() -> Embed { - EmbedBuilder::new() - .title(NOT_TIME_TO_GUESS_HEADER) - .description(NOT_TIME_TO_GUESS_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(NOT_TIME_TO_GUESS_HEADER, NOT_TIME_TO_GUESS_DESCRIPTION).build() } pub fn embed_for_not_time_to_award() -> Embed { - EmbedBuilder::new() - .title(NOT_TIME_TO_AWARD_HEADER) - .description(NOT_TIME_TO_AWARD_DESCRIPTION) - .validate() - .unwrap() - .build() + error_embed(NOT_TIME_TO_AWARD_HEADER, NOT_TIME_TO_AWARD_DESCRIPTION).build() } pub fn embed_for_focused_response( @@ -610,16 +540,19 @@ pub fn embed_for_focused_response( awarders: BTreeSet>, names: &BTreeMap, String>, ) -> Embed { - let guesser_names = format_names(guessers, names); - let awarder_names = format_names(awarders, names); + let mut embed = EmbedBuilder::new().description(response); - EmbedBuilder::new() - .description(response) - .field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build()) - .field(EmbedFieldBuilder::new(FOUND_BY_HEADER, guesser_names).build()) - .validate() - .unwrap() - .build() + if !awarders.is_empty() { + let awarder_names = format_names(awarders, names); + embed = embed.field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build()) + } + + if !guessers.is_empty() { + let guesser_names = format_names(guessers, names); + embed = embed.field(EmbedFieldBuilder::new(GUESSED_BY_HEADER, guesser_names).build()) + } + + embed.validate().unwrap().build() } pub fn embed_for_impersonation_reveal( @@ -629,16 +562,26 @@ pub fn embed_for_impersonation_reveal( awarders: BTreeSet>, names: &BTreeMap, String>, ) -> Embed { - let guesser_names = format_names(guessers, names); - let awarder_names = format_names(awarders, names); + let mut embed = EmbedBuilder::new().color(COLOR_WRONG).description(response); - EmbedBuilder::new() - .color(COLOR_WRONG) - .title(IMPERSONATION_TITLE) - .description(response) - .field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build()) - .field(EmbedFieldBuilder::new(GUESSED_BY_HEADER, guesser_names).build()) - .field(EmbedFieldBuilder::new(WRITTEN_BY_HEADER, fooler_name).build()) + if !awarders.is_empty() { + let awarder_names = format_names(awarders, names); + embed = embed.field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build()) + } + + if !guessers.is_empty() { + let guesser_names = format_names(guessers, names); + embed = embed.field(EmbedFieldBuilder::new(GUESSED_BY_HEADER, guesser_names).build()) + } + + embed + .field( + EmbedFieldBuilder::new( + IMPERSONATION_TITLE, + format!("{WRITTEN_BY_HEADER} {fooler_name}"), + ) + .build(), + ) .validate() .unwrap() .build() @@ -657,16 +600,20 @@ pub fn embed_for_subject_reveal( (COLOR_CORRECT, AUTHENTIC_TITLE) }; - let guesser_names = format_names(guessers, names); - let awarder_names = format_names(awarders, names); + let mut embed = EmbedBuilder::new().color(color).description(response); - EmbedBuilder::new() - .color(color) - .title(title) - .description(response) - .field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build()) - .field(EmbedFieldBuilder::new(FOUND_BY_HEADER, guesser_names).build()) - .field(EmbedFieldBuilder::new(WRITTEN_BY_HEADER, subject_name).build()) + if !awarders.is_empty() { + let awarder_names = format_names(awarders, names); + embed = embed.field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build()) + } + + if !guessers.is_empty() { + let guesser_names = format_names(guessers, names); + embed = embed.field(EmbedFieldBuilder::new(FOUND_BY_HEADER, guesser_names).build()) + } + + embed + .field(EmbedFieldBuilder::new(title, format!("{WRITTEN_BY_HEADER} {subject_name}")).build()) .validate() .unwrap() .build() @@ -769,8 +716,9 @@ pub fn embed_for_endgame_placement( "{player_names} with {score} points!\nJust one placement away from first! For that, you've each earned a silver medal: {silver_medals}\n\nNot to spoil your celebration, but wait, who actually got first?!" ) } else { - "{player_names} with {score} points!\nFor your impressive performance this game: 🥈" - .into() + format!( + "{player_names} with {score} points!\nFor your impressive performance this game: 🥈" + ) }, ), 3 => ( @@ -782,7 +730,7 @@ pub fn embed_for_endgame_placement( "{player_names} with {score} points!\nWe got bronze medals made just for you guys: {bronze_medals}" ) } else { - "{player_names} with {score} points!\nI think this belongs to you: 🥉".into() + format!("{player_names} with {score} points!\nI think this belongs to you: 🥉") }, ), more => ( diff --git a/discord-bot/src/game.rs b/discord-bot/src/game.rs index b3a9a61..6765f70 100644 --- a/discord-bot/src/game.rs +++ b/discord-bot/src/game.rs @@ -100,7 +100,7 @@ const AUTOCLOSE_MINS: u64 = 15; #[derive(Debug, Clone)] struct Settings { write_time: Duration, - guess_time: Duration, + guess_time_per_response: Duration, max_p: f64, @@ -113,8 +113,8 @@ struct Settings { impl Default for Settings { fn default() -> Self { Self { - write_time: Duration::from_secs(90), - guess_time: Duration::from_secs(90), + write_time: Duration::from_secs(70), + guess_time_per_response: Duration::from_secs(7), max_p: 2., @@ -545,19 +545,7 @@ pub async fn actor( // endregion: Join and settings time - let (initial_topics_sender, initial_topics_receiver) = oneshot::channel(); - - topic_picker - .send(topic_picker::Message::GiveTopics { - existing: vec![], - desired: performances.len(), - max_p: settings.max_p, - callback: initial_topics_sender, - }) - .await - .unwrap(); - - let mut topics = initial_topics_receiver.await.unwrap(); + let mut topics = Vec::new(); // region: Each player's turn @@ -569,6 +557,18 @@ pub async fn actor( while let Some(subject) = { random_remove_no_preserve_order(&mut needs_to_go, &mut rng()) } { already_gone.insert(subject); + let (new_topics_sender, new_topics_receiver) = oneshot::channel(); + topic_picker + .send(topic_picker::Message::GiveTopics { + existing: topics, + desired: performances.len(), + max_p: settings.max_p, + callback: new_topics_sender, + }) + .await + .unwrap(); + topics = new_topics_receiver.await.unwrap(); + let (_topic_id, topic) = topics[turn]; turn += 1; @@ -687,15 +687,6 @@ pub async fn actor( indexmap::map::Entry::Vacant(vacant_entry) => { vacant_entry.insert(Default::default()); - let (new_topics_sender, new_topics_receiver) = oneshot::channel(); - topic_picker.send(topic_picker::Message::GiveTopics { - existing: topics, - desired: performances.len(), - max_p: settings.max_p, - callback: new_topics_sender, - }).await.unwrap(); - topics = new_topics_receiver.await.unwrap(); - if !already_gone.contains(&user) { insert_into_vec_without_duplicating(&mut needs_to_go, user); } @@ -831,7 +822,7 @@ pub async fn actor( insert_into_vec_without_duplicating(&mut needs_to_go, user); } - responses.insert(user, response); + responses.insert(user, response.clone()); if let Err(error) = discord_client.interaction(application_id).create_response( interaction_id, @@ -839,7 +830,7 @@ pub async fn actor( &InteractionResponse { kind: InteractionResponseType::ChannelMessageWithSource, data: Some(InteractionResponseDataBuilder::new() - .embeds([embed_for_response_recorded()]) + .embeds([embed_for_response_recorded(&response)]) .flags(MessageFlags::EPHEMERAL) .build() ) @@ -1001,12 +992,17 @@ pub async fn actor( sleep(small_amount_of_reading_time).await; } + let responses_to_guess_on = responses.len(); + + let guess_time = + settings.guess_time_per_response * responses_to_guess_on.try_into().unwrap(); + let postfix_guessing_phase_message = match discord_client .create_message(channel_id) .embeds(&[embed_for_postfix_guessing_phase( &subject_name, &topic, - settings.guess_time, + guess_time, )]) .await { @@ -1037,11 +1033,11 @@ pub async fn actor( let mut guesses = BTreeMap::new(); let mut awards = BTreeMap::new(); - let mut time_shown_in_message = settings.guess_time; + let mut time_shown_in_message = guess_time; - let end_time = Instant::now() + settings.guess_time; + let end_time = Instant::now() + guess_time; - let guessing_time_up = sleep(settings.guess_time); + let guessing_time_up = sleep(guess_time); pin!(guessing_time_up); let mut about_every_half_second = interval(Duration::from_millis(500)); @@ -1086,15 +1082,6 @@ pub async fn actor( indexmap::map::Entry::Vacant(vacant_entry) => { vacant_entry.insert(Default::default()); - let (new_topics_sender, new_topics_receiver) = oneshot::channel(); - topic_picker.send(topic_picker::Message::GiveTopics { - existing: topics, - desired: performances.len(), - max_p: settings.max_p, - callback: new_topics_sender, - }).await.unwrap(); - topics = new_topics_receiver.await.unwrap(); - if let Err(error) = discord_client.interaction(application_id).create_response( interaction_id, interaction_token.expose_secret(), @@ -1262,7 +1249,6 @@ pub async fn actor( tracing::warn!(?error, ?user, "couldn't inform that their guess was recorded"); } - if guesses.len() == performances.len().saturating_sub(1) && awards.len() == performances.len() { if let Err(err) = discord_client.create_message(channel_id) .embeds(&[embed_for_all_guesses_and_awards_in()]) @@ -1455,7 +1441,7 @@ pub async fn actor( Ok(message_response) => match message_response.model().await { Ok(message) => { // Delay before showing the answer so people have time to read (and for drama) - let reading_time = Duration::from_secs(2); + let reading_time = Duration::from_secs(3); sleep(reading_time).await; if let Err(error) = discord_client @@ -1492,62 +1478,67 @@ pub async fn actor( } // Delay before showing the next one so people have time to read - sleep(Duration::from_millis(2500)).await; + let reading_time = Duration::from_secs(3); + sleep(reading_time).await; } // endregion: Responses that fooled someone // region: The subject's response - let response = responses.get(&subject).unwrap(); - let awarders = awarders_of_response.remove(&subject).unwrap_or_default(); + if let Some(response) = responses.get(&subject) { + let awarders = awarders_of_response.remove(&subject).unwrap_or_default(); - match discord_client - .create_message(channel_id) - .embeds(&[embed_for_focused_response( - response, - guessed_subject.clone(), - awarders.clone(), - &names, - )]) - .await - { - Ok(message_response) => match message_response.model().await { - Ok(message) => { - // Delay before showing the answer so people have time to read (and for drama) - let reading_time = Duration::from_secs(2); - sleep(reading_time).await; + match discord_client + .create_message(channel_id) + .embeds(&[embed_for_focused_response( + response, + guessed_subject.clone(), + awarders.clone(), + &names, + )]) + .await + { + Ok(message_response) => match message_response.model().await { + Ok(message) => { + // Delay before showing the answer so people have time to read (and for drama) + let reading_time = Duration::from_secs(3); + sleep(reading_time).await; - if let Err(error) = discord_client - .update_message(channel_id, message.id) - .embeds(Some(&[embed_for_subject_reveal( - &subject_name, - &response, - guessed_subject, - awarders, - &names, - )])) - .await - { + if let Err(error) = discord_client + .update_message(channel_id, message.id) + .embeds(Some(&[embed_for_subject_reveal( + &subject_name, + &response, + guessed_subject, + awarders, + &names, + )])) + .await + { + tracing::error!( + ?error, + "couldn't update the message to show who the actual author (the subject) was!" + ); + } + + let reading_time = Duration::from_secs(3); + sleep(reading_time).await; + } + Err(error) => { tracing::error!( ?error, - "couldn't update the message to show who the actual author (the subject) was!" + "couldn't deserialize the sent message to be able to update it with the actual author" ); } - } + }, Err(error) => { tracing::error!( ?error, - "couldn't deserialize the sent message to be able to update it with the actual author" + "couldn't send a message showing how this guess shook out; the points will still count but we have no choice but to move on to the next one" ); + continue; } - }, - Err(error) => { - tracing::error!( - ?error, - "couldn't send a message showing how this guess shook out; the points will still count but we have no choice but to move on to the next one" - ); - continue; } } // endregion: The subject's response @@ -1618,6 +1609,9 @@ pub async fn actor( tracing::warn!(?error, "failed to show a mid-game leaderboard"); } + let reading_time = Duration::from_secs(4); + sleep(reading_time).await; + // endregion: Show the leaderboard (currently) // endregion: Showing this turn's results @@ -1632,7 +1626,7 @@ pub async fn actor( tracing::warn!(?error, "failed to inform of the missing inverse round I have in mind"); } - let reading_time = Duration::from_secs(2); + let reading_time = Duration::from_secs(3); sleep(reading_time).await; if let Err(error) = discord_client @@ -1643,7 +1637,7 @@ pub async fn actor( tracing::warn!(?error, "failed to introduce the endgame placements"); } - let reading_time = Duration::from_secs(2); + let reading_time = Duration::from_secs(3); sleep(reading_time).await; let scores = BTreeMap::from_iter( @@ -1656,7 +1650,7 @@ pub async fn actor( let leaderboard = create_leaderboard(scores); for (score, (placement, players)) in leaderboard { - let drama = Duration::from_secs(5).div_f64(placement as f64); + let drama = Duration::from_secs(7).div_f64(placement as f64); sleep(drama).await; if let Err(error) = discord_client @@ -1675,7 +1669,7 @@ pub async fn actor( } } - let time_focus_on_winners_before_saying_end_of_game = Duration::from_secs(3); + let time_focus_on_winners_before_saying_end_of_game = Duration::from_secs(5); sleep(time_focus_on_winners_before_saying_end_of_game).await; if let Err(error) = discord_client diff --git a/discord-bot/src/main.rs b/discord-bot/src/main.rs index d688a7e..8a299ff 100644 --- a/discord-bot/src/main.rs +++ b/discord-bot/src/main.rs @@ -149,7 +149,7 @@ async fn main() -> Result<(), AppError> { Event::InteractionCreate(interaction_create) => { let InteractionCreate(interaction) = *interaction_create; - tracing::info!(?interaction); + tracing::debug!(?interaction); match &interaction.data { None => { diff --git a/discord-bot/src/topic_picker.rs b/discord-bot/src/topic_picker.rs index b749205..de6193c 100644 --- a/discord-bot/src/topic_picker.rs +++ b/discord-bot/src/topic_picker.rs @@ -59,13 +59,14 @@ pub async fn actor(operator: Operator, mut messages: mpsc::Receiver) { while let Some(message) = messages.recv().await { match message { Message::GiveTopics { - mut existing, + existing, desired, max_p, callback, } => { let difference = (desired as isize) - (existing.len() as isize); - match usize::try_from(difference) { + let topics = match usize::try_from(difference) { + Ok(0) => existing, Ok(_positive) => { let exclude = BTreeSet::from_iter(existing.into_iter().map(|(index, _topic)| index)); @@ -83,22 +84,26 @@ pub async fn actor(operator: Operator, mut messages: mpsc::Receiver) { occurrences[*topic_id] = occurrences[*topic_id].saturating_add(1); } - // if the other end dropped, so what? - let _ = callback.send(chosen_ids_and_topics); + chosen_ids_and_topics } Err(_negative_error) => { + let mut trimmed = existing; + let chop_off = difference.abs() as usize; for _ in 0..chop_off { - let (topic_id, _occurrence) = existing.pop().unwrap(); + let (topic_id, _occurrence) = trimmed.pop().unwrap(); occurrences[topic_id] = occurrences[topic_id].saturating_sub(1); } - // if the other end dropped, so what? - let _ = callback.send(existing); + trimmed } - } + }; + + tracing::info!(?topics, "going to give"); + // if the other end dropped, so what? + let _ = callback.send(topics); tracing::info!(?occurrences, "going to save");