diff --git a/src/main/java/de/tosoxdev/tosoxjr/commands/CommandManager.java b/src/main/java/de/tosoxdev/tosoxjr/commands/CommandManager.java index 261ded3..0a68147 100644 --- a/src/main/java/de/tosoxdev/tosoxjr/commands/CommandManager.java +++ b/src/main/java/de/tosoxdev/tosoxjr/commands/CommandManager.java @@ -6,6 +6,7 @@ import de.tosoxdev.tosoxjr.commands.joke.JokeCmd; import de.tosoxdev.tosoxjr.commands.quote.QuoteCmd; import de.tosoxdev.tosoxjr.commands.say.SayCmd; +import de.tosoxdev.tosoxjr.commands.scramble.ScrambleCmd; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.ArrayList; @@ -20,7 +21,9 @@ public CommandManager() { addCommand(new QuoteCmd()); addCommand(new JokeCmd()); addCommand(new CSStatsCmd()); + addCommand(new HangmanCmd()); + addCommand(new ScrambleCmd()); } public List getCommands() { diff --git a/src/main/java/de/tosoxdev/tosoxjr/commands/hangman/GameState.java b/src/main/java/de/tosoxdev/tosoxjr/commands/hangman/GameState.java new file mode 100644 index 0000000..79cb443 --- /dev/null +++ b/src/main/java/de/tosoxdev/tosoxjr/commands/hangman/GameState.java @@ -0,0 +1,18 @@ +package de.tosoxdev.tosoxjr.commands.hangman; + +enum GameState { + ONGOING("Hangman"), + WIN("You won!"), + DEFEAT("You lost!"), + TIMEOUT("Timeout"); + + private final String title; + + GameState(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } +} diff --git a/src/main/java/de/tosoxdev/tosoxjr/commands/hangman/Hangman.java b/src/main/java/de/tosoxdev/tosoxjr/commands/hangman/Hangman.java index 71d4dbb..7ce24e8 100644 --- a/src/main/java/de/tosoxdev/tosoxjr/commands/hangman/Hangman.java +++ b/src/main/java/de/tosoxdev/tosoxjr/commands/hangman/Hangman.java @@ -15,31 +15,23 @@ import java.awt.*; import java.util.*; +import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; -enum GameState { - ONGOING("Hangman"), - WIN("You won!"), - DEFEAT("You lost!"), - TIMEOUT("Timeout"); - - private final String title; - - GameState(String title) { - this.title = title; - } - - public String getTitle() { - return title; - } -} - public class Hangman { public static final HashMap RANDOM_WORD_APIS = new HashMap<>(Map.of( - "en", "https://random-word-api.vercel.app/api?words=1", - "de", "https://alex-riedel.de/randV2.php?anz=1" + "en", "https://capitalizemytitle.com/wp-content/tools/random-word/en/nouns.txt", + "de", "https://capitalizemytitle.com/wp-content/tools/random-word/de/nouns.txt" )); + private static final HashMap> RANDOM_WORD_LIST = new HashMap<>(); + + static { + for (Map.Entry entry : RANDOM_WORD_APIS.entrySet()) { + String response = APIRequest.getString(entry.getValue()); + RANDOM_WORD_LIST.put(entry.getKey(), response != null ? List.of(response.split(",")) : null); + } + } private static final String API_DICTIONARY = "https://www.dictionaryapi.com/api/v3/references/collegiate/json/%s?key=%s"; private static final int REGIONAL_INDICATOR_A_CP = 0x1F1E6; @@ -219,14 +211,9 @@ private void endGame(GameState state) { } private String generateWord() { - String lang = RANDOM_WORD_APIS.getOrDefault(language.toLowerCase(), RANDOM_WORD_APIS.get("en")); - String response = APIRequest.getString(lang); - if (response == null) { - return null; - } - - // Remove brackets and quotation marks - return response.substring(2, response.length() - 2); + List words = RANDOM_WORD_LIST.getOrDefault(language.toLowerCase(), RANDOM_WORD_LIST.get("en")); + int randomIdx = ThreadLocalRandom.current().nextInt(words.size()); + return words.get(randomIdx); } private String showWord() { @@ -275,9 +262,9 @@ private EmbedBuilder createGameEmbed(GameState state) { gameEmbed.addField( "How To Play", """ - React with emojis (e.g. \uD83C\uDDE6, \uD83C\uDDE7) to make a guess - React with the joker (🃏) to get a hint - React with the stop sign (🛑) to end the game + - React with emojis (e.g. \uD83C\uDDE6, \uD83C\uDDE7) to make a guess + - React with the joker (🃏) to get a hint + - React with the stop sign (🛑) to end the game """, false); } return gameEmbed; diff --git a/src/main/java/de/tosoxdev/tosoxjr/commands/scramble/GameState.java b/src/main/java/de/tosoxdev/tosoxjr/commands/scramble/GameState.java new file mode 100644 index 0000000..e3ed84d --- /dev/null +++ b/src/main/java/de/tosoxdev/tosoxjr/commands/scramble/GameState.java @@ -0,0 +1,18 @@ +package de.tosoxdev.tosoxjr.commands.scramble; + +enum GameState { + ONGOING("Scramble"), + WIN("You won!"), + DEFEAT("You lose!"), + TIMEOUT("Timeout"); + + private final String title; + + GameState(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } +} diff --git a/src/main/java/de/tosoxdev/tosoxjr/commands/scramble/Scramble.java b/src/main/java/de/tosoxdev/tosoxjr/commands/scramble/Scramble.java new file mode 100644 index 0000000..6ad06d7 --- /dev/null +++ b/src/main/java/de/tosoxdev/tosoxjr/commands/scramble/Scramble.java @@ -0,0 +1,170 @@ +package de.tosoxdev.tosoxjr.commands.scramble; + +import de.tosoxdev.tosoxjr.utils.APIRequest; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.entities.emoji.EmojiUnion; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; + +import java.awt.*; +import java.util.*; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public class Scramble { + public static final HashMap RANDOM_WORD_APIS = new HashMap<>(Map.of( + "en", "https://capitalizemytitle.com/wp-content/tools/random-word/en/nouns.txt", + "de", "https://capitalizemytitle.com/wp-content/tools/random-word/de/nouns.txt" + )); + private static final HashMap> RANDOM_WORD_LIST = new HashMap<>(); + + static { + for (Map.Entry entry : RANDOM_WORD_APIS.entrySet()) { + String response = APIRequest.getString(entry.getValue()); + RANDOM_WORD_LIST.put(entry.getKey(), response != null ? List.of(response.split(",")) : null); + } + } + + private static final int TIMEOUT_MS = 2 * 60 * 1000; + private static final int STOP_SIGN_CP = 0x1F6D1; + + private final MessageChannel channel; + private final String player; + private final boolean coop; + private final String language; + + private Timer tmTimeout = new Timer(); + private String embedMessageId; + private long timer; + private int attempts; + private String word; + private String scrambledWord; + + public Scramble(String player, MessageChannel channel, boolean coop, String language) { + this.channel = channel; + this.player = player; + this.coop = coop; + this.language = language; + } + + public boolean initialize() { + word = generateWord(); + if (word == null) { + channel.sendMessage("I'm unable to generate a random word").queue(); + return false; + } + + // Shuffle word + List chars = new ArrayList<>(word.toUpperCase().chars().mapToObj(c -> String.valueOf((char) c)).toList()); + Collections.shuffle(chars); + scrambledWord = String.join("", chars); + + // Start timers + tmTimeout.schedule(new TimerTask() { + @Override + public void run() { + endGame(GameState.TIMEOUT, null); + } + }, TIMEOUT_MS); + timer = System.currentTimeMillis(); + + channel.sendMessageEmbeds(createGameEmbed(GameState.ONGOING, null).build()).queue(m -> embedMessageId = m.getId()); + return true; + } + + public void handleEvent(Event event) { + if (event instanceof MessageReceivedEvent e) { + handleMessageReceivedEvent(e); + } else if (event instanceof MessageReactionAddEvent e) { + handleMessageReactionAddEvent(e); + } + } + + private void handleMessageReactionAddEvent(MessageReactionAddEvent event) { + EmojiUnion emoji = event.getEmoji(); + if (!event.getMessageId().equals(embedMessageId)) return; + if (emoji.getType() == Emoji.Type.CUSTOM) return; + + // Check sender + User sender = event.getUser(); + if (sender == null) return; + if (sender.isBot()) return; + if (!sender.getAsTag().equals(player)) return; + + // Get code point from emoji + int codePoint = emoji.getName().codePointAt(0); + if (codePoint != STOP_SIGN_CP) return; + + endGame(GameState.DEFEAT, null); + } + + private void handleMessageReceivedEvent(MessageReceivedEvent event) { + if (event.isWebhookMessage()) return; + if (!event.getChannel().getId().equals(channel.getId())) return; + + User sender = event.getAuthor(); + if (sender.isBot()) return; + if ((!coop) && (!sender.getAsTag().equals(player))) return; + + // Reset timeout timer + resetTimer(); + + attempts++; + + if (event.getMessage().getContentDisplay().equalsIgnoreCase(word)) { + endGame(GameState.WIN, sender.getName()); + } + } + + private void resetTimer() { + tmTimeout.cancel(); + tmTimeout = new Timer(); + tmTimeout.schedule(new TimerTask() { + @Override + public void run() { + endGame(GameState.TIMEOUT, null); + } + }, TIMEOUT_MS); + } + + private void endGame(GameState state, String sender) { + tmTimeout.cancel(); + channel.retrieveMessageById(embedMessageId).queue(m -> m.clearReactions().queue()); + channel.sendMessageEmbeds(createGameEmbed(state, sender).build()).queue(); + ScrambleCmd.getInstance().removePlayer(player); + } + + private String generateWord() { + List words = RANDOM_WORD_LIST.getOrDefault(language.toLowerCase(), RANDOM_WORD_LIST.get("en")); + int randomIdx = ThreadLocalRandom.current().nextInt(words.size()); + return words.get(randomIdx); + } + + private EmbedBuilder createGameEmbed(GameState state, String sender) { + EmbedBuilder gameEmbed = new EmbedBuilder(); + gameEmbed.setTitle(state.getTitle()); + gameEmbed.setColor(Color.CYAN); + gameEmbed.addField(state == GameState.ONGOING ? "Word" : "The word was", state == GameState.ONGOING ? scrambledWord : word, false); + if (state == GameState.WIN) { + double time = (double)(System.currentTimeMillis() - timer) / 1000; + String results = coop + ? String.format("%s guessed the word first after %.2fs", sender, time) + : String.format("You guessed the word after %.2fs and %d attempts", time, attempts); + gameEmbed.addField("Results", results, false); + } + if (state == GameState.ONGOING) { + gameEmbed.addField( + "How To Play", + """ + - Try to unscramble the word + - Write your guess in this channel + - React with the stop sign (🛑) to end the game + """, false); + } + return gameEmbed; + } +} diff --git a/src/main/java/de/tosoxdev/tosoxjr/commands/scramble/ScrambleCmd.java b/src/main/java/de/tosoxdev/tosoxjr/commands/scramble/ScrambleCmd.java new file mode 100644 index 0000000..8212acc --- /dev/null +++ b/src/main/java/de/tosoxdev/tosoxjr/commands/scramble/ScrambleCmd.java @@ -0,0 +1,75 @@ +package de.tosoxdev.tosoxjr.commands.scramble; + +import de.tosoxdev.tosoxjr.commands.GameBase; +import de.tosoxdev.tosoxjr.commands.hangman.Hangman; +import de.tosoxdev.tosoxjr.utils.ArgumentParser; +import net.dv8tion.jda.api.events.Event; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.HashMap; +import java.util.List; + +public class ScrambleCmd extends GameBase { + private static final int MAX_GAMES = 10; + private static ScrambleCmd instance; + private final HashMap games = new HashMap<>(); + private final String languages; + + public ScrambleCmd() { + super("scramble", "Play a game of Scramble", List.of( + new OptionData(OptionType.STRING, "lang", "Decide the langauge of the word. Use 'list' to list all available ones", false), + new OptionData(OptionType.BOOLEAN, "coop", "Play Scramble with all your friends on the server", false) + )); + instance = this; + + StringBuilder sb = new StringBuilder(); + Hangman.RANDOM_WORD_APIS.forEach((key, value) -> sb.append(String.format("- %s\n", key))); + languages = sb.toString(); + } + + @Override + public void handle(SlashCommandInteractionEvent event) { + String lang = ArgumentParser.getString(event.getOption("lang"), ""); + if (lang.equalsIgnoreCase("list")) { + String msg = String.format("Available languages\n%s", languages); + event.reply(msg).queue(); + return; + } + + String user = event.getUser().getAsTag(); + if (games.containsKey(user)) { + event.reply("You already started a game of Scramble").queue(); + return; + } + + if (games.size() > MAX_GAMES) { + event.reply("Sorry, there are to many games of Scramble already").queue(); + return; + } + + event.deferReply().queue(m -> m.deleteOriginal().queue()); + + boolean coop = ArgumentParser.getBoolean(event.getOption("coop"), false); + Scramble scramble = new Scramble(user, event.getChannel(), coop, lang); + if (scramble.initialize()) { + games.put(user, scramble); + } + } + + @Override + public void handleEvent(Event event) { + for (Scramble scramble : games.values()) { + new Thread(() -> scramble.handleEvent(event)).start(); + } + } + + public void removePlayer(String user) { + games.remove(user); + } + + public static ScrambleCmd getInstance() { + return instance; + } +} diff --git a/src/main/java/de/tosoxdev/tosoxjr/listener/UserInputListener.java b/src/main/java/de/tosoxdev/tosoxjr/listener/UserInputListener.java index 1619752..abba470 100644 --- a/src/main/java/de/tosoxdev/tosoxjr/listener/UserInputListener.java +++ b/src/main/java/de/tosoxdev/tosoxjr/listener/UserInputListener.java @@ -2,7 +2,9 @@ import de.tosoxdev.tosoxjr.Main; import de.tosoxdev.tosoxjr.commands.hangman.HangmanCmd; +import de.tosoxdev.tosoxjr.commands.scramble.ScrambleCmd; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.jetbrains.annotations.NotNull; @@ -16,5 +18,11 @@ public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent even @Override public void onMessageReactionAdd(@NotNull MessageReactionAddEvent event) { HangmanCmd.getInstance().handleEvent(event); + ScrambleCmd.getInstance().handleEvent(event); + } + + @Override + public void onMessageReceived(@NotNull MessageReceivedEvent event) { + ScrambleCmd.getInstance().handleEvent(event); } }