Restructured folders and project names, ci should be fixed

This commit is contained in:
Kwoth
2021-06-17 23:40:48 +02:00
parent 7aca29ae8a
commit 91ecf9ca41
788 changed files with 204 additions and 146 deletions

View File

@@ -0,0 +1,186 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Common;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Modules.Gambling.Common.AnimalRacing;
using NadekoBot.Core.Modules.Gambling.Services;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Gambling
{
// wth is this, needs full rewrite
public partial class Gambling
{
[Group]
public class AnimalRacingCommands : GamblingSubmodule<AnimalRaceService>
{
private readonly ICurrencyService _cs;
private readonly DiscordSocketClient _client;
private readonly GamesConfigService _gamesConf;
public AnimalRacingCommands(ICurrencyService cs, DiscordSocketClient client,
GamblingConfigService gamblingConf, GamesConfigService gamesConf) : base(gamblingConf)
{
_cs = cs;
_client = client;
_gamesConf = gamesConf;
}
private IUserMessage raceMessage = null;
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptionsAttribute(typeof(RaceOptions))]
public Task Race(params string[] args)
{
var (options, success) = OptionsParser.ParseFrom(new RaceOptions(), args);
var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle());
if (!_service.AnimalRaces.TryAdd(ctx.Guild.Id, ar))
return ctx.Channel.SendErrorAsync(GetText("animal_race"), GetText("animal_race_already_started"));
ar.Initialize();
var count = 0;
Task _client_MessageReceived(SocketMessage arg)
{
var _ = Task.Run(() =>
{
try
{
if (arg.Channel.Id == ctx.Channel.Id)
{
if (ar.CurrentPhase == AnimalRace.Phase.Running && ++count % 9 == 0)
{
raceMessage = null;
}
}
}
catch { }
});
return Task.CompletedTask;
}
Task Ar_OnEnded(AnimalRace race)
{
_client.MessageReceived -= _client_MessageReceived;
_service.AnimalRaces.TryRemove(ctx.Guild.Id, out _);
var winner = race.FinishedUsers[0];
if (race.FinishedUsers[0].Bet > 0)
{
return ctx.Channel.SendConfirmAsync(GetText("animal_race"),
GetText("animal_race_won_money", Format.Bold(winner.Username),
winner.Animal.Icon, (race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign));
}
else
{
return ctx.Channel.SendConfirmAsync(GetText("animal_race"),
GetText("animal_race_won", Format.Bold(winner.Username), winner.Animal.Icon));
}
}
ar.OnStartingFailed += Ar_OnStartingFailed;
ar.OnStateUpdate += Ar_OnStateUpdate;
ar.OnEnded += Ar_OnEnded;
ar.OnStarted += Ar_OnStarted;
_client.MessageReceived += _client_MessageReceived;
return ctx.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting", options.StartTime),
footer: GetText("animal_race_join_instr", Prefix));
}
private Task Ar_OnStarted(AnimalRace race)
{
if (race.Users.Count == race.MaxUsers)
return ctx.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_full"));
else
return ctx.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting_with_x", race.Users.Count));
}
private async Task Ar_OnStateUpdate(AnimalRace race)
{
var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|
{String.Join("\n", race.Users.Select(p =>
{
var index = race.FinishedUsers.IndexOf(p);
var extra = (index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}");
return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}";
}))}
|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|";
var msg = raceMessage;
if (msg == null)
raceMessage = await ctx.Channel.SendConfirmAsync(text)
.ConfigureAwait(false);
else
await msg.ModifyAsync(x => x.Embed = new EmbedBuilder()
.WithTitle(GetText("animal_race"))
.WithDescription(text)
.WithOkColor()
.Build())
.ConfigureAwait(false);
}
private Task Ar_OnStartingFailed(AnimalRace race)
{
_service.AnimalRaces.TryRemove(ctx.Guild.Id, out _);
return ReplyErrorLocalizedAsync("animal_race_failed");
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task JoinRace(ShmartNumber amount = default)
{
if (!await CheckBetOptional(amount).ConfigureAwait(false))
return;
if (!_service.AnimalRaces.TryGetValue(ctx.Guild.Id, out var ar))
{
await ReplyErrorLocalizedAsync("race_not_exist").ConfigureAwait(false);
return;
}
try
{
var user = await ar.JoinRace(ctx.User.Id, ctx.User.ToString(), amount)
.ConfigureAwait(false);
if (amount > 0)
await ctx.Channel.SendConfirmAsync(GetText("animal_race_join_bet", ctx.User.Mention, user.Animal.Icon, amount + CurrencySign)).ConfigureAwait(false);
else
await ctx.Channel.SendConfirmAsync(GetText("animal_race_join", ctx.User.Mention, user.Animal.Icon)).ConfigureAwait(false);
}
catch (ArgumentOutOfRangeException)
{
//ignore if user inputed an invalid amount
}
catch (AlreadyJoinedException)
{
// just ignore this
}
catch (AlreadyStartedException)
{
//ignore
}
catch (AnimalRaceFullException)
{
await ctx.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_full"))
.ConfigureAwait(false);
}
catch (NotEnoughFundsException)
{
await ctx.Channel.SendErrorAsync(GetText("not_enough", CurrencySign)).ConfigureAwait(false);
}
}
}
}
}

View File

@@ -0,0 +1,193 @@
using Discord;
using Discord.Commands;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Common;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Modules.Gambling.Common.Blackjack;
using NadekoBot.Core.Modules.Gambling.Services;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using System.Linq;
using System.Threading.Tasks;
using Serilog;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
public class BlackJackCommands : GamblingSubmodule<BlackJackService>
{
private readonly ICurrencyService _cs;
private readonly DbService _db;
private IUserMessage _msg;
public enum BjAction
{
Hit = int.MinValue,
Stand,
Double,
}
public BlackJackCommands(ICurrencyService cs, DbService db,
GamblingConfigService gamblingConf) : base(gamblingConf)
{
_cs = cs;
_db = db;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task BlackJack(ShmartNumber amount)
{
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
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).ConfigureAwait(false))
{
_service.Games.TryRemove(ctx.Channel.Id, out _);
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
return;
}
bj.StateUpdated += Bj_StateUpdated;
bj.GameEnded += Bj_GameEnded;
bj.Start();
await ReplyConfirmLocalizedAsync("bj_created").ConfigureAwait(false);
}
else
{
if (await bj.Join(ctx.User, amount).ConfigureAwait(false))
await ReplyConfirmLocalizedAsync("bj_joined").ConfigureAwait(false);
else
{
Log.Information($"{ctx.User} can't join a blackjack game as it's in " + bj.State.ToString() + " state already.");
}
}
await ctx.Message.DeleteAsync().ConfigureAwait(false);
}
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 != 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.Substring(0, x.Length - 1) + " "));
cStr += "\n" + string.Concat(c.Select(x => x.Last() + " "));
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle("BlackJack")
.AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr);
if (bj.CurrentUser != null)
{
embed.WithFooter($"Player to make a choice: {bj.CurrentUser.DiscordUser.ToString()}");
}
foreach (var p in bj.Players)
{
c = p.Cards.Select(x => x.GetEmojiString());
cStr = "-\t" + string.Concat(c.Select(x => x.Substring(0, x.Length - 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).ConfigureAwait(false);
}
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";
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public Task Hit() => InternalBlackJack(BjAction.Hit);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public Task Stand() => InternalBlackJack(BjAction.Stand);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public Task Double() => InternalBlackJack(BjAction.Double);
public 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).ConfigureAwait(false);
else if (a == BjAction.Stand)
await bj.Stand(ctx.User).ConfigureAwait(false);
else if (a == BjAction.Double)
{
if (!await bj.Double(ctx.User).ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
}
}
await ctx.Message.DeleteAsync().ConfigureAwait(false);
}
}
}
}

View 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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -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;
}
}
}

View 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,
};
}
}
}

View 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);
}
}
}

View 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;
}
}

View 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)];
}
}
}

View 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());
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@@ -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();
}
}

View 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;
}
}
}

View 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;
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Core.Modules.Gambling.Common
{
public class Payout
{
public string User { get; set; }
public int Amount { get; set; }
}
}

View 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; }
}
}

View 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
}
}

View 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,
}
}

View File

@@ -0,0 +1,10 @@
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
{
public enum DivorceResult
{
Success,
SucessWithPenalty,
NotYourWife,
Cooldown
}
}

View File

@@ -0,0 +1,9 @@
namespace NadekoBot.Core.Modules.Gambling.Common.Waifu
{
public enum WaifuClaimResult
{
Success,
NotEnoughFunds,
InsufficientAmount
}
}

View File

@@ -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;
}
}
}

View File

@@ -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,
};
}
}
}

View File

@@ -0,0 +1,401 @@
using CommandLine;
using NadekoBot.Common;
using NadekoBot.Core.Common;
using NadekoBot.Core.Services;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Gambling.Common.Connect4
{
public sealed class Connect4Game : IDisposable
{
public enum Phase
{
Joining, // waiting for second player to join
P1Move,
P2Move,
Ended,
}
public enum Field //temporary most likely
{
Empty,
P1,
P2,
}
public enum Result
{
Draw,
CurrentPlayerWon,
OtherPlayerWon,
}
public const int NumberOfColumns = 7;
public const int NumberOfRows = 6;
public Phase CurrentPhase { get; private set; } = Phase.Joining;
//state is bottom to top, left to right
private readonly Field[] _gameState = new Field[NumberOfRows * NumberOfColumns];
private readonly (ulong UserId, string Username)?[] _players = new(ulong, string)?[2];
public ImmutableArray<Field> GameState => _gameState.ToImmutableArray();
public ImmutableArray<(ulong UserId, string Username)?> Players => _players.ToImmutableArray();
public (ulong UserId, string Username) CurrentPlayer => CurrentPhase == Phase.P1Move
? _players[0].Value
: _players[1].Value;
public (ulong UserId, string Username) OtherPlayer => CurrentPhase == Phase.P2Move
? _players[0].Value
: _players[1].Value;
//public event Func<Connect4Game, Task> OnGameStarted;
public event Func<Connect4Game, Task> OnGameStateUpdated;
public event Func<Connect4Game, Task> OnGameFailedToStart;
public event Func<Connect4Game, Result, Task> OnGameEnded;
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private readonly Options _options;
private readonly ICurrencyService _cs;
private readonly NadekoRandom _rng;
private Timer _playerTimeoutTimer;
/* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
*/
public Connect4Game(ulong userId, string userName, Options options, ICurrencyService cs)
{
_players[0] = (userId, userName);
_options = options;
_cs = cs;
_rng = new NadekoRandom();
for (int i = 0; i < NumberOfColumns * NumberOfRows; i++)
{
_gameState[i] = Field.Empty;
}
}
public void Initialize()
{
if (CurrentPhase != Phase.Joining)
return;
var _ = Task.Run(async () =>
{
await Task.Delay(15000).ConfigureAwait(false);
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (_players[1] == null)
{
var __ = OnGameFailedToStart?.Invoke(this);
CurrentPhase = Phase.Ended;
await _cs.AddAsync(_players[0].Value.UserId, "Connect4-refund", _options.Bet, true).ConfigureAwait(false);
return;
}
}
finally { _locker.Release(); }
});
}
public async Task<bool> Join(ulong userId, string userName, int bet)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase != Phase.Joining) //can't join if its not a joining phase
return false;
if (_players[0].Value.UserId == userId) // same user can't join own game
return false;
if (bet != _options.Bet) // can't join if bet amount is not the same
return false;
if (!await _cs.RemoveAsync(userId, "Connect4-bet", bet, true).ConfigureAwait(false)) // user doesn't have enough money to gamble
return false;
if (_rng.Next(0, 2) == 0) //rolling from 0-1, if number is 0, join as first player
{
_players[1] = _players[0];
_players[0] = (userId, userName);
}
else //else join as a second player
_players[1] = (userId, userName);
CurrentPhase = Phase.P1Move; //start the game
_playerTimeoutTimer = new Timer(async state =>
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
EndGame(Result.OtherPlayerWon, OtherPlayer.UserId);
}
finally { _locker.Release(); }
}, null, TimeSpan.FromSeconds(_options.TurnTimer), TimeSpan.FromSeconds(_options.TurnTimer));
var __ = OnGameStateUpdated?.Invoke(this);
return true;
}
finally { _locker.Release(); }
}
public async Task<bool> Input(ulong userId, int inputCol)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
inputCol -= 1;
if (CurrentPhase == Phase.Ended || CurrentPhase == Phase.Joining)
return false;
if (!((_players[0].Value.UserId == userId && CurrentPhase == Phase.P1Move)
|| (_players[1].Value.UserId == userId && CurrentPhase == Phase.P2Move)))
return false;
if (inputCol < 0 || inputCol > NumberOfColumns) //invalid input
return false;
if (IsColumnFull(inputCol)) //can't play there event?
return false;
var start = NumberOfRows * inputCol;
for (int i = start; i < start + NumberOfRows; i++)
{
if (_gameState[i] == Field.Empty)
{
_gameState[i] = GetPlayerPiece(userId);
break;
}
}
//check winnning condition
// ok, i'll go from [0-2] in rows (and through all columns) and check upward if 4 are connected
for (int i = 0; i < NumberOfRows - 3; i++)
{
if (CurrentPhase == Phase.Ended)
break;
for (int j = 0; j < NumberOfColumns; j++)
{
if (CurrentPhase == Phase.Ended)
break;
var first = _gameState[i + j * NumberOfRows];
if (first != Field.Empty)
{
for (int k = 1; k < 4; k++)
{
var next = _gameState[i + k + j * NumberOfRows];
if (next == first)
{
if (k == 3)
EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
else
continue;
}
else break;
}
}
}
}
// i'll go [0-1] in columns (and through all rows) and check to the right if 4 are connected
for (int i = 0; i < NumberOfColumns - 3; i++)
{
if (CurrentPhase == Phase.Ended)
break;
for (int j = 0; j < NumberOfRows; j++)
{
if (CurrentPhase == Phase.Ended)
break;
var first = _gameState[j + i * NumberOfRows];
if (first != Field.Empty)
{
for (int k = 1; k < 4; k++)
{
var next = _gameState[j + (i + k) * NumberOfRows];
if (next == first)
if (k == 3)
EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
else
continue;
else break;
}
}
}
}
//need to check diagonal now
for (int col = 0; col < NumberOfColumns; col++)
{
if (CurrentPhase == Phase.Ended)
break;
for (int row = 0; row < NumberOfRows; row++)
{
if (CurrentPhase == Phase.Ended)
break;
var first = _gameState[row + col * NumberOfRows];
if (first != Field.Empty)
{
var same = 1;
//top left
for (int i = 1; i < 4; i++)
{
//while going top left, rows are increasing, columns are decreasing
var curRow = row + i;
var curCol = col - i;
//check if current values are in range
if (curRow >= NumberOfRows || curRow < 0)
break;
if (curCol < 0 || curCol >= NumberOfColumns)
break;
var cur = _gameState[curRow + curCol * NumberOfRows];
if (cur == first)
same++;
else break;
}
if (same == 4)
{
EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
break;
}
same = 1;
//top right
for (int i = 1; i < 4; i++)
{
//while going top right, rows are increasing, columns are increasing
var curRow = row + i;
var curCol = col + i;
//check if current values are in range
if (curRow >= NumberOfRows || curRow < 0)
break;
if (curCol < 0 || curCol >= NumberOfColumns)
break;
var cur = _gameState[curRow + curCol * NumberOfRows];
if (cur == first)
same++;
else break;
}
if (same == 4)
{
EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
break;
}
}
}
}
//check draw? if it's even possible
if (_gameState.All(x => x != Field.Empty))
{
EndGame(Result.Draw, null);
}
if (CurrentPhase != Phase.Ended)
{
if (CurrentPhase == Phase.P1Move)
CurrentPhase = Phase.P2Move;
else
CurrentPhase = Phase.P1Move;
ResetTimer();
}
var _ = OnGameStateUpdated?.Invoke(this);
return true;
}
finally { _locker.Release(); }
}
private void ResetTimer()
{
_playerTimeoutTimer.Change(TimeSpan.FromSeconds(_options.TurnTimer), TimeSpan.FromSeconds(_options.TurnTimer));
}
private void EndGame(Result result, ulong? winId)
{
if (CurrentPhase == Phase.Ended)
return;
var _ = OnGameEnded?.Invoke(this, result);
CurrentPhase = Phase.Ended;
if (result == Result.Draw)
{
_cs.AddAsync(CurrentPlayer.UserId, "Connect4-draw", this._options.Bet, true);
_cs.AddAsync(OtherPlayer.UserId, "Connect4-draw", this._options.Bet, true);
return;
}
if (winId != null)
_cs.AddAsync(winId.Value, "Connnect4-win", (long)(this._options.Bet * 1.98), true);
}
private Field GetPlayerPiece(ulong userId) => _players[0].Value.UserId == userId
? Field.P1
: Field.P2;
//column is full if there are no empty fields
private bool IsColumnFull(int column)
{
var start = NumberOfRows * column;
for (int i = start; i < start + NumberOfRows; i++)
{
if (_gameState[i] == Field.Empty)
return false;
}
return true;
}
public void Dispose()
{
OnGameFailedToStart = null;
OnGameStateUpdated = null;
OnGameEnded = null;
_playerTimeoutTimer?.Change(Timeout.Infinite, Timeout.Infinite);
}
public class Options : INadekoCommandOptions
{
public void NormalizeOptions()
{
if (TurnTimer < 5 || TurnTimer > 60)
TurnTimer = 15;
if (Bet < 0)
Bet = 0;
}
[Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. It has to be between 5 and 60. Default 15.")]
public int TurnTimer { get; set; } = 15;
[Option('b', "bet", Required = false, Default = 0, HelpText = "Amount you bet. Default 0.")]
public int Bet { get; set; } = 0;
}
}
}

View File

@@ -0,0 +1,210 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Common;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using NadekoBot.Modules.Gambling.Common.Connect4;
using NadekoBot.Modules.Gambling.Services;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NadekoBot.Core.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class Connect4Commands : GamblingSubmodule<GamblingService>
{
private readonly DiscordSocketClient _client;
private readonly ICurrencyService _cs;
private static readonly string[] numbers = new string[] { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:" };
public Connect4Commands(DiscordSocketClient client, ICurrencyService cs, GamblingConfigService gamb)
: base(gamb)
{
_client = client;
_cs = cs;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptionsAttribute(typeof(Connect4Game.Options))]
public async Task Connect4(params string[] args)
{
var (options, _) = OptionsParser.ParseFrom(new Connect4Game.Options(), args);
if (!await CheckBetOptional(options.Bet).ConfigureAwait(false))
return;
var newGame = new Connect4Game(ctx.User.Id, ctx.User.ToString(), options, _cs);
Connect4Game game;
if ((game = _service.Connect4Games.GetOrAdd(ctx.Channel.Id, newGame)) != newGame)
{
if (game.CurrentPhase != Connect4Game.Phase.Joining)
return;
newGame.Dispose();
//means game already exists, try to join
var joined = await game.Join(ctx.User.Id, ctx.User.ToString(), options.Bet).ConfigureAwait(false);
return;
}
if (options.Bet > 0)
{
if (!await _cs.RemoveAsync(ctx.User.Id, "Connect4-bet", options.Bet, true).ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
_service.Connect4Games.TryRemove(ctx.Channel.Id, out _);
game.Dispose();
return;
}
}
game.OnGameStateUpdated += Game_OnGameStateUpdated;
game.OnGameFailedToStart += Game_OnGameFailedToStart;
game.OnGameEnded += Game_OnGameEnded;
_client.MessageReceived += _client_MessageReceived;
game.Initialize();
if (options.Bet == 0)
{
await ReplyConfirmLocalizedAsync("connect4_created").ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalizedAsync("connect4_created_bet", options.Bet + CurrencySign).ConfigureAwait(false);
}
Task _client_MessageReceived(SocketMessage arg)
{
if (ctx.Channel.Id != arg.Channel.Id)
return Task.CompletedTask;
var _ = Task.Run(async () =>
{
bool success = false;
if (int.TryParse(arg.Content, out var col))
{
success = await game.Input(arg.Author.Id, col).ConfigureAwait(false);
}
if (success)
try { await arg.DeleteAsync().ConfigureAwait(false); } catch { }
else
{
if (game.CurrentPhase == Connect4Game.Phase.Joining
|| game.CurrentPhase == Connect4Game.Phase.Ended)
{
return;
}
RepostCounter++;
if (RepostCounter == 0)
try { msg = await ctx.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()).ConfigureAwait(false); } catch { }
}
});
return Task.CompletedTask;
}
Task Game_OnGameFailedToStart(Connect4Game arg)
{
if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose))
{
_client.MessageReceived -= _client_MessageReceived;
toDispose.Dispose();
}
return ErrorLocalizedAsync("connect4_failed_to_start");
}
Task Game_OnGameEnded(Connect4Game arg, Connect4Game.Result result)
{
if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose))
{
_client.MessageReceived -= _client_MessageReceived;
toDispose.Dispose();
}
string title;
if (result == Connect4Game.Result.CurrentPlayerWon)
{
title = GetText("connect4_won", Format.Bold(arg.CurrentPlayer.Username), Format.Bold(arg.OtherPlayer.Username));
}
else if (result == Connect4Game.Result.OtherPlayerWon)
{
title = GetText("connect4_won", Format.Bold(arg.OtherPlayer.Username), Format.Bold(arg.CurrentPlayer.Username));
}
else
title = GetText("connect4_draw");
return msg.ModifyAsync(x => x.Embed = new EmbedBuilder()
.WithTitle(title)
.WithDescription(GetGameStateText(game))
.WithOkColor()
.Build());
}
}
private IUserMessage msg;
private int _repostCounter = 0;
private int RepostCounter
{
get => _repostCounter;
set
{
if (value < 0 || value > 7)
_repostCounter = 0;
else _repostCounter = value;
}
}
private async Task Game_OnGameStateUpdated(Connect4Game game)
{
var embed = new EmbedBuilder()
.WithTitle($"{game.CurrentPlayer.Username} vs {game.OtherPlayer.Username}")
.WithDescription(GetGameStateText(game))
.WithOkColor();
if (msg == null)
msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
else
await msg.ModifyAsync(x => x.Embed = embed.Build()).ConfigureAwait(false);
}
private string GetGameStateText(Connect4Game game)
{
var sb = new StringBuilder();
if (game.CurrentPhase == Connect4Game.Phase.P1Move ||
game.CurrentPhase == Connect4Game.Phase.P2Move)
sb.AppendLine(GetText("connect4_player_to_move", Format.Bold(game.CurrentPlayer.Username)));
for (int i = Connect4Game.NumberOfRows; i > 0; i--)
{
for (int j = 0; j < Connect4Game.NumberOfColumns; j++)
{
var cur = game.GameState[i + (j * Connect4Game.NumberOfRows) - 1];
if (cur == Connect4Game.Field.Empty)
sb.Append("⚫"); //black circle
else if (cur == Connect4Game.Field.P1)
sb.Append("🔴"); //red circle
else
sb.Append("🔵"); //blue circle
}
sb.AppendLine();
}
for (int i = 0; i < Connect4Game.NumberOfColumns; i++)
{
sb.Append(numbers[i]);
}
return sb.ToString();
}
}
}
}

View File

@@ -0,0 +1,94 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Core.Common;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Modules.Gambling.Common.Events;
using System;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class CurrencyEventsCommands : GamblingSubmodule<CurrencyEventsService>
{
public enum OtherEvent
{
BotListUpvoters
}
public CurrencyEventsCommands(GamblingConfigService gamblingConf) : base(gamblingConf)
{
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptionsAttribute(typeof(EventOptions))]
[OwnerOnly]
public async Task EventStart(CurrencyEvent.Type ev, params string[] options)
{
var (opts, _) = OptionsParser.ParseFrom(new EventOptions(), options);
if (!await _service.TryCreateEventAsync(ctx.Guild.Id,
ctx.Channel.Id,
ev,
opts,
GetEmbed
).ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync("start_event_fail").ConfigureAwait(false);
return;
}
}
private EmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot)
{
switch (type)
{
case CurrencyEvent.Type.Reaction:
return new EmbedBuilder()
.WithOkColor()
.WithTitle(GetText("event_title", type.ToString()))
.WithDescription(GetReactionDescription(opts.Amount, currentPot))
.WithFooter(GetText("event_duration_footer", opts.Hours));
case CurrencyEvent.Type.GameStatus:
return new EmbedBuilder()
.WithOkColor()
.WithTitle(GetText("event_title", type.ToString()))
.WithDescription(GetGameStatusDescription(opts.Amount, currentPot))
.WithFooter(GetText("event_duration_footer", opts.Hours));
default:
break;
}
throw new ArgumentOutOfRangeException(nameof(type));
}
private string GetReactionDescription(long amount, long potSize)
{
string potSizeStr = Format.Bold(potSize == 0
? "∞" + CurrencySign
: potSize.ToString() + CurrencySign);
return GetText("new_reaction_event",
CurrencySign,
Format.Bold(amount + CurrencySign),
potSizeStr);
}
private string GetGameStatusDescription(long amount, long potSize)
{
string potSizeStr = Format.Bold(potSize == 0
? "∞" + CurrencySign
: potSize.ToString() + CurrencySign);
return GetText("new_gamestatus_event",
CurrencySign,
Format.Bold(amount + CurrencySign),
potSizeStr);
}
}
}
}

View File

@@ -0,0 +1,60 @@
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Modules.Gambling.Services;
using System.Threading.Tasks;
using Discord;
using NadekoBot.Extensions;
using System.Linq;
using Discord.Commands;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Common;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
public class CurrencyRaffleCommands : GamblingSubmodule<CurrencyRaffleService>
{
public enum Mixed { Mixed }
public CurrencyRaffleCommands(GamblingConfigService gamblingConfService) : base(gamblingConfService)
{
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public Task RaffleCur(Mixed _, ShmartNumber amount) =>
RaffleCur(amount, true);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task RaffleCur(ShmartNumber amount, bool mixed = false)
{
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
return;
async Task OnEnded(IUser arg, long won)
{
await ctx.Channel.SendConfirmAsync(GetText("rafflecur_ended", CurrencyName, Format.Bold(arg.ToString()), won + CurrencySign)).ConfigureAwait(false);
}
var res = await _service.JoinOrCreateGame(ctx.Channel.Id,
ctx.User, amount, mixed, OnEnded)
.ConfigureAwait(false);
if (res.Item1 != null)
{
await ctx.Channel.SendConfirmAsync(GetText("rafflecur", res.Item1.GameType.ToString()),
string.Join("\n", res.Item1.Users.Select(x => $"{x.DiscordUser} ({x.Amount})")),
footer: GetText("rafflecur_joined", ctx.User.ToString())).ConfigureAwait(false);
}
else
{
if (res.Item2 == CurrencyRaffleService.JoinErrorType.AlreadyJoinedOrInvalidAmount)
await ReplyErrorLocalizedAsync("rafflecur_already_joined").ConfigureAwait(false);
else if (res.Item2 == CurrencyRaffleService.JoinErrorType.NotEnoughCurrency)
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
}
}
}
}
}

View File

@@ -0,0 +1,230 @@
using Discord;
using Discord.Commands;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class DiceRollCommands : NadekoSubmodule
{
private static readonly Regex dndRegex = new Regex(@"^(?<n1>\d+)d(?<n2>\d+)(?:\+(?<add>\d+))?(?:\-(?<sub>\d+))?$", RegexOptions.Compiled);
private static readonly Regex fudgeRegex = new Regex(@"^(?<n1>\d+)d(?:F|f)$", RegexOptions.Compiled);
private static readonly char[] _fateRolls = { '-', ' ', '+' };
private readonly IImageCache _images;
public DiceRollCommands(IDataCache data)
{
_images = data.LocalImages;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Roll()
{
var rng = new NadekoRandom();
var gen = rng.Next(1, 101);
var num1 = gen / 10;
var num2 = gen % 10;
using (var img1 = GetDice(num1))
using (var img2 = GetDice(num2))
using (var img = new[] { img1, img2 }.Merge(out var format))
using (var ms = img.ToStream(format))
{
await ctx.Channel.SendFileAsync(ms,
$"dice.{format.FileExtensions.First()}",
Format.Bold(ctx.User.ToString()) + " " + GetText("dice_rolled", Format.Code(gen.ToString()))).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(1)]
public async Task Roll(int num)
{
await InternalRoll(num, true).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(1)]
public async Task Rolluo(int num = 1)
{
await InternalRoll(num, false).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(0)]
public async Task Roll(string arg)
{
await InternallDndRoll(arg, true).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(0)]
public async Task Rolluo(string arg)
{
await InternallDndRoll(arg, false).ConfigureAwait(false);
}
private async Task InternalRoll(int num, bool ordered)
{
if (num < 1 || num > 30)
{
await ReplyErrorLocalizedAsync("dice_invalid_number", 1, 30).ConfigureAwait(false);
return;
}
var rng = new NadekoRandom();
var dice = new List<Image<Rgba32>>(num);
var values = new List<int>(num);
for (var i = 0; i < num; i++)
{
var randomNumber = rng.Next(1, 7);
var toInsert = dice.Count;
if (ordered)
{
if (randomNumber == 6 || dice.Count == 0)
toInsert = 0;
else if (randomNumber != 1)
for (var j = 0; j < dice.Count; j++)
{
if (values[j] < randomNumber)
{
toInsert = j;
break;
}
}
}
else
{
toInsert = dice.Count;
}
dice.Insert(toInsert, GetDice(randomNumber));
values.Insert(toInsert, randomNumber);
}
using (var bitmap = dice.Merge(out var format))
using (var ms = bitmap.ToStream(format))
{
foreach (var d in dice)
{
d.Dispose();
}
await ctx.Channel.SendFileAsync(ms, $"dice.{format.FileExtensions.First()}",
Format.Bold(ctx.User.ToString()) + " " +
GetText("dice_rolled_num", Format.Bold(values.Count.ToString())) +
" " + GetText("total_average",
Format.Bold(values.Sum().ToString()),
Format.Bold((values.Sum() / (1.0f * values.Count)).ToString("N2")))).ConfigureAwait(false);
}
}
private async Task InternallDndRoll(string arg, bool ordered)
{
Match match;
if ((match = fudgeRegex.Match(arg)).Length != 0 &&
int.TryParse(match.Groups["n1"].ToString(), out int n1) &&
n1 > 0 && n1 < 500)
{
var rng = new NadekoRandom();
var rolls = new List<char>();
for (int i = 0; i < n1; i++)
{
rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]);
}
var embed = new EmbedBuilder().WithOkColor().WithDescription(ctx.User.Mention + " " + GetText("dice_rolled_num", Format.Bold(n1.ToString())))
.AddField(efb => efb.WithName(Format.Bold("Result"))
.WithValue(string.Join(" ", rolls.Select(c => Format.Code($"[{c}]")))));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
else if ((match = dndRegex.Match(arg)).Length != 0)
{
var rng = new NadekoRandom();
if (int.TryParse(match.Groups["n1"].ToString(), out n1) &&
int.TryParse(match.Groups["n2"].ToString(), out int n2) &&
n1 <= 50 && n2 <= 100000 && n1 > 0 && n2 > 0)
{
if (!int.TryParse(match.Groups["add"].Value, out int add))
add = 0;
if (!int.TryParse(match.Groups["sub"].Value, out int sub))
sub = 0;
var arr = new int[n1];
for (int i = 0; i < n1; i++)
{
arr[i] = rng.Next(1, n2 + 1);
}
var sum = arr.Sum();
var embed = new EmbedBuilder().WithOkColor().WithDescription(ctx.User.Mention + " " + GetText("dice_rolled_num", n1) + $"`1 - {n2}`")
.AddField(efb => efb.WithName(Format.Bold("Rolls"))
.WithValue(string.Join(" ", (ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x => Format.Code(x.ToString())))))
.AddField(efb => efb.WithName(Format.Bold("Sum"))
.WithValue(sum + " + " + add + " - " + sub + " = " + (sum + add - sub)));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task NRoll([Leftover] string range)
{
int rolled;
if (range.Contains("-"))
{
var arr = range.Split('-')
.Take(2)
.Select(int.Parse)
.ToArray();
if (arr[0] > arr[1])
{
await ReplyErrorLocalizedAsync("second_larger_than_first").ConfigureAwait(false);
return;
}
rolled = new NadekoRandom().Next(arr[0], arr[1] + 1);
}
else
{
rolled = new NadekoRandom().Next(0, int.Parse(range) + 1);
}
await ReplyConfirmLocalizedAsync("dice_rolled", Format.Bold(rolled.ToString())).ConfigureAwait(false);
}
private Image<Rgba32> GetDice(int num)
{
if (num < 0 || num > 10)
throw new ArgumentOutOfRangeException(nameof(num));
if (num == 10)
{
var images = _images.Dice;
using (var imgOne = Image.Load(images[1]))
using (var imgZero = Image.Load(images[0]))
{
return new[] { imgOne, imgZero }.Merge();
}
}
return Image.Load(_images.Dice[num]);
}
}
}
}

View File

@@ -0,0 +1,124 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Gambling.Common;
using Image = SixLabors.ImageSharp.Image;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using NadekoBot.Core.Services;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class DrawCommands : NadekoSubmodule
{
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new ConcurrentDictionary<IGuild, Deck>();
private readonly IImageCache _images;
public DrawCommands(IDataCache data)
{
_images = data.LocalImages;
}
private async Task<(Stream ImageStream, string ToSend)> InternalDraw(int num, ulong? guildId = null)
{
if (num < 1 || num > 10)
throw new ArgumentOutOfRangeException(nameof(num));
Deck cards = guildId == null ? new Deck() : _allDecks.GetOrAdd(ctx.Guild, (s) => new Deck());
var images = new List<Image<Rgba32>>();
var cardObjects = new List<Deck.Card>();
for (var i = 0; i < num; i++)
{
if (cards.CardPool.Count == 0 && i != 0)
{
try
{
await ReplyErrorLocalizedAsync("no_more_cards").ConfigureAwait(false);
}
catch
{
// ignored
}
break;
}
var currentCard = cards.Draw();
cardObjects.Add(currentCard);
images.Add(Image.Load(_images.GetCard(currentCard.ToString().ToLowerInvariant().Replace(' ', '_'))));
}
using (var img = images.Merge())
{
foreach (var i in images)
{
i.Dispose();
}
var toSend = $"{Format.Bold(ctx.User.ToString())}";
if (cardObjects.Count == 5)
toSend += $" drew `{Deck.GetHandValue(cardObjects)}`";
if (guildId != null)
toSend += "\n" + GetText("cards_left", Format.Bold(cards.CardPool.Count.ToString()));
return (img.ToStream(), toSend);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Draw(int num = 1)
{
if (num < 1)
num = 1;
if (num > 10)
num = 10;
var (ImageStream, ToSend) = await InternalDraw(num, ctx.Guild.Id).ConfigureAwait(false);
using (ImageStream)
{
await ctx.Channel.SendFileAsync(ImageStream, num + " cards.jpg", ToSend).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task DrawNew(int num = 1)
{
if (num < 1)
num = 1;
if (num > 10)
num = 10;
var (ImageStream, ToSend) = await InternalDraw(num).ConfigureAwait(false);
using (ImageStream)
{
await ctx.Channel.SendFileAsync(ImageStream, num + " cards.jpg", ToSend).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task DeckShuffle()
{
//var channel = (ITextChannel)ctx.Channel;
_allDecks.AddOrUpdate(ctx.Guild,
(g) => new Deck(),
(g, c) =>
{
c.Restart();
return c;
});
await ReplyConfirmLocalizedAsync("deck_reshuffled").ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,135 @@
using Discord;
using Discord.Commands;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Common;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using NadekoBot.Modules.Gambling.Services;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Core.Modules.Gambling.Services;
using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class FlipCoinCommands : GamblingSubmodule<GamblingService>
{
private readonly IImageCache _images;
private readonly ICurrencyService _cs;
private readonly DbService _db;
private static readonly NadekoRandom rng = new NadekoRandom();
public FlipCoinCommands(IDataCache data, ICurrencyService cs, DbService db,
GamblingConfigService gss) : base(gss)
{
_images = data.LocalImages;
_cs = cs;
_db = db;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Flip(int count = 1)
{
if (count > 10 || count < 1)
{
await ReplyErrorLocalizedAsync("flip_invalid", 10).ConfigureAwait(false);
return;
}
var headCount = 0;
var tailCount = 0;
var imgs = new Image<Rgba32>[count];
for (var i = 0; i < count; i++)
{
var headsArr = _images.Heads[rng.Next(0, _images.Heads.Count)];
var tailsArr = _images.Tails[rng.Next(0, _images.Tails.Count)];
if (rng.Next(0, 10) < 5)
{
imgs[i] = Image.Load(headsArr);
headCount++;
}
else
{
imgs[i] = Image.Load(tailsArr);
tailCount++;
}
}
using (var img = imgs.Merge(out var format))
using (var stream = img.ToStream(format))
{
foreach (var i in imgs)
{
i.Dispose();
}
var msg = count != 1
? Format.Bold(ctx.User.ToString()) + " " + GetText("flip_results", count, headCount, tailCount)
: Format.Bold(ctx.User.ToString()) + " " + GetText("flipped", headCount > 0
? Format.Bold(GetText("heads"))
: Format.Bold(GetText("tails")));
await ctx.Channel.SendFileAsync(stream, $"{count} coins.{format.FileExtensions.First()}", msg).ConfigureAwait(false);
}
}
public enum BetFlipGuess
{
H = 1,
Head = 1,
Heads = 1,
T = 2,
Tail = 2,
Tails = 2
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Betflip(ShmartNumber amount, BetFlipGuess guess)
{
if (!await CheckBetMandatory(amount).ConfigureAwait(false) || amount == 1)
return;
var removed = await _cs.RemoveAsync(ctx.User, "Betflip Gamble", amount, false, gamble: true).ConfigureAwait(false);
if (!removed)
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
return;
}
BetFlipGuess result;
Uri imageToSend;
var coins = _images.ImageUrls.Coins;
if (rng.Next(0, 1000) <= 499)
{
imageToSend = coins.Heads[rng.Next(0, coins.Heads.Length)];
result = BetFlipGuess.Heads;
}
else
{
imageToSend = coins.Tails[rng.Next(0, coins.Tails.Length)];
result = BetFlipGuess.Tails;
}
string str;
if (guess == result)
{
var toWin = (long)(amount * _config.BetFlip.Multiplier);
str = Format.Bold(ctx.User.ToString()) + " " + GetText("flip_guess", toWin + CurrencySign);
await _cs.AddAsync(ctx.User, "Betflip Gamble", toWin, false, gamble: true).ConfigureAwait(false);
}
else
{
str = ctx.User.ToString() + " " + GetText("better_luck");
}
await ctx.Channel.EmbedAsync(new EmbedBuilder()
.WithDescription(str)
.WithOkColor()
.WithImageUrl(imageToSend.ToString())).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,669 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Common;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
using NadekoBot.Modules.Gambling.Services;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NadekoBot.Core.Modules.Gambling.Services;
using NadekoBot.Core.Services.Database;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling : GamblingModule<GamblingService>
{
private readonly DbService _db;
private readonly ICurrencyService _cs;
private readonly IDataCache _cache;
private readonly DiscordSocketClient _client;
private readonly NumberFormatInfo _enUsCulture;
private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService;
public Gambling(DbService db, ICurrencyService currency,
IDataCache cache, DiscordSocketClient client,
DownloadTracker tracker, GamblingConfigService configService) : base(configService)
{
_db = db;
_cs = currency;
_cache = cache;
_client = client;
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0;
_enUsCulture.NumberGroupSeparator = "";
_tracker = tracker;
_configService = configService;
}
private string n(long cur) => cur.ToString("N", _enUsCulture);
public string GetCurrency(ulong id)
{
using (var uow = _db.GetDbContext())
{
return n(uow.DiscordUsers.GetUserCurrency(id));
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Economy()
{
var ec = _service.GetEconomy();
decimal onePercent = 0;
if (ec.Cash > 0)
{
onePercent = ec.OnePercent / (ec.Cash-ec.Bot); // This stops the top 1% from owning more than 100% of the money
// [21:03] Bob Page: Kinda remids me of US economy
}
var embed = new EmbedBuilder()
.WithTitle(GetText("economy_state"))
.AddField(GetText("currency_owned"), ((BigInteger)(ec.Cash - ec.Bot)) + CurrencySign)
.AddField(GetText("currency_one_percent"), (onePercent * 100).ToString("F2") + "%")
.AddField(GetText("currency_planted"), ((BigInteger)ec.Planted) + CurrencySign)
.AddField(GetText("owned_waifus_total"), ((BigInteger)ec.Waifus) + CurrencySign)
.AddField(GetText("bot_currency"), ec.Bot + CurrencySign)
.AddField(GetText("total"), ((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", _enUsCulture) + CurrencySign)
.WithOkColor();
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Timely()
{
var val = _config.Timely.Amount;
var period = _config.Timely.Cooldown;
if (val <= 0 || period <= 0)
{
await ReplyErrorLocalizedAsync("timely_none").ConfigureAwait(false);
return;
}
TimeSpan? rem;
if ((rem = _cache.AddTimelyClaim(ctx.User.Id, period)) != null)
{
await ReplyErrorLocalizedAsync("timely_already_claimed", rem?.ToString(@"dd\d\ hh\h\ mm\m\ ss\s")).ConfigureAwait(false);
return;
}
await _cs.AddAsync(ctx.User.Id, "Timely claim", val).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync("timely", n(val) + CurrencySign, period).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
public async Task TimelyReset()
{
_cache.RemoveAllTimelyClaims();
await ReplyConfirmLocalizedAsync("timely_reset").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
public async Task TimelySet(int amount, int period = 24)
{
if (amount < 0 || period < 0)
return;
_configService.ModifyConfig(gs =>
{
gs.Timely.Amount = amount;
gs.Timely.Cooldown = period;
});
if (amount == 0)
await ReplyConfirmLocalizedAsync("timely_set_none").ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync("timely_set", Format.Bold(n(amount) + CurrencySign), Format.Bold(period.ToString())).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Raffle([Leftover] IRole role = null)
{
role = role ?? ctx.Guild.EveryoneRole;
var members = (await role.GetMembersAsync().ConfigureAwait(false)).Where(u => u.Status != UserStatus.Offline);
var membersArray = members as IUser[] ?? members.ToArray();
if (membersArray.Length == 0)
{
return;
}
var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)];
await ctx.Channel.SendConfirmAsync("🎟 " + GetText("raffled_user"), $"**{usr.Username}#{usr.Discriminator}**", footer: $"ID: {usr.Id}").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task RaffleAny([Leftover] IRole role = null)
{
role = role ?? ctx.Guild.EveryoneRole;
var members = (await role.GetMembersAsync().ConfigureAwait(false));
var membersArray = members as IUser[] ?? members.ToArray();
if (membersArray.Length == 0)
{
return;
}
var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)];
await ctx.Channel.SendConfirmAsync("🎟 " + GetText("raffled_user"), $"**{usr.Username}#{usr.Discriminator}**", footer: $"ID: {usr.Id}").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(1)]
public async Task Cash([Leftover] IUser user = null)
{
user = user ?? ctx.User;
await ConfirmLocalizedAsync("has", Format.Bold(user.ToString()), $"{GetCurrency(user.Id)} {CurrencySign}").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(2)]
public Task CurrencyTransactions(int page = 1) =>
InternalCurrencyTransactions(ctx.User.Id, page);
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
[Priority(0)]
public Task CurrencyTransactions([Leftover] IUser usr) =>
InternalCurrencyTransactions(usr.Id, 1);
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
[Priority(1)]
public Task CurrencyTransactions(IUser usr, int page) =>
InternalCurrencyTransactions(usr.Id, page);
private async Task InternalCurrencyTransactions(ulong userId, int page)
{
if (--page < 0)
return;
var trs = new List<CurrencyTransaction>();
using (var uow = _db.GetDbContext())
{
trs = uow._context.CurrencyTransactions.GetPageFor(userId, page);
}
var embed = new EmbedBuilder()
.WithTitle(GetText("transactions",
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() ?? $"{userId}"))
.WithOkColor();
var desc = "";
foreach (var tr in trs)
{
var type = tr.Amount > 0 ? "🔵" : "🔴";
var date = Format.Code($"〖{tr.DateAdded:HH:mm yyyy-MM-dd}〗");
desc += $"\\{type} {date} {Format.Bold(n(tr.Amount))}\n\t{tr.Reason?.Trim()}\n";
}
embed.WithDescription(desc);
embed.WithFooter(GetText("page", page + 1));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(0)]
public async Task Cash(ulong userId)
{
await ReplyConfirmLocalizedAsync("has", Format.Code(userId.ToString()), $"{GetCurrency(userId)} {CurrencySign}").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task Give(ShmartNumber amount, IGuildUser receiver, [Leftover] string msg = null)
{
if (amount <= 0 || ctx.User.Id == receiver.Id || receiver.IsBot)
return;
var success = await _cs.RemoveAsync((IGuildUser)ctx.User, $"Gift to {receiver.Username} ({receiver.Id}).", amount, false).ConfigureAwait(false);
if (!success)
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
return;
}
await _cs.AddAsync(receiver, $"Gift from {ctx.User.Username} ({ctx.User.Id}) - {msg}.", amount, true).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync("gifted", n(amount) + CurrencySign, Format.Bold(receiver.ToString()), msg)
.ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task Give(ShmartNumber amount, [Leftover] IGuildUser receiver)
=> Give(amount, receiver, null);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(0)]
public Task Award(ShmartNumber amount, IGuildUser usr, [Leftover] string msg) =>
Award(amount, usr.Id, msg);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(1)]
public Task Award(ShmartNumber amount, [Leftover] IGuildUser usr) =>
Award(amount, usr.Id);
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
[Priority(2)]
public async Task Award(ShmartNumber amount, ulong usrId, [Leftover] string msg = null)
{
if (amount <= 0)
return;
await _cs.AddAsync(usrId,
$"Awarded by bot owner. ({ctx.User.Username}/{ctx.User.Id}) {(msg ?? "")}",
amount,
gamble: (ctx.Client.CurrentUser.Id != usrId)).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync("awarded", n(amount) + CurrencySign, $"<@{usrId}>").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(2)]
public async Task Award(ShmartNumber amount, [Leftover] IRole role)
{
var users = (await ctx.Guild.GetUsersAsync().ConfigureAwait(false))
.Where(u => u.GetRoles().Contains(role))
.ToList();
await _cs.AddBulkAsync(users.Select(x => x.Id),
users.Select(x => $"Awarded by bot owner to **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
users.Select(x => amount.Value),
gamble: true)
.ConfigureAwait(false);
await ReplyConfirmLocalizedAsync("mass_award",
n(amount) + CurrencySign,
Format.Bold(users.Count.ToString()),
Format.Bold(role.Name)).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(0)]
public async Task Take(ShmartNumber amount, [Leftover] IRole role)
{
var users = (await role.GetMembersAsync()).ToList();
await _cs.RemoveBulkAsync(users.Select(x => x.Id),
users.Select(x => $"Taken by bot owner from **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
users.Select(x => amount.Value),
gamble: true)
.ConfigureAwait(false);
await ReplyConfirmLocalizedAsync("mass_take",
n(amount) + CurrencySign,
Format.Bold(users.Count.ToString()),
Format.Bold(role.Name)).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(1)]
public async Task Take(ShmartNumber amount, [Leftover] IGuildUser user)
{
if (amount <= 0)
return;
if (await _cs.RemoveAsync(user, $"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})", amount,
gamble: (ctx.Client.CurrentUser.Id != user.Id)).ConfigureAwait(false))
await ReplyConfirmLocalizedAsync("take", n(amount) + CurrencySign, Format.Bold(user.ToString())).ConfigureAwait(false);
else
await ReplyErrorLocalizedAsync("take_fail", n(amount) + CurrencySign, Format.Bold(user.ToString()), CurrencySign).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
public async Task Take(ShmartNumber amount, [Leftover] ulong usrId)
{
if (amount <= 0)
return;
if (await _cs.RemoveAsync(usrId, $"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})", amount,
gamble: (ctx.Client.CurrentUser.Id != usrId)).ConfigureAwait(false))
await ReplyConfirmLocalizedAsync("take", amount + CurrencySign, $"<@{usrId}>").ConfigureAwait(false);
else
await ReplyErrorLocalizedAsync("take_fail", amount + CurrencySign, Format.Code(usrId.ToString()), CurrencySign).ConfigureAwait(false);
}
private IUserMessage rdMsg = null;
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task RollDuel(IUser u)
{
if (ctx.User.Id == u.Id)
return;
//since the challenge is created by another user, we need to reverse the ids
//if it gets removed, means challenge is accepted
if (_service.Duels.TryRemove((ctx.User.Id, u.Id), out var game))
{
await game.StartGame().ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task RollDuel(ShmartNumber amount, IUser u)
{
if (ctx.User.Id == u.Id)
return;
if (amount <= 0)
return;
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle(GetText("roll_duel"));
var game = new RollDuelGame(_cs, _client.CurrentUser.Id, ctx.User.Id, u.Id, amount);
//means challenge is just created
if (_service.Duels.TryGetValue((ctx.User.Id, u.Id), out var other))
{
if (other.Amount != amount)
{
await ReplyErrorLocalizedAsync("roll_duel_already_challenged").ConfigureAwait(false);
}
else
{
await RollDuel(u).ConfigureAwait(false);
}
return;
}
if (_service.Duels.TryAdd((u.Id, ctx.User.Id), game))
{
game.OnGameTick += Game_OnGameTick;
game.OnEnded += Game_OnEnded;
await ReplyConfirmLocalizedAsync("roll_duel_challenge",
Format.Bold(ctx.User.ToString()),
Format.Bold(u.ToString()),
Format.Bold(amount + CurrencySign))
.ConfigureAwait(false);
}
async Task Game_OnGameTick(RollDuelGame arg)
{
var rolls = arg.Rolls.Last();
embed.Description += $@"{Format.Bold(ctx.User.ToString())} rolled **{rolls.Item1}**
{Format.Bold(u.ToString())} rolled **{rolls.Item2}**
--
";
if (rdMsg == null)
{
rdMsg = await ctx.Channel.EmbedAsync(embed)
.ConfigureAwait(false);
}
else
{
await rdMsg.ModifyAsync(x =>
{
x.Embed = embed.Build();
}).ConfigureAwait(false);
}
}
async Task Game_OnEnded(RollDuelGame rdGame, RollDuelGame.Reason reason)
{
try
{
if (reason == RollDuelGame.Reason.Normal)
{
var winner = rdGame.Winner == rdGame.P1
? ctx.User
: u;
embed.Description += $"\n**{winner}** Won {n(((long)(rdGame.Amount * 2 * 0.98))) + CurrencySign}";
await rdMsg.ModifyAsync(x => x.Embed = embed.Build())
.ConfigureAwait(false);
}
else if (reason == RollDuelGame.Reason.Timeout)
{
await ReplyErrorLocalizedAsync("roll_duel_timeout").ConfigureAwait(false);
}
else if (reason == RollDuelGame.Reason.NoFunds)
{
await ReplyErrorLocalizedAsync("roll_duel_no_funds").ConfigureAwait(false);
}
}
finally
{
_service.Duels.TryRemove((u.Id, ctx.User.Id), out var _);
}
}
}
private async Task InternallBetroll(long amount)
{
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
return;
if (!await _cs.RemoveAsync(ctx.User, "Betroll Gamble", amount, false, gamble: true).ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
return;
}
var br = new Betroll(base._config.BetRoll);
var result = br.Roll();
var str = Format.Bold(ctx.User.ToString()) + Format.Code(GetText("roll", result.Roll));
if (result.Multiplier > 0)
{
var win = (long)(amount * result.Multiplier);
str += GetText("br_win",
n(win) + CurrencySign,
result.Threshold + (result.Roll == 100 ? " 👑" : ""));
await _cs.AddAsync(ctx.User, "Betroll Gamble",
win, false, gamble: true).ConfigureAwait(false);
}
else
{
str += GetText("better_luck");
}
await ctx.Channel.SendConfirmAsync(str).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public Task BetRoll(ShmartNumber amount)
=> InternallBetroll(amount);
[NadekoCommand, Usage, Description, Aliases]
[NadekoOptions(typeof(LbOpts))]
[Priority(0)]
public Task Leaderboard(params string[] args)
=> Leaderboard(1, args);
[NadekoCommand, Usage, Description, Aliases]
[NadekoOptions(typeof(LbOpts))]
[Priority(1)]
public async Task Leaderboard(int page = 1, params string[] args)
{
if (--page < 0)
return;
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
List<DiscordUser> cleanRichest = new List<DiscordUser>();
// it's pointless to have clean on dm context
if (Context.Guild is null)
{
opts.Clean = false;
}
if (opts.Clean)
{
var now = DateTime.UtcNow;
using (var uow = _db.GetDbContext())
{
cleanRichest = uow.DiscordUsers.GetTopRichest(_client.CurrentUser.Id, 10_000);
}
await Context.Channel.TriggerTypingAsync().ConfigureAwait(false);
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild).ConfigureAwait(false);
var sg = (SocketGuild)Context.Guild;
cleanRichest = cleanRichest.Where(x => sg.GetUser(x.UserId) != null)
.ToList();
}
else
{
using (var uow = _db.GetDbContext())
{
cleanRichest = uow.DiscordUsers.GetTopRichest(_client.CurrentUser.Id, 9, page).ToList();
}
}
await Context.SendPaginatedConfirmAsync(page, curPage =>
{
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle(CurrencySign + " " + GetText("leaderboard"));
List<DiscordUser> toSend;
if (!opts.Clean)
{
using (var uow = _db.GetDbContext())
{
toSend = uow.DiscordUsers.GetTopRichest(_client.CurrentUser.Id, 9, curPage);
}
}
else
{
toSend = cleanRichest.Skip(curPage * 9).Take(9).ToList();
}
if (!toSend.Any())
{
embed.WithDescription(GetText("no_user_on_this_page"));
return embed;
}
for (var i = 0; i < toSend.Count; i++)
{
var x = toSend[i];
var usrStr = x.ToString().TrimTo(20, true);
var j = i;
embed.AddField(efb => efb.WithName("#" + (9 * curPage + j + 1) + " " + usrStr)
.WithValue(n(x.CurrencyAmount) + " " + CurrencySign)
.WithIsInline(true));
}
return embed;
}, opts.Clean ? cleanRichest.Count() : 9000, 9, opts.Clean);
}
public enum RpsPick
{
R = 0,
Rock = 0,
Rocket = 0,
P = 1,
Paper = 1,
Paperclip = 1,
S = 2,
Scissors = 2
}
public enum RpsResult
{
Win,
Loss,
Draw,
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Rps(RpsPick pick, ShmartNumber amount = default)
{
long oldAmount = amount;
if (!await CheckBetOptional(amount).ConfigureAwait(false) || (amount == 1))
return;
string getRpsPick(RpsPick p)
{
switch (p)
{
case RpsPick.R:
return "🚀";
case RpsPick.P:
return "📎";
default:
return "✂️";
}
}
var embed = new EmbedBuilder();
var nadekoPick = (RpsPick)new NadekoRandom().Next(0, 3);
if (amount > 0)
{
if (!await _cs.RemoveAsync(ctx.User.Id,
"Rps-bet", amount, gamble: true).ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
return;
}
}
string msg;
if (pick == nadekoPick)
{
await _cs.AddAsync(ctx.User.Id,
"Rps-draw", amount, gamble: true).ConfigureAwait(false);
embed.WithOkColor();
msg = GetText("rps_draw", getRpsPick(pick));
}
else if ((pick == RpsPick.Paper && nadekoPick == RpsPick.Rock) ||
(pick == RpsPick.Rock && nadekoPick == RpsPick.Scissors) ||
(pick == RpsPick.Scissors && nadekoPick == RpsPick.Paper))
{
amount = (long)(amount * base._config.BetFlip.Multiplier);
await _cs.AddAsync(ctx.User.Id,
"Rps-win", amount, gamble: true).ConfigureAwait(false);
embed.WithOkColor();
embed.AddField(GetText("won"), n(amount));
msg = GetText("rps_win", ctx.User.Mention,
getRpsPick(pick), getRpsPick(nadekoPick));
}
else
{
embed.WithErrorColor();
amount = 0;
msg = GetText("rps_win", ctx.Client.CurrentUser.Mention, getRpsPick(nadekoPick),
getRpsPick(pick));
}
embed
.WithDescription(msg);
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using NadekoBot.Core.Services;
using System.Collections.Concurrent;
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
namespace NadekoBot.Modules.Gambling.Services
{
public class AnimalRaceService : INService, IUnloadableService
{
public ConcurrentDictionary<ulong, AnimalRace> AnimalRaces { get; } = new ConcurrentDictionary<ulong, AnimalRace>();
public Task Unload()
{
foreach (var kvp in AnimalRaces)
{
try { kvp.Value.Dispose(); } catch { }
}
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,11 @@
using NadekoBot.Core.Modules.Gambling.Common.Blackjack;
using NadekoBot.Core.Services;
using System.Collections.Concurrent;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public class BlackJackService : INService
{
public ConcurrentDictionary<ulong, Blackjack> Games { get; } = new ConcurrentDictionary<ulong, Blackjack>();
}
}

View File

@@ -0,0 +1,137 @@
using NadekoBot.Core.Services;
using NadekoBot.Core.Modules.Gambling.Common.Events;
using System.Collections.Concurrent;
using NadekoBot.Modules.Gambling.Common;
using Discord;
using Discord.WebSocket;
using System.Threading.Tasks;
using System;
using NadekoBot.Core.Services.Database.Models;
using System.Net.Http;
using Newtonsoft.Json;
using System.Linq;
using NadekoBot.Core.Modules.Gambling.Services;
using Serilog;
namespace NadekoBot.Modules.Gambling.Services
{
public class CurrencyEventsService : INService
{
public class VoteModel
{
public ulong User { get; set; }
public long Date { get; set; }
}
private readonly DiscordSocketClient _client;
private readonly ICurrencyService _cs;
private readonly IBotCredentials _creds;
private readonly IHttpClientFactory _http;
private readonly GamblingConfigService _configService;
private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events =
new ConcurrentDictionary<ulong, ICurrencyEvent>();
public CurrencyEventsService(DiscordSocketClient client,
IBotCredentials creds, ICurrencyService cs,
IHttpClientFactory http, GamblingConfigService configService)
{
_client = client;
_cs = cs;
_creds = creds;
_http = http;
_configService = configService;
if (_client.ShardId == 0)
{
Task t = BotlistUpvoteLoop();
}
}
private async Task BotlistUpvoteLoop()
{
if (string.IsNullOrWhiteSpace(_creds.VotesUrl))
return;
while (true)
{
await Task.Delay(TimeSpan.FromHours(1)).ConfigureAwait(false);
await TriggerVoteCheck().ConfigureAwait(false);
}
}
private async Task TriggerVoteCheck()
{
try
{
using (var req = new HttpRequestMessage(HttpMethod.Get, _creds.VotesUrl))
{
if (!string.IsNullOrWhiteSpace(_creds.VotesToken))
req.Headers.Add("Authorization", _creds.VotesToken);
using (var http = _http.CreateClient())
using (var res = await http.SendAsync(req).ConfigureAwait(false))
{
if (!res.IsSuccessStatusCode)
{
Log.Warning("Botlist API not reached.");
return;
}
var resStr = await res.Content.ReadAsStringAsync().ConfigureAwait(false);
var ids = JsonConvert.DeserializeObject<VoteModel[]>(resStr)
.Select(x => x.User)
.Distinct();
await _cs.AddBulkAsync(ids, ids.Select(x => "Voted - <https://discordbots.org/bot/nadeko/vote>"), ids.Select(x => 10L), true).ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error in TriggerVoteCheck");
}
}
public async Task<bool> TryCreateEventAsync(ulong guildId, ulong channelId, CurrencyEvent.Type type,
EventOptions opts, Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> embed)
{
SocketGuild g = _client.GetGuild(guildId);
SocketTextChannel ch = g?.GetChannel(channelId) as SocketTextChannel;
if (ch == null)
return false;
ICurrencyEvent ce;
if (type == CurrencyEvent.Type.Reaction)
{
ce = new ReactionEvent(_client, _cs, g, ch, opts, _configService.Data, embed);
}
else if (type == CurrencyEvent.Type.GameStatus)
{
ce = new GameStatusEvent(_client, _cs, g, ch, opts, embed);
}
else
{
return false;
}
var added = _events.TryAdd(guildId, ce);
if (added)
{
try
{
ce.OnEnded += OnEventEnded;
await ce.StartEvent().ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error starting event");
_events.TryRemove(guildId, out ce);
return false;
}
}
return added;
}
private Task OnEventEnded(ulong gid)
{
_events.TryRemove(gid, out _);
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,88 @@
using System.Threading.Tasks;
using NadekoBot.Core.Services;
using NadekoBot.Core.Modules.Gambling.Common;
using System.Threading;
using System.Linq;
using System.Collections.Generic;
using Discord;
using System;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public class CurrencyRaffleService : INService
{
public enum JoinErrorType
{
NotEnoughCurrency,
AlreadyJoinedOrInvalidAmount
}
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private readonly DbService _db;
private readonly ICurrencyService _cs;
public Dictionary<ulong, CurrencyRaffleGame> Games { get; } = new Dictionary<ulong, CurrencyRaffleGame>();
public CurrencyRaffleService(DbService db, ICurrencyService cs)
{
_db = db;
_cs = cs;
}
public async Task<(CurrencyRaffleGame, JoinErrorType?)> JoinOrCreateGame(ulong channelId, IUser user, long amount, bool mixed, Func<IUser, long, Task> onEnded)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
var newGame = false;
if (!Games.TryGetValue(channelId, out var crg))
{
newGame = true;
crg = new CurrencyRaffleGame(mixed
? CurrencyRaffleGame.Type.Mixed
: CurrencyRaffleGame.Type.Normal);
Games.Add(channelId, crg);
}
//remove money, and stop the game if this
// user created it and doesn't have the money
if (!await _cs.RemoveAsync(user.Id, "Currency Raffle Join", amount).ConfigureAwait(false))
{
if (newGame)
Games.Remove(channelId);
return (null, JoinErrorType.NotEnoughCurrency);
}
if (!crg.AddUser(user, amount))
{
await _cs.AddAsync(user.Id, "Curency Raffle Refund", amount).ConfigureAwait(false);
return (null, JoinErrorType.AlreadyJoinedOrInvalidAmount);
}
if (newGame)
{
var _t = Task.Run(async () =>
{
await Task.Delay(60000).ConfigureAwait(false);
await _locker.WaitAsync().ConfigureAwait(false);
try
{
var winner = crg.GetWinner();
var won = crg.Users.Sum(x => x.Amount);
await _cs.AddAsync(winner.DiscordUser.Id, "Currency Raffle Win",
won).ConfigureAwait(false);
Games.Remove(channelId, out _);
var oe = onEnded(winner.DiscordUser, won);
}
catch { }
finally { _locker.Release(); }
});
}
return (crg, null);
}
finally
{
_locker.Release();
}
}
}
}

View File

@@ -0,0 +1,42 @@
using NadekoBot.Core.Common;
using NadekoBot.Core.Common.Configs;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Services;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
{
public override string Name { get; } = "gambling";
private const string FilePath = "data/gambling.yml";
private static TypedKey<GamblingConfig> changeKey = new TypedKey<GamblingConfig>("config.gambling.updated");
public GamblingConfigService(IConfigSeria serializer, IPubSub pubSub)
: base(FilePath, serializer, pubSub, changeKey)
{
AddParsedProp("currency.name", gs => gs.Currency.Name, ConfigParsers.String, ConfigPrinters.ToString);
AddParsedProp("currency.sign", gs => gs.Currency.Sign, ConfigParsers.String, ConfigPrinters.ToString);
AddParsedProp("minbet", gs => gs.MinBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("maxbet", gs => gs.MaxBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("gen.min", gs => gs.Generation.MinAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1);
AddParsedProp("gen.max", gs => gs.Generation.MaxAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1);
AddParsedProp("gen.cd", gs => gs.Generation.GenCooldown, int.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("gen.chance", gs => gs.Generation.Chance, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1);
AddParsedProp("gen.has_pw", gs => gs.Generation.HasPassword, bool.TryParse, ConfigPrinters.ToString);
AddParsedProp("bf.multi", gs => gs.BetFlip.Multiplier, decimal.TryParse, ConfigPrinters.ToString, val => val >= 1);
AddParsedProp("waifu.min_price", gs => gs.Waifu.MinPrice, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("waifu.multi.reset", gs => gs.Waifu.Multipliers.WaifuReset, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("waifu.multi.crush_claim", gs => gs.Waifu.Multipliers.CrushClaim, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("waifu.multi.normal_claim", gs => gs.Waifu.Multipliers.NormalClaim, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("waifu.multi.divorce_value", gs => gs.Waifu.Multipliers.DivorceNewValue, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("waifu.multi.all_gifts", gs => gs.Waifu.Multipliers.AllGiftPrices, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("waifu.multi.gift_effect", gs => gs.Waifu.Multipliers.GiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("decay.percent", gs => gs.Decay.Percent, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1);
AddParsedProp("decay.maxdecay", gs => gs.Decay.MaxDecay, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("decay.threshold", gs => gs.Decay.MinThreshold, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
}
}
}

View File

@@ -0,0 +1,162 @@
using Discord.WebSocket;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Services;
using NadekoBot.Modules.Gambling.Common.Connect4;
using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Modules.Gambling.Services;
using Serilog;
namespace NadekoBot.Modules.Gambling.Services
{
public class GamblingService : INService
{
private readonly DbService _db;
private readonly ICurrencyService _cs;
private readonly NadekoBot _bot;
private readonly DiscordSocketClient _client;
private readonly IDataCache _cache;
private readonly GamblingConfigService _gss;
public ConcurrentDictionary<(ulong, ulong), RollDuelGame> Duels { get; } = new ConcurrentDictionary<(ulong, ulong), RollDuelGame>();
public ConcurrentDictionary<ulong, Connect4Game> Connect4Games { get; } = new ConcurrentDictionary<ulong, Connect4Game>();
private readonly Timer _decayTimer;
public GamblingService(DbService db, NadekoBot bot, ICurrencyService cs,
DiscordSocketClient client, IDataCache cache, GamblingConfigService gss)
{
_db = db;
_cs = cs;
_bot = bot;
_client = client;
_cache = cache;
_gss = gss;
if (_bot.Client.ShardId == 0)
{
_decayTimer = new Timer(_ =>
{
var config = _gss.Data;
var maxDecay = config.Decay.MaxDecay;
if (config.Decay.Percent <= 0 || config.Decay.Percent > 1 || maxDecay < 0)
return;
using (var uow = _db.GetDbContext())
{
var lastCurrencyDecay = _cache.GetLastCurrencyDecay();
if (DateTime.UtcNow - lastCurrencyDecay < TimeSpan.FromHours(config.Decay.HourInterval))
return;
Log.Information($"Decaying users' currency - decay: {config.Decay.Percent * 100}% " +
$"| max: {maxDecay} " +
$"| threshold: {config.Decay.MinThreshold}");
if (maxDecay == 0)
maxDecay = int.MaxValue;
uow._context.Database.ExecuteSqlInterpolated($@"
UPDATE DiscordUser
SET CurrencyAmount=
CASE WHEN
{maxDecay} > ROUND(CurrencyAmount * {config.Decay.Percent} - 0.5)
THEN
CurrencyAmount - ROUND(CurrencyAmount * {config.Decay.Percent} - 0.5)
ELSE
CurrencyAmount - {maxDecay}
END
WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentUser.Id};");
_cache.SetLastCurrencyDecay();
uow.SaveChanges();
}
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
//using (var uow = _db.UnitOfWork)
//{
// //refund all of the currency users had at stake in gambling games
// //at the time bot was restarted
// var stakes = uow._context.Set<Stake>()
// .ToArray();
// var userIds = stakes.Select(x => x.UserId).ToArray();
// var reasons = stakes.Select(x => "Stake-" + x.Source).ToArray();
// var amounts = stakes.Select(x => x.Amount).ToArray();
// _cs.AddBulkAsync(userIds, reasons, amounts, gamble: true).ConfigureAwait(false);
// foreach (var s in stakes)
// {
// _cs.AddAsync(s.UserId, "Stake-" + s.Source, s.Amount, gamble: true)
// .GetAwaiter()
// .GetResult();
// }
// uow._context.Set<Stake>().RemoveRange(stakes);
// uow.Complete();
// Log.Information("Refunded {0} users' stakes.", stakes.Length);
//}
}
public struct EconomyResult
{
public decimal Cash { get; set; }
public decimal Planted { get; set; }
public decimal Waifus { get; set; }
public decimal OnePercent { get; set; }
public long Bot { get; set; }
}
public EconomyResult GetEconomy()
{
if (_cache.TryGetEconomy(out var data))
{
try
{
return JsonConvert.DeserializeObject<EconomyResult>(data);
}
catch { }
}
decimal cash;
decimal onePercent;
decimal planted;
decimal waifus;
long bot;
using (var uow = _db.GetDbContext())
{
cash = uow.DiscordUsers.GetTotalCurrency();
onePercent = uow.DiscordUsers.GetTopOnePercentCurrency(_client.CurrentUser.Id);
planted = uow.PlantedCurrency.GetTotalPlanted();
waifus = uow.Waifus.GetTotalValue();
bot = uow.DiscordUsers.GetUserCurrency(_client.CurrentUser.Id);
}
var result = new EconomyResult
{
Cash = cash,
Planted = planted,
Bot = bot,
Waifus = waifus,
OnePercent = onePercent,
};
_cache.SetEconomy(JsonConvert.SerializeObject(result));
return result;
}
public Task<WheelOfFortuneGame.Result> WheelOfFortuneSpinAsync(ulong userId, long bet)
{
return new WheelOfFortuneGame(userId, bet, _gss.Data, _cs).SpinAsync();
}
}
}

View File

@@ -0,0 +1,43 @@
using System.Threading.Tasks;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public interface IShopService
{
/// <summary>
/// Changes the price of a shop item
/// </summary>
/// <param name="guildId">Id of the guild in which the shop is</param>
/// <param name="index">Index of the item</param>
/// <param name="newPrice">New item price</param>
/// <returns>Success status</returns>
Task<bool> ChangeEntryPriceAsync(ulong guildId, int index, int newPrice);
/// <summary>
/// Changes the name of a shop item
/// </summary>
/// <param name="guildId">Id of the guild in which the shop is</param>
/// <param name="index">Index of the item</param>
/// <param name="newName">New item name</param>
/// <returns>Success status</returns>
Task<bool> ChangeEntryNameAsync(ulong guildId, int index, string newName);
/// <summary>
/// Swaps indexes of 2 items in the shop
/// </summary>
/// <param name="guildId">Id of the guild in which the shop is</param>
/// <param name="index1">First entry's index</param>
/// <param name="index2">Second entry's index</param>
/// <returns>Whether swap was successful</returns>
Task<bool> SwapEntriesAsync(ulong guildId, int index1, int index2);
/// <summary>
/// Swaps indexes of 2 items in the shop
/// </summary>
/// <param name="guildId">Id of the guild in which the shop is</param>
/// <param name="fromIndex">Current index of the entry to move</param>
/// <param name="toIndex">Destination index of the entry</param>
/// <returns>Whether swap was successful</returns>
Task<bool> MoveEntryAsync(ulong guildId, int fromIndex, int toIndex);
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.Collections;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public class ShopService : IShopService
{
private readonly DbService _db;
public ShopService(DbService db)
{
_db = db;
}
private IndexedCollection<ShopEntry> GetEntriesInternal(IUnitOfWork uow, ulong guildId) =>
uow.GuildConfigs.ForId(
guildId,
set => set.Include(x => x.ShopEntries).ThenInclude(x => x.Items)
)
.ShopEntries
.ToIndexed();
public async Task<bool> ChangeEntryPriceAsync(ulong guildId, int index, int newPrice)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (newPrice <= 0)
throw new ArgumentOutOfRangeException(nameof(newPrice));
using var uow = _db.GetDbContext();
var entries = GetEntriesInternal(uow, guildId);
if (index >= entries.Count)
return false;
entries[index].Price = newPrice;
await uow.SaveChangesAsync();
return true;
}
public async Task<bool> ChangeEntryNameAsync(ulong guildId, int index, string newName)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (string.IsNullOrWhiteSpace(newName))
throw new ArgumentNullException(nameof(newName));
using var uow = _db.GetDbContext();
var entries = GetEntriesInternal(uow, guildId);
if (index >= entries.Count)
return false;
entries[index].Name = newName.TrimTo(100);
await uow.SaveChangesAsync();
return true;
}
public async Task<bool> SwapEntriesAsync(ulong guildId, int index1, int index2)
{
if (index1 < 0)
throw new ArgumentOutOfRangeException(nameof(index1));
if (index2 < 0)
throw new ArgumentOutOfRangeException(nameof(index2));
using var uow = _db.GetDbContext();
var entries = GetEntriesInternal(uow, guildId);
if (index1 >= entries.Count || index2 >= entries.Count || index1 == index2)
return false;
entries[index1].Index = index2;
entries[index2].Index = index1;
await uow.SaveChangesAsync();
return true;
}
public async Task<bool> MoveEntryAsync(ulong guildId, int fromIndex, int toIndex)
{
if (fromIndex < 0)
throw new ArgumentOutOfRangeException(nameof(fromIndex));
if (toIndex < 0)
throw new ArgumentOutOfRangeException(nameof(toIndex));
using var uow = _db.GetDbContext();
var entries = GetEntriesInternal(uow, guildId);
if (fromIndex >= entries.Count || toIndex >= entries.Count || fromIndex == toIndex)
return false;
var entry = entries[fromIndex];
entries.RemoveAt(fromIndex);
entries.Insert(toIndex, entry);
await uow.SaveChangesAsync();
return true;
}
}
}

View File

@@ -0,0 +1,376 @@
using Discord;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Database.Repositories;
using NadekoBot.Core.Services.Impl;
using NadekoBot.Extensions;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Core.Modules.Gambling.Services;
using Image = SixLabors.ImageSharp.Image;
using Color = SixLabors.ImageSharp.Color;
namespace NadekoBot.Modules.Gambling.Services
{
public class PlantPickService : INService
{
private readonly DbService _db;
private readonly IBotStrings _strings;
private readonly IImageCache _images;
private readonly FontProvider _fonts;
private readonly ICurrencyService _cs;
private readonly CommandHandler _cmdHandler;
private readonly NadekoRandom _rng;
private readonly DiscordSocketClient _client;
private readonly GamblingConfigService _gss;
public readonly ConcurrentHashSet<ulong> _generationChannels = new ConcurrentHashSet<ulong>();
//channelId/last generation
public ConcurrentDictionary<ulong, DateTime> LastGenerations { get; } = new ConcurrentDictionary<ulong, DateTime>();
private readonly SemaphoreSlim pickLock = new SemaphoreSlim(1, 1);
public PlantPickService(DbService db, CommandHandler cmd, IBotStrings strings,
IDataCache cache, FontProvider fonts, ICurrencyService cs,
CommandHandler cmdHandler, DiscordSocketClient client, GamblingConfigService gss)
{
_db = db;
_strings = strings;
_images = cache.LocalImages;
_fonts = fonts;
_cs = cs;
_cmdHandler = cmdHandler;
_rng = new NadekoRandom();
_client = client;
_gss = gss;
cmd.OnMessageNoTrigger += PotentialFlowerGeneration;
using (var uow = db.GetDbContext())
{
var guildIds = client.Guilds.Select(x => x.Id).ToList();
var configs = uow._context.Set<GuildConfig>()
.AsQueryable()
.Include(x => x.GenerateCurrencyChannelIds)
.Where(x => guildIds.Contains(x.GuildId))
.ToList();
_generationChannels = new ConcurrentHashSet<ulong>(configs
.SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId)));
}
}
private string GetText(ulong gid, string key, params object[] rep)
=> _strings.GetText(key, gid, rep);
public bool ToggleCurrencyGeneration(ulong gid, ulong cid)
{
bool enabled;
using (var uow = _db.GetDbContext())
{
var guildConfig = uow.GuildConfigs.ForId(gid, set => set.Include(gc => gc.GenerateCurrencyChannelIds));
var toAdd = new GCChannelId() { ChannelId = cid };
if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd))
{
guildConfig.GenerateCurrencyChannelIds.Add(toAdd);
_generationChannels.Add(cid);
enabled = true;
}
else
{
var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd));
if (toDelete != null)
{
uow._context.Remove(toDelete);
}
_generationChannels.TryRemove(cid);
enabled = false;
}
uow.SaveChanges();
}
return enabled;
}
public IEnumerable<GeneratingChannel> GetAllGeneratingChannels()
{
using (var uow = _db.GetDbContext())
{
var chs = uow.GuildConfigs.GetGeneratingChannels();
return chs;
}
}
/// <summary>
/// Get a random currency image stream, with an optional password sticked onto it.
/// </summary>
/// <param name="pass">Optional password to add to top left corner.</param>
/// <returns>Stream of the currency image</returns>
public Stream GetRandomCurrencyImage(string pass, out string extension)
{
// get a random currency image bytes
var rng = new NadekoRandom();
var curImg = _images.Currency[rng.Next(0, _images.Currency.Count)];
if (string.IsNullOrWhiteSpace(pass))
{
// determine the extension
using (var img = Image.Load(curImg, out var format))
{
extension = format.FileExtensions.FirstOrDefault() ?? "png";
}
// return the image
return curImg.ToStream();
}
// get the image stream and extension
var (s, ext) = AddPassword(curImg, pass);
// set the out extension parameter to the extension we've got
extension = ext;
// return the image
return s;
}
/// <summary>
/// Add a password to the image.
/// </summary>
/// <param name="curImg">Image to add password to.</param>
/// <param name="pass">Password to add to top left corner.</param>
/// <returns>Image with the password in the top left corner.</returns>
private (Stream, string) AddPassword(byte[] curImg, string pass)
{
// draw lower, it looks better
pass = pass.TrimTo(10, true).ToLowerInvariant();
using (var img = Image.Load<Rgba32>(curImg, out var format))
{
// choose font size based on the image height, so that it's visible
var font = _fonts.NotoSans.CreateFont(img.Height / 12, FontStyle.Bold);
img.Mutate(x =>
{
// measure the size of the text to be drawing
var size = TextMeasurer.Measure(pass, new RendererOptions(font, new PointF(0, 0)));
// fill the background with black, add 5 pixels on each side to make it look better
x.FillPolygon(Color.ParseHex("00000080"),
new PointF(0, 0),
new PointF(size.Width + 5, 0),
new PointF(size.Width + 5, size.Height + 10),
new PointF(0, size.Height + 10));
// draw the password over the background
x.DrawText(pass,
font,
SixLabors.ImageSharp.Color.White,
new PointF(0, 0));
});
// return image as a stream for easy sending
return (img.ToStream(format), format.FileExtensions.FirstOrDefault() ?? "png");
}
}
private Task PotentialFlowerGeneration(IUserMessage imsg)
{
var msg = imsg as SocketUserMessage;
if (msg == null || msg.Author.IsBot)
return Task.CompletedTask;
if (!(imsg.Channel is ITextChannel channel))
return Task.CompletedTask;
if (!_generationChannels.Contains(channel.Id))
return Task.CompletedTask;
var _ = Task.Run(async () =>
{
try
{
var config = _gss.Data;
var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue);
var rng = new NadekoRandom();
if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown) < lastGeneration) //recently generated in this channel, don't generate again
return;
var num = rng.Next(1, 101) + config.Generation.Chance * 100;
if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration))
{
var dropAmount = config.Generation.MinAmount;
var dropAmountMax = config.Generation.MaxAmount;
if (dropAmountMax > dropAmount)
dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax + 1);
if (dropAmount > 0)
{
var prefix = _cmdHandler.GetPrefix(channel.Guild.Id);
var toSend = dropAmount == 1
? GetText(channel.GuildId, "curgen_sn", config.Currency.Sign)
+ " " + GetText(channel.GuildId, "pick_sn", prefix)
: GetText(channel.GuildId, "curgen_pl", dropAmount, config.Currency.Sign)
+ " " + GetText(channel.GuildId, "pick_pl", prefix);
var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null;
IUserMessage sent;
using (var stream = GetRandomCurrencyImage(pw, out var ext))
{
sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend).ConfigureAwait(false);
}
await AddPlantToDatabase(channel.GuildId,
channel.Id,
_client.CurrentUser.Id,
sent.Id,
dropAmount,
pw).ConfigureAwait(false);
}
}
}
catch
{
}
});
return Task.CompletedTask;
}
/// <summary>
/// Generate a hexadecimal string from 1000 to ffff.
/// </summary>
/// <returns>A hexadecimal string from 1000 to ffff</returns>
private string GenerateCurrencyPassword()
{
// generate a number from 1000 to ffff
var num = _rng.Next(4096, 65536);
// convert it to hexadecimal
return num.ToString("x4");
}
public async Task<long> PickAsync(ulong gid, ITextChannel ch, ulong uid, string pass)
{
await pickLock.WaitAsync();
try
{
long amount;
ulong[] ids;
using (var uow = _db.GetDbContext())
{
// this method will sum all plants with that password,
// remove them, and get messageids of the removed plants
(amount, ids) = uow.PlantedCurrency.RemoveSumAndGetMessageIdsFor(ch.Id, pass);
if (amount > 0)
{
// give the picked currency to the user
await _cs.AddAsync(uid, "Picked currency", amount, gamble: false);
}
uow.SaveChanges();
}
try
{
// delete all of the plant messages which have just been picked
var _ = ch.DeleteMessagesAsync(ids);
}
catch { }
// return the amount of currency the user picked
return amount;
}
finally
{
pickLock.Release();
}
}
public async Task<ulong?> SendPlantMessageAsync(ulong gid, IMessageChannel ch, string user, long amount, string pass)
{
try
{
// get the text
var prefix = _cmdHandler.GetPrefix(gid);
var msgToSend = GetText(gid,
"planted",
Format.Bold(user),
amount + _gss.Data.Currency.Sign,
prefix);
if (amount > 1)
msgToSend += " " + GetText(gid, "pick_pl", prefix);
else
msgToSend += " " + GetText(gid, "pick_sn", prefix);
//get the image
using (var stream = GetRandomCurrencyImage(pass, out var ext))
{
// send it
var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend).ConfigureAwait(false);
// return sent message's id (in order to be able to delete it when it's picked)
return msg.Id;
}
}
catch
{
// if sending fails, return null as message id
return null;
}
}
public async Task<bool> PlantAsync(ulong gid, IMessageChannel ch, ulong uid, string user, long amount, string pass)
{
// normalize it - no more than 10 chars, uppercase
pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant();
// has to be either null or alphanumeric
if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric())
return false;
// remove currency from the user who's planting
if (await _cs.RemoveAsync(uid, "Planted currency", amount, gamble: false))
{
// try to send the message with the currency image
var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass).ConfigureAwait(false);
if (msgId == null)
{
// if it fails it will return null, if it returns null, refund
await _cs.AddAsync(uid, "Planted currency refund", amount, gamble: false);
return false;
}
// if it doesn't fail, put the plant in the database for other people to pick
await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass).ConfigureAwait(false);
return true;
}
// if user doesn't have enough currency, fail
return false;
}
private async Task AddPlantToDatabase(ulong gid, ulong cid, ulong uid, ulong mid, long amount, string pass)
{
using (var uow = _db.GetDbContext())
{
uow.PlantedCurrency.Add(new PlantedCurrency
{
Amount = amount,
GuildId = gid,
ChannelId = cid,
Password = pass,
UserId = uid,
MessageId = mid,
});
await uow.SaveChangesAsync();
}
}
}
}

View File

@@ -0,0 +1,519 @@
using Discord;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Modules.Gambling.Common.Waifu;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Database.Repositories;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling.Services
{
public class WaifuService : INService
{
public class FullWaifuInfo
{
public WaifuInfo Waifu { get; set; }
public IEnumerable<string> Claims { get; set; }
public int Divorces { get; set; }
}
private readonly DbService _db;
private readonly ICurrencyService _cs;
private readonly IDataCache _cache;
private readonly GamblingConfigService _gss;
public WaifuService(DbService db, ICurrencyService cs, IDataCache cache,
GamblingConfigService gss)
{
_db = db;
_cs = cs;
_cache = cache;
_gss = gss;
}
public async Task<bool> WaifuTransfer(IUser owner, ulong waifuId, IUser newOwner)
{
if (owner.Id == newOwner.Id || waifuId == newOwner.Id)
return false;
var settings = _gss.Data;
using (var uow = _db.GetDbContext())
{
var waifu = uow.Waifus.ByWaifuUserId(waifuId);
var ownerUser = uow.DiscordUsers.GetOrCreate(owner);
// owner has to be the owner of the waifu
if (waifu == null || waifu.ClaimerId != ownerUser.Id)
return false;
// if waifu likes the person, gotta pay the penalty
if (waifu.AffinityId == ownerUser.Id)
{
if (!await _cs.RemoveAsync(owner.Id,
"Waifu Transfer - affinity penalty",
(int)(waifu.Price * 0.6),
true))
{
// unable to pay 60% penalty
return false;
}
waifu.Price = (int)(waifu.Price * 0.7); // half of 60% = 30% price reduction
if (waifu.Price < settings.Waifu.MinPrice)
waifu.Price = settings.Waifu.MinPrice;
}
else // if not, pay 10% fee
{
if (!await _cs.RemoveAsync(owner.Id, "Waifu Transfer", waifu.Price / 10, gamble: true))
{
return false;
}
waifu.Price = (int) (waifu.Price * 0.95); // half of 10% = 5% price reduction
if (waifu.Price < settings.Waifu.MinPrice)
waifu.Price = settings.Waifu.MinPrice;
}
//new claimerId is the id of the new owner
var newOwnerUser = uow.DiscordUsers.GetOrCreate(newOwner);
waifu.ClaimerId = newOwnerUser.Id;
await uow.SaveChangesAsync();
}
return true;
}
public int GetResetPrice(IUser user)
{
var settings = _gss.Data;
using (var uow = _db.GetDbContext())
{
var waifu = uow.Waifus.ByWaifuUserId(user.Id);
if (waifu == null)
return settings.Waifu.MinPrice;
var divorces = uow._context.WaifuUpdates.Count(x => x.Old != null &&
x.Old.UserId == user.Id &&
x.UpdateType == WaifuUpdateType.Claimed &&
x.New == null);
var affs = uow._context.WaifuUpdates
.AsQueryable()
.Where(w => w.User.UserId == user.Id && w.UpdateType == WaifuUpdateType.AffinityChanged &&
w.New != null)
.ToList()
.GroupBy(x => x.New)
.Count();
return (int) Math.Ceiling(waifu.Price * 1.25f) +
((divorces + affs + 2) * settings.Waifu.Multipliers.WaifuReset);
}
}
public async Task<bool> TryReset(IUser user)
{
using (var uow = _db.GetDbContext())
{
var price = GetResetPrice(user);
if (!await _cs.RemoveAsync(user.Id, "Waifu Reset", price, gamble: true))
return false;
var affs = uow._context.WaifuUpdates
.AsQueryable()
.Where(w => w.User.UserId == user.Id
&& w.UpdateType == WaifuUpdateType.AffinityChanged
&& w.New != null);
var divorces = uow._context.WaifuUpdates
.AsQueryable()
.Where(x => x.Old != null &&
x.Old.UserId == user.Id &&
x.UpdateType == WaifuUpdateType.Claimed &&
x.New == null);
//reset changes of heart to 0
uow._context.WaifuUpdates.RemoveRange(affs);
//reset divorces to 0
uow._context.WaifuUpdates.RemoveRange(divorces);
var waifu = uow.Waifus.ByWaifuUserId(user.Id);
//reset price, remove items
//remove owner, remove affinity
waifu.Price = 50;
waifu.Items.Clear();
waifu.ClaimerId = null;
waifu.AffinityId = null;
//wives stay though
uow.SaveChanges();
}
return true;
}
public async Task<(WaifuInfo, bool, WaifuClaimResult)> ClaimWaifuAsync(IUser user, IUser target, int amount)
{
var settings = _gss.Data;
WaifuClaimResult result;
WaifuInfo w;
bool isAffinity;
using (var uow = _db.GetDbContext())
{
w = uow.Waifus.ByWaifuUserId(target.Id);
isAffinity = (w?.Affinity?.UserId == user.Id);
if (w == null)
{
var claimer = uow.DiscordUsers.GetOrCreate(user);
var waifu = uow.DiscordUsers.GetOrCreate(target);
if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true))
{
result = WaifuClaimResult.NotEnoughFunds;
}
else
{
uow.Waifus.Add(w = new WaifuInfo()
{
Waifu = waifu,
Claimer = claimer,
Affinity = null,
Price = amount
});
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = waifu,
Old = null,
New = claimer,
UpdateType = WaifuUpdateType.Claimed
});
result = WaifuClaimResult.Success;
}
}
else if (isAffinity && amount > w.Price * settings.Waifu.Multipliers.CrushClaim)
{
if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true))
{
result = WaifuClaimResult.NotEnoughFunds;
}
else
{
var oldClaimer = w.Claimer;
w.Claimer = uow.DiscordUsers.GetOrCreate(user);
w.Price = amount + (amount / 4);
result = WaifuClaimResult.Success;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldClaimer,
New = w.Claimer,
UpdateType = WaifuUpdateType.Claimed
});
}
}
else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity
{
if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true))
{
result = WaifuClaimResult.NotEnoughFunds;
}
else
{
var oldClaimer = w.Claimer;
w.Claimer = uow.DiscordUsers.GetOrCreate(user);
w.Price = amount;
result = WaifuClaimResult.Success;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldClaimer,
New = w.Claimer,
UpdateType = WaifuUpdateType.Claimed
});
}
}
else
result = WaifuClaimResult.InsufficientAmount;
await uow.SaveChangesAsync();
}
return (w, isAffinity, result);
}
public async Task<(DiscordUser, bool, TimeSpan?)> ChangeAffinityAsync(IUser user, IGuildUser target)
{
DiscordUser oldAff = null;
var success = false;
TimeSpan? remaining = null;
using (var uow = _db.GetDbContext())
{
var w = uow.Waifus.ByWaifuUserId(user.Id);
var newAff = target == null ? null : uow.DiscordUsers.GetOrCreate(target);
if (w?.Affinity?.UserId == target?.Id)
{
}
else if (!_cache.TryAddAffinityCooldown(user.Id, out remaining))
{
}
else if (w == null)
{
var thisUser = uow.DiscordUsers.GetOrCreate(user);
uow.Waifus.Add(new WaifuInfo()
{
Affinity = newAff,
Waifu = thisUser,
Price = 1,
Claimer = null
});
success = true;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = thisUser,
Old = null,
New = newAff,
UpdateType = WaifuUpdateType.AffinityChanged
});
}
else
{
if (w.Affinity != null)
oldAff = w.Affinity;
w.Affinity = newAff;
success = true;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldAff,
New = newAff,
UpdateType = WaifuUpdateType.AffinityChanged
});
}
await uow.SaveChangesAsync();
}
return (oldAff, success, remaining);
}
public IEnumerable<WaifuLbResult> GetTopWaifusAtPage(int page)
{
using (var uow = _db.GetDbContext())
{
return uow.Waifus.GetTop(9, page * 9);
}
}
public ulong GetWaifuUserId(ulong ownerId, string name)
{
using var uow = _db.GetDbContext();
return uow.Waifus.GetWaifuUserId(ownerId, name);
}
public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
{
DivorceResult result;
TimeSpan? remaining = null;
long amount = 0;
WaifuInfo w = null;
using (var uow = _db.GetDbContext())
{
w = uow.Waifus.ByWaifuUserId(targetId);
var now = DateTime.UtcNow;
if (w?.Claimer == null || w.Claimer.UserId != user.Id)
result = DivorceResult.NotYourWife;
else if (!_cache.TryAddDivorceCooldown(user.Id, out remaining))
{
result = DivorceResult.Cooldown;
}
else
{
amount = w.Price / 2;
if (w.Affinity?.UserId == user.Id)
{
await _cs.AddAsync(w.Waifu.UserId, "Waifu Compensation", amount, gamble: true);
w.Price = (int) Math.Floor(w.Price * _gss.Data.Waifu.Multipliers.DivorceNewValue);
result = DivorceResult.SucessWithPenalty;
}
else
{
await _cs.AddAsync(user.Id, "Waifu Refund", amount, gamble: true);
result = DivorceResult.Success;
}
var oldClaimer = w.Claimer;
w.Claimer = null;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldClaimer,
New = null,
UpdateType = WaifuUpdateType.Claimed
});
}
await uow.SaveChangesAsync();
}
return (w, result, amount, remaining);
}
public async Task<bool> GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj)
{
if (!await _cs.RemoveAsync(from, "Bought waifu item", itemObj.Price, gamble: true))
{
return false;
}
using (var uow = _db.GetDbContext())
{
var w = uow.Waifus.ByWaifuUserId(giftedWaifu.Id,
set => set.Include(x => x.Items)
.Include(x => x.Claimer));
if (w == null)
{
uow.Waifus.Add(w = new WaifuInfo()
{
Affinity = null,
Claimer = null,
Price = 1,
Waifu = uow.DiscordUsers.GetOrCreate(giftedWaifu),
});
}
w.Items.Add(new WaifuItem()
{
Name = itemObj.Name.ToLowerInvariant(),
ItemEmoji = itemObj.ItemEmoji,
});
if (w.Claimer?.UserId == from.Id)
{
w.Price += (int) (itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
}
else
{
w.Price += itemObj.Price / 2;
}
await uow.SaveChangesAsync();
}
return true;
}
public WaifuInfoStats GetFullWaifuInfoAsync(ulong targetId)
{
using (var uow = _db.GetDbContext())
{
var wi = uow.Waifus.GetWaifuInfo(targetId);
if (wi is null)
{
wi = new WaifuInfoStats
{
AffinityCount = 0,
AffinityName = null,
ClaimCount = 0,
ClaimerName = null,
Claims = new List<string>(),
Fans = new List<string>(),
DivorceCount = 0,
FullName = null,
Items = new List<WaifuItem>(),
Price = 1
};
}
return wi;
}
}
public WaifuInfoStats GetFullWaifuInfoAsync(IGuildUser target)
{
using (var uow = _db.GetDbContext())
{
var du = uow.DiscordUsers.GetOrCreate(target);
return GetFullWaifuInfoAsync(target.Id);
}
}
public string GetClaimTitle(int count)
{
ClaimTitle title;
if (count == 0)
title = ClaimTitle.Lonely;
else if (count == 1)
title = ClaimTitle.Devoted;
else if (count < 3)
title = ClaimTitle.Rookie;
else if (count < 6)
title = ClaimTitle.Schemer;
else if (count < 10)
title = ClaimTitle.Dilettante;
else if (count < 17)
title = ClaimTitle.Intermediate;
else if (count < 25)
title = ClaimTitle.Seducer;
else if (count < 35)
title = ClaimTitle.Expert;
else if (count < 50)
title = ClaimTitle.Veteran;
else if (count < 75)
title = ClaimTitle.Incubis;
else if (count < 100)
title = ClaimTitle.Harem_King;
else
title = ClaimTitle.Harem_God;
return title.ToString().Replace('_', ' ');
}
public string GetAffinityTitle(int count)
{
AffinityTitle title;
if (count < 1)
title = AffinityTitle.Pure;
else if (count < 2)
title = AffinityTitle.Faithful;
else if (count < 4)
title = AffinityTitle.Playful;
else if (count < 8)
title = AffinityTitle.Cheater;
else if (count < 11)
title = AffinityTitle.Tainted;
else if (count < 15)
title = AffinityTitle.Corrupted;
else if (count < 20)
title = AffinityTitle.Lewd;
else if (count < 25)
title = AffinityTitle.Sloot;
else if (count < 35)
title = AffinityTitle.Depraved;
else
title = AffinityTitle.Harlot;
return title.ToString().Replace('_', ' ');
}
public IReadOnlyList<WaifuItemModel> GetWaifuItems()
{
var conf = _gss.Data;
return _gss.Data.Waifu.Items
.Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name))
.ToList();
}
}
}

View File

@@ -0,0 +1,466 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Common.Collections;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Modules.Gambling.Services;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
using Serilog;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class ShopCommands : GamblingSubmodule<IShopService>
{
private readonly DbService _db;
private readonly ICurrencyService _cs;
public enum Role
{
Role
}
public enum List
{
List
}
public ShopCommands(DbService db, ICurrencyService cs, GamblingConfigService gamblingConf)
: base(gamblingConf)
{
_db = db;
_cs = cs;
}
private Task ShopInternalAsync(int page = 0)
{
if (page < 0)
throw new ArgumentOutOfRangeException(nameof(page));
using var uow = _db.GetDbContext();
var entries = uow.GuildConfigs.ForId(ctx.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries
.ToIndexed();
return ctx.SendPaginatedConfirmAsync(page, (curPage) =>
{
var theseEntries = entries.Skip(curPage * 9).Take(9).ToArray();
if (!theseEntries.Any())
return new EmbedBuilder().WithErrorColor()
.WithDescription(GetText("shop_none"));
var embed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("shop", CurrencySign));
for (int i = 0; i < theseEntries.Length; i++)
{
var entry = theseEntries[i];
embed.AddField(
$"#{curPage * 9 + i + 1} - {entry.Price}{CurrencySign}",
EntryToString(entry),
true);
}
return embed;
}, entries.Count, 9, true);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public Task Shop(int page = 1)
{
if (--page < 0)
return Task.CompletedTask;
return ShopInternalAsync(page);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Buy(int index)
{
index -= 1;
if (index < 0)
return;
ShopEntry entry;
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(ctx.Guild.Id, set => set
.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items));
var entries = new IndexedCollection<ShopEntry>(config.ShopEntries);
entry = entries.ElementAtOrDefault(index);
uow.SaveChanges();
}
if (entry == null)
{
await ReplyErrorLocalizedAsync("shop_item_not_found").ConfigureAwait(false);
return;
}
if (entry.Type == ShopEntryType.Role)
{
var guser = (IGuildUser)ctx.User;
var role = ctx.Guild.GetRole(entry.RoleId);
if (role == null)
{
await ReplyErrorLocalizedAsync("shop_role_not_found").ConfigureAwait(false);
return;
}
if (guser.RoleIds.Any(id => id == role.Id))
{
await ReplyErrorLocalizedAsync("shop_role_already_bought").ConfigureAwait(false);
return;
}
if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price).ConfigureAwait(false))
{
try
{
await guser.AddRoleAsync(role).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error adding shop role");
await _cs.AddAsync(ctx.User.Id, $"Shop error refund", entry.Price).ConfigureAwait(false);
await ReplyErrorLocalizedAsync("shop_role_purchase_error").ConfigureAwait(false);
return;
}
var profit = GetProfitAmount(entry.Price);
await _cs.AddAsync(entry.AuthorId, $"Shop sell item - {entry.Type}", profit).ConfigureAwait(false);
await _cs.AddAsync(ctx.Client.CurrentUser.Id, $"Shop sell item - cut", entry.Price - profit).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync("shop_role_purchase", Format.Bold(role.Name)).ConfigureAwait(false);
return;
}
else
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
return;
}
}
else if (entry.Type == ShopEntryType.List)
{
if (entry.Items.Count == 0)
{
await ReplyErrorLocalizedAsync("out_of_stock").ConfigureAwait(false);
return;
}
var item = entry.Items.ToArray()[new NadekoRandom().Next(0, entry.Items.Count)];
if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price).ConfigureAwait(false))
{
using (var uow = _db.GetDbContext())
{
var x = uow._context.Set<ShopEntryItem>().Remove(item);
uow.SaveChanges();
}
try
{
await (await ctx.User.GetOrCreateDMChannelAsync().ConfigureAwait(false))
.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle(GetText("shop_purchase", ctx.Guild.Name))
.AddField(efb => efb.WithName(GetText("item")).WithValue(item.Text).WithIsInline(false))
.AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("name")).WithValue(entry.Name).WithIsInline(true)))
.ConfigureAwait(false);
await _cs.AddAsync(entry.AuthorId,
$"Shop sell item - {entry.Name}",
GetProfitAmount(entry.Price)).ConfigureAwait(false);
}
catch
{
await _cs.AddAsync(ctx.User.Id,
$"Shop error refund - {entry.Name}",
entry.Price).ConfigureAwait(false);
using (var uow = _db.GetDbContext())
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.ForId(ctx.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries);
entry = entries.ElementAtOrDefault(index);
if (entry != null)
{
if (entry.Items.Add(item))
{
uow.SaveChanges();
}
}
}
await ReplyErrorLocalizedAsync("shop_buy_error").ConfigureAwait(false);
return;
}
await ReplyConfirmLocalizedAsync("shop_item_purchase").ConfigureAwait(false);
}
else
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
return;
}
}
}
private static long GetProfitAmount(int price) =>
(int)(Math.Ceiling(0.90 * price));
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ShopAdd(Role _, int price, [Leftover] IRole role)
{
var entry = new ShopEntry()
{
Name = "-",
Price = price,
Type = ShopEntryType.Role,
AuthorId = ctx.User.Id,
RoleId = role.Id,
RoleName = role.Name
};
using (var uow = _db.GetDbContext())
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.ForId(ctx.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries)
{
entry
};
uow.GuildConfigs.ForId(ctx.Guild.Id, set => set).ShopEntries = entries;
uow.SaveChanges();
}
await ctx.Channel.EmbedAsync(EntryToEmbed(entry)
.WithTitle(GetText("shop_item_add"))).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task ShopAdd(List _, int price, [Leftover]string name)
{
var entry = new ShopEntry()
{
Name = name.TrimTo(100),
Price = price,
Type = ShopEntryType.List,
AuthorId = ctx.User.Id,
Items = new HashSet<ShopEntryItem>(),
};
using (var uow = _db.GetDbContext())
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.ForId(ctx.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries)
{
entry
};
uow.GuildConfigs.ForId(ctx.Guild.Id, set => set).ShopEntries = entries;
uow.SaveChanges();
}
await ctx.Channel.EmbedAsync(EntryToEmbed(entry)
.WithTitle(GetText("shop_item_add"))).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task ShopListAdd(int index, [Leftover] string itemText)
{
index -= 1;
if (index < 0)
return;
var item = new ShopEntryItem()
{
Text = itemText
};
ShopEntry entry;
bool rightType = false;
bool added = false;
using (var uow = _db.GetDbContext())
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.ForId(ctx.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries);
entry = entries.ElementAtOrDefault(index);
if (entry != null && (rightType = (entry.Type == ShopEntryType.List)))
{
if (added = entry.Items.Add(item))
{
uow.SaveChanges();
}
}
}
if (entry == null)
await ReplyErrorLocalizedAsync("shop_item_not_found").ConfigureAwait(false);
else if (!rightType)
await ReplyErrorLocalizedAsync("shop_item_wrong_type").ConfigureAwait(false);
else if (added == false)
await ReplyErrorLocalizedAsync("shop_list_item_not_unique").ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync("shop_list_item_added").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task ShopRemove(int index)
{
index -= 1;
if (index < 0)
return;
ShopEntry removed;
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigs.ForId(ctx.Guild.Id, set => set
.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items));
var entries = new IndexedCollection<ShopEntry>(config.ShopEntries);
removed = entries.ElementAtOrDefault(index);
if (removed != null)
{
uow._context.RemoveRange(removed.Items);
uow._context.Remove(removed);
uow.SaveChanges();
}
}
if (removed == null)
await ReplyErrorLocalizedAsync("shop_item_not_found").ConfigureAwait(false);
else
await ctx.Channel.EmbedAsync(EntryToEmbed(removed)
.WithTitle(GetText("shop_item_rm"))).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task ShopChangePrice(int index, int price)
{
if (--index < 0 || price <= 0)
return;
var succ = await _service.ChangeEntryPriceAsync(Context.Guild.Id, index, price);
if (succ)
{
await ShopInternalAsync(index / 9);
await ctx.OkAsync();
}
else
{
await ctx.ErrorAsync();
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task ShopChangeName(int index, [Leftover] string newName)
{
if (--index < 0 || string.IsNullOrWhiteSpace(newName))
return;
var succ = await _service.ChangeEntryNameAsync(Context.Guild.Id, index, newName);
if (succ)
{
await ShopInternalAsync(index / 9);
await ctx.OkAsync();
}
else
{
await ctx.ErrorAsync();
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task ShopSwap(int index1, int index2)
{
if (--index1 < 0 || --index2 < 0 || index1 == index2)
return;
var succ = await _service.SwapEntriesAsync(Context.Guild.Id, index1, index2);
if (succ)
{
await ShopInternalAsync(index1 / 9);
await ctx.OkAsync();
}
else
{
await ctx.ErrorAsync();
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task ShopMove(int fromIndex, int toIndex)
{
if (--fromIndex < 0 || --toIndex < 0 || fromIndex == toIndex)
return;
var succ = await _service.MoveEntryAsync(Context.Guild.Id, fromIndex, toIndex);
if (succ)
{
await ShopInternalAsync(toIndex / 9);
await ctx.OkAsync();
}
else
{
await ctx.ErrorAsync();
}
}
public EmbedBuilder EntryToEmbed(ShopEntry entry)
{
var embed = new EmbedBuilder().WithOkColor();
if (entry.Type == ShopEntryType.Role)
return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"))).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("type")).WithValue(entry.Type.ToString()).WithIsInline(true));
else if (entry.Type == ShopEntryType.List)
return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(entry.Name).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("type")).WithValue(GetText("random_unique_item")).WithIsInline(true));
//else if (entry.Type == ShopEntryType.Infinite_List)
// return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(entry.RoleName))).WithIsInline(true))
// .AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
// .AddField(efb => efb.WithName(GetText("type")).WithValue(entry.Type.ToString()).WithIsInline(true));
else return null;
}
public string EntryToString(ShopEntry entry)
{
if (entry.Type == ShopEntryType.Role)
{
return GetText("shop_role", Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"));
}
else if (entry.Type == ShopEntryType.List)
{
return GetText("unique_items_left", entry.Items.Count) + "\n" + entry.Name;
}
//else if (entry.Type == ShopEntryType.Infinite_List)
//{
//}
return "";
}
}
}
}

View File

@@ -0,0 +1,234 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Common;
using NadekoBot.Core.Modules.Gambling.Services;
using Image = SixLabors.ImageSharp.Image;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class SlotCommands : GamblingSubmodule<GamblingService>
{
private static long _totalBet;
private static long _totalPaidOut;
private static readonly HashSet<ulong> _runningUsers = new HashSet<ulong>();
//here is a payout chart
//https://lh6.googleusercontent.com/-i1hjAJy_kN4/UswKxmhrbPI/AAAAAAAAB1U/82wq_4ZZc-Y/DE6B0895-6FC1-48BE-AC4F-14D1B91AB75B.jpg
//thanks to judge for helping me with this
private readonly IImageCache _images;
private readonly ICurrencyService _cs;
public SlotCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gamb) : base(gamb)
{
_images = data.LocalImages;
_cs = cs;
}
public sealed class SlotMachine
{
public const int MaxValue = 5;
static readonly List<Func<int[], int>> _winningCombos = new List<Func<int[], int>>()
{
//three flowers
(arr) => arr.All(a=>a==MaxValue) ? 30 : 0,
//three of the same
(arr) => !arr.Any(a => a != arr[0]) ? 10 : 0,
//two flowers
(arr) => arr.Count(a => a == MaxValue) == 2 ? 4 : 0,
//one flower
(arr) => arr.Any(a => a == MaxValue) ? 1 : 0,
};
public static SlotResult Pull()
{
var numbers = new int[3];
for (var i = 0; i < numbers.Length; i++)
{
numbers[i] = new NadekoRandom().Next(0, MaxValue + 1);
}
var multi = 0;
foreach (var t in _winningCombos)
{
multi = t(numbers);
if (multi != 0)
break;
}
return new SlotResult(numbers, multi);
}
public struct SlotResult
{
public int[] Numbers { get; }
public int Multiplier { get; }
public SlotResult(int[] nums, int multi)
{
Numbers = nums;
Multiplier = multi;
}
}
}
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
public async Task SlotStats()
{
//i remembered to not be a moron
var paid = _totalPaidOut;
var bet = _totalBet;
if (bet <= 0)
bet = 1;
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle("Slot Stats")
.AddField(efb => efb.WithName("Total Bet").WithValue(bet.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName("Paid Out").WithValue(paid.ToString()).WithIsInline(true))
.WithFooter(efb => efb.WithText($"Payout Rate: {paid * 1.0 / bet * 100:f4}%"));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
public async Task SlotTest(int tests = 1000)
{
if (tests <= 0)
return;
//multi vs how many times it occured
var dict = new Dictionary<int, int>();
for (int i = 0; i < tests; i++)
{
var res = SlotMachine.Pull();
if (dict.ContainsKey(res.Multiplier))
dict[res.Multiplier] += 1;
else
dict.Add(res.Multiplier, 1);
}
var sb = new StringBuilder();
const int bet = 1;
int payout = 0;
foreach (var key in dict.Keys.OrderByDescending(x => x))
{
sb.AppendLine($"x{key} occured {dict[key]} times. {dict[key] * 1.0f / tests * 100}%");
payout += key * dict[key];
}
await ctx.Channel.SendConfirmAsync("Slot Test Results", sb.ToString(),
footer: $"Total Bet: {tests * bet} | Payout: {payout * bet} | {payout * 1.0f / tests * 100}%").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Slot(ShmartNumber amount)
{
if (!_runningUsers.Add(ctx.User.Id))
return;
try
{
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
return;
const int maxAmount = 9999;
if (amount > maxAmount)
{
await ReplyErrorLocalizedAsync("max_bet_limit", maxAmount + CurrencySign).ConfigureAwait(false);
return;
}
if (!await _cs.RemoveAsync(ctx.User, "Slot Machine", amount, false, gamble: true).ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
return;
}
Interlocked.Add(ref _totalBet, amount.Value);
using (var bgImage = Image.Load(_images.SlotBackground))
{
var result = SlotMachine.Pull();
int[] numbers = result.Numbers;
for (int i = 0; i < 3; i++)
{
using (var randomImage = Image.Load(_images.SlotEmojis[numbers[i]]))
{
bgImage.Mutate(x => x.DrawImage(randomImage, new Point(95 + 142 * i, 330), new GraphicsOptions()));
}
}
var won = amount * result.Multiplier;
var printWon = won;
var n = 0;
do
{
var digit = (int)(printWon % 10);
using (var img = Image.Load(_images.SlotNumbers[digit]))
{
bgImage.Mutate(x => x.DrawImage(img, new Point(230 - n * 16, 462), new GraphicsOptions()));
}
n++;
} while ((printWon /= 10) != 0);
var printAmount = amount;
n = 0;
do
{
var digit = (int)(printAmount % 10);
using (var img = Image.Load(_images.SlotNumbers[digit]))
{
bgImage.Mutate(x => x.DrawImage(img, new Point(395 - n * 16, 462), new GraphicsOptions()));
}
n++;
} while ((printAmount /= 10) != 0);
var msg = GetText("better_luck");
if (result.Multiplier != 0)
{
await _cs.AddAsync(ctx.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false, gamble: true).ConfigureAwait(false);
Interlocked.Add(ref _totalPaidOut, amount * result.Multiplier);
if (result.Multiplier == 1)
msg = GetText("slot_single", CurrencySign, 1);
else if (result.Multiplier == 4)
msg = GetText("slot_two", CurrencySign, 4);
else if (result.Multiplier == 10)
msg = GetText("slot_three", 10);
else if (result.Multiplier == 30)
msg = GetText("slot_jackpot", 30);
}
using (var imgStream = bgImage.ToStream())
{
await ctx.Channel.SendFileAsync(imgStream, "result.png", ctx.User.Mention + " " + msg + $"\n`{GetText("slot_bet")}:`{amount} `{GetText("won")}:` {amount * result.Multiplier}{CurrencySign}").ConfigureAwait(false);
}
}
}
finally
{
var _ = Task.Run(async () =>
{
await Task.Delay(1000).ConfigureAwait(false);
_runningUsers.Remove(ctx.User.Id);
});
}
}
}
}
}

View File

@@ -0,0 +1,362 @@
using Discord;
using Discord.Commands;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Modules.Gambling.Common.Waifu;
using NadekoBot.Extensions;
using NadekoBot.Modules.Gambling.Services;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Internal;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Modules.Gambling.Services;
using NadekoBot.Core.Services.Database.Models;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class WaifuClaimCommands : GamblingSubmodule<WaifuService>
{
public WaifuClaimCommands(GamblingConfigService gamblingConfService) : base(gamblingConfService)
{
}
[NadekoCommand, Usage, Description, Aliases]
public async Task WaifuReset()
{
var price = _service.GetResetPrice(ctx.User);
var embed = new EmbedBuilder()
.WithTitle(GetText("waifu_reset_confirm"))
.WithDescription(GetText("waifu_reset_price", Format.Bold(price + CurrencySign)));
if (!await PromptUserConfirmAsync(embed))
return;
if (await _service.TryReset(ctx.User))
{
await ReplyConfirmLocalizedAsync("waifu_reset");
return;
}
await ReplyErrorLocalizedAsync("waifu_reset_fail");
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task WaifuClaim(int amount, [Leftover]IUser target)
{
if (amount < _config.Waifu.MinPrice)
{
await ReplyErrorLocalizedAsync("waifu_isnt_cheap", _config.Waifu.MinPrice + CurrencySign);
return;
}
if (target.Id == ctx.User.Id)
{
await ReplyErrorLocalizedAsync("waifu_not_yourself");
return;
}
var (w, isAffinity, result) = await _service.ClaimWaifuAsync(ctx.User, target, amount);
if (result == WaifuClaimResult.InsufficientAmount)
{
await ReplyErrorLocalizedAsync("waifu_not_enough", Math.Ceiling(w.Price * (isAffinity ? 0.88f : 1.1f)));
return;
}
if (result == WaifuClaimResult.NotEnoughFunds)
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign);
return;
}
var msg = GetText("waifu_claimed",
Format.Bold(target.ToString()),
amount + CurrencySign);
if (w.Affinity?.UserId == ctx.User.Id)
msg += "\n" + GetText("waifu_fulfilled", target, w.Price + CurrencySign);
else
msg = " " + msg;
await ctx.Channel.SendConfirmAsync(ctx.User.Mention + msg);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task WaifuTransfer(ulong waifuId, IUser newOwner)
{
if (!await _service.WaifuTransfer(ctx.User, waifuId, newOwner)
)
{
await ReplyErrorLocalizedAsync("waifu_transfer_fail");
return;
}
await ReplyConfirmLocalizedAsync("waifu_transfer_success",
Format.Bold(waifuId.ToString()),
Format.Bold(ctx.User.ToString()),
Format.Bold(newOwner.ToString()));
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task WaifuTransfer(IUser waifu, IUser newOwner)
{
if (!await _service.WaifuTransfer(ctx.User, waifu.Id, newOwner))
{
await ReplyErrorLocalizedAsync("waifu_transfer_fail");
return;
}
await ReplyConfirmLocalizedAsync("waifu_transfer_success",
Format.Bold(waifu.ToString()),
Format.Bold(ctx.User.ToString()),
Format.Bold(newOwner.ToString()));
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(-1)]
public Task Divorce([Leftover] string target)
{
var waifuUserId = _service.GetWaifuUserId(ctx.User.Id, target);
if (waifuUserId == default)
{
return ReplyErrorLocalizedAsync("waifu_not_yours");
}
return Divorce(waifuUserId);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public Task Divorce([Leftover]IGuildUser target)
=> Divorce(target.Id);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task Divorce([Leftover]ulong targetId)
{
if (targetId == ctx.User.Id)
return;
var (w, result, amount, remaining) = await _service.DivorceWaifuAsync(ctx.User, targetId);
if (result == DivorceResult.SucessWithPenalty)
{
await ReplyConfirmLocalizedAsync("waifu_divorced_like", Format.Bold(w.Waifu.ToString()), amount + CurrencySign);
}
else if (result == DivorceResult.Success)
{
await ReplyConfirmLocalizedAsync("waifu_divorced_notlike", amount + CurrencySign);
}
else if (result == DivorceResult.NotYourWife)
{
await ReplyErrorLocalizedAsync("waifu_not_yours");
}
else
{
await ReplyErrorLocalizedAsync("waifu_recent_divorce",
Format.Bold(((int)remaining?.TotalHours).ToString()),
Format.Bold(remaining?.Minutes.ToString()));
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Affinity([Leftover]IGuildUser u = null)
{
if (u?.Id == ctx.User.Id)
{
await ReplyErrorLocalizedAsync("waifu_egomaniac");
return;
}
var (oldAff, sucess, remaining) = await _service.ChangeAffinityAsync(ctx.User, u);
if (!sucess)
{
if (remaining != null)
{
await ReplyErrorLocalizedAsync("waifu_affinity_cooldown",
Format.Bold(((int)remaining?.TotalHours).ToString()),
Format.Bold(remaining?.Minutes.ToString()));
}
else
{
await ReplyErrorLocalizedAsync("waifu_affinity_already");
}
return;
}
if (u == null)
{
await ReplyConfirmLocalizedAsync("waifu_affinity_reset");
}
else if (oldAff == null)
{
await ReplyConfirmLocalizedAsync("waifu_affinity_set", Format.Bold(u.ToString()));
}
else
{
await ReplyConfirmLocalizedAsync("waifu_affinity_changed", Format.Bold(oldAff.ToString()), Format.Bold(u.ToString()));
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task WaifuLb(int page = 1)
{
page--;
if (page < 0)
return;
if (page > 100)
page = 100;
var waifus = _service.GetTopWaifusAtPage(page);
if (waifus.Count() == 0)
{
await ReplyConfirmLocalizedAsync("waifus_none");
return;
}
var embed = new EmbedBuilder()
.WithTitle(GetText("waifus_top_waifus"))
.WithOkColor();
var i = 0;
foreach (var w in waifus)
{
var j = i++;
embed.AddField(efb => efb.WithName("#" + ((page * 9) + j + 1) + " - " + w.Price + CurrencySign).WithValue(w.ToString()).WithIsInline(false));
}
await ctx.Channel.EmbedAsync(embed);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task WaifuInfo([Leftover]IUser target = null)
{
if (target == null)
target = ctx.User;
return InternalWaifuInfo(target.Id, target.ToString());
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public Task WaifuInfo(ulong targetId)
=> InternalWaifuInfo(targetId);
private Task InternalWaifuInfo(ulong targetId, string name = null)
{
var wi = _service.GetFullWaifuInfoAsync(targetId);
var affInfo = _service.GetAffinityTitle(wi.AffinityCount);
var waifuItems = _service.GetWaifuItems()
.ToDictionary(x => x.ItemEmoji, x => x);
var nobody = GetText("nobody");
var itemsStr = !wi.Items.Any()
? "-"
: string.Join("\n", wi.Items
.Where(x => waifuItems.TryGetValue(x.ItemEmoji, out _))
.OrderBy(x => waifuItems[x.ItemEmoji].Price)
.GroupBy(x => x.ItemEmoji)
.Select(x => $"{x.Key} x{x.Count(),-3}")
.Chunk(2)
.Select(x => string.Join(" ", x)));
var fansStr = wi
.Fans
.Shuffle()
.Take(30)
.Select(x => wi.Claims.Contains(x) ? $"{x} 💞" : x)
.JoinWith('\n');
if (string.IsNullOrWhiteSpace(fansStr))
fansStr = "-";
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle(GetText("waifu") + " " + (wi.FullName ?? name ?? targetId.ToString()) + " - \"the " +
_service.GetClaimTitle(wi.ClaimCount) + "\"")
.AddField(GetText("price"), wi.Price.ToString(), true)
.AddField(GetText("claimed_by"), wi.ClaimerName ?? nobody, true)
.AddField(GetText("likes"), wi.AffinityName ?? nobody, true)
.AddField(GetText("changes_of_heart"), $"{wi.AffinityCount} - \"the {affInfo}\"", true)
.AddField(GetText("divorces"), wi.DivorceCount.ToString(), true)
.AddField("\u200B", "\u200B", true)
.AddField(GetText("fans", wi.Fans.Count), fansStr, true)
.AddField($"Waifus ({wi.ClaimCount})", wi.ClaimCount == 0
? nobody
: string.Join("\n", wi.Claims.Shuffle().Take(30)), true)
.AddField(GetText("gifts"), itemsStr, true);
return ctx.Channel.EmbedAsync(embed);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task WaifuGift(int page = 1)
{
if (--page < 0 || page > 3)
return;
var waifuItems = _service.GetWaifuItems();
await ctx.SendPaginatedConfirmAsync(page, (cur) =>
{
var embed = new EmbedBuilder()
.WithTitle(GetText("waifu_gift_shop"))
.WithOkColor();
waifuItems
.OrderBy(x => x.Price)
.Skip(9 * cur)
.Take(9)
.ForEach(x => embed.AddField($"{x.ItemEmoji} {x.Name}", x.Price, true));
return embed;
}, waifuItems.Count, 9);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task WaifuGift(string itemName, [Leftover] IUser waifu)
{
if (waifu.Id == ctx.User.Id)
return;
var allItems = _service.GetWaifuItems();
var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName.ToLowerInvariant());
if (item is null)
{
await ReplyErrorLocalizedAsync("waifu_gift_not_exist");
return;
}
var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, item);
if (sucess)
{
await ReplyConfirmLocalizedAsync("waifu_gift",
Format.Bold(item.ToString() + " " + item.ItemEmoji),
Format.Bold(waifu.ToString()));
}
else
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign);
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
using Discord;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using System.Threading.Tasks;
using Wof = NadekoBot.Modules.Gambling.Common.WheelOfFortune.WheelOfFortuneGame;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Common;
using System.Collections.Immutable;
using NadekoBot.Core.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
public class WheelOfFortuneCommands : GamblingSubmodule<GamblingService>
{
private static readonly ImmutableArray<string> _emojis = new string[] {
"⬆",
"↖",
"⬅",
"↙",
"⬇",
"↘",
"➡",
"↗" }.ToImmutableArray();
private readonly ICurrencyService _cs;
private readonly DbService _db;
public WheelOfFortuneCommands(ICurrencyService cs, DbService db, GamblingConfigService gamblingConfService)
: base(gamblingConfService)
{
_cs = cs;
_db = db;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task WheelOfFortune(ShmartNumber amount)
{
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
return;
if (!await _cs.RemoveAsync(ctx.User.Id, "Wheel Of Fortune - bet", amount, gamble: true).ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
return;
}
var result = await _service.WheelOfFortuneSpinAsync(ctx.User.Id, amount).ConfigureAwait(false);
var wofMultipliers = _config.WheelOfFortune.Multipliers;
await ctx.Channel.SendConfirmAsync(
Format.Bold($@"{ctx.User.ToString()} won: {result.Amount + CurrencySign}
『{wofMultipliers[1]}』 『{wofMultipliers[0]}』 『{wofMultipliers[7]}』
『{wofMultipliers[2]}』 {_emojis[result.Index]} 『{wofMultipliers[6]}』
『{wofMultipliers[3]}』 『{wofMultipliers[4]}』 『{wofMultipliers[5]}』")).ConfigureAwait(false);
}
}
}
}