diff --git a/pom.xml b/pom.xml
index b1d2179..ef5b782 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.tictactoebot
TicTacToeBot
- 1.0
+ 3.0
17
@@ -14,6 +14,11 @@
UTF-8
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ 3.0.7
+
net.dv8tion
JDA
@@ -30,10 +35,11 @@
2.0.0
- org.mongodb
- mongodb-driver
- 3.12.11
+ com.zaxxer
+ HikariCP
+ 5.0.1
+
diff --git a/src/main/java/com/tictactoebot/Database.java b/src/main/java/com/tictactoebot/Database.java
index 43e1464..8db1d3d 100644
--- a/src/main/java/com/tictactoebot/Database.java
+++ b/src/main/java/com/tictactoebot/Database.java
@@ -1,16 +1,292 @@
package com.tictactoebot;
-import com.mongodb.client.MongoClient;
+/*import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
-import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.MongoDatabase;*/
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.User;
+import org.mariadb.jdbc.MariaDbPoolDataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.*;
import java.util.HashMap;
import java.util.Map;
-public class Database {
- private static final MongoClient mongoClient = MongoClients.create("mongodb+srv://srin:dQum7hg9t6nNrza@cluster0.wk7r9tr.mongodb.net/?retryWrites=true&w=majority");
- private static final MongoDatabase database = mongoClient.getDatabase("tictactoe");
- public static final MongoCollection LEVELS = database.getCollection("levels");
+public class Database implements AutoCloseable {
+ private final MariaDbPoolDataSource pool;
+
public static final Map MATCH_DATA_MAP = new HashMap<>();
+ private static final Logger LOGGER = LoggerFactory.getLogger(Database.class);
+
+ public Database(String user, String password, String host, String name) {
+ String connectionStr = "jdbc:mariadb://" + host + "/" + name + "?user=" + user + "&password=" + password + "&maxPoolSize=20";
+ try {
+ pool = new MariaDbPoolDataSource(connectionStr);
+ try (Connection connection = pool.getConnection()) {
+ try (Statement statement = connection.createStatement()) {
+ ResultSet res = statement.executeQuery("select TABLE_NAME from information_schema.TABLES where TABLE_NAME='levels'");
+ if (res.next()) return;
+ statement.execute("""
+ create table levels(
+ user_id varchar(18),
+ guild_id varchar(18),
+ draws int default 0,
+ wins int default 0,
+ loses int default 0,
+ level int default 0,
+ xp int default 0,
+ xp_limit int default 100,
+ tag varchar(20),
+
+ constraint id primary key (user_id, guild_id)
+ );
+ """);
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+
+ public void insertNewUser(Guild guild, User user) {
+ if (user.isBot()) return;
+ String guildId = guild.getId();
+ String userId = user.getId();
+ try (Connection connection = pool.getConnection()) {
+ try (Statement statement = connection.createStatement()) {
+ ResultSet res = statement.executeQuery("select tag from levels where user_id='" + userId + "' and guild_id='" + guildId + '\'');
+ if (res.next()) {
+ return;
+ }
+ statement.execute("insert into levels(user_id, guild_id, tag) values ('" + userId + "', '" + guildId + "', '" + user.getAsTag() + "')");
+ }
+ } catch (SQLException exception) {
+ LOGGER.error(exception.getSQLState(), exception);
+ }
+ /*Document result = Database.LEVELS.find(
+ Filters.and(
+ Filters.eq("guildId", guildId),
+ Filters.eq("userId", userId)
+ )
+ ).first();
+ if (result == null) {
+ Database.LEVELS.insertOne(new Document()
+ .append("guildId", guildId)
+ .append("userId", userId)
+ .append("tag", user.getAsTag())
+ .append("game-level", 0)
+ .append("game-xp", 0)
+ .append("game-xp-limit", 100)
+ .append("wins", 0)
+ .append("loses", 0)
+ .append("draws", 0)
+ );
+ }*/
+ }
+
+ public UserInfo getUserInfo(String id, String guildId) {
+ UserInfo info = UserInfo.NULL;
+ try (Connection connection = pool.getConnection()) {
+ try (Statement statement = connection.createStatement()) {
+ ResultSet res = statement.executeQuery("select * from levels where user_id='" + id + "' and guild_id='" + guildId + '\'');
+ if (res.next()) {
+ info = new UserInfo(
+ res.getString("user_id"),
+ res.getString("guild_id"),
+ res.getInt("draws"),
+ res.getInt("wins"),
+ res.getInt("loses"),
+ res.getInt("level"),
+ res.getInt("xp"),
+ res.getInt("xp_limit"),
+ res.getString("tag")
+ );
+ }
+ }
+ } catch (SQLException exception) {
+ LOGGER.error(exception.getSQLState(), exception);
+ }
+ return info;
+ }
+
+ public void resetXPIfCrossesLimit(UserInfo info) {
+ int xp = info.xp;
+ int limit = info.xp_limit;
+ int level = info.level;
+ if (xp >= limit) {
+ level++;
+ limit = level * 201;
+
+ info.xp(xp % limit);
+ info.xp_limit(limit);
+ info.level(level);
+
+ /*Database.LEVELS.updateOne(
+ Filters.and(
+ Filters.eq("userId", id),
+ Filters.eq("guildId", guildId)
+ ),
+ Filters.and(
+ Filters.eq("$set", Filters.eq("game-xp", xp % limit)),
+ Filters.eq("$set", Filters.eq("game-xp-limit", limit)),
+ Filters.eq("$set", Filters.eq("game-level", level))
+ )
+ );*/
+ }
+ }
+
+ public void update(UserInfo winner, UserInfo loser) {
+ try (Connection connection = pool.getConnection()) {
+ try (Statement statement = connection.createStatement()) {
+ statement.executeUpdate("update levels set wins=wins+1, xp=" + (winner.xp + 10) + ", xp_limit=" + winner.xp_limit + ", level=" + winner.level + " where guild_id='" + winner.guild_id + "' and user_id='" + winner.user_id + '\'');
+ statement.executeUpdate("update levels set loses=loses+1, xp=" + (loser.xp + 1) + ", xp_limit=" + loser.xp_limit + ", level=" + loser.level + " where guild_id='" + loser.guild_id + "' and user_id='" + loser.user_id + '\'');
+ }
+ } catch (SQLException e) {
+ LOGGER.error(e.getSQLState(), e);
+ }
+ }
+
+ public void updateDraw(UserInfo winner, UserInfo loser) {
+ try (Connection connection = pool.getConnection()) {
+ try (Statement statement = connection.createStatement()) {
+ statement.executeUpdate("update levels set draws=draws+1, xp=" + (winner.xp + 5) + ", xp_limit=" + winner.xp_limit + ", level=" + winner.level + " where guild_id='" + winner.guild_id + "' and user_id='" + winner.user_id + '\'');
+ statement.executeUpdate("update levels set draws=draws+1, xp=" + (loser.xp + 5) + ", xp_limit=" + loser.xp_limit + ", level=" + loser.level + " where guild_id='" + loser.guild_id + "' and user_id='" + loser.user_id + '\'');
+ }
+ } catch (SQLException e) {
+ LOGGER.error(e.getSQLState(), e);
+ }
+ }
+
+ public void forEach(String guildID, ILeaderboardInfo info) {
+ try (Connection connection = pool.getConnection()) {
+ try (Statement statement = connection.createStatement()) {
+ ResultSet res = statement.executeQuery("select tag, xp, xp_limit, level from levels where guild_id='" + guildID + "' order by level DESC, xp DESC, tag");
+ while (res.next()) {
+ info.info(
+ res.getString("tag"),
+ res.getInt("xp"),
+ res.getInt("xp_limit"),
+ res.getInt("level")
+ );
+ }
+ }
+ } catch (SQLException e) {
+ LOGGER.error(e.getSQLState(), e);
+ }
+ }
+
+ @Override
+ public void close() {
+ pool.close();
+ }
+
+ @SuppressWarnings("unused")
+ public static class UserInfo {
+ public static final UserInfo NULL = null;
+ String user_id;
+ String guild_id;
+ int draws;
+ int wins;
+ int loses;
+ int level;
+ int xp;
+ int xp_limit;
+ String tag;
+
+ public UserInfo(String user_id, String guild_id, int draws, int wins, int loses, int level, int xp, int xp_limit, String tag) {
+ this.user_id = user_id;
+ this.guild_id = guild_id;
+ this.draws = draws;
+ this.wins = wins;
+ this.loses = loses;
+ this.level = level;
+ this.xp = xp;
+ this.xp_limit = xp_limit;
+ this.tag = tag;
+ }
+
+ public String user_id() {
+ return user_id;
+ }
+
+ public void user_id(String user_id) {
+ this.user_id = user_id;
+ }
+
+ public String guild_id() {
+ return guild_id;
+ }
+
+ public void guild_id(String guild_id) {
+ this.guild_id = guild_id;
+ }
+
+ public int draws() {
+ return draws;
+ }
+
+ public void draws(int draws) {
+ this.draws = draws;
+ }
+
+ public int wins() {
+ return wins;
+ }
+
+ public void wins(int wins) {
+ this.wins = wins;
+ }
+
+ public int loses() {
+ return loses;
+ }
+
+ public void loses(int loses) {
+ this.loses = loses;
+ }
+
+ public int level() {
+ return level;
+ }
+
+ public void level(int level) {
+ this.level = level;
+ }
+
+ public int xp() {
+ return xp;
+ }
+
+ public void xp(int xp) {
+ this.xp = xp;
+ }
+
+ public int xp_limit() {
+ return xp_limit;
+ }
+
+ public void xp_limit(int xp_limit) {
+ this.xp_limit = xp_limit;
+ }
+
+ public String tag() {
+ return tag;
+ }
+
+ public void tag(String tag) {
+ this.tag = tag;
+ }
+ }
+
+ @FunctionalInterface
+ public interface ILeaderboardInfo {
+ void info(String tag, int xp, int xp_limit, int level);
+ }
}
diff --git a/src/main/java/com/tictactoebot/Events.java b/src/main/java/com/tictactoebot/Events.java
deleted file mode 100644
index 590547d..0000000
--- a/src/main/java/com/tictactoebot/Events.java
+++ /dev/null
@@ -1,406 +0,0 @@
-package com.tictactoebot;
-
-import com.mongodb.client.model.Filters;
-import com.tictactoebot.game.Game;
-import com.tictactoebot.game.Player;
-import com.tictactoebot.game.TicTacToe;
-import net.dv8tion.jda.api.Permission;
-import net.dv8tion.jda.api.entities.Guild;
-import net.dv8tion.jda.api.entities.MessageEmbed;
-import net.dv8tion.jda.api.entities.User;
-import net.dv8tion.jda.api.events.ReadyEvent;
-import net.dv8tion.jda.api.events.ShutdownEvent;
-import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
-import net.dv8tion.jda.api.events.guild.GuildReadyEvent;
-import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
-import net.dv8tion.jda.api.events.interaction.ButtonClickEvent;
-import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
-import net.dv8tion.jda.api.hooks.ListenerAdapter;
-import net.dv8tion.jda.api.interactions.commands.OptionMapping;
-import net.dv8tion.jda.api.interactions.commands.OptionType;
-import net.dv8tion.jda.api.interactions.components.ActionRow;
-import net.dv8tion.jda.api.interactions.components.Button;
-import org.bson.Document;
-import org.jetbrains.annotations.NotNull;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicInteger;
-
-public class Events extends ListenerAdapter {
- public static final Events INSTANCE = new Events();
- public static final Logger LOGGER = LoggerFactory.getLogger(Events.class);
-
- @Override
- public void onReady(@NotNull ReadyEvent event) {
- LOGGER.info("%s is ready!".formatted(event.getJDA().getSelfUser().getName()));
- }
-
- @Override
- public void onShutdown(@NotNull ShutdownEvent event) {
- LOGGER.info("%s is shutting down...".formatted(event.getJDA().getSelfUser().getName()));
- }
-
- @Override
- public void onGuildReady(@NotNull GuildReadyEvent event) {
- Guild guild = event.getGuild();
- upsertCommands(guild);
-
- guild.getMembers().forEach( member -> {
- User user = member.getUser();
- insertNewUser(guild, user);
- });
- }
-
- private static void upsertCommands(Guild guild) {
- guild
- .upsertCommand("play", "Play a game of TicTacToe with a mentioned user")
- .addOption(OptionType.USER, "user", "The user to play with", true)
- .queue();
- guild
- .upsertCommand("leaderboard", "View the leaderboard").queue();
- guild
- .upsertCommand("stats", "View your current level or the mentioned user")
- .addOption(OptionType.USER, "user", "check level of this user", false)
- .queue();
-
- LOGGER.info("Registered commands for %s".formatted(guild.getName()));
- }
-
- @Override
- public void onGuildMemberJoin(@NotNull GuildMemberJoinEvent event) {
- Guild guild = event.getGuild();
- User user = event.getUser();
- insertNewUser(guild, user);
- }
-
- private static void insertNewUser(Guild guild, User user) {
- if (user.isBot()) return;
- Document result = Database.LEVELS.find(
- Filters.and(
- Filters.eq("guildId", guild.getId()),
- Filters.eq("userId", user.getId())
- )
- ).first();
- if (result == null) {
- Database.LEVELS.insertOne(new Document()
- .append("guildId", guild.getId())
- .append("userId", user.getId())
- .append("tag", user.getAsTag())
- .append("game-level", 0)
- .append("game-xp", 0)
- .append("game-xp-limit", 100)
- .append("wins", 0)
- .append("loses", 0)
- .append("draws", 0)
- );
- }
- }
-
- @Override
- public void onGuildJoin(@NotNull GuildJoinEvent event) {
- Guild guild = event.getGuild();
- upsertCommands(guild);
- LOGGER.info("Joined guild: " + guild.getName());
- guild.getMembers().forEach( member -> {
- User user = member.getUser();
- insertNewUser(guild, user);
- });
- }
-
- @Override
- public void onSlashCommand(@NotNull SlashCommandEvent event) {
- List permissions = List.of(
- Permission.USE_SLASH_COMMANDS,
- Permission.MESSAGE_EMBED_LINKS,
- Permission.MESSAGE_ATTACH_FILES,
- Permission.MESSAGE_WRITE
- );
- if (!Objects.requireNonNull(event.getGuild()).getSelfMember().hasPermission(
- permissions
- )) {
- StringBuilder builder = new StringBuilder();
- builder.append("```").append('\n');
- permissions.forEach(permission -> builder.append(permission.getName()).append('\n'));
- builder.append("```");
- event.reply("_I do not have one of the following permissions:_\n" + builder).setEphemeral(true).queue();
- return;
- }
- Guild guild = event.getGuild();
- if (guild == null) {
- return;
- }
- String commandName = event.getName();
- User user = event.getUser();
- switch (commandName) {
- case "play" -> {
- User mentionedUser = Objects.requireNonNull(event.getOption("user")).getAsUser();
- if (mentionedUser.equals(user)) {
- event.reply("You can't play with yourself!").setEphemeral(true).queue();
- return;
- }
- if (mentionedUser.isBot()) {
- event.reply("You can't play with a bot!").setEphemeral(true).queue();
- return;
- }
- // confirm if the mentioned user is ready to play
- event.deferReply().queue(
- interactionHook ->
- interactionHook.sendMessage("%s, will you accept a challenge of a game of TicTacToe from %s?".formatted(
- mentionedUser.getAsMention(),
- user.getAsMention()
- )).addActionRow(
- Button.success("ready", "Yes"),
- Button.danger("not-ready", "No")
- ).queue(message -> {
- long messageID = message.getIdLong();
- MatchData matchData = MatchData.create(user, mentionedUser);
- matchData.setPendingEdit(message);
- Database.MATCH_DATA_MAP.put(messageID, matchData);
- }));
- }
- case "stats" -> {
- OptionMapping option = event.getOption("user");
- final var optionUser = new Object() {
- User current = user;
- };
- if (option != null) {
- optionUser.current = option.getAsUser();
- if (optionUser.current.isBot()) {
- event.reply("Bots don't participate in leveling").setEphemeral(true).queue();
- return;
- }
- }
-
- event.deferReply().queue(hook -> {
- Document document = getUserInfo(optionUser.current.getId(), guild.getId());
-
- int xp = document.getInteger("game-xp");
- double xpLimit = document.getInteger("game-xp-limit");
- int level = document.getInteger("game-level");
- double ratio = xp / xpLimit;
- int progressLength = (int) (ratio * 10);
- int empty = 10 - progressLength;
- String progressBar = "%s%s %d%%".formatted("\u2588".repeat(progressLength), "_".repeat(empty), (int) (ratio * 100));
- String statsWindow =
- "```js\n" +
- "// %s's stats //\n\n".formatted(optionUser.current.getAsTag()) +
- "game level: " + level + '\n' +
- "game xp: %d/%d\n\nprogress bar\n".formatted(xp, (int) xpLimit) +
- progressBar + "\n\n" +
- "wins: " + document.getInteger("wins") + ", " +
- "loses: " + document.getInteger("loses") + ", " +
- "draws: " + document.getInteger("draws") + '\n' +
- "```";
- hook.sendMessage(statsWindow).queue();
- });
- }
- case "leaderboard" -> event.deferReply().queue(hook -> {
- try (var result = Database.LEVELS.aggregate(List.of(
- Filters.eq("$match", Filters.eq("guildId", guild.getId())),
- Filters.eq("$group", Filters.and(
- Filters.eq("_id", "$_id"),
- Filters.eq("userId", Filters.eq("$first", "$userId")),
- Filters.eq("tag", Filters.eq("$first", "$tag")),
- Filters.eq("game-level", Filters.eq("$first", "$game-level")),
- Filters.eq("game-xp", Filters.eq("$first", "$game-xp")),
- Filters.eq("game-xp-limit", Filters.eq("$first", "$game-xp-limit"))
- )),
- Filters.eq("$sort", Filters.and(
- Filters.eq("game-level", -1),
- Filters.eq("game-xp", -1),
- Filters.eq("tag", 1)
- ))
- )).iterator()) {
- final StringBuilder leaderboard = new StringBuilder();
- AtomicInteger rank = new AtomicInteger(1);
- leaderboard.append("```kt\nleaderboard\n\n");
- String cell = "%6s";
- leaderboard.append(cell.formatted("rank"))
- .append(cell.formatted("level"))
- .append("%13s".formatted("xp"))
- .append(" name").append('\n');
- result.forEachRemaining(document -> {
- String tag = document.getString("tag");
- int level = document.getInteger("game-level");
- int xp = document.getInteger("game-xp");
- int xpLimit = document.getInteger("game-xp-limit");
- String xpStr = "(%d/%d)".formatted(xp, xpLimit);
- leaderboard.append(cell.formatted(rank.getAndIncrement()))
- .append(cell.formatted(level))
- .append("%13s".formatted(xpStr))
- .append(" %s".formatted(tag)).append('\n');
- });
- leaderboard.append("\n```");
- hook.sendMessage(leaderboard.toString()).queue();
- } catch (Exception e) {
- LOGGER.error(e.getMessage(), e);
- }
- });
- }
- }
-
- @Override
- public void onButtonClick(@NotNull ButtonClickEvent event) {
- String buttonId = event.getComponentId();
- final User user = event.getUser();
- final long messageID = event.getMessage().getIdLong();
-
- final boolean isPlayer = Database.MATCH_DATA_MAP.containsKey(messageID);
-
- if (!isPlayer) {
- event.getInteraction().deferEdit().queue();
- return;
- }
- final MatchData matchData = Database.MATCH_DATA_MAP.get(messageID);
- final User challenger = matchData.getChallenger();
- final User challengeAccepter = matchData.getChallengeAccepter();
- switch (buttonId) {
- case "ready" -> { try {
- if (!user.equals(challengeAccepter)) {
- event.deferEdit().queue();
- return;
- }
-
- Game game = matchData.acknowledge();
- File boardImage = game.getBoardImage();
-
- event.editMessage("game started")
- .setEmbeds(game.getEmbed())
- .addFile(boardImage)
- .setActionRows(game.getRows()).queue();
- } catch (Exception e) { LOGGER.error(e.getMessage(), e); } }
- case "not-ready" -> {
- if (!user.equals(challengeAccepter)) {
- event.deferEdit().queue();
- return;
- }
- event.editMessageFormat("%s declined the challenge!", user.getAsMention()).setActionRows(List.of()).queue();
- Database.MATCH_DATA_MAP.remove(messageID);
- }
- case "8", "7", "6", "5", "4", "3", "2", "1", "0" -> { try {
- long userID = user.getIdLong();
- Game game = matchData.getGame();
-
- if (!game.contains(userID)) {
- event.deferEdit().queue();
- return;
- }
- Player.Type playerType = game.getPlayerType(user);
- TicTacToe.Move result = game.select(buttonId, playerType);
- final String guildId = Objects.requireNonNull(event.getGuild()).getId();
- List board = game.getRows();
- File boardImage = game.getBoardImage();
- MessageEmbed embed = game.getEmbed();
- switch (result) {
- case WIN -> event.editMessage("game ended").queue(hook -> {
- User winner = game.getCurrentUser();
- User loser = winner.getIdLong() == challenger.getIdLong() ? challengeAccepter : challenger;
- hook.editOriginalComponents(List.of())
- .setEmbeds(embed)
- .retainFilesById(List.of())
- .addFile(boardImage).queue();
-
- resetXPIfCrossesLimit(winner.getId(), guildId);
- resetXPIfCrossesLimit(loser.getId(), guildId);
-
- Database.LEVELS.updateOne(
- Filters.and(
- Filters.eq("userId", winner.getId()),
- Filters.eq("guildId", guildId)
- ),
- Filters.eq("$inc", Filters.and(Filters.eq("wins", 1), Filters.eq("game-xp", 10)))
- );
- Database.LEVELS.updateOne(
- Filters.and(
- Filters.eq("userId", loser.getId()),
- Filters.eq("guildId", guildId)
- ),
- Filters.eq("$inc", Filters.and(Filters.eq("loses", 1), Filters.eq("game-xp", 1)))
- );
-
- Database.MATCH_DATA_MAP.remove(messageID);
- });
- case DRAW -> event.editMessage("game ended").queue(hook -> {
- hook.editOriginalComponents(List.of())
- .setEmbeds(embed)
- .retainFilesById(List.of())
- .addFile(boardImage)
- .queue();
- resetXPIfCrossesLimit(challenger.getId(), guildId);
- Database.LEVELS.updateOne(
- Filters.and(
- Filters.eq("userId", challenger.getId()),
- Filters.eq("guildId", guildId)
- ),
- Filters.eq("$inc", Filters.and(Filters.eq("draws", 1), Filters.eq("game-xp", 5)))
- );
-
- resetXPIfCrossesLimit(challengeAccepter.getId(), guildId);
- Database.LEVELS.updateOne(
- Filters.and(
- Filters.eq("userId", challengeAccepter.getId()),
- Filters.eq("guildId", guildId)
- ),
- Filters.eq("$inc", Filters.and(Filters.eq("draws", 1), Filters.eq("game-xp", 5)))
- );
- Database.MATCH_DATA_MAP.remove(messageID);
- });
- case NONE ->
- event.deferEdit()
- .queue(hook ->
- hook.editOriginalComponents(board)
- .setEmbeds(embed)
- .retainFilesById(List.of())
- .addFile(boardImage).queue());
- case INVALID -> event.deferEdit().queue();
- }
- } catch (IOException e) { LOGGER.error(e.getMessage(), e); } }
- case "resign" -> {
- Game game = matchData.getGame();
- if (!game.contains(user.getIdLong())) {
- event.deferEdit().queue();
- return;
- }
- game.resign(user);
- MessageEmbed embed = game.getEmbed();
- game.disableButtons();
- event.deferEdit().queue(hook -> hook.editOriginal("game ended").setActionRows(List.of()).setEmbeds(embed).queue());
- Database.MATCH_DATA_MAP.remove(messageID);
- }
- }
- }
-
- private void resetXPIfCrossesLimit(String id, String guildId) {
- Document info = getUserInfo(id, guildId);
- double xp = info.getInteger("game-xp");
- double limit = info.getInteger("game-xp-limit");
- int level = info.getInteger("game-level");
- if (xp >= limit) {
- level++;
- limit = level * 201;
- Database.LEVELS.updateOne(
- Filters.and(
- Filters.eq("userId", id),
- Filters.eq("guildId", guildId)
- ),
- Filters.and(
- Filters.eq("$set", Filters.eq("game-xp", xp % limit)),
- Filters.eq("$set", Filters.eq("game-xp-limit", limit)),
- Filters.eq("$set", Filters.eq("game-level", level))
- )
- );
- }
- }
-
- private static Document getUserInfo(String id, String guildId) {
- return Objects.requireNonNull(Database.LEVELS.find(Filters.and(
- Filters.eq("userId", id),
- Filters.eq("guildId", guildId)
- )).first());
- }
-}
diff --git a/src/main/java/com/tictactoebot/Main.java b/src/main/java/com/tictactoebot/Main.java
index 6a8f442..7561c1e 100644
--- a/src/main/java/com/tictactoebot/Main.java
+++ b/src/main/java/com/tictactoebot/Main.java
@@ -1,5 +1,6 @@
package com.tictactoebot;
+import com.tictactoebot.events.*;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus;
@@ -13,10 +14,27 @@
public class Main {
public static void main(String[] args) throws LoginException {
- String token = args.length != 0? args[0] : System.getenv("BOT_TOKEN");
- if (token == null) {
- throw new IllegalArgumentException("Either the bot token not been provided in command line argument or environment variable BOT_TOKEN is not set!");
+ String bot_token_env = System.getenv("BOT_TOKEN");
+ String token;
+ int index = 0;
+ int expectedLength = 5;
+ if (bot_token_env != null) {
+ token = bot_token_env;
+ expectedLength = 4;
+ } else token = args[index++];
+
+ if (args.length != expectedLength) {
+ System.out.println("Usage: java -jar TicTacToeBot.jar ");
+ return;
}
+
+ String dbUser = args[index++];
+ String dbPassword = args[index++];
+ String dbHost = args[index++];
+ String dbName = args[index];
+
+ Database db = new Database(dbUser, dbPassword, dbHost, dbName);
+
JDA bot = JDABuilder.createDefault(
token,
GatewayIntent.GUILD_MESSAGES,
@@ -29,7 +47,15 @@ public static void main(String[] args) throws LoginException {
.setMemberCachePolicy(MemberCachePolicy.ALL)
.enableCache(CacheFlag.CLIENT_STATUS)
.disableCache(CacheFlag.VOICE_STATE, CacheFlag.EMOTE)
- .addEventListeners(Events.INSTANCE)
+ .addEventListeners(
+ new BotReadyEvent(),
+ new BotShutdownEvent(db),
+ new GuildJoinEvent(db),
+ new GuildMemberJoinEvent(db),
+ new OnGuildReadyEvent(db),
+ new SlashCommandEvent(db),
+ new ButtonClickEvent(db)
+ )
.setStatus(OnlineStatus.ONLINE)
.setActivity(Activity.playing("TicTacToe"))
.build();
diff --git a/src/main/java/com/tictactoebot/events/BotReadyEvent.java b/src/main/java/com/tictactoebot/events/BotReadyEvent.java
new file mode 100644
index 0000000..7dba20b
--- /dev/null
+++ b/src/main/java/com/tictactoebot/events/BotReadyEvent.java
@@ -0,0 +1,20 @@
+package com.tictactoebot.events;
+
+import com.tictactoebot.Database;
+import net.dv8tion.jda.api.events.ReadyEvent;
+import org.jetbrains.annotations.NotNull;
+
+public class BotReadyEvent extends Event {
+ public BotReadyEvent(Database db) {
+ super(db);
+ }
+
+ public BotReadyEvent() {
+ this(null);
+ }
+
+ @Override
+ public void onReady(@NotNull ReadyEvent event) {
+ LOGGER.info("%s is ready!".formatted(event.getJDA().getSelfUser().getName()));
+ }
+}
diff --git a/src/main/java/com/tictactoebot/events/BotShutdownEvent.java b/src/main/java/com/tictactoebot/events/BotShutdownEvent.java
new file mode 100644
index 0000000..7cad907
--- /dev/null
+++ b/src/main/java/com/tictactoebot/events/BotShutdownEvent.java
@@ -0,0 +1,17 @@
+package com.tictactoebot.events;
+
+import com.tictactoebot.Database;
+import net.dv8tion.jda.api.events.ShutdownEvent;
+import org.jetbrains.annotations.NotNull;
+
+public class BotShutdownEvent extends Event {
+ public BotShutdownEvent(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void onShutdown(@NotNull ShutdownEvent event) {
+ LOGGER.info("%s is shutting down...".formatted(event.getJDA().getSelfUser().getName()));
+ db.close();
+ }
+}
diff --git a/src/main/java/com/tictactoebot/events/ButtonClickEvent.java b/src/main/java/com/tictactoebot/events/ButtonClickEvent.java
new file mode 100644
index 0000000..9f50763
--- /dev/null
+++ b/src/main/java/com/tictactoebot/events/ButtonClickEvent.java
@@ -0,0 +1,160 @@
+package com.tictactoebot.events;
+
+import com.tictactoebot.Database;
+import com.tictactoebot.MatchData;
+import com.tictactoebot.game.Game;
+import com.tictactoebot.game.Player;
+import com.tictactoebot.game.TicTacToe;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.interactions.components.ActionRow;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+public class ButtonClickEvent extends Event {
+ public ButtonClickEvent(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void onButtonClick(@NotNull net.dv8tion.jda.api.events.interaction.ButtonClickEvent event) {
+ String buttonId = event.getComponentId();
+ final User user = event.getUser();
+ final long messageID = event.getMessage().getIdLong();
+
+ final boolean isPlayer = Database.MATCH_DATA_MAP.containsKey(messageID);
+
+ if (!isPlayer) {
+ event.getInteraction().deferEdit().queue();
+ return;
+ }
+ final MatchData matchData = Database.MATCH_DATA_MAP.get(messageID);
+ final User challenger = matchData.getChallenger();
+ final User challengeAccepter = matchData.getChallengeAccepter();
+ switch (buttonId) {
+ case "ready" -> { try {
+ if (!user.equals(challengeAccepter)) {
+ event.deferEdit().queue();
+ return;
+ }
+
+ Game game = matchData.acknowledge();
+ File boardImage = game.getBoardImage();
+
+ event.editMessage("game started")
+ .setEmbeds(game.getEmbed())
+ .addFile(boardImage)
+ .setActionRows(game.getRows()).queue();
+ } catch (Exception e) { LOGGER.error(e.getMessage(), e); } }
+ case "not-ready" -> {
+ if (!user.equals(challengeAccepter)) {
+ event.deferEdit().queue();
+ return;
+ }
+ event.editMessageFormat("%s declined the challenge!", user.getAsMention()).setActionRows(List.of()).queue();
+ Database.MATCH_DATA_MAP.remove(messageID);
+ }
+ case "8", "7", "6", "5", "4", "3", "2", "1", "0" -> { try {
+ long userID = user.getIdLong();
+ Game game = matchData.getGame();
+
+ if (!game.contains(userID)) {
+ event.deferEdit().queue();
+ return;
+ }
+ Player.Type playerType = game.getPlayerType(user);
+ TicTacToe.Move result = game.select(buttonId, playerType);
+ final String guildId = Objects.requireNonNull(event.getGuild()).getId();
+ List board = game.getRows();
+ File boardImage = game.getBoardImage();
+ MessageEmbed embed = game.getEmbed();
+ switch (result) {
+ case WIN -> event.editMessage("game ended").queue(hook -> {
+ User winner = game.getCurrentUser();
+ User loser = winner.getIdLong() == challenger.getIdLong() ? challengeAccepter : challenger;
+ hook.editOriginalComponents(List.of())
+ .setEmbeds(embed)
+ .retainFilesById(List.of())
+ .addFile(boardImage).queue();
+
+ Database.UserInfo winnerInfo = db.getUserInfo(winner.getId(), guildId);
+ Database.UserInfo loserInfo = db.getUserInfo(loser.getId(), guildId);
+ db.resetXPIfCrossesLimit(winnerInfo);
+ db.resetXPIfCrossesLimit(loserInfo);
+ db.update(winnerInfo, loserInfo);
+ /*Database.LEVELS.updateOne(
+ Filters.and(
+ Filters.eq("userId", winner.getId()),
+ Filters.eq("guildId", guildId)
+ ),
+ Filters.eq("$inc", Filters.and(Filters.eq("wins", 1), Filters.eq("game-xp", 10)))
+ );
+ Database.LEVELS.updateOne(
+ Filters.and(
+ Filters.eq("userId", loser.getId()),
+ Filters.eq("guildId", guildId)
+ ),
+ Filters.eq("$inc", Filters.and(Filters.eq("loses", 1), Filters.eq("game-xp", 1)))
+ );*/
+
+ Database.MATCH_DATA_MAP.remove(messageID);
+ });
+ case DRAW -> event.editMessage("game ended").queue(hook -> {
+ hook.editOriginalComponents(List.of())
+ .setEmbeds(embed)
+ .retainFilesById(List.of())
+ .addFile(boardImage)
+ .queue();
+ Database.UserInfo challengerInfo = db.getUserInfo(challenger.getId(), guildId);
+ Database.UserInfo challengeAccepterInfo = db.getUserInfo(challengeAccepter.getId(), guildId);
+ db.resetXPIfCrossesLimit(challengerInfo);
+ db.resetXPIfCrossesLimit(challengeAccepterInfo);
+ db.updateDraw(challengerInfo, challengeAccepterInfo);
+ /*resetXPIfCrossesLimit(challenger.getId(), guildId);
+ Database.LEVELS.updateOne(
+ Filters.and(
+ Filters.eq("userId", challenger.getId()),
+ Filters.eq("guildId", guildId)
+ ),
+ Filters.eq("$inc", Filters.and(Filters.eq("draws", 1), Filters.eq("game-xp", 5)))
+ );
+
+ db.resetXPIfCrossesLimit(challengeAccepter.getId(), guildId);
+ Database.LEVELS.updateOne(
+ Filters.and(
+ Filters.eq("userId", challengeAccepter.getId()),
+ Filters.eq("guildId", guildId)
+ ),
+ Filters.eq("$inc", Filters.and(Filters.eq("draws", 1), Filters.eq("game-xp", 5)))
+ );*/
+ Database.MATCH_DATA_MAP.remove(messageID);
+ });
+ case NONE ->
+ event.deferEdit()
+ .queue(hook ->
+ hook.editOriginalComponents(board)
+ .setEmbeds(embed)
+ .retainFilesById(List.of())
+ .addFile(boardImage).queue());
+ case INVALID -> event.deferEdit().queue();
+ }
+ } catch (IOException e) { LOGGER.error(e.getMessage(), e); } }
+ case "resign" -> {
+ Game game = matchData.getGame();
+ if (!game.contains(user.getIdLong())) {
+ event.deferEdit().queue();
+ return;
+ }
+ game.resign(user);
+ MessageEmbed embed = game.getEmbed();
+ game.disableButtons();
+ event.deferEdit().queue(hook -> hook.editOriginal("game ended").setActionRows(List.of()).setEmbeds(embed).queue());
+ Database.MATCH_DATA_MAP.remove(messageID);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/tictactoebot/events/Event.java b/src/main/java/com/tictactoebot/events/Event.java
new file mode 100644
index 0000000..d14074a
--- /dev/null
+++ b/src/main/java/com/tictactoebot/events/Event.java
@@ -0,0 +1,15 @@
+package com.tictactoebot.events;
+
+import com.tictactoebot.Database;
+import net.dv8tion.jda.api.hooks.ListenerAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class Event extends ListenerAdapter {
+ protected final Database db;
+ protected static final Logger LOGGER = LoggerFactory.getLogger(Event.class);
+
+ public Event(Database db) {
+ this.db = db;
+ }
+}
diff --git a/src/main/java/com/tictactoebot/events/GuildEvent.java b/src/main/java/com/tictactoebot/events/GuildEvent.java
new file mode 100644
index 0000000..18aa196
--- /dev/null
+++ b/src/main/java/com/tictactoebot/events/GuildEvent.java
@@ -0,0 +1,26 @@
+package com.tictactoebot.events;
+
+import com.tictactoebot.Database;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+
+public class GuildEvent extends Event {
+ public GuildEvent(Database db) {
+ super(db);
+ }
+
+ protected static void upsertCommands(Guild guild) {
+ guild
+ .upsertCommand("play", "Play a game of TicTacToe with a mentioned user")
+ .addOption(OptionType.USER, "user", "The user to play with", true)
+ .queue();
+ guild
+ .upsertCommand("leaderboard", "View the leaderboard").queue();
+ guild
+ .upsertCommand("stats", "View your current level or the mentioned user")
+ .addOption(OptionType.USER, "user", "check level of this user", false)
+ .queue();
+
+ LOGGER.info("Registered commands for %s".formatted(guild.getName()));
+ }
+}
diff --git a/src/main/java/com/tictactoebot/events/GuildJoinEvent.java b/src/main/java/com/tictactoebot/events/GuildJoinEvent.java
new file mode 100644
index 0000000..aa4f740
--- /dev/null
+++ b/src/main/java/com/tictactoebot/events/GuildJoinEvent.java
@@ -0,0 +1,23 @@
+package com.tictactoebot.events;
+
+import com.tictactoebot.Database;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.User;
+import org.jetbrains.annotations.NotNull;
+
+public class GuildJoinEvent extends GuildEvent {
+ public GuildJoinEvent(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void onGuildJoin(@NotNull net.dv8tion.jda.api.events.guild.GuildJoinEvent event) {
+ Guild guild = event.getGuild();
+ upsertCommands(guild);
+ LOGGER.info("Joined guild: " + guild.getName());
+ guild.getMembers().forEach( member -> {
+ User user = member.getUser();
+ db.insertNewUser(guild, user);
+ });
+ }
+}
diff --git a/src/main/java/com/tictactoebot/events/GuildMemberJoinEvent.java b/src/main/java/com/tictactoebot/events/GuildMemberJoinEvent.java
new file mode 100644
index 0000000..0a6505b
--- /dev/null
+++ b/src/main/java/com/tictactoebot/events/GuildMemberJoinEvent.java
@@ -0,0 +1,19 @@
+package com.tictactoebot.events;
+
+import com.tictactoebot.Database;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.User;
+import org.jetbrains.annotations.NotNull;
+
+public class GuildMemberJoinEvent extends GuildEvent {
+ public GuildMemberJoinEvent(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void onGuildMemberJoin(@NotNull net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent event) {
+ Guild guild = event.getGuild();
+ User user = event.getUser();
+ db.insertNewUser(guild, user);
+ }
+}
diff --git a/src/main/java/com/tictactoebot/events/OnGuildReadyEvent.java b/src/main/java/com/tictactoebot/events/OnGuildReadyEvent.java
new file mode 100644
index 0000000..bf6627f
--- /dev/null
+++ b/src/main/java/com/tictactoebot/events/OnGuildReadyEvent.java
@@ -0,0 +1,24 @@
+package com.tictactoebot.events;
+
+import com.tictactoebot.Database;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.events.guild.GuildReadyEvent;
+import org.jetbrains.annotations.NotNull;
+
+public class OnGuildReadyEvent extends GuildEvent {
+ public OnGuildReadyEvent(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void onGuildReady(@NotNull GuildReadyEvent event) {
+ Guild guild = event.getGuild();
+ upsertCommands(guild);
+
+ guild.getMembers().forEach( member -> {
+ User user = member.getUser();
+ db.insertNewUser(guild, user);
+ });
+ }
+}
diff --git a/src/main/java/com/tictactoebot/events/SlashCommandEvent.java b/src/main/java/com/tictactoebot/events/SlashCommandEvent.java
new file mode 100644
index 0000000..e60dde3
--- /dev/null
+++ b/src/main/java/com/tictactoebot/events/SlashCommandEvent.java
@@ -0,0 +1,161 @@
+package com.tictactoebot.events;
+
+import com.tictactoebot.Database;
+import com.tictactoebot.MatchData;
+import net.dv8tion.jda.api.Permission;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.interactions.commands.OptionMapping;
+import net.dv8tion.jda.api.interactions.components.Button;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class SlashCommandEvent extends Event {
+ public SlashCommandEvent(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void onSlashCommand(@NotNull net.dv8tion.jda.api.events.interaction.SlashCommandEvent event) {
+ List permissions = List.of(
+ Permission.USE_SLASH_COMMANDS,
+ Permission.MESSAGE_EMBED_LINKS,
+ Permission.MESSAGE_ATTACH_FILES,
+ Permission.MESSAGE_WRITE
+ );
+ if (!Objects.requireNonNull(event.getGuild()).getSelfMember().hasPermission(
+ permissions
+ )) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("```").append('\n');
+ permissions.forEach(permission -> builder.append(permission.getName()).append('\n'));
+ builder.append("```");
+ event.reply("_I do not have one of the following permissions:_\n" + builder).setEphemeral(true).queue();
+ return;
+ }
+ Guild guild = event.getGuild();
+ if (guild == null) {
+ return;
+ }
+ String commandName = event.getName();
+ User user = event.getUser();
+ switch (commandName) {
+ case "play" -> {
+ User mentionedUser = Objects.requireNonNull(event.getOption("user")).getAsUser();
+ if (mentionedUser.equals(user)) {
+ event.reply("You can't play with yourself!").setEphemeral(true).queue();
+ return;
+ }
+ if (mentionedUser.isBot()) {
+ event.reply("You can't play with a bot!").setEphemeral(true).queue();
+ return;
+ }
+ // confirm if the mentioned user is ready to play
+ event.deferReply().queue(
+ interactionHook ->
+ interactionHook.sendMessage("%s, will you accept a challenge of a game of TicTacToe from %s?".formatted(
+ mentionedUser.getAsMention(),
+ user.getAsMention()
+ )).addActionRow(
+ Button.success("ready", "Yes"),
+ Button.danger("not-ready", "No")
+ ).queue(message -> {
+ long messageID = message.getIdLong();
+ MatchData matchData = MatchData.create(user, mentionedUser);
+ matchData.setPendingEdit(message);
+ Database.MATCH_DATA_MAP.put(messageID, matchData);
+ }));
+ }
+ case "stats" -> {
+ OptionMapping option = event.getOption("user");
+ final var optionUser = new Object() {
+ User current = user;
+ };
+ if (option != null) {
+ optionUser.current = option.getAsUser();
+ if (optionUser.current.isBot()) {
+ event.reply("Bots don't participate in leveling").setEphemeral(true).queue();
+ return;
+ }
+ }
+
+ event.deferReply().queue(hook -> {
+ Database.UserInfo info = db.getUserInfo(optionUser.current.getId(), guild.getId());
+
+ int xp = info.xp();
+ double xpLimit = info.xp_limit();
+ int level = info.level();
+ double ratio = xp / xpLimit;
+ int progressLength = (int) (ratio * 10);
+ int empty = 10 - progressLength;
+ String progressBar = "%s%s %d%%".formatted("\u2588".repeat(progressLength), "_".repeat(empty), (int) (ratio * 100));
+ String statsWindow =
+ "```js\n" +
+ "// %s's stats //\n\n".formatted(optionUser.current.getAsTag()) +
+ "game level: " + level + '\n' +
+ "game xp: %d/%d\n\nprogress bar\n".formatted(xp, (int) xpLimit) +
+ progressBar + "\n\n" +
+ "wins: " + info.wins() + ", " +
+ "loses: " + info.loses() + ", " +
+ "draws: " + info.draws() + '\n' +
+ "```";
+ hook.sendMessage(statsWindow).queue();
+ });
+ }
+ case "leaderboard" -> event.deferReply().queue(hook -> {
+ final StringBuilder leaderboard = new StringBuilder();
+ AtomicInteger rank = new AtomicInteger(1);
+ leaderboard.append("```kt\nleaderboard\n\n");
+ String cell = "%6s";
+ leaderboard.append(cell.formatted("rank"))
+ .append(cell.formatted("level"))
+ .append("%13s".formatted("xp"))
+ .append(" name").append('\n');
+ db.forEach(guild.getId(), ((tag, xp, xp_limit, level) -> {
+ String xpStr = "(%d/%d)".formatted(xp, xp_limit);
+ leaderboard.append(cell.formatted(rank.getAndIncrement()))
+ .append(cell.formatted(level))
+ .append("%13s".formatted(xpStr))
+ .append(" %s".formatted(tag)).append('\n');
+ }));
+ leaderboard.append("\n```");
+ hook.sendMessage(leaderboard.toString()).queue();
+ /*try (var result = Database.LEVELS.aggregate(List.of(
+ Filters.eq("$match", Filters.eq("guildId", guild.getId())),
+ Filters.eq("$group", Filters.and(
+ Filters.eq("_id", "$_id"),
+ Filters.eq("userId", Filters.eq("$first", "$userId")),
+ Filters.eq("tag", Filters.eq("$first", "$tag")),
+ Filters.eq("game-level", Filters.eq("$first", "$game-level")),
+ Filters.eq("game-xp", Filters.eq("$first", "$game-xp")),
+ Filters.eq("game-xp-limit", Filters.eq("$first", "$game-xp-limit"))
+ )),
+ Filters.eq("$sort", Filters.and(
+ Filters.eq("game-level", -1),
+ Filters.eq("game-xp", -1),
+ Filters.eq("tag", 1)
+ ))
+ )).iterator()) {
+ result.forEachRemaining(document -> {
+ String tag = document.getString("tag");
+ int level = document.getInteger("game-level");
+ int xp = document.getInteger("game-xp");
+ int xpLimit = document.getInteger("game-xp-limit");
+ String xpStr = "(%d/%d)".formatted(xp, xpLimit);
+ leaderboard.append(cell.formatted(rank.getAndIncrement()))
+ .append(cell.formatted(level))
+ .append("%13s".formatted(xpStr))
+ .append(" %s".formatted(tag)).append('\n');
+ });
+ leaderboard.append("\n```");
+ hook.sendMessage(leaderboard.toString()).queue();
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
+ }*/
+ });
+ }
+ }
+}