mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
Fixed some crashes in response strings source generator, reorganized more submodules into their folders
This commit is contained in:
101
src/NadekoBot/Modules/Games/Trivia/TriviaCommands.cs
Normal file
101
src/NadekoBot/Modules/Games/Trivia/TriviaCommands.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Games.Common.Trivia;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public partial class TriviaCommands : NadekoSubmodule<GamesService>
|
||||
{
|
||||
private readonly IDataCache _cache;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly GamesConfigService _gamesConfig;
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public TriviaCommands(
|
||||
DiscordSocketClient client,
|
||||
IDataCache cache,
|
||||
ICurrencyService cs,
|
||||
GamesConfigService gamesConfig)
|
||||
{
|
||||
_cache = cache;
|
||||
_cs = cs;
|
||||
_gamesConfig = gamesConfig;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(0)]
|
||||
[NadekoOptionsAttribute(typeof(TriviaOptions))]
|
||||
public partial Task Trivia(params string[] args)
|
||||
=> InternalTrivia(args);
|
||||
|
||||
private async Task InternalTrivia(params string[] args)
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
|
||||
var (opts, _) = OptionsParser.ParseFrom(new TriviaOptions(), args);
|
||||
|
||||
var config = _gamesConfig.Data;
|
||||
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);
|
||||
if (_service.RunningTrivias.TryAdd(channel.Guild.Id, trivia))
|
||||
{
|
||||
try
|
||||
{
|
||||
await trivia.StartGame();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_service.RunningTrivias.TryRemove(channel.Guild.Id, out trivia);
|
||||
await trivia.EnsureStopped();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await SendErrorAsync(GetText(strs.trivia_already_running) + "\n" + trivia.CurrentQuestion);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async partial Task Tl()
|
||||
{
|
||||
if (_service.RunningTrivias.TryGetValue(ctx.Guild.Id, out var trivia))
|
||||
{
|
||||
await SendConfirmAsync(GetText(strs.leaderboard), trivia.GetLeaderboard());
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyErrorLocalizedAsync(strs.trivia_none);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async partial Task Tq()
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
|
||||
if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out var trivia))
|
||||
{
|
||||
await trivia.StopGame();
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyErrorLocalizedAsync(strs.trivia_none);
|
||||
}
|
||||
}
|
||||
}
|
284
src/NadekoBot/Modules/Games/Trivia/TriviaGame.cs
Normal file
284
src/NadekoBot/Modules/Games/Trivia/TriviaGame.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
#nullable disable
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia;
|
||||
|
||||
public class TriviaGame
|
||||
{
|
||||
public IGuild Guild { get; }
|
||||
public ITextChannel Channel { get; }
|
||||
|
||||
public TriviaQuestion CurrentQuestion { get; private set; }
|
||||
public HashSet<TriviaQuestion> OldQuestions { get; } = new();
|
||||
|
||||
public ConcurrentDictionary<IGuildUser, int> Users { get; } = new();
|
||||
|
||||
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;
|
||||
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)
|
||||
{
|
||||
_cache = cache;
|
||||
_questionPool = new(_cache);
|
||||
_strings = strings;
|
||||
_client = client;
|
||||
_config = config;
|
||||
_cs = cs;
|
||||
_options = options;
|
||||
_quitCommand = quitCommand;
|
||||
_eb = eb;
|
||||
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
}
|
||||
|
||||
private string GetText(in LocStr key)
|
||||
=> _strings.GetText(key, Channel.GuildId);
|
||||
|
||||
public async Task StartGame()
|
||||
{
|
||||
var showHowToQuit = false;
|
||||
while (!ShouldStopGame)
|
||||
{
|
||||
// reset the cancellation source
|
||||
_triviaCancelSource = new();
|
||||
showHowToQuit = !showHowToQuit;
|
||||
|
||||
// load question
|
||||
CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.IsPokemon);
|
||||
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);
|
||||
|
||||
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 HttpStatusCode.NotFound
|
||||
or HttpStatusCode.Forbidden
|
||||
or HttpStatusCode.BadRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error sending trivia embed");
|
||||
await Task.Delay(2000);
|
||||
continue;
|
||||
}
|
||||
|
||||
//receive messages
|
||||
try
|
||||
{
|
||||
_client.MessageReceived += PotentialGuess;
|
||||
|
||||
//allow people to guess
|
||||
GameActive = true;
|
||||
try
|
||||
{
|
||||
//hint
|
||||
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token);
|
||||
if (!_options.NoHint)
|
||||
try
|
||||
{
|
||||
await questionMessage.ModifyAsync(m
|
||||
=> m.Embed = questionEmbed.WithFooter(CurrentQuestion.GetHint()).Build());
|
||||
}
|
||||
catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound
|
||||
or HttpStatusCode.Forbidden)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) { Log.Warning(ex, "Error editing triva message"); }
|
||||
|
||||
//timeout
|
||||
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException) { _timeoutCount = 0; } //means someone guessed the answer
|
||||
}
|
||||
finally
|
||||
{
|
||||
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))));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
|
||||
await Channel.EmbedAsync(embed);
|
||||
|
||||
if (_options.Timeout != 0 && ++_timeoutCount >= _options.Timeout)
|
||||
await StopGame();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error sending trivia time's up message");
|
||||
}
|
||||
|
||||
await Task.Delay(5000);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureStopped()
|
||||
{
|
||||
ShouldStopGame = true;
|
||||
|
||||
await Channel.EmbedAsync(_eb.Create()
|
||||
.WithOkColor()
|
||||
.WithAuthor("Trivia Game Ended")
|
||||
.WithTitle("Final Results")
|
||||
.WithDescription(GetLeaderboard()));
|
||||
}
|
||||
|
||||
public async Task StopGame()
|
||||
{
|
||||
var old = ShouldStopGame;
|
||||
ShouldStopGame = true;
|
||||
if (!old)
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (imsg.Author.IsBot)
|
||||
return;
|
||||
|
||||
var umsg = imsg as SocketUserMessage;
|
||||
|
||||
if (umsg?.Channel is not ITextChannel textChannel || textChannel.Guild != Guild)
|
||||
return;
|
||||
|
||||
var guildUser = (IGuildUser)umsg.Author;
|
||||
|
||||
var guess = false;
|
||||
await _guessLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (GameActive
|
||||
&& CurrentQuestion.IsAnswerCorrect(umsg.Content)
|
||||
&& !_triviaCancelSource.IsCancellationRequested)
|
||||
{
|
||||
Users.AddOrUpdate(guildUser, 1, (_, 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 = _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);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 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))));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
await Channel.EmbedAsync(embed);
|
||||
}
|
||||
catch (Exception ex) { Log.Warning(ex.ToString()); }
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetLeaderboard()
|
||||
{
|
||||
if (Users.Count == 0)
|
||||
return GetText(strs.no_results);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
44
src/NadekoBot/Modules/Games/Trivia/TriviaOptions.cs
Normal file
44
src/NadekoBot/Modules/Games/Trivia/TriviaOptions.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
#nullable disable
|
||||
using CommandLine;
|
||||
|
||||
namespace NadekoBot.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 is < 10 or > 300)
|
||||
QuestionTimer = 30;
|
||||
if (Timeout is < 0 or > 20)
|
||||
Timeout = 10;
|
||||
}
|
||||
}
|
108
src/NadekoBot/Modules/Games/Trivia/TriviaQuestion.cs
Normal file
108
src/NadekoBot/Modules/Games/Trivia/TriviaQuestion.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
#nullable disable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
// THANKS @ShoMinamimoto for suggestions and coding help
|
||||
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)
|
||||
};
|
||||
|
||||
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; }
|
||||
|
||||
public string CleanAnswer
|
||||
=> _cleanAnswer ?? (_cleanAnswer = Clean(Answer));
|
||||
|
||||
private string _cleanAnswer;
|
||||
|
||||
public TriviaQuestion(
|
||||
string q,
|
||||
string a,
|
||||
string c,
|
||||
string img = null,
|
||||
string answerImage = null)
|
||||
{
|
||||
Question = q;
|
||||
Answer = a;
|
||||
Category = c;
|
||||
ImageUrl = img;
|
||||
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;
|
||||
|
||||
var levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess);
|
||||
var 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 (var level in strictness)
|
||||
if (guessLength <= level.Item1 || answerLength <= level.Item1)
|
||||
{
|
||||
if (levDistance <= level.Item2)
|
||||
return true;
|
||||
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[..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());
|
||||
}
|
||||
}
|
43
src/NadekoBot/Modules/Games/Trivia/TriviaQuestionPool.cs
Normal file
43
src/NadekoBot/Modules/Games/Trivia/TriviaQuestionPool.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
#nullable disable
|
||||
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();
|
||||
|
||||
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("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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user