From 601ab2bf40749ef85dd70500ced74cbf960df3fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:53:27 +0000 Subject: [PATCH 1/2] Initial plan From d1e37cc528afeb8faa006f76bd1389d17fba124f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:07:34 +0000 Subject: [PATCH 2/2] Implement core Russian Roulette game with both simulation systems Co-authored-by: Pierre-Demessence <1756398+Pierre-Demessence@users.noreply.github.com> --- .../RussianRouletteDiscordGameSession.cs | 160 ++++++++ .../Games/RussianRoulette/RussianRoulette.cs | 344 ++++++++++++++++++ .../Modules/Casino/CasinoSlashModule.Games.cs | 2 + DiscordBot/Services/Casino/GameService.cs | 2 + 4 files changed, 508 insertions(+) create mode 100644 DiscordBot/Domain/Casino/Discord/RussianRouletteDiscordGameSession.cs create mode 100644 DiscordBot/Domain/Casino/Games/RussianRoulette/RussianRoulette.cs diff --git a/DiscordBot/Domain/Casino/Discord/RussianRouletteDiscordGameSession.cs b/DiscordBot/Domain/Casino/Discord/RussianRouletteDiscordGameSession.cs new file mode 100644 index 00000000..fdaa3c47 --- /dev/null +++ b/DiscordBot/Domain/Casino/Discord/RussianRouletteDiscordGameSession.cs @@ -0,0 +1,160 @@ +using Discord.WebSocket; +using DiscordBot.Domain; + +public class RussianRouletteDiscordGameSession : DiscordGameSession +{ + public RussianRouletteDiscordGameSession(RussianRoulette game, int maxSeats, DiscordSocketClient client, SocketUser user, IGuild guild) + : base(game, maxSeats, client, user, guild) + { } + + protected override Embed GenerateInProgressEmbed() + { + var embed = new EmbedBuilder() + .WithTitle($"{Game.Emoji} Russian Roulette") + .WithDescription(GenerateGameDescription()) + .WithColor(Game.State == GameState.Finished + ? (Players.Count > 0 && Game.GameData[Players[0]].WonGame ? Color.Green : Color.Red) + : Color.Orange); + + if (Game.State == GameState.Finished) + { + var player = Players[0]; + var data = Game.GameData[player]; + + if (data.WonGame) + { + embed.WithFooter("šŸŽ‰ Victory! You beat the house!"); + } + else + { + embed.WithFooter("šŸ’€ The house always wins... eventually."); + } + } + else if (Players.Count > 0 && Game.GameData[Players[0]].HasSelectedSystem) + { + embed.WithFooter("āš ļø Remember: You're playing against the house. The odds are not in your favor."); + } + + return embed.Build(); + } + + private string GenerateGameDescription() + { + if (Players.Count == 0) + return "**Waiting for a player to join...**"; + + var player = Players[0]; // Only one player in Russian Roulette + var data = Game.GameData[player]; + + var description = $"**Player:** {GetPlayerName(player)}\n"; + description += $"**Bet:** {player.Bet} tokens\n\n"; + + if (!data.HasSelectedSystem) + { + description += "šŸŽ² **Choose your game system:**\n\n"; + description += "**System 1 - Fixed Risk**\n"; + description += "• 1/6 chance of bullet each turn\n"; + description += "• Payouts: 1x → 1.1x → 1.4x → 1.9x → 2.9x → 5.9x\n\n"; + description += "**System 2 - Escalating Risk**\n"; + description += "• Turn 1: 1/6, Turn 2: 2/6, Turn 3: 3/6, etc.\n"; + description += "• Payouts: 1x → 1.1x → 1.65x → 3.4x → 10.4x → 63x\n\n"; + description += "*Choose wisely - the risk and reward profiles are very different!*"; + } + else + { + var systemName = data.SelectedSystem == RussianRouletteSystem.System1 ? "System 1 (Fixed Risk)" : "System 2 (Escalating Risk)"; + description += $"**System:** {systemName}\n"; + description += $"**Turn:** {data.CurrentTurn + 1}/6\n"; + description += $"**Bullets Survived:** {data.BulletsSurvived}\n\n"; + + if (data.GameEnded) + { + if (data.WonGame) + { + description += "šŸŽ‰ **CONGRATULATIONS!**\n"; + if (data.BulletsSurvived == 6) + { + description += "You survived all 6 chambers! The house bows to your courage!\n"; + } + else + { + description += "You wisely cashed out while ahead!\n"; + } + description += $"**Bullets Survived:** {data.BulletsSurvived}\n"; + description += $"**Final Payout:** {Game.GetCurrentPayoutMultiplier(player):F1}x ({(long)(player.Bet * Game.GetCurrentPayoutMultiplier(player))} tokens)\n"; + } + else + { + description += "šŸ’€ **GAME OVER**\n"; + description += "You pulled the trigger and got the bullet!\n"; + description += $"**Bullets Survived:** {data.BulletsSurvived}\n"; + description += $"**Tokens Lost:** {player.Bet}\n"; + } + } + else + { + // Show current risk level + if (data.SelectedSystem == RussianRouletteSystem.System1) + { + description += $"**Current Risk:** 1/6 chance of bullet\n"; + } + else + { + var bullets = data.CurrentTurn + 1; + description += $"**Current Risk:** {bullets}/6 chance of bullet\n"; + } + + description += $"**Current Payout:** {Game.GetCurrentPayoutMultiplier(player):F1}x ({(long)(player.Bet * Game.GetCurrentPayoutMultiplier(player))} tokens)\n"; + + if (data.BulletsSurvived < 5) + { + description += $"**Next Payout:** {Game.GetNextPayoutMultiplier(player):F1}x ({(long)(player.Bet * Game.GetNextPayoutMultiplier(player))} tokens)\n"; + } + + description += "\nšŸ”« Pull the trigger and risk it all...\n"; + if (data.BulletsSurvived > 0) + { + description += "šŸ’° Or cash out and take your winnings!\n"; + } + } + } + + return description; + } + + public override Embed GenerateRules() + { + var embed = new EmbedBuilder() + .WithTitle($"{Game.Emoji} Russian Roulette Rules") + .WithDescription("Survive as many chambers as possible by avoiding the bullet!") + .WithColor(Color.Orange) + .AddField("šŸŽÆ Objective", "Survive as many chambers as possible by avoiding the bullet!", false) + .AddField("šŸŽ² System 1 - Fixed Risk", + "• Each turn has exactly 1/6 chance of bullet\n" + + "• Consistent risk, moderate rewards\n" + + "• Payouts: 1x → 1.1x → 1.4x → 1.9x → 2.9x → 5.9x", false) + .AddField("⚔ System 2 - Escalating Risk", + "• Turn 1: 1/6 chance, Turn 2: 2/6 chance, etc.\n" + + "• Risk increases each turn, massive rewards\n" + + "• Payouts: 1x → 1.1x → 1.65x → 3.4x → 10.4x → 63x", false) + .AddField("šŸŽ® How to Play", + "1. **Join** the game and place your bet\n" + + "2. **Choose** your preferred system (1 or 2)\n" + + "3. Each turn, choose to:\n" + + " • **Pull Trigger** - Risk everything for higher payout\n" + + " • **Cash Out** - Take current winnings (after turn 1)\n" + + "4. **Survive** 6 chambers to automatically cash out with maximum payout", false) + .AddField("šŸ† Winning", + "• **Cash out** to secure your current payout multiplier\n" + + "• **Survive all 6** chambers for automatic maximum payout\n" + + "• **Hit the bullet** and lose your entire bet", false) + .AddField("šŸ’” Strategy Tips", + "• System 1: More consistent, good for steady gains\n" + + "• System 2: High risk/high reward, massive payouts possible\n" + + "• Consider cashing out early vs. going for maximum payout\n" + + "• The house edge is built into the payout multipliers", false) + .WithFooter("Remember: You're playing against the house - know when to quit!"); + + return embed.Build(); + } +} \ No newline at end of file diff --git a/DiscordBot/Domain/Casino/Games/RussianRoulette/RussianRoulette.cs b/DiscordBot/Domain/Casino/Games/RussianRoulette/RussianRoulette.cs new file mode 100644 index 00000000..49af1797 --- /dev/null +++ b/DiscordBot/Domain/Casino/Games/RussianRoulette/RussianRoulette.cs @@ -0,0 +1,344 @@ +namespace DiscordBot.Domain; + +/// +/// Represents the different actions a player can take in Russian Roulette +/// +public enum RussianRoulettePlayerAction +{ + [ButtonMetadata(Label = "System 1 (Fixed Risk)", Style = ButtonStyle.Secondary)] + SelectSystem1, + [ButtonMetadata(Label = "System 2 (Escalating Risk)", Style = ButtonStyle.Secondary)] + SelectSystem2, + [ButtonMetadata(Emoji = "šŸ”«", Label = "Pull Trigger", Style = ButtonStyle.Danger)] + PullTrigger, + [ButtonMetadata(Emoji = "šŸ’°", Label = "Cash Out", Style = ButtonStyle.Success)] + CashOut +} + +/// +/// Simulation system types for Russian Roulette +/// +public enum RussianRouletteSystem +{ + None, + System1, // Fixed 1/6 chance each turn + System2 // Escalating bullets (1/6, 2/6, 3/6, etc.) +} + +public class RussianRoulettePlayerData : ICasinoGamePlayerData +{ + public RussianRouletteSystem SelectedSystem { get; set; } = RussianRouletteSystem.None; + public int CurrentTurn { get; set; } = 0; + public int BulletsSurvived { get; set; } = 0; + public bool HasMadeAction { get; set; } = false; + public bool GameEnded { get; set; } = false; + public bool WonGame { get; set; } = false; + + // For System 2: chamber simulation + public List Chamber { get; set; } = new(); + + public bool HasSelectedSystem => SelectedSystem != RussianRouletteSystem.None; +} + +public class RussianRoulette : ACasinoGame +{ + private static readonly Random _random = new(); + + public override string Emoji => "šŸ”«"; + public override string Name => "Russian Roulette"; + public override int MinPlayers => 1; + public override int MaxPlayers => 1; // Single player vs house only + public override bool HasPrivateHands => false; + + public override GamePlayer? CurrentPlayer => Players.FirstOrDefault(p => !GameData[p].GameEnded); + + #region Payout Multipliers + + private static readonly Dictionary PayoutMultipliers = new() + { + { + RussianRouletteSystem.System1, + new double[] { 1.0, 1.1, 1.4, 1.9, 2.9, 5.9 } + }, + { + RussianRouletteSystem.System2, + new double[] { 1.0, 1.1, 1.65, 3.4, 10.4, 63.0 } + } + }; + + #endregion + + #region Game Lifecycle + + protected override RussianRoulettePlayerData CreatePlayerData(GamePlayer player) => new(); + + protected override void InitializeGame() + { + State = GameState.InProgress; + + // Reset all player data + foreach (var player in Players) + { + var data = GameData[player]; + data.SelectedSystem = RussianRouletteSystem.None; + data.CurrentTurn = 0; + data.BulletsSurvived = 0; + data.HasMadeAction = false; + data.GameEnded = false; + data.WonGame = false; + data.Chamber.Clear(); + } + } + + public override string ShowHand(GamePlayer player) + { + var data = GameData[player]; + if (!data.HasSelectedSystem) + { + return "Select your preferred game system to begin."; + } + + return $"Turn {data.CurrentTurn + 1}/6 | Bullets Survived: {data.BulletsSurvived} | Current Payout: {GetCurrentPayoutMultiplier(player):F1}x"; + } + + #endregion + + #region Game Logic + + public override void DoPlayerAction(GamePlayer player, RussianRoulettePlayerAction action) + { + if (State != GameState.InProgress) + throw new InvalidOperationException("Game is not in progress"); + + var data = GameData[player]; + + switch (action) + { + case RussianRoulettePlayerAction.SelectSystem1: + if (data.HasSelectedSystem) + throw new InvalidOperationException("System already selected"); + data.SelectedSystem = RussianRouletteSystem.System1; + SetupSystem1(data); + break; + + case RussianRoulettePlayerAction.SelectSystem2: + if (data.HasSelectedSystem) + throw new InvalidOperationException("System already selected"); + data.SelectedSystem = RussianRouletteSystem.System2; + SetupSystem2(data); + break; + + case RussianRoulettePlayerAction.PullTrigger: + if (!data.HasSelectedSystem) + throw new InvalidOperationException("Must select system first"); + if (data.GameEnded) + throw new InvalidOperationException("Game has already ended"); + HandlePullTrigger(player); + break; + + case RussianRoulettePlayerAction.CashOut: + if (!data.HasSelectedSystem) + throw new InvalidOperationException("Must select system first"); + if (data.GameEnded) + throw new InvalidOperationException("Game has already ended"); + if (data.CurrentTurn == 0) + throw new InvalidOperationException("Cannot cash out before first turn"); + HandleCashOut(player); + break; + } + + data.HasMadeAction = true; + } + + private void SetupSystem1(RussianRoulettePlayerData data) + { + // System 1: No chamber setup needed, just random chance each turn + data.CurrentTurn = 0; + } + + private void SetupSystem2(RussianRoulettePlayerData data) + { + // System 2: Setup initial chamber with 1 bullet out of 6 for turn 1 + data.Chamber = new List { true, false, false, false, false, false }; + ShuffleChamber(data.Chamber); + data.CurrentTurn = 0; + } + + private void SetupSystem2NextTurn(RussianRoulettePlayerData data) + { + // For System 2: Create chamber for next turn with (currentTurn + 1) bullets + var bulletsCount = data.CurrentTurn + 1; // Turn 1=1 bullet, Turn 2=2 bullets, etc. + data.Chamber = new List(); + + for (int i = 0; i < bulletsCount; i++) + { + data.Chamber.Add(true); + } + for (int i = bulletsCount; i < 6; i++) + { + data.Chamber.Add(false); + } + + ShuffleChamber(data.Chamber); + } + + private void HandlePullTrigger(GamePlayer player) + { + var data = GameData[player]; + bool hitBullet = false; + + if (data.SelectedSystem == RussianRouletteSystem.System1) + { + // System 1: Fixed 1/6 chance each turn + hitBullet = _random.Next(6) == 0; + } + else if (data.SelectedSystem == RussianRouletteSystem.System2) + { + // System 2: Check current chamber position (first position) + hitBullet = data.Chamber[0]; + } + + if (hitBullet) + { + // Game over - player loses + data.GameEnded = true; + data.WonGame = false; + } + else + { + // Survived this turn + data.BulletsSurvived++; + data.CurrentTurn++; + + // Check if all 6 chambers survived (auto cash out) + if (data.BulletsSurvived >= 6) + { + data.GameEnded = true; + data.WonGame = true; + } + else if (data.SelectedSystem == RussianRouletteSystem.System2) + { + // For System 2: Setup next turn with more bullets + SetupSystem2NextTurn(data); + } + } + } + + private void HandleCashOut(GamePlayer player) + { + var data = GameData[player]; + data.GameEnded = true; + data.WonGame = true; + } + + private void ShuffleChamber(List chamber) + { + for (int i = chamber.Count - 1; i > 0; i--) + { + int j = _random.Next(i + 1); + (chamber[i], chamber[j]) = (chamber[j], chamber[i]); + } + } + + #endregion + + #region Results and Payouts + + public override GamePlayerResult GetPlayerGameResult(GamePlayer player) + { + var data = GameData[player]; + + if (!data.GameEnded) + return GamePlayerResult.NoResult; + + return data.WonGame ? GamePlayerResult.Won : GamePlayerResult.Lost; + } + + public override long CalculatePayout(GamePlayer player, ulong totalPot) + { + var result = GetPlayerGameResult(player); + var data = GameData[player]; + + switch (result) + { + case GamePlayerResult.Won: + var multiplier = GetCurrentPayoutMultiplier(player); + var winnings = (long)(player.Bet * multiplier); + return winnings - (long)player.Bet; // Net gain + + case GamePlayerResult.Lost: + return -(long)player.Bet; // Lose entire bet + + default: + return 0; + } + } + + public double GetCurrentPayoutMultiplier(GamePlayer player) + { + var data = GameData[player]; + if (!data.HasSelectedSystem) return 1.0; + + var multipliers = PayoutMultipliers[data.SelectedSystem]; + int index = Math.Min(data.BulletsSurvived, multipliers.Length - 1); + return multipliers[index]; + } + + public double GetNextPayoutMultiplier(GamePlayer player) + { + var data = GameData[player]; + if (!data.HasSelectedSystem) return 1.0; + + var multipliers = PayoutMultipliers[data.SelectedSystem]; + int nextIndex = Math.Min(data.BulletsSurvived + 1, multipliers.Length - 1); + return multipliers[nextIndex]; + } + + #endregion + + #region Action Validation + + public bool CanPlayerSelectSystem(GamePlayer player) + { + if (State != GameState.InProgress) return false; + var data = GameData[player]; + return !data.HasSelectedSystem && !data.GameEnded; + } + + public bool CanPlayerPullTrigger(GamePlayer player) + { + if (State != GameState.InProgress) return false; + var data = GameData[player]; + return data.HasSelectedSystem && !data.GameEnded; + } + + public bool CanPlayerCashOut(GamePlayer player) + { + if (State != GameState.InProgress) return false; + var data = GameData[player]; + return data.HasSelectedSystem && !data.GameEnded && data.BulletsSurvived > 0; + } + + #endregion + + #region AI Actions (Not used in single player game) + + protected override AIAction? GetNextAIAction() + { + // Russian Roulette is single player vs house, no AI actions needed + return null; + } + + public override bool ShouldFinish() + { + // Game should finish when player has ended the game (win or lose) + if (Players.Count == 0) return false; + + var player = Players[0]; + var data = GameData[player]; + + return data.GameEnded; + } + + #endregion +} \ No newline at end of file diff --git a/DiscordBot/Modules/Casino/CasinoSlashModule.Games.cs b/DiscordBot/Modules/Casino/CasinoSlashModule.Games.cs index 3f4b4b27..659c89df 100644 --- a/DiscordBot/Modules/Casino/CasinoSlashModule.Games.cs +++ b/DiscordBot/Modules/Casino/CasinoSlashModule.Games.cs @@ -11,6 +11,8 @@ public enum CasinoGame [ChoiceDisplay("Rock Paper Scissors")] RockPaperScissors, Poker, + [ChoiceDisplay("Russian Roulette")] + RussianRoulette, } public partial class CasinoSlashModule : InteractionModuleBase diff --git a/DiscordBot/Services/Casino/GameService.cs b/DiscordBot/Services/Casino/GameService.cs index a794ef0c..c23655ad 100644 --- a/DiscordBot/Services/Casino/GameService.cs +++ b/DiscordBot/Services/Casino/GameService.cs @@ -26,6 +26,7 @@ public ICasinoGame GetGameInstance(CasinoGame game) CasinoGame.Blackjack => new Blackjack(), CasinoGame.RockPaperScissors => new RockPaperScissors(), CasinoGame.Poker => new Poker(), + CasinoGame.RussianRoulette => new RussianRoulette(), _ => throw new ArgumentOutOfRangeException(nameof(game), $"Unknown game: {game}") }; } @@ -37,6 +38,7 @@ private IDiscordGameSession CreateDiscordGameSession(CasinoGame game, ICasinoGam 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.RussianRoulette => new RussianRouletteDiscordGameSession((RussianRoulette)gameInstance, maxSeats, client, user, guild), _ => throw new ArgumentOutOfRangeException(nameof(game), $"Unknown game session type: {game}") }; }