mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
Restructured folders and project names, ci should be fixed
This commit is contained in:
163
src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs
Normal file
163
src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Core.Modules.Gambling.Common.AnimalRacing;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing
|
||||
{
|
||||
public sealed class AnimalRace : IDisposable
|
||||
{
|
||||
public enum Phase
|
||||
{
|
||||
WaitingForPlayers,
|
||||
Running,
|
||||
Ended,
|
||||
}
|
||||
|
||||
public Phase CurrentPhase { get; private set; } = Phase.WaitingForPlayers;
|
||||
|
||||
public event Func<AnimalRace, Task> OnStarted = delegate { return Task.CompletedTask; };
|
||||
public event Func<AnimalRace, Task> OnStartingFailed = delegate { return Task.CompletedTask; };
|
||||
public event Func<AnimalRace, Task> OnStateUpdate = delegate { return Task.CompletedTask; };
|
||||
public event Func<AnimalRace, Task> OnEnded = delegate { return Task.CompletedTask; };
|
||||
|
||||
public IReadOnlyCollection<AnimalRacingUser> Users => _users.ToList();
|
||||
public List<AnimalRacingUser> FinishedUsers { get; } = new List<AnimalRacingUser>();
|
||||
|
||||
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
|
||||
private readonly HashSet<AnimalRacingUser> _users = new HashSet<AnimalRacingUser>();
|
||||
private readonly ICurrencyService _currency;
|
||||
private readonly RaceOptions _options;
|
||||
private readonly Queue<RaceAnimal> _animalsQueue;
|
||||
public int MaxUsers { get; }
|
||||
|
||||
public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable<RaceAnimal> availableAnimals)
|
||||
{
|
||||
this._currency = currency;
|
||||
this._options = options;
|
||||
this._animalsQueue = new Queue<RaceAnimal>(availableAnimals);
|
||||
this.MaxUsers = _animalsQueue.Count;
|
||||
|
||||
if (this._animalsQueue.Count == 0)
|
||||
CurrentPhase = Phase.Ended;
|
||||
}
|
||||
|
||||
public void Initialize() //lame name
|
||||
{
|
||||
var _t = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(_options.StartTime * 1000).ConfigureAwait(false);
|
||||
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (CurrentPhase != Phase.WaitingForPlayers)
|
||||
return;
|
||||
|
||||
await Start().ConfigureAwait(false);
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<AnimalRacingUser> JoinRace(ulong userId, string userName, long bet = 0)
|
||||
{
|
||||
if (bet < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(bet));
|
||||
|
||||
var user = new AnimalRacingUser(userName, userId, bet);
|
||||
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_users.Count == MaxUsers)
|
||||
throw new AnimalRaceFullException();
|
||||
|
||||
if (CurrentPhase != Phase.WaitingForPlayers)
|
||||
throw new AlreadyStartedException();
|
||||
|
||||
if (!await _currency.RemoveAsync(userId, "BetRace", bet).ConfigureAwait(false))
|
||||
throw new NotEnoughFundsException();
|
||||
|
||||
if (_users.Contains(user))
|
||||
throw new AlreadyJoinedException();
|
||||
|
||||
var animal = _animalsQueue.Dequeue();
|
||||
user.Animal = animal;
|
||||
_users.Add(user);
|
||||
|
||||
if (_animalsQueue.Count == 0) //start if no more spots left
|
||||
await Start().ConfigureAwait(false);
|
||||
|
||||
return user;
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}
|
||||
|
||||
private async Task Start()
|
||||
{
|
||||
CurrentPhase = Phase.Running;
|
||||
if (_users.Count <= 1)
|
||||
{
|
||||
foreach (var user in _users)
|
||||
{
|
||||
if (user.Bet > 0)
|
||||
await _currency.AddAsync(user.UserId, "Race refund", user.Bet).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var _sf = OnStartingFailed?.Invoke(this);
|
||||
CurrentPhase = Phase.Ended;
|
||||
return;
|
||||
}
|
||||
|
||||
var _ = OnStarted?.Invoke(this);
|
||||
var _t = Task.Run(async () =>
|
||||
{
|
||||
var rng = new NadekoRandom();
|
||||
while (!_users.All(x => x.Progress >= 60))
|
||||
{
|
||||
foreach (var user in _users)
|
||||
{
|
||||
user.Progress += rng.Next(1, 11);
|
||||
if (user.Progress >= 60)
|
||||
user.Progress = 60;
|
||||
}
|
||||
|
||||
var finished = _users.Where(x => x.Progress >= 60 && !FinishedUsers.Contains(x))
|
||||
.Shuffle();
|
||||
|
||||
FinishedUsers.AddRange(finished);
|
||||
|
||||
var _ignore = OnStateUpdate?.Invoke(this);
|
||||
await Task.Delay(2500).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (FinishedUsers[0].Bet > 0)
|
||||
await _currency.AddAsync(FinishedUsers[0].UserId, "Won a Race", FinishedUsers[0].Bet * (_users.Count - 1))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var _ended = OnEnded?.Invoke(this);
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CurrentPhase = Phase.Ended;
|
||||
OnStarted = null;
|
||||
OnEnded = null;
|
||||
OnStartingFailed = null;
|
||||
OnStateUpdate = null;
|
||||
_locker.Dispose();
|
||||
_users.Clear();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing
|
||||
{
|
||||
public class AnimalRacingUser
|
||||
{
|
||||
public long Bet { get; }
|
||||
public string Username { get; }
|
||||
public ulong UserId { get; }
|
||||
public RaceAnimal Animal { get; set; }
|
||||
public int Progress { get; set; }
|
||||
|
||||
public AnimalRacingUser(string username, ulong userId, long bet)
|
||||
{
|
||||
this.Bet = bet;
|
||||
this.Username = username;
|
||||
this.UserId = userId;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is AnimalRacingUser x
|
||||
? x.UserId == this.UserId
|
||||
: false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.UserId.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
|
||||
{
|
||||
public class AlreadyJoinedException : Exception
|
||||
{
|
||||
public AlreadyJoinedException()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public AlreadyJoinedException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AlreadyJoinedException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
|
||||
{
|
||||
public class AlreadyStartedException : Exception
|
||||
{
|
||||
public AlreadyStartedException()
|
||||
{
|
||||
}
|
||||
|
||||
public AlreadyStartedException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AlreadyStartedException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
|
||||
{
|
||||
public class AnimalRaceFullException : Exception
|
||||
{
|
||||
public AnimalRaceFullException()
|
||||
{
|
||||
}
|
||||
|
||||
public AnimalRaceFullException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AnimalRaceFullException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
|
||||
{
|
||||
public class NotEnoughFundsException : Exception
|
||||
{
|
||||
public NotEnoughFundsException()
|
||||
{
|
||||
}
|
||||
|
||||
public NotEnoughFundsException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public NotEnoughFundsException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
using CommandLine;
|
||||
using NadekoBot.Core.Common;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.AnimalRacing
|
||||
{
|
||||
public class RaceOptions : INadekoCommandOptions
|
||||
{
|
||||
[Option('s', "start-time", Default = 20, Required = false)]
|
||||
public int StartTime { get; set; } = 20;
|
||||
|
||||
public void NormalizeOptions()
|
||||
{
|
||||
if (this.StartTime < 10 || this.StartTime > 120)
|
||||
this.StartTime = 20;
|
||||
}
|
||||
}
|
||||
}
|
47
src/NadekoBot/Modules/Gambling/Common/BetRoll.cs
Normal file
47
src/NadekoBot/Modules/Gambling/Common/BetRoll.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common
|
||||
{
|
||||
public class Betroll
|
||||
{
|
||||
public class Result
|
||||
{
|
||||
public int Roll { get; set; }
|
||||
public float Multiplier { get; set; }
|
||||
public int Threshold { get; set; }
|
||||
}
|
||||
|
||||
|
||||
private readonly IOrderedEnumerable<GamblingConfig.BetRollConfig.Pair> _thresholdPairs;
|
||||
private readonly Random _rng;
|
||||
|
||||
public Betroll(GamblingConfig.BetRollConfig settings)
|
||||
{
|
||||
_thresholdPairs = settings.Pairs.OrderByDescending(x => x.WhenAbove);
|
||||
_rng = new Random();
|
||||
}
|
||||
|
||||
public Result Roll()
|
||||
{
|
||||
var roll = _rng.Next(0, 101);
|
||||
|
||||
var pair = _thresholdPairs.FirstOrDefault(x => x.WhenAbove < roll);
|
||||
if (pair is null)
|
||||
{
|
||||
return new Result
|
||||
{
|
||||
Multiplier = 0,
|
||||
Roll = roll,
|
||||
};
|
||||
}
|
||||
|
||||
return new Result
|
||||
{
|
||||
Multiplier = pair.MultiplyBy,
|
||||
Roll = roll,
|
||||
Threshold = pair.WhenAbove,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
353
src/NadekoBot/Modules/Gambling/Common/Blackjack/Blackjack.cs
Normal file
353
src/NadekoBot/Modules/Gambling/Common/Blackjack/Blackjack.cs
Normal file
@@ -0,0 +1,353 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Blackjack
|
||||
{
|
||||
public class Blackjack
|
||||
{
|
||||
public enum GameState
|
||||
{
|
||||
Starting,
|
||||
Playing,
|
||||
Ended
|
||||
}
|
||||
|
||||
private Deck Deck { get; set; } = new QuadDeck();
|
||||
public Dealer Dealer { get; set; }
|
||||
|
||||
|
||||
public List<User> Players { get; set; } = new List<User>();
|
||||
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;
|
||||
|
||||
public event Func<Blackjack, Task> StateUpdated;
|
||||
public event Func<Blackjack, Task> GameEnded;
|
||||
|
||||
private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
|
||||
|
||||
public Blackjack(ICurrencyService cs, DbService db)
|
||||
{
|
||||
_cs = cs;
|
||||
_db = db;
|
||||
Dealer = new Dealer();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
var _ = GameLoop();
|
||||
}
|
||||
|
||||
public async Task GameLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
//wait for players to join
|
||||
await Task.Delay(20000).ConfigureAwait(false);
|
||||
await locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
State = GameState.Playing;
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.Release();
|
||||
}
|
||||
await PrintState().ConfigureAwait(false);
|
||||
//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).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
await PrintState().ConfigureAwait(false);
|
||||
State = GameState.Ended;
|
||||
await Task.Delay(2500).ConfigureAwait(false);
|
||||
Log.Information("Dealer moves");
|
||||
await DealerMoves().ConfigureAwait(false);
|
||||
await PrintState().ConfigureAwait(false);
|
||||
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 TaskCompletionSource<bool>();
|
||||
await PrintState().ConfigureAwait(false);
|
||||
// either wait for the user to make an action and
|
||||
// if he doesn't - stand
|
||||
var finished = await Task.WhenAny(pause, _currentUserMove.Task).ConfigureAwait(false);
|
||||
if (finished == pause)
|
||||
{
|
||||
await Stand(usr).ConfigureAwait(false);
|
||||
}
|
||||
CurrentUser = null;
|
||||
_currentUserMove = null;
|
||||
}
|
||||
|
||||
public async Task<bool> Join(IUser user, long bet)
|
||||
{
|
||||
await locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (State != GameState.Starting)
|
||||
return false;
|
||||
|
||||
if (Players.Count >= 5)
|
||||
return false;
|
||||
|
||||
if (!await _cs.RemoveAsync(user, "BlackJack-gamble", bet, gamble: true).ConfigureAwait(false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Players.Add(new User(user, bet));
|
||||
var _ = PrintState();
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> Stand(IUser u)
|
||||
{
|
||||
var cu = CurrentUser;
|
||||
|
||||
if (cu != null && cu.DiscordUser == u)
|
||||
return await Stand(cu).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> Stand(User u)
|
||||
{
|
||||
await locker.WaitAsync().ConfigureAwait(false);
|
||||
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 == User.UserState.Stand || usr.State == 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 == User.UserState.Won || usr.State == User.UserState.Blackjack)
|
||||
{
|
||||
await _cs.AddAsync(usr.DiscordUser.Id, "BlackJack-win", usr.Bet * 2, gamble: true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> Double(IUser u)
|
||||
{
|
||||
var cu = CurrentUser;
|
||||
|
||||
if (cu != null && cu.DiscordUser == u)
|
||||
return await Double(cu).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> Double(User u)
|
||||
{
|
||||
await locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (State != GameState.Playing)
|
||||
return false;
|
||||
|
||||
if (CurrentUser != u)
|
||||
return false;
|
||||
|
||||
if (!await _cs.RemoveAsync(u.DiscordUser.Id, "Blackjack-double", u.Bet).ConfigureAwait(false))
|
||||
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 != null && cu.DiscordUser == u)
|
||||
return await Hit(cu).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> Hit(User u)
|
||||
{
|
||||
await locker.WaitAsync().ConfigureAwait(false);
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
//you can hit or stand again
|
||||
}
|
||||
_currentUserMove.TrySetResult(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public Task PrintState()
|
||||
{
|
||||
if (StateUpdated == null)
|
||||
return Task.CompletedTask;
|
||||
return StateUpdated.Invoke(this);
|
||||
}
|
||||
}
|
||||
}
|
65
src/NadekoBot/Modules/Gambling/Common/Blackjack/Player.cs
Normal file
65
src/NadekoBot/Modules/Gambling/Common/Blackjack/Player.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Discord;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Blackjack
|
||||
{
|
||||
public abstract class Player
|
||||
{
|
||||
public List<Deck.Card> Cards { get; } = new List<Deck.Card>();
|
||||
|
||||
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()
|
||||
{
|
||||
return 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 User(IUser user, long bet)
|
||||
{
|
||||
if (bet <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(bet));
|
||||
|
||||
this.Bet = bet;
|
||||
this.DiscordUser = user;
|
||||
}
|
||||
|
||||
public UserState State { get; set; } = UserState.Waiting;
|
||||
public long Bet { get; set; }
|
||||
public IUser DiscordUser { get; }
|
||||
public bool Done => State != UserState.Waiting;
|
||||
}
|
||||
}
|
81
src/NadekoBot/Modules/Gambling/Common/CurrencyRaffleGame.cs
Normal file
81
src/NadekoBot/Modules/Gambling/Common/CurrencyRaffleGame.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Discord;
|
||||
using NadekoBot.Common;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common
|
||||
{
|
||||
public class CurrencyRaffleGame
|
||||
{
|
||||
public enum Type {
|
||||
Mixed,
|
||||
Normal
|
||||
}
|
||||
|
||||
public class User
|
||||
{
|
||||
public IUser DiscordUser { get; set; }
|
||||
public long Amount { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return DiscordUser.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is User u
|
||||
? u.DiscordUser == DiscordUser
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly HashSet<User> _users = new HashSet<User>();
|
||||
public IEnumerable<User> Users => _users;
|
||||
public Type GameType { get; }
|
||||
|
||||
public CurrencyRaffleGame(Type type)
|
||||
{
|
||||
GameType = type;
|
||||
}
|
||||
|
||||
public bool AddUser(IUser usr, long amount)
|
||||
{
|
||||
// if game type is normal, and someone already joined the game
|
||||
// (that's the user who created it)
|
||||
if (GameType == Type.Normal && _users.Count > 0 &&
|
||||
_users.First().Amount != amount)
|
||||
return false;
|
||||
|
||||
if (!_users.Add(new User
|
||||
{
|
||||
DiscordUser = usr,
|
||||
Amount = amount,
|
||||
}))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public User GetWinner()
|
||||
{
|
||||
var rng = new NadekoRandom();
|
||||
if (GameType == Type.Mixed)
|
||||
{
|
||||
var num = rng.NextLong(0L, Users.Sum(x => x.Amount));
|
||||
var sum = 0L;
|
||||
foreach (var u in Users)
|
||||
{
|
||||
sum += u.Amount;
|
||||
if (sum > num)
|
||||
return u;
|
||||
}
|
||||
}
|
||||
|
||||
var usrs = _users.ToArray();
|
||||
return usrs[rng.Next(0, usrs.Length)];
|
||||
}
|
||||
}
|
||||
}
|
312
src/NadekoBot/Modules/Gambling/Common/Deck.cs
Normal file
312
src/NadekoBot/Modules/Gambling/Common/Deck.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common
|
||||
{
|
||||
public class QuadDeck : Deck
|
||||
{
|
||||
protected override void RefillPool()
|
||||
{
|
||||
CardPool = new List<Card>(52 * 4);
|
||||
for (var j = 1; j < 14; j++)
|
||||
{
|
||||
for (var i = 1; i < 5; i++)
|
||||
{
|
||||
CardPool.Add(new Card((CardSuit)i, j));
|
||||
CardPool.Add(new Card((CardSuit)i, j));
|
||||
CardPool.Add(new Card((CardSuit)i, j));
|
||||
CardPool.Add(new Card((CardSuit)i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Deck
|
||||
{
|
||||
private static readonly Dictionary<int, string> cardNames = new Dictionary<int, string>() {
|
||||
{ 1, "Ace" },
|
||||
{ 2, "Two" },
|
||||
{ 3, "Three" },
|
||||
{ 4, "Four" },
|
||||
{ 5, "Five" },
|
||||
{ 6, "Six" },
|
||||
{ 7, "Seven" },
|
||||
{ 8, "Eight" },
|
||||
{ 9, "Nine" },
|
||||
{ 10, "Ten" },
|
||||
{ 11, "Jack" },
|
||||
{ 12, "Queen" },
|
||||
{ 13, "King" }
|
||||
};
|
||||
private static Dictionary<string, Func<List<Card>, bool>> handValues;
|
||||
|
||||
|
||||
public enum CardSuit
|
||||
{
|
||||
Spades = 1,
|
||||
Hearts = 2,
|
||||
Diamonds = 3,
|
||||
Clubs = 4
|
||||
}
|
||||
|
||||
public class Card : IComparable
|
||||
{
|
||||
public CardSuit Suit { get; }
|
||||
public int Number { get; }
|
||||
|
||||
public string FullName
|
||||
{
|
||||
get
|
||||
{
|
||||
var str = "";
|
||||
|
||||
if (Number <= 10 && Number > 1)
|
||||
{
|
||||
str += "_" + Number;
|
||||
}
|
||||
else
|
||||
{
|
||||
str += GetValueText().ToLowerInvariant();
|
||||
}
|
||||
return str + "_of_" + Suit.ToString().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
public Card(CardSuit s, int cardNum)
|
||||
{
|
||||
this.Suit = s;
|
||||
this.Number = cardNum;
|
||||
}
|
||||
|
||||
public string GetValueText() => cardNames[Number];
|
||||
|
||||
public override string ToString() => cardNames[Number] + " Of " + Suit;
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (!(obj is Card)) return 0;
|
||||
var c = (Card)obj;
|
||||
return this.Number - c.Number;
|
||||
}
|
||||
|
||||
public static Card Parse(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
if (input.Length != 2
|
||||
|| !_numberCharToNumber.TryGetValue(input[0], out var n)
|
||||
|| !_suitCharToSuit.TryGetValue(input[1].ToString(), out var s))
|
||||
{
|
||||
throw new ArgumentException("Invalid input", nameof(input));
|
||||
}
|
||||
|
||||
return new Card(s, n);
|
||||
}
|
||||
|
||||
public string GetEmojiString()
|
||||
{
|
||||
var str = "";
|
||||
|
||||
str += _regIndicators[this.Number - 1];
|
||||
str += _suitToSuitChar[this.Suit];
|
||||
|
||||
return str;
|
||||
}
|
||||
private readonly string[] _regIndicators = new[]
|
||||
{
|
||||
"🇦",
|
||||
":two:",
|
||||
":three:",
|
||||
":four:",
|
||||
":five:",
|
||||
":six:",
|
||||
":seven:",
|
||||
":eight:",
|
||||
":nine:",
|
||||
":keycap_ten:",
|
||||
"🇯",
|
||||
"🇶",
|
||||
"🇰"
|
||||
};
|
||||
private static readonly IReadOnlyDictionary<CardSuit, string> _suitToSuitChar = new Dictionary<CardSuit, string>
|
||||
{
|
||||
{CardSuit.Diamonds, "♦"},
|
||||
{CardSuit.Clubs, "♣"},
|
||||
{CardSuit.Spades, "♠"},
|
||||
{CardSuit.Hearts, "♥"},
|
||||
};
|
||||
private static IReadOnlyDictionary<string, CardSuit> _suitCharToSuit = new Dictionary<string, CardSuit>
|
||||
{
|
||||
{"♦", CardSuit.Diamonds },
|
||||
{"d", CardSuit.Diamonds },
|
||||
{"♣", CardSuit.Clubs },
|
||||
{"c", CardSuit.Clubs },
|
||||
{"♠", CardSuit.Spades },
|
||||
{"s", CardSuit.Spades },
|
||||
{"♥", CardSuit.Hearts },
|
||||
{"h", CardSuit.Hearts },
|
||||
};
|
||||
private static IReadOnlyDictionary<char, int> _numberCharToNumber = new Dictionary<char, int>()
|
||||
{
|
||||
{'a', 1 },
|
||||
{'2', 2 },
|
||||
{'3', 3 },
|
||||
{'4', 4 },
|
||||
{'5', 5 },
|
||||
{'6', 6 },
|
||||
{'7', 7 },
|
||||
{'8', 8 },
|
||||
{'9', 9 },
|
||||
{'t', 10 },
|
||||
{'j', 11 },
|
||||
{'q', 12 },
|
||||
{'k', 13 },
|
||||
};
|
||||
}
|
||||
|
||||
public List<Card> CardPool { get; set; }
|
||||
/// <summary>
|
||||
/// Creates a new instance of the BlackJackGame, this allows you to create multiple games running at one time.
|
||||
/// </summary>
|
||||
public Deck()
|
||||
{
|
||||
RefillPool();
|
||||
}
|
||||
static Deck()
|
||||
{
|
||||
InitHandValues();
|
||||
}
|
||||
/// <summary>
|
||||
/// Restart the game of blackjack. It will only refill the pool for now. Probably wont be used, unless you want to have only 1 bjg running at one time,
|
||||
/// then you will restart the same game every time.
|
||||
/// </summary>
|
||||
public void Restart() => RefillPool();
|
||||
|
||||
/// <summary>
|
||||
/// Removes all cards from the pool and refills the pool with all of the possible cards. NOTE: I think this is too expensive.
|
||||
/// We should probably make it so it copies another premade list with all the cards, or something.
|
||||
/// </summary>
|
||||
protected virtual void RefillPool()
|
||||
{
|
||||
CardPool = new List<Card>(52);
|
||||
//foreach suit
|
||||
for (var j = 1; j < 14; j++)
|
||||
{
|
||||
// and number
|
||||
for (var i = 1; i < 5; i++)
|
||||
{
|
||||
//generate a card of that suit and number and add it to the pool
|
||||
|
||||
// the pool will go from ace of spades,hears,diamonds,clubs all the way to the king of spades. hearts, ...
|
||||
CardPool.Add(new Card((CardSuit)i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
private Random r = new NadekoRandom();
|
||||
/// <summary>
|
||||
/// Take a card from the pool, you either take it from the top if the deck is shuffled, or from a random place if the deck is in the default order.
|
||||
/// </summary>
|
||||
/// <returns>A card from the pool</returns>
|
||||
public Card Draw()
|
||||
{
|
||||
if (CardPool.Count == 0)
|
||||
Restart();
|
||||
//you can either do this if your deck is not shuffled
|
||||
|
||||
var num = r.Next(0, CardPool.Count);
|
||||
var c = CardPool[num];
|
||||
CardPool.RemoveAt(num);
|
||||
return c;
|
||||
|
||||
// if you want to shuffle when you fill, then take the first one
|
||||
/*
|
||||
Card c = cardPool[0];
|
||||
cardPool.RemoveAt(0);
|
||||
return c;
|
||||
*/
|
||||
}
|
||||
/// <summary>
|
||||
/// Shuffles the deck. Use this if you want to take cards from the top of the deck, instead of randomly. See DrawACard method.
|
||||
/// </summary>
|
||||
private void Shuffle()
|
||||
{
|
||||
if (CardPool.Count <= 1) return;
|
||||
var orderedPool = CardPool.Shuffle();
|
||||
CardPool = CardPool as List<Card> ?? orderedPool.ToList();
|
||||
}
|
||||
public override string ToString() => string.Concat(CardPool.Select(c => c.ToString())) + Environment.NewLine;
|
||||
|
||||
private static void InitHandValues()
|
||||
{
|
||||
bool hasPair(List<Card> cards) => cards.GroupBy(card => card.Number)
|
||||
.Count(group => group.Count() == 2) == 1;
|
||||
bool isPair(List<Card> cards) => cards.GroupBy(card => card.Number)
|
||||
.Count(group => group.Count() == 3) == 0
|
||||
&& hasPair(cards);
|
||||
|
||||
bool isTwoPair(List<Card> cards) => cards.GroupBy(card => card.Number)
|
||||
.Count(group => group.Count() == 2) == 2;
|
||||
|
||||
bool isStraight(List<Card> cards)
|
||||
{
|
||||
if (cards.GroupBy(card => card.Number).Count() != cards.Count())
|
||||
return false;
|
||||
var toReturn = (cards.Max(card => (int)card.Number)
|
||||
- cards.Min(card => (int)card.Number) == 4);
|
||||
if (toReturn || cards.All(c => c.Number != 1)) return toReturn;
|
||||
|
||||
var newCards = cards.Select(c => c.Number == 1 ? new Card(c.Suit, 14) : c);
|
||||
return (newCards.Max(card => (int)card.Number)
|
||||
- newCards.Min(card => (int)card.Number) == 4);
|
||||
}
|
||||
|
||||
bool hasThreeOfKind(List<Card> cards) => cards.GroupBy(card => card.Number)
|
||||
.Any(group => group.Count() == 3);
|
||||
|
||||
bool isThreeOfKind(List<Card> cards) => hasThreeOfKind(cards) && !hasPair(cards);
|
||||
|
||||
bool isFlush(List<Card> cards) => cards.GroupBy(card => card.Suit).Count() == 1;
|
||||
|
||||
bool isFourOfKind(List<Card> cards) => cards.GroupBy(card => card.Number)
|
||||
.Any(group => group.Count() == 4);
|
||||
|
||||
bool isFullHouse(List<Card> cards) => hasPair(cards) && hasThreeOfKind(cards);
|
||||
|
||||
bool hasStraightFlush(List<Card> cards) => isFlush(cards) && isStraight(cards);
|
||||
|
||||
bool isRoyalFlush(List<Card> cards) => cards.Min(card => card.Number) == 1 &&
|
||||
cards.Max(card => card.Number) == 13
|
||||
&& hasStraightFlush(cards);
|
||||
|
||||
bool isStraightFlush(List<Card> cards) => hasStraightFlush(cards) && !isRoyalFlush(cards);
|
||||
|
||||
handValues = new Dictionary<string, Func<List<Card>, bool>>
|
||||
{
|
||||
{ "Royal Flush", isRoyalFlush },
|
||||
{ "Straight Flush", isStraightFlush },
|
||||
{ "Four Of A Kind", isFourOfKind },
|
||||
{ "Full House", isFullHouse },
|
||||
{ "Flush", isFlush },
|
||||
{ "Straight", isStraight },
|
||||
{ "Three Of A Kind", isThreeOfKind },
|
||||
{ "Two Pairs", isTwoPair },
|
||||
{ "A Pair", isPair }
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetHandValue(List<Card> cards)
|
||||
{
|
||||
if (handValues == null)
|
||||
InitHandValues();
|
||||
foreach (var kvp in handValues.Where(x => x.Value(cards)))
|
||||
{
|
||||
return kvp.Key;
|
||||
}
|
||||
return "High card " + (cards.FirstOrDefault(c => c.Number == 1)?.GetValueText() ?? cards.Max().GetValueText());
|
||||
}
|
||||
}
|
||||
}
|
30
src/NadekoBot/Modules/Gambling/Common/Events/EventOptions.cs
Normal file
30
src/NadekoBot/Modules/Gambling/Common/Events/EventOptions.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using CommandLine;
|
||||
using NadekoBot.Core.Common;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Events
|
||||
{
|
||||
public class EventOptions : INadekoCommandOptions
|
||||
{
|
||||
[Option('a', "amount", Required = false, Default = 100, HelpText = "Amount of currency each user receives.")]
|
||||
public long Amount { get; set; } = 100;
|
||||
[Option('p', "pot-size", Required = false, Default = 0, HelpText = "The maximum amount of currency that can be rewarded. 0 means no limit.")]
|
||||
public long PotSize { get; set; } = 0;
|
||||
//[Option('t', "type", Required = false, Default = "reaction", HelpText = "Type of the event. reaction, gamestatus or joinserver.")]
|
||||
//public string TypeString { get; set; } = "reaction";
|
||||
[Option('d', "duration", Required = false, Default = 24, HelpText = "Number of hours the event should run for. Default 24.")]
|
||||
public int Hours { get; set; } = 24;
|
||||
|
||||
|
||||
public void NormalizeOptions()
|
||||
{
|
||||
if (Amount < 0)
|
||||
Amount = 100;
|
||||
if (PotSize < 0)
|
||||
PotSize = 0;
|
||||
if (Hours <= 0)
|
||||
Hours = 24;
|
||||
if (PotSize != 0 && PotSize < Amount)
|
||||
PotSize = 0;
|
||||
}
|
||||
}
|
||||
}
|
210
src/NadekoBot/Modules/Gambling/Common/Events/GameStatusEvent.cs
Normal file
210
src/NadekoBot/Modules/Gambling/Common/Events/GameStatusEvent.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common.Collections;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Events
|
||||
{
|
||||
public class GameStatusEvent : ICurrencyEvent
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IGuild _guild;
|
||||
private IUserMessage _msg;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly long _amount;
|
||||
|
||||
private long PotSize { get; set; }
|
||||
public bool Stopped { get; private set; }
|
||||
public bool PotEmptied { get; private set; } = false;
|
||||
|
||||
private readonly Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> _embedFunc;
|
||||
private readonly bool _isPotLimited;
|
||||
private readonly ITextChannel _channel;
|
||||
private readonly ConcurrentHashSet<ulong> _awardedUsers = new ConcurrentHashSet<ulong>();
|
||||
private readonly ConcurrentQueue<ulong> _toAward = new ConcurrentQueue<ulong>();
|
||||
private readonly Timer _t;
|
||||
private readonly Timer _timeout = null;
|
||||
private readonly EventOptions _opts;
|
||||
|
||||
private readonly string _code;
|
||||
|
||||
public event Func<ulong, Task> OnEnded;
|
||||
|
||||
private readonly char[] _sneakyGameStatusChars = Enumerable.Range(48, 10)
|
||||
.Concat(Enumerable.Range(65, 26))
|
||||
.Concat(Enumerable.Range(97, 26))
|
||||
.Select(x => (char)x)
|
||||
.ToArray();
|
||||
|
||||
public GameStatusEvent(DiscordSocketClient client, ICurrencyService cs,SocketGuild g, ITextChannel ch,
|
||||
EventOptions opt, Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> embedFunc)
|
||||
{
|
||||
_client = client;
|
||||
_guild = g;
|
||||
_cs = cs;
|
||||
_amount = opt.Amount;
|
||||
PotSize = opt.PotSize;
|
||||
_embedFunc = embedFunc;
|
||||
_isPotLimited = PotSize > 0;
|
||||
_channel = ch;
|
||||
_opts = opt;
|
||||
// generate code
|
||||
_code = new string(_sneakyGameStatusChars.Shuffle().Take(5).ToArray());
|
||||
|
||||
_t = new Timer(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2));
|
||||
if (_opts.Hours > 0)
|
||||
{
|
||||
_timeout = new Timer(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
}
|
||||
|
||||
private void EventTimeout(object state)
|
||||
{
|
||||
var _ = StopEvent();
|
||||
}
|
||||
|
||||
private async void OnTimerTick(object state)
|
||||
{
|
||||
var potEmpty = PotEmptied;
|
||||
List<ulong> toAward = new List<ulong>();
|
||||
while (_toAward.TryDequeue(out var x))
|
||||
{
|
||||
toAward.Add(x);
|
||||
}
|
||||
|
||||
if (!toAward.Any())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await _cs.AddBulkAsync(toAward,
|
||||
toAward.Select(x => "GameStatus Event"),
|
||||
toAward.Select(x => _amount),
|
||||
gamble: true).ConfigureAwait(false);
|
||||
|
||||
if (_isPotLimited)
|
||||
{
|
||||
await _msg.ModifyAsync(m =>
|
||||
{
|
||||
m.Embed = GetEmbed(PotSize).Build();
|
||||
}, new RequestOptions() { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Log.Information("Awarded {0} users {1} currency.{2}",
|
||||
toAward.Count,
|
||||
_amount,
|
||||
_isPotLimited ? $" {PotSize} left." : "");
|
||||
|
||||
if (potEmpty)
|
||||
{
|
||||
var _ = StopEvent();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error in OnTimerTick in gamestatusevent");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartEvent()
|
||||
{
|
||||
_msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)).ConfigureAwait(false);
|
||||
await _client.SetGameAsync(_code).ConfigureAwait(false);
|
||||
_client.MessageDeleted += OnMessageDeleted;
|
||||
_client.MessageReceived += HandleMessage;
|
||||
_t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
private EmbedBuilder GetEmbed(long pot)
|
||||
{
|
||||
return _embedFunc(CurrencyEvent.Type.GameStatus, _opts, pot);
|
||||
}
|
||||
|
||||
private async Task OnMessageDeleted(Cacheable<IMessage, ulong> msg, ISocketMessageChannel _)
|
||||
{
|
||||
if (msg.Id == _msg.Id)
|
||||
{
|
||||
await StopEvent().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object stopLock = new object();
|
||||
public async Task StopEvent()
|
||||
{
|
||||
await Task.Yield();
|
||||
lock (stopLock)
|
||||
{
|
||||
if (Stopped)
|
||||
return;
|
||||
Stopped = true;
|
||||
_client.MessageDeleted -= OnMessageDeleted;
|
||||
_client.MessageReceived -= HandleMessage;
|
||||
_client.SetGameAsync(null);
|
||||
_t.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
_timeout?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
try { var _ = _msg.DeleteAsync(); } catch { }
|
||||
var os = OnEnded(_guild.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleMessage(SocketMessage msg)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
if (!(msg.Author is IGuildUser gu) // no unknown users, as they could be bots, or alts
|
||||
|| gu.IsBot // no bots
|
||||
|| msg.Content != _code // code has to be the same
|
||||
|| (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5) // no recently created accounts
|
||||
{
|
||||
return;
|
||||
}
|
||||
// there has to be money left in the pot
|
||||
// and the user wasn't rewarded
|
||||
if (_awardedUsers.Add(msg.Author.Id) && TryTakeFromPot())
|
||||
{
|
||||
_toAward.Enqueue(msg.Author.Id);
|
||||
if (_isPotLimited && PotSize < _amount)
|
||||
PotEmptied = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await msg.DeleteAsync(new RequestOptions()
|
||||
{
|
||||
RetryMode = RetryMode.AlwaysFail
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private readonly object potLock = new object();
|
||||
private bool TryTakeFromPot()
|
||||
{
|
||||
if (_isPotLimited)
|
||||
{
|
||||
lock (potLock)
|
||||
{
|
||||
if (PotSize < _amount)
|
||||
return false;
|
||||
|
||||
PotSize -= _amount;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common
|
||||
{
|
||||
public interface ICurrencyEvent
|
||||
{
|
||||
event Func<ulong, Task> OnEnded;
|
||||
Task StopEvent();
|
||||
Task StartEvent();
|
||||
}
|
||||
}
|
210
src/NadekoBot/Modules/Gambling/Common/Events/ReactionEvent.cs
Normal file
210
src/NadekoBot/Modules/Gambling/Common/Events/ReactionEvent.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common.Collections;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Modules.Gambling.Services;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Events
|
||||
{
|
||||
public class ReactionEvent : ICurrencyEvent
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IGuild _guild;
|
||||
private IUserMessage _msg;
|
||||
private IEmote _emote;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly long _amount;
|
||||
|
||||
private long PotSize { get; set; }
|
||||
public bool Stopped { get; private set; }
|
||||
public bool PotEmptied { get; private set; } = false;
|
||||
|
||||
private readonly Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> _embedFunc;
|
||||
private readonly bool _isPotLimited;
|
||||
private readonly ITextChannel _channel;
|
||||
private readonly ConcurrentHashSet<ulong> _awardedUsers = new ConcurrentHashSet<ulong>();
|
||||
private readonly ConcurrentQueue<ulong> _toAward = new ConcurrentQueue<ulong>();
|
||||
private readonly Timer _t;
|
||||
private readonly Timer _timeout = null;
|
||||
private readonly bool _noRecentlyJoinedServer;
|
||||
private readonly EventOptions _opts;
|
||||
private readonly GamblingConfig _config;
|
||||
|
||||
public event Func<ulong, Task> OnEnded;
|
||||
|
||||
public ReactionEvent(DiscordSocketClient client, ICurrencyService cs,
|
||||
SocketGuild g, ITextChannel ch, EventOptions opt, GamblingConfig config,
|
||||
Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> embedFunc)
|
||||
{
|
||||
_client = client;
|
||||
_guild = g;
|
||||
_cs = cs;
|
||||
_amount = opt.Amount;
|
||||
PotSize = opt.PotSize;
|
||||
_embedFunc = embedFunc;
|
||||
_isPotLimited = PotSize > 0;
|
||||
_channel = ch;
|
||||
_noRecentlyJoinedServer = false;
|
||||
_opts = opt;
|
||||
_config = config;
|
||||
|
||||
_t = new Timer(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2));
|
||||
if (_opts.Hours > 0)
|
||||
{
|
||||
_timeout = new Timer(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
}
|
||||
|
||||
private void EventTimeout(object state)
|
||||
{
|
||||
var _ = StopEvent();
|
||||
}
|
||||
|
||||
private async void OnTimerTick(object state)
|
||||
{
|
||||
var potEmpty = PotEmptied;
|
||||
List<ulong> toAward = new List<ulong>();
|
||||
while (_toAward.TryDequeue(out var x))
|
||||
{
|
||||
toAward.Add(x);
|
||||
}
|
||||
|
||||
if (!toAward.Any())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await _cs.AddBulkAsync(toAward,
|
||||
toAward.Select(x => "Reaction Event"),
|
||||
toAward.Select(x => _amount),
|
||||
gamble: true).ConfigureAwait(false);
|
||||
|
||||
if (_isPotLimited)
|
||||
{
|
||||
await _msg.ModifyAsync(m =>
|
||||
{
|
||||
m.Embed = GetEmbed(PotSize).Build();
|
||||
}, new RequestOptions() { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Log.Information("Awarded {0} users {1} currency.{2}",
|
||||
toAward.Count,
|
||||
_amount,
|
||||
_isPotLimited ? $" {PotSize} left." : "");
|
||||
|
||||
if (potEmpty)
|
||||
{
|
||||
var _ = StopEvent();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error adding bulk currency to users");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartEvent()
|
||||
{
|
||||
if (Emote.TryParse(_config.Currency.Sign, out var emote))
|
||||
{
|
||||
_emote = emote;
|
||||
}
|
||||
else
|
||||
{
|
||||
_emote = new Emoji(_config.Currency.Sign);
|
||||
}
|
||||
_msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)).ConfigureAwait(false);
|
||||
await _msg.AddReactionAsync(_emote).ConfigureAwait(false);
|
||||
_client.MessageDeleted += OnMessageDeleted;
|
||||
_client.ReactionAdded += HandleReaction;
|
||||
_t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
private EmbedBuilder GetEmbed(long pot)
|
||||
{
|
||||
return _embedFunc(CurrencyEvent.Type.Reaction, _opts, pot);
|
||||
}
|
||||
|
||||
private async Task OnMessageDeleted(Cacheable<IMessage, ulong> msg, ISocketMessageChannel _)
|
||||
{
|
||||
if (msg.Id == _msg.Id)
|
||||
{
|
||||
await StopEvent().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object stopLock = new object();
|
||||
public async Task StopEvent()
|
||||
{
|
||||
await Task.Yield();
|
||||
lock (stopLock)
|
||||
{
|
||||
if (Stopped)
|
||||
return;
|
||||
Stopped = true;
|
||||
_client.MessageDeleted -= OnMessageDeleted;
|
||||
_client.ReactionAdded -= HandleReaction;
|
||||
_t.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
_timeout?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
try { var _ = _msg.DeleteAsync(); } catch { }
|
||||
var os = OnEnded(_guild.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleReaction(Cacheable<IUserMessage, ulong> msg,
|
||||
ISocketMessageChannel ch, SocketReaction r)
|
||||
{
|
||||
var _ = Task.Run(() =>
|
||||
{
|
||||
if (_emote.Name != r.Emote.Name)
|
||||
return;
|
||||
var gu = (r.User.IsSpecified ? r.User.Value : null) as IGuildUser;
|
||||
if (gu == null // no unknown users, as they could be bots, or alts
|
||||
|| msg.Id != _msg.Id // same message
|
||||
|| gu.IsBot // no bots
|
||||
|| (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5 // no recently created accounts
|
||||
|| (_noRecentlyJoinedServer && // if specified, no users who joined the server in the last 24h
|
||||
(gu.JoinedAt == null || (DateTime.UtcNow - gu.JoinedAt.Value).TotalDays < 1))) // and no users for who we don't know when they joined
|
||||
{
|
||||
return;
|
||||
}
|
||||
// there has to be money left in the pot
|
||||
// and the user wasn't rewarded
|
||||
if (_awardedUsers.Add(r.UserId) && TryTakeFromPot())
|
||||
{
|
||||
_toAward.Enqueue(r.UserId);
|
||||
if (_isPotLimited && PotSize < _amount)
|
||||
PotEmptied = true;
|
||||
}
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private readonly object potLock = new object();
|
||||
private bool TryTakeFromPot()
|
||||
{
|
||||
if (_isPotLimited)
|
||||
{
|
||||
lock (potLock)
|
||||
{
|
||||
if (PotSize < _amount)
|
||||
return false;
|
||||
|
||||
PotSize -= _amount;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
293
src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs
Normal file
293
src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NadekoBot.Common.Yml;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common
|
||||
{
|
||||
public sealed class GamblingConfig
|
||||
{
|
||||
public GamblingConfig()
|
||||
{
|
||||
BetRoll = new BetRollConfig();
|
||||
WheelOfFortune = new WheelOfFortuneSettings();
|
||||
Waifu = new WaifuConfig();
|
||||
Currency = new CurrencyConfig();
|
||||
BetFlip = new BetFlipConfig();
|
||||
Generation = new GenerationConfig();
|
||||
Timely = new TimelyConfig();
|
||||
Decay = new DecayConfig();
|
||||
}
|
||||
|
||||
[Comment(@"DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
[Comment(@"Currency settings")]
|
||||
public CurrencyConfig Currency { get; set; }
|
||||
|
||||
[Comment(@"Minimum amount users can bet (>=0)")]
|
||||
public int MinBet { get; set; } = 0;
|
||||
|
||||
[Comment(@"Maximum amount users can bet
|
||||
Set 0 for unlimited")]
|
||||
public int MaxBet { get; set; } = 0;
|
||||
|
||||
[Comment(@"Settings for betflip command")]
|
||||
public BetFlipConfig BetFlip { get; set; }
|
||||
|
||||
[Comment(@"Settings for betroll command")]
|
||||
public BetRollConfig BetRoll { get; set; }
|
||||
|
||||
[Comment(@"Automatic currency generation settings.")]
|
||||
public GenerationConfig Generation { get; set; }
|
||||
|
||||
[Comment(@"Settings for timely command
|
||||
(letting people claim X amount of currency every Y hours)")]
|
||||
public TimelyConfig Timely { get; set; }
|
||||
|
||||
[Comment(@"How much will each user's owned currency decay over time.")]
|
||||
public DecayConfig Decay { get; set; }
|
||||
[Comment(@"Settings for Wheel Of Fortune command.")]
|
||||
public WheelOfFortuneSettings WheelOfFortune { get; set; }
|
||||
[Comment(@"Settings related to waifus")]
|
||||
public WaifuConfig Waifu { get; set; }
|
||||
|
||||
[Comment(@"Amount of currency selfhosters will get PER pledged dollar CENT.
|
||||
1 = 100 currency per $. Used almost exclusively on public nadeko.")]
|
||||
public decimal PatreonCurrencyPerCent { get; set; } = 1;
|
||||
|
||||
public class CurrencyConfig
|
||||
{
|
||||
[Comment(@"What is the emoji/character which represents the currency")]
|
||||
public string Sign { get; set; } = "🌸";
|
||||
|
||||
[Comment(@"What is the name of the currency")]
|
||||
public string Name { get; set; } = "Nadeko Flower";
|
||||
}
|
||||
|
||||
public class TimelyConfig
|
||||
{
|
||||
[Comment(@"How much currency will the users get every time they run .timely command
|
||||
setting to 0 or less will disable this feature")]
|
||||
public int Amount { get; set; } = 0;
|
||||
|
||||
[Comment(@"How often (in hours) can users claim currency with .timely command
|
||||
setting to 0 or less will disable this feature")]
|
||||
public int Cooldown { get; set; } = 24;
|
||||
}
|
||||
|
||||
public class BetFlipConfig
|
||||
{
|
||||
[Comment(@"Bet multiplier if user guesses correctly")]
|
||||
public decimal Multiplier { get; set; } = 1.95M;
|
||||
}
|
||||
|
||||
public class BetRollConfig
|
||||
{
|
||||
[Comment(@"When betroll is played, user will roll a number 0-100.
|
||||
This setting will describe which multiplier is used for when the roll is higher than the given number.
|
||||
Doesn't have to be ordered.")]
|
||||
public Pair[] Pairs { get; set; } = Array.Empty<Pair>();
|
||||
|
||||
public BetRollConfig()
|
||||
{
|
||||
Pairs = new BetRollConfig.Pair[]
|
||||
{
|
||||
new BetRollConfig.Pair(99, 10),
|
||||
new BetRollConfig.Pair(90, 4),
|
||||
new BetRollConfig.Pair(66, 2)
|
||||
};
|
||||
}
|
||||
|
||||
public class Pair
|
||||
{
|
||||
|
||||
public int WhenAbove { get; set; }
|
||||
|
||||
public float MultiplyBy { get; set; }
|
||||
|
||||
public Pair()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Pair(int threshold, int multiplier)
|
||||
{
|
||||
WhenAbove = threshold;
|
||||
MultiplyBy = multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class GenerationConfig
|
||||
{
|
||||
[Comment(@"when currency is generated, should it also have a random password
|
||||
associated with it which users have to type after the .pick command
|
||||
in order to get it")]
|
||||
public bool HasPassword { get; set; } = true;
|
||||
|
||||
[Comment(@"Every message sent has a certain % chance to generate the currency
|
||||
specify the percentage here (1 being 100%, 0 being 0% - for example
|
||||
default is 0.02, which is 2%")]
|
||||
public decimal Chance { get; set; } = 0.02M;
|
||||
|
||||
[Comment(@"How many seconds have to pass for the next message to have a chance to spawn currency")]
|
||||
public int GenCooldown { get; set; } = 10;
|
||||
|
||||
[Comment(@"Minimum amount of currency that can spawn")]
|
||||
public int MinAmount { get; set; } = 1;
|
||||
|
||||
[Comment(@"Maximum amount of currency that can spawn.
|
||||
Set to the same value as MinAmount to always spawn the same amount")]
|
||||
public int MaxAmount { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class DecayConfig
|
||||
{
|
||||
[Comment(@"Percentage of user's current currency which will be deducted every 24h.
|
||||
0 - 1 (1 is 100%, 0.5 50%, 0 disabled)")]
|
||||
public decimal Percent { get; set; } = 0;
|
||||
|
||||
[Comment(@"Maximum amount of user's currency that can decay at each interval. 0 for unlimited.")]
|
||||
public int MaxDecay { get; set; } = 0;
|
||||
|
||||
[Comment(@"Only users who have more than this amount will have their currency decay.")]
|
||||
public int MinThreshold { get; set; } = 99;
|
||||
|
||||
[Comment(@"How often, in hours, does the decay run. Default is 24 hours")]
|
||||
public int HourInterval { get; set; } = 24;
|
||||
}
|
||||
|
||||
public class WheelOfFortuneSettings
|
||||
{
|
||||
[Comment(@"Self-Explanatory. Has to have 8 values, otherwise the command won't work.")]
|
||||
public decimal[] Multipliers { get; set; }
|
||||
|
||||
public WheelOfFortuneSettings()
|
||||
{
|
||||
Multipliers = new decimal[]
|
||||
{
|
||||
1.7M,
|
||||
1.5M,
|
||||
0.2M,
|
||||
0.1M,
|
||||
0.3M,
|
||||
0.5M,
|
||||
1.2M,
|
||||
2.4M,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class WaifuConfig
|
||||
{
|
||||
[Comment(@"Minimum price a waifu can have")]
|
||||
public int MinPrice { get; set; } = 50;
|
||||
public MultipliersData Multipliers { get; set; } = new MultipliersData();
|
||||
|
||||
[Comment(@"List of items available for gifting.")]
|
||||
public List<WaifuItemModel> Items { get; set; } = new List<WaifuItemModel>();
|
||||
|
||||
public WaifuConfig()
|
||||
{
|
||||
Items = new List<WaifuItemModel>()
|
||||
{
|
||||
new WaifuItemModel("🥔", 5, "Potato"),
|
||||
new WaifuItemModel("🍪", 10, "Cookie"),
|
||||
new WaifuItemModel("🥖", 20, "Bread"),
|
||||
new WaifuItemModel("🍭", 30, "Lollipop"),
|
||||
new WaifuItemModel("🌹", 50, "Rose"),
|
||||
new WaifuItemModel("🍺", 70, "Beer"),
|
||||
new WaifuItemModel("🌮", 85, "Taco"),
|
||||
new WaifuItemModel("💌", 100, "LoveLetter"),
|
||||
new WaifuItemModel("🥛", 125, "Milk"),
|
||||
new WaifuItemModel("🍕", 150, "Pizza"),
|
||||
new WaifuItemModel("🍫", 200, "Chocolate"),
|
||||
new WaifuItemModel("🍦", 250, "Icecream"),
|
||||
new WaifuItemModel("🍣", 300, "Sushi"),
|
||||
new WaifuItemModel("🍚", 400, "Rice"),
|
||||
new WaifuItemModel("🍉", 500, "Watermelon"),
|
||||
new WaifuItemModel("🍱", 600, "Bento"),
|
||||
new WaifuItemModel("🎟", 800, "MovieTicket"),
|
||||
new WaifuItemModel("🍰", 1000, "Cake"),
|
||||
new WaifuItemModel("📔", 1500, "Book"),
|
||||
new WaifuItemModel("🐱", 2000, "Cat"),
|
||||
new WaifuItemModel("🐶", 2001, "Dog"),
|
||||
new WaifuItemModel("🐼", 2500, "Panda"),
|
||||
new WaifuItemModel("💄", 3000, "Lipstick"),
|
||||
new WaifuItemModel("👛", 3500, "Purse"),
|
||||
new WaifuItemModel("📱", 4000, "iPhone"),
|
||||
new WaifuItemModel("👗", 4500, "Dress"),
|
||||
new WaifuItemModel("💻", 5000, "Laptop"),
|
||||
new WaifuItemModel("🎻", 7500, "Violin"),
|
||||
new WaifuItemModel("🎹", 8000, "Piano"),
|
||||
new WaifuItemModel("🚗", 9000, "Car"),
|
||||
new WaifuItemModel("💍", 10000, "Ring"),
|
||||
new WaifuItemModel("🛳", 12000, "Ship"),
|
||||
new WaifuItemModel("🏠", 15000, "House"),
|
||||
new WaifuItemModel("🚁", 20000, "Helicopter"),
|
||||
new WaifuItemModel("🚀", 30000, "Spaceship"),
|
||||
new WaifuItemModel("🌕", 50000, "Moon")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public class MultipliersData
|
||||
{
|
||||
[Comment(@"Multiplier for waifureset. Default 150.
|
||||
Formula (at the time of writing this):
|
||||
price = (waifu_price * 1.25f) + ((number_of_divorces + changes_of_heart + 2) * WaifuReset) rounded up")]
|
||||
public int WaifuReset { get; set; } = 150;
|
||||
|
||||
[Comment(@"The minimum amount of currency that you have to pay
|
||||
in order to buy a waifu who doesn't have a crush on you.
|
||||
Default is 1.1
|
||||
Example: If a waifu is worth 100, you will have to pay at least 100 * NormalClaim currency to claim her.
|
||||
(100 * 1.1 = 110)")]
|
||||
public decimal NormalClaim { get; set; } = 1.1m;
|
||||
|
||||
[Comment(@"The minimum amount of currency that you have to pay
|
||||
in order to buy a waifu that has a crush on you.
|
||||
Default is 0.88
|
||||
Example: If a waifu is worth 100, you will have to pay at least 100 * CrushClaim currency to claim her.
|
||||
(100 * 0.88 = 88)")]
|
||||
public decimal CrushClaim { get; set; } = 0.88M;
|
||||
|
||||
[Comment(@"When divorcing a waifu, her new value will be her current value multiplied by this number.
|
||||
Default 0.75 (meaning will lose 25% of her value)")]
|
||||
public decimal DivorceNewValue { get; set; } = 0.75M;
|
||||
|
||||
[Comment(@"All gift prices will be multiplied by this number.
|
||||
Default 1 (meaning no effect)")]
|
||||
public decimal AllGiftPrices { get; set; } = 1.0M;
|
||||
|
||||
[Comment(@"What percentage of the value of the gift will a waifu gain when she's gifted.
|
||||
Default 0.95 (meaning 95%)
|
||||
Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095)")]
|
||||
public decimal GiftEffect { get; set; } = 0.95M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WaifuItemModel
|
||||
{
|
||||
public string ItemEmoji { get; set; }
|
||||
|
||||
public int Price { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public WaifuItemModel()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public WaifuItemModel(string itemEmoji, int price, string name)
|
||||
{
|
||||
ItemEmoji = itemEmoji;
|
||||
Price = price;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using Discord;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Modules;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Modules.Gambling.Services;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common
|
||||
{
|
||||
public abstract class GamblingModule<TService> : NadekoModule<TService>
|
||||
{
|
||||
private readonly Lazy<GamblingConfig> _lazyConfig;
|
||||
protected GamblingConfig _config => _lazyConfig.Value;
|
||||
protected string CurrencySign => _config.Currency.Sign;
|
||||
protected string CurrencyName => _config.Currency.Name;
|
||||
|
||||
protected GamblingModule(GamblingConfigService gambService)
|
||||
{
|
||||
_lazyConfig = new Lazy<GamblingConfig>(() => gambService.Data);
|
||||
}
|
||||
|
||||
private async Task<bool> InternalCheckBet(long amount)
|
||||
{
|
||||
if (amount < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (amount < _config.MinBet)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("min_bet_limit",
|
||||
Format.Bold(_config.MinBet.ToString()) + CurrencySign).ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
if (_config.MaxBet > 0 && amount > _config.MaxBet)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("max_bet_limit",
|
||||
Format.Bold(_config.MaxBet.ToString()) + CurrencySign).ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected Task<bool> CheckBetMandatory(long amount)
|
||||
{
|
||||
if (amount < 1)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
return InternalCheckBet(amount);
|
||||
}
|
||||
|
||||
protected Task<bool> CheckBetOptional(long amount)
|
||||
{
|
||||
if (amount == 0)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
return InternalCheckBet(amount);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class GamblingSubmodule<TService> : GamblingModule<TService>
|
||||
{
|
||||
protected GamblingSubmodule(GamblingConfigService gamblingConfService) : base(gamblingConfService)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
8
src/NadekoBot/Modules/Gambling/Common/Payout.cs
Normal file
8
src/NadekoBot/Modules/Gambling/Common/Payout.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common
|
||||
{
|
||||
public class Payout
|
||||
{
|
||||
public string User { get; set; }
|
||||
public int Amount { get; set; }
|
||||
}
|
||||
}
|
140
src/NadekoBot/Modules/Gambling/Common/RollDuelGame.cs
Normal file
140
src/NadekoBot/Modules/Gambling/Common/RollDuelGame.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Core.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common
|
||||
{
|
||||
public class RollDuelGame
|
||||
{
|
||||
public ulong P1 { get; }
|
||||
public ulong P2 { get; }
|
||||
|
||||
private readonly ulong _botId;
|
||||
|
||||
public long Amount { get; }
|
||||
|
||||
private readonly ICurrencyService _cs;
|
||||
|
||||
public enum State
|
||||
{
|
||||
Waiting,
|
||||
Running,
|
||||
Ended,
|
||||
}
|
||||
|
||||
public enum Reason
|
||||
{
|
||||
Normal,
|
||||
NoFunds,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
private readonly Timer _timeoutTimer;
|
||||
private readonly NadekoRandom _rng = new NadekoRandom();
|
||||
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
|
||||
|
||||
public event Func<RollDuelGame, Task> OnGameTick;
|
||||
public event Func<RollDuelGame, Reason, Task> OnEnded;
|
||||
|
||||
public List<(int, int)> Rolls { get; } = new List<(int, int)>();
|
||||
public State CurrentState { get; private set; }
|
||||
public ulong Winner { get; private set; }
|
||||
|
||||
public RollDuelGame(ICurrencyService cs, ulong botId, ulong p1, ulong p2, long amount)
|
||||
{
|
||||
this.P1 = p1;
|
||||
this.P2 = p2;
|
||||
this._botId = botId;
|
||||
this.Amount = amount;
|
||||
_cs = cs;
|
||||
|
||||
_timeoutTimer = new Timer(async delegate
|
||||
{
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (CurrentState != State.Waiting)
|
||||
return;
|
||||
CurrentState = State.Ended;
|
||||
await (OnEnded?.Invoke(this, Reason.Timeout)).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
}, null, TimeSpan.FromSeconds(15), TimeSpan.FromMilliseconds(-1));
|
||||
}
|
||||
|
||||
public async Task StartGame()
|
||||
{
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (CurrentState != State.Waiting)
|
||||
return;
|
||||
_timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
CurrentState = State.Running;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
|
||||
if(!await _cs.RemoveAsync(P1, "Roll Duel", Amount).ConfigureAwait(false))
|
||||
{
|
||||
await (OnEnded?.Invoke(this, Reason.NoFunds)).ConfigureAwait(false);
|
||||
CurrentState = State.Ended;
|
||||
return;
|
||||
}
|
||||
if(!await _cs.RemoveAsync(P2, "Roll Duel", Amount).ConfigureAwait(false))
|
||||
{
|
||||
await _cs.AddAsync(P1, "Roll Duel - refund", Amount).ConfigureAwait(false);
|
||||
await (OnEnded?.Invoke(this, Reason.NoFunds)).ConfigureAwait(false);
|
||||
CurrentState = State.Ended;
|
||||
return;
|
||||
}
|
||||
|
||||
int n1, n2;
|
||||
do
|
||||
{
|
||||
n1 = _rng.Next(0, 5);
|
||||
n2 = _rng.Next(0, 5);
|
||||
Rolls.Add((n1, n2));
|
||||
if (n1 != n2)
|
||||
{
|
||||
if (n1 > n2)
|
||||
{
|
||||
Winner = P1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Winner = P2;
|
||||
}
|
||||
var won = (long)(Amount * 2 * 0.98f);
|
||||
await _cs.AddAsync(Winner, "Roll Duel win", won)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await _cs.AddAsync(_botId, "Roll Duel fee", Amount * 2 - won)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
try { await (OnGameTick?.Invoke(this)).ConfigureAwait(false); } catch { }
|
||||
await Task.Delay(2500).ConfigureAwait(false);
|
||||
if (n1 != n2)
|
||||
break;
|
||||
}
|
||||
while (true);
|
||||
CurrentState = State.Ended;
|
||||
await (OnEnded?.Invoke(this, Reason.Normal)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public struct RollDuelChallenge
|
||||
{
|
||||
public ulong Player1 { get; set; }
|
||||
public ulong Player2 { get; set; }
|
||||
}
|
||||
}
|
16
src/NadekoBot/Modules/Gambling/Common/Waifu/AffinityTitle.cs
Normal file
16
src/NadekoBot/Modules/Gambling/Common/Waifu/AffinityTitle.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
|
||||
{
|
||||
public enum AffinityTitle
|
||||
{
|
||||
Pure,
|
||||
Faithful,
|
||||
Playful,
|
||||
Cheater,
|
||||
Tainted,
|
||||
Corrupted,
|
||||
Lewd,
|
||||
Sloot,
|
||||
Depraved,
|
||||
Harlot
|
||||
}
|
||||
}
|
18
src/NadekoBot/Modules/Gambling/Common/Waifu/ClaimTitle.cs
Normal file
18
src/NadekoBot/Modules/Gambling/Common/Waifu/ClaimTitle.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
|
||||
{
|
||||
public enum ClaimTitle
|
||||
{
|
||||
Lonely,
|
||||
Devoted,
|
||||
Rookie,
|
||||
Schemer,
|
||||
Dilettante,
|
||||
Intermediate,
|
||||
Seducer,
|
||||
Expert,
|
||||
Veteran,
|
||||
Incubis,
|
||||
Harem_King,
|
||||
Harem_God,
|
||||
}
|
||||
}
|
10
src/NadekoBot/Modules/Gambling/Common/Waifu/DivorceResult.cs
Normal file
10
src/NadekoBot/Modules/Gambling/Common/Waifu/DivorceResult.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
|
||||
{
|
||||
public enum DivorceResult
|
||||
{
|
||||
Success,
|
||||
SucessWithPenalty,
|
||||
NotYourWife,
|
||||
Cooldown
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
|
||||
{
|
||||
public enum WaifuClaimResult
|
||||
{
|
||||
Success,
|
||||
NotEnoughFunds,
|
||||
InsufficientAmount
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
|
||||
{
|
||||
public struct WaifuProfileTitle
|
||||
{
|
||||
public int Count { get; }
|
||||
public string Title { get; }
|
||||
|
||||
public WaifuProfileTitle(int count, string title)
|
||||
{
|
||||
Count = count;
|
||||
Title = title;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Core.Modules.Gambling.Common;
|
||||
using NadekoBot.Core.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.WheelOfFortune
|
||||
{
|
||||
public class WheelOfFortuneGame
|
||||
{
|
||||
public class Result
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public long Amount { get; set; }
|
||||
}
|
||||
|
||||
private readonly NadekoRandom _rng;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly long _bet;
|
||||
private readonly GamblingConfig _config;
|
||||
private readonly ulong _userId;
|
||||
|
||||
public WheelOfFortuneGame(ulong userId, long bet, GamblingConfig config, ICurrencyService cs)
|
||||
{
|
||||
_rng = new NadekoRandom();
|
||||
_cs = cs;
|
||||
_bet = bet;
|
||||
_config = config;
|
||||
_userId = userId;
|
||||
}
|
||||
|
||||
public async Task<Result> SpinAsync()
|
||||
{
|
||||
var result = _rng.Next(0, _config.WheelOfFortune.Multipliers.Length);
|
||||
|
||||
var amount = (long)(_bet * _config.WheelOfFortune.Multipliers[result]);
|
||||
|
||||
if (amount > 0)
|
||||
await _cs.AddAsync(_userId, "Wheel Of Fortune - won", amount, gamble: true).ConfigureAwait(false);
|
||||
|
||||
return new Result
|
||||
{
|
||||
Index = result,
|
||||
Amount = amount,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user