├── .gitattributes ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── me │ └── polymarsdev │ └── sokobot │ ├── Bot.java │ ├── Game.java │ ├── commands │ ├── GameInputCommand.java │ ├── InfoCommand.java │ └── PrefixCommand.java │ ├── database │ └── Database.java │ ├── entity │ ├── Command.java │ └── Player.java │ ├── event │ └── CommandEvent.java │ ├── listener │ ├── CommandListener.java │ └── GameListener.java │ ├── objects │ ├── Box.java │ ├── Destination.java │ ├── Grid.java │ └── Tile.java │ └── util │ ├── GameUtil.java │ └── Randomizer.java └── resources └── token.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sokobot 2 | 3 | Sokobot is a Discord bot written with [JDA](https://github.com/DV8FromTheWorld/JDA) that lets you play [Sokoban](https://en.wikipedia.org/wiki/Sokoban), the classic box-pushing puzzle game. 4 | 5 | ## Screenshots 6 | ![Level 1](https://cdn.discordapp.com/attachments/670425377503707146/727568442034487316/sokobot_v1.1.gif) 7 | ![Level 2](https://cdn.discordapp.com/attachments/670425377503707146/727567694597193829/sokobot_v1.1_.gif) 8 | 9 | ## Features 10 | ### Infinite levels 11 | The maps in Sokobot are randomly generated, increasing in difficulty as you progress. 12 | ### Varied controls 13 | Sokobot has multiple control options to improve the player's experience, including reactions and wasd commands! 14 | ### Simultaneous games 15 | Thanks to the power of Java HashMaps™️, multiple users can use the bot at the same time without interfering with one another. 16 | ### Custom prefixes ``New!`` 17 | To prevent Sokobot from conflicting with other bots, admins can choose any single-character prefix to preface Sokobot's commands. 18 | 19 | ## Commands 20 | ### User 21 | - ``!play`` can be used to start a game if you are not currently in one. 22 | - ``!stop`` can be used to stop your active game at any time. 23 | - ``!info`` provides some useful details about the bot and rules of the game. 24 | ### Admin ``New!`` 25 | - ``!prefix [character]`` can be used to change the prefix the bot responds to in the current server. 26 | 27 | ## Usage 28 | ### Public host ``New!`` 29 | Sokobot is available on top.gg and can be added to your server [in one click](https://top.gg/bot/713635251703906336/)! 30 | ### Self-hosting 31 | Grab the [latest .jar](https://github.com/PolyMarsDev/Sokobot/releases) or [build it yourself](#compiling). Then, create a Discord Bot Application [here](https://discord.com/developers/applications/) and paste the bot token into ``token.txt``. Then, ensure the two files are in the same directory and run the .jar file. 32 | Please note, this bot differs a bit from the public bot. For example, there is no voting rewards (custom emotes are always unlocked), no leaderboard, no progress saving, etc. 33 | 34 | 35 | 36 | ## Compiling 37 | 38 | Install [Java 8 JDK](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) and [Gradle](https://gradle.org/). 39 | In the root folder of the project, execute ``gradlew shadowJar``. 40 | The compiled .jar file will be located in ``build/libs``. 41 | 42 | ## Contributing 43 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Feel free to create a fork and use the code for any noncommercial purposes. 44 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'application' 4 | id 'com.github.johnrengelman.shadow' version '5.2.0' 5 | } 6 | 7 | group 'com.polymars' 8 | version '1.1' 9 | 10 | mainClassName = "Bot" 11 | sourceCompatibility = 1.8; 12 | 13 | repositories { 14 | mavenCentral() 15 | jcenter() 16 | } 17 | 18 | dependencies { 19 | compile 'net.dv8tion:JDA:4.1.1_162' 20 | compile 'com.vdurmont:emoji-java:5.1.1' 21 | } 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolyMarsDev/Sokobot/aeac78c2adf629364a6480f1ee94867b19100204/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jun 11 09:36:06 CDT 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Sokobot' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/Bot.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot; 2 | 3 | import me.polymarsdev.sokobot.database.Database; 4 | import me.polymarsdev.sokobot.listener.CommandListener; 5 | import me.polymarsdev.sokobot.listener.GameListener; 6 | import me.polymarsdev.sokobot.util.GameUtil; 7 | import net.dv8tion.jda.api.JDA; 8 | import net.dv8tion.jda.api.OnlineStatus; 9 | import net.dv8tion.jda.api.entities.Activity; 10 | import net.dv8tion.jda.api.entities.Guild; 11 | import net.dv8tion.jda.api.requests.GatewayIntent; 12 | import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder; 13 | import net.dv8tion.jda.api.sharding.ShardManager; 14 | import net.dv8tion.jda.api.utils.cache.CacheFlag; 15 | 16 | import javax.security.auth.login.LoginException; 17 | import java.io.File; 18 | import java.nio.file.Files; 19 | import java.nio.file.Paths; 20 | import java.sql.ResultSet; 21 | import java.sql.SQLException; 22 | import java.util.*; 23 | 24 | public class Bot { 25 | static HashMap prefixes = new HashMap<>(); 26 | 27 | /** 28 | * You can enable the database here. 29 | * Set the DB Type to MySQL or SQLite, which you want to use. 30 | * - 31 | * You can configure login data in the Database class. 32 | */ 33 | private static final boolean enableDatabase = false; 34 | private static final Database.DBType dbType = Database.DBType.SQLite; 35 | 36 | public static boolean debug = false; 37 | 38 | private static ShardManager shardManager; 39 | private static Database database = null; 40 | 41 | public static void main(String[] args) throws LoginException { 42 | String token = null; 43 | try { 44 | File tokenFile = Paths.get("token.txt").toFile(); 45 | if (!tokenFile.exists()) { 46 | System.out.println("[ERROR] Could not find token.txt file"); 47 | System.out.print("Please paste in your bot token: "); 48 | Scanner s = new Scanner(System.in); 49 | token = s.nextLine(); 50 | System.out.println(); 51 | System.out.println("[INFO] Creating token.txt - please wait"); 52 | if (!tokenFile.createNewFile()) { 53 | System.out.println( 54 | "[ERROR] Could not create token.txt - please create this file and paste in your token" 55 | + "."); 56 | s.close(); 57 | return; 58 | } 59 | Files.write(tokenFile.toPath(), token.getBytes()); 60 | s.close(); 61 | } 62 | token = new String(Files.readAllBytes(tokenFile.toPath())); 63 | } catch (Exception ex) { 64 | ex.printStackTrace(); 65 | } 66 | if (token == null) return; 67 | if (enableDatabase) database = new Database(dbType); 68 | if (database != null) { 69 | if (!database.isConnected()) { 70 | database = null; 71 | System.out.println("[ERROR] Database connection failed. Continuing without database."); 72 | } else { 73 | database.update( 74 | "CREATE TABLE IF NOT EXISTS guildprefix (guildId VARCHAR(18) NOT NULL, prefix VARCHAR(8) NOT " 75 | + "NULL);"); 76 | } 77 | } 78 | List intents = new ArrayList<>( 79 | Arrays.asList(GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_EMOJIS, 80 | GatewayIntent.GUILD_MESSAGE_REACTIONS)); 81 | DefaultShardManagerBuilder builder = DefaultShardManagerBuilder.create(token, intents); 82 | builder.setStatus(OnlineStatus.ONLINE); 83 | builder.setActivity(Activity.playing("@Sokobot for info!")); 84 | builder.addEventListeners(new GameListener(), new CommandListener()); 85 | builder.disableCache( 86 | CacheFlag.CLIENT_STATUS, CacheFlag.ACTIVITY, CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE); 87 | shardManager = builder.build(); 88 | GameUtil.runGameTimer(); 89 | Thread consoleThread = new Thread(() -> { 90 | Scanner s = new Scanner(System.in); 91 | while (s.hasNextLine()) { 92 | processCommand(s.nextLine()); 93 | } 94 | }); 95 | consoleThread.setDaemon(true); 96 | consoleThread.setName("Console Thread"); 97 | consoleThread.start(); 98 | } 99 | 100 | private static void processCommand(String cmd) { 101 | if (cmd.equalsIgnoreCase("help")) { 102 | System.out.println("Commands:\nstop - Shuts down the bot and exits the program\ndebug - Toggle debug mode"); 103 | return; 104 | } 105 | if (cmd.equalsIgnoreCase("debug")) { 106 | debug = !debug; 107 | String response = debug ? "on" : "off"; 108 | System.out.println("[INFO] Turned " + response + " debug mode"); 109 | Bot.debug("Make sure to turn off debug mode after necessary information has been collected."); 110 | return; 111 | } 112 | if (cmd.equalsIgnoreCase("stop")) { 113 | System.out.println("Shutting down..."); 114 | shardManager.shutdown(); 115 | if (database != null) { 116 | System.out.println("Disconnecting database..."); 117 | database.disconnect(); 118 | } 119 | System.out.println("Bye!"); 120 | System.exit(0); 121 | return; 122 | } 123 | System.out.println("Unknown command. Please use \"help\" for a list of commands."); 124 | } 125 | 126 | /* 127 | Debug Info for Developer information 128 | > Limit update to 10 seconds minimum because of JDA shard checks 129 | */ 130 | private static long lastDebugInfoUpdate = -1L; 131 | private static String debugInfo = ""; 132 | 133 | private static void updateDebugInfo() { 134 | long now = System.currentTimeMillis(); 135 | if (now - lastDebugInfoUpdate < 10000) return; 136 | lastDebugInfoUpdate = now; 137 | int a = enableDatabase ? 1 : 0; 138 | int b = enableDatabase ? database.isConnected() ? 1 : 0 : 0; 139 | int c = 0; 140 | int d = shardManager.getShardsTotal(); 141 | for (JDA shard : shardManager.getShards()) if (shard.getStatus() == JDA.Status.CONNECTED) c++; 142 | debugInfo = a + b + c + d + ""; 143 | } 144 | 145 | // Print a message when debug is on 146 | public static void debug(String log) { 147 | if (debug) { 148 | updateDebugInfo(); 149 | System.out.println("[DEBUG " + debugInfo + "] " + log); 150 | } 151 | } 152 | 153 | public static ShardManager getShardManager() { 154 | return shardManager; 155 | } 156 | 157 | public static void removePrefix(long guildId) { 158 | prefixes.remove(guildId); 159 | if (database != null) { 160 | database.update("DELETE FROM guildprefix WHERE guildId=?;", String.valueOf(guildId)); 161 | } 162 | } 163 | 164 | public static void setPrefix(Guild guild, String prefix) { 165 | prefixes.put(guild.getIdLong(), prefix); 166 | if (database != null) { 167 | database.update("DELETE FROM guildprefix WHERE guildId=?;", guild.getId()); 168 | database.update("INSERT INTO guildprefix VALUES (?, ?);", guild.getId(), prefix); 169 | } 170 | } 171 | 172 | public static String getPrefix(Guild guild) { 173 | if (prefixes.containsKey(guild.getIdLong())) return prefixes.get(guild.getIdLong()); 174 | if (database != null) { 175 | try (ResultSet rs = database.query("SELECT prefix FROM guildprefix WHERE guildId=?;", guild.getId())) { 176 | if (rs.next()) { 177 | String prefix = rs.getString("prefix"); 178 | prefixes.put(guild.getIdLong(), prefix); 179 | return prefix; 180 | } 181 | prefixes.put(guild.getIdLong(), "!"); 182 | return "!"; 183 | } catch (SQLException ex) { 184 | System.out.println("[ERROR] Error at retrieving guild prefix of guild id " + guild.getId() + ": " + ex 185 | .getMessage()); 186 | } 187 | } 188 | return "!"; 189 | } 190 | } -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/Game.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot; 2 | 3 | import me.polymarsdev.sokobot.objects.Grid; 4 | import me.polymarsdev.sokobot.util.GameUtil; 5 | import net.dv8tion.jda.api.entities.*; 6 | import net.dv8tion.jda.api.requests.RestAction; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.function.Consumer; 10 | 11 | public class Game { 12 | long gameMessageID; 13 | long channelID; 14 | User user; 15 | String playerEmote = ":flushed:"; 16 | public boolean gameActive = false; 17 | public int level = 1; 18 | int width = 9; 19 | int height = 6; 20 | public long lastAction; 21 | Grid grid = new Grid(width, height, level, playerEmote); 22 | 23 | public Game(User user) { 24 | this.user = user; 25 | } 26 | 27 | public void setPlayerEmote(String emote) { 28 | playerEmote = emote; 29 | } 30 | 31 | public void setGameMessage(Message gameMessage) { 32 | // To avoid an Unknown Message error, we will store the IDs and retrieve the Channel object when needed. 33 | gameMessageID = gameMessage.getIdLong(); 34 | channelID = gameMessage.getChannel().getIdLong(); 35 | } 36 | 37 | public void newGame(MessageChannel channel) { 38 | if (!gameActive) { 39 | width = 9; 40 | height = 6; 41 | for (int i = 1; i < level; i++) updateWidthHeight(); 42 | grid = new Grid(width, height, level, playerEmote); 43 | gameActive = true; 44 | lastAction = System.currentTimeMillis(); 45 | GameUtil.sendGameEmbed(channel, String.valueOf(level), grid.toString(), user); 46 | } 47 | } 48 | 49 | // This method used to something earlier. (I actually just forgot what I used it for) 50 | // It did not work like it was supposed to, so it was changed to this basic line. 51 | private void queue(RestAction restAction, Consumer success) { 52 | restAction.queue(success); 53 | } 54 | 55 | public void stop() { 56 | gameActive = false; 57 | TextChannel textChannel = Bot.getShardManager().getTextChannelById(channelID); 58 | if (textChannel != null) { 59 | textChannel.retrieveMessageById(gameMessageID).queue(gameMessage -> gameMessage.delete().queue()); 60 | } 61 | } 62 | 63 | public void run(Guild guild, TextChannel channel, String userInput) { 64 | if (userInput.equals("stop") && gameActive) { 65 | stop(); 66 | channel.sendMessage("Thanks for playing, " + user.getAsMention() + "!") 67 | .queue(msg -> msg.delete().queueAfter(10, TimeUnit.SECONDS)); 68 | } 69 | if (userInput.equals("play") && !gameActive) { 70 | newGame(channel); 71 | } else if (gameActive) { 72 | lastAction = System.currentTimeMillis(); 73 | boolean won = grid.hasWon(); 74 | if (!won) { 75 | boolean moved = false; 76 | switch (userInput) { 77 | case "up": 78 | case "w": 79 | moved = grid.getPlayer().moveUp(); 80 | break; 81 | case "down": 82 | case "s": 83 | moved = grid.getPlayer().moveDown(); 84 | break; 85 | case "left": 86 | case "a": 87 | moved = grid.getPlayer().moveLeft(); 88 | break; 89 | case "right": 90 | case "d": 91 | moved = grid.getPlayer().moveRight(); 92 | break; 93 | case "mr": 94 | grid.resetMap(); 95 | moved = true; 96 | break; 97 | case "r": 98 | grid.reset(); 99 | moved = true; 100 | break; 101 | } 102 | grid.updateGrid(); 103 | won = grid.hasWon(); 104 | if (!won && moved) { 105 | TextChannel textChannel = Bot.getShardManager().getTextChannelById(channelID); 106 | if (textChannel != null) { 107 | queue(textChannel.retrieveMessageById(gameMessageID), gameMessage -> GameUtil 108 | .updateGameEmbed(gameMessage, String.valueOf(level), grid.toString(), user)); 109 | } 110 | } 111 | } 112 | if (won) { 113 | level += 1; 114 | updateWidthHeight(); 115 | TextChannel textChannel = Bot.getShardManager().getTextChannelById(channelID); 116 | if (textChannel != null) { 117 | queue( 118 | textChannel.retrieveMessageById(gameMessageID), 119 | gameMessage -> GameUtil.sendWinEmbed(guild, gameMessage, String.valueOf(level))); 120 | } 121 | grid = new Grid(width, height, level, playerEmote); 122 | } 123 | } 124 | } 125 | 126 | private void updateWidthHeight() { 127 | if (width < 13) { 128 | width += 2; 129 | } 130 | if (height < 8) { 131 | height += 1; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/commands/GameInputCommand.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.commands; 2 | 3 | import com.vdurmont.emoji.EmojiManager; 4 | import me.polymarsdev.sokobot.Bot; 5 | import me.polymarsdev.sokobot.Game; 6 | import me.polymarsdev.sokobot.entity.Command; 7 | import me.polymarsdev.sokobot.event.CommandEvent; 8 | import me.polymarsdev.sokobot.util.GameUtil; 9 | import net.dv8tion.jda.api.Permission; 10 | import net.dv8tion.jda.api.entities.Guild; 11 | import net.dv8tion.jda.api.entities.TextChannel; 12 | import net.dv8tion.jda.api.entities.User; 13 | 14 | public class GameInputCommand extends Command { 15 | 16 | public GameInputCommand(String name) { 17 | super(name); 18 | } 19 | 20 | @Override 21 | public void execute(CommandEvent event) { 22 | User user = event.getAuthor(); 23 | String[] args = event.getArgs(); 24 | String prefix = Bot.getPrefix(event.getGuild()); 25 | Game game; 26 | if (!GameUtil.hasGame(user.getIdLong())) { 27 | game = new Game(user); 28 | GameUtil.setGame(user.getIdLong(), game); 29 | } else game = GameUtil.getGame(user.getIdLong()); 30 | // 31 | String userInput = this.getName().toLowerCase(); 32 | Bot.debug("Processing game input: " + userInput); 33 | if (userInput.equals("play")) { 34 | if (!game.gameActive) { 35 | if (args.length > 0 && EmojiManager.isEmoji(args[0])) game.setPlayerEmote(args[0]); 36 | } else { 37 | event.reply(user.getAsMention() + ", you already have an active game.\nUse `" + prefix 38 | + "stop` to stop your current game first."); 39 | } 40 | } 41 | Guild guild = event.getGuild(); 42 | TextChannel channel = event.getTextChannel(); 43 | game.run(event.getGuild(), channel, userInput); 44 | if (userInput.equals("stop")) GameUtil.removeGame(user.getIdLong()); 45 | if (game.gameActive && guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_MANAGE)) 46 | event.getMessage().delete().queue(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/commands/InfoCommand.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.commands; 2 | 3 | import me.polymarsdev.sokobot.Bot; 4 | import me.polymarsdev.sokobot.entity.Command; 5 | import me.polymarsdev.sokobot.event.CommandEvent; 6 | import net.dv8tion.jda.api.EmbedBuilder; 7 | import net.dv8tion.jda.api.entities.Guild; 8 | 9 | public class InfoCommand extends Command { 10 | 11 | public InfoCommand() { 12 | super("info"); 13 | } 14 | 15 | @Override 16 | public void execute(CommandEvent event) { 17 | Bot.debug("Received info command (or bot mention)"); 18 | Guild guild = event.getGuild(); 19 | EmbedBuilder info = new EmbedBuilder(); 20 | final String prefix = Bot.getPrefix(guild); 21 | info.setTitle("Sokobot"); 22 | info.setThumbnail(guild.getSelfMember().getUser().getAvatarUrl()); 23 | info.setDescription("Sokobot is a bot that lets you play Sokoban, the classic box-pushing puzzle game."); 24 | info.setColor(0xdd2e53); 25 | info.addField("How to Play", "You are a **Sokoban** :flushed:.\nYour job is to push **boxes** :brown_square: " 26 | + "on top of their **destinations** :negative_squared_cross_mark:.", false); 27 | info.addField("Features", ":white_small_square:**Infinite levels**\nThe maps in Sokobot are randomly " 28 | + "generated, increasing in difficulty as you progress.\n:white_small_square:**Varied " + "controls" 29 | + "**\nSokobot has multiple control options to improve the player's experience, including " 30 | + "reactions and wasd commands!\n:white_small_square:**Simultaneous games**\nThanks to the power of " 31 | + "Java HashMaps:tm:, multiple users can use the bot at the same time without interfering with one " 32 | + "another.\n:white_small_square:**Custom prefixes**\nTo prevent Sokobot from conflicting with other " 33 | + "bots, admins can choose any single-character prefix to preface Sokobot's commands.", false); 34 | info.addField( 35 | "Commands", 36 | ("``" + prefix + "play`` can be used to start a game if you are not " + "currently in " + "one.\n``" 37 | + prefix + "stop`` can be used to stop your active game at any " + "time.\n``" + prefix 38 | + "info`` provides some useful details about the bot and " + "rules of " + "the game.\n``" + Bot 39 | .getPrefix(guild) + "prefix [character]`` can be used to " + "change the prefix the " 40 | + "bot responds to."), false); 41 | info.addField( 42 | "Add to your server", 43 | "https://top.gg/bot/713635251703906336\nSokobot is currently in " + Bot.getShardManager().getGuilds() 44 | .size() + " servers.", false); 45 | /* 46 | // Official Support Server 47 | info.addField("Support / Feedback", 48 | "Official Support Server: https://invite.affluentproductions.org/apserver", false); 49 | */ 50 | info.addField("Source code", "https://github.com/PolyMarsDev/Sokobot", false); 51 | info.setFooter("created by PolyMars", "https://avatars0.githubusercontent" + ".com/u/51007356?s=460&u" 52 | + "=4eb8fd498421a2eee9781edfbadf654386cf06c7&v=4"); 53 | event.reply(info.build()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/commands/PrefixCommand.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.commands; 2 | 3 | import me.polymarsdev.sokobot.Bot; 4 | import me.polymarsdev.sokobot.entity.Command; 5 | import me.polymarsdev.sokobot.event.CommandEvent; 6 | import net.dv8tion.jda.api.Permission; 7 | import net.dv8tion.jda.api.entities.Guild; 8 | import net.dv8tion.jda.api.entities.Member; 9 | import net.dv8tion.jda.api.entities.User; 10 | 11 | public class PrefixCommand extends Command { 12 | 13 | public PrefixCommand() { 14 | super("prefix"); 15 | } 16 | 17 | @Override 18 | public void execute(CommandEvent event) { 19 | User user = event.getAuthor(); 20 | Member member = event.getMember(); 21 | String[] args = event.getArgs(); 22 | Guild guild = event.getGuild(); 23 | Bot.debug("Received prefix command: " + event.getMessage().getContentRaw()); 24 | if (args.length > 0) { 25 | if (!member.hasPermission(Permission.ADMINISTRATOR)) { 26 | Bot.debug("Failed to change prefix of " + guild.getName() + " (" + guild.getId() 27 | + "): Insufficient permissions"); 28 | event.reply(user.getAsMention() + ", you do not have permission to use this command."); 29 | return; 30 | } 31 | String newPrefix = args[0].toLowerCase(); 32 | if (newPrefix.length() > 1) { 33 | Bot.debug("Failed to change prefix of " + guild.getName() + " (" + guild.getId() + "): length"); 34 | event.reply(user.getAsMention() + ", the prefix must be one character long!"); 35 | return; 36 | } 37 | Bot.setPrefix(guild, newPrefix); 38 | Bot.debug("Successfully changed server prefix of " + guild.getName() + " (" + guild.getId() + ") to: " 39 | + newPrefix); 40 | event.reply("Prefix successfully changed to ``" + newPrefix + "``."); 41 | return; 42 | } 43 | event.reply(user.getAsMention() + ", please use `" + Bot.getPrefix(guild) 44 | + "prefix ` to set a server-prefix."); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/database/Database.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolyMarsDev/Sokobot/aeac78c2adf629364a6480f1ee94867b19100204/src/main/java/me/polymarsdev/sokobot/database/Database.java -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/entity/Command.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.entity; 2 | 3 | import me.polymarsdev.sokobot.event.CommandEvent; 4 | 5 | public abstract class Command { 6 | 7 | private final String name; 8 | 9 | public Command(String name){ 10 | this.name = name; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public abstract void execute(CommandEvent commandEvent); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/entity/Player.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.entity; 2 | 3 | import me.polymarsdev.sokobot.objects.Grid; 4 | 5 | public class Player { 6 | int x = 0; 7 | int y = 0; 8 | Grid currentGrid; 9 | 10 | public Player(int x, int y, Grid currentGrid) { 11 | this.x = x; 12 | this.y = y; 13 | this.currentGrid = currentGrid; 14 | } 15 | 16 | public int getX() { 17 | return x; 18 | } 19 | 20 | public int getY() { 21 | return y; 22 | } 23 | 24 | public void resetPosition() { 25 | int setX = 2; 26 | int setY = 2; 27 | while (currentGrid.isBoxRaw(setX, setY)) { 28 | if (setX >= currentGrid.getWidth() - 1) { 29 | setY++; 30 | setX = 1; 31 | } else setX++; 32 | } 33 | this.x = setX; 34 | this.y = setY; 35 | } 36 | 37 | public boolean moveUp() { 38 | if (!currentGrid.isWall(x, y - 1)) { 39 | if (currentGrid.isBox(x, y - 1)) { 40 | if (currentGrid.getBox(x, y - 1).moveUp()) { 41 | y -= 1; 42 | return true; 43 | } 44 | return false; 45 | } 46 | y -= 1; 47 | return true; 48 | } 49 | return false; 50 | } 51 | 52 | public boolean moveDown() { 53 | if (!currentGrid.isWall(x, y + 1)) { 54 | if (currentGrid.isBox(x, y + 1)) { 55 | if (currentGrid.getBox(x, y + 1).moveDown()) { 56 | y += 1; 57 | return true; 58 | } 59 | return false; 60 | } 61 | y += 1; 62 | return true; 63 | } 64 | return false; 65 | } 66 | 67 | public boolean moveLeft() { 68 | if (!currentGrid.isWall(x - 1, y)) { 69 | if (currentGrid.isBox(x - 1, y)) { 70 | if (currentGrid.getBox(x - 1, y).moveLeft()) { 71 | x -= 1; 72 | return true; 73 | } 74 | return false; 75 | } 76 | x -= 1; 77 | return true; 78 | } 79 | return false; 80 | } 81 | 82 | public boolean moveRight() { 83 | if (!currentGrid.isWall(x + 1, y)) { 84 | if (currentGrid.isBox(x + 1, y)) { 85 | if (currentGrid.getBox(x + 1, y).moveRight()) { 86 | x += 1; 87 | return true; 88 | } 89 | return false; 90 | } 91 | x += 1; 92 | return true; 93 | } 94 | return false; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/event/CommandEvent.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.event; 2 | 3 | import net.dv8tion.jda.api.JDA; 4 | import net.dv8tion.jda.api.entities.*; 5 | import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; 6 | 7 | import java.io.File; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.function.Consumer; 11 | 12 | public class CommandEvent { 13 | private static final int MAX_MESSAGES = 2; 14 | 15 | private final GuildMessageReceivedEvent event; 16 | private String[] args; 17 | 18 | public CommandEvent(GuildMessageReceivedEvent event, String[] args) { 19 | this.event = event; 20 | this.args = args; 21 | } 22 | 23 | public String[] getArgs() { 24 | return args; 25 | } 26 | 27 | public GuildMessageReceivedEvent getEvent() { 28 | return event; 29 | } 30 | 31 | public List getMentionedMembers() { 32 | List mentionedMembers = new ArrayList<>(event.getMessage().getMentionedMembers()); 33 | if (event.getMessage().getContentRaw().startsWith("<@!" + event.getJDA().getSelfUser().getId() + ">")) 34 | mentionedMembers.remove(0); 35 | return mentionedMembers; 36 | } 37 | 38 | public void reply(String message) { 39 | sendMessage(event.getChannel(), message); 40 | } 41 | 42 | public void reply(String message, Consumer success) { 43 | sendMessage(event.getChannel(), message, success); 44 | } 45 | 46 | public void reply(String message, Consumer success, Consumer failure) { 47 | sendMessage(event.getChannel(), message, success, failure); 48 | } 49 | 50 | public void reply(MessageEmbed embed) { 51 | event.getChannel().sendMessage(embed).queue(); 52 | } 53 | 54 | public void reply(MessageEmbed embed, Consumer success) { 55 | event.getChannel().sendMessage(embed).queue(success); 56 | } 57 | 58 | public void reply(MessageEmbed embed, Consumer success, Consumer failure) { 59 | event.getChannel().sendMessage(embed).queue(success, failure); 60 | } 61 | 62 | public void reply(Message message) { 63 | event.getChannel().sendMessage(message).queue(); 64 | } 65 | 66 | public void reply(Message message, Consumer success) { 67 | event.getChannel().sendMessage(message).queue(success); 68 | } 69 | 70 | public void reply(Message message, Consumer success, Consumer failure) { 71 | event.getChannel().sendMessage(message).queue(success, failure); 72 | } 73 | 74 | public void reply(File file, String filename) { 75 | event.getChannel().sendFile(file, filename).queue(); 76 | } 77 | 78 | public void reply(String message, File file, String filename) { 79 | String msg = message == null ? null : splitMessage(message).get(0); 80 | if (msg == null) event.getChannel().sendFile(file, filename).queue(); 81 | else event.getChannel().sendMessage(msg).addFile(file, filename).queue(); 82 | } 83 | 84 | private void sendMessage(MessageChannel chan, String message) { 85 | ArrayList messages = splitMessage(message); 86 | for (int i = 0; i < MAX_MESSAGES && i < messages.size(); i++) { 87 | chan.sendMessage(messages.get(i)).queue(); 88 | } 89 | } 90 | 91 | private void sendMessage(MessageChannel chan, String message, Consumer success) { 92 | ArrayList messages = splitMessage(message); 93 | for (int i = 0; i < MAX_MESSAGES && i < messages.size(); i++) { 94 | if (i + 1 == MAX_MESSAGES || i + 1 == messages.size()) { 95 | chan.sendMessage(messages.get(i)).queue(success); 96 | } else { 97 | chan.sendMessage(messages.get(i)).queue(); 98 | } 99 | } 100 | } 101 | 102 | private void sendMessage(MessageChannel chan, String message, Consumer success, 103 | Consumer failure) { 104 | ArrayList messages = splitMessage(message); 105 | for (int i = 0; i < MAX_MESSAGES && i < messages.size(); i++) { 106 | if (i + 1 == MAX_MESSAGES || i + 1 == messages.size()) { 107 | chan.sendMessage(messages.get(i)).queue(success, failure); 108 | } else { 109 | chan.sendMessage(messages.get(i)).queue(); 110 | } 111 | } 112 | } 113 | 114 | private static ArrayList splitMessage(String stringtoSend) { 115 | ArrayList msgs = new ArrayList<>(); 116 | if (stringtoSend != null) { 117 | stringtoSend = stringtoSend.replace("@everyone", "@\u0435veryone").replace("@here", "@h\u0435re").trim(); 118 | while (stringtoSend.length() > 2000) { 119 | int leeway = 2000 - (stringtoSend.length() % 2000); 120 | int index = stringtoSend.lastIndexOf("\n", 2000); 121 | if (index < leeway) index = stringtoSend.lastIndexOf(" ", 2000); 122 | if (index < leeway) index = 2000; 123 | String temp = stringtoSend.substring(0, index).trim(); 124 | if (!temp.equals("")) msgs.add(temp); 125 | stringtoSend = stringtoSend.substring(index).trim(); 126 | } 127 | if (!stringtoSend.equals("")) msgs.add(stringtoSend); 128 | } 129 | return msgs; 130 | } 131 | 132 | SelfUser getSelfUser() { 133 | return event.getJDA().getSelfUser(); 134 | } 135 | 136 | public User getAuthor() { 137 | return event.getAuthor(); 138 | } 139 | 140 | public Guild getGuild() { 141 | return event.getGuild(); 142 | } 143 | 144 | public JDA getJDA() { 145 | return event.getJDA(); 146 | } 147 | 148 | public Member getMember() { 149 | return event.getMember(); 150 | } 151 | 152 | public Message getMessage() { 153 | return event.getMessage(); 154 | } 155 | 156 | public TextChannel getTextChannel() { 157 | return event.getChannel(); 158 | } 159 | } -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/listener/CommandListener.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.listener; 2 | 3 | import me.polymarsdev.sokobot.Bot; 4 | import me.polymarsdev.sokobot.commands.GameInputCommand; 5 | import me.polymarsdev.sokobot.commands.InfoCommand; 6 | import me.polymarsdev.sokobot.commands.PrefixCommand; 7 | import me.polymarsdev.sokobot.entity.Command; 8 | import me.polymarsdev.sokobot.event.CommandEvent; 9 | import net.dv8tion.jda.api.Permission; 10 | import net.dv8tion.jda.api.entities.*; 11 | import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; 12 | import net.dv8tion.jda.api.hooks.ListenerAdapter; 13 | 14 | import java.util.*; 15 | 16 | public class CommandListener extends ListenerAdapter { 17 | private static final ArrayList commandsNoPrefix = new ArrayList<>( 18 | Arrays.asList("w", "a", "s", "d", "up", "left", "down", "right", "r", "mr")); 19 | private static final HashMap commands = new HashMap<>(); 20 | 21 | public CommandListener() { 22 | List botCommands = new ArrayList<>(Arrays.asList(new InfoCommand(), new PrefixCommand())); 23 | botCommands.addAll(Arrays.asList(new GameInputCommand("play"), new GameInputCommand("continue"), 24 | new GameInputCommand("stop"))); 25 | for (String cnp : commandsNoPrefix) botCommands.add(new GameInputCommand(cnp)); 26 | for (Command command : botCommands) commands.put(command.getName().toLowerCase(), command); 27 | System.out.println("[INFO] Loaded " + commands.size() + " commands"); 28 | } 29 | 30 | 31 | @Override 32 | public void onGuildMessageReceived(GuildMessageReceivedEvent event) { 33 | User user = event.getAuthor(); 34 | Message message = event.getMessage(); 35 | TextChannel channel = event.getChannel(); 36 | Guild guild = event.getGuild(); 37 | String msgRaw = message.getContentRaw(); 38 | String[] args = msgRaw.split("\\s+"); 39 | if (args.length > 0) { 40 | boolean isMention = msgRaw.equals("<@" + event.getJDA().getSelfUser().getId() + ">") || msgRaw 41 | .equals("<@!" + event.getJDA().getSelfUser().getId() + ">"); 42 | String prefix = Bot.getPrefix(guild); 43 | String arg = args[0].toLowerCase(); 44 | boolean isCommand; 45 | if (isMention) isCommand = true; 46 | else { 47 | if (arg.startsWith(prefix)) { 48 | if (commandsNoPrefix.contains(arg)) { 49 | isCommand = true; 50 | } else { 51 | String commandName = arg.substring(prefix.length()).toLowerCase(); 52 | isCommand = commands.containsKey(commandName); 53 | if (isCommand) arg = commandName; 54 | } 55 | } else { 56 | isCommand = commandsNoPrefix.contains(arg); 57 | } 58 | } 59 | if (isCommand) { 60 | Bot.debug("Command received: " + arg); 61 | if (!hasPermissions(guild, channel)) { 62 | Bot.debug("Not enough permissions to run command: " + arg); 63 | sendInvalidPermissionsMessage(user, channel); 64 | return; 65 | } 66 | Command command = commands.get(arg); 67 | if (isMention) command = commands.get("info"); 68 | if (command == null) { 69 | Bot.debug("Received command does not exist: " + arg); 70 | return; 71 | } 72 | Bot.debug("Executing command: " + arg); 73 | command.execute(new CommandEvent(event, Arrays.copyOfRange(msgRaw.split("\\s+"), 1, args.length))); 74 | } 75 | } 76 | } 77 | 78 | private static final Collection requiredPermissions = Arrays 79 | .asList(Permission.MESSAGE_ADD_REACTION, Permission.MESSAGE_EMBED_LINKS, Permission.MESSAGE_MANAGE, 80 | Permission.MESSAGE_WRITE); 81 | 82 | private boolean hasPermissions(Guild guild, TextChannel channel) { 83 | Member self = guild.getSelfMember(); 84 | if (self.hasPermission(Permission.ADMINISTRATOR)) return true; 85 | return self.hasPermission(channel, requiredPermissions); 86 | } 87 | 88 | private void sendInvalidPermissionsMessage(User user, TextChannel channel) { 89 | if (channel.canTalk()) { 90 | StringBuilder requiredPermissionsDisplay = new StringBuilder(); 91 | for (Permission requiredPermission : requiredPermissions) { 92 | requiredPermissionsDisplay.append("`").append(requiredPermission.getName()).append("`, "); 93 | } 94 | if (requiredPermissionsDisplay.toString().endsWith(", ")) requiredPermissionsDisplay = new StringBuilder( 95 | requiredPermissionsDisplay.substring(0, requiredPermissionsDisplay.length() - 2)); 96 | channel.sendMessage(user.getAsMention() + ", I don't have enough permissions to work properly.\nMake " 97 | + "sure I have the following permissions: " + requiredPermissionsDisplay 98 | + "\nIf you think this is " 99 | + "an error, please contact a server administrator.").queue(); 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/listener/GameListener.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.listener; 2 | 3 | import me.polymarsdev.sokobot.Bot; 4 | import me.polymarsdev.sokobot.Game; 5 | import me.polymarsdev.sokobot.util.GameUtil; 6 | import net.dv8tion.jda.api.Permission; 7 | import net.dv8tion.jda.api.entities.Guild; 8 | import net.dv8tion.jda.api.entities.MessageReaction; 9 | import net.dv8tion.jda.api.entities.TextChannel; 10 | import net.dv8tion.jda.api.entities.User; 11 | import net.dv8tion.jda.api.events.guild.GuildLeaveEvent; 12 | import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent; 13 | import net.dv8tion.jda.api.hooks.ListenerAdapter; 14 | 15 | public class GameListener extends ListenerAdapter { 16 | 17 | @Override 18 | public void onGuildLeave(GuildLeaveEvent event) { 19 | Guild guild = event.getGuild(); 20 | Bot.removePrefix(guild.getIdLong()); 21 | } 22 | 23 | @Override 24 | public void onGuildMessageReactionAdd(GuildMessageReactionAddEvent event) { 25 | User user = event.getUser(); 26 | if (user.isBot()) { 27 | return; 28 | } 29 | Guild guild = event.getGuild(); 30 | MessageReaction reaction = event.getReaction(); 31 | TextChannel channel = event.getChannel(); 32 | channel.retrieveMessageById(event.getMessageId()).queue(message -> { 33 | if (message.getAuthor().getId().equals(event.getJDA().getSelfUser().getId())) { 34 | Game game; 35 | if (!GameUtil.hasGame(user.getIdLong())) { 36 | game = new Game(user); 37 | GameUtil.setGame(user.getIdLong(), game); 38 | } else game = GameUtil.getGame(user.getIdLong()); 39 | boolean reactionCommand = true; 40 | String userInput = ""; 41 | switch (event.getReactionEmote().toString()) { 42 | case "RE:U+2b05": 43 | userInput = "left"; 44 | break; 45 | case "RE:U+27a1": 46 | userInput = "right"; 47 | break; 48 | case "RE:U+2b06": 49 | userInput = "up"; 50 | break; 51 | case "RE:U+2b07": 52 | userInput = "down"; 53 | break; 54 | case "RE:U+1f504": 55 | userInput = "r"; 56 | break; 57 | default: 58 | reactionCommand = false; 59 | break; 60 | } 61 | Bot.debug("Executing reaction input: " + userInput); 62 | if (reactionCommand) { 63 | game.run(guild, channel, userInput); 64 | } else Bot.debug("Received invalid reaction command: " + event.getReactionEmote().getName()); 65 | if (guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_MANAGE)) 66 | reaction.removeReaction(user).queue(); 67 | } 68 | }); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/objects/Box.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.objects; 2 | 3 | public class Box 4 | { 5 | int x = 0; 6 | int y = 0; 7 | int originalX = 0; //in case player messes up and wants to reset 8 | int originalY = 0; 9 | Grid currentGrid; 10 | public Box(int x, int y, Grid currentGrid) 11 | { 12 | this.x = x; 13 | this.y = y; 14 | originalX = x; 15 | originalY = y; 16 | this.currentGrid = currentGrid; 17 | } 18 | public void reset() 19 | { 20 | x = originalX; 21 | y = originalY; 22 | } 23 | public int getX() 24 | { 25 | return x; 26 | } 27 | public int getY() 28 | { 29 | return y; 30 | } 31 | public boolean moveUp() 32 | { 33 | if (!currentGrid.isWall(x, y - 1) && !currentGrid.isBox(x, y - 1)) 34 | { 35 | y -= 1; 36 | return true; 37 | } 38 | return false; 39 | } 40 | public boolean moveDown() 41 | { 42 | if (!currentGrid.isWall(x, y + 1) && !currentGrid.isBox(x, y + 1)) 43 | { 44 | y += 1; 45 | return true; 46 | } 47 | return false; 48 | } 49 | public boolean moveLeft() 50 | { 51 | if (!currentGrid.isWall(x - 1, y) && !currentGrid.isBox(x - 1, y)) 52 | { 53 | x -= 1; 54 | return true; 55 | } 56 | return false; 57 | } 58 | public boolean moveRight() 59 | { 60 | if (!currentGrid.isWall(x + 1, y) && !currentGrid.isBox(x + 1, y)) 61 | { 62 | x += 1; 63 | return true; 64 | } 65 | return false; 66 | } 67 | public boolean onDestination() 68 | { 69 | return currentGrid.isDestination(x, y); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/objects/Destination.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.objects; 2 | 3 | public class Destination { 4 | int x = 0; 5 | int y = 0; 6 | Grid currentGrid; 7 | 8 | public Destination(int x, int y, Grid currentGrid) { 9 | this.x = x; 10 | this.y = y; 11 | this.currentGrid = currentGrid; 12 | } 13 | 14 | public boolean hasBox(Grid currentGrid) { 15 | return currentGrid.isWall(x, y); 16 | } 17 | 18 | public int getX() { 19 | return x; 20 | } 21 | 22 | public int getY() { 23 | return y; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/objects/Grid.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.objects; 2 | 3 | import me.polymarsdev.sokobot.entity.Player; 4 | import me.polymarsdev.sokobot.util.Randomizer; 5 | 6 | public class Grid { 7 | final int GROUND = 0; 8 | final int WALL = 1; 9 | final int BOX = 2; 10 | final int DESTINATION = 3; 11 | final int PLAYER = 4; 12 | final int MAX_BOXES = 8; 13 | Tile[][] grid; 14 | Box[] boxes; 15 | Destination[] destinations; 16 | int boxCount; 17 | int height = 0; 18 | int width = 0; 19 | int color = 0; 20 | Player player; 21 | String playerEmote; 22 | 23 | public Grid(int width, int height, int boxCount, String playerEmote) //create a random grid with specific width, 24 | // height, and number of boxes 25 | { 26 | this.playerEmote = playerEmote; 27 | player = new Player(2, 2, this); 28 | if (boxCount > MAX_BOXES) boxCount = MAX_BOXES; 29 | this.boxCount = boxCount; 30 | boxes = new Box[boxCount]; 31 | destinations = new Destination[boxCount]; 32 | this.height = height; 33 | this.width = width; 34 | grid = new Tile[width][height]; 35 | createBoxes(); 36 | createDestinations(); 37 | player.resetPosition(); 38 | updateGrid(); 39 | } 40 | 41 | public Player getPlayer() { 42 | return player; 43 | } 44 | 45 | public void reset() { 46 | for (int i = 0; i < boxCount; i++) { 47 | boxes[i].reset(); 48 | } 49 | player.resetPosition(); 50 | updateGrid(); 51 | } 52 | 53 | public void resetMap() { 54 | boxes = new Box[boxCount]; 55 | destinations = new Destination[boxCount]; 56 | createBoxes(); 57 | createDestinations(); 58 | player.resetPosition(); 59 | updateGrid(); 60 | } 61 | 62 | public void setStatus(int x, int y, int status) { 63 | grid[x][y].setStatus(status); 64 | } 65 | 66 | public int getStatus(int x, int y) { 67 | return grid[x][y].getStatus(); 68 | } 69 | 70 | public boolean isWall(int x, int y) { 71 | return grid[x][y].getStatus() == WALL; 72 | } 73 | 74 | public boolean isBox(int x, int y) { 75 | return grid[x][y].getStatus() == BOX; 76 | } 77 | 78 | public boolean isBoxRaw(int x, int y) //allows you to check if a box is at a position before grid is set up 79 | { 80 | for (int i = 0; i < boxCount; i++) { 81 | if (x == boxes[i].getX() && y == boxes[i].getY()) { 82 | return true; 83 | } 84 | } 85 | return false; 86 | } 87 | 88 | public boolean isDestination(int x, int y) { 89 | return grid[x][y].getStatus() == DESTINATION; 90 | 91 | } 92 | 93 | public Box getBox(int x, int y) { 94 | for (int i = 0; i < boxCount; i++) { 95 | if (x == boxes[i].getX() && y == boxes[i].getY()) { 96 | return boxes[i]; 97 | } 98 | } 99 | return null; 100 | } 101 | 102 | public void createBoxes() { 103 | color = Randomizer.nextInt(6); //runs after each level 104 | for (int i = 0; i < boxCount; i++) { 105 | int x = Randomizer.nextInt(width - 4) + 2; 106 | int y = Randomizer.nextInt(height - 4) + 2; 107 | for (int j = 0; j < i; j++) { 108 | while ((x == boxes[j].getX() && y == boxes[j].getY()) || (x == 2 && y == 2) || (x - 1 == boxes[j].getX() 109 | && y == boxes[j].getY()) || (x + 1 == boxes[j].getX() && y == boxes[j].getY())) { 110 | x = Randomizer.nextInt(width - 4) + 2; 111 | y = Randomizer.nextInt(height - 4) + 2; 112 | } 113 | } 114 | boxes[i] = new Box(x, y, this); 115 | } 116 | } 117 | 118 | public void createDestinations() { 119 | for (int i = 0; i < boxCount; i++) { 120 | int x = Randomizer.nextInt(width - 2) + 1; 121 | int y = Randomizer.nextInt(height - 2) + 1; 122 | for (int j = 0; j < i; j++) { 123 | while (((x == destinations[j].getX() && y == destinations[j].getY())) || isBoxRaw(x, y)) { 124 | x = Randomizer.nextInt(width - 2) + 1; 125 | y = Randomizer.nextInt(height - 2) + 1; 126 | } 127 | } 128 | destinations[i] = new Destination(x, y, this); 129 | } 130 | } 131 | 132 | public void updateGrid() { 133 | for (int i = 0; i < height; i++) { 134 | for (int j = 0; j < width; j++) { 135 | grid[j][i] = new Tile(GROUND, playerEmote); 136 | if (j == 0 || j == width - 1 || i == 0 || i == height - 1) { 137 | grid[j][i] = new Tile(WALL, color, playerEmote); 138 | } 139 | for (int k = 0; k < boxCount; k++) { 140 | if (destinations[k].getX() == j && destinations[k].getY() == i) { 141 | grid[j][i] = new Tile(DESTINATION, playerEmote); 142 | } 143 | } 144 | if (player.getX() == j && player.getY() == i) { 145 | grid[j][i] = new Tile(PLAYER, playerEmote); 146 | } 147 | for (int k = 0; k < boxCount; k++) { 148 | if (boxes[k].getX() == j && boxes[k].getY() == i) { 149 | if (boxes[k].onDestination()) { 150 | grid[j][i] = new Tile(WALL, color, playerEmote); 151 | } else { 152 | grid[j][i] = new Tile(BOX, playerEmote); 153 | } 154 | 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | public boolean hasWon() { 162 | for (int i = 0; i < boxCount; i++) { 163 | if (!destinations[i].hasBox(this)) { 164 | return false; 165 | } 166 | } 167 | return true; 168 | } 169 | 170 | public Tile[][] getGrid() { 171 | return grid; 172 | } 173 | 174 | public Box[] getBoxes() { 175 | return boxes; 176 | } 177 | 178 | public Destination[] getDestinations() { 179 | return destinations; 180 | } 181 | 182 | public int getBoxCount() { 183 | return boxCount; 184 | } 185 | 186 | public int getHeight() { 187 | return height; 188 | } 189 | 190 | public int getWidth() { 191 | return width; 192 | } 193 | 194 | public String toString() { 195 | updateGrid(); 196 | String result = ""; 197 | for (int i = 0; i < height; i++) { 198 | for (int j = 0; j < width; j++) { 199 | result += grid[j][i]; 200 | } 201 | result += "\n"; 202 | } 203 | return result; 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/objects/Tile.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.objects; 2 | 3 | public class Tile 4 | { 5 | final int GROUND = 0; 6 | final int WALL = 1; 7 | final int BOX = 2; 8 | final int DESTINATION = 3; 9 | final int PLAYER = 4; 10 | int color = 0; 11 | int status = 0; 12 | String playerEmote; 13 | public Tile(int status, String playerEmote) 14 | { 15 | this.status = status; 16 | this.playerEmote = playerEmote; 17 | } 18 | public Tile(int status, int color, String playerEmote) 19 | { 20 | this.status = status; 21 | this.color = color; 22 | this.playerEmote = playerEmote; 23 | } 24 | public void setStatus(int status) 25 | { 26 | this.status = status; 27 | } 28 | public void setStatus(int status, int color) 29 | { 30 | this.status = status; 31 | this.color = color; 32 | } 33 | public int getStatus() 34 | { 35 | return this.status; 36 | } 37 | public String toString() 38 | { 39 | if (status == GROUND) 40 | { 41 | return ":black_large_square:"; 42 | } 43 | if (status == WALL) 44 | { 45 | switch (color) { 46 | case 0: 47 | return ":red_square:"; 48 | case 1: 49 | return ":orange_square:"; 50 | case 2: 51 | return ":yellow_square:"; 52 | case 3: 53 | return ":green_square:"; 54 | case 4: 55 | return ":blue_square:"; 56 | default: 57 | return ":purple_square:"; 58 | } 59 | } 60 | if (status == BOX) 61 | { 62 | return ":brown_square:"; 63 | } 64 | if (status == DESTINATION) 65 | { 66 | return ":negative_squared_cross_mark:"; 67 | } 68 | return playerEmote; 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/util/GameUtil.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.util; 2 | 3 | import me.polymarsdev.sokobot.Bot; 4 | import me.polymarsdev.sokobot.Game; 5 | import net.dv8tion.jda.api.EmbedBuilder; 6 | import net.dv8tion.jda.api.entities.Guild; 7 | import net.dv8tion.jda.api.entities.Message; 8 | import net.dv8tion.jda.api.entities.MessageChannel; 9 | import net.dv8tion.jda.api.entities.User; 10 | 11 | import java.util.HashMap; 12 | import java.util.Timer; 13 | import java.util.TimerTask; 14 | 15 | public class GameUtil { 16 | 17 | private static final HashMap games = new HashMap<>(); 18 | 19 | public static void setGame(long userId, Game game) { 20 | games.put(userId, game); 21 | } 22 | 23 | public static boolean hasGame(long userId) { 24 | return games.containsKey(userId); 25 | } 26 | 27 | public static Game getGame(long userId) { 28 | return games.get(userId); 29 | } 30 | 31 | public static void removeGame(long userId) { 32 | games.remove(userId); 33 | } 34 | 35 | public static void sendGameEmbed(MessageChannel channel, String level, String game, User user) { 36 | EmbedBuilder embed = new EmbedBuilder(); 37 | embed.setTitle("Sokobot | Level " + level); 38 | embed.setDescription(game); 39 | embed.addField("Enter direction (``up``, ``down``, ``left``, ``right``/``wasd``), ``r`` to reset or ``mr`` to " 40 | + "recreate the map", "", false); 41 | embed.addField("Player", user.getAsMention(), false); 42 | channel.sendMessage(embed.build()).queue(message -> { 43 | message.addReaction("U+2B05").queue(); 44 | message.addReaction("U+27A1").queue(); 45 | message.addReaction("U+2B06").queue(); 46 | message.addReaction("U+2B07").queue(); 47 | message.addReaction("U+1F504").queue(); 48 | Game theGame = GameUtil.getGame(user.getIdLong()); 49 | theGame.setGameMessage(message); 50 | }); 51 | } 52 | 53 | public static void updateGameEmbed(Message message, String level, String game, User user) { 54 | EmbedBuilder embed = new EmbedBuilder(); 55 | embed.setTitle("Sokobot | Level " + level); 56 | embed.setDescription(game); 57 | embed.addField("Enter direction (``up``, ``down``, ``left``, ``right``/``wasd``), ``r`` to reset or ``mr`` to " 58 | + "recreate the map", "", false); 59 | embed.addField("Player", user.getAsMention(), false); 60 | message.editMessage(embed.build()).queue(); 61 | } 62 | 63 | public static void sendWinEmbed(Guild guild, Message message, String level) { 64 | EmbedBuilder embed = new EmbedBuilder(); 65 | embed.setTitle("Sokobot | You win!"); 66 | embed.setDescription( 67 | "Type ``" + Bot.getPrefix(guild) + "continue`` to continue to Level " + level + " or ``" + Bot 68 | .getPrefix(guild) + "stop`` to quit "); 69 | embed.setFooter("You can also press any reaction to continue."); 70 | message.editMessage(embed.build()).queue(); 71 | } 72 | 73 | public static void runGameTimer() { 74 | new Timer().scheduleAtFixedRate(new TimerTask() { 75 | @Override 76 | public void run() { 77 | long now = System.currentTimeMillis(); 78 | for (long playerId : games.keySet()) { 79 | Game game = games.get(playerId); 80 | long timeDifference = now - game.lastAction; 81 | if (timeDifference > 10 * 60 * 1000) { 82 | System.out.println("[INFO] Stopped inactive game of " + playerId); 83 | game.stop(); 84 | GameUtil.removeGame(playerId); 85 | } 86 | } 87 | } 88 | }, 10 * 60 * 1000, 60 * 1000); 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/me/polymarsdev/sokobot/util/Randomizer.java: -------------------------------------------------------------------------------- 1 | package me.polymarsdev.sokobot.util; 2 | 3 | import java.util.*; 4 | 5 | public class Randomizer{ 6 | 7 | public static Random theInstance = null; 8 | 9 | public static Random getInstance(){ 10 | if(theInstance == null){ 11 | theInstance = new Random(); 12 | } 13 | return theInstance; 14 | } 15 | 16 | /** 17 | * Return a random boolean value. 18 | * @return True or false value simulating a coin flip. 19 | */ 20 | public static boolean nextBoolean(){ 21 | return Randomizer.getInstance().nextBoolean(); 22 | } 23 | 24 | /** 25 | * This method simulates a weighted coin flip which will return 26 | * true with the probability passed as a parameter. 27 | * 28 | * @param probability The probability that the method returns true, 29 | * a value between 0 to 1 inclusive. 30 | * @return True or false value simulating a weighted coin flip. 31 | */ 32 | public static boolean nextBoolean(double probability){ 33 | return Randomizer.nextDouble() < probability; 34 | } 35 | 36 | /** 37 | * This method returns a random integer. 38 | * @return A random integer. 39 | */ 40 | public static int nextInt(){ 41 | return Randomizer.getInstance().nextInt(); 42 | } 43 | 44 | /** 45 | * This method returns a random integer between 0 and n, exclusive. 46 | * @param n The maximum value for the range. 47 | * @return A random integer between 0 and n, exclusive. 48 | */ 49 | public static int nextInt(int n){ 50 | return Randomizer.getInstance().nextInt(n); 51 | } 52 | 53 | /** 54 | * Return a number between min and max, inclusive. 55 | * @param min The minimum integer value of the range, inclusive. 56 | * @param max The maximum integer value in the range, inclusive. 57 | * @return A random integer between min and max. 58 | */ 59 | public static int nextInt(int min, int max){ 60 | return min + Randomizer.nextInt(max - min + 1); 61 | } 62 | 63 | /** 64 | * Return a random double between 0 and 1. 65 | * @return A random double between 0 and 1. 66 | */ 67 | public static double nextDouble(){ 68 | return Randomizer.getInstance().nextDouble(); 69 | } 70 | 71 | /** 72 | * Return a random double between min and max. 73 | * @param min The minimum double value in the range. 74 | * @param max The maximum double value in the rang. 75 | * @return A random double between min and max. 76 | */ 77 | public static double nextDouble(double min, double max){ 78 | return min + (max - min) * Randomizer.nextDouble(); 79 | } 80 | 81 | /** 82 | * Return a random color. 83 | * @return A random hex string that represents a color. 84 | */ 85 | public static String nextColor(){ 86 | String randomNum = Integer.toHexString(Randomizer.nextInt(0, 16777216)); 87 | return "'#" + randomNum + "'"; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/resources/token.txt: -------------------------------------------------------------------------------- 1 | replace this with your Discord bot token (https://discord.com/developers/applications) --------------------------------------------------------------------------------