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
This commit is contained in:
2025-04-19 15:33:50 -04:00
parent 0625fd2fea
commit d5705f7648
4 changed files with 230 additions and 283 deletions

View File

@@ -16,6 +16,10 @@ use twilight_util::builder::embed::{EmbedBuilder, EmbedFieldBuilder, EmbedFooter
use crate::game::GameTopic; 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_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?"; 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_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 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_HEADER: &str = "All Responses In";
const ALL_RESPONSES_IN_DESCRIPTION: &str = const ALL_RESPONSES_IN_DESCRIPTION: &str =
"Amazing! Everyone submitted a response before time ran out!"; "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 = const ALL_GUESSES_AND_AWARDS_IN_DESCRIPTION: &str =
"Hurray! Everyone's guessed and awarded a response!"; "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_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."; 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_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 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 AWARD_FROM_HEADER: &str = "🏆 Award from";
const GUESSED_BY_HEADER: &str = "Guessed by"; const GUESSED_BY_HEADER: &str = "🔘 Guessed by";
const FOUND_BY_HEADER: &str = "Found by"; const FOUND_BY_HEADER: &str = "🔎 Found by";
const WRITTEN_BY_HEADER: &str = "Written by"; const WRITTEN_BY_HEADER: &str = "✏️ Written by";
const IMPERSONATION_TITLE: &str = "Impersonation!"; const IMPERSONATION_TITLE: &str = "👹 Impersonation!";
const AUTHENTIC_TITLE: &str = "The Authentic Response!"; const MISSED_TITLE: &str = "😞 Missed!";
const MISSED_TITLE: &str = "Missed!"; const AUTHENTIC_TITLE: &str = "❇️ The Authentic Response!";
const COLOR_WRONG: u32 = 0xef4444; const COLOR_WRONG: u32 = 0xef4444;
const COLOR_MISSED: u32 = 0xeab308; const COLOR_MISSED: u32 = 0xeab308;
const COLOR_CORRECT: u32 = 0x10b981; 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 = const PREFIX_ENDGAME_DESCRIPTION: &str =
"Let's all see everyone's final score and place since this is the end of the game!"; "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() EmbedBuilder::new()
.title(NO_LOBBY_FOR_PRESS_HEADER) .color(color)
.description(NO_LOBBY_FOR_PRESS_DESCRIPTION) .title(title)
.description(description)
.validate() .validate()
.unwrap() .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 { pub fn embed_for_no_lobby_for_response(response: String) -> Embed {
EmbedBuilder::new() error_embed(
.title(NO_LOBBY_FOR_RESPONSE_HEADER) NO_LOBBY_FOR_RESPONSE_HEADER,
.description(NO_LOBBY_FOR_RESPONSE_DESCRIPTION) NO_LOBBY_FOR_RESPONSE_DESCRIPTION,
.field(EmbedFieldBuilder::new(REJECTED_RESPONSE_HEADER, response).build()) )
.validate() .field(EmbedFieldBuilder::new(YOUR_SUBMISSION_HEADER, response).build())
.unwrap() .build()
.build()
} }
pub fn embed_for_unrecognized_button() -> Embed { pub fn embed_for_unrecognized_button() -> Embed {
EmbedBuilder::new() error_embed(UNRECOGNIZED_BUTTON_HEADER, UNRECOGNIZED_BUTTON_DESCRIPTION).build()
.title(UNRECOGNIZED_BUTTON_HEADER)
.description(UNRECOGNIZED_BUTTON_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_not_meeting_minimum_necessary_players() -> Embed { pub fn embed_for_not_meeting_minimum_necessary_players() -> Embed {
EmbedBuilder::new() error_embed(
.title(INSUFFICIENT_PLAYERS_HEADER) INSUFFICIENT_PLAYERS_HEADER,
.description(INSUFFICIENT_PLAYERS_DESCRIPTION) INSUFFICIENT_PLAYERS_DESCRIPTION,
.validate() )
.unwrap() .build()
.build()
} }
pub fn embed_for_private_lobby_controls() -> Embed { 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 { pub fn embed_for_autoclosing_lobby() -> Embed {
EmbedBuilder::new() error_embed(AUTOCLOSING_LOBBY_HEADER, AUTOCLOSING_LOBBY_DESCRIPTION).build()
.title(AUTOCLOSING_LOBBY_HEADER)
.description(AUTOCLOSING_LOBBY_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_successful_abort() -> Embed { pub fn embed_for_successful_abort() -> Embed {
EmbedBuilder::new() info_embed(SUCCESSFUL_ABORT_HEADER, SUCCESSFUL_ABORT_DESCRIPTION).build()
.title(SUCCESSFUL_ABORT_HEADER)
.description(SUCCESSFUL_ABORT_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_already_existing_game() -> Embed { pub fn embed_for_already_existing_game() -> Embed {
EmbedBuilder::new() error_embed(
.title(ALREADY_EXISTING_GAME_HEADER) ALREADY_EXISTING_GAME_HEADER,
.description(ALREADY_EXISTING_GAME_DESCRIPTION) ALREADY_EXISTING_GAME_DESCRIPTION,
.validate() )
.unwrap() .build()
.build()
} }
pub fn embed_for_game_already_running() -> Embed { pub fn embed_for_game_already_running() -> Embed {
EmbedBuilder::new() error_embed(
.title(GAME_ALREADY_RUNNING_HEADER) GAME_ALREADY_RUNNING_HEADER,
.description(GAME_ALREADY_RUNNING_DESCRIPTION) GAME_ALREADY_RUNNING_DESCRIPTION,
.validate() )
.unwrap() .build()
.build()
} }
pub fn embed_to_say_hello_into_game() -> Embed { pub fn embed_to_say_hello_into_game() -> Embed {
EmbedBuilder::new() info_embed(HELLO_TO_GAME_HEADER, HELLO_TO_GAME_DESCRIPTION).build()
.title(HELLO_TO_GAME_HEADER)
.description(HELLO_TO_GAME_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_to_say_goodbye_from_game() -> Embed { pub fn embed_to_say_goodbye_from_game() -> Embed {
EmbedBuilder::new() info_embed(GOODBYE_FROM_GAME_HEADER, GOODBYE_FROM_GAME_DESCRIPTION).build()
.title(GOODBYE_FROM_GAME_HEADER)
.description(GOODBYE_FROM_GAME_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_to_say_already_in_game() -> Embed { pub fn embed_to_say_already_in_game() -> Embed {
EmbedBuilder::new() error_embed(ALREADY_IN_GAME_HEADER, ALREADY_IN_GAME_DESCRIPTION).build()
.title(ALREADY_IN_GAME_HEADER)
.description(ALREADY_IN_GAME_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_to_say_already_not_in_game() -> Embed { pub fn embed_to_say_already_not_in_game() -> Embed {
EmbedBuilder::new() error_embed(ALREADY_NOT_IN_GAME_HEADER, ALREADY_NOT_IN_GAME_DESCRIPTION).build()
.title(ALREADY_NOT_IN_GAME_HEADER)
.description(ALREADY_NOT_IN_GAME_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_may_not_abort() -> Embed { pub fn embed_for_may_not_abort() -> Embed {
EmbedBuilder::new() error_embed(MAY_NOT_ABORT_HEADER, MAY_NOT_ABORT_DESCRIPTION).build()
.title(MAY_NOT_ABORT_HEADER)
.description(MAY_NOT_ABORT_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
fn format_description_for_new_turn(subject_name: &str) -> String { 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 { fn format_title_for_writing_phase(subject_name: &str, game_topic: &GameTopic) -> String {
match game_topic { 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) => { 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() .build()
} }
pub fn embed_for_response_recorded() -> Embed { pub fn embed_for_response_recorded(response: &str) -> Embed {
EmbedBuilder::new() info_embed(RESPONSE_RECORDED_HEADER, RESPONSE_RECORDED_DESCRIPTION)
.title(RESPONSE_RECORDED_HEADER) .field(EmbedFieldBuilder::new(YOUR_SUBMISSION_HEADER, response))
.description(RESPONSE_RECORDED_DESCRIPTION)
.validate() .validate()
.unwrap() .unwrap()
.build() .build()
@@ -439,21 +413,15 @@ pub fn embed_for_all_responses_in() -> Embed {
.build() .build()
} }
pub fn embed_for_time_up_not_all_responses_in() -> Embed { pub fn embed_for_time_up_not_all_responses_in() -> Embed {
EmbedBuilder::new() warn_embed(
.title(TIME_UP_NOT_ALL_RESPONSES_IN_HEADER) TIME_UP_NOT_ALL_RESPONSES_IN_HEADER,
.description(TIME_UP_NOT_ALL_RESPONSES_IN_DESCRIPTION) TIME_UP_NOT_ALL_RESPONSES_IN_DESCRIPTION,
.validate() )
.unwrap() .build()
.build()
} }
pub fn embed_for_no_responses() -> Embed { pub fn embed_for_no_responses() -> Embed {
EmbedBuilder::new() error_embed(NO_RESPONSES_HEADER, NO_RESPONSES_DESCRIPTION).build()
.title(NO_RESPONSES_HEADER)
.description(NO_RESPONSES_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
fn format_description_for_prefix_guessing_phase( 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 { pub fn embed_for_guess_recorded() -> Embed {
EmbedBuilder::new() info_embed(GUESS_RECORDED_HEADER, GUESS_RECORDED_DESCRIPTION).build()
.title(GUESS_RECORDED_HEADER)
.description(GUESS_RECORDED_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_award_recorded() -> Embed { pub fn embed_for_award_recorded() -> Embed {
EmbedBuilder::new() info_embed(AWARD_RECORDED_HEADER, AWARD_RECORDED_DESCRIPTION).build()
.title(AWARD_RECORDED_HEADER)
.description(AWARD_RECORDED_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_no_self_awarding() -> Embed { pub fn embed_for_no_self_awarding() -> Embed {
EmbedBuilder::new() error_embed(NO_SELF_AWARDING_HEADER, NO_SELF_AWARDING_DESCRIPTION).build()
.title(NO_SELF_AWARDING_HEADER)
.description(NO_SELF_AWARDING_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_no_self_guessing() -> Embed { pub fn embed_for_no_self_guessing() -> Embed {
EmbedBuilder::new() error_embed(NO_SELF_GUESSING_HEADER, NO_SELF_GUESSING_DESCRIPTION).build()
.title(NO_SELF_GUESSING_HEADER)
.description(NO_SELF_GUESSING_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_no_subject_guessing() -> Embed { pub fn embed_for_no_subject_guessing() -> Embed {
EmbedBuilder::new() error_embed(NO_SUBJECT_GUESSING_HEADER, NO_SUBJECT_GUESSING_DESCRIPTION).build()
.title(NO_SUBJECT_GUESSING_HEADER)
.description(NO_SUBJECT_GUESSING_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_not_time_to_respond(response: String) -> Embed { pub fn embed_for_not_time_to_respond(response: String) -> Embed {
EmbedBuilder::new() error_embed(NOT_TIME_TO_RESPOND_HEADER, NOT_TIME_TO_RESPOND_DESCRIPTION)
.title(NOT_TIME_TO_RESPOND_HEADER) .field(EmbedFieldBuilder::new(YOUR_SUBMISSION_HEADER, response))
.description(NOT_TIME_TO_RESPOND_DESCRIPTION)
.field(EmbedFieldBuilder::new(REJECTED_RESPONSE_HEADER, response))
.validate() .validate()
.unwrap() .unwrap()
.build() .build()
@@ -579,29 +520,18 @@ pub fn embed_for_all_guesses_and_awards_in() -> Embed {
.build() .build()
} }
pub fn embed_for_time_up_not_all_guesses_or_awards_in() -> Embed { pub fn embed_for_time_up_not_all_guesses_or_awards_in() -> Embed {
EmbedBuilder::new() warn_embed(
.title(TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_HEADER) TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_HEADER,
.description(TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_DESCRIPTION) TIME_UP_NOT_ALL_GUESSES_OR_AWARDS_IN_DESCRIPTION,
.validate() )
.unwrap() .build()
.build()
} }
pub fn embed_for_not_time_to_guess() -> Embed { pub fn embed_for_not_time_to_guess() -> Embed {
EmbedBuilder::new() error_embed(NOT_TIME_TO_GUESS_HEADER, NOT_TIME_TO_GUESS_DESCRIPTION).build()
.title(NOT_TIME_TO_GUESS_HEADER)
.description(NOT_TIME_TO_GUESS_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_not_time_to_award() -> Embed { pub fn embed_for_not_time_to_award() -> Embed {
EmbedBuilder::new() error_embed(NOT_TIME_TO_AWARD_HEADER, NOT_TIME_TO_AWARD_DESCRIPTION).build()
.title(NOT_TIME_TO_AWARD_HEADER)
.description(NOT_TIME_TO_AWARD_DESCRIPTION)
.validate()
.unwrap()
.build()
} }
pub fn embed_for_focused_response( pub fn embed_for_focused_response(
@@ -610,16 +540,19 @@ pub fn embed_for_focused_response(
awarders: BTreeSet<Id<UserMarker>>, awarders: BTreeSet<Id<UserMarker>>,
names: &BTreeMap<Id<UserMarker>, String>, names: &BTreeMap<Id<UserMarker>, String>,
) -> Embed { ) -> Embed {
let guesser_names = format_names(guessers, names); let mut embed = EmbedBuilder::new().description(response);
let awarder_names = format_names(awarders, names);
EmbedBuilder::new() if !awarders.is_empty() {
.description(response) let awarder_names = format_names(awarders, names);
.field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build()) embed = embed.field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build())
.field(EmbedFieldBuilder::new(FOUND_BY_HEADER, guesser_names).build()) }
.validate()
.unwrap() if !guessers.is_empty() {
.build() 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( pub fn embed_for_impersonation_reveal(
@@ -629,16 +562,26 @@ pub fn embed_for_impersonation_reveal(
awarders: BTreeSet<Id<UserMarker>>, awarders: BTreeSet<Id<UserMarker>>,
names: &BTreeMap<Id<UserMarker>, String>, names: &BTreeMap<Id<UserMarker>, String>,
) -> Embed { ) -> Embed {
let guesser_names = format_names(guessers, names); let mut embed = EmbedBuilder::new().color(COLOR_WRONG).description(response);
let awarder_names = format_names(awarders, names);
EmbedBuilder::new() if !awarders.is_empty() {
.color(COLOR_WRONG) let awarder_names = format_names(awarders, names);
.title(IMPERSONATION_TITLE) embed = embed.field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build())
.description(response) }
.field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build())
.field(EmbedFieldBuilder::new(GUESSED_BY_HEADER, guesser_names).build()) if !guessers.is_empty() {
.field(EmbedFieldBuilder::new(WRITTEN_BY_HEADER, fooler_name).build()) 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() .validate()
.unwrap() .unwrap()
.build() .build()
@@ -657,16 +600,20 @@ pub fn embed_for_subject_reveal(
(COLOR_CORRECT, AUTHENTIC_TITLE) (COLOR_CORRECT, AUTHENTIC_TITLE)
}; };
let guesser_names = format_names(guessers, names); let mut embed = EmbedBuilder::new().color(color).description(response);
let awarder_names = format_names(awarders, names);
EmbedBuilder::new() if !awarders.is_empty() {
.color(color) let awarder_names = format_names(awarders, names);
.title(title) embed = embed.field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build())
.description(response) }
.field(EmbedFieldBuilder::new(AWARD_FROM_HEADER, awarder_names).build())
.field(EmbedFieldBuilder::new(FOUND_BY_HEADER, guesser_names).build()) if !guessers.is_empty() {
.field(EmbedFieldBuilder::new(WRITTEN_BY_HEADER, subject_name).build()) 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() .validate()
.unwrap() .unwrap()
.build() .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?!" "{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 { } else {
"{player_names} with {score} points!\nFor your impressive performance this game: 🥈" format!(
.into() "{player_names} with {score} points!\nFor your impressive performance this game: 🥈"
)
}, },
), ),
3 => ( 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}" "{player_names} with {score} points!\nWe got bronze medals made just for you guys: {bronze_medals}"
) )
} else { } 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 => ( more => (

View File

@@ -100,7 +100,7 @@ const AUTOCLOSE_MINS: u64 = 15;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Settings { struct Settings {
write_time: Duration, write_time: Duration,
guess_time: Duration, guess_time_per_response: Duration,
max_p: f64, max_p: f64,
@@ -113,8 +113,8 @@ struct Settings {
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Self { Self {
write_time: Duration::from_secs(90), write_time: Duration::from_secs(70),
guess_time: Duration::from_secs(90), guess_time_per_response: Duration::from_secs(7),
max_p: 2., max_p: 2.,
@@ -545,19 +545,7 @@ pub async fn actor(
// endregion: Join and settings time // endregion: Join and settings time
let (initial_topics_sender, initial_topics_receiver) = oneshot::channel(); let mut topics = Vec::new();
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();
// region: Each player's turn // 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()) } { while let Some(subject) = { random_remove_no_preserve_order(&mut needs_to_go, &mut rng()) } {
already_gone.insert(subject); 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]; let (_topic_id, topic) = topics[turn];
turn += 1; turn += 1;
@@ -687,15 +687,6 @@ pub async fn actor(
indexmap::map::Entry::Vacant(vacant_entry) => { indexmap::map::Entry::Vacant(vacant_entry) => {
vacant_entry.insert(Default::default()); 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) { if !already_gone.contains(&user) {
insert_into_vec_without_duplicating(&mut needs_to_go, 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); 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( if let Err(error) = discord_client.interaction(application_id).create_response(
interaction_id, interaction_id,
@@ -839,7 +830,7 @@ pub async fn actor(
&InteractionResponse { &InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource, kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(InteractionResponseDataBuilder::new() data: Some(InteractionResponseDataBuilder::new()
.embeds([embed_for_response_recorded()]) .embeds([embed_for_response_recorded(&response)])
.flags(MessageFlags::EPHEMERAL) .flags(MessageFlags::EPHEMERAL)
.build() .build()
) )
@@ -1001,12 +992,17 @@ pub async fn actor(
sleep(small_amount_of_reading_time).await; 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 let postfix_guessing_phase_message = match discord_client
.create_message(channel_id) .create_message(channel_id)
.embeds(&[embed_for_postfix_guessing_phase( .embeds(&[embed_for_postfix_guessing_phase(
&subject_name, &subject_name,
&topic, &topic,
settings.guess_time, guess_time,
)]) )])
.await .await
{ {
@@ -1037,11 +1033,11 @@ pub async fn actor(
let mut guesses = BTreeMap::new(); let mut guesses = BTreeMap::new();
let mut awards = 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); pin!(guessing_time_up);
let mut about_every_half_second = interval(Duration::from_millis(500)); 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) => { indexmap::map::Entry::Vacant(vacant_entry) => {
vacant_entry.insert(Default::default()); 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( if let Err(error) = discord_client.interaction(application_id).create_response(
interaction_id, interaction_id,
interaction_token.expose_secret(), interaction_token.expose_secret(),
@@ -1262,7 +1249,6 @@ pub async fn actor(
tracing::warn!(?error, ?user, "couldn't inform that their guess was recorded"); 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 guesses.len() == performances.len().saturating_sub(1) && awards.len() == performances.len() {
if let Err(err) = discord_client.create_message(channel_id) if let Err(err) = discord_client.create_message(channel_id)
.embeds(&[embed_for_all_guesses_and_awards_in()]) .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_response) => match message_response.model().await {
Ok(message) => { Ok(message) => {
// Delay before showing the answer so people have time to read (and for drama) // 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; sleep(reading_time).await;
if let Err(error) = discord_client 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 // 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 // endregion: Responses that fooled someone
// region: The subject's response // region: The subject's response
let response = responses.get(&subject).unwrap(); if let Some(response) = responses.get(&subject) {
let awarders = awarders_of_response.remove(&subject).unwrap_or_default(); let awarders = awarders_of_response.remove(&subject).unwrap_or_default();
match discord_client match discord_client
.create_message(channel_id) .create_message(channel_id)
.embeds(&[embed_for_focused_response( .embeds(&[embed_for_focused_response(
response, response,
guessed_subject.clone(), guessed_subject.clone(),
awarders.clone(), awarders.clone(),
&names, &names,
)]) )])
.await .await
{ {
Ok(message_response) => match message_response.model().await { Ok(message_response) => match message_response.model().await {
Ok(message) => { Ok(message) => {
// Delay before showing the answer so people have time to read (and for drama) // 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; sleep(reading_time).await;
if let Err(error) = discord_client if let Err(error) = discord_client
.update_message(channel_id, message.id) .update_message(channel_id, message.id)
.embeds(Some(&[embed_for_subject_reveal( .embeds(Some(&[embed_for_subject_reveal(
&subject_name, &subject_name,
&response, &response,
guessed_subject, guessed_subject,
awarders, awarders,
&names, &names,
)])) )]))
.await .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!( tracing::error!(
?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) => { Err(error) => {
tracing::error!( tracing::error!(
?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 // endregion: The subject's response
@@ -1618,6 +1609,9 @@ pub async fn actor(
tracing::warn!(?error, "failed to show a mid-game leaderboard"); 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: Show the leaderboard (currently)
// endregion: Showing this turn's results // 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"); 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; sleep(reading_time).await;
if let Err(error) = discord_client if let Err(error) = discord_client
@@ -1643,7 +1637,7 @@ pub async fn actor(
tracing::warn!(?error, "failed to introduce the endgame placements"); 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; sleep(reading_time).await;
let scores = BTreeMap::from_iter( let scores = BTreeMap::from_iter(
@@ -1656,7 +1650,7 @@ pub async fn actor(
let leaderboard = create_leaderboard(scores); let leaderboard = create_leaderboard(scores);
for (score, (placement, players)) in leaderboard { 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; sleep(drama).await;
if let Err(error) = discord_client 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; sleep(time_focus_on_winners_before_saying_end_of_game).await;
if let Err(error) = discord_client if let Err(error) = discord_client

View File

@@ -149,7 +149,7 @@ async fn main() -> Result<(), AppError> {
Event::InteractionCreate(interaction_create) => { Event::InteractionCreate(interaction_create) => {
let InteractionCreate(interaction) = *interaction_create; let InteractionCreate(interaction) = *interaction_create;
tracing::info!(?interaction); tracing::debug!(?interaction);
match &interaction.data { match &interaction.data {
None => { None => {

View File

@@ -59,13 +59,14 @@ pub async fn actor(operator: Operator, mut messages: mpsc::Receiver<Message>) {
while let Some(message) = messages.recv().await { while let Some(message) = messages.recv().await {
match message { match message {
Message::GiveTopics { Message::GiveTopics {
mut existing, existing,
desired, desired,
max_p, max_p,
callback, callback,
} => { } => {
let difference = (desired as isize) - (existing.len() as isize); 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) => { Ok(_positive) => {
let exclude = let exclude =
BTreeSet::from_iter(existing.into_iter().map(|(index, _topic)| index)); BTreeSet::from_iter(existing.into_iter().map(|(index, _topic)| index));
@@ -83,22 +84,26 @@ pub async fn actor(operator: Operator, mut messages: mpsc::Receiver<Message>) {
occurrences[*topic_id] = occurrences[*topic_id].saturating_add(1); occurrences[*topic_id] = occurrences[*topic_id].saturating_add(1);
} }
// if the other end dropped, so what? chosen_ids_and_topics
let _ = callback.send(chosen_ids_and_topics);
} }
Err(_negative_error) => { Err(_negative_error) => {
let mut trimmed = existing;
let chop_off = difference.abs() as usize; let chop_off = difference.abs() as usize;
for _ in 0..chop_off { 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); occurrences[topic_id] = occurrences[topic_id].saturating_sub(1);
} }
// if the other end dropped, so what? trimmed
let _ = callback.send(existing);
} }
} };
tracing::info!(?topics, "going to give");
// if the other end dropped, so what?
let _ = callback.send(topics);
tracing::info!(?occurrences, "going to save"); tracing::info!(?occurrences, "going to save");