mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-03 16:24:27 -05:00 
			
		
		
		
	Merge branch '4.3' of https://gitlab.com/kwoth/nadekobot into 4.3
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
 | 
					public interface IQuestionPool
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    Task<TriviaQuestion?> GetRandomQuestionAsync(ISet<TriviaQuestion> exclude);
 | 
					    Task<TriviaQuestion?> GetQuestionAsync();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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.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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										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
 | 
					// 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;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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!",
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user