Applied codestyle to all .cs files

This commit is contained in:
Kwoth
2021-12-29 06:07:16 +01:00
parent 723447c7d4
commit 82000c97a4
543 changed files with 13221 additions and 14059 deletions

View File

@@ -1,7 +1,7 @@
#nullable disable
using System.Collections.Immutable;
using NadekoBot.Modules.Games.Common.Acrophobia;
using NadekoBot.Modules.Games.Services;
using System.Collections.Immutable;
namespace NadekoBot.Modules.Games;
@@ -15,7 +15,8 @@ public partial class Games
public AcropobiaCommands(DiscordSocketClient client)
=> _client = client;
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptions(typeof(AcrophobiaGame.Options))]
public async Task Acrophobia(params string[] args)
@@ -25,7 +26,6 @@ public partial class Games
var game = new AcrophobiaGame(options);
if (_service.AcrophobiaGames.TryAdd(channel.Id, game))
{
try
{
game.OnStarted += Game_OnStarted;
@@ -41,11 +41,8 @@ public partial class Games
_service.AcrophobiaGames.TryRemove(channel.Id, out game);
game.Dispose();
}
}
else
{
await ReplyErrorLocalizedAsync(strs.acro_running);
}
Task _client_MessageReceived(SocketMessage msg)
{
@@ -69,44 +66,52 @@ public partial class Games
private Task Game_OnStarted(AcrophobiaGame game)
{
var embed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(GetText(strs.acro_started(Format.Bold(string.Join(".", game.StartingLetters)))))
.WithFooter(GetText(strs.acro_started_footer(game.Opts.SubmissionTime)));
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(
GetText(strs.acro_started(Format.Bold(string.Join(".", game.StartingLetters)))))
.WithFooter(GetText(strs.acro_started_footer(game.Opts.SubmissionTime)));
return ctx.Channel.EmbedAsync(embed);
}
private Task Game_OnUserVoted(string user)
=> SendConfirmAsync(
GetText(strs.acrophobia),
GetText(strs.acro_vote_cast(Format.Bold(user))));
=> SendConfirmAsync(GetText(strs.acrophobia), GetText(strs.acro_vote_cast(Format.Bold(user))));
private async Task Game_OnVotingStarted(AcrophobiaGame game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> submissions)
private async Task Game_OnVotingStarted(
AcrophobiaGame game,
ImmutableArray<KeyValuePair<AcrophobiaUser, int>> submissions)
{
if (submissions.Length == 0)
{
await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_ended_no_sub));
return;
}
if (submissions.Length == 1)
{
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription(GetText(strs.acro_winner_only(Format.Bold(submissions.First().Key.UserName))))
.WithFooter(submissions.First().Key.Input));
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithDescription(GetText(
strs.acro_winner_only(
Format.Bold(submissions.First().Key.UserName))))
.WithFooter(submissions.First().Key.Input));
return;
}
var i = 0;
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed))
.WithDescription(GetText(strs.acro_nym_was(Format.Bold(string.Join(".", game.StartingLetters)) + "\n" +
$@"--
.WithOkColor()
.WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed))
.WithDescription(GetText(strs.acro_nym_was(
Format.Bold(string.Join(".", game.StartingLetters))
+ "\n"
+ $@"--
{submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.Input}**\n")}
--")))
.WithFooter(GetText(strs.acro_vote));
.WithFooter(GetText(strs.acro_vote));
await ctx.Channel.EmbedAsync(embed);
}
@@ -118,15 +123,17 @@ public partial class Games
await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_no_votes_cast));
return;
}
var table = votes.OrderByDescending(v => v.Value);
var winner = table.First();
var embed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName),
Format.Bold(winner.Value.ToString()))))
.WithFooter(winner.Key.Input);
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName),
Format.Bold(winner.Value.ToString()))))
.WithFooter(winner.Key.Input);
await ctx.Channel.EmbedAsync(embed);
}
}
}
}

View File

@@ -15,7 +15,8 @@ public partial class Games
=> _db = db;
[NoPublicBot]
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task Cleverbot()
@@ -29,6 +30,7 @@ public partial class Games
uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, false);
await uow.SaveChangesAsync();
}
await ReplyConfirmLocalizedAsync(strs.cleverbot_disabled);
return;
}
@@ -44,6 +46,4 @@ public partial class Games
await ReplyConfirmLocalizedAsync(strs.cleverbot_enabled);
}
}
}
}

View File

@@ -1,28 +1,11 @@
#nullable disable
using System.Collections.Immutable;
using CommandLine;
using System.Collections.Immutable;
namespace NadekoBot.Modules.Games.Common.Acrophobia;
public sealed class AcrophobiaGame : IDisposable
{
public class Options : INadekoCommandOptions
{
[Option('s', "submission-time", Required = false, Default = 60, HelpText = "Time after which the submissions are closed and voting starts.")]
public int SubmissionTime { get; set; } = 60;
[Option('v', "vote-time", Required = false, Default = 60, HelpText = "Time after which the voting is closed and the winner is declared.")]
public int VoteTime { get; set; } = 30;
public void NormalizeOptions()
{
if (SubmissionTime is < 15 or > 300)
SubmissionTime = 60;
if (VoteTime is < 15 or > 120)
VoteTime = 30;
}
}
public enum Phase
{
Submission,
@@ -39,19 +22,26 @@ public sealed class AcrophobiaGame : IDisposable
Failed
}
public event Func<AcrophobiaGame, Task> OnStarted = delegate { return Task.CompletedTask; };
public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnVotingStarted =
delegate { return Task.CompletedTask; };
public event Func<string, Task> OnUserVoted = delegate { return Task.CompletedTask; };
public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnEnded = delegate
{
return Task.CompletedTask;
};
public Phase CurrentPhase { get; private set; } = Phase.Submission;
public ImmutableArray<char> StartingLetters { get; private set; }
public Options Opts { get; }
private readonly Dictionary<AcrophobiaUser, int> submissions = new();
private readonly SemaphoreSlim locker = new(1, 1);
public Options Opts { get; }
private readonly NadekoRandom _rng;
public event Func<AcrophobiaGame, Task> OnStarted = delegate { return Task.CompletedTask; };
public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnVotingStarted = delegate { return Task.CompletedTask; };
public event Func<string, Task> OnUserVoted = delegate { return Task.CompletedTask; };
public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnEnded = delegate { return Task.CompletedTask; };
private readonly HashSet<ulong> _usersWhoVoted = new();
public AcrophobiaGame(Options options)
@@ -74,6 +64,7 @@ public sealed class AcrophobiaGame : IDisposable
await OnVotingStarted(this, ImmutableArray.Create<KeyValuePair<AcrophobiaUser, int>>());
return;
}
if (submissions.Count == 1)
{
CurrentPhase = Phase.Ended;
@@ -108,6 +99,7 @@ public sealed class AcrophobiaGame : IDisposable
var randChar = (char)_rng.Next(65, 91);
lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar;
}
StartingLetters = lettersArr.ToImmutableArray();
}
@@ -137,9 +129,8 @@ public sealed class AcrophobiaGame : IDisposable
++submissions[toVoteFor];
var _ = Task.Run(() => OnUserVoted(userName));
return true;
default:
break;
}
return false;
}
finally
@@ -154,14 +145,16 @@ public sealed class AcrophobiaGame : IDisposable
var inputWords = input.Split(' ');
if (inputWords.Length != StartingLetters.Length) // number of words must be the same as the number of the starting letters
if (inputWords.Length
!= StartingLetters.Length) // number of words must be the same as the number of the starting letters
return false;
for (var i = 0; i < StartingLetters.Length; i++)
{
var letter = StartingLetters[i];
if (!inputWords[i].StartsWith(letter.ToString(), StringComparison.InvariantCulture)) // all first letters must match
if (!inputWords[i]
.StartsWith(letter.ToString(), StringComparison.InvariantCulture)) // all first letters must match
return false;
}
@@ -170,7 +163,7 @@ public sealed class AcrophobiaGame : IDisposable
public void Dispose()
{
this.CurrentPhase = Phase.Ended;
CurrentPhase = Phase.Ended;
OnStarted = null;
OnEnded = null;
OnUserVoted = null;
@@ -179,4 +172,29 @@ public sealed class AcrophobiaGame : IDisposable
submissions.Clear();
locker.Dispose();
}
}
public class Options : INadekoCommandOptions
{
[Option('s',
"submission-time",
Required = false,
Default = 60,
HelpText = "Time after which the submissions are closed and voting starts.")]
public int SubmissionTime { get; set; } = 60;
[Option('v',
"vote-time",
Required = false,
Default = 60,
HelpText = "Time after which the voting is closed and the winner is declared.")]
public int VoteTime { get; set; } = 30;
public void NormalizeOptions()
{
if (SubmissionTime is < 15 or > 300)
SubmissionTime = 60;
if (VoteTime is < 15 or > 120)
VoteTime = 30;
}
}
}

View File

@@ -9,16 +9,14 @@ public class AcrophobiaUser
public AcrophobiaUser(ulong userId, string userName, string input)
{
this.UserName = userName;
this.UserId = userId;
this.Input = input;
UserName = userName;
UserId = userId;
Input = input;
}
public override int GetHashCode()
=> UserId.GetHashCode();
public override bool Equals(object obj)
=> obj is AcrophobiaUser x
? x.UserId == this.UserId
: false;
}
=> obj is AcrophobiaUser x ? x.UserId == UserId : false;
}

View File

@@ -5,4 +5,4 @@ public class ChatterBotResponse
{
public string Convo_id { get; set; }
public string BotSay { get; set; }
}
}

View File

@@ -7,6 +7,13 @@ public class ChatterBotSession : IChatterBotSession
{
private static NadekoRandom Rng { get; } = new();
private string ApiEndpoint
=> "http://api.program-o.com/v2/chatbot/"
+ $"?bot_id={_botId}&"
+ "say={0}&"
+ $"convo_id=nadekobot_{_chatterBotId}&"
+ "format=json";
private readonly string _chatterBotId;
private readonly IHttpClientFactory _httpFactory;
private readonly int _botId = 6;
@@ -17,12 +24,6 @@ public class ChatterBotSession : IChatterBotSession
_httpFactory = httpFactory;
}
private string ApiEndpoint => "http://api.program-o.com/v2/chatbot/" +
$"?bot_id={_botId}&" +
"say={0}&" +
$"convo_id=nadekobot_{_chatterBotId}&" +
"format=json";
public async Task<string> Think(string message)
{
using var http = _httpFactory.CreateClient();
@@ -30,4 +31,4 @@ public class ChatterBotSession : IChatterBotSession
var cbr = JsonConvert.DeserializeObject<ChatterBotResponse>(res);
return cbr.BotSay.Replace("<br/>", "\n", StringComparison.InvariantCulture);
}
}
}

View File

@@ -17,4 +17,4 @@ public class CleverbotIOAskResponse
{
public string Status { get; set; }
public string Response { get; set; }
}
}

View File

@@ -4,4 +4,4 @@ namespace NadekoBot.Modules.Games.Common.ChatterBot;
public interface IChatterBotSession
{
Task<string> Think(string input);
}
}

View File

@@ -5,19 +5,17 @@ namespace NadekoBot.Modules.Games.Common.ChatterBot;
public class OfficialCleverbotSession : IChatterBotSession
{
private string QueryString
=> $"https://www.cleverbot.com/getreply?key={_apiKey}" + "&wrapper=nadekobot" + "&input={0}" + "&cs={1}";
private readonly string _apiKey;
private readonly IHttpClientFactory _httpFactory;
private string _cs = null;
private string QueryString => $"https://www.cleverbot.com/getreply?key={_apiKey}" +
"&wrapper=nadekobot" +
"&input={0}" +
"&cs={1}";
private string _cs;
public OfficialCleverbotSession(string apiKey, IHttpClientFactory factory)
{
this._apiKey = apiKey;
this._httpFactory = factory;
_apiKey = apiKey;
_httpFactory = factory;
}
public async Task<string> Think(string input)
@@ -47,14 +45,14 @@ public class CleverbotIOSession : IChatterBotSession
private readonly IHttpClientFactory _httpFactory;
private readonly AsyncLazy<string> _nick;
private readonly string _createEndpoint = $"https://cleverbot.io/1.0/create";
private readonly string _askEndpoint = $"https://cleverbot.io/1.0/ask";
private readonly string _createEndpoint = "https://cleverbot.io/1.0/create";
private readonly string _askEndpoint = "https://cleverbot.io/1.0/ask";
public CleverbotIOSession(string user, string key, IHttpClientFactory factory)
{
this._key = key;
this._user = user;
this._httpFactory = factory;
_key = key;
_user = user;
_httpFactory = factory;
_nick = new(GetNick);
}
@@ -64,8 +62,7 @@ public class CleverbotIOSession : IChatterBotSession
using var _http = _httpFactory.CreateClient();
using var msg = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("user", _user),
new KeyValuePair<string, string>("key", _key),
new KeyValuePair<string, string>("user", _user), new KeyValuePair<string, string>("key", _key)
});
using var data = await _http.PostAsync(_createEndpoint, msg);
var str = await data.Content.ReadAsStringAsync();
@@ -81,10 +78,8 @@ public class CleverbotIOSession : IChatterBotSession
using var _http = _httpFactory.CreateClient();
using var msg = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("user", _user),
new KeyValuePair<string, string>("key", _key),
new KeyValuePair<string, string>("nick", await _nick),
new KeyValuePair<string, string>("text", input),
new KeyValuePair<string, string>("user", _user), new KeyValuePair<string, string>("key", _key),
new KeyValuePair<string, string>("nick", await _nick), new KeyValuePair<string, string>("text", input)
});
using var data = await _http.PostAsync(_askEndpoint, msg);
var str = await data.Content.ReadAsStringAsync();
@@ -94,4 +89,4 @@ public class CleverbotIOSession : IChatterBotSession
return obj.Response;
}
}
}

View File

@@ -11,17 +11,10 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
public int Version { get; set; }
[Comment("Hangman related settings (.hangman command)")]
public HangmanConfig Hangman { get; set; } = new()
{
CurrencyReward = 0
};
public HangmanConfig Hangman { get; set; } = new() { CurrencyReward = 0 };
[Comment("Trivia related settings (.t command)")]
public TriviaConfig Trivia { get; set; } = new()
{
CurrencyReward = 0,
MinimumWinReq = 1,
};
public TriviaConfig Trivia { get; set; } = new() { CurrencyReward = 0, MinimumWinReq = 1 };
[Comment("List of responses for the .8ball command. A random one will be selected every time")]
public List<string> EightBallResponses { get; set; } = new()
@@ -76,7 +69,7 @@ public sealed partial class HangmanConfig
public sealed partial class TriviaConfig
{
[Comment("The amount of currency awarded to the winner of the trivia game.")]
public long CurrencyReward { get; set; } = 0;
public long CurrencyReward { get; set; }
[Comment(@"Users won't be able to start trivia games which have
a smaller win requirement than the one specified by this setting.")]
@@ -88,4 +81,4 @@ public sealed partial class RaceAnimal
{
public string Icon { get; set; }
public string Name { get; set; }
}
}

View File

@@ -1,24 +1,29 @@
#nullable disable
using Image = SixLabors.ImageSharp.Image;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Games.Common;
public class GirlRating
{
private readonly IImageCache _images;
public double Crazy { get; }
public double Hot { get; }
public int Roll { get; }
public string Advice { get; }
public AsyncLazy<Stream> Stream { get; }
private readonly IImageCache _images;
private readonly IHttpClientFactory _httpFactory;
public AsyncLazy<Stream> Stream { get; }
public GirlRating(IImageCache images, IHttpClientFactory factory, double crazy, double hot, int roll, string advice)
public GirlRating(
IImageCache images,
IHttpClientFactory factory,
double crazy,
double hot,
int roll,
string advice)
{
_images = images;
Crazy = crazy;
@@ -26,7 +31,7 @@ public class GirlRating
Roll = roll;
Advice = advice; // convenient to have it here, even though atm there are only few different ones.
_httpFactory = factory;
Stream = new(() =>
{
try
@@ -64,4 +69,4 @@ public class GirlRating
}
});
}
}
}

View File

@@ -1,5 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using NadekoBot.Common.Yml;
using NadekoBot.Common.Yml;
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Modules.Games.Hangman;
@@ -24,7 +24,6 @@ public sealed class DefaultHangmanSource : IHangmanSource
var qs = new Dictionary<string, HangmanTerm[]>();
foreach (var file in Directory.EnumerateFiles("data/hangman/", "*.yml"))
{
try
{
var data = Yaml.Deserializer.Deserialize<HangmanTerm[]>(File.ReadAllText(file));
@@ -34,14 +33,13 @@ public sealed class DefaultHangmanSource : IHangmanSource
{
Log.Error(ex, "Loading {HangmanFile} failed.", file);
}
}
_terms = qs;
Log.Information("Loaded {HangmanCategoryCount} hangman categories.", qs.Count);
}
public IReadOnlyCollection<string> GetCategories()
public IReadOnlyCollection<string> GetCategories()
=> _terms.Keys.ToList();
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term)
@@ -61,4 +59,4 @@ public sealed class DefaultHangmanSource : IHangmanSource
term = null;
return false;
}
}
}

View File

@@ -5,22 +5,12 @@ namespace NadekoBot.Modules.Games.Hangman;
public sealed class HangmanGame
{
public enum Phase { Running, Ended }
public enum GuessResult { NoAction, AlreadyTried, Incorrect, Guess, Win }
public record State(
int Errors,
Phase Phase,
string Word,
GuessResult GuessResult,
List<char> missedLetters,
string ImageUrl)
{
public bool Failed => Errors > 5;
}
public enum Phase { Running, Ended }
private Phase CurrentPhase { get; set; }
private readonly HashSet<char> _incorrect = new();
private readonly HashSet<char> _correct = new();
private readonly HashSet<char> _remaining = new();
@@ -32,25 +22,17 @@ public sealed class HangmanGame
{
_word = term.Word;
_imageUrl = term.ImageUrl;
_remaining = _word
.ToLowerInvariant()
.Where(x => x.IsLetter())
.Select(char.ToLowerInvariant)
.ToHashSet();
_remaining = _word.ToLowerInvariant().Where(x => x.IsLetter()).Select(char.ToLowerInvariant).ToHashSet();
}
public State GetState(GuessResult guessResult = GuessResult.NoAction)
=> new(_incorrect.Count,
CurrentPhase,
CurrentPhase == Phase.Ended
? _word
: GetScrambledWord(),
CurrentPhase == Phase.Ended ? _word : GetScrambledWord(),
guessResult,
_incorrect.ToList(),
CurrentPhase == Phase.Ended
? _imageUrl
: string.Empty);
CurrentPhase == Phase.Ended ? _imageUrl : string.Empty);
private string GetScrambledWord()
{
@@ -59,11 +41,11 @@ public sealed class HangmanGame
{
var ch = _word[i];
if (ch == ' ')
output[i*2] = '';
output[i * 2] = '';
if (!ch.IsLetter() || !_remaining.Contains(char.ToLowerInvariant(ch)))
output[i*2] = ch;
output[i * 2] = ch;
else
output[i*2] = '_';
output[i * 2] = '_';
output[(i * 2) + 1] = ' ';
}
@@ -74,7 +56,7 @@ public sealed class HangmanGame
public State Guess(string guess)
{
if (CurrentPhase != Phase.Running)
return GetState(GuessResult.NoAction);
return GetState();
guess = guess.Trim();
if (guess.Length > 1)
@@ -85,12 +67,12 @@ public sealed class HangmanGame
return GetState(GuessResult.Win);
}
return GetState(GuessResult.NoAction);
return GetState();
}
var charGuess = guess[0];
if (!char.IsLetter(charGuess))
return GetState(GuessResult.NoAction);
return GetState();
if (_incorrect.Contains(charGuess) || _correct.Contains(charGuess))
return GetState(GuessResult.AlreadyTried);
@@ -116,4 +98,16 @@ public sealed class HangmanGame
return GetState(GuessResult.Incorrect);
}
}
public record State(
int Errors,
Phase Phase,
string Word,
GuessResult GuessResult,
List<char> missedLetters,
string ImageUrl)
{
public bool Failed
=> Errors > 5;
}
}

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Memory;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Games.Services;
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Modules.Games.Hangman;
@@ -15,8 +15,12 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
private readonly IMemoryCache _cdCache;
private readonly object _locker = new();
public HangmanService(IHangmanSource source, IEmbedBuilderService eb, GamesConfigService gcs,
ICurrencyService cs, IMemoryCache cdCache)
public HangmanService(
IHangmanSource source,
IEmbedBuilderService eb,
GamesConfigService gcs,
ICurrencyService cs,
IMemoryCache cdCache)
{
_source = source;
_eb = eb;
@@ -25,10 +29,7 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
_cdCache = cdCache;
}
public bool StartHangman(
ulong channelId,
string? category,
[NotNullWhen(true)] out HangmanGame.State? state)
public bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? state)
{
state = null;
if (!_source.GetTerm(category, out var term))
@@ -53,10 +54,7 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
{
lock (_locker)
{
if (_hangmanGames.TryRemove(channelId, out var game))
{
return new(true);
}
if (_hangmanGames.TryRemove(channelId, out var game)) return new(true);
}
return new(false);
@@ -74,7 +72,7 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
if (_cdCache.TryGetValue(msg.Author.Id, out _))
return;
HangmanGame.State state;
long rew = 0;
lock (_locker)
@@ -88,13 +86,10 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
return;
if (state.GuessResult is HangmanGame.GuessResult.Incorrect or HangmanGame.GuessResult.AlreadyTried)
{
_cdCache.Set(msg.Author.Id, string.Empty, new MemoryCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3)
});
}
_cdCache.Set(msg.Author.Id,
string.Empty,
new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3) });
if (state.Phase == HangmanGame.Phase.Ended)
if (_hangmanGames.TryRemove(msg.Channel.Id, out _))
rew = _gcs.Data.Hangman.CurrencyReward;
@@ -106,32 +101,28 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state);
}
}
private Task<IUserMessage> SendState(ITextChannel channel, IUser user, string content, HangmanGame.State state)
private Task<IUserMessage> SendState(
ITextChannel channel,
IUser user,
string content,
HangmanGame.State state)
{
var embed = Games.HangmanCommands.GetEmbed(_eb, state);
if (state.GuessResult == HangmanGame.GuessResult.Guess)
embed.WithDescription($"{user} guessed the letter {content}!")
.WithOkColor();
embed.WithDescription($"{user} guessed the letter {content}!").WithOkColor();
else if (state.GuessResult == HangmanGame.GuessResult.Incorrect && state.Failed)
embed.WithDescription($"{user} Letter {content} doesn't exist! Game over!")
.WithErrorColor();
embed.WithDescription($"{user} Letter {content} doesn't exist! Game over!").WithErrorColor();
else if (state.GuessResult == HangmanGame.GuessResult.Incorrect)
embed.WithDescription($"{user} Letter {content} doesn't exist!")
.WithErrorColor();
embed.WithDescription($"{user} Letter {content} doesn't exist!").WithErrorColor();
else if (state.GuessResult == HangmanGame.GuessResult.AlreadyTried)
embed.WithDescription($"{user} Letter {content} has already been used.")
.WithPendingColor();
embed.WithDescription($"{user} Letter {content} has already been used.").WithPendingColor();
else if (state.GuessResult == HangmanGame.GuessResult.Win)
embed.WithDescription($"{user} won!")
.WithOkColor();
embed.WithDescription($"{user} won!").WithOkColor();
if (!string.IsNullOrWhiteSpace(state.ImageUrl)
&& Uri.IsWellFormedUriString(state.ImageUrl, UriKind.Absolute))
{
if (!string.IsNullOrWhiteSpace(state.ImageUrl) && Uri.IsWellFormedUriString(state.ImageUrl, UriKind.Absolute))
embed.WithImageUrl(state.ImageUrl);
}
return channel.EmbedAsync(embed);
}
}
}

View File

@@ -5,4 +5,4 @@ public sealed class HangmanTerm
{
public string Word { get; set; }
public string ImageUrl { get; set; }
}
}

View File

@@ -7,4 +7,4 @@ public interface IHangmanService
bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? hangmanController);
ValueTask<bool> StopHangman(ulong channelId);
IReadOnlyCollection<string> GetHangmanTypes();
}
}

View File

@@ -7,4 +7,4 @@ public interface IHangmanSource : INService
public IReadOnlyCollection<string> GetCategories();
public void Reload();
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term);
}
}

View File

@@ -10,11 +10,11 @@ public sealed class NunchiGame : IDisposable
Joining,
Playing,
WaitingForNextRound,
Ended,
Ended
}
public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100);
public Phase CurrentPhase { get; private set; } = Phase.Joining;
private const int _killTimeout = 20 * 1000;
private const int _nextRoundTimeout = 5 * 1000;
public event Func<NunchiGame, Task> OnGameStarted;
public event Func<NunchiGame, int, Task> OnRoundStarted;
@@ -22,16 +22,19 @@ public sealed class NunchiGame : IDisposable
public event Func<NunchiGame, (ulong Id, string Name)?, Task> OnRoundEnded; // tuple of the user who failed
public event Func<NunchiGame, string, Task> OnGameEnded; // name of the user who won
public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100);
public Phase CurrentPhase { get; private set; } = Phase.Joining;
public ImmutableArray<(ulong Id, string Name)> Participants
=> _participants.ToImmutableArray();
public int ParticipantCount
=> _participants.Count;
private readonly SemaphoreSlim _locker = new(1, 1);
private HashSet<(ulong Id, string Name)> _participants = new();
private readonly HashSet<(ulong Id, string Name)> _passed = new();
public ImmutableArray<(ulong Id, string Name)> Participants => _participants.ToImmutableArray();
public int ParticipantCount => _participants.Count;
private const int _killTimeout = 20 * 1000;
private const int _nextRoundTimeout = 5 * 1000;
private Timer _killTimer;
public NunchiGame(ulong creatorId, string creatorName)
@@ -64,19 +67,22 @@ public sealed class NunchiGame : IDisposable
}
_killTimer = new(async state =>
{
await _locker.WaitAsync();
try
{
if (CurrentPhase != Phase.Playing)
return;
await _locker.WaitAsync();
try
{
if (CurrentPhase != Phase.Playing)
return;
//if some players took too long to type a number, boot them all out and start a new round
_participants = new HashSet<(ulong, string)>(_passed);
EndRound();
}
finally { _locker.Release(); }
}, null, _killTimeout, _killTimeout);
//if some players took too long to type a number, boot them all out and start a new round
_participants = new HashSet<(ulong, string)>(_passed);
EndRound();
}
finally { _locker.Release(); }
},
null,
_killTimeout,
_killTimeout);
CurrentPhase = Phase.Playing;
var _ = OnGameStarted?.Invoke(this);
@@ -145,7 +151,7 @@ public sealed class NunchiGame : IDisposable
_killTimer.Change(_killTimeout, _killTimeout);
CurrentNumber = new NadekoRandom().Next(0, 100); // reset the counter
_passed.Clear(); // reset all users who passed (new round starts)
if(failure != null)
if (failure != null)
_participants.Remove(failure.Value); // remove the dude who failed from the list of players
var __ = OnRoundEnded?.Invoke(this, failure);
@@ -156,6 +162,7 @@ public sealed class NunchiGame : IDisposable
var _ = OnGameEnded?.Invoke(this, _participants.Count > 0 ? _participants.First().Name : null);
return;
}
CurrentPhase = Phase.WaitingForNextRound;
var throwawayDelay = Task.Run(async () =>
{
@@ -163,7 +170,6 @@ public sealed class NunchiGame : IDisposable
CurrentPhase = Phase.Playing;
var ___ = OnRoundStarted?.Invoke(this, CurrentNumber);
});
}
public void Dispose()
@@ -174,4 +180,4 @@ public sealed class NunchiGame : IDisposable
OnRoundStarted = null;
OnUserGuessed = null;
}
}
}

View File

@@ -5,11 +5,10 @@ namespace NadekoBot.Modules.Games.Common;
public class PollRunner
{
public event Func<IUserMessage, IGuildUser, Task> OnVoted;
public Poll Poll { get; }
private readonly DbService _db;
public event Func<IUserMessage, IGuildUser, Task> OnVoted;
private readonly SemaphoreSlim _locker = new(1, 1);
public PollRunner(DbService db, Poll poll)
@@ -40,11 +39,7 @@ public class PollRunner
if (usr is null)
return false;
voteObj = new()
{
UserId = msg.Author.Id,
VoteIndex = vote,
};
voteObj = new() { UserId = msg.Author.Id, VoteIndex = vote };
if (!Poll.Votes.Add(voteObj))
return false;
@@ -61,4 +56,4 @@ public class PollRunner
public void End()
=> OnVoted = null;
}
}

View File

@@ -1,30 +1,12 @@
#nullable disable
using System.Text;
using CommandLine;
using System.Text;
namespace NadekoBot.Modules.Games.Common;
public class TicTacToe
{
public class Options : INadekoCommandOptions
{
public void NormalizeOptions()
{
if (TurnTimer is < 5 or > 60)
TurnTimer = 15;
}
[Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. Default 15.")]
public int TurnTimer { get; set; } = 15;
}
private enum Phase
{
Starting,
Started,
Ended
}
public event Action<TicTacToe> OnEnded;
private readonly ITextChannel _channel;
private readonly IGuildUser[] _users;
private readonly int?[,] _state;
@@ -34,9 +16,10 @@ public class TicTacToe
private IGuildUser _winner;
private readonly string[] _numbers = { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:" };
public event Action<TicTacToe> OnEnded;
private readonly string[] _numbers =
{
":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:"
};
private IUserMessage _previousMessage;
private Timer _timeoutTimer;
@@ -45,8 +28,13 @@ public class TicTacToe
private readonly Options _options;
private readonly IEmbedBuilderService _eb;
public TicTacToe(IBotStrings strings, DiscordSocketClient client, ITextChannel channel,
IGuildUser firstUser, Options options, IEmbedBuilderService eb)
public TicTacToe(
IBotStrings strings,
DiscordSocketClient client,
ITextChannel channel,
IGuildUser firstUser,
Options options,
IEmbedBuilderService eb)
{
_channel = channel;
_strings = strings;
@@ -55,11 +43,7 @@ public class TicTacToe
_eb = eb;
_users = new[] { firstUser, null };
_state = new int?[,] {
{ null, null, null },
{ null, null, null },
{ null, null, null },
};
_state = new int?[,] { { null, null, null }, { null, null, null }, { null, null, null } };
_phase = Phase.Starting;
_moveLock = new(1, 1);
@@ -79,6 +63,7 @@ public class TicTacToe
if (j < _state.GetLength(1) - 1)
sb.Append("┃");
}
if (i < _state.GetLength(0) - 1)
sb.AppendLine("\n──────────");
}
@@ -89,9 +74,9 @@ public class TicTacToe
public IEmbedBuilder GetEmbed(string title = null)
{
var embed = _eb.Create()
.WithOkColor()
.WithDescription(Environment.NewLine + GetState())
.WithAuthor(GetText(strs.vs(_users[0], _users[1])));
.WithOkColor()
.WithDescription(Environment.NewLine + GetState())
.WithAuthor(GetText(strs.vs(_users[0], _users[1])));
if (!string.IsNullOrWhiteSpace(title))
embed.WithTitle(title);
@@ -104,7 +89,9 @@ public class TicTacToe
embed.WithFooter(GetText(strs.ttt_users_move(_users[_curUserIndex])));
}
else
{
embed.WithFooter(GetText(strs.ttt_has_won(_winner)));
}
return embed;
}
@@ -133,7 +120,8 @@ public class TicTacToe
await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.ttt_already_running));
return;
}
else if (_users[0] == user)
if (_users[0] == user)
{
await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.ttt_against_yourself));
return;
@@ -144,35 +132,38 @@ public class TicTacToe
_phase = Phase.Started;
_timeoutTimer = new(async _ =>
{
await _moveLock.WaitAsync();
try
{
if (_phase == Phase.Ended)
return;
_phase = Phase.Ended;
if (_users[1] != null)
await _moveLock.WaitAsync();
try
{
_winner = _users[_curUserIndex ^= 1];
var del = _previousMessage?.DeleteAsync();
try
{
await _channel.EmbedAsync(GetEmbed(GetText(strs.ttt_time_expired)));
if (del != null)
await del;
}
catch { }
}
if (_phase == Phase.Ended)
return;
OnEnded?.Invoke(this);
}
catch { }
finally
{
_moveLock.Release();
}
}, null, _options.TurnTimer * 1000, Timeout.Infinite);
_phase = Phase.Ended;
if (_users[1] != null)
{
_winner = _users[_curUserIndex ^= 1];
var del = _previousMessage?.DeleteAsync();
try
{
await _channel.EmbedAsync(GetEmbed(GetText(strs.ttt_time_expired)));
if (del != null)
await del;
}
catch { }
}
OnEnded?.Invoke(this);
}
catch { }
finally
{
_moveLock.Release();
}
},
null,
_options.TurnTimer * 1000,
Timeout.Infinite);
_client.MessageReceived += Client_MessageReceived;
@@ -183,13 +174,9 @@ public class TicTacToe
private bool IsDraw()
{
for (var i = 0; i < 3; i++)
{
for (var j = 0; j < 3; j++)
{
if (_state[i, j] is null)
return false;
}
}
for (var j = 0; j < 3; j++)
if (_state[i, j] is null)
return false;
return true;
}
@@ -204,10 +191,10 @@ public class TicTacToe
if (_phase == Phase.Ended || msg.Author?.Id != curUser.Id)
return;
if (int.TryParse(msg.Content, out var index) &&
--index >= 0 &&
index <= 9 &&
_state[index / 3, index % 3] is null)
if (int.TryParse(msg.Content, out var index)
&& --index >= 0
&& index <= 9
&& _state[index / 3, index % 3] is null)
{
_state[index / 3, index % 3] = _curUserIndex;
@@ -220,7 +207,8 @@ public class TicTacToe
_phase = Phase.Ended;
}
else if (_state[0, index % 3] == _state[1, index % 3] && _state[1, index % 3] == _state[2, index % 3])
else if (_state[0, index % 3] == _state[1, index % 3]
&& _state[1, index % 3] == _state[2, index % 3])
{
_state[0, index % 3] = _curUserIndex + 2;
_state[1, index % 3] = _curUserIndex + 2;
@@ -228,7 +216,9 @@ public class TicTacToe
_phase = Phase.Ended;
}
else if (_curUserIndex == _state[0, 0] && _state[0, 0] == _state[1, 1] && _state[1, 1] == _state[2, 2])
else if (_curUserIndex == _state[0, 0]
&& _state[0, 0] == _state[1, 1]
&& _state[1, 1] == _state[2, 2])
{
_state[0, 0] = _curUserIndex + 2;
_state[1, 1] = _curUserIndex + 2;
@@ -236,7 +226,9 @@ public class TicTacToe
_phase = Phase.Ended;
}
else if (_curUserIndex == _state[0, 2] && _state[0, 2] == _state[1, 1] && _state[1, 1] == _state[2, 0])
else if (_curUserIndex == _state[0, 2]
&& _state[0, 2] == _state[1, 1]
&& _state[1, 1] == _state[2, 0])
{
_state[0, 2] = _curUserIndex + 2;
_state[1, 1] = _curUserIndex + 2;
@@ -244,6 +236,7 @@ public class TicTacToe
_phase = Phase.Ended;
}
var reason = string.Empty;
if (_phase == Phase.Ended) // if user won, stop receiving moves
@@ -265,9 +258,17 @@ public class TicTacToe
{
var del1 = msg.DeleteAsync();
var del2 = _previousMessage?.DeleteAsync();
try { _previousMessage = await _channel.EmbedAsync(GetEmbed(reason)); } catch { }
try { await del1; } catch { }
try { if (del2 != null) await del2; } catch { }
try { _previousMessage = await _channel.EmbedAsync(GetEmbed(reason)); }
catch { }
try { await del1; }
catch { }
try
{
if (del2 != null) await del2;
}
catch { }
});
_curUserIndex ^= 1;
@@ -282,4 +283,23 @@ public class TicTacToe
return Task.CompletedTask;
}
}
public class Options : INadekoCommandOptions
{
[Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. Default 15.")]
public int TurnTimer { get; set; } = 15;
public void NormalizeOptions()
{
if (TurnTimer is < 5 or > 60)
TurnTimer = 15;
}
}
private enum Phase
{
Starting,
Started,
Ended
}
}

View File

@@ -1,23 +1,14 @@
#nullable disable
using System.Net;
using System.Text;
namespace NadekoBot.Modules.Games.Common.Trivia;
public class TriviaGame
{
private readonly SemaphoreSlim _guessLock = new(1, 1);
private readonly IDataCache _cache;
private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client;
private readonly GamesConfig _config;
private readonly ICurrencyService _cs;
private readonly TriviaOptions _options;
public IGuild Guild { get; }
public ITextChannel Channel { get; }
private CancellationTokenSource _triviaCancelSource;
public TriviaQuestion CurrentQuestion { get; private set; }
public HashSet<TriviaQuestion> OldQuestions { get; } = new();
@@ -25,15 +16,32 @@ public class TriviaGame
public bool GameActive { get; private set; }
public bool ShouldStopGame { get; private set; }
private readonly SemaphoreSlim _guessLock = new(1, 1);
private readonly IDataCache _cache;
private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client;
private readonly GamesConfig _config;
private readonly ICurrencyService _cs;
private readonly TriviaOptions _options;
private CancellationTokenSource _triviaCancelSource;
private readonly TriviaQuestionPool _questionPool;
private int _timeoutCount = 0;
private int _timeoutCount;
private readonly string _quitCommand;
private readonly IEmbedBuilderService _eb;
public TriviaGame(IBotStrings strings, DiscordSocketClient client, GamesConfig config,
IDataCache cache, ICurrencyService cs, IGuild guild, ITextChannel channel,
TriviaOptions options, string quitCommand, IEmbedBuilderService eb)
public TriviaGame(
IBotStrings strings,
DiscordSocketClient client,
GamesConfig config,
IDataCache cache,
ICurrencyService cs,
IGuild guild,
ITextChannel channel,
TriviaOptions options,
string quitCommand,
IEmbedBuilderService eb)
{
_cache = cache;
_questionPool = new(_cache);
@@ -63,31 +71,36 @@ public class TriviaGame
// load question
CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.IsPokemon);
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer)
|| string.IsNullOrWhiteSpace(CurrentQuestion.Question))
{
await Channel.SendErrorAsync(_eb, GetText(strs.trivia_game), GetText(strs.failed_loading_question));
return;
}
OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again
IEmbedBuilder questionEmbed;
IUserMessage questionMessage;
try
{
questionEmbed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.AddField(GetText(strs.category), CurrentQuestion.Category)
.AddField(GetText(strs.question), CurrentQuestion.Question);
questionEmbed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.AddField(GetText(strs.category), CurrentQuestion.Category)
.AddField(GetText(strs.question), CurrentQuestion.Question);
if (showHowToQuit)
questionEmbed.WithFooter(GetText(strs.trivia_quit(_quitCommand)));
if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute))
questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl);
questionMessage = await Channel.EmbedAsync(questionEmbed);
}
catch (HttpException ex) when (ex.HttpCode is System.Net.HttpStatusCode.NotFound or System.Net.HttpStatusCode.Forbidden or System.Net.HttpStatusCode.BadRequest)
catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound
or HttpStatusCode.Forbidden
or HttpStatusCode.BadRequest)
{
return;
}
@@ -112,9 +125,11 @@ public class TriviaGame
if (!_options.NoHint)
try
{
await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(CurrentQuestion.GetHint()).Build());
await questionMessage.ModifyAsync(m
=> m.Embed = questionEmbed.WithFooter(CurrentQuestion.GetHint()).Build());
}
catch (HttpException ex) when (ex.HttpCode is System.Net.HttpStatusCode.NotFound or System.Net.HttpStatusCode.Forbidden)
catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound
or HttpStatusCode.Forbidden)
{
break;
}
@@ -122,7 +137,6 @@ public class TriviaGame
//timeout
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token);
}
catch (TaskCanceledException) { _timeoutCount = 0; } //means someone guessed the answer
}
@@ -131,13 +145,14 @@ public class TriviaGame
GameActive = false;
_client.MessageReceived -= PotentialGuess;
}
if (!_triviaCancelSource.IsCancellationRequested)
{
try
{
var embed = _eb.Create().WithErrorColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_times_up(Format.Bold(CurrentQuestion.Answer))));
var embed = _eb.Create()
.WithErrorColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_times_up(Format.Bold(CurrentQuestion.Answer))));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
@@ -150,7 +165,7 @@ public class TriviaGame
{
Log.Warning(ex, "Error sending trivia time's up message");
}
}
await Task.Delay(5000);
}
}
@@ -159,10 +174,11 @@ public class TriviaGame
{
ShouldStopGame = true;
await Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithAuthor("Trivia Game Ended")
.WithTitle("Final Results")
.WithDescription(GetLeaderboard()));
await Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithAuthor("Trivia Game Ended")
.WithTitle("Final Results")
.WithDescription(GetLeaderboard()));
}
public async Task StopGame()
@@ -170,19 +186,14 @@ public class TriviaGame
var old = ShouldStopGame;
ShouldStopGame = true;
if (!old)
{
try
{
await Channel.SendConfirmAsync(_eb,
GetText(strs.trivia_game),
GetText(strs.trivia_stopping));
await Channel.SendConfirmAsync(_eb, GetText(strs.trivia_game), GetText(strs.trivia_stopping));
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia stopping message");
}
}
}
private Task PotentialGuess(SocketMessage imsg)
@@ -205,13 +216,16 @@ public class TriviaGame
await _guessLock.WaitAsync();
try
{
if (GameActive && CurrentQuestion.IsAnswerCorrect(umsg.Content) && !_triviaCancelSource.IsCancellationRequested)
if (GameActive
&& CurrentQuestion.IsAnswerCorrect(umsg.Content)
&& !_triviaCancelSource.IsCancellationRequested)
{
Users.AddOrUpdate(guildUser, 1, (gu, old) => ++old);
guess = true;
}
}
finally { _guessLock.Release(); }
if (!guess) return;
_triviaCancelSource.Cancel();
@@ -221,11 +235,11 @@ public class TriviaGame
ShouldStopGame = true;
try
{
var embedS = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_win(
guildUser.Mention,
Format.Bold(CurrentQuestion.Answer))));
var embedS = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_win(guildUser.Mention,
Format.Bold(CurrentQuestion.Answer))));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl);
await Channel.EmbedAsync(embedS);
@@ -234,14 +248,18 @@ public class TriviaGame
{
// ignored
}
var reward = _config.Trivia.CurrencyReward;
if (reward > 0)
await _cs.AddAsync(guildUser, "Won trivia", reward, true);
return;
}
var embed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_guess(guildUser.Mention, Format.Bold(CurrentQuestion.Answer))));
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_guess(guildUser.Mention,
Format.Bold(CurrentQuestion.Answer))));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
await Channel.EmbedAsync(embed);
@@ -259,10 +277,8 @@ public class TriviaGame
var sb = new StringBuilder();
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value))
{
sb.AppendLine(GetText(strs.trivia_points(Format.Bold(kvp.Key.ToString()), kvp.Value)));
}
return sb.ToString();
}
}
}

View File

@@ -7,13 +7,29 @@ public class TriviaOptions : INadekoCommandOptions
{
[Option('p', "pokemon", Required = false, Default = false, HelpText = "Whether it's 'Who's that pokemon?' trivia.")]
public bool IsPokemon { get; set; } = false;
[Option("nohint", Required = false, Default = false, HelpText = "Don't show any hints.")]
public bool NoHint { get; set; } = false;
[Option('w', "win-req", Required = false, Default = 10, HelpText = "Winning requirement. Set 0 for an infinite game. Default 10.")]
[Option('w',
"win-req",
Required = false,
Default = 10,
HelpText = "Winning requirement. Set 0 for an infinite game. Default 10.")]
public int WinRequirement { get; set; } = 10;
[Option('q', "question-timer", Required = false, Default = 30, HelpText = "How long until the question ends. Default 30.")]
[Option('q',
"question-timer",
Required = false,
Default = 30,
HelpText = "How long until the question ends. Default 30.")]
public int QuestionTimer { get; set; } = 30;
[Option('t', "timeout", Required = false, Default = 10, HelpText = "Number of questions of inactivity in order stop. Set 0 for never. Default 10.")]
[Option('t',
"timeout",
Required = false,
Default = 10,
HelpText = "Number of questions of inactivity in order stop. Set 0 for never. Default 10.")]
public int Timeout { get; set; } = 10;
public void NormalizeOptions()
@@ -24,6 +40,5 @@ public class TriviaOptions : INadekoCommandOptions
QuestionTimer = 30;
if (Timeout is < 0 or > 20)
Timeout = 10;
}
}
}

View File

@@ -6,46 +6,47 @@ namespace NadekoBot.Modules.Games.Common.Trivia;
public class TriviaQuestion
{
public const int maxStringLength = 22;
//represents the min size to judge levDistance with
private static readonly HashSet<Tuple<int, int>> strictness = new()
{
new(9, 0),
new(14, 1),
new(19, 2),
new(22, 3),
new(9, 0), new(14, 1), new(19, 2), new(22, 3)
};
public const int maxStringLength = 22;
public string Category { get; set; }
public string Question { get; set; }
public string ImageUrl { get; set; }
public string AnswerImageUrl { get; set; }
public string Answer { get; set; }
private string _cleanAnswer;
public string CleanAnswer => _cleanAnswer ?? (_cleanAnswer = Clean(Answer));
public TriviaQuestion(string q, string a, string c, string img = null, string answerImage = null)
public string CleanAnswer
=> _cleanAnswer ?? (_cleanAnswer = Clean(Answer));
private string _cleanAnswer;
public TriviaQuestion(
string q,
string a,
string c,
string img = null,
string answerImage = null)
{
this.Question = q;
this.Answer = a;
this.Category = c;
this.ImageUrl = img;
this.AnswerImageUrl = answerImage ?? img;
Question = q;
Answer = a;
Category = c;
ImageUrl = img;
AnswerImageUrl = answerImage ?? img;
}
public string GetHint() => Scramble(Answer);
public string GetHint()
=> Scramble(Answer);
public bool IsAnswerCorrect(string guess)
{
if (Answer.Equals(guess, StringComparison.InvariantCulture))
{
return true;
}
if (Answer.Equals(guess, StringComparison.InvariantCulture)) return true;
var cleanGuess = Clean(guess);
if (CleanAnswer.Equals(cleanGuess, StringComparison.InvariantCulture))
{
return true;
}
if (CleanAnswer.Equals(cleanGuess, StringComparison.InvariantCulture)) return true;
var levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess);
var levDistanceNormal = Answer.LevenshteinDistance(guess);
@@ -56,15 +57,13 @@ public class TriviaQuestion
private static bool JudgeGuess(int guessLength, int answerLength, int levDistance)
{
foreach (var level in strictness)
{
if (guessLength <= level.Item1 || answerLength <= level.Item1)
{
if (levDistance <= level.Item2)
return true;
else
return false;
return false;
}
}
return false;
}
@@ -102,6 +101,8 @@ public class TriviaQuestion
if (letters[i] != ' ')
letters[i] = '_';
}
return string.Join(" ", new string(letters).Replace(" ", " \u2000", StringComparison.InvariantCulture).AsEnumerable());
return string.Join(" ",
new string(letters).Replace(" ", " \u2000", StringComparison.InvariantCulture).AsEnumerable());
}
}
}

View File

@@ -3,14 +3,17 @@ namespace NadekoBot.Modules.Games.Common.Trivia;
public class TriviaQuestionPool
{
private TriviaQuestion[] Pool
=> _cache.LocalData.TriviaQuestions;
private IReadOnlyDictionary<int, string> Map
=> _cache.LocalData.PokemonMap;
private readonly IDataCache _cache;
private readonly int maxPokemonId;
private readonly NadekoRandom _rng = new();
private TriviaQuestion[] Pool => _cache.LocalData.TriviaQuestions;
private IReadOnlyDictionary<int, string> Map => _cache.LocalData.PokemonMap;
public TriviaQuestionPool(IDataCache cache)
{
_cache = cache;
@@ -31,9 +34,10 @@ public class TriviaQuestionPool
$@"https://nadeko.bot/images/pokemon/shadows/{num}.png",
$@"https://nadeko.bot/images/pokemon/real/{num}.png");
}
TriviaQuestion randomQuestion;
while (exclude.Contains(randomQuestion = Pool[_rng.Next(0, Pool.Length)])) ;
return randomQuestion;
}
}
}

View File

@@ -6,4 +6,4 @@ public class TypingArticle
public string Source { get; set; }
public string Extra { get; set; }
public string Text { get; set; }
}
}

View File

@@ -1,24 +1,12 @@
#nullable disable
using System.Diagnostics;
using NadekoBot.Modules.Games.Services;
using CommandLine;
using NadekoBot.Modules.Games.Services;
using System.Diagnostics;
namespace NadekoBot.Modules.Games.Common;
public class TypingGame
{
public class Options : INadekoCommandOptions
{
[Option('s', "start-time", Default = 5, Required = false, HelpText = "How long does it take for the race to start. Default 5.")]
public int StartTime { get; set; } = 5;
public void NormalizeOptions()
{
if (StartTime is < 3 or > 30)
StartTime = 5;
}
}
public const float WORD_VALUE = 4.5f;
public ITextChannel Channel { get; }
public string CurrentSentence { get; private set; }
@@ -31,8 +19,13 @@ public class TypingGame
private readonly Options _options;
private readonly IEmbedBuilderService _eb;
public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel,
string prefix, Options options, IEmbedBuilderService eb)
public TypingGame(
GamesService games,
DiscordSocketClient client,
ITextChannel channel,
string prefix,
Options options,
IEmbedBuilderService eb)
{
_games = games;
_client = client;
@@ -40,7 +33,7 @@ public class TypingGame
_options = options;
_eb = eb;
this.Channel = channel;
Channel = channel;
IsActive = false;
sw = new();
finishedUserIds = new();
@@ -80,19 +73,19 @@ public class TypingGame
var time = _options.StartTime;
var msg = await Channel.SendMessageAsync($"Starting new typing contest in **{time}**...", options: new()
{
RetryMode = RetryMode.AlwaysRetry
});
var msg = await Channel.SendMessageAsync($"Starting new typing contest in **{time}**...",
options: new() { RetryMode = RetryMode.AlwaysRetry });
do
{
await Task.Delay(2000);
time -= 2;
try { await msg.ModifyAsync(m => m.Content = $"Starting new typing contest in **{time}**.."); } catch { }
try { await msg.ModifyAsync(m => m.Content = $"Starting new typing contest in **{time}**.."); }
catch { }
} while (time > 2);
await msg.ModifyAsync(m => {
await msg.ModifyAsync(m =>
{
m.Content = CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture);
});
sw.Start();
@@ -105,7 +98,6 @@ public class TypingGame
if (!IsActive)
return;
}
}
catch { }
finally
@@ -118,9 +110,7 @@ public class TypingGame
{
if (_games.TypingArticles.Any())
return _games.TypingArticles[new NadekoRandom().Next(0, _games.TypingArticles.Count)].Text;
else
return $"No typing articles found. Use {_prefix}typeadd command to add a new article for typing.";
return $"No typing articles found. Use {_prefix}typeadd command to add a new article for typing.";
}
private void HandleAnswers()
@@ -137,7 +127,7 @@ public class TypingGame
if (imsg is not SocketUserMessage msg)
return;
if (this.Channel is null || this.Channel.Id != msg.Channel.Id) return;
if (Channel is null || Channel.Id != msg.Channel.Id) return;
var guess = msg.Content;
@@ -148,18 +138,17 @@ public class TypingGame
var elapsed = sw.Elapsed;
var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60;
finishedUserIds.Add(msg.Author.Id);
await this.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithTitle($"{msg.Author} finished the race!")
.AddField("Place", $"#{finishedUserIds.Count}", true)
.AddField("WPM", $"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*", true)
.AddField("Errors", distance.ToString(), true));
await Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle($"{msg.Author} finished the race!")
.AddField("Place", $"#{finishedUserIds.Count}", true)
.AddField("WPM", $"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*", true)
.AddField("Errors", distance.ToString(), true));
if (finishedUserIds.Count % 4 == 0)
{
await this.Channel.SendConfirmAsync(_eb,
$":exclamation: A lot of people finished, here is the text for those still typing:" +
$"\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture)).SanitizeMentions(true)}**");
}
await Channel.SendConfirmAsync(_eb,
":exclamation: A lot of people finished, here is the text for those still typing:"
+ $"\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture)).SanitizeMentions(true)}**");
}
}
catch (Exception ex)
@@ -170,6 +159,22 @@ public class TypingGame
return Task.CompletedTask;
}
private static bool Judge(int errors, int textLength) => errors <= textLength / 25;
private static bool Judge(int errors, int textLength)
=> errors <= textLength / 25;
}
public class Options : INadekoCommandOptions
{
[Option('s',
"start-time",
Default = 5,
Required = false,
HelpText = "How long does it take for the race to start. Default 5.")]
public int StartTime { get; set; } = 5;
public void NormalizeOptions()
{
if (StartTime is < 3 or > 30)
StartTime = 5;
}
}
}

View File

@@ -1,2 +1 @@
#nullable disable
#nullable disable

View File

@@ -20,7 +20,8 @@ public partial class Games : NadekoModule<GamesService>
_httpFactory = factory;
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
public async Task Choose([Leftover] string list = null)
{
if (string.IsNullOrWhiteSpace(list))
@@ -32,20 +33,23 @@ public partial class Games : NadekoModule<GamesService>
await SendConfirmAsync("🤔", listArr[rng.Next(0, listArr.Length)]);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
public async Task EightBall([Leftover] string question = null)
{
if (string.IsNullOrWhiteSpace(question))
return;
var res = _service.GetEightballResponse(ctx.User.Id, question);
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription(ctx.User.ToString())
.AddField("❓ " + GetText(strs.question), question, false)
.AddField("🎱 " + GetText(strs._8ball), res, false));
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithDescription(ctx.User.ToString())
.AddField(" " + GetText(strs.question), question)
.AddField("🎱 " + GetText(strs._8ball), res));
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task RateGirl([Leftover] IGuildUser usr)
{
@@ -64,16 +68,17 @@ public partial class Games : NadekoModule<GamesService>
originalStream.Position = 0;
originalStream.CopyTo(imgStream);
}
imgStream.Position = 0;
await ctx.Channel.SendFileAsync(stream: imgStream,
filename: $"girl_{usr}.png",
text: Format.Bold($"{ctx.User.Mention} Girl Rating For {usr}"),
await ctx.Channel.SendFileAsync(imgStream,
$"girl_{usr}.png",
Format.Bold($"{ctx.User.Mention} Girl Rating For {usr}"),
embed: _eb.Create()
.WithOkColor()
.AddField("Hot", gr.Hot.ToString("F2"), true)
.AddField("Crazy", gr.Crazy.ToString("F2"), true)
.AddField("Advice", gr.Advice, false)
.Build());
.WithOkColor()
.AddField("Hot", gr.Hot.ToString("F2"), true)
.AddField("Crazy", gr.Crazy.ToString("F2"), true)
.AddField("Advice", gr.Advice)
.Build());
}
private double NextDouble(double x, double y)
@@ -136,13 +141,13 @@ public partial class Games : NadekoModule<GamesService>
return new(_images, _httpFactory, crazy, hot, roll, advice);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
public async Task Linux(string guhnoo, string loonix)
=> await SendConfirmAsync(
$@"I'd just like to interject for moment. What you're refering to as {loonix}, is in fact, {guhnoo}/{loonix}, or as I've recently taken to calling it, {guhnoo} plus {loonix}. {loonix} is not an operating system unto itself, but rather another free component of a fully functioning {guhnoo} system made useful by the {guhnoo} corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX.
Many computer users run a modified version of the {guhnoo} system every day, without realizing it. Through a peculiar turn of events, the version of {guhnoo} which is widely used today is often called {loonix}, and many of its users are not aware that it is basically the {guhnoo} system, developed by the {guhnoo} Project.
There really is a {loonix}, and these people are using it, but it is just a part of the system they use. {loonix} is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. {loonix} is normally used in combination with the {guhnoo} operating system: the whole system is basically {guhnoo} with {loonix} added, or {guhnoo}/{loonix}. All the so-called {loonix} distributions are really distributions of {guhnoo}/{loonix}."
);
}
There really is a {loonix}, and these people are using it, but it is just a part of the system they use. {loonix} is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. {loonix} is normally used in combination with the {guhnoo} operating system: the whole system is basically {guhnoo} with {loonix} added, or {guhnoo}/{loonix}. All the so-called {loonix} distributions are really distributions of {guhnoo}/{loonix}.");
}

View File

@@ -7,12 +7,11 @@ public partial class Games
[Group]
public class HangmanCommands : NadekoSubmodule<IHangmanService>
{
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Hangmanlist()
=> await SendConfirmAsync(
GetText(strs.hangman_types(Prefix)),
_service.GetHangmanTypes().Join('\n'));
=> await SendConfirmAsync(GetText(strs.hangman_types(Prefix)), _service.GetHangmanTypes().Join('\n'));
private static string Draw(HangmanGame.State state)
=> $@". ┌─────┐
@@ -27,28 +26,26 @@ public partial class Games
{
if (state.Phase == HangmanGame.Phase.Running)
return eb.Create()
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.Join(' '));
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.Join(' '));
if (state.Phase == HangmanGame.Phase.Ended && state.Failed)
return eb.Create()
.WithErrorColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.Join(' '));
else
{
return eb.Create()
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.Join(' '));
}
.WithErrorColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.Join(' '));
return eb.Create()
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.Join(' '));
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Hangman([Leftover] string? type = null)
{
@@ -63,14 +60,12 @@ public partial class Games
await ctx.Channel.EmbedAsync(eb);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task HangmanStop()
{
if (await _service.StopHangman(ctx.Channel.Id))
{
await ReplyConfirmLocalizedAsync(strs.hangman_stopped);
}
if (await _service.StopHangman(ctx.Channel.Id)) await ReplyConfirmLocalizedAsync(strs.hangman_stopped);
}
}
}
}

View File

@@ -14,7 +14,8 @@ public partial class Games
public NunchiCommands(DiscordSocketClient client)
=> _client = client;
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Nunchi()
{
@@ -26,18 +27,17 @@ public partial class Games
{
// join it
if (!await nunchi.Join(ctx.User.Id, ctx.User.ToString()))
{
// if you failed joining, that means game is running or just ended
// await ReplyErrorLocalized("nunchi_already_started");
return;
}
await ReplyErrorLocalizedAsync(strs.nunchi_joined(nunchi.ParticipantCount));
return;
}
try { await ConfirmLocalizedAsync(strs.nunchi_created); } catch { }
try { await ConfirmLocalizedAsync(strs.nunchi_created); }
catch { }
nunchi.OnGameEnded += Nunchi_OnGameEnded;
//nunchi.OnGameStarted += Nunchi_OnGameStarted;
@@ -45,7 +45,7 @@ public partial class Games
nunchi.OnUserGuessed += Nunchi_OnUserGuessed;
nunchi.OnRoundStarted += Nunchi_OnRoundStarted;
_client.MessageReceived += _client_MessageReceived;
var success = await nunchi.Initialize();
if (!success)
{
@@ -84,14 +84,12 @@ public partial class Games
if (arg2 is null)
return ConfirmLocalizedAsync(strs.nunchi_ended_no_winner);
else
return ConfirmLocalizedAsync(strs.nunchi_ended(Format.Bold(arg2)));
return ConfirmLocalizedAsync(strs.nunchi_ended(Format.Bold(arg2)));
}
}
private Task Nunchi_OnRoundStarted(NunchiGame arg, int cur)
=> ConfirmLocalizedAsync(strs.nunchi_round_started(
Format.Bold(arg.ParticipantCount.ToString()),
=> ConfirmLocalizedAsync(strs.nunchi_round_started(Format.Bold(arg.ParticipantCount.ToString()),
Format.Bold(cur.ToString())));
private Task Nunchi_OnUserGuessed(NunchiGame arg)
@@ -99,14 +97,16 @@ public partial class Games
private Task Nunchi_OnRoundEnded(NunchiGame arg1, (ulong Id, string Name)? arg2)
{
if(arg2.HasValue)
if (arg2.HasValue)
return ConfirmLocalizedAsync(strs.nunchi_round_ended(Format.Bold(arg2.Value.Name)));
else
return ConfirmLocalizedAsync(strs.nunchi_round_ended_boot(
Format.Bold("\n" + string.Join("\n, ", arg1.Participants.Select(x => x.Name))))); // this won't work if there are too many users
return ConfirmLocalizedAsync(strs.nunchi_round_ended_boot(
Format.Bold("\n"
+ string.Join("\n, ",
arg1.Participants.Select(x
=> x.Name))))); // this won't work if there are too many users
}
private Task Nunchi_OnGameStarted(NunchiGame arg)
=> ConfirmLocalizedAsync(strs.nunchi_started(Format.Bold(arg.ParticipantCount.ToString())));
}
}
}

View File

@@ -15,7 +15,8 @@ public partial class Games
public PollCommands(DiscordSocketClient client)
=> _client = client;
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[UserPerm(GuildPerm.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task Poll([Leftover] string arg)
@@ -23,31 +24,28 @@ public partial class Games
if (string.IsNullOrWhiteSpace(arg))
return;
var poll = _service.CreatePoll(ctx.Guild.Id,
ctx.Channel.Id, arg);
if(poll is null)
var poll = _service.CreatePoll(ctx.Guild.Id, ctx.Channel.Id, arg);
if (poll is null)
{
await ReplyErrorLocalizedAsync(strs.poll_invalid_input);
return;
}
if (_service.StartPoll(poll))
{
await ctx.Channel
.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.poll_created(ctx.User.ToString())))
.WithDescription(
Format.Bold(poll.Question) + "\n\n" +
string.Join("\n", poll.Answers
.Select(x => $"`{x.Index + 1}.` {Format.Bold(x.Text)}"))));
}
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.poll_created(ctx.User.ToString())))
.WithDescription(Format.Bold(poll.Question)
+ "\n\n"
+ string.Join("\n",
poll.Answers.Select(x
=> $"`{x.Index + 1}.` {Format.Bold(x.Text)}"))));
else
{
await ReplyErrorLocalizedAsync(strs.poll_already_running);
}
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[UserPerm(GuildPerm.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task PollStats()
@@ -58,7 +56,8 @@ public partial class Games
await ctx.Channel.EmbedAsync(GetStats(pr.Poll, GetText(strs.current_poll_results)));
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[UserPerm(GuildPerm.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task Pollend()
@@ -75,39 +74,32 @@ public partial class Games
public IEmbedBuilder GetStats(Poll poll, string title)
{
var results = poll.Votes.GroupBy(kvp => kvp.VoteIndex)
.ToDictionary(x => x.Key, x => x.Sum(kvp => 1));
var results = poll.Votes.GroupBy(kvp => kvp.VoteIndex).ToDictionary(x => x.Key, x => x.Sum(kvp => 1));
var totalVotesCast = results.Sum(x => x.Value);
var eb = _eb.Create().WithTitle(title);
var sb = new StringBuilder()
.AppendLine(Format.Bold(poll.Question))
.AppendLine();
var sb = new StringBuilder().AppendLine(Format.Bold(poll.Question)).AppendLine();
var stats = poll.Answers
.Select(x =>
{
results.TryGetValue(x.Index, out var votes);
var stats = poll.Answers.Select(x =>
{
results.TryGetValue(x.Index, out var votes);
return (x.Index, votes, x.Text);
})
.OrderByDescending(x => x.votes)
.ToArray();
return (x.Index, votes, x.Text);
})
.OrderByDescending(x => x.votes)
.ToArray();
for (var i = 0; i < stats.Length; i++)
{
var (Index, votes, Text) = stats[i];
sb.AppendLine(GetText(strs.poll_result(
Index + 1,
Format.Bold(Text),
Format.Bold(votes.ToString()))));
sb.AppendLine(GetText(strs.poll_result(Index + 1, Format.Bold(Text), Format.Bold(votes.ToString()))));
}
return eb.WithDescription(sb.ToString())
.WithFooter(GetText(strs.x_votes_cast(totalVotesCast)))
.WithOkColor();
.WithFooter(GetText(strs.x_votes_cast(totalVotesCast)))
.WithOkColor();
}
}
}
}

View File

@@ -1,13 +1,18 @@
#nullable disable
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Games.Common.ChatterBot;
using NadekoBot.Modules.Permissions.Common;
using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Modules.Games.Common.ChatterBot;
namespace NadekoBot.Modules.Games.Services;
public class ChatterBotService : IEarlyBehavior
{
public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
public int Priority
=> 1;
private readonly DiscordSocketClient _client;
private readonly PermissionService _perms;
private readonly CommandHandler _cmd;
@@ -16,13 +21,15 @@ public class ChatterBotService : IEarlyBehavior
private readonly IEmbedBuilderService _eb;
private readonly IHttpClientFactory _httpFactory;
public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
public int Priority => 1;
public ChatterBotService(DiscordSocketClient client, PermissionService perms,
Bot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory,
IBotCredentials creds, IEmbedBuilderService eb)
public ChatterBotService(
DiscordSocketClient client,
PermissionService perms,
Bot bot,
CommandHandler cmd,
IBotStrings strings,
IHttpClientFactory factory,
IBotCredentials creds,
IEmbedBuilderService eb)
{
_client = client;
_perms = perms;
@@ -32,18 +39,16 @@ public class ChatterBotService : IEarlyBehavior
_eb = eb;
_httpFactory = factory;
ChatterBotGuilds = new(
bot.AllGuildConfigs
.Where(gc => gc.CleverbotEnabled)
.ToDictionary(gc => gc.GuildId, gc => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
ChatterBotGuilds = new(bot.AllGuildConfigs.Where(gc => gc.CleverbotEnabled)
.ToDictionary(gc => gc.GuildId,
gc => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
}
public IChatterBotSession CreateSession()
{
if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory);
else
return new CleverbotIOSession("GAh3wUfzDCpDpdpT", "RStKgqn7tcO9blbrv4KbXM8NDlb7H37C", _httpFactory);
return new CleverbotIOSession("GAh3wUfzDCpDpdpT", "RStKgqn7tcO9blbrv4KbXM8NDlb7H37C", _httpFactory);
}
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
@@ -64,17 +69,11 @@ public class ChatterBotService : IEarlyBehavior
var nickMention = $"<@!{nadekoId}> ";
string message;
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
{
message = msg.Content[normalMention.Length..].Trim();
}
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
{
message = msg.Content[nickMention.Length..].Trim();
}
else
{
return null;
}
return message;
}
@@ -92,6 +91,7 @@ public class ChatterBotService : IEarlyBehavior
{
await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)); // try twice :\
}
return true;
}
@@ -106,19 +106,19 @@ public class ChatterBotService : IEarlyBehavior
return false;
var pc = _perms.GetCacheFor(guild.Id);
if (!pc.Permissions.CheckPermissions(usrMsg,
"cleverbot",
"Games".ToLowerInvariant(),
out var index))
if (!pc.Permissions.CheckPermissions(usrMsg, "cleverbot", "Games".ToLowerInvariant(), out var index))
{
if (pc.Verbose)
{
var returnMsg = _strings.GetText(strs.perm_prevent(index + 1,
Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(sg), sg))));
try { await usrMsg.Channel.SendErrorAsync(_eb, returnMsg); } catch { }
try { await usrMsg.Channel.SendErrorAsync(_eb, returnMsg); }
catch { }
Log.Information(returnMsg);
}
return true;
}
@@ -135,8 +135,9 @@ Message: {usrMsg.Content}");
}
catch (Exception ex)
{
Log.Warning(ex,"Error in cleverbot");
Log.Warning(ex, "Error in cleverbot");
}
return false;
}
}
}

View File

@@ -6,35 +6,39 @@ namespace NadekoBot.Modules.Games.Services;
public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
{
public override string Name { get; } = "games";
private const string FilePath = "data/games.yml";
private static readonly TypedKey<GamesConfig> changeKey = new("config.games.updated");
public override string Name { get; } = "games";
public GamesConfigService(IConfigSeria serializer, IPubSub pubSub)
: base(FilePath, serializer, pubSub, changeKey)
{
AddParsedProp("trivia.min_win_req", gs => gs.Trivia.MinimumWinReq, int.TryParse,
ConfigPrinters.ToString, val => val > 0);
AddParsedProp("trivia.currency_reward", gs => gs.Trivia.CurrencyReward, long.TryParse,
ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("hangman.currency_reward", gs => gs.Hangman.CurrencyReward, long.TryParse,
ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("trivia.min_win_req",
gs => gs.Trivia.MinimumWinReq,
int.TryParse,
ConfigPrinters.ToString,
val => val > 0);
AddParsedProp("trivia.currency_reward",
gs => gs.Trivia.CurrencyReward,
long.TryParse,
ConfigPrinters.ToString,
val => val >= 0);
AddParsedProp("hangman.currency_reward",
gs => gs.Hangman.CurrencyReward,
long.TryParse,
ConfigPrinters.ToString,
val => val >= 0);
Migrate();
}
private void Migrate()
{
if (data.Version < 1)
{
ModifyConfig(c =>
{
c.Version = 1;
c.Hangman = new()
{
CurrencyReward = 0
};
c.Hangman = new() { CurrencyReward = 0 };
});
}
}
}
}

View File

@@ -1,27 +1,21 @@
#nullable disable
using Microsoft.Extensions.Caching.Memory;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Modules.Games.Common.Acrophobia;
using NadekoBot.Modules.Games.Common.Nunchi;
using NadekoBot.Modules.Games.Common.Trivia;
using Newtonsoft.Json;
using Microsoft.Extensions.Caching.Memory;
namespace NadekoBot.Modules.Games.Services;
public class GamesService : INService
{
private readonly GamesConfigService _gamesConfig;
private const string TypingArticlesPath = "data/typing_articles3.json";
public ConcurrentDictionary<ulong, GirlRating> GirlRatings { get; } = new();
public IReadOnlyList<string> EightBallResponses => _gamesConfig.Data.EightBallResponses;
private readonly Timer _t;
private readonly IHttpClientFactory _httpFactory;
private readonly IMemoryCache _8BallCache;
private readonly Random _rng;
private const string TypingArticlesPath = "data/typing_articles3.json";
public IReadOnlyList<string> EightBallResponses
=> _gamesConfig.Data.EightBallResponses;
public List<TypingArticle> TypingArticles { get; } = new();
@@ -33,36 +27,30 @@ public class GamesService : INService
public ConcurrentDictionary<ulong, NunchiGame> NunchiGames { get; } = new();
public AsyncLazy<RatingTexts> Ratings { get; }
private readonly GamesConfigService _gamesConfig;
public class RatingTexts
{
public string Nog { get; set; }
public string Tra { get; set; }
public string Fun { get; set; }
public string Uni { get; set; }
public string Wif { get; set; }
public string Dat { get; set; }
public string Dan { get; set; }
}
private readonly Timer _t;
private readonly IHttpClientFactory _httpFactory;
private readonly IMemoryCache _8BallCache;
private readonly Random _rng;
public GamesService(GamesConfigService gamesConfig, IHttpClientFactory httpFactory)
{
_gamesConfig = gamesConfig;
_httpFactory = httpFactory;
_8BallCache = new MemoryCache(new MemoryCacheOptions()
{
SizeLimit = 500_000
});
_8BallCache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 500_000 });
Ratings = new(GetRatingTexts);
_rng = new NadekoRandom();
//girl ratings
_t = new(_ =>
{
GirlRatings.Clear();
}, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1));
{
GirlRatings.Clear();
},
null,
TimeSpan.FromDays(1),
TimeSpan.FromDays(1));
try
{
@@ -78,7 +66,8 @@ public class GamesService : INService
private async Task<RatingTexts> GetRatingTexts()
{
using var http = _httpFactory.CreateClient();
var text = await http.GetStringAsync("https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/rates.json");
var text = await http.GetStringAsync(
"https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/rates.json");
return JsonConvert.DeserializeObject<RatingTexts>(text);
}
@@ -88,19 +77,21 @@ public class GamesService : INService
{
Source = user.ToString(),
Extra = $"Text added on {DateTime.UtcNow} by {user}.",
Text = text.SanitizeMentions(true),
Text = text.SanitizeMentions(true)
});
File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(TypingArticles));
}
public string GetEightballResponse(ulong userId, string question)
=> _8BallCache.GetOrCreate($"8ball:{userId}:{question}", e =>
{
e.Size = question.Length;
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
return EightBallResponses[_rng.Next(0, EightBallResponses.Count)];;
});
=> _8BallCache.GetOrCreate($"8ball:{userId}:{question}",
e =>
{
e.Size = question.Length;
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
return EightBallResponses[_rng.Next(0, EightBallResponses.Count)];
;
});
public TypingArticle RemoveTypingArticle(int index)
{
@@ -110,8 +101,19 @@ public class GamesService : INService
var removed = articles[index];
TypingArticles.RemoveAt(index);
File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(articles));
return removed;
}
}
public class RatingTexts
{
public string Nog { get; set; }
public string Tra { get; set; }
public string Fun { get; set; }
public string Uni { get; set; }
public string Wif { get; set; }
public string Dat { get; set; }
public string Dan { get; set; }
}
}

View File

@@ -1,9 +1,9 @@
#nullable disable
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Services.Database.Models;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Games.Services;
@@ -11,7 +11,8 @@ public class PollService : IEarlyBehavior
{
public ConcurrentDictionary<ulong, PollRunner> ActivePolls { get; } = new();
public int Priority => 5;
public int Priority
=> 5;
private readonly DbService _db;
private readonly IBotStrings _strs;
@@ -25,13 +26,14 @@ public class PollService : IEarlyBehavior
using var uow = db.GetDbContext();
ActivePolls = uow.Poll.GetAllPolls()
.ToDictionary(x => x.GuildId, x =>
{
var pr = new PollRunner(db, x);
pr.OnVoted += Pr_OnVoted;
return pr;
})
.ToConcurrent();
.ToDictionary(x => x.GuildId,
x =>
{
var pr = new PollRunner(db, x);
pr.OnVoted += Pr_OnVoted;
return pr;
})
.ToConcurrent();
}
public Poll CreatePoll(ulong guildId, ulong channelId, string input)
@@ -42,8 +44,7 @@ public class PollService : IEarlyBehavior
if (data.Length < 3)
return null;
var col = new IndexedCollection<PollAnswer>(data.Skip(1)
.Select(x => new PollAnswer() { Text = x }));
var col = new IndexedCollection<PollAnswer>(data.Skip(1).Select(x => new PollAnswer { Text = x }));
return new()
{
@@ -57,7 +58,7 @@ public class PollService : IEarlyBehavior
public bool StartPoll(Poll p)
{
var pr = new PollRunner(_db, p);
var pr = new PollRunner(_db, p);
if (ActivePolls.TryAdd(p.GuildId, pr))
{
using (var uow = _db.GetDbContext())
@@ -69,6 +70,7 @@ public class PollService : IEarlyBehavior
pr.OnVoted += Pr_OnVoted;
return true;
}
return false;
}
@@ -77,22 +79,24 @@ public class PollService : IEarlyBehavior
if (ActivePolls.TryRemove(guildId, out var pr))
{
pr.OnVoted -= Pr_OnVoted;
using var uow = _db.GetDbContext();
uow.RemovePoll(pr.Poll.Id);
uow.SaveChanges();
return pr.Poll;
}
return null;
}
private async Task Pr_OnVoted(IUserMessage msg, IGuildUser usr)
{
var toDelete = await msg.Channel.SendConfirmAsync(_eb,
_strs.GetText(strs.poll_voted(Format.Bold(usr.ToString())), usr.GuildId));
var toDelete = await msg.Channel.SendConfirmAsync(_eb,
_strs.GetText(strs.poll_voted(Format.Bold(usr.ToString())), usr.GuildId));
toDelete.DeleteAfter(5);
try { await msg.DeleteAsync(); } catch { }
try { await msg.DeleteAsync(); }
catch { }
}
public async Task<bool> RunBehavior(IGuild guild, IUserMessage msg)
@@ -108,14 +112,12 @@ public class PollService : IEarlyBehavior
var voted = await poll.TryVote(msg);
if (voted)
{
Log.Information("User {UserName} [{UserId}] voted in a poll on {GuildName} [{GuildId}] server",
msg.Author.ToString(),
msg.Author.Id,
guild.Name,
guild.Id);
}
return voted;
}
catch (Exception ex)
@@ -125,4 +127,4 @@ public class PollService : IEarlyBehavior
return false;
}
}
}

View File

@@ -18,7 +18,8 @@ public partial class Games
_client = client;
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptionsAttribute(typeof(TypingGame.Options))]
public async Task TypeStart(params string[] args)
@@ -26,19 +27,17 @@ public partial class Games
var (options, _) = OptionsParser.ParseFrom(new TypingGame.Options(), args);
var channel = (ITextChannel)ctx.Channel;
var game = _service.RunningContests.GetOrAdd(ctx.Guild.Id, id => new(_games, _client, channel, Prefix, options, _eb));
var game = _service.RunningContests.GetOrAdd(ctx.Guild.Id,
id => new(_games, _client, channel, Prefix, options, _eb));
if (game.IsActive)
{
await SendErrorAsync($"Contest already running in {game.Channel.Mention} channel.");
}
else
{
await game.Start();
}
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task TypeStop()
{
@@ -47,12 +46,13 @@ public partial class Games
await game.Stop();
return;
}
await SendErrorAsync("No contest to stop on this channel.");
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Typeadd([Leftover] string text)
@@ -60,12 +60,13 @@ public partial class Games
if (string.IsNullOrWhiteSpace(text))
return;
_games.AddTypingArticle(ctx.User, text);
_games.AddTypingArticle(ctx.User, text);
await SendConfirmAsync("Added new article for typing game.");
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Typelist(int page = 1)
{
@@ -79,28 +80,28 @@ public partial class Games
await SendErrorAsync($"{ctx.User.Mention} `No articles found on that page.`");
return;
}
var i = (page - 1) * 15;
await SendConfirmAsync("List of articles for Type Race", string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}")));
await SendConfirmAsync("List of articles for Type Race",
string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}")));
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Typedel(int index)
{
var removed = _service.RemoveTypingArticle(--index);
if (removed is null)
{
return;
}
if (removed is null) return;
var embed = _eb.Create()
.WithTitle($"Removed typing article #{index + 1}")
.WithDescription(removed.Text.TrimTo(50))
.WithOkColor();
.WithTitle($"Removed typing article #{index + 1}")
.WithDescription(removed.Text.TrimTo(50))
.WithOkColor();
await ctx.Channel.EmbedAsync(embed);
}
}
}
}

View File

@@ -15,7 +15,8 @@ public partial class Games
public TicTacToeCommands(DiscordSocketClient client)
=> _client = client;
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptions(typeof(TicTacToe.Options))]
public async Task TicTacToe(params string[] args)
@@ -34,7 +35,8 @@ public partial class Games
});
return;
}
game = new(base.Strings, this._client, channel, (IGuildUser)ctx.User, options, _eb);
game = new(Strings, _client, channel, (IGuildUser)ctx.User, options, _eb);
_service.TicTacToeGames.Add(channel.Id, game);
await ReplyConfirmLocalizedAsync(strs.ttt_created);
@@ -50,4 +52,4 @@ public partial class Games
}
}
}
}
}

View File

@@ -14,7 +14,10 @@ public partial class Games
private readonly GamesConfigService _gamesConfig;
private readonly DiscordSocketClient _client;
public TriviaCommands(DiscordSocketClient client, IDataCache cache, ICurrencyService cs,
public TriviaCommands(
DiscordSocketClient client,
IDataCache cache,
ICurrencyService cs,
GamesConfigService gamesConfig)
{
_cache = cache;
@@ -23,7 +26,8 @@ public partial class Games
_client = client;
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
[NadekoOptionsAttribute(typeof(TriviaOptions))]
@@ -37,13 +41,18 @@ public partial class Games
var (opts, _) = OptionsParser.ParseFrom(new TriviaOptions(), args);
var config = _gamesConfig.Data;
if (config.Trivia.MinimumWinReq > 0 && config.Trivia.MinimumWinReq > opts.WinRequirement)
{
return;
}
if (config.Trivia.MinimumWinReq > 0 && config.Trivia.MinimumWinReq > opts.WinRequirement) return;
var trivia = new TriviaGame(Strings, _client, config, _cache, _cs, channel.Guild, channel, opts,
Prefix + "tq", _eb);
var trivia = new TriviaGame(Strings,
_client,
config,
_cache,
_cs,
channel.Guild,
channel,
opts,
Prefix + "tq",
_eb);
if (_service.RunningTrivias.TryAdd(channel.Guild.Id, trivia))
{
try
@@ -55,13 +64,15 @@ public partial class Games
_service.RunningTrivias.TryRemove(channel.Guild.Id, out trivia);
await trivia.EnsureStopped();
}
return;
}
await SendErrorAsync(GetText(strs.trivia_already_running) + "\n" + trivia.CurrentQuestion);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Tl()
{
@@ -74,7 +85,8 @@ public partial class Games
await ReplyErrorLocalizedAsync(strs.trivia_none);
}
[NadekoCommand, Aliases]
[NadekoCommand]
[Aliases]
[RequireContext(ContextType.Guild)]
public async Task Tq()
{
@@ -89,4 +101,4 @@ public partial class Games
await ReplyErrorLocalizedAsync(strs.trivia_none);
}
}
}
}