diff --git a/src/main/java/Bot.java b/src/main/java/Bot.java deleted file mode 100644 index 574df11..0000000 --- a/src/main/java/Bot.java +++ /dev/null @@ -1,75 +0,0 @@ -import net.dv8tion.jda.api.OnlineStatus; -import net.dv8tion.jda.api.entities.Activity; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder; -import net.dv8tion.jda.api.sharding.ShardManager; - -import javax.security.auth.login.LoginException; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Scanner; - -public class Bot { - static HashMap prefixes = new HashMap<>(); - - private static ShardManager shardManager; - - public static void main(String[] args) throws LoginException { - String token = null; - try { - File tokenFile = Paths.get("token.txt").toFile(); - if (!tokenFile.exists()) { - System.out.println("[ERROR] Could not find token.txt file"); - System.out.println("[ERROR] Please create a file called \"token.txt\" in the same folder as the jar " + "file and paste in your bot token."); - return; - } - token = new String(Files.readAllBytes(tokenFile.toPath())); - } catch (Exception ex) { - ex.printStackTrace(); - } - if (token == null) return; - DefaultShardManagerBuilder builder = new DefaultShardManagerBuilder(token); - builder.setStatus(OnlineStatus.ONLINE); - builder.setActivity(Activity.playing("@Sokobot for info!")); - builder.addEventListeners(new Commands()); - shardManager = builder.build(); - Thread consoleThread = new Thread(() -> { - Scanner s = new Scanner(System.in); - while (s.hasNextLine()) { - processCommand(s.nextLine()); - } - }); - consoleThread.setDaemon(true); - consoleThread.setName("Console Thread"); - consoleThread.start(); - } - - private static void processCommand(String cmd) { - if (cmd.equalsIgnoreCase("help")) { - System.out.println("Commands:\nstop - Shuts down the bot and exits the program"); - return; - } - if (cmd.equalsIgnoreCase("stop")) { - System.out.println("Shutting down..."); - shardManager.shutdown(); - System.out.println("Bye!"); - System.exit(0); - return; - } - System.out.println("Unknown command. Please use \"help\" for a list of commands."); - } - - public static ShardManager getShardManager() { - return shardManager; - } - - static void setPrefix(Guild guild, String prefix) { - prefixes.put(guild.getIdLong(), prefix); - } - - static String getPrefix(Guild guild) { - return prefixes.getOrDefault(guild.getIdLong(), "!"); - } -} \ No newline at end of file diff --git a/src/main/java/Commands.java b/src/main/java/Commands.java deleted file mode 100644 index 69cc57c..0000000 --- a/src/main/java/Commands.java +++ /dev/null @@ -1,221 +0,0 @@ -import com.vdurmont.emoji.EmojiManager; -import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.entities.*; -import net.dv8tion.jda.api.events.guild.GuildLeaveEvent; -import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; -import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; - -import java.util.*; - -public class Commands extends ListenerAdapter { - HashMap games = new HashMap<>(); - ArrayList commandsPrefix = new ArrayList<>(Arrays.asList("play", "continue", "stop")); - ArrayList commandsNoPrefix = new ArrayList<>(Arrays.asList("w", "a", "s", "d", "up", "left", "down", - "right", "r")); - - @Override - public void onGuildLeave(GuildLeaveEvent event) { - Guild guild = event.getGuild(); - Bot.prefixes.remove(guild.getIdLong()); - } - - @Override - public void onGuildMessageReceived(GuildMessageReceivedEvent event) { - User user = event.getAuthor(); - Member member = event.getMember(); - Message message = event.getMessage(); - TextChannel channel = event.getChannel(); - Guild guild = event.getGuild(); - String prefix = Bot.getPrefix(guild); - if (user.getId().equals(event.getJDA().getSelfUser().getId())) { - List embeds = message.getEmbeds(); - if (embeds.size() > 0) { - MessageEmbed embed = embeds.get(0); - if (embed.getTitle() != null && embed.getTitle().length() > 0) { - if (embed.getTitle().startsWith("Sokobot | Level ")) { - message.addReaction("U+2B05").queue(); - message.addReaction("U+27A1").queue(); - message.addReaction("U+2B06").queue(); - message.addReaction("U+2B07").queue(); - message.addReaction("U+1F504").queue(); - MessageEmbed.Footer footerObject = embed.getFooter(); - if (footerObject != null) { - String footer = footerObject.getText(); - if (footer != null) { - long playerId = Long.parseLong(footer.substring(10, footer.length() - 1)); - if (games.containsKey(playerId)) { - Game game = games.get(playerId); - game.setGameMessage(message); - } - } - } - } - } - } - return; - } - String[] args = message.getContentRaw().split("\\s+"); - if (args.length > 0) { - String arg = args[0].toLowerCase(); - if (arg.equals(prefix + "prefix")) { - if (!hasPermissions(guild, channel)) { - sendInvalidPermissionsMessage(user, channel); - return; - } - if (member.hasPermission(Permission.ADMINISTRATOR)) { - if (args.length == 2 && args[1].length() == 1) { - String newPrefix = args[1].toLowerCase(); - Bot.setPrefix(event.getGuild(), newPrefix); - channel.sendMessage("Prefix successfully changed to ``" + newPrefix + "``.").queue(); - } else channel.sendMessage("The prefix must be one character long!").queue(); - } else - channel.sendMessage(user.getAsMention() + ", you do not have permission to use this " + "command" + ".").queue(); - // No need to delete prefix-set command - // message.delete().queue(); - } else if (((commandsNoPrefix.contains(arg)) || (arg.length() > 0 && Character.toString(arg.charAt(0)).equals(prefix) && commandsPrefix.contains(arg.substring(1))))) { - if (!hasPermissions(guild, channel)) { - sendInvalidPermissionsMessage(user, channel); - return; - } - Game game; - if (!games.containsKey(user.getIdLong())) { - game = new Game(user); - games.put(user.getIdLong(), game); - } else game = games.get(user.getIdLong()); - String userInput = arg; - if (userInput.substring(0, 1).equals(prefix)) userInput = userInput.substring(1); - if (!game.gameActive && userInput.equals("play") && args.length == 2 && EmojiManager.isEmoji(args[1])) { - game.setPlayerEmote(args[1]); - } - game.run(event.getGuild(), channel, userInput); - if (userInput.equals("stop")) games.remove(user.getIdLong()); - if (guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_MANAGE)) message.delete().queue(); - } else if ((arg.equals(prefix + "info")) || (message.getMentionedUsers().size() > 0 && message.getMentionedUsers().get(0).equals(event.getJDA().getSelfUser()))) { - if (!hasPermissions(guild, channel)) { - sendInvalidPermissionsMessage(user, channel); - return; - } - channel.sendMessage(info(event.getGuild()).build()).queue(); - if (guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_MANAGE)) message.delete().queue(); - } - } - } - - private static final Collection requiredPermissions = Arrays.asList(Permission.MESSAGE_ADD_REACTION, - Permission.MESSAGE_EMBED_LINKS, Permission.MESSAGE_MANAGE, Permission.MESSAGE_WRITE); - - private boolean hasPermissions(Guild guild, TextChannel channel) { - Member self = guild.getSelfMember(); - if (self.hasPermission(Permission.ADMINISTRATOR)) return true; - return self.hasPermission(channel, requiredPermissions); - } - - private void sendInvalidPermissionsMessage(User user, TextChannel channel) { - if (channel.canTalk()) { - StringBuilder requiredPermissionsDisplay = new StringBuilder(); - for (Permission requiredPermission : requiredPermissions) { - requiredPermissionsDisplay.append("`").append(requiredPermission.getName()).append("`, "); - } - if (requiredPermissionsDisplay.toString().endsWith(", ")) - requiredPermissionsDisplay = new StringBuilder(requiredPermissionsDisplay.substring(0, - requiredPermissionsDisplay.length() - 2)); - channel.sendMessage(user.getAsMention() + ", I don't have enough permissions to work properly.\nMake " + - "sure I have the following permissions: " + requiredPermissionsDisplay + "\nIf you think this is " - + "an error, please contact a server administrator.").queue(); - } - } - - @Override - public void onGuildMessageReactionAdd(GuildMessageReactionAddEvent event) { - User user = event.getUser(); - if (user.isBot()) { - return; - } - Guild guild = event.getGuild(); - MessageReaction reaction = event.getReaction(); - TextChannel channel = event.getChannel(); - channel.retrieveMessageById(event.getMessageId()).queue(message -> { - if (message.getAuthor().getId().equals(event.getJDA().getSelfUser().getId())) { - Game game; - if (!games.containsKey(user.getIdLong())) { - game = new Game(user); - games.put(user.getIdLong(), game); - } else game = games.get(user.getIdLong()); - boolean reactionCommand = true; - String userInput = ""; - switch (event.getReactionEmote().toString()) { - case "RE:U+2b05": - userInput = "left"; - break; - case "RE:U+27a1": - userInput = "right"; - break; - case "RE:U+2b06": - userInput = "up"; - break; - case "RE:U+2b07": - userInput = "down"; - break; - case "RE:U+1f504": - userInput = "r"; - break; - default: - reactionCommand = false; - break; - } - if (reactionCommand) game.run(guild, channel, userInput); - reaction.removeReaction(user).queue(); - } - }); - } - - EmbedBuilder info(Guild guild) { - EmbedBuilder info = new EmbedBuilder(); - info.setTitle("Sokobot"); - info.setThumbnail(guild.getSelfMember().getUser().getAvatarUrl()); - info.setDescription("Sokobot is a bot that lets you play Sokoban, the classic box-pushing puzzle game."); - info.setColor(0xdd2e53); - info.addField("How to Play", "You are a **Sokoban** :flushed:.\nYour job is to push **boxes** :brown_square: " - + "on top of their **destinations** :negative_squared_cross_mark:.", false); - info.addField("Features", ":white_small_square:**Infinite levels**\nThe maps in Sokobot are randomly " + - "generated, increasing in difficulty as you progress.\n:white_small_square:**Varied " + "controls" + - "**\nSokobot has multiple control options to improve the player's experience, including " + - "reactions and wasd commands!\n:white_small_square:**Simultaneous games**\nThanks to the power of " + "Java HashMaps:tm:, multiple users can use the bot at the same time without interfering with one " + "another.\n:white_small_square:**Custom prefixes**\nTo prevent Sokobot from conflicting with other " + "bots, admins can choose any single-character prefix to preface Sokobot's commands.", false); - info.addField("Commands", - ("``" + Bot.getPrefix(guild) + "play`` can be used to start a game if you are not " + "currently in " + "one.\n``" + Bot.getPrefix(guild) + "stop`` can be used to stop your active game at any " + "time.\n``" + Bot.getPrefix(guild) + "info`` provides some useful details about the bot and " + "rules of " + "the game.\n``" + Bot.getPrefix(guild) + "prefix [character]`` can be used to " + "change the prefix the " + "bot responds to."), false); - info.addField("Add to your server", - "https://top.gg/bot/713635251703906336\nSokobot is currently in " + Bot.getShardManager().getGuilds().size() + " servers.", false); - info.addField("Source code", "https://github.com/PolyMarsDev/Sokobot", false); - info.setFooter("created by PolyMars", "https://avatars0.githubusercontent" + ".com/u/51007356?s=460&u" + - "=4eb8fd498421a2eee9781edfbadf654386cf06c7&v=4"); - return info; - } - - public static void sendGameEmbed(MessageChannel channel, String level, String game, User user) { - EmbedBuilder embed = new EmbedBuilder(); - embed.setTitle("Sokobot | Level " + level); - embed.setDescription(game); - embed.addField("Enter direction (``up``, ``down``, ``left``, ``right``/``wasd``) or ``r`` to reset", "", false); - embed.setFooter("Game of " + user.getAsMention(), user.getAvatarUrl()); - channel.sendMessage(embed.build()).queue(); - } - - public static void updateGameEmbed(Message message, String level, String game, User user) { - EmbedBuilder embed = new EmbedBuilder(); - embed.setTitle("Sokobot | Level " + level); - embed.setDescription(game); - embed.addField("Enter direction (``up``, ``down``, ``left``, ``right``/``wasd``) or ``r`` to reset", "", false); - embed.setFooter("Game of " + user.getAsMention(), user.getAvatarUrl()); - message.editMessage(embed.build()).queue(); - } - - public static void sendWinEmbed(Guild guild, Message message, String level) { - EmbedBuilder embed = new EmbedBuilder(); - embed.setTitle("Sokobot | You win!"); - embed.setDescription("Type ``" + Bot.getPrefix(guild) + "continue`` to continue to Level " + level + " or ``" + Bot.getPrefix(guild) + "stop`` to quit "); - embed.setFooter("You can also press any reaction to continue."); - message.editMessage(embed.build()).queue(); - } -} diff --git a/src/main/java/Grid.java b/src/main/java/Grid.java deleted file mode 100644 index d352b12..0000000 --- a/src/main/java/Grid.java +++ /dev/null @@ -1,216 +0,0 @@ -public class Grid -{ - final int GROUND = 0; - final int WALL = 1; - final int BOX = 2; - final int DESTINATION = 3; - final int PLAYER = 4; - final int MAX_BOXES = 8; - Tile[][] grid; - Box[] boxes; - Destination[] destinations; - int boxCount; - int height = 0; - int width = 0; - int color = 0; - Player player; - String playerEmote; - Randomizer rand = new Randomizer(); - public Grid(int width, int height, int boxCount, String playerEmote) //create a random grid with specific width, height, and number of boxes - { - this.playerEmote = playerEmote; - player = new Player(2, 2, this); - if (boxCount > MAX_BOXES) - { - boxCount = MAX_BOXES; - } - this.boxCount = boxCount; - boxes = new Box[boxCount]; - destinations = new Destination[boxCount]; - this.height = height; - this.width = width; - grid = new Tile[width][height]; - createBoxes(); - createDestinations(); - updateGrid(); - } - public Player getPlayer() - { - return player; - } - public void reset() - { - player.setPosition(2, 2); - for (int i = 0; i < boxCount; i++) - { - boxes[i].reset(); - } - updateGrid(); - } - public void setStatus(int x, int y, int status) - { - grid[x][y].setStatus(status); - } - public int getStatus(int x, int y) - { - return grid[x][y].getStatus(); - } - public boolean isWall(int x, int y) - { - return grid[x][y].getStatus() == WALL; - } - public boolean isBox(int x, int y) - { - return grid[x][y].getStatus() == BOX; - } - public boolean isBoxRaw(int x, int y) //allows you to check if a box is at a position before grid is set up - { - for (int i = 0; i < boxCount; i++) - { - if (x == boxes[i].getX() && y == boxes[i].getY()) - { - return true; - } - } - return false; - } - public boolean isDestination(int x, int y) - { - return grid[x][y].getStatus() == DESTINATION; - - } - public Box getBox(int x, int y) - { - for (int i = 0; i < boxCount; i++) - { - if (x == boxes[i].getX() && y == boxes[i].getY()) - { - return boxes[i]; - } - } - return null; - } - public void createBoxes() - { - color = rand.nextInt(6); //runs after each level - for (int i = 0; i < boxCount; i++) - { - int x = rand.nextInt(width - 4) + 2; - int y = rand.nextInt(height - 4) + 2; - for (int j = 0; j < i; j++) - { - while ((x == boxes[j].getX() && y == boxes[j].getY()) || (x == 2 && y == 2) || (x - 1 == boxes[j].getX() && y == boxes[j].getY()) || (x + 1 == boxes[j].getX() && y == boxes[j].getY())) - { - x = rand.nextInt(width - 4) + 2; - y = rand.nextInt(height - 4) + 2; - } - } - boxes[i] = new Box(x, y, this); - } - } - public void createDestinations() - { - for (int i = 0; i < boxCount; i++) - { - int x = rand.nextInt(width - 2) + 1; - int y = rand.nextInt(height - 2) + 1; - for (int j = 0; j < i; j++) - { - while (((x == destinations[j].getX() && y == destinations[j].getY())) || isBoxRaw(x, y)) - { - x = rand.nextInt(width - 2) + 1; - y = rand.nextInt(height - 2) + 1; - } - } - destinations[i] = new Destination(x, y, this); - } - } - public void updateGrid() - { - for (int i = 0; i < height; i++) - { - for (int j = 0; j < width; j++) - { - grid[j][i] = new Tile(GROUND, playerEmote); - if (j == 0 || j == width - 1 || i == 0 || i == height - 1) - { - grid[j][i] = new Tile(WALL, color, playerEmote); - } - for (int k = 0; k < boxCount; k++) - { - if (destinations[k].getX() == j && destinations[k].getY() == i) - { - grid[j][i] = new Tile(DESTINATION, playerEmote); - } - } - if (player.getX() == j && player.getY() == i) - { - grid[j][i] = new Tile(PLAYER, playerEmote); - } - for (int k = 0; k < boxCount; k++) - { - if (boxes[k].getX() == j && boxes[k].getY() == i) - { - if (boxes[k].onDestination()) - { - grid[j][i] = new Tile(WALL, color, playerEmote); - } else { - grid[j][i] = new Tile(BOX, playerEmote); - } - - } - } - } - } - } - public boolean hasWon() - { - for (int i = 0; i < boxCount; i++) - { - if (!destinations[i].hasBox(this)) - { - return false; - } - } - return true; - } - public Tile[][] getGrid() - { - return grid; - } - public Box[] getBoxes() - { - return boxes; - } - public Destination[] getDestinations() - { - return destinations; - } - public int getBoxCount() - { - return boxCount; - } - public int getHeight() - { - return height; - } - public int getWidth() - { - return width; - } - public String toString() - { - updateGrid(); - String result = ""; - for (int i = 0; i < height; i++) - { - for (int j = 0; j < width; j++) - { - result += grid[j][i]; - } - result += "\n"; - } - return result; - } - -} diff --git a/src/main/java/Player.java b/src/main/java/Player.java deleted file mode 100644 index aea126d..0000000 --- a/src/main/java/Player.java +++ /dev/null @@ -1,98 +0,0 @@ -public class Player -{ - int x = 0; - int y = 0; - Grid currentGrid; - public Player(int x, int y, Grid currentGrid) - { - this.x = x; - this.y = y; - this.currentGrid = currentGrid; - } - public int getX() - { - return x; - } - public int getY() - { - return y; - } - public void setPosition(int x, int y) - { - this.x = x; - this.y = y; - } - public boolean moveUp() - { - if (!currentGrid.isWall(x, y - 1)) - { - if (currentGrid.isBox(x, y - 1)) - { - if (currentGrid.getBox(x, y - 1).moveUp()) - { - y -= 1; - return true; - } - return false; - } - y -= 1; - return true; - } - return false; - } - public boolean moveDown() - { - if (!currentGrid.isWall(x, y + 1)) - { - if (currentGrid.isBox(x, y + 1)) - { - if (currentGrid.getBox(x, y + 1).moveDown()) - { - y += 1; - return true; - } - return false; - } - y += 1; - return true; - } - return false; - } - public boolean moveLeft() - { - if (!currentGrid.isWall(x - 1, y)) - { - if (currentGrid.isBox(x - 1, y)) - { - if (currentGrid.getBox(x - 1, y).moveLeft()) - { - x -= 1; - return true; - } - return false; - } - x -= 1; - return true; - } - return false; - } - public boolean moveRight() - { - if (!currentGrid.isWall(x + 1, y)) - { - if (currentGrid.isBox(x + 1, y)) - { - if (currentGrid.getBox(x + 1, y).moveRight()) - { - x += 1; - return true; - } - return false; - } - x += 1; - return true; - } - return false; - } - -} diff --git a/src/main/java/me/polymarsdev/sokobot/Bot.java b/src/main/java/me/polymarsdev/sokobot/Bot.java new file mode 100644 index 0000000..e36f658 --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/Bot.java @@ -0,0 +1,147 @@ +package me.polymarsdev.sokobot; + +import me.polymarsdev.sokobot.database.Database; +import me.polymarsdev.sokobot.listener.CommandListener; +import me.polymarsdev.sokobot.listener.GameListener; +import me.polymarsdev.sokobot.util.GameUtil; +import net.dv8tion.jda.api.OnlineStatus; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder; +import net.dv8tion.jda.api.sharding.ShardManager; + +import javax.security.auth.login.LoginException; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Scanner; + +public class Bot { + static HashMap prefixes = new HashMap<>(); + + /** + * You can enable the database here. + * Set the DB Type to MySQL or SQLite, which you want to use. + * - + * You can configure login data in the Database class. + */ + private static final boolean enableDatabase = false; + private static final Database.DBType dbType = Database.DBType.SQLite; + + private static ShardManager shardManager; + private static Database database = null; + + public static void main(String[] args) throws LoginException { + String token = null; + try { + File tokenFile = Paths.get("token.txt").toFile(); + if (!tokenFile.exists()) { + System.out.println("[ERROR] Could not find token.txt file"); + System.out.print("Please paste in your bot token: "); + Scanner s = new Scanner(System.in); + token = s.nextLine(); + System.out.println(); + System.out.println("[INFO] Creating token.txt - please wait"); + if (!tokenFile.createNewFile()) { + System.out.println( + "[ERROR] Could not create token.txt - please create this file and paste in your token" + + "."); + s.close(); + return; + } + Files.write(tokenFile.toPath(), token.getBytes()); + s.close(); + } + token = new String(Files.readAllBytes(tokenFile.toPath())); + } catch (Exception ex) { + ex.printStackTrace(); + } + if (token == null) return; + if (enableDatabase) database = new Database(dbType); + if (database != null) { + if (!database.isConnected()) { + database = null; + System.out.println("[ERROR] Database connection failed. Continuing without database."); + } else { + database.update( + "CREATE TABLE IF NOT EXISTS guildprefix (guildId VARCHAR(18) NOT NULL, prefix VARCHAR(8) NOT " + + "NULL);"); + } + } + DefaultShardManagerBuilder builder = new DefaultShardManagerBuilder(token); + builder.setStatus(OnlineStatus.ONLINE); + builder.setActivity(Activity.playing("@Sokobot for info!")); + builder.addEventListeners(new GameListener(), new CommandListener()); + shardManager = builder.build(); + GameUtil.runGameTimer(); + Thread consoleThread = new Thread(() -> { + Scanner s = new Scanner(System.in); + while (s.hasNextLine()) { + processCommand(s.nextLine()); + } + }); + consoleThread.setDaemon(true); + consoleThread.setName("Console Thread"); + consoleThread.start(); + } + + private static void processCommand(String cmd) { + if (cmd.equalsIgnoreCase("help")) { + System.out.println("Commands:\nstop - Shuts down the bot and exits the program"); + return; + } + if (cmd.equalsIgnoreCase("stop")) { + System.out.println("Shutting down..."); + shardManager.shutdown(); + if (database != null) { + System.out.println("Disconnecting database..."); + database.disconnect(); + } + System.out.println("Bye!"); + System.exit(0); + return; + } + System.out.println("Unknown command. Please use \"help\" for a list of commands."); + } + + public static ShardManager getShardManager() { + return shardManager; + } + + public static void removePrefix(long guildId) { + prefixes.remove(guildId); + if (database != null) { + database.update("DELETE FROM guildprefix WHERE guildId=?;", String.valueOf(guildId)); + } + } + + public static void setPrefix(Guild guild, String prefix) { + prefixes.put(guild.getIdLong(), prefix); + if (database != null) { + database.update("DELETE FROM guildprefix WHERE guildId=?;", guild.getId()); + database.update("INSERT INTO guildprefix VALUES (?, ?);", guild.getId(), prefix); + } + } + + public static String getPrefix(Guild guild) { + if (prefixes.containsKey(guild.getIdLong())) return prefixes.get(guild.getIdLong()); + if (database != null) { + try (ResultSet rs = database.query("SELECT prefix FROM guildprefix WHERE guildId=?;", guild.getId())) { + if (rs.next()) { + String prefix = rs.getString("prefix"); + prefixes.put(guild.getIdLong(), prefix); + return prefix; + } + prefixes.put(guild.getIdLong(), "!"); + return "!"; + } catch (SQLException ex) { + System.out.println("[ERROR] Error at retrieving guild prefix of guild id " + guild.getId() + ": " + ex + .getMessage()); + } + } + return "!"; + } +} \ No newline at end of file diff --git a/src/main/java/Game.java b/src/main/java/me/polymarsdev/sokobot/Game.java similarity index 55% rename from src/main/java/Game.java rename to src/main/java/me/polymarsdev/sokobot/Game.java index c2d30f2..e1c4a23 100644 --- a/src/main/java/Game.java +++ b/src/main/java/me/polymarsdev/sokobot/Game.java @@ -1,5 +1,11 @@ +package me.polymarsdev.sokobot; + +import me.polymarsdev.sokobot.objects.Grid; +import me.polymarsdev.sokobot.util.GameUtil; import net.dv8tion.jda.api.entities.*; +import java.util.concurrent.TimeUnit; + public class Game { long gameMessageID; long channelID; @@ -9,6 +15,7 @@ public class Game { public int level = 1; int width = 9; int height = 6; + public long lastAction; Grid grid = new Grid(width, height, level, playerEmote); public Game(User user) { @@ -32,36 +39,60 @@ public class Game { height = 6; grid = new Grid(width, height, level, playerEmote); gameActive = true; - Commands.sendGameEmbed(channel, String.valueOf(level), grid.toString(), user); + lastAction = System.currentTimeMillis(); + GameUtil.sendGameEmbed(channel, String.valueOf(level), grid.toString(), user); + } + } + + public void stop() { + gameActive = false; + TextChannel textChannel = Bot.getShardManager().getTextChannelById(channelID); + if (textChannel != null) { + textChannel.retrieveMessageById(gameMessageID).queue(gameMessage -> gameMessage.delete().queue()); } } public void run(Guild guild, TextChannel channel, String userInput) { + lastAction = System.currentTimeMillis(); if (userInput.equals("stop") && gameActive) { - channel.sendMessage("Thanks for playing, " + user.getAsMention() + "!").queue(); - gameActive = false; + stop(); + channel.sendMessage("Thanks for playing, " + user.getAsMention() + "!") + .queue(msg -> msg.delete().queueAfter(10, TimeUnit.SECONDS)); } - if (userInput.equals("play") && !gameActive) { newGame(channel); } else if (gameActive) { if (!grid.hasWon()) { String direction = userInput; - if (direction.equals("up") || direction.equals("w")) { - grid.getPlayer().moveUp(); - } else if (direction.equals("down") || direction.equals("s")) { - grid.getPlayer().moveDown(); - } else if (direction.equals("left") || direction.equals("a")) { - grid.getPlayer().moveLeft(); - } else if (direction.equals("right") || direction.equals("d")) { - grid.getPlayer().moveRight(); - } else if (direction.equals("r")) { - grid.reset(); + switch (direction) { + case "up": + case "w": + grid.getPlayer().moveUp(); + break; + case "down": + case "s": + grid.getPlayer().moveDown(); + break; + case "left": + case "a": + grid.getPlayer().moveLeft(); + break; + case "right": + case "d": + grid.getPlayer().moveRight(); + break; + case "mr": + grid.resetMap(); + break; + case "r": + grid.reset(); + break; } if (!grid.hasWon()) { TextChannel textChannel = Bot.getShardManager().getTextChannelById(channelID); if (textChannel != null) { - textChannel.retrieveMessageById(gameMessageID).queue(gameMessage -> Commands.updateGameEmbed(gameMessage, String.valueOf(level), grid.toString(), user)); + textChannel.retrieveMessageById(gameMessageID).queue(gameMessage -> GameUtil + .updateGameEmbed(gameMessage, String.valueOf(level), grid.toString(), user)); } } } @@ -75,8 +106,8 @@ public class Game { } TextChannel textChannel = Bot.getShardManager().getTextChannelById(channelID); if (textChannel != null) { - textChannel.retrieveMessageById(gameMessageID).queue(gameMessage -> Commands.sendWinEmbed(guild, - gameMessage, String.valueOf(level))); + textChannel.retrieveMessageById(gameMessageID) + .queue(gameMessage -> GameUtil.sendWinEmbed(guild, gameMessage, String.valueOf(level))); } grid = new Grid(width, height, level, playerEmote); } diff --git a/src/main/java/me/polymarsdev/sokobot/commands/GameInputCommand.java b/src/main/java/me/polymarsdev/sokobot/commands/GameInputCommand.java new file mode 100644 index 0000000..aab59a6 --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/commands/GameInputCommand.java @@ -0,0 +1,47 @@ +package me.polymarsdev.sokobot.commands; + +import com.vdurmont.emoji.EmojiManager; +import me.polymarsdev.sokobot.Bot; +import me.polymarsdev.sokobot.Game; +import me.polymarsdev.sokobot.entity.Command; +import me.polymarsdev.sokobot.event.CommandEvent; +import me.polymarsdev.sokobot.util.GameUtil; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.User; + +public class GameInputCommand extends Command { + + public GameInputCommand(String name) { + super(name); + } + + @Override + public void execute(CommandEvent event) { + User user = event.getAuthor(); + String[] args = event.getArgs(); + String prefix = Bot.getPrefix(event.getGuild()); + Game game; + if (!GameUtil.hasGame(user.getIdLong())) { + game = new Game(user); + GameUtil.setGame(user.getIdLong(), game); + } else game = GameUtil.getGame(user.getIdLong()); + // + String userInput = this.getName().toLowerCase(); + if (userInput.equals("play")) { + if (!game.gameActive) { + if (args.length > 0 && EmojiManager.isEmoji(args[0])) game.setPlayerEmote(args[0]); + } else { + event.reply(user.getAsMention() + ", you already have an active game.\nUse `" + prefix + + "stop` to stop your current game first."); + } + } + Guild guild = event.getGuild(); + TextChannel channel = event.getTextChannel(); + game.run(event.getGuild(), channel, userInput); + if (userInput.equals("stop")) GameUtil.removeGame(user.getIdLong()); + if (game.gameActive && guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_MANAGE)) + event.getMessage().delete().queue(); + } +} diff --git a/src/main/java/me/polymarsdev/sokobot/commands/InfoCommand.java b/src/main/java/me/polymarsdev/sokobot/commands/InfoCommand.java new file mode 100644 index 0000000..df47eb2 --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/commands/InfoCommand.java @@ -0,0 +1,53 @@ +package me.polymarsdev.sokobot.commands; + +import me.polymarsdev.sokobot.Bot; +import me.polymarsdev.sokobot.entity.Command; +import me.polymarsdev.sokobot.event.CommandEvent; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; + +public class InfoCommand extends Command { + + public InfoCommand() { + super("info"); + } + + @Override + public void execute(CommandEvent event) { + Guild guild = event.getGuild(); + EmbedBuilder info = new EmbedBuilder(); + final String prefix = Bot.getPrefix(guild); + info.setTitle("Sokobot"); + info.setThumbnail(guild.getSelfMember().getUser().getAvatarUrl()); + info.setDescription("Sokobot is a bot that lets you play Sokoban, the classic box-pushing puzzle game."); + info.setColor(0xdd2e53); + info.addField("How to Play", "You are a **Sokoban** :flushed:.\nYour job is to push **boxes** :brown_square: " + + "on top of their **destinations** :negative_squared_cross_mark:.", false); + info.addField("Features", ":white_small_square:**Infinite levels**\nThe maps in Sokobot are randomly " + + "generated, increasing in difficulty as you progress.\n:white_small_square:**Varied " + "controls" + + "**\nSokobot has multiple control options to improve the player's experience, including " + + "reactions and wasd commands!\n:white_small_square:**Simultaneous games**\nThanks to the power of " + + "Java HashMaps:tm:, multiple users can use the bot at the same time without interfering with one " + + "another.\n:white_small_square:**Custom prefixes**\nTo prevent Sokobot from conflicting with other " + + "bots, admins can choose any single-character prefix to preface Sokobot's commands.", false); + info.addField("Commands", + ("``" + prefix + "play`` can be used to start a game if you are not " + "currently in " + + "one.\n``" + prefix + "stop`` can be used to stop your active game at any " + + "time.\n``" + prefix + "info`` provides some useful details about the bot and " + + "rules of " + "the game.\n``" + Bot.getPrefix(guild) + + "prefix [character]`` can be used to " + "change the prefix the " + "bot responds to."), + false); + info.addField("Add to your server", + "https://top.gg/bot/713635251703906336\nSokobot is currently in " + Bot.getShardManager() + .getGuilds().size() + " servers.", false); + /* + // Official Support Server + info.addField("Support / Feedback", + "Official Support Server: https://invite.affluentproductions.org/apserver", false); + */ + info.addField("Source code", "https://github.com/PolyMarsDev/Sokobot", false); + info.setFooter("created by PolyMars", "https://avatars0.githubusercontent" + ".com/u/51007356?s=460&u" + + "=4eb8fd498421a2eee9781edfbadf654386cf06c7&v=4"); + event.reply(info.build()); + } +} diff --git a/src/main/java/me/polymarsdev/sokobot/commands/PrefixCommand.java b/src/main/java/me/polymarsdev/sokobot/commands/PrefixCommand.java new file mode 100644 index 0000000..7224d40 --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/commands/PrefixCommand.java @@ -0,0 +1,37 @@ +package me.polymarsdev.sokobot.commands; + +import me.polymarsdev.sokobot.Bot; +import me.polymarsdev.sokobot.entity.Command; +import me.polymarsdev.sokobot.event.CommandEvent; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; + +public class PrefixCommand extends Command { + + public PrefixCommand() { + super("prefix"); + } + + @Override + public void execute(CommandEvent event) { + User user = event.getAuthor(); + Member member = event.getMember(); + String[] args = event.getArgs(); + if (args.length > 0) { + if (!member.hasPermission(Permission.ADMINISTRATOR)) { + event.reply(user.getAsMention() + ", you do not have permission to use this command."); + return; + } + String newPrefix = args[0].toLowerCase(); + if (newPrefix.length() > 1) { + event.reply(user.getAsMention() + ", the prefix must be one character long!"); + return; + } + Bot.setPrefix(event.getGuild(), newPrefix); + event.reply("Prefix successfully changed to ``" + newPrefix + "``."); + } + } +} diff --git a/src/main/java/me/polymarsdev/sokobot/database/Database.java b/src/main/java/me/polymarsdev/sokobot/database/Database.java new file mode 100644 index 0000000..2ce40aa --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/database/Database.java @@ -0,0 +1,118 @@ +package me.polymarsdev.sokobot.database; + +import java.io.File; +import java.sql.*; + +public class Database { + + public enum DBType {MySQL, SQLite} + + /** + * SQLite Data + * Set this data if you use DBType#SQLite + * + * field filePath - This can either be a relative or absolute path. + * ex: sokobot.db + * or: C:/sqlite/db/sokobot.db + */ + private final String filePath = "sokobot.db"; + + /** + * MySQL Data + * Set this data if you use DBType#MySQL + */ + private final String mysql_hostname = "127.0.0.1"; + private final int mysql_port = 3306; + private final String mysql_database = "sokobot"; + private final String mysql_username = "sokobot"; + private final String mysql_password = "$€cUR€_P4sSw0R!)"; + + private Connection con = null; + + public Database(DBType dbType) { + try { + if (dbType == DBType.MySQL) { + con = DriverManager.getConnection( + "jdbc:mysql://" + mysql_hostname + ":" + mysql_port + "/" + mysql_database + + "?autoReconnect=true", mysql_username, mysql_password); + System.out.println("[INFO] Successfully initialized database connection."); + } else if (dbType == DBType.SQLite) { + File sqliteFile = new File(filePath); + if (!sqliteFile.exists()) { + System.out.println("[INFO] SQLite file \"" + filePath + "\" not found, creating file..."); + boolean create = sqliteFile.createNewFile(); + if (!create) System.out.println("[ERROR] Could not create SQLite file at " + filePath); + } + con = DriverManager.getConnection("jdbc:sqlite:" + filePath); + System.out.println("[INFO] Successfully initialized database connection."); + } + } catch (Exception ex) { + System.out.println("[ERROR] Error at creating database connection: " + ex.getMessage()); + } + } + + public void disconnect() { + try { + con.clearWarnings(); + con.close(); + con = null; + } catch (SQLException ex) { + ex.printStackTrace(); + } + } + + public Connection getCon() { + return con; + } + + public ResultSet query(String sql, Object... preparedParameters) { + try { + PreparedStatement ps = con.prepareStatement(sql); + int id = 1; + for (Object preparedParameter : preparedParameters) { + ps.setObject(id, preparedParameter); + id++; + } + return ps.executeQuery(); + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + public ResultSet query(String sql) { + try { + ResultSet rs = con.prepareStatement(sql).executeQuery(); + return rs; + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + + public void update(String sql, Object... preparedParameters) { + try (PreparedStatement ps = con.prepareStatement(sql)) { + int id = 1; + for (Object preparedParameter : preparedParameters) { + ps.setObject(id, preparedParameter); + id++; + } + ps.executeUpdate(); + } catch (SQLException ex) { + ex.printStackTrace(); + } + } + + public void update(String sql) { + try (PreparedStatement ps = con.prepareStatement(sql)) { + ps.executeUpdate(); + } catch (SQLException ex) { + ex.printStackTrace(); + } + } + + public boolean isConnected() { + return con != null; + } +} \ No newline at end of file diff --git a/src/main/java/me/polymarsdev/sokobot/entity/Command.java b/src/main/java/me/polymarsdev/sokobot/entity/Command.java new file mode 100644 index 0000000..8d51a6d --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/entity/Command.java @@ -0,0 +1,18 @@ +package me.polymarsdev.sokobot.entity; + +import me.polymarsdev.sokobot.event.CommandEvent; + +public abstract class Command { + + private final String name; + + public Command(String name){ + this.name = name; + } + + public String getName() { + return name; + } + + public abstract void execute(CommandEvent commandEvent); +} diff --git a/src/main/java/me/polymarsdev/sokobot/entity/Player.java b/src/main/java/me/polymarsdev/sokobot/entity/Player.java new file mode 100644 index 0000000..706ef26 --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/entity/Player.java @@ -0,0 +1,97 @@ +package me.polymarsdev.sokobot.entity; + +import me.polymarsdev.sokobot.objects.Grid; + +public class Player { + int x = 0; + int y = 0; + Grid currentGrid; + + public Player(int x, int y, Grid currentGrid) { + this.x = x; + this.y = y; + this.currentGrid = currentGrid; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public void resetPosition() { + int setX = 2; + int setY = 2; + while (currentGrid.isBoxRaw(setX, setY)) { + if (setX >= currentGrid.getWidth() - 1) { + setY++; + setX = 1; + } else setX++; + } + this.x = setX; + this.y = setY; + } + + public boolean moveUp() { + if (!currentGrid.isWall(x, y - 1)) { + if (currentGrid.isBox(x, y - 1)) { + if (currentGrid.getBox(x, y - 1).moveUp()) { + y -= 1; + return true; + } + return false; + } + y -= 1; + return true; + } + return false; + } + + public boolean moveDown() { + if (!currentGrid.isWall(x, y + 1)) { + if (currentGrid.isBox(x, y + 1)) { + if (currentGrid.getBox(x, y + 1).moveDown()) { + y += 1; + return true; + } + return false; + } + y += 1; + return true; + } + return false; + } + + public boolean moveLeft() { + if (!currentGrid.isWall(x - 1, y)) { + if (currentGrid.isBox(x - 1, y)) { + if (currentGrid.getBox(x - 1, y).moveLeft()) { + x -= 1; + return true; + } + return false; + } + x -= 1; + return true; + } + return false; + } + + public boolean moveRight() { + if (!currentGrid.isWall(x + 1, y)) { + if (currentGrid.isBox(x + 1, y)) { + if (currentGrid.getBox(x + 1, y).moveRight()) { + x += 1; + return true; + } + return false; + } + x += 1; + return true; + } + return false; + } + +} diff --git a/src/main/java/me/polymarsdev/sokobot/event/CommandEvent.java b/src/main/java/me/polymarsdev/sokobot/event/CommandEvent.java new file mode 100644 index 0000000..fa3b7f7 --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/event/CommandEvent.java @@ -0,0 +1,159 @@ +package me.polymarsdev.sokobot.event; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class CommandEvent { + private static final int MAX_MESSAGES = 2; + + private final GuildMessageReceivedEvent event; + private String[] args; + + public CommandEvent(GuildMessageReceivedEvent event, String[] args) { + this.event = event; + this.args = args; + } + + public String[] getArgs() { + return args; + } + + public GuildMessageReceivedEvent getEvent() { + return event; + } + + public List getMentionedMembers() { + List mentionedMembers = new ArrayList<>(event.getMessage().getMentionedMembers()); + if (event.getMessage().getContentRaw().startsWith("<@!" + event.getJDA().getSelfUser().getId() + ">")) + mentionedMembers.remove(0); + return mentionedMembers; + } + + public void reply(String message) { + sendMessage(event.getChannel(), message); + } + + public void reply(String message, Consumer success) { + sendMessage(event.getChannel(), message, success); + } + + public void reply(String message, Consumer success, Consumer failure) { + sendMessage(event.getChannel(), message, success, failure); + } + + public void reply(MessageEmbed embed) { + event.getChannel().sendMessage(embed).queue(); + } + + public void reply(MessageEmbed embed, Consumer success) { + event.getChannel().sendMessage(embed).queue(success); + } + + public void reply(MessageEmbed embed, Consumer success, Consumer failure) { + event.getChannel().sendMessage(embed).queue(success, failure); + } + + public void reply(Message message) { + event.getChannel().sendMessage(message).queue(); + } + + public void reply(Message message, Consumer success) { + event.getChannel().sendMessage(message).queue(success); + } + + public void reply(Message message, Consumer success, Consumer failure) { + event.getChannel().sendMessage(message).queue(success, failure); + } + + public void reply(File file, String filename) { + event.getChannel().sendFile(file, filename).queue(); + } + + public void reply(String message, File file, String filename) { + String msg = message == null ? null : splitMessage(message).get(0); + if (msg == null) event.getChannel().sendFile(file, filename).queue(); + else event.getChannel().sendMessage(msg).addFile(file, filename).queue(); + } + + private void sendMessage(MessageChannel chan, String message) { + ArrayList messages = splitMessage(message); + for (int i = 0; i < MAX_MESSAGES && i < messages.size(); i++) { + chan.sendMessage(messages.get(i)).queue(); + } + } + + private void sendMessage(MessageChannel chan, String message, Consumer success) { + ArrayList messages = splitMessage(message); + for (int i = 0; i < MAX_MESSAGES && i < messages.size(); i++) { + if (i + 1 == MAX_MESSAGES || i + 1 == messages.size()) { + chan.sendMessage(messages.get(i)).queue(success); + } else { + chan.sendMessage(messages.get(i)).queue(); + } + } + } + + private void sendMessage(MessageChannel chan, String message, Consumer success, + Consumer failure) { + ArrayList messages = splitMessage(message); + for (int i = 0; i < MAX_MESSAGES && i < messages.size(); i++) { + if (i + 1 == MAX_MESSAGES || i + 1 == messages.size()) { + chan.sendMessage(messages.get(i)).queue(success, failure); + } else { + chan.sendMessage(messages.get(i)).queue(); + } + } + } + + private static ArrayList splitMessage(String stringtoSend) { + ArrayList msgs = new ArrayList<>(); + if (stringtoSend != null) { + stringtoSend = stringtoSend.replace("@everyone", "@\u0435veryone").replace("@here", "@h\u0435re").trim(); + while (stringtoSend.length() > 2000) { + int leeway = 2000 - (stringtoSend.length() % 2000); + int index = stringtoSend.lastIndexOf("\n", 2000); + if (index < leeway) index = stringtoSend.lastIndexOf(" ", 2000); + if (index < leeway) index = 2000; + String temp = stringtoSend.substring(0, index).trim(); + if (!temp.equals("")) msgs.add(temp); + stringtoSend = stringtoSend.substring(index).trim(); + } + if (!stringtoSend.equals("")) msgs.add(stringtoSend); + } + return msgs; + } + + SelfUser getSelfUser() { + return event.getJDA().getSelfUser(); + } + + public User getAuthor() { + return event.getAuthor(); + } + + public Guild getGuild() { + return event.getGuild(); + } + + public JDA getJDA() { + return event.getJDA(); + } + + public Member getMember() { + return event.getMember(); + } + + public Message getMessage() { + return event.getMessage(); + } + + public TextChannel getTextChannel() { + return event.getChannel(); + } +} \ No newline at end of file diff --git a/src/main/java/me/polymarsdev/sokobot/listener/CommandListener.java b/src/main/java/me/polymarsdev/sokobot/listener/CommandListener.java new file mode 100644 index 0000000..5c466c0 --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/listener/CommandListener.java @@ -0,0 +1,127 @@ +package me.polymarsdev.sokobot.listener; + +import me.polymarsdev.sokobot.Bot; +import me.polymarsdev.sokobot.Game; +import me.polymarsdev.sokobot.commands.GameInputCommand; +import me.polymarsdev.sokobot.commands.InfoCommand; +import me.polymarsdev.sokobot.commands.PrefixCommand; +import me.polymarsdev.sokobot.entity.Command; +import me.polymarsdev.sokobot.event.CommandEvent; +import me.polymarsdev.sokobot.util.GameUtil; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; + +import java.util.*; + +public class CommandListener extends ListenerAdapter { + private static final ArrayList commandsNoPrefix = new ArrayList<>( + Arrays.asList("w", "a", "s", "d", "up", "left", "down", "right", "r", "mr")); + private static final HashMap commands = new HashMap<>(); + + public CommandListener() { + List botCommands = new ArrayList<>(Arrays.asList(new InfoCommand(), new PrefixCommand())); + botCommands.addAll(Arrays.asList(new GameInputCommand("play"), new GameInputCommand("continue"), + new GameInputCommand("stop"))); + for (String cnp : commandsNoPrefix) botCommands.add(new GameInputCommand(cnp)); + for (Command command : botCommands) commands.put(command.getName().toLowerCase(), command); + System.out.println("[INFO] Loaded " + commands.size() + " commands"); + } + + + @Override + public void onGuildMessageReceived(GuildMessageReceivedEvent event) { + User user = event.getAuthor(); + Message message = event.getMessage(); + TextChannel channel = event.getChannel(); + Guild guild = event.getGuild(); + if (user.getId().equals(event.getJDA().getSelfUser().getId())) { + List embeds = message.getEmbeds(); + if (embeds.size() > 0) { + MessageEmbed embed = embeds.get(0); + if (embed.getTitle() != null && embed.getTitle().length() > 0) { + if (embed.getTitle().startsWith("Sokobot | Level ")) { + message.addReaction("U+2B05").queue(); + message.addReaction("U+27A1").queue(); + message.addReaction("U+2B06").queue(); + message.addReaction("U+2B07").queue(); + message.addReaction("U+1F504").queue(); + List fields = embed.getFields(); + for (MessageEmbed.Field field : fields) { + if (field.getName() != null && field.getName().equals("Player")) { + if (field.getValue() != null) { + long playerId = Long + .parseLong(field.getValue().substring(2, field.getValue().length() - 1)); + if (GameUtil.hasGame(playerId)) { + Game game = GameUtil.getGame(playerId); + game.setGameMessage(message); + } + } + } + } + } + } + } + return; + } + String msgRaw = message.getContentRaw(); + String[] args = msgRaw.split("\\s+"); + if (args.length > 0) { + boolean isMention = msgRaw.equals("<@" + event.getJDA().getSelfUser().getId() + ">") || msgRaw + .equals("<@!" + event.getJDA().getSelfUser().getId() + ">"); + String prefix = Bot.getPrefix(guild); + String arg = args[0].toLowerCase(); + boolean isCommand; + if (isMention) isCommand = true; + else { + if (arg.startsWith(prefix)) { + if (commandsNoPrefix.contains(arg)) { + isCommand = true; + } else { + String commandName = arg.substring(prefix.length()).toLowerCase(); + isCommand = commands.containsKey(commandName); + if (isCommand) arg = commandName; + } + } else { + isCommand = commandsNoPrefix.contains(arg); + } + } + if (isCommand) { + if (!hasPermissions(guild, channel)) { + sendInvalidPermissionsMessage(user, channel); + return; + } + Command command = commands.get(arg); + if (isMention) command = commands.get("info"); + if (command == null) return; + command.execute(new CommandEvent(event, Arrays.copyOfRange(msgRaw.split("\\s+"), 1, args.length))); + } + } + } + + private static final Collection requiredPermissions = Arrays + .asList(Permission.MESSAGE_ADD_REACTION, Permission.MESSAGE_EMBED_LINKS, Permission.MESSAGE_MANAGE, + Permission.MESSAGE_WRITE); + + private boolean hasPermissions(Guild guild, TextChannel channel) { + Member self = guild.getSelfMember(); + if (self.hasPermission(Permission.ADMINISTRATOR)) return true; + return self.hasPermission(channel, requiredPermissions); + } + + private void sendInvalidPermissionsMessage(User user, TextChannel channel) { + if (channel.canTalk()) { + StringBuilder requiredPermissionsDisplay = new StringBuilder(); + for (Permission requiredPermission : requiredPermissions) { + requiredPermissionsDisplay.append("`").append(requiredPermission.getName()).append("`, "); + } + if (requiredPermissionsDisplay.toString().endsWith(", ")) requiredPermissionsDisplay = new StringBuilder( + requiredPermissionsDisplay.substring(0, requiredPermissionsDisplay.length() - 2)); + channel.sendMessage(user.getAsMention() + ", I don't have enough permissions to work properly.\nMake " + + "sure I have the following permissions: " + requiredPermissionsDisplay + + "\nIf you think this is " + + "an error, please contact a server administrator.").queue(); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/polymarsdev/sokobot/listener/GameListener.java b/src/main/java/me/polymarsdev/sokobot/listener/GameListener.java new file mode 100644 index 0000000..c98577d --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/listener/GameListener.java @@ -0,0 +1,68 @@ +package me.polymarsdev.sokobot.listener; + +import me.polymarsdev.sokobot.Bot; +import me.polymarsdev.sokobot.Game; +import me.polymarsdev.sokobot.util.GameUtil; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.MessageReaction; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.guild.GuildLeaveEvent; +import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; + +public class GameListener extends ListenerAdapter { + + @Override + public void onGuildLeave(GuildLeaveEvent event) { + Guild guild = event.getGuild(); + Bot.removePrefix(guild.getIdLong()); + } + + @Override + public void onGuildMessageReactionAdd(GuildMessageReactionAddEvent event) { + User user = event.getUser(); + if (user.isBot()) { + return; + } + Guild guild = event.getGuild(); + MessageReaction reaction = event.getReaction(); + TextChannel channel = event.getChannel(); + channel.retrieveMessageById(event.getMessageId()).queue(message -> { + if (message.getAuthor().getId().equals(event.getJDA().getSelfUser().getId())) { + Game game; + if (!GameUtil.hasGame(user.getIdLong())) { + game = new Game(user); + GameUtil.setGame(user.getIdLong(), game); + } else game = GameUtil.getGame(user.getIdLong()); + boolean reactionCommand = true; + String userInput = ""; + switch (event.getReactionEmote().toString()) { + case "RE:U+2b05": + userInput = "left"; + break; + case "RE:U+27a1": + userInput = "right"; + break; + case "RE:U+2b06": + userInput = "up"; + break; + case "RE:U+2b07": + userInput = "down"; + break; + case "RE:U+1f504": + userInput = "r"; + break; + default: + reactionCommand = false; + break; + } + if (reactionCommand) game.run(guild, channel, userInput); + if (guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_MANAGE)) + reaction.removeReaction(user).queue(); + } + }); + } + +} diff --git a/src/main/java/Box.java b/src/main/java/me/polymarsdev/sokobot/objects/Box.java similarity index 97% rename from src/main/java/Box.java rename to src/main/java/me/polymarsdev/sokobot/objects/Box.java index c39dfaa..6432c0b 100644 --- a/src/main/java/Box.java +++ b/src/main/java/me/polymarsdev/sokobot/objects/Box.java @@ -1,3 +1,5 @@ +package me.polymarsdev.sokobot.objects; + public class Box { int x = 0; diff --git a/src/main/java/Destination.java b/src/main/java/me/polymarsdev/sokobot/objects/Destination.java similarity index 51% rename from src/main/java/Destination.java rename to src/main/java/me/polymarsdev/sokobot/objects/Destination.java index fd9f93d..128282f 100644 --- a/src/main/java/Destination.java +++ b/src/main/java/me/polymarsdev/sokobot/objects/Destination.java @@ -1,24 +1,25 @@ -public class Destination -{ +package me.polymarsdev.sokobot.objects; + +public class Destination { int x = 0; int y = 0; Grid currentGrid; - public Destination(int x, int y, Grid currentGrid) - { + + public Destination(int x, int y, Grid currentGrid) { this.x = x; this.y = y; this.currentGrid = currentGrid; } - public boolean hasBox(Grid currentGrid) - { + + public boolean hasBox(Grid currentGrid) { return currentGrid.isWall(x, y); } - public int getX() - { + + public int getX() { return x; } - public int getY() - { + + public int getY() { return y; } } diff --git a/src/main/java/me/polymarsdev/sokobot/objects/Grid.java b/src/main/java/me/polymarsdev/sokobot/objects/Grid.java new file mode 100644 index 0000000..64c947a --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/objects/Grid.java @@ -0,0 +1,206 @@ +package me.polymarsdev.sokobot.objects; + +import me.polymarsdev.sokobot.entity.Player; +import me.polymarsdev.sokobot.util.Randomizer; + +public class Grid { + final int GROUND = 0; + final int WALL = 1; + final int BOX = 2; + final int DESTINATION = 3; + final int PLAYER = 4; + final int MAX_BOXES = 8; + Tile[][] grid; + Box[] boxes; + Destination[] destinations; + int boxCount; + int height = 0; + int width = 0; + int color = 0; + Player player; + String playerEmote; + + public Grid(int width, int height, int boxCount, String playerEmote) //create a random grid with specific width, + // height, and number of boxes + { + this.playerEmote = playerEmote; + player = new Player(2, 2, this); + if (boxCount > MAX_BOXES) boxCount = MAX_BOXES; + this.boxCount = boxCount; + boxes = new Box[boxCount]; + destinations = new Destination[boxCount]; + this.height = height; + this.width = width; + grid = new Tile[width][height]; + createBoxes(); + createDestinations(); + player.resetPosition(); + updateGrid(); + } + + public Player getPlayer() { + return player; + } + + public void reset() { + for (int i = 0; i < boxCount; i++) { + boxes[i].reset(); + } + player.resetPosition(); + updateGrid(); + } + + public void resetMap() { + boxes = new Box[boxCount]; + destinations = new Destination[boxCount]; + createBoxes(); + createDestinations(); + player.resetPosition(); + updateGrid(); + } + + public void setStatus(int x, int y, int status) { + grid[x][y].setStatus(status); + } + + public int getStatus(int x, int y) { + return grid[x][y].getStatus(); + } + + public boolean isWall(int x, int y) { + return grid[x][y].getStatus() == WALL; + } + + public boolean isBox(int x, int y) { + return grid[x][y].getStatus() == BOX; + } + + public boolean isBoxRaw(int x, int y) //allows you to check if a box is at a position before grid is set up + { + for (int i = 0; i < boxCount; i++) { + if (x == boxes[i].getX() && y == boxes[i].getY()) { + return true; + } + } + return false; + } + + public boolean isDestination(int x, int y) { + return grid[x][y].getStatus() == DESTINATION; + + } + + public Box getBox(int x, int y) { + for (int i = 0; i < boxCount; i++) { + if (x == boxes[i].getX() && y == boxes[i].getY()) { + return boxes[i]; + } + } + return null; + } + + public void createBoxes() { + color = Randomizer.nextInt(6); //runs after each level + for (int i = 0; i < boxCount; i++) { + int x = Randomizer.nextInt(width - 4) + 2; + int y = Randomizer.nextInt(height - 4) + 2; + for (int j = 0; j < i; j++) { + while ((x == boxes[j].getX() && y == boxes[j].getY()) || (x == 2 && y == 2) || (x - 1 == boxes[j].getX() + && y == boxes[j].getY()) || (x + 1 == boxes[j].getX() && y == boxes[j].getY())) { + x = Randomizer.nextInt(width - 4) + 2; + y = Randomizer.nextInt(height - 4) + 2; + } + } + boxes[i] = new Box(x, y, this); + } + } + + public void createDestinations() { + for (int i = 0; i < boxCount; i++) { + int x = Randomizer.nextInt(width - 2) + 1; + int y = Randomizer.nextInt(height - 2) + 1; + for (int j = 0; j < i; j++) { + while (((x == destinations[j].getX() && y == destinations[j].getY())) || isBoxRaw(x, y)) { + x = Randomizer.nextInt(width - 2) + 1; + y = Randomizer.nextInt(height - 2) + 1; + } + } + destinations[i] = new Destination(x, y, this); + } + } + + public void updateGrid() { + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + grid[j][i] = new Tile(GROUND, playerEmote); + if (j == 0 || j == width - 1 || i == 0 || i == height - 1) { + grid[j][i] = new Tile(WALL, color, playerEmote); + } + for (int k = 0; k < boxCount; k++) { + if (destinations[k].getX() == j && destinations[k].getY() == i) { + grid[j][i] = new Tile(DESTINATION, playerEmote); + } + } + if (player.getX() == j && player.getY() == i) { + grid[j][i] = new Tile(PLAYER, playerEmote); + } + for (int k = 0; k < boxCount; k++) { + if (boxes[k].getX() == j && boxes[k].getY() == i) { + if (boxes[k].onDestination()) { + grid[j][i] = new Tile(WALL, color, playerEmote); + } else { + grid[j][i] = new Tile(BOX, playerEmote); + } + + } + } + } + } + } + + public boolean hasWon() { + for (int i = 0; i < boxCount; i++) { + if (!destinations[i].hasBox(this)) { + return false; + } + } + return true; + } + + public Tile[][] getGrid() { + return grid; + } + + public Box[] getBoxes() { + return boxes; + } + + public Destination[] getDestinations() { + return destinations; + } + + public int getBoxCount() { + return boxCount; + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public String toString() { + updateGrid(); + String result = ""; + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + result += grid[j][i]; + } + result += "\n"; + } + return result; + } + +} diff --git a/src/main/java/Tile.java b/src/main/java/me/polymarsdev/sokobot/objects/Tile.java similarity index 97% rename from src/main/java/Tile.java rename to src/main/java/me/polymarsdev/sokobot/objects/Tile.java index 6c88636..81f2235 100644 --- a/src/main/java/Tile.java +++ b/src/main/java/me/polymarsdev/sokobot/objects/Tile.java @@ -1,3 +1,5 @@ +package me.polymarsdev.sokobot.objects; + public class Tile { final int GROUND = 0; diff --git a/src/main/java/me/polymarsdev/sokobot/util/GameUtil.java b/src/main/java/me/polymarsdev/sokobot/util/GameUtil.java new file mode 100644 index 0000000..5e8988e --- /dev/null +++ b/src/main/java/me/polymarsdev/sokobot/util/GameUtil.java @@ -0,0 +1,81 @@ +package me.polymarsdev.sokobot.util; + +import me.polymarsdev.sokobot.Bot; +import me.polymarsdev.sokobot.Game; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.User; + +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; + +public class GameUtil { + + private static final HashMap games = new HashMap<>(); + + public static void setGame(long userId, Game game) { + games.put(userId, game); + } + + public static boolean hasGame(long userId) { + return games.containsKey(userId); + } + + public static Game getGame(long userId) { + return games.get(userId); + } + + public static void removeGame(long userId) { + games.remove(userId); + } + + public static void sendGameEmbed(MessageChannel channel, String level, String game, User user) { + EmbedBuilder embed = new EmbedBuilder(); + embed.setTitle("Sokobot | Level " + level); + embed.setDescription(game); + embed.addField("Enter direction (``up``, ``down``, ``left``, ``right``/``wasd``), ``r`` to reset or ``mr`` to " + + "recreate the map", "", false); + embed.addField("Player", user.getAsMention(), false); + channel.sendMessage(embed.build()).queue(); + } + + public static void updateGameEmbed(Message message, String level, String game, User user) { + EmbedBuilder embed = new EmbedBuilder(); + embed.setTitle("Sokobot | Level " + level); + embed.setDescription(game); + embed.addField("Enter direction (``up``, ``down``, ``left``, ``right``/``wasd``), ``r`` to reset or ``mr`` to " + + "recreate the map", "", false); + embed.addField("Player", user.getAsMention(), false); + message.editMessage(embed.build()).queue(); + } + + public static void sendWinEmbed(Guild guild, Message message, String level) { + EmbedBuilder embed = new EmbedBuilder(); + embed.setTitle("Sokobot | You win!"); + embed.setDescription( + "Type ``" + Bot.getPrefix(guild) + "continue`` to continue to Level " + level + " or ``" + Bot + .getPrefix(guild) + "stop`` to quit "); + embed.setFooter("You can also press any reaction to continue."); + message.editMessage(embed.build()).queue(); + } + + public static void runGameTimer() { + new Timer().scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + long now = System.currentTimeMillis(); + for (long playerId : games.keySet()) { + Game game = games.get(playerId); + long timeDifference = now - game.lastAction; + if (timeDifference > 10 * 60 * 1000) { + game.stop(); + GameUtil.removeGame(playerId); + } + } + } + }, 10 * 60 * 1000, 60 * 1000); + } +} diff --git a/src/main/java/Randomizer.java b/src/main/java/me/polymarsdev/sokobot/util/Randomizer.java similarity index 98% rename from src/main/java/Randomizer.java rename to src/main/java/me/polymarsdev/sokobot/util/Randomizer.java index ad762ad..a0f6eee 100644 --- a/src/main/java/Randomizer.java +++ b/src/main/java/me/polymarsdev/sokobot/util/Randomizer.java @@ -1,13 +1,11 @@ +package me.polymarsdev.sokobot.util; + import java.util.*; public class Randomizer{ public static Random theInstance = null; - public Randomizer(){ - - } - public static Random getInstance(){ if(theInstance == null){ theInstance = new Random();