mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Trivia game cleanup
This commit is contained in:
		@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										265
									
								
								src/NadekoBot/Modules/Games/Trivia/Games.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								src/NadekoBot/Modules/Games/Trivia/Games.cs
									
									
									
									
									
										Normal 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();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,5 +2,5 @@ namespace NadekoBot.Modules.Games.Common.Trivia;
 | 
			
		||||
 | 
			
		||||
public interface IQuestionPool
 | 
			
		||||
{
 | 
			
		||||
    Task<TriviaQuestion?> GetRandomQuestionAsync(ISet<TriviaQuestion> exclude);
 | 
			
		||||
    Task<TriviaQuestion?> GetQuestionAsync();
 | 
			
		||||
}
 | 
			
		||||
@@ -12,7 +12,7 @@ public sealed class PokemonQuestionPool : IQuestionPool
 | 
			
		||||
        _rng = new NadekoRandom();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<TriviaQuestion?> GetRandomQuestionAsync(ISet<TriviaQuestion> exclude)
 | 
			
		||||
    public async Task<TriviaQuestion?> GetQuestionAsync()
 | 
			
		||||
    {
 | 
			
		||||
        var pokes = await _cache.GetPokemonMapAsync();
 | 
			
		||||
 | 
			
		||||
@@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,291 +1,202 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Channels;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.Trivia;
 | 
			
		||||
 | 
			
		||||
public class TriviaGame
 | 
			
		||||
public sealed class TriviaGame
 | 
			
		||||
{
 | 
			
		||||
    public IGuild Guild { get; }
 | 
			
		||||
    public ITextChannel Channel { get; }
 | 
			
		||||
    private readonly TriviaOptions _opts;
 | 
			
		||||
 | 
			
		||||
    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; }
 | 
			
		||||
    public bool ShouldStopGame { get; private set; }
 | 
			
		||||
    private readonly SemaphoreSlim _guessLock = new(1, 1);
 | 
			
		||||
    private readonly ILocalDataCache _cache;
 | 
			
		||||
    private readonly IBotStrings _strings;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly GamesConfig _config;
 | 
			
		||||
    private readonly ICurrencyService _cs;
 | 
			
		||||
    private readonly TriviaOptions _options;
 | 
			
		||||
    #region Events
 | 
			
		||||
    public event Func<TriviaGame, TriviaQuestion, Task> OnQuestion = static delegate { return Task.CompletedTask; };
 | 
			
		||||
    public event Func<TriviaGame, TriviaQuestion, Task> OnHint = static delegate { return Task.CompletedTask; };
 | 
			
		||||
    public event Func<TriviaGame, Task> OnStats = static delegate { return Task.CompletedTask; };
 | 
			
		||||
    public event Func<TriviaGame, TriviaUser, TriviaQuestion, bool, Task> OnGuess = static delegate { return Task.CompletedTask; };
 | 
			
		||||
    public event Func<TriviaGame, TriviaQuestion, Task> OnTimeout = static delegate { return Task.CompletedTask; };
 | 
			
		||||
    public event Func<TriviaGame, Task> OnEnded = static delegate { return Task.CompletedTask; };
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    private CancellationTokenSource triviaCancelSource;
 | 
			
		||||
    private bool _isStopped;
 | 
			
		||||
 | 
			
		||||
    private readonly TriviaQuestionPool _questionPool;
 | 
			
		||||
    private int timeoutCount;
 | 
			
		||||
    private readonly string _quitCommand;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
    public TriviaQuestion? CurrentQuestion { get; set; }
 | 
			
		||||
 | 
			
		||||
    public TriviaGame(
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        GamesConfig config,
 | 
			
		||||
        ILocalDataCache cache,
 | 
			
		||||
        ICurrencyService cs,
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        ITextChannel channel,
 | 
			
		||||
        TriviaOptions options,
 | 
			
		||||
        string quitCommand,
 | 
			
		||||
        IEmbedBuilderService eb)
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, int> _users = new ();
 | 
			
		||||
 | 
			
		||||
    private readonly Channel<(TriviaUser User, string Input)> _inputs
 | 
			
		||||
        = Channel.CreateUnbounded<(TriviaUser, string)>(new UnboundedChannelOptions
 | 
			
		||||
        {
 | 
			
		||||
            AllowSynchronousContinuations = true,
 | 
			
		||||
            SingleReader = true,
 | 
			
		||||
            SingleWriter = false,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    public TriviaGame(TriviaOptions options, ILocalDataCache cache)
 | 
			
		||||
    {
 | 
			
		||||
        _cache = cache;
 | 
			
		||||
        _questionPool = new(_cache);
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _config = config;
 | 
			
		||||
        _cs = cs;
 | 
			
		||||
        _options = options;
 | 
			
		||||
        _quitCommand = quitCommand;
 | 
			
		||||
        _eb = eb;
 | 
			
		||||
        _opts = options;
 | 
			
		||||
 | 
			
		||||
        Guild = guild;
 | 
			
		||||
        Channel = channel;
 | 
			
		||||
        _questionPool = _opts.IsPokemon
 | 
			
		||||
            ? new PokemonQuestionPool(cache)
 | 
			
		||||
            : new DefaultQuestionPool(cache);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    public async Task RunAsync()
 | 
			
		||||
    {
 | 
			
		||||
        await GameLoop();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string GetText(in LocStr key)
 | 
			
		||||
        => _strings.GetText(key, Channel.GuildId);
 | 
			
		||||
 | 
			
		||||
    public async Task StartGame()
 | 
			
		||||
    private async Task GameLoop()
 | 
			
		||||
    {
 | 
			
		||||
        var showHowToQuit = false;
 | 
			
		||||
        while (!ShouldStopGame)
 | 
			
		||||
        {
 | 
			
		||||
            // reset the cancellation source    
 | 
			
		||||
            triviaCancelSource = new();
 | 
			
		||||
            showHowToQuit = !showHowToQuit;
 | 
			
		||||
        Task TimeOutFactory() => Task.Delay(_opts.QuestionTimer * 1000 / 2);
 | 
			
		||||
 | 
			
		||||
            // load question
 | 
			
		||||
            CurrentQuestion = await _questionPool.GetRandomQuestionAsync(OldQuestions, _options.IsPokemon);
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer)
 | 
			
		||||
                || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
 | 
			
		||||
        var errorCount = 0;
 | 
			
		||||
        var inactivity = 0;
 | 
			
		||||
 | 
			
		||||
        // 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));
 | 
			
		||||
                return;
 | 
			
		||||
                Log.Warning("Trivia errored 5 times and will quit");
 | 
			
		||||
                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;
 | 
			
		||||
            IUserMessage questionMessage;
 | 
			
		||||
            var maybeQuestion = await _questionPool.GetQuestionAsync();
 | 
			
		||||
 | 
			
		||||
            if(!(maybeQuestion is TriviaQuestion question))
 | 
			
		||||
            {
 | 
			
		||||
                // if question is null (ran out of question, or other bugg ) - stop
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            CurrentQuestion = question;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                questionEmbed = _eb.Create()
 | 
			
		||||
                                   .WithOkColor()
 | 
			
		||||
                                   .WithTitle(GetText(strs.trivia_game))
 | 
			
		||||
                                   .AddField(GetText(strs.category), CurrentQuestion.Category)
 | 
			
		||||
                                   .AddField(GetText(strs.question), CurrentQuestion.Question);
 | 
			
		||||
                // clear out all of the past guesses
 | 
			
		||||
                while (_inputs.Reader.TryRead(out _)) ;
 | 
			
		||||
 | 
			
		||||
                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;
 | 
			
		||||
                await OnQuestion(this, question);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex, "Error sending trivia embed");
 | 
			
		||||
                await Task.Delay(2000);
 | 
			
		||||
                Log.Warning(ex, "Error executing OnQuestion: {Message}", ex.Message);
 | 
			
		||||
                errorCount++;
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //receive messages
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                _client.MessageReceived += PotentialGuess;
 | 
			
		||||
 | 
			
		||||
                //allow people to guess
 | 
			
		||||
                GameActive = true;
 | 
			
		||||
                try
 | 
			
		||||
            // just keep looping through user inputs until someone guesses the answer
 | 
			
		||||
            // or the timer expires
 | 
			
		||||
            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
 | 
			
		||||
                    await Task.Delay(_options.QuestionTimer * 1000 / 2, triviaCancelSource.Token);
 | 
			
		||||
                    if (!_options.NoHint)
 | 
			
		||||
                    // if hint is already sent, means time expired
 | 
			
		||||
                    // break (end the round)
 | 
			
		||||
                    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
 | 
			
		||||
                        {
 | 
			
		||||
                            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"); }
 | 
			
		||||
                        _isStopped = true;
 | 
			
		||||
                        isWin = true;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    //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");
 | 
			
		||||
                    // call onguess
 | 
			
		||||
                    await OnGuess(this, user, question, isWin);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
            if (!guessed)
 | 
			
		||||
            {
 | 
			
		||||
                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)
 | 
			
		||||
    {
 | 
			
		||||
        _ = 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;
 | 
			
		||||
                await OnTimeout(this, question);
 | 
			
		||||
                
 | 
			
		||||
                triviaCancelSource.Cancel();
 | 
			
		||||
 | 
			
		||||
                if (_options.WinRequirement != 0 && Users[guildUser] == _options.WinRequirement)
 | 
			
		||||
                if (_opts.Timeout != 0 && ++inactivity >= _opts.Timeout)
 | 
			
		||||
                {
 | 
			
		||||
                    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, reward, new("trivia", "win"));
 | 
			
		||||
                    return;
 | 
			
		||||
                    Log.Information("Trivia game is stopping due to inactivity");
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                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)
 | 
			
		||||
            return GetText(strs.no_results);
 | 
			
		||||
        var isStopped = _isStopped;
 | 
			
		||||
        _isStopped = true;
 | 
			
		||||
        return !isStopped;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        var sb = new StringBuilder();
 | 
			
		||||
    public async ValueTask TriggerStatsAsync()
 | 
			
		||||
    {
 | 
			
		||||
        await OnStats(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        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();
 | 
			
		||||
    public async Task TriggerQuestionAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if(CurrentQuestion is TriviaQuestion q)
 | 
			
		||||
            await OnQuestion(this, q);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								src/NadekoBot/Modules/Games/Trivia/TriviaGamesService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/NadekoBot/Modules/Games/Trivia/TriviaGamesService.cs
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,15 +4,6 @@ using System.Text.RegularExpressions;
 | 
			
		||||
// THANKS @ShoMinamimoto for suggestions and coding help
 | 
			
		||||
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 const int MAX_STRING_LENGTH = 22;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								src/NadekoBot/Modules/Games/Trivia/TriviaQuestionModel.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/NadekoBot/Modules/Games/Trivia/TriviaQuestionModel.cs
									
									
									
									
									
										Normal 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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/NadekoBot/Modules/Games/Trivia/TriviaUser.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/NadekoBot/Modules/Games/Trivia/TriviaUser.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.Trivia;
 | 
			
		||||
 | 
			
		||||
public record class TriviaUser(string Name, ulong Id);
 | 
			
		||||
@@ -2,10 +2,8 @@ using Humanizer.Localisation;
 | 
			
		||||
using Nadeko.Medusa;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Net.Http.Headers;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Extensions;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -343,6 +343,7 @@
 | 
			
		||||
  "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_quit": "You can stop trivia by typing {0}",
 | 
			
		||||
  "trivia_ended": "Trivia game ended",
 | 
			
		||||
  "ttt_against_yourself": "You can't play against yourself.",
 | 
			
		||||
  "ttt_already_running": "TicTacToe Game is already running in this channel.",
 | 
			
		||||
  "ttt_a_draw": "A draw!",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user