Reorganizing module and submodule folders

This commit is contained in:
Kwoth
2022-01-01 16:25:00 +01:00
parent 9b4eb21321
commit 9c590668df
69 changed files with 17 additions and 41 deletions

View File

@@ -0,0 +1,185 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.Blackjack;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
public partial class BlackJackCommands : GamblingSubmodule<BlackJackService>
{
public enum BjAction
{
Hit = int.MinValue,
Stand,
Double
}
private readonly ICurrencyService _cs;
private readonly DbService _db;
private IUserMessage _msg;
public BlackJackCommands(ICurrencyService cs, DbService db, GamblingConfigService gamblingConf)
: base(gamblingConf)
{
_cs = cs;
_db = db;
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async partial Task BlackJack(ShmartNumber amount)
{
if (!await CheckBetMandatory(amount))
return;
var newBj = new Blackjack(_cs, _db);
Blackjack bj;
if (newBj == (bj = _service.Games.GetOrAdd(ctx.Channel.Id, newBj)))
{
if (!await bj.Join(ctx.User, amount))
{
_service.Games.TryRemove(ctx.Channel.Id, out _);
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
bj.StateUpdated += Bj_StateUpdated;
bj.GameEnded += Bj_GameEnded;
bj.Start();
await ReplyConfirmLocalizedAsync(strs.bj_created);
}
else
{
if (await bj.Join(ctx.User, amount))
await ReplyConfirmLocalizedAsync(strs.bj_joined);
else
Log.Information(
$"{ctx.User} can't join a blackjack game as it's in " + bj.State + " state already.");
}
await ctx.Message.DeleteAsync();
}
private Task Bj_GameEnded(Blackjack arg)
{
_service.Games.TryRemove(ctx.Channel.Id, out _);
return Task.CompletedTask;
}
private async Task Bj_StateUpdated(Blackjack bj)
{
try
{
if (_msg is not null)
{
var _ = _msg.DeleteAsync();
}
var c = bj.Dealer.Cards.Select(x => x.GetEmojiString());
var dealerIcon = "❔ ";
if (bj.State == Blackjack.GameState.Ended)
{
if (bj.Dealer.GetHandValue() == 21)
dealerIcon = "💰 ";
else if (bj.Dealer.GetHandValue() > 21)
dealerIcon = "💥 ";
else
dealerIcon = "🏁 ";
}
var cStr = string.Concat(c.Select(x => x[..^1] + " "));
cStr += "\n" + string.Concat(c.Select(x => x.Last() + " "));
var embed = _eb.Create()
.WithOkColor()
.WithTitle("BlackJack")
.AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr);
if (bj.CurrentUser is not null) embed.WithFooter($"Player to make a choice: {bj.CurrentUser.DiscordUser}");
foreach (var p in bj.Players)
{
c = p.Cards.Select(x => x.GetEmojiString());
cStr = "-\t" + string.Concat(c.Select(x => x[..^1] + " "));
cStr += "\n-\t" + string.Concat(c.Select(x => x.Last() + " "));
var full = $"{p.DiscordUser.ToString().TrimTo(20)} | Bet: {p.Bet} | Value: {p.GetHandValue()}";
if (bj.State == Blackjack.GameState.Ended)
{
if (p.State == User.UserState.Lost)
full = "❌ " + full;
else
full = "✅ " + full;
}
else if (p == bj.CurrentUser)
{
full = "▶ " + full;
}
else if (p.State == User.UserState.Stand)
{
full = "⏹ " + full;
}
else if (p.State == User.UserState.Bust)
{
full = "💥 " + full;
}
else if (p.State == User.UserState.Blackjack)
{
full = "💰 " + full;
}
embed.AddField(full, cStr);
}
_msg = await ctx.Channel.EmbedAsync(embed);
}
catch
{
}
}
private string UserToString(User x)
{
var playerName = x.State == User.UserState.Bust
? Format.Strikethrough(x.DiscordUser.ToString().TrimTo(30))
: x.DiscordUser.ToString();
var hand = $"{string.Concat(x.Cards.Select(y => "" + y.GetEmojiString() + ""))}";
return $"{playerName} | Bet: {x.Bet}\n";
}
[Cmd]
[RequireContext(ContextType.Guild)]
public partial Task Hit()
=> InternalBlackJack(BjAction.Hit);
[Cmd]
[RequireContext(ContextType.Guild)]
public partial Task Stand()
=> InternalBlackJack(BjAction.Stand);
[Cmd]
[RequireContext(ContextType.Guild)]
public partial Task Double()
=> InternalBlackJack(BjAction.Double);
private async Task InternalBlackJack(BjAction a)
{
if (!_service.Games.TryGetValue(ctx.Channel.Id, out var bj))
return;
if (a == BjAction.Hit)
await bj.Hit(ctx.User);
else if (a == BjAction.Stand)
await bj.Stand(ctx.User);
else if (a == BjAction.Double)
if (!await bj.Double(ctx.User))
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
await ctx.Message.DeleteAsync();
}
}
}

View File

@@ -0,0 +1,9 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common.Blackjack;
namespace NadekoBot.Modules.Gambling.Services;
public class BlackJackService : INService
{
public ConcurrentDictionary<ulong, Blackjack> Games { get; } = new();
}

View File

@@ -0,0 +1,313 @@
#nullable disable
namespace NadekoBot.Modules.Gambling.Common.Blackjack;
public class Blackjack
{
public enum GameState
{
Starting,
Playing,
Ended
}
public event Func<Blackjack, Task> StateUpdated;
public event Func<Blackjack, Task> GameEnded;
private Deck Deck { get; } = new QuadDeck();
public Dealer Dealer { get; set; }
public List<User> Players { get; set; } = new();
public GameState State { get; set; } = GameState.Starting;
public User CurrentUser { get; private set; }
private TaskCompletionSource<bool> _currentUserMove;
private readonly ICurrencyService _cs;
private readonly DbService _db;
private readonly SemaphoreSlim locker = new(1, 1);
public Blackjack(ICurrencyService cs, DbService db)
{
_cs = cs;
_db = db;
Dealer = new();
}
public void Start()
{
var _ = GameLoop();
}
public async Task GameLoop()
{
try
{
//wait for players to join
await Task.Delay(20000);
await locker.WaitAsync();
try
{
State = GameState.Playing;
}
finally
{
locker.Release();
}
await PrintState();
//if no users joined the game, end it
if (!Players.Any())
{
State = GameState.Ended;
var end = GameEnded?.Invoke(this);
return;
}
//give 1 card to the dealer and 2 to each player
Dealer.Cards.Add(Deck.Draw());
foreach (var usr in Players)
{
usr.Cards.Add(Deck.Draw());
usr.Cards.Add(Deck.Draw());
if (usr.GetHandValue() == 21)
usr.State = User.UserState.Blackjack;
}
//go through all users and ask them what they want to do
foreach (var usr in Players.Where(x => !x.Done))
while (!usr.Done)
{
Log.Information($"Waiting for {usr.DiscordUser}'s move");
await PromptUserMove(usr);
}
await PrintState();
State = GameState.Ended;
await Task.Delay(2500);
Log.Information("Dealer moves");
await DealerMoves();
await PrintState();
var _ = GameEnded?.Invoke(this);
}
catch (Exception ex)
{
Log.Error(ex, "REPORT THE MESSAGE BELOW IN #NadekoLog SERVER PLEASE");
State = GameState.Ended;
var _ = GameEnded?.Invoke(this);
}
}
private async Task PromptUserMove(User usr)
{
var pause = Task.Delay(20000); //10 seconds to decide
CurrentUser = usr;
_currentUserMove = new();
await PrintState();
// either wait for the user to make an action and
// if he doesn't - stand
var finished = await Task.WhenAny(pause, _currentUserMove.Task);
if (finished == pause) await Stand(usr);
CurrentUser = null;
_currentUserMove = null;
}
public async Task<bool> Join(IUser user, long bet)
{
await locker.WaitAsync();
try
{
if (State != GameState.Starting)
return false;
if (Players.Count >= 5)
return false;
if (!await _cs.RemoveAsync(user, "BlackJack-gamble", bet, gamble: true)) return false;
Players.Add(new(user, bet));
var _ = PrintState();
return true;
}
finally
{
locker.Release();
}
}
public async Task<bool> Stand(IUser u)
{
var cu = CurrentUser;
if (cu is not null && cu.DiscordUser == u)
return await Stand(cu);
return false;
}
public async Task<bool> Stand(User u)
{
await locker.WaitAsync();
try
{
if (State != GameState.Playing)
return false;
if (CurrentUser != u)
return false;
u.State = User.UserState.Stand;
_currentUserMove.TrySetResult(true);
return true;
}
finally
{
locker.Release();
}
}
private async Task DealerMoves()
{
var hw = Dealer.GetHandValue();
while (hw < 17
|| (hw == 17
&& Dealer.Cards.Count(x => x.Number == 1) > (Dealer.GetRawHandValue() - 17) / 10)) // hit on soft 17
{
/* Dealer has
A 6
That's 17, soft
hw == 17 => true
number of aces = 1
1 > 17-17 /10 => true
AA 5
That's 17, again soft, since one ace is worth 11, even though another one is 1
hw == 17 => true
number of aces = 2
2 > 27 - 17 / 10 => true
AA Q 5
That's 17, but not soft, since both aces are worth 1
hw == 17 => true
number of aces = 2
2 > 37 - 17 / 10 => false
* */
Dealer.Cards.Add(Deck.Draw());
hw = Dealer.GetHandValue();
}
if (hw > 21)
foreach (var usr in Players)
if (usr.State is User.UserState.Stand or User.UserState.Blackjack)
usr.State = User.UserState.Won;
else
usr.State = User.UserState.Lost;
else
foreach (var usr in Players)
if (usr.State == User.UserState.Blackjack)
usr.State = User.UserState.Won;
else if (usr.State == User.UserState.Stand)
usr.State = hw < usr.GetHandValue() ? User.UserState.Won : User.UserState.Lost;
else
usr.State = User.UserState.Lost;
foreach (var usr in Players)
if (usr.State is User.UserState.Won or User.UserState.Blackjack)
await _cs.AddAsync(usr.DiscordUser.Id, "BlackJack-win", usr.Bet * 2, true);
}
public async Task<bool> Double(IUser u)
{
var cu = CurrentUser;
if (cu is not null && cu.DiscordUser == u)
return await Double(cu);
return false;
}
public async Task<bool> Double(User u)
{
await locker.WaitAsync();
try
{
if (State != GameState.Playing)
return false;
if (CurrentUser != u)
return false;
if (!await _cs.RemoveAsync(u.DiscordUser.Id, "Blackjack-double", u.Bet))
return false;
u.Bet *= 2;
u.Cards.Add(Deck.Draw());
if (u.GetHandValue() == 21)
//blackjack
u.State = User.UserState.Blackjack;
else if (u.GetHandValue() > 21)
// user busted
u.State = User.UserState.Bust;
else
//with double you just get one card, and then you're done
u.State = User.UserState.Stand;
_currentUserMove.TrySetResult(true);
return true;
}
finally
{
locker.Release();
}
}
public async Task<bool> Hit(IUser u)
{
var cu = CurrentUser;
if (cu is not null && cu.DiscordUser == u)
return await Hit(cu);
return false;
}
public async Task<bool> Hit(User u)
{
await locker.WaitAsync();
try
{
if (State != GameState.Playing)
return false;
if (CurrentUser != u)
return false;
u.Cards.Add(Deck.Draw());
if (u.GetHandValue() == 21)
//blackjack
u.State = User.UserState.Blackjack;
else if (u.GetHandValue() > 21)
// user busted
u.State = User.UserState.Bust;
_currentUserMove.TrySetResult(true);
return true;
}
finally
{
locker.Release();
}
}
public Task PrintState()
{
if (StateUpdated is null)
return Task.CompletedTask;
return StateUpdated.Invoke(this);
}
}

View File

@@ -0,0 +1,55 @@
#nullable disable
namespace NadekoBot.Modules.Gambling.Common.Blackjack;
public abstract class Player
{
public List<Deck.Card> Cards { get; } = new();
public int GetHandValue()
{
var val = GetRawHandValue();
// while the hand value is greater than 21, for each ace you have in the deck
// reduce the value by 10 until it drops below 22
// (emulating the fact that ace is either a 1 or a 11)
var i = Cards.Count(x => x.Number == 1);
while (val > 21 && i-- > 0) val -= 10;
return val;
}
public int GetRawHandValue()
=> Cards.Sum(x => x.Number == 1 ? 11 : x.Number >= 10 ? 10 : x.Number);
}
public class Dealer : Player
{
}
public class User : Player
{
public enum UserState
{
Waiting,
Stand,
Bust,
Blackjack,
Won,
Lost
}
public UserState State { get; set; } = UserState.Waiting;
public long Bet { get; set; }
public IUser DiscordUser { get; }
public bool Done
=> State != UserState.Waiting;
public User(IUser user, long bet)
{
if (bet <= 0)
throw new ArgumentOutOfRangeException(nameof(bet));
Bet = bet;
DiscordUser = user;
}
}