From 62b13449df3147024296988549837042c7c6c046 Mon Sep 17 00:00:00 2001 From: beanbeanjuice Date: Wed, 3 Jul 2024 05:05:28 -0400 Subject: [PATCH] Added Tic-Tac-Toe --- .../com/beanbeanjuice/cafebot/CafeBot.java | 6 +- .../commands/games/TicTacToeCommand.java | 87 ++++++++ .../sections/game/TicTacToeListener.java | 188 ++++++++++++++++++ 3 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/beanbeanjuice/cafebot/commands/games/TicTacToeCommand.java create mode 100644 src/main/java/com/beanbeanjuice/cafebot/utility/sections/game/TicTacToeListener.java diff --git a/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java b/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java index 7c6b410d..9e735818 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java +++ b/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java @@ -12,6 +12,7 @@ import com.beanbeanjuice.cafebot.commands.fun.rate.RateCommand; import com.beanbeanjuice.cafebot.commands.games.CoinFlipCommand; import com.beanbeanjuice.cafebot.commands.games.DiceRollCommand; +import com.beanbeanjuice.cafebot.commands.games.TicTacToeCommand; import com.beanbeanjuice.cafebot.commands.games.game.GameCommand; import com.beanbeanjuice.cafebot.commands.generic.PingCommand; import com.beanbeanjuice.cafebot.commands.generic.*; @@ -28,6 +29,7 @@ import com.beanbeanjuice.cafeapi.wrapper.CafeAPI; import com.beanbeanjuice.cafeapi.wrapper.requests.RequestLocation; import com.beanbeanjuice.cafebot.utility.sections.cafe.MenuHandler; +import com.beanbeanjuice.cafebot.utility.sections.game.TicTacToeListener; import com.beanbeanjuice.cafebot.utility.sections.generic.HelpHandler; import com.beanbeanjuice.cafebot.utility.sections.generic.HelpListener; import com.sun.management.OperatingSystemMXBean; @@ -188,6 +190,7 @@ private void setupCommands() { new CoinFlipCommand(this), new DiceRollCommand(this), new GameCommand(this), + new TicTacToeCommand(this), // Social new MemberCountCommand(this), @@ -245,7 +248,8 @@ private void setupListeners() { new BotAddListener(this), new BotRemoveListener(this), new CountingListener(this), - new HelpListener(commandHandler, helpHandler) + new HelpListener(commandHandler, helpHandler), + new TicTacToeListener(cafeAPI.getWinStreaksEndpoint()) ); } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/games/TicTacToeCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/games/TicTacToeCommand.java new file mode 100644 index 00000000..ac0a9c98 --- /dev/null +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/games/TicTacToeCommand.java @@ -0,0 +1,87 @@ +package com.beanbeanjuice.cafebot.commands.games; + +import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.ICommand; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.emoji.Emoji; +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 net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; + +public class TicTacToeCommand extends Command implements ICommand { + + public TicTacToeCommand(final CafeBot cafeBot) { + super(cafeBot); + } + + @Override + public void handle(SlashCommandInteractionEvent event) { + // cafeBot:tictactoe:index:user1:user2 + User opponent = event.getOption("opponent").getAsUser(); + + String playerID = event.getUser().getId(); + String opponentID = opponent.getId(); + + event.getHook() + .sendMessage(String.format("%s vs %s", event.getUser().getAsMention(), opponent.getEffectiveName())) + .addComponents( + ActionRow.of(getButton(0, playerID, opponentID), getButton(1, playerID, opponentID), getButton(2, playerID, opponentID)), + ActionRow.of(getButton(3, playerID, opponentID), getButton(4, playerID, opponentID), getButton(5, playerID, opponentID)), + ActionRow.of(getButton(6, playerID, opponentID), getButton(7, playerID, opponentID), getButton(8, playerID, opponentID)) + ) + .queue(); + } + + private Button getButton(final int index, final String user1ID, final String user2ID) { + return Button.secondary(String.format("cafeBot:tictactoe:%d:%s:%s", index, user1ID, user2ID), Emoji.fromFormatted("❓")); + } + + @Override + public String getName() { + return "tictactoe"; + } + + @Override + public String getDescription() { + return "Play tic-tac-toe with someone!"; + } + + @Override + public CommandCategory getCategory() { + return CommandCategory.GAME; + } + + @Override + public OptionData[] getOptions() { + return new OptionData[] { + new OptionData(OptionType.USER, "opponent", "The person you want to play against.", true) + }; + } + + @Override + public Permission[] getPermissions() { + return new Permission[0]; + } + + @Override + public boolean isEphemeral() { + return false; + } + + @Override + public boolean isNSFW() { + return false; + } + + @Override + public boolean allowDM() { + return false; + } + +} diff --git a/src/main/java/com/beanbeanjuice/cafebot/utility/sections/game/TicTacToeListener.java b/src/main/java/com/beanbeanjuice/cafebot/utility/sections/game/TicTacToeListener.java new file mode 100644 index 00000000..309f8e58 --- /dev/null +++ b/src/main/java/com/beanbeanjuice/cafebot/utility/sections/game/TicTacToeListener.java @@ -0,0 +1,188 @@ +package com.beanbeanjuice.cafebot.utility.sections.game; + +import com.beanbeanjuice.cafeapi.wrapper.endpoints.minigames.winstreaks.MinigameType; +import com.beanbeanjuice.cafeapi.wrapper.endpoints.minigames.winstreaks.WinStreak; +import com.beanbeanjuice.cafeapi.wrapper.endpoints.minigames.winstreaks.WinStreaksEndpoint; +import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.utility.helper.Helper; +import lombok.Getter; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.components.Component; +import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; +import net.dv8tion.jda.api.requests.restaction.MessageEditAction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class TicTacToeListener extends ListenerAdapter { + + private final WinStreaksEndpoint winStreaksEndpoint; + + public TicTacToeListener(final WinStreaksEndpoint winStreaksEndpoint) { + this.winStreaksEndpoint = winStreaksEndpoint; + } + + /* + This will be in the format cafeBot:tictactoe:index:user1:user2 + */ + @Override + public void onButtonInteraction(ButtonInteractionEvent event) { + if (!event.getComponentId().startsWith("cafeBot:tictactoe:")) return; + + User currentPerson = event.getMessage().getMentions().getUsers().getFirst(); + if (!event.getUser().getId().equals(currentPerson.getId())) return; + + int index = Integer.parseInt(event.getComponentId().split(":")[2]); + String player1ID = event.getComponentId().split(":")[3]; + String player2ID = event.getComponentId().split(":")[4]; + + Guild guild = event.getGuild(); + guild.retrieveMembersByIds(player1ID, player2ID).onSuccess((members) -> { + Member player1 = members.get(0); + Member player2 = members.get(1); + boolean isPlayer1 = currentPerson.getId().equals(player1ID); + + if (player1 == null || player2 == null) { + event.editMessage(String.format("**Game Cancelled**: Cannot get players %s and %s...", player1ID, player2ID)).setReplace(true).queue(); + return; + } + + Button button = event.getButton(); + ButtonStyle style = (isPlayer1) ? ButtonStyle.SUCCESS : ButtonStyle.DANGER; + Emoji emoji = (isPlayer1) ? Emoji.fromFormatted("U+1F1FD") : Emoji.fromFormatted("U+1F1F4"); + + Result result = checkGame(index, isPlayer1, event.getMessage().getComponents()); + event.editButton(button.withStyle(style).withEmoji(emoji).asDisabled()).queue((ignored) -> { + String formatString = String.format( + "%s (:regional_indicator_x:) vs %s (:regional_indicator_o:) : **%s**", + (isPlayer1) ? player1.getEffectiveName() : player1.getAsMention(), + (isPlayer1) ? player2.getAsMention() : player2.getEffectiveName(), + result.getMessage() + ); + + MessageEditAction action = event.getMessage().editMessage(formatString); + + if (result != Result.ONGOING) { + List components = new ArrayList<>(event.getMessage().getComponents()); + components.replaceAll(itemComponents -> itemComponents.withDisabled(true)); + action.setComponents(components); + + if (result != Result.TIED) { + updateWins(player1, player2, result, event.getMessage()); + } + } + + action.queue(); + }); + }); + + } + + private void updateWins(final Member player1, final Member player2, final Result result, final Message message) { + CompletableFuture player1WinStreakFuture = winStreaksEndpoint.getAndCreateUserWinStreak(player1.getId()); + CompletableFuture player2WinStreakFuture = winStreaksEndpoint.getAndCreateUserWinStreak(player2.getId()); + + player1WinStreakFuture.thenCombineAsync(player2WinStreakFuture, (player1WinStreak, player2WinStreak) -> { + int player1Wins = (result == Result.PLAYER1) ? player1WinStreak.getWins(MinigameType.TIC_TAC_TOE) + 1: 0; + int player2Wins = (result == Result.PLAYER2) ? player2WinStreak.getWins(MinigameType.TIC_TAC_TOE) + 1: 0; + + winStreaksEndpoint.updateUserWinStreak(player1.getId(), MinigameType.TIC_TAC_TOE, player1Wins); + winStreaksEndpoint.updateUserWinStreak(player2.getId(), MinigameType.TIC_TAC_TOE, player2Wins); + + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setColor(Helper.getRandomColor()); + embedBuilder.setDescription(String.format( + """ + # Tic-Tac-Toe Win Streaks + **%s** - *%d Wins* + **%s** - *%d Wins* + """, player1.getEffectiveName(), player1Wins, player2.getEffectiveName(), player2Wins)); + + message.replyEmbeds(embedBuilder.build()).queue(); + + return true; + }); + } + + private Result checkGame(final int index, final boolean isPlayer1, final List componentLayouts) { + List board = getBoard(index, isPlayer1, componentLayouts); + Result result = (isPlayer1) ? Result.PLAYER1 : Result.PLAYER2; + + // Check rows + for (int i = 0; i < 9; i += 3) { + if (board.get(i) != Square.NONE && board.get(i) == board.get(i + 1) && board.get(i) == board.get(i + 2)) { + return result; + } + } + + // Check columns + for (int i = 0; i < 3; i++) { + if (board.get(i) != Square.NONE && board.get(i) == board.get(i + 3) && board.get(i) == board.get(i + 6)) { + return result; + } + } + + // Check diagonals + if (board.get(0) != Square.NONE && board.get(0) == board.get(4) && board.get(0) == board.get(8)) { + return result; + } + + if (board.get(2) != Square.NONE && board.get(2) == board.get(4) && board.get(2) == board.get(6)) { + return result; + } + + if (!board.contains(Square.NONE)) return Result.TIED; + return Result.ONGOING; + } + + private List getBoard(final int index, final boolean isPlayer1, final List componentLayouts) { + Square[] game = new Square[9]; + + + componentLayouts.forEach((layout) -> { + layout.getButtons().forEach((button) -> { + int buttonIndex = Integer.parseInt(button.getId().split(":")[2]); + if (!button.isDisabled()) { + game[buttonIndex] = Square.NONE; + return; + } + + game[buttonIndex] = button.getStyle().equals(ButtonStyle.SUCCESS) ? Square.PLAYER1 : Square.PLAYER2; + }); + }); + game[index] = (isPlayer1) ? Square.PLAYER1 : Square.PLAYER2; + + return Arrays.stream(game).toList(); + } + + private enum Result { + PLAYER1 (":regional_indicator_x: WINS"), + PLAYER2 (":regional_indicator_o: WINS"), + TIED ("TIED"), + ONGOING ("ONGOING"); + + @Getter final String message; + + Result(final String message) { + this.message = message; + } + } + + private enum Square { + PLAYER1, + PLAYER2, + NONE + } + +}