This commit is contained in:
Kwoth
2022-07-13 06:22:46 +02:00
14 changed files with 494 additions and 444 deletions

View File

@@ -1,33 +0,0 @@
namespace NadekoBot.Modules.Games.Common.Trivia;
public sealed class DefaultQuestionPool : IQuestionPool
{
private readonly ILocalDataCache _cache;
private readonly NadekoRandom _rng;
public DefaultQuestionPool(ILocalDataCache cache)
{
_cache = cache;
_rng = new NadekoRandom();
}
public async Task<TriviaQuestion?> GetRandomQuestionAsync(ISet<TriviaQuestion> exclude)
{
TriviaQuestion randomQuestion;
var pool = await _cache.GetTriviaQuestionsAsync();
if(pool is null)
return default;
while (exclude.Contains(randomQuestion = new(pool[_rng.Next(0, pool.Length)])))
{
// if too many questions are excluded, clear the exclusion list and start over
if (exclude.Count > pool.Length / 10 * 9)
{
exclude.Clear();
break;
}
}
return randomQuestion;
}
}

View File

@@ -0,0 +1,265 @@
using System.Net;
using System.Text;
using NadekoBot.Modules.Games.Common.Trivia;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Games;
public partial class Games
{
[Group]
public partial class TriviaCommands : NadekoModule<TriviaGamesService>
{
private readonly ILocalDataCache _cache;
private readonly ICurrencyService _cs;
private readonly GamesConfigService _gamesConfig;
private readonly DiscordSocketClient _client;
public TriviaCommands(
DiscordSocketClient client,
ILocalDataCache cache,
ICurrencyService cs,
GamesConfigService gamesConfig)
{
_cache = cache;
_cs = cs;
_gamesConfig = gamesConfig;
_client = client;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(0)]
[NadekoOptions(typeof(TriviaOptions))]
public async partial Task Trivia(params string[] args)
{
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(opts, _cache);
if (_service.RunningTrivias.TryAdd(ctx.Guild.Id, trivia))
{
RegisterEvents(trivia);
await trivia.RunAsync();
return;
}
if (_service.RunningTrivias.TryGetValue(ctx.Guild.Id, out var tg))
{
await SendErrorAsync(GetText(strs.trivia_already_running));
await tg.TriggerQuestionAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async partial Task Tl()
{
if (_service.RunningTrivias.TryGetValue(ctx.Guild.Id, out var trivia))
{
await trivia.TriggerStatsAsync();
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))
{
if (trivia.Stop())
{
try
{
await ctx.Channel.SendConfirmAsync(_eb,
GetText(strs.trivia_game),
GetText(strs.trivia_stopping));
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia stopping message");
}
}
return;
}
await ReplyErrorLocalizedAsync(strs.trivia_none);
}
private void RegisterEvents(TriviaGame trivia)
{
IEmbedBuilder? questionEmbed = null;
IUserMessage? questionMessage = null;
var showHowToQuit = false;
trivia.OnQuestion += async (_, question) =>
{
try
{
questionEmbed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.AddField(GetText(strs.category), question.Category)
.AddField(GetText(strs.question), question.Question);
showHowToQuit = !showHowToQuit;
if (showHowToQuit)
questionEmbed.WithFooter(GetText(strs.trivia_quit($"{prefix}tq")));
if (Uri.IsWellFormedUriString(question.ImageUrl, UriKind.Absolute))
questionEmbed.WithImageUrl(question.ImageUrl);
questionMessage = await ctx.Channel.EmbedAsync(questionEmbed);
}
catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound
or HttpStatusCode.Forbidden
or HttpStatusCode.BadRequest)
{
Log.Warning("Unable to send trivia questions. Stopping immediately");
trivia.Stop();
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia embed");
await Task.Delay(2000);
}
};
trivia.OnHint += async (_, question) =>
{
try
{
if (questionMessage is null)
{
trivia.Stop();
return;
}
if (questionEmbed is not null)
await questionMessage.ModifyAsync(m
=> m.Embed = questionEmbed.WithFooter(question.GetHint()).Build());
}
catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound
or HttpStatusCode.Forbidden)
{
Log.Warning("Unable to edit message to show hint. Stopping trivia");
trivia.Stop();
}
catch (Exception ex) { Log.Warning(ex, "Error editing triva message"); }
};
trivia.OnGuess += async (_, user, question, isWin) =>
{
try
{
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_win(user.Name,
Format.Bold(question.Answer))));
if (Uri.IsWellFormedUriString(question.AnswerImageUrl, UriKind.Absolute))
embed.WithImageUrl(question.AnswerImageUrl);
if (isWin)
{
await ctx.Channel.EmbedAsync(embed);
var reward = _gamesConfig.Data.Trivia.CurrencyReward;
if (reward > 0)
await _cs.AddAsync(user.Id, reward, new("trivia", "win"));
return;
}
embed.WithDescription(GetText(strs.trivia_guess(user.Name,
Format.Bold(question.Answer))));
await ctx.Channel.EmbedAsync(embed);
}
catch
{
// ignored
}
};
trivia.OnEnded += async (game) =>
{
try
{
await ctx.Channel.EmbedAsync(_eb.Create(ctx)
.WithOkColor()
.WithAuthor(GetText(strs.trivia_ended))
.WithTitle(GetText(strs.leaderboard))
.WithDescription(GetLeaderboardString(game)));
}
catch
{
// ignored
}
finally
{
_service.RunningTrivias.TryRemove(ctx.Guild.Id, out _);
}
};
trivia.OnStats += async (game) =>
{
try
{
await SendConfirmAsync(GetText(strs.leaderboard), GetLeaderboardString(game));
}
catch
{
// ignored
}
};
trivia.OnTimeout += async (_, question) =>
{
try
{
var embed = _eb.Create()
.WithErrorColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_times_up(Format.Bold(question.Answer))));
if (Uri.IsWellFormedUriString(question.AnswerImageUrl, UriKind.Absolute))
embed.WithImageUrl(question.AnswerImageUrl);
await ctx.Channel.EmbedAsync(embed);
}
catch
{
// ignored
}
};
}
private string GetLeaderboardString(TriviaGame tg)
{
var sb = new StringBuilder();
foreach (var (id, pts) in tg.GetLeaderboard())
sb.AppendLine(GetText(strs.trivia_points(Format.Bold($"<@{id}>"), pts)));
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,22 @@
namespace NadekoBot.Modules.Games.Common.Trivia;
public sealed class DefaultQuestionPool : IQuestionPool
{
private readonly ILocalDataCache _cache;
private readonly NadekoRandom _rng;
public DefaultQuestionPool(ILocalDataCache cache)
{
_cache = cache;
_rng = new NadekoRandom();
}
public async Task<TriviaQuestion?> GetQuestionAsync()
{
var pool = await _cache.GetTriviaQuestionsAsync();
if(pool is null or {Length: 0})
return default;
return new(pool[_rng.Next(0, pool.Length)]);
}
}

View File

@@ -2,5 +2,5 @@ namespace NadekoBot.Modules.Games.Common.Trivia;
public interface IQuestionPool public interface IQuestionPool
{ {
Task<TriviaQuestion?> GetRandomQuestionAsync(ISet<TriviaQuestion> exclude); Task<TriviaQuestion?> GetQuestionAsync();
} }

View File

@@ -12,7 +12,7 @@ public sealed class PokemonQuestionPool : IQuestionPool
_rng = new NadekoRandom(); _rng = new NadekoRandom();
} }
public async Task<TriviaQuestion?> GetRandomQuestionAsync(ISet<TriviaQuestion> exclude) public async Task<TriviaQuestion?> GetQuestionAsync()
{ {
var pokes = await _cache.GetPokemonMapAsync(); var pokes = await _cache.GetPokemonMapAsync();

View File

@@ -1,102 +0,0 @@
#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 : NadekoModule<GamesService>
{
private readonly ILocalDataCache _cache;
private readonly ICurrencyService _cs;
private readonly GamesConfigService _gamesConfig;
private readonly DiscordSocketClient _client;
public TriviaCommands(
DiscordSocketClient client,
ILocalDataCache 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);
}
}
}

View File

@@ -1,291 +1,202 @@
#nullable disable using System.Threading.Channels;
using System.Net;
using System.Text;
namespace NadekoBot.Modules.Games.Common.Trivia; namespace NadekoBot.Modules.Games.Common.Trivia;
public class TriviaGame public sealed class TriviaGame
{ {
public IGuild Guild { get; } private readonly TriviaOptions _opts;
public ITextChannel Channel { get; }
public TriviaQuestion CurrentQuestion { get; private set; }
public HashSet<TriviaQuestion> OldQuestions { get; } = new();
public ConcurrentDictionary<IGuildUser, int> Users { get; } = new(); private readonly IQuestionPool _questionPool;
public bool GameActive { get; private set; } #region Events
public bool ShouldStopGame { get; private set; } public event Func<TriviaGame, TriviaQuestion, Task> OnQuestion = static delegate { return Task.CompletedTask; };
private readonly SemaphoreSlim _guessLock = new(1, 1); public event Func<TriviaGame, TriviaQuestion, Task> OnHint = static delegate { return Task.CompletedTask; };
private readonly ILocalDataCache _cache; public event Func<TriviaGame, Task> OnStats = static delegate { return Task.CompletedTask; };
private readonly IBotStrings _strings; public event Func<TriviaGame, TriviaUser, TriviaQuestion, bool, Task> OnGuess = static delegate { return Task.CompletedTask; };
private readonly DiscordSocketClient _client; public event Func<TriviaGame, TriviaQuestion, Task> OnTimeout = static delegate { return Task.CompletedTask; };
private readonly GamesConfig _config; public event Func<TriviaGame, Task> OnEnded = static delegate { return Task.CompletedTask; };
private readonly ICurrencyService _cs; #endregion
private readonly TriviaOptions _options;
private CancellationTokenSource triviaCancelSource; private bool _isStopped;
private readonly TriviaQuestionPool _questionPool; public TriviaQuestion? CurrentQuestion { get; set; }
private int timeoutCount;
private readonly string _quitCommand;
private readonly IEmbedBuilderService _eb;
public TriviaGame(
IBotStrings strings, private readonly ConcurrentDictionary<ulong, int> _users = new ();
DiscordSocketClient client,
GamesConfig config, private readonly Channel<(TriviaUser User, string Input)> _inputs
ILocalDataCache cache, = Channel.CreateUnbounded<(TriviaUser, string)>(new UnboundedChannelOptions
ICurrencyService cs, {
IGuild guild, AllowSynchronousContinuations = true,
ITextChannel channel, SingleReader = true,
TriviaOptions options, SingleWriter = false,
string quitCommand, });
IEmbedBuilderService eb)
public TriviaGame(TriviaOptions options, ILocalDataCache cache)
{ {
_cache = cache; _opts = options;
_questionPool = new(_cache);
_strings = strings;
_client = client;
_config = config;
_cs = cs;
_options = options;
_quitCommand = quitCommand;
_eb = eb;
Guild = guild; _questionPool = _opts.IsPokemon
Channel = channel; ? new PokemonQuestionPool(cache)
: new DefaultQuestionPool(cache);
}
public async Task RunAsync()
{
await GameLoop();
} }
private string GetText(in LocStr key) private async Task GameLoop()
=> _strings.GetText(key, Channel.GuildId);
public async Task StartGame()
{ {
var showHowToQuit = false; Task TimeOutFactory() => Task.Delay(_opts.QuestionTimer * 1000 / 2);
while (!ShouldStopGame)
{
// reset the cancellation source
triviaCancelSource = new();
showHowToQuit = !showHowToQuit;
// load question var errorCount = 0;
CurrentQuestion = await _questionPool.GetRandomQuestionAsync(OldQuestions, _options.IsPokemon); var inactivity = 0;
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer)
|| string.IsNullOrWhiteSpace(CurrentQuestion.Question)) // loop until game is stopped
// each iteration is one round
var firstRun = true;
while (!_isStopped)
{
if (errorCount >= 5)
{ {
await Channel.SendErrorAsync(_eb, GetText(strs.trivia_game), GetText(strs.failed_loading_question)); Log.Warning("Trivia errored 5 times and will quit");
return; break;
} }
OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again // wait for 3 seconds before posting the next question
if (firstRun)
{
firstRun = false;
}
else
{
await Task.Delay(3000);
}
IEmbedBuilder questionEmbed; var maybeQuestion = await _questionPool.GetQuestionAsync();
IUserMessage questionMessage;
if(!(maybeQuestion is TriviaQuestion question))
{
// if question is null (ran out of question, or other bugg ) - stop
break;
}
CurrentQuestion = question;
try try
{ {
questionEmbed = _eb.Create() // clear out all of the past guesses
.WithOkColor() while (_inputs.Reader.TryRead(out _)) ;
.WithTitle(GetText(strs.trivia_game))
.AddField(GetText(strs.category), CurrentQuestion.Category)
.AddField(GetText(strs.question), CurrentQuestion.Question);
if (showHowToQuit) await OnQuestion(this, question);
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) catch (Exception ex)
{ {
Log.Warning(ex, "Error sending trivia embed"); Log.Warning(ex, "Error executing OnQuestion: {Message}", ex.Message);
await Task.Delay(2000); errorCount++;
continue; continue;
} }
//receive messages
try
{
_client.MessageReceived += PotentialGuess;
//allow people to guess // just keep looping through user inputs until someone guesses the answer
GameActive = true; // or the timer expires
try var halfGuessTimerTask = TimeOutFactory();
var hintSent = false;
var guessed = false;
while (true)
{
var readTask = _inputs.Reader.ReadAsync().AsTask();
// wait for either someone to attempt to guess
// or for timeout
var task = await Task.WhenAny(readTask, halfGuessTimerTask);
// if the task which completed is the timeout task
if (task == halfGuessTimerTask)
{ {
//hint // if hint is already sent, means time expired
await Task.Delay(_options.QuestionTimer * 1000 / 2, triviaCancelSource.Token); // break (end the round)
if (!_options.NoHint) if (hintSent)
break;
// else, means half time passed, send a hint
hintSent = true;
// start a new countdown of the same length
halfGuessTimerTask = TimeOutFactory();
// send a hint out
await OnHint(this, question);
continue;
}
// otherwise, read task is successful, and we're gonna
// get the user input data
var (user, input) = await readTask;
// check the guess
if (question.IsAnswerCorrect(input))
{
// add 1 point to the user
var val = _users.AddOrUpdate(user.Id, 1, (_, points) => ++points);
guessed = true;
// reset inactivity counter
inactivity = 0;
var isWin = false;
// if user won the game, tell the game to stop
if (val >= _opts.WinRequirement)
{ {
try _isStopped = true;
{ isWin = true;
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 // call onguess
await Task.Delay(_options.QuestionTimer * 1000 / 2, triviaCancelSource.Token); await OnGuess(this, user, question, isWin);
} break;
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); if (!guessed)
}
}
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)); await OnTimeout(this, question);
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia stopping message");
}
}
}
private Task PotentialGuess(SocketMessage imsg)
{
_ = 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 (_opts.Timeout != 0 && ++inactivity >= _opts.Timeout)
if (_options.WinRequirement != 0 && Users[guildUser] == _options.WinRequirement)
{ {
ShouldStopGame = true; Log.Information("Trivia game is stopping due to inactivity");
try break;
{
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, reward, new("trivia", "win"));
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, "Exception in a potential guess"); } }
});
return Task.CompletedTask; // make sure game is set as ended
_isStopped = true;
await OnEnded(this);
} }
public string GetLeaderboard() public IReadOnlyList<(ulong User, int points)> GetLeaderboard()
=> _users.Select(x => (x.Key, x.Value)).ToArray();
public ValueTask InputAsync(TriviaUser user, string input)
=> _inputs.Writer.WriteAsync((user, input));
public bool Stop()
{ {
if (Users.Count == 0) var isStopped = _isStopped;
return GetText(strs.no_results); _isStopped = true;
return !isStopped;
}
var sb = new StringBuilder(); public async ValueTask TriggerStatsAsync()
{
await OnStats(this);
}
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value)) public async Task TriggerQuestionAsync()
sb.AppendLine(GetText(strs.trivia_points(Format.Bold(kvp.Key.ToString()), kvp.Value))); {
if(CurrentQuestion is TriviaQuestion q)
return sb.ToString(); await OnQuestion(this, q);
} }
} }

View File

@@ -0,0 +1,37 @@
#nullable disable
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Games.Common.Trivia;
namespace NadekoBot.Modules.Games;
public sealed class TriviaGamesService : IReadyExecutor, INService
{
private readonly DiscordSocketClient _client;
public ConcurrentDictionary<ulong, TriviaGame> RunningTrivias { get; } = new();
public TriviaGamesService(DiscordSocketClient client)
{
_client = client;
}
public Task OnReadyAsync()
{
_client.MessageReceived += OnMessageReceived;
return Task.CompletedTask;
}
private async Task OnMessageReceived(SocketMessage msg)
{
if (msg.Author.IsBot)
return;
var umsg = msg as SocketUserMessage;
if (umsg?.Channel is not IGuildChannel gc)
return;
if (RunningTrivias.TryGetValue(gc.GuildId, out var tg))
await tg.InputAsync(new(umsg.Author.Mention, umsg.Author.Id), umsg.Content);
}
}

View File

@@ -4,15 +4,6 @@ using System.Text.RegularExpressions;
// THANKS @ShoMinamimoto for suggestions and coding help // THANKS @ShoMinamimoto for suggestions and coding help
namespace NadekoBot.Modules.Games.Common.Trivia; namespace NadekoBot.Modules.Games.Common.Trivia;
public sealed class TriviaQuestionModel
{
public string Category { get; init; }
public string Question { get; init; }
public string ImageUrl { get; init; }
public string AnswerImageUrl { get; init; }
public string Answer { get; init; }
}
public class TriviaQuestion public class TriviaQuestion
{ {
public const int MAX_STRING_LENGTH = 22; public const int MAX_STRING_LENGTH = 22;

View File

@@ -0,0 +1,11 @@
#nullable disable
namespace NadekoBot.Modules.Games.Common.Trivia;
public sealed class TriviaQuestionModel
{
public string Category { get; init; }
public string Question { get; init; }
public string ImageUrl { get; init; }
public string AnswerImageUrl { get; init; }
public string Answer { get; init; }
}

View File

@@ -1,54 +0,0 @@
namespace NadekoBot.Modules.Games.Common.Trivia;
public class TriviaQuestionPool
{
private readonly ILocalDataCache _cache;
private readonly int _maxPokemonId;
private readonly NadekoRandom _rng = new();
public TriviaQuestionPool(ILocalDataCache cache)
{
_cache = cache;
_maxPokemonId = 721; //xd
}
public async Task<TriviaQuestion?> GetRandomQuestionAsync(HashSet<TriviaQuestion> exclude, bool isPokemon)
{
if (isPokemon)
{
var pokes = await _cache.GetPokemonMapAsync();
if (pokes is null or { Count: 0 })
return default;
var num = _rng.Next(1, _maxPokemonId + 1);
return new(new()
{
Question = "Who's That Pokémon?",
Answer = pokes[num].ToTitleCase(),
Category = "Pokemon",
ImageUrl = $@"https://nadeko.bot/images/pokemon/shadows/{num}.png",
AnswerImageUrl = $@"https://nadeko.bot/images/pokemon/real/{num}.png"
});
}
TriviaQuestion randomQuestion;
var pool = await _cache.GetTriviaQuestionsAsync();
if(pool is null)
return default;
while (exclude.Contains(randomQuestion = new(pool[_rng.Next(0, pool.Length)])))
{
// if too many questions are excluded, clear the exclusion list and start over
if (exclude.Count > pool.Length / 10 * 9)
{
exclude.Clear();
break;
}
}
return randomQuestion;
}
}

View File

@@ -0,0 +1,3 @@
namespace NadekoBot.Modules.Games.Common.Trivia;
public record class TriviaUser(string Name, ulong Id);

View File

@@ -2,10 +2,8 @@ using Humanizer.Localisation;
using Nadeko.Medusa; using Nadeko.Medusa;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Net.Http.Headers;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Nadeko.Common;
namespace NadekoBot.Extensions; namespace NadekoBot.Extensions;

View File

@@ -343,6 +343,7 @@
"trivia_times_up": "Time's up! The correct answer was {0}", "trivia_times_up": "Time's up! The correct answer was {0}",
"trivia_win": "{0} guessed it and WON the game! The answer was: {1}", "trivia_win": "{0} guessed it and WON the game! The answer was: {1}",
"trivia_quit": "You can stop trivia by typing {0}", "trivia_quit": "You can stop trivia by typing {0}",
"trivia_ended": "Trivia game ended",
"ttt_against_yourself": "You can't play against yourself.", "ttt_against_yourself": "You can't play against yourself.",
"ttt_already_running": "TicTacToe Game is already running in this channel.", "ttt_already_running": "TicTacToe Game is already running in this channel.",
"ttt_a_draw": "A draw!", "ttt_a_draw": "A draw!",