Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

public class BlackjackDiscordGameSession : DiscordGameSession<Blackjack>
{
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()
Expand Down
4 changes: 2 additions & 2 deletions DiscordBot/Domain/Casino/Discord/PokerDiscordGameSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

public class PokerDiscordGameSession : DiscordGameSession<Poker>
{
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

public class RockPaperScissorsDiscordGameSession : DiscordGameSession<RockPaperScissors>
{
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()

Check warning on line 10 in DiscordBot/Domain/Casino/Discord/RockPaperScissorsDiscordGameSession.cs

View workflow job for this annotation

GitHub Actions / Build & Test

'RockPaperScissorsDiscordGameSession.GetCurrentPlayerName()' hides inherited member 'DiscordGameSession<RockPaperScissors>.GetCurrentPlayerName()'. Use the new keyword if hiding was intended.

Check warning on line 10 in DiscordBot/Domain/Casino/Discord/RockPaperScissorsDiscordGameSession.cs

View workflow job for this annotation

GitHub Actions / Build & Test

'RockPaperScissorsDiscordGameSession.GetCurrentPlayerName()' hides inherited member 'DiscordGameSession<RockPaperScissors>.GetCurrentPlayerName()'. Use the new keyword if hiding was intended.
{
if (Game.CurrentPlayer == null) return "All players have chosen";
return GetPlayerName((DiscordGamePlayer)Game.CurrentPlayer);
Expand Down
140 changes: 92 additions & 48 deletions DiscordBot/Domain/Casino/DiscordGameSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public abstract class DiscordGameSession<TGame> : GameSession<TGame>, 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;
Expand Down Expand Up @@ -93,11 +93,16 @@ private async Task<Embed> 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}")
Expand Down Expand Up @@ -127,52 +132,91 @@ private Embed GenerateAbandonedEmbed()

private MessageComponent GenerateNotStartedButtons()
{
return new ComponentBuilder()
.WithRows(new List<ActionRowBuilder>
{
// 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<ActionRowBuilder>();

// 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()
Expand Down
6 changes: 5 additions & 1 deletion DiscordBot/Domain/Casino/GameSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -42,20 +44,22 @@ public class GameSession<TGame> : 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;

if (maxSeats < game.MinPlayers || maxSeats > game.MaxPlayers)
throw new ArgumentOutOfRangeException(nameof(maxSeats), $"Max players for {game.Name} must be between {game.MinPlayers} and {game.MaxPlayers}");

MaxSeats = maxSeats;
IsPrivate = isPrivate;
}

// Convenience properties
Expand Down
79 changes: 77 additions & 2 deletions DiscordBot/Modules/Casino/CasinoSlashModule.Games.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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

Expand Down
27 changes: 21 additions & 6 deletions DiscordBot/Services/Casino/GameService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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());
Expand Down
Loading