From 4eb4569f1ca213e31737d5fd76156ee1828ff740 Mon Sep 17 00:00:00 2001 From: Chew Date: Sat, 4 Nov 2023 20:11:49 -0500 Subject: [PATCH 1/6] initial betting command --- .../chew/chewbotcca/util/DatabaseHelper.java | 4 + src/main/java/pw/chew/mlb/MLBBot.java | 3 +- .../pw/chew/mlb/commands/BettingCommand.java | 109 ++++++++++++++++++ src/main/java/pw/chew/mlb/models/Bet.kt | 31 +++++ src/main/java/pw/chew/mlb/models/Profile.kt | 17 +++ 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/main/java/pw/chew/mlb/commands/BettingCommand.java create mode 100644 src/main/java/pw/chew/mlb/models/Bet.kt create mode 100644 src/main/java/pw/chew/mlb/models/Profile.kt diff --git a/src/main/java/pw/chew/chewbotcca/util/DatabaseHelper.java b/src/main/java/pw/chew/chewbotcca/util/DatabaseHelper.java index 3f6c1bb..6f43ba5 100644 --- a/src/main/java/pw/chew/chewbotcca/util/DatabaseHelper.java +++ b/src/main/java/pw/chew/chewbotcca/util/DatabaseHelper.java @@ -20,7 +20,9 @@ import org.hibernate.boot.MetadataSources; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import pw.chew.mlb.models.Bet; import pw.chew.mlb.models.Channel; +import pw.chew.mlb.models.Profile; import java.io.File; @@ -36,6 +38,8 @@ public static void openConnection() { sessionFactory = new MetadataSources(registry) // MLB Bot - Change to channel .addAnnotatedClass(Channel.class) + .addAnnotatedClass(Profile.class) + .addAnnotatedClass(Bet.class) .buildMetadata() .buildSessionFactory(); } catch (Exception e) { diff --git a/src/main/java/pw/chew/mlb/MLBBot.java b/src/main/java/pw/chew/mlb/MLBBot.java index fe9599a..f2aa0d2 100644 --- a/src/main/java/pw/chew/mlb/MLBBot.java +++ b/src/main/java/pw/chew/mlb/MLBBot.java @@ -13,6 +13,7 @@ import pw.chew.chewbotcca.util.DatabaseHelper; import pw.chew.chewbotcca.util.RestClient; import pw.chew.mlb.commands.AdminCommand; +import pw.chew.mlb.commands.BettingCommand; import pw.chew.mlb.commands.ConfigCommand; import pw.chew.mlb.commands.PlanGameCommand; import pw.chew.mlb.commands.ScoreCommand; @@ -58,7 +59,7 @@ public static void main(String[] args) throws IOException { client.addCommands(new ShutdownCommand(), new AdminCommand()); client.addSlashCommands(new StartGameCommand(), new StopGameCommand(), new ScoreCommand(), new SetInfoCommand(), new ConfigCommand(), - new PlanGameCommand()); + new PlanGameCommand(), new BettingCommand()); //client.forceGuildOnly("148195924567392257"); diff --git a/src/main/java/pw/chew/mlb/commands/BettingCommand.java b/src/main/java/pw/chew/mlb/commands/BettingCommand.java new file mode 100644 index 0000000..e349b4f --- /dev/null +++ b/src/main/java/pw/chew/mlb/commands/BettingCommand.java @@ -0,0 +1,109 @@ +package pw.chew.mlb.commands; + +import com.jagrosh.jdautilities.command.SlashCommand; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.utils.TimeFormat; +import org.hibernate.Transaction; +import pw.chew.chewbotcca.util.DatabaseHelper; +import pw.chew.mlb.models.Bet; +import pw.chew.mlb.models.Profile; + +import java.util.ArrayList; +import java.util.List; + +public class BettingCommand extends SlashCommand { + public BettingCommand() { + this.name = "betting"; + this.help = "Bet on a team"; + this.children = new SlashCommand[]{ + new BettingProfileSubCommand() + }; + } + + @Override + protected void execute(SlashCommandEvent event) { + // unused for children + } + + public static class BettingProfileSubCommand extends SlashCommand { + public BettingProfileSubCommand() { + this.name = "profile"; + this.help = "View your betting profile"; + } + + @Override + protected void execute(SlashCommandEvent event) { + // try to get profile + Profile profile = retrieveProfile(event.getUser().getIdLong()); + List bets = retrieveBets(event.getUser().getIdLong()); + + // build embed + event.replyEmbeds(buildProfileEmbed(event, profile, bets)).queue(); + } + + private MessageEmbed buildProfileEmbed(SlashCommandEvent event, Profile profile, List bets) { + EmbedBuilder embed = new EmbedBuilder(); + embed.setTitle("Betting Profile for " + event.getUser().getName()); + embed.addField("Credits", profile.getCredits() + "", true); + + if (event.getMember() != null) { + embed.setColor(event.getMember().getColor()); + } + + // get 5 most recent bets + List betString = new ArrayList<>(); + for (int i = 0; i < 5 && i < bets.size(); i++) { + Bet bet = bets.get(i); + betString.add("%s | %s%s - %s".formatted( + TimeFormat.DATE_SHORT.format(bet.getCreatedAt()), + bet.getSuccessful() ? "+" : "-", + bet.getAmount(), + bet.getReason() + )); + } + + embed.addField("Recent Bets", String.join("\n", betString), false); + + return embed.build(); + } + } + + public static Profile retrieveProfile(long userId) { + var session = DatabaseHelper.getSessionFactory().openSession(); + Profile profile = session.find(Profile.class, userId); + if (profile == null) { + Transaction trans = session.beginTransaction(); + profile = new Profile(); + profile.setId(userId); + session.save(profile); + + // add initial betting credits + Bet bet = new Bet(); + bet.setAmount(100); + bet.setAutomated(true); + bet.setSuccessful(true); + bet.setReason("Initial betting credits"); + bet.setUserId(userId); + session.save(bet); + + trans.commit(); + } + session.close(); + return profile; + } + + public static List retrieveBets(long userId) { + var session = DatabaseHelper.getSessionFactory().openSession(); + + // get Bets where user_id == userId + List bets = session.createQuery("from Bet where userId = :userId order by createdAt desc", Bet.class) + .setParameter("userId", userId) + .getResultList(); + + session.close(); + + return bets; + } +} diff --git a/src/main/java/pw/chew/mlb/models/Bet.kt b/src/main/java/pw/chew/mlb/models/Bet.kt new file mode 100644 index 0000000..4744f4b --- /dev/null +++ b/src/main/java/pw/chew/mlb/models/Bet.kt @@ -0,0 +1,31 @@ +package pw.chew.mlb.models + +import java.time.Instant +import javax.persistence.* + +@Entity +@Table(name = "bets") +open class Bet { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + open var id: Int? = null + + @Column(name = "user_id", nullable = false) + open var userId: Long? = null + + @Column(name = "amount", nullable = false) + open var amount: Int = 0 + + @Column(name = "successful", nullable = false) + open var successful: Boolean = false + + @Column(name = "automated", nullable = false) + open var automated: Boolean = false + + @Column(name = "reason", nullable = false, length = 128) + open var reason: String = "" + + @Column(name = "created_at", nullable = false) + open var createdAt: Instant = Instant.now() +} diff --git a/src/main/java/pw/chew/mlb/models/Profile.kt b/src/main/java/pw/chew/mlb/models/Profile.kt new file mode 100644 index 0000000..e4fdafd --- /dev/null +++ b/src/main/java/pw/chew/mlb/models/Profile.kt @@ -0,0 +1,17 @@ +package pw.chew.mlb.models + +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Table + +@Entity +@Table(name = "profiles") +open class Profile { + @Id + @Column(name = "id", nullable = false) + open var id: Long? = null + + @Column(name = "credits", nullable = false) + open var credits: Int = 100 +} From 7653adbe5a8d8a59a30a89234e535af2331d2f85 Mon Sep 17 00:00:00 2001 From: Chew Date: Sat, 4 Nov 2023 23:42:16 -0500 Subject: [PATCH 2/6] support betting on games --- .../pw/chew/mlb/commands/BettingCommand.java | 137 +++++++++++++++++- .../pw/chew/mlb/commands/PlanGameCommand.java | 26 ++-- src/main/java/pw/chew/mlb/models/Bet.kt | 30 +++- .../java/pw/chew/mlb/objects/GameState.java | 2 +- 4 files changed, 171 insertions(+), 24 deletions(-) diff --git a/src/main/java/pw/chew/mlb/commands/BettingCommand.java b/src/main/java/pw/chew/mlb/commands/BettingCommand.java index e349b4f..c8e9ce8 100644 --- a/src/main/java/pw/chew/mlb/commands/BettingCommand.java +++ b/src/main/java/pw/chew/mlb/commands/BettingCommand.java @@ -4,11 +4,17 @@ import com.jagrosh.jdautilities.command.SlashCommandEvent; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.utils.TimeFormat; import org.hibernate.Transaction; import pw.chew.chewbotcca.util.DatabaseHelper; import pw.chew.mlb.models.Bet; +import pw.chew.mlb.models.BetKind; import pw.chew.mlb.models.Profile; +import pw.chew.mlb.objects.GameBlurb; +import pw.chew.mlb.objects.GameState; import java.util.ArrayList; import java.util.List; @@ -18,7 +24,8 @@ public BettingCommand() { this.name = "betting"; this.help = "Bet on a team"; this.children = new SlashCommand[]{ - new BettingProfileSubCommand() + new BettingProfileSubCommand(), + new BettingBetSubCommand() }; } @@ -40,7 +47,7 @@ protected void execute(SlashCommandEvent event) { List bets = retrieveBets(event.getUser().getIdLong()); // build embed - event.replyEmbeds(buildProfileEmbed(event, profile, bets)).queue(); + event.replyEmbeds(buildProfileEmbed(event, profile, bets)).setEphemeral(true).queue(); } private MessageEmbed buildProfileEmbed(SlashCommandEvent event, Profile profile, List bets) { @@ -58,8 +65,8 @@ private MessageEmbed buildProfileEmbed(SlashCommandEvent event, Profile profile, Bet bet = bets.get(i); betString.add("%s | %s%s - %s".formatted( TimeFormat.DATE_SHORT.format(bet.getCreatedAt()), - bet.getSuccessful() ? "+" : "-", - bet.getAmount(), + bet.amount() > 0 ? "+" : "", + bet.amount(), bet.getReason() )); } @@ -70,6 +77,122 @@ private MessageEmbed buildProfileEmbed(SlashCommandEvent event, Profile profile, } } + public static class BettingBetSubCommand extends SlashCommand { + public BettingBetSubCommand() { + this.name = "bet"; + this.help = "Bet on a team"; + this.options = List.of( + new OptionData(OptionType.INTEGER, "team", "Which team to bet on", true, true), + new OptionData(OptionType.INTEGER, "game", "Which game to bet on", true, true), + new OptionData(OptionType.INTEGER, "amount", "How much to bet. 0 to remove bet.", true, false) + .setMinValue(0) + ); + } + + @Override + protected void execute(SlashCommandEvent event) { + // options + int teamId = (int) event.optLong("team", 0); + int gamePk = (int) event.optLong("game", 0); + int amount = (int) event.optLong("amount", 0); + + // Check the game status + GameState state = GameState.fromPk(String.valueOf(gamePk)); + if (state.isFinal()) { + event.reply("This game is already over!").setEphemeral(true).queue(); + return; + } + if (state.inning() > 4) { + event.reply("Bets cannot be placed or changed past the 4th inning! To see your bet, run /betting placed").setEphemeral(true).queue(); + return; + } + + // start session + var session = DatabaseHelper.getSessionFactory().openSession(); + + // check for a bet, check by user_id, game_pk, and team_id + Bet bet = session.createQuery("from Bet where userId = :userId and gamePk = :gamePk and teamId = :teamId", Bet.class) + .setParameter("userId", event.getUser().getIdLong()) + .setParameter("gamePk", gamePk) + .setParameter("teamId", teamId) + .uniqueResult(); + + // Ensure we have enough credit to bet + Profile user = retrieveProfile(event.getUser().getIdLong()); + + // Get game blurb + GameBlurb blurb = new GameBlurb(gamePk + ""); + + // if bet exists, update it + if (bet == null) { + if (amount == 0) { + event.reply("You must bet at least 1 credit!").setEphemeral(true).queue(); + return; + } + + if (user.getCredits() < amount) { + event.reply("You don't have enough credits to bet that much! You only have " + user.getCredits() + " credits!").setEphemeral(true).queue(); + return; + } + + String team = blurb.away().id() == teamId ? blurb.away().name() : blurb.home().name(); + + Bet newBet = new Bet(); + newBet.setUserId(event.getUser().getIdLong()); + newBet.setGamePk(gamePk); + newBet.setTeamId(teamId); + newBet.setBet(amount); + newBet.setKind(BetKind.PENDING); + newBet.setReason("Bet on " + team + " for " + blurb.name()); + + user.setCredits(user.getCredits() - amount); + + Transaction trans = session.beginTransaction(); + session.save(newBet); + session.update(user); + trans.commit(); + + event.reply("Bet placed!").setEphemeral(true).queue(); + } else if (amount == 0) { + int currentBet = bet.getBet(); + + Transaction trans = session.beginTransaction(); + session.delete(bet); + + user.setCredits(user.getCredits() + currentBet); + session.update(user); + + trans.commit(); + + event.reply("Bet removed!").setEphemeral(true).queue(); + } else { + int currentBet = bet.getBet(); + int creditsToDeduct = amount - currentBet; + if (amount > currentBet && creditsToDeduct > user.getCredits()) { + event.reply("You don't have enough credits to bet that much! You only have " + user.getCredits() + " credits!").queue(); + return; + } + + user.setCredits(user.getCredits() - creditsToDeduct); + bet.setBet(amount); + + Transaction trans = session.beginTransaction(); + session.update(bet); + session.update(user); + trans.commit(); + + event.reply("Bet updated!").setEphemeral(true).queue(); + } + + session.close(); + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event) { + event.replyChoices(PlanGameCommand.handleAutoComplete(event)).queue(); + } + } + public static Profile retrieveProfile(long userId) { var session = DatabaseHelper.getSessionFactory().openSession(); Profile profile = session.find(Profile.class, userId); @@ -81,9 +204,9 @@ public static Profile retrieveProfile(long userId) { // add initial betting credits Bet bet = new Bet(); - bet.setAmount(100); - bet.setAutomated(true); - bet.setSuccessful(true); + bet.setKind(BetKind.AUTOMATED); + bet.setBet(0); + bet.setPayout(100); bet.setReason("Initial betting credits"); bet.setUserId(userId); session.save(bet); diff --git a/src/main/java/pw/chew/mlb/commands/PlanGameCommand.java b/src/main/java/pw/chew/mlb/commands/PlanGameCommand.java index 6f0e2e4..fd0260c 100644 --- a/src/main/java/pw/chew/mlb/commands/PlanGameCommand.java +++ b/src/main/java/pw/chew/mlb/commands/PlanGameCommand.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -171,6 +172,16 @@ public void handle(InteractionHook event, String gamePk, GuildChannelUnion chann @Override public void onAutoComplete(CommandAutoCompleteInteractionEvent event) { + event.replyChoices(handleAutoComplete(event)).queue(); + } + + /** + * Handles auto complete for: teams (MLB), sports, and dates (for the team) + * + * @param event the event + * @return a list of choices, maybe empty, maybe 1 choice, maybe 25 choices, who knows. + */ + public static List handleAutoComplete(CommandAutoCompleteInteractionEvent event) { switch (event.getFocusedOption().getName()) { case "team" -> { // get current value of sport @@ -187,20 +198,17 @@ public void onAutoComplete(CommandAutoCompleteInteractionEvent event) { // Ensure no duplicates and no more than 25 choices choices = choices.stream().distinct().limit(25).toList(); - event.replyChoices(choices).queue(); - return; + return choices; } case "sport" -> { - event.replyChoices(MLBAPIUtil.getSports().asChoices()).queue(); - return; + return MLBAPIUtil.getSports().asChoices(); } case "date" -> { int teamId = event.getOption("team", -1, OptionMapping::getAsInt); String sport = event.getOption("sport", "1", OptionMapping::getAsString); if (teamId == -1) { - event.replyChoices(new Command.Choice("Please select a team first!", -1)).queue(); - return; + return Collections.singletonList(new Command.Choice("Please select a team first!", -1)); } JSONArray games = new JSONObject(RestClient.get("https://statsapi.mlb.com/api/v1/schedule?lang=en&sportId=%S&season=2023&teamId=%S&fields=dates,date,games,gamePk,teams,away,team,teamName,id&hydrate=team".formatted(sport, teamId))) @@ -245,13 +253,11 @@ public void onAutoComplete(CommandAutoCompleteInteractionEvent event) { // Ensure no more than 25 choices, and no duplicates choices = choices.stream().distinct().limit(25).toList(); - event.replyChoices(choices).queue(); - - return; + return choices; } } - event.replyChoices().queue(); + return Collections.emptyList(); } public static List