mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 09:48:26 -04:00
Restructured the project structure back to the way it was, there's no reasonable way to split the modules
This commit is contained in:
154
src/NadekoBot/Modules/Gambling/AnimalRacing/AnimalRace.cs
Normal file
154
src/NadekoBot/Modules/Gambling/AnimalRacing/AnimalRace.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
#nullable disable
|
||||
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 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 Phase CurrentPhase { get; private set; } = Phase.WaitingForPlayers;
|
||||
|
||||
public IReadOnlyCollection<AnimalRacingUser> Users
|
||||
=> _users.ToList();
|
||||
|
||||
public List<AnimalRacingUser> FinishedUsers { get; } = new();
|
||||
public int MaxUsers { get; }
|
||||
|
||||
private readonly SemaphoreSlim _locker = new(1, 1);
|
||||
private readonly HashSet<AnimalRacingUser> _users = new();
|
||||
private readonly ICurrencyService _currency;
|
||||
private readonly RaceOptions _options;
|
||||
private readonly Queue<RaceAnimal> _animalsQueue;
|
||||
|
||||
public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable<RaceAnimal> availableAnimals)
|
||||
{
|
||||
_currency = currency;
|
||||
_options = options;
|
||||
_animalsQueue = new(availableAnimals);
|
||||
MaxUsers = _animalsQueue.Count;
|
||||
|
||||
if (_animalsQueue.Count == 0)
|
||||
CurrentPhase = Phase.Ended;
|
||||
}
|
||||
|
||||
public void Initialize() //lame name
|
||||
=> _ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(_options.StartTime * 1000);
|
||||
|
||||
await _locker.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (CurrentPhase != Phase.WaitingForPlayers)
|
||||
return;
|
||||
|
||||
await Start();
|
||||
}
|
||||
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();
|
||||
try
|
||||
{
|
||||
if (_users.Count == MaxUsers)
|
||||
throw new AnimalRaceFullException();
|
||||
|
||||
if (CurrentPhase != Phase.WaitingForPlayers)
|
||||
throw new AlreadyStartedException();
|
||||
|
||||
if (!await _currency.RemoveAsync(userId, bet, new("animalrace", "bet")))
|
||||
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();
|
||||
|
||||
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, user.Bet, new("animalrace", "refund"));
|
||||
}
|
||||
|
||||
_ = OnStartingFailed?.Invoke(this);
|
||||
CurrentPhase = Phase.Ended;
|
||||
return;
|
||||
}
|
||||
|
||||
_ = OnStarted?.Invoke(this);
|
||||
_ = 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);
|
||||
|
||||
_ = OnStateUpdate?.Invoke(this);
|
||||
await Task.Delay(2500);
|
||||
}
|
||||
|
||||
if (FinishedUsers[0].Bet > 0)
|
||||
{
|
||||
await _currency.AddAsync(FinishedUsers[0].UserId,
|
||||
FinishedUsers[0].Bet * (_users.Count - 1),
|
||||
new("animalrace", "win"));
|
||||
}
|
||||
|
||||
_ = OnEnded?.Invoke(this);
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CurrentPhase = Phase.Ended;
|
||||
OnStarted = null;
|
||||
OnEnded = null;
|
||||
OnStartingFailed = null;
|
||||
OnStateUpdate = null;
|
||||
_locker.Dispose();
|
||||
_users.Clear();
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Services;
|
||||
|
||||
public class AnimalRaceService : INService
|
||||
{
|
||||
public ConcurrentDictionary<ulong, AnimalRace> AnimalRaces { get; } = new();
|
||||
}
|
@@ -0,0 +1,183 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Common.TypeReaders;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
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 partial class AnimalRacingCommands : GamblingSubmodule<AnimalRaceService>
|
||||
{
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly GamesConfigService _gamesConf;
|
||||
|
||||
private IUserMessage raceMessage;
|
||||
|
||||
public AnimalRacingCommands(
|
||||
ICurrencyService cs,
|
||||
DiscordSocketClient client,
|
||||
GamblingConfigService gamblingConf,
|
||||
GamesConfigService gamesConf)
|
||||
: base(gamblingConf)
|
||||
{
|
||||
_cs = cs;
|
||||
_client = client;
|
||||
_gamesConf = gamesConf;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[NadekoOptions<RaceOptions>]
|
||||
public Task Race(params string[] args)
|
||||
{
|
||||
var (options, _) = OptionsParser.ParseFrom(new RaceOptions(), args);
|
||||
|
||||
var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle());
|
||||
if (!_service.AnimalRaces.TryAdd(ctx.Guild.Id, ar))
|
||||
return SendErrorAsync(GetText(strs.animal_race), GetText(strs.animal_race_already_started));
|
||||
|
||||
ar.Initialize();
|
||||
|
||||
var count = 0;
|
||||
|
||||
Task ClientMessageReceived(SocketMessage arg)
|
||||
{
|
||||
_ = 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 ArOnEnded(AnimalRace race)
|
||||
{
|
||||
_client.MessageReceived -= ClientMessageReceived;
|
||||
_service.AnimalRaces.TryRemove(ctx.Guild.Id, out _);
|
||||
var winner = race.FinishedUsers[0];
|
||||
if (race.FinishedUsers[0].Bet > 0)
|
||||
{
|
||||
return SendConfirmAsync(GetText(strs.animal_race),
|
||||
GetText(strs.animal_race_won_money(Format.Bold(winner.Username),
|
||||
winner.Animal.Icon,
|
||||
(race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign)));
|
||||
}
|
||||
|
||||
ar.Dispose();
|
||||
return SendConfirmAsync(GetText(strs.animal_race),
|
||||
GetText(strs.animal_race_won(Format.Bold(winner.Username), winner.Animal.Icon)));
|
||||
}
|
||||
|
||||
ar.OnStartingFailed += Ar_OnStartingFailed;
|
||||
ar.OnStateUpdate += Ar_OnStateUpdate;
|
||||
ar.OnEnded += ArOnEnded;
|
||||
ar.OnStarted += Ar_OnStarted;
|
||||
_client.MessageReceived += ClientMessageReceived;
|
||||
|
||||
return SendConfirmAsync(GetText(strs.animal_race),
|
||||
GetText(strs.animal_race_starting(options.StartTime)),
|
||||
footer: GetText(strs.animal_race_join_instr(prefix)));
|
||||
}
|
||||
|
||||
private Task Ar_OnStarted(AnimalRace race)
|
||||
{
|
||||
if (race.Users.Count == race.MaxUsers)
|
||||
return SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full));
|
||||
return SendConfirmAsync(GetText(strs.animal_race),
|
||||
GetText(strs.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 is null)
|
||||
raceMessage = await SendConfirmAsync(text);
|
||||
else
|
||||
{
|
||||
await msg.ModifyAsync(x => x.Embed = _eb.Create()
|
||||
.WithTitle(GetText(strs.animal_race))
|
||||
.WithDescription(text)
|
||||
.WithOkColor()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
private Task Ar_OnStartingFailed(AnimalRace race)
|
||||
{
|
||||
_service.AnimalRaces.TryRemove(ctx.Guild.Id, out _);
|
||||
race.Dispose();
|
||||
return ReplyErrorLocalizedAsync(strs.animal_race_failed);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task JoinRace([OverrideTypeReader(typeof(BalanceTypeReader))] long amount = default)
|
||||
{
|
||||
if (!await CheckBetOptional(amount))
|
||||
return;
|
||||
|
||||
if (!_service.AnimalRaces.TryGetValue(ctx.Guild.Id, out var ar))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.race_not_exist);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var user = await ar.JoinRace(ctx.User.Id, ctx.User.ToString(), amount);
|
||||
if (amount > 0)
|
||||
{
|
||||
await SendConfirmAsync(GetText(strs.animal_race_join_bet(ctx.User.Mention,
|
||||
user.Animal.Icon,
|
||||
amount + CurrencySign)));
|
||||
}
|
||||
else
|
||||
await SendConfirmAsync(GetText(strs.animal_race_join(ctx.User.Mention, user.Animal.Icon)));
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
//ignore if user inputed an invalid amount
|
||||
}
|
||||
catch (AlreadyJoinedException)
|
||||
{
|
||||
// just ignore this
|
||||
}
|
||||
catch (AlreadyStartedException)
|
||||
{
|
||||
//ignore
|
||||
}
|
||||
catch (AnimalRaceFullException)
|
||||
{
|
||||
await SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full));
|
||||
}
|
||||
catch (NotEnoughFundsException)
|
||||
{
|
||||
await SendErrorAsync(GetText(strs.not_enough(CurrencySign)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
#nullable disable
|
||||
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)
|
||||
{
|
||||
Bet = bet;
|
||||
Username = username;
|
||||
UserId = userId;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is AnimalRacingUser x ? x.UserId == UserId : false;
|
||||
|
||||
public override int GetHashCode()
|
||||
=> UserId.GetHashCode();
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
|
||||
|
||||
public class AlreadyJoinedException : Exception
|
||||
{
|
||||
public AlreadyJoinedException()
|
||||
{
|
||||
}
|
||||
|
||||
public AlreadyJoinedException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AlreadyJoinedException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
|
||||
|
||||
public class AlreadyStartedException : Exception
|
||||
{
|
||||
public AlreadyStartedException()
|
||||
{
|
||||
}
|
||||
|
||||
public AlreadyStartedException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AlreadyStartedException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
|
||||
|
||||
public class AnimalRaceFullException : Exception
|
||||
{
|
||||
public AnimalRaceFullException()
|
||||
{
|
||||
}
|
||||
|
||||
public AnimalRaceFullException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AnimalRaceFullException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
#nullable disable
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
16
src/NadekoBot/Modules/Gambling/AnimalRacing/RaceOptions.cs
Normal file
16
src/NadekoBot/Modules/Gambling/AnimalRacing/RaceOptions.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
#nullable disable
|
||||
using CommandLine;
|
||||
|
||||
namespace NadekoBot.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 (StartTime is < 10 or > 120)
|
||||
StartTime = 20;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user