From ed0900daabf5b3e9f897da26ce55fa5ee7b27e12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 27 Jul 2025 14:50:30 +0000 Subject: [PATCH 1/2] Initial plan From 4cc8a76b3b01535c870eb51d24401f95768d0b42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:00:33 +0000 Subject: [PATCH 2/2] Implement core private games functionality Co-authored-by: Pierre-Demessence <1756398+Pierre-Demessence@users.noreply.github.com> --- .../Discord/BlackjackDiscordGameSession.cs | 4 +- .../Casino/Discord/PokerDiscordGameSession.cs | 4 +- .../RockPaperScissorsDiscordGameSession.cs | 4 +- .../Domain/Casino/DiscordGameSession.cs | 140 ++++++++++++------ DiscordBot/Domain/Casino/GameSession.cs | 6 +- .../Modules/Casino/CasinoSlashModule.Games.cs | 79 +++++++++- DiscordBot/Services/Casino/GameService.cs | 27 +++- 7 files changed, 201 insertions(+), 63 deletions(-) diff --git a/DiscordBot/Domain/Casino/Discord/BlackjackDiscordGameSession.cs b/DiscordBot/Domain/Casino/Discord/BlackjackDiscordGameSession.cs index c302b006..eb1a2d35 100644 --- a/DiscordBot/Domain/Casino/Discord/BlackjackDiscordGameSession.cs +++ b/DiscordBot/Domain/Casino/Discord/BlackjackDiscordGameSession.cs @@ -3,8 +3,8 @@ public class BlackjackDiscordGameSession : DiscordGameSession { - public BlackjackDiscordGameSession(Blackjack game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild) - : base(game, maxSeats, client, user, guild) + public BlackjackDiscordGameSession(Blackjack game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild, bool isPrivate = false) + : base(game, maxSeats, client, user, guild, isPrivate) { } private new string GetCurrentPlayerName() diff --git a/DiscordBot/Domain/Casino/Discord/PokerDiscordGameSession.cs b/DiscordBot/Domain/Casino/Discord/PokerDiscordGameSession.cs index 29604aaf..9d4fcd4b 100644 --- a/DiscordBot/Domain/Casino/Discord/PokerDiscordGameSession.cs +++ b/DiscordBot/Domain/Casino/Discord/PokerDiscordGameSession.cs @@ -3,8 +3,8 @@ public class PokerDiscordGameSession : DiscordGameSession { - public PokerDiscordGameSession(Poker game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild) - : base(game, maxSeats, client, user, guild) + public PokerDiscordGameSession(Poker game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild, bool isPrivate = false) + : base(game, maxSeats, client, user, guild, isPrivate) { } private string GenerateGameDescription() diff --git a/DiscordBot/Domain/Casino/Discord/RockPaperScissorsDiscordGameSession.cs b/DiscordBot/Domain/Casino/Discord/RockPaperScissorsDiscordGameSession.cs index 91900739..d076d7cf 100644 --- a/DiscordBot/Domain/Casino/Discord/RockPaperScissorsDiscordGameSession.cs +++ b/DiscordBot/Domain/Casino/Discord/RockPaperScissorsDiscordGameSession.cs @@ -3,8 +3,8 @@ public class RockPaperScissorsDiscordGameSession : DiscordGameSession { - public RockPaperScissorsDiscordGameSession(RockPaperScissors game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild) - : base(game, maxSeats, client, user, guild) + public RockPaperScissorsDiscordGameSession(RockPaperScissors game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild, bool isPrivate = false) + : base(game, maxSeats, client, user, guild, isPrivate) { } private string GetCurrentPlayerName() diff --git a/DiscordBot/Domain/Casino/DiscordGameSession.cs b/DiscordBot/Domain/Casino/DiscordGameSession.cs index e16cbf6e..92f5831f 100644 --- a/DiscordBot/Domain/Casino/DiscordGameSession.cs +++ b/DiscordBot/Domain/Casino/DiscordGameSession.cs @@ -24,7 +24,7 @@ public abstract class DiscordGameSession : GameSession, IDiscordGa protected DiscordSocketClient Client { get; init; } // The context of the interaction that started the game protected SocketUser User { get; init; } // The user who started the game session - public DiscordGameSession(TGame game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild) : base(game, maxSeats) + public DiscordGameSession(TGame game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild, bool isPrivate = false) : base(game, maxSeats, isPrivate) { Client = client; User = user; @@ -93,11 +93,16 @@ private async Task GenerateNotStartedEmbed() { var challenger = await Guild.GetUserAsync(User.Id); + var title = IsPrivate ? $"🔒 {Game.Emoji} {GameName} Game Session (Private)" : $"{Game.Emoji} {GameName} Game Session"; + var description = IsPrivate + ? $"This is a private {GameName} game! Only players in the game can invite others." + : $"Welcome to {GameName}! Click the buttons below to take actions."; + return new EmbedBuilder() - .WithTitle($"{Game.Emoji} {GameName} Game Session") - .WithDescription($"Welcome to {GameName}! Click the buttons below to take actions.") + .WithTitle(title) + .WithDescription(description) .WithAuthor($"Game started by {challenger.DisplayName}") - .WithColor(Color.Green) + .WithColor(IsPrivate ? Color.Orange : Color.Green) .AddField("Players", GeneratePlayersList(), true) .AddField("Seats Available", $"{PlayerCount}/{MaxSeats}", true) .AddField("Total Pot", $"{GetTotalPot}") @@ -127,52 +132,91 @@ private Embed GenerateAbandonedEmbed() private MessageComponent GenerateNotStartedButtons() { - return new ComponentBuilder() - .WithRows(new List - { - // Buttons to join, leave, and toggle ready - new ActionRowBuilder() - .WithButton(new ButtonBuilder - { - CustomId = $"join_game:{Id}", - Emote = new Emoji("✅"), - Label = "Join Game", - Style = ButtonStyle.Success, - IsDisabled = Players.Count >= MaxSeats - }) - .WithButton(new ButtonBuilder - { - CustomId = $"leave_game:{Id}", - Emote = new Emoji("❌"), - Label = "Leave Game", - Style = ButtonStyle.Danger, - IsDisabled = Players.Count == 0 - }) - .WithButton(new ButtonBuilder - { - CustomId = $"toggle_ready:{Id}", - Emote = new Emoji("✅"), - Label = "Ready", - Style = ButtonStyle.Primary, - IsDisabled = Players.Count == 0 - }), - // Buttons for adding/removing AI players + var rows = new List(); + + // For private games, show different buttons + if (IsPrivate) + { + // Show invite button (access control handled in interaction handler) + rows.Add(new ActionRowBuilder() + .WithSelectMenu(new SelectMenuBuilder() + .WithCustomId($"invite_user:{Id}") + .WithPlaceholder("Select a user to invite...") + .WithType(ComponentType.UserSelect) + .WithMinValues(1) + .WithMaxValues(1)) + ); + + // Show leave button for players in the game + rows.Add(new ActionRowBuilder() + .WithButton(new ButtonBuilder + { + CustomId = $"leave_game:{Id}", + Emote = new Emoji("❌"), + Label = "Leave Game", + Style = ButtonStyle.Danger, + IsDisabled = Players.Count == 0 + }) + .WithButton(new ButtonBuilder + { + CustomId = $"toggle_ready:{Id}", + Emote = new Emoji("✅"), + Label = "Ready", + Style = ButtonStyle.Primary, + IsDisabled = Players.Count == 0 + }) + ); + } + else + { + // Original buttons for public games + rows.Add(new ActionRowBuilder() + .WithButton(new ButtonBuilder + { + CustomId = $"join_game:{Id}", + Emote = new Emoji("✅"), + Label = "Join Game", + Style = ButtonStyle.Success, + IsDisabled = Players.Count >= MaxSeats + }) + .WithButton(new ButtonBuilder + { + CustomId = $"leave_game:{Id}", + Emote = new Emoji("❌"), + Label = "Leave Game", + Style = ButtonStyle.Danger, + IsDisabled = Players.Count == 0 + }) + .WithButton(new ButtonBuilder + { + CustomId = $"toggle_ready:{Id}", + Emote = new Emoji("✅"), + Label = "Ready", + Style = ButtonStyle.Primary, + IsDisabled = Players.Count == 0 + }) + ); + } + #if DEBUG - new ActionRowBuilder() - .WithButton("Add AI", $"ai_add:{Id}", ButtonStyle.Success, new Emoji("🤖")) - .WithButton("Add FULL AI", $"ai_add_full:{Id}", ButtonStyle.Success, new Emoji("🤖")) - .WithButton("Remove AI", $"ai_remove:{Id}", ButtonStyle.Danger, new Emoji("❌")), + // AI buttons (for debugging) + rows.Add(new ActionRowBuilder() + .WithButton("Add AI", $"ai_add:{Id}", ButtonStyle.Success, new Emoji("🤖")) + .WithButton("Add FULL AI", $"ai_add_full:{Id}", ButtonStyle.Success, new Emoji("🤖")) + .WithButton("Remove AI", $"ai_remove:{Id}", ButtonStyle.Danger, new Emoji("❌")) + ); #endif - // Buttons for betting - new ActionRowBuilder() - .WithButton("+1", $"bet_add:{Id}:1", ButtonStyle.Secondary, new Emoji("1️⃣")) - .WithButton("+10", $"bet_add:{Id}:10", ButtonStyle.Secondary, new Emoji("🔟")) - .WithButton("+100", $"bet_add:{Id}:100", ButtonStyle.Secondary, new Emoji("💯")) - // .WithButton("Custom", $"bet_custom:{Id}", ButtonStyle.Secondary, new Emoji("✏️")) - .WithButton("All In", $"bet_allin:{Id}", ButtonStyle.Primary, new Emoji("💰")) - .WithButton("Reset to 1", $"bet_set:{Id}:1", ButtonStyle.Danger, new Emoji("🔄")) - }) - .Build(); + + // Betting buttons + rows.Add(new ActionRowBuilder() + .WithButton("+1", $"bet_add:{Id}:1", ButtonStyle.Secondary, new Emoji("1️⃣")) + .WithButton("+10", $"bet_add:{Id}:10", ButtonStyle.Secondary, new Emoji("🔟")) + .WithButton("+100", $"bet_add:{Id}:100", ButtonStyle.Secondary, new Emoji("💯")) + .WithButton("All In", $"bet_allin:{Id}", ButtonStyle.Primary, new Emoji("💰")) + .WithButton("Reset to 1", $"bet_set:{Id}:1", ButtonStyle.Danger, new Emoji("🔄")) + ); + + return new ComponentBuilder().WithRows(rows).Build(); } private MessageComponent GenerateInProgressButtons() diff --git a/DiscordBot/Domain/Casino/GameSession.cs b/DiscordBot/Domain/Casino/GameSession.cs index 3294d156..3fb51573 100644 --- a/DiscordBot/Domain/Casino/GameSession.cs +++ b/DiscordBot/Domain/Casino/GameSession.cs @@ -8,6 +8,8 @@ public interface IGameSession public DiscordGamePlayer? CurrentPlayer { get; } public string GameName { get; } public Type ActionType { get; } + public int MaxSeats { get; } + public bool IsPrivate { get; } public DiscordGamePlayer? GetPlayer(ulong userId); public bool AddPlayer(ulong userId, ulong bet); @@ -42,13 +44,14 @@ public class GameSession : IGameSession // public TimeSpan ExpiryTime { get; set; } = TimeSpan.FromMinutes(5); // public ulong UserId { get; set; } // The user who started the game public int MaxSeats { get; init; } // Max player cannot exceed the game's MaxPlayers and should be at least the game's MinPlayers + public bool IsPrivate { get; init; } // Whether this is a private game (invitation only) // Game instance - strongly typed protected TGame Game { get; init; } public GameState State => Game.State; // Constructor - public GameSession(TGame game, int maxSeats) + public GameSession(TGame game, int maxSeats, bool isPrivate = false) { Game = game; @@ -56,6 +59,7 @@ public GameSession(TGame game, int maxSeats) throw new ArgumentOutOfRangeException(nameof(maxSeats), $"Max players for {game.Name} must be between {game.MinPlayers} and {game.MaxPlayers}"); MaxSeats = maxSeats; + IsPrivate = isPrivate; } // Convenience properties diff --git a/DiscordBot/Modules/Casino/CasinoSlashModule.Games.cs b/DiscordBot/Modules/Casino/CasinoSlashModule.Games.cs index a01687e0..2bd5253d 100644 --- a/DiscordBot/Modules/Casino/CasinoSlashModule.Games.cs +++ b/DiscordBot/Modules/Casino/CasinoSlashModule.Games.cs @@ -108,13 +108,15 @@ public async Task Rules(CasinoGame game) public async Task CreateGameSession(CasinoGame game, [Summary("seats", "Number of seats for the game (minimum 1)")] [MinValue(1)] - int seats = 0) + int seats = 0, + [Summary("private", "Make this a private game (invitation only)")] + bool isPrivate = false) { try { await DeferAsync(); - var gameSession = GameService.CreateGameSession(game, seats, Context.Client, Context.User, Context.Guild); + var gameSession = GameService.CreateGameSession(game, seats, Context.Client, Context.User, Context.Guild, isPrivate); gameSession.AddPlayer(Context.User.Id, 1); // Add the command user as a player with a bet of 1 await GenerateResponse(gameSession); @@ -296,6 +298,79 @@ public async Task RemoveAIPlayer(string id) await GenerateResponse(gameSession); } + [ComponentInteraction("invite_user:*", true)] + public async Task InviteUser(string id, string[] userIds) + { + await DeferAsync(); + + var gameSession = GameService.GetActiveSession(id); + if (gameSession == null) + { + await FollowupAsync("Game session not found.", ephemeral: true); + return; + } + + // Check if this is a private game + if (!gameSession.IsPrivate) + { + await FollowupAsync("This feature is only available for private games.", ephemeral: true); + return; + } + + // Check if the user invoking this is in the game + var invoker = gameSession.GetPlayer(Context.User.Id); + if (invoker == null) + { + await FollowupAsync("Only players in the game can invite others.", ephemeral: true); + return; + } + + // Get the first selected user ID + if (userIds.Length == 0) + { + await FollowupAsync("No user was selected.", ephemeral: true); + return; + } + + if (!ulong.TryParse(userIds[0], out var targetUserId)) + { + await FollowupAsync("Invalid user selection.", ephemeral: true); + return; + } + + // Check if user is already in the game + if (gameSession.GetPlayer(targetUserId) != null) + { + await FollowupAsync("This user is already in the game.", ephemeral: true); + return; + } + + // Check if game is full + if (gameSession.Players.Count >= gameSession.MaxSeats) + { + await FollowupAsync("The game is already full.", ephemeral: true); + return; + } + + try + { + // Invite the user (add them but not ready) + await GameService.InviteToGame(gameSession, targetUserId); + + var invitedUser = await ((IGuild)Context.Guild).GetUserAsync(targetUserId); + await FollowupAsync($"{invitedUser?.Mention ?? "User"} has been invited to the game and added to the player list.", ephemeral: false); + + await GenerateResponse(gameSession); + await Task.Delay(500); + await ContinueGame(gameSession); + } + catch (InvalidOperationException ex) + { + await FollowupAsync(ex.Message, ephemeral: true); + return; + } + } + #endregion #region Betting Actions diff --git a/DiscordBot/Services/Casino/GameService.cs b/DiscordBot/Services/Casino/GameService.cs index a794ef0c..9fa51ae4 100644 --- a/DiscordBot/Services/Casino/GameService.cs +++ b/DiscordBot/Services/Casino/GameService.cs @@ -30,21 +30,21 @@ public ICasinoGame GetGameInstance(CasinoGame game) }; } - private IDiscordGameSession CreateDiscordGameSession(CasinoGame game, ICasinoGame gameInstance, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild) + private IDiscordGameSession CreateDiscordGameSession(CasinoGame game, ICasinoGame gameInstance, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild, bool isPrivate = false) { return game switch { - CasinoGame.Blackjack => new BlackjackDiscordGameSession((Blackjack)gameInstance, maxSeats, client, user, guild), - CasinoGame.RockPaperScissors => new RockPaperScissorsDiscordGameSession((RockPaperScissors)gameInstance, maxSeats, client, user, guild), - CasinoGame.Poker => new PokerDiscordGameSession((Poker)gameInstance, maxSeats, client, user, guild), + CasinoGame.Blackjack => new BlackjackDiscordGameSession((Blackjack)gameInstance, maxSeats, client, user, guild, isPrivate), + CasinoGame.RockPaperScissors => new RockPaperScissorsDiscordGameSession((RockPaperScissors)gameInstance, maxSeats, client, user, guild, isPrivate), + CasinoGame.Poker => new PokerDiscordGameSession((Poker)gameInstance, maxSeats, client, user, guild, isPrivate), _ => throw new ArgumentOutOfRangeException(nameof(game), $"Unknown game session type: {game}") }; } - public IDiscordGameSession CreateGameSession(CasinoGame game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild) + public IDiscordGameSession CreateGameSession(CasinoGame game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild, bool isPrivate = false) { var gameInstance = GetGameInstance(game); - var session = CreateDiscordGameSession(game, gameInstance, maxSeats == 0 ? gameInstance.MaxPlayers : maxSeats, client, user, guild); + var session = CreateDiscordGameSession(game, gameInstance, maxSeats == 0 ? gameInstance.MaxPlayers : maxSeats, client, user, guild, isPrivate); _activeSessions.Add(session); return session; } @@ -73,6 +73,21 @@ public async Task JoinGame(IDiscordGameSession session, ulong userId) session.AddPlayer(userId, 1); } + public async Task InviteToGame(IDiscordGameSession session, ulong userId) + { + if (!session.IsPrivate) throw new InvalidOperationException("Invitations are only available for private games."); + + var user = await _casinoService.GetOrCreateCasinoUser(userId.ToString()); + if (user.Tokens < 1) throw new InvalidOperationException("This user must have at least 1 token to join the game."); + + // Add player but set them as not ready (they need to ready up manually) + var added = session.AddPlayer(userId, 1); + if (!added) throw new InvalidOperationException("Could not invite this user to the game."); + + // Set the player as not ready since they were invited + session.SetPlayerReady(userId, false); + } + public async Task SetBet(IDiscordGameSession session, ulong userId, ulong bet) { var user = await _casinoService.GetOrCreateCasinoUser(userId.ToString());