Restructured folders and project names, ci should be fixed

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

View File

@@ -0,0 +1,283 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.Net;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using NadekoBot.Core.Modules.Games.Common.Trivia;
using NadekoBot.Modules.Games.Services;
using Serilog;
namespace NadekoBot.Modules.Games.Common.Trivia
{
public class TriviaGame
{
private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(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 HashSet<TriviaQuestion>();
public ConcurrentDictionary<IGuildUser, int> Users { get; } = new ConcurrentDictionary<IGuildUser, int>();
public bool GameActive { get; private set; }
public bool ShouldStopGame { get; private set; }
private readonly TriviaQuestionPool _questionPool;
private int _timeoutCount = 0;
private readonly string _quitCommand;
public TriviaGame(IBotStrings strings, DiscordSocketClient client, GamesConfig config,
IDataCache cache, ICurrencyService cs, IGuild guild, ITextChannel channel,
TriviaOptions options, string quitCommand)
{
_cache = cache;
_questionPool = new TriviaQuestionPool(_cache);
_strings = strings;
_client = client;
_config = config;
_cs = cs;
_options = options;
_quitCommand = quitCommand;
Guild = guild;
Channel = channel;
}
private string GetText(string key, params object[] replacements)
=> _strings.GetText(key, Channel.GuildId, replacements);
public async Task StartGame()
{
var showHowToQuit = false;
while (!ShouldStopGame)
{
// reset the cancellation source
_triviaCancelSource = new CancellationTokenSource();
showHowToQuit = !showHowToQuit;
// load question
CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.IsPokemon);
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
{
await Channel.SendErrorAsync(GetText("trivia_game"), GetText("failed_loading_question")).ConfigureAwait(false);
return;
}
OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again
EmbedBuilder questionEmbed;
IUserMessage questionMessage;
try
{
questionEmbed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("trivia_game"))
.AddField(eab => eab.WithName(GetText("category")).WithValue(CurrentQuestion.Category))
.AddField(eab => eab.WithName(GetText("question")).WithValue(CurrentQuestion.Question));
if (showHowToQuit)
questionEmbed.WithFooter(GetText("trivia_quit", _quitCommand));
if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute))
questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl);
questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound ||
ex.HttpCode == System.Net.HttpStatusCode.Forbidden ||
ex.HttpCode == System.Net.HttpStatusCode.BadRequest)
{
return;
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia embed");
await Task.Delay(2000).ConfigureAwait(false);
continue;
}
//receive messages
try
{
_client.MessageReceived += PotentialGuess;
//allow people to guess
GameActive = true;
try
{
//hint
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false);
if (!_options.NoHint)
try
{
await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(efb => efb.WithText(CurrentQuestion.GetHint())).Build())
.ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
break;
}
catch (Exception ex) { Log.Warning(ex, "Error editing triva message"); }
//timeout
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false);
}
catch (TaskCanceledException) { _timeoutCount = 0; } //means someone guessed the answer
}
finally
{
GameActive = false;
_client.MessageReceived -= PotentialGuess;
}
if (!_triviaCancelSource.IsCancellationRequested)
{
try
{
var embed = new EmbedBuilder().WithErrorColor()
.WithTitle(GetText("trivia_game"))
.WithDescription(GetText("trivia_times_up", Format.Bold(CurrentQuestion.Answer)));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
await Channel.EmbedAsync(embed).ConfigureAwait(false);
if (_options.Timeout != 0 && ++_timeoutCount >= _options.Timeout)
await StopGame().ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia time's up message");
}
}
await Task.Delay(5000).ConfigureAwait(false);
}
}
public async Task EnsureStopped()
{
ShouldStopGame = true;
await Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName("Trivia Game Ended"))
.WithTitle("Final Results")
.WithDescription(GetLeaderboard())).ConfigureAwait(false);
}
public async Task StopGame()
{
var old = ShouldStopGame;
ShouldStopGame = true;
if (!old)
{
try
{
await Channel.SendConfirmAsync(GetText("trivia_game"), GetText("trivia_stopping"))
.ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia stopping message");
}
}
}
private Task PotentialGuess(SocketMessage imsg)
{
var _ = Task.Run(async () =>
{
try
{
if (imsg.Author.IsBot)
return;
var umsg = imsg as SocketUserMessage;
var textChannel = umsg?.Channel as ITextChannel;
if (textChannel == null || textChannel.Guild != Guild)
return;
var guildUser = (IGuildUser)umsg.Author;
var guess = false;
await _guessLock.WaitAsync().ConfigureAwait(false);
try
{
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();
if (_options.WinRequirement != 0 && Users[guildUser] == _options.WinRequirement)
{
ShouldStopGame = true;
try
{
var embedS = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("trivia_game"))
.WithDescription(GetText("trivia_win",
guildUser.Mention,
Format.Bold(CurrentQuestion.Answer)));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl);
await Channel.EmbedAsync(embedS).ConfigureAwait(false);
}
catch
{
// ignored
}
var reward = _config.Trivia.CurrencyReward;
if (reward > 0)
await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("trivia_game"))
.WithDescription(GetText("trivia_guess", guildUser.Mention, Format.Bold(CurrentQuestion.Answer)));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
await Channel.EmbedAsync(embed).ConfigureAwait(false);
}
catch (Exception ex) { Log.Warning(ex.ToString()); }
});
return Task.CompletedTask;
}
public string GetLeaderboard()
{
if (Users.Count == 0)
return GetText("no_results");
var sb = new StringBuilder();
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value))
{
sb.AppendLine(GetText("trivia_points", Format.Bold(kvp.Key.ToString()), kvp.Value).SnPl(kvp.Value));
}
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,30 @@
using CommandLine;
using NadekoBot.Core.Common;
namespace NadekoBot.Core.Modules.Games.Common.Trivia
{
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.")]
public int WinRequirement { get; set; } = 10;
[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.")]
public int Timeout { get; set; } = 10;
public void NormalizeOptions()
{
if (WinRequirement < 0)
WinRequirement = 10;
if (QuestionTimer < 10 || QuestionTimer > 300)
QuestionTimer = 30;
if (Timeout < 0 || Timeout > 20)
Timeout = 10;
}
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NadekoBot.Extensions;
// THANKS @ShoMinamimoto for suggestions and coding help
namespace NadekoBot.Modules.Games.Common.Trivia
{
public class TriviaQuestion
{
//represents the min size to judge levDistance with
private static readonly HashSet<Tuple<int, int>> strictness = new HashSet<Tuple<int, int>> {
new Tuple<int, int>(9, 0),
new Tuple<int, int>(14, 1),
new Tuple<int, int>(19, 2),
new Tuple<int, int>(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)
{
this.Question = q;
this.Answer = a;
this.Category = c;
this.ImageUrl = img;
this.AnswerImageUrl = answerImage ?? img;
}
public string GetHint() => Scramble(Answer);
public bool IsAnswerCorrect(string guess)
{
if (Answer.Equals(guess, StringComparison.InvariantCulture))
{
return true;
}
var cleanGuess = Clean(guess);
if (CleanAnswer.Equals(cleanGuess, StringComparison.InvariantCulture))
{
return true;
}
int levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess);
int levDistanceNormal = Answer.LevenshteinDistance(guess);
return JudgeGuess(CleanAnswer.Length, cleanGuess.Length, levDistanceClean)
|| JudgeGuess(Answer.Length, guess.Length, levDistanceNormal);
}
private static bool JudgeGuess(int guessLength, int answerLength, int levDistance)
{
foreach (Tuple<int, int> level in strictness)
{
if (guessLength <= level.Item1 || answerLength <= level.Item1)
{
if (levDistance <= level.Item2)
return true;
else
return false;
}
}
return false;
}
private static string Clean(string str)
{
str = " " + str.ToLowerInvariant() + " ";
str = Regex.Replace(str, "\\s+", " ");
str = Regex.Replace(str, "[^\\w\\d\\s]", "");
//Here's where custom modification can be done
str = Regex.Replace(str, "\\s(a|an|the|of|in|for|to|as|at|be)\\s", " ");
//End custom mod and cleanup whitespace
str = Regex.Replace(str, "^\\s+", "");
str = Regex.Replace(str, "\\s+$", "");
//Trim the really long answers
str = str.Length <= maxStringLength ? str : str.Substring(0, maxStringLength);
return str;
}
private static string Scramble(string word)
{
var letters = word.ToCharArray();
var count = 0;
for (var i = 0; i < letters.Length; i++)
{
if (letters[i] == ' ')
continue;
count++;
if (count <= letters.Length / 5)
continue;
if (count % 3 == 0)
continue;
if (letters[i] != ' ')
letters[i] = '_';
}
return string.Join(" ", new string(letters).Replace(" ", " \u2000", StringComparison.InvariantCulture).AsEnumerable());
}
}
}

View File

@@ -0,0 +1,44 @@
using NadekoBot.Common;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using System.Collections.Generic;
namespace NadekoBot.Modules.Games.Common.Trivia
{
public class TriviaQuestionPool
{
private readonly IDataCache _cache;
private readonly int maxPokemonId;
private readonly NadekoRandom _rng = new NadekoRandom();
private TriviaQuestion[] Pool => _cache.LocalData.TriviaQuestions;
private IReadOnlyDictionary<int, string> Map => _cache.LocalData.PokemonMap;
public TriviaQuestionPool(IDataCache cache)
{
_cache = cache;
maxPokemonId = 721; //xd
}
public TriviaQuestion GetRandomQuestion(HashSet<TriviaQuestion> exclude, bool isPokemon)
{
if (Pool.Length == 0)
return null;
if (isPokemon)
{
var num = _rng.Next(1, maxPokemonId + 1);
return new TriviaQuestion("Who's That Pokémon?",
Map[num].ToTitleCase(),
"Pokemon",
$@"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;
}
}
}