Removed module projects because it can't work like that atm. Commented out package commands.
This commit is contained in:
266
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaGame.cs
Normal file
266
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaGame.cs
Normal file
@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using NLog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaGame
|
||||
{
|
||||
private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1);
|
||||
private readonly Logger _log;
|
||||
private readonly NadekoStrings _strings;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotConfigProvider _bc;
|
||||
private readonly CurrencyService _cs;
|
||||
|
||||
public IGuild Guild { get; }
|
||||
public ITextChannel Channel { get; }
|
||||
|
||||
private readonly int _questionDurationMiliseconds = 30000;
|
||||
private readonly int _hintTimeoutMiliseconds = 6000;
|
||||
public bool ShowHints { get; }
|
||||
public bool IsPokemon { get; }
|
||||
private CancellationTokenSource _triviaCancelSource;
|
||||
|
||||
public TriviaQuestion CurrentQuestion { get; private set; }
|
||||
public HashSet<TriviaQuestion> OldQuestions { get; } = new HashSet<TriviaQuestion>();
|
||||
|
||||
public ConcurrentDictionary<IGuildUser, int> Users { get; } = new ConcurrentDictionary<IGuildUser, int>();
|
||||
|
||||
public bool GameActive { get; private set; }
|
||||
public bool ShouldStopGame { get; private set; }
|
||||
|
||||
public int WinRequirement { get; }
|
||||
|
||||
public TriviaGame(NadekoStrings strings, DiscordSocketClient client, IBotConfigProvider bc,
|
||||
CurrencyService cs, IGuild guild, ITextChannel channel,
|
||||
bool showHints, int winReq, bool isPokemon)
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
_strings = strings;
|
||||
_client = client;
|
||||
_bc = bc;
|
||||
_cs = cs;
|
||||
|
||||
ShowHints = showHints;
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
WinRequirement = winReq;
|
||||
IsPokemon = isPokemon;
|
||||
}
|
||||
|
||||
private string GetText(string key, params object[] replacements) =>
|
||||
_strings.GetText(key,
|
||||
Channel.GuildId,
|
||||
typeof(Games).Name.ToLowerInvariant(),
|
||||
replacements);
|
||||
|
||||
public async Task StartGame()
|
||||
{
|
||||
while (!ShouldStopGame)
|
||||
{
|
||||
// reset the cancellation source
|
||||
_triviaCancelSource = new CancellationTokenSource();
|
||||
|
||||
// load question
|
||||
CurrentQuestion = TriviaQuestionPool.Instance.GetRandomQuestion(OldQuestions, IsPokemon);
|
||||
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
|
||||
{
|
||||
await Channel.SendErrorAsync(GetText("trivia_game"), GetText("failed_loading_question")).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again
|
||||
|
||||
EmbedBuilder questionEmbed;
|
||||
IUserMessage questionMessage;
|
||||
try
|
||||
{
|
||||
questionEmbed = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.AddField(eab => eab.WithName(GetText("category")).WithValue(CurrentQuestion.Category))
|
||||
.AddField(eab => eab.WithName(GetText("question")).WithValue(CurrentQuestion.Question));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute))
|
||||
questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl);
|
||||
|
||||
questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound ||
|
||||
ex.HttpCode == System.Net.HttpStatusCode.Forbidden ||
|
||||
ex.HttpCode == System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
//receive messages
|
||||
try
|
||||
{
|
||||
_client.MessageReceived += PotentialGuess;
|
||||
|
||||
//allow people to guess
|
||||
GameActive = true;
|
||||
try
|
||||
{
|
||||
//hint
|
||||
await Task.Delay(_hintTimeoutMiliseconds, _triviaCancelSource.Token).ConfigureAwait(false);
|
||||
if (ShowHints)
|
||||
try
|
||||
{
|
||||
await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(efb => efb.WithText(CurrentQuestion.GetHint())).Build())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) { _log.Warn(ex); }
|
||||
|
||||
//timeout
|
||||
await Task.Delay(_questionDurationMiliseconds - _hintTimeoutMiliseconds, _triviaCancelSource.Token).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (TaskCanceledException) { } //means someone guessed the answer
|
||||
}
|
||||
finally
|
||||
{
|
||||
GameActive = false;
|
||||
_client.MessageReceived -= PotentialGuess;
|
||||
}
|
||||
if (!_triviaCancelSource.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var embed = new EmbedBuilder().WithErrorColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.WithDescription(GetText("trivia_times_up", Format.Bold(CurrentQuestion.Answer)));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
|
||||
await Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
}
|
||||
}
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureStopped()
|
||||
{
|
||||
ShouldStopGame = true;
|
||||
|
||||
await Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName("Trivia Game Ended"))
|
||||
.WithTitle("Final Results")
|
||||
.WithDescription(GetLeaderboard())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StopGame()
|
||||
{
|
||||
var old = ShouldStopGame;
|
||||
ShouldStopGame = true;
|
||||
if (!old)
|
||||
try { await Channel.SendConfirmAsync(GetText("trivia_game"), GetText("trivia_stopping")).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
|
||||
}
|
||||
|
||||
private Task PotentialGuess(SocketMessage imsg)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (imsg.Author.IsBot)
|
||||
return;
|
||||
|
||||
var umsg = imsg as SocketUserMessage;
|
||||
|
||||
var textChannel = umsg?.Channel as ITextChannel;
|
||||
if (textChannel == null || textChannel.Guild != Guild)
|
||||
return;
|
||||
|
||||
var guildUser = (IGuildUser)umsg.Author;
|
||||
|
||||
var guess = false;
|
||||
await _guessLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (GameActive && CurrentQuestion.IsAnswerCorrect(umsg.Content) && !_triviaCancelSource.IsCancellationRequested)
|
||||
{
|
||||
Users.AddOrUpdate(guildUser, 1, (gu, old) => ++old);
|
||||
guess = true;
|
||||
}
|
||||
}
|
||||
finally { _guessLock.Release(); }
|
||||
if (!guess) return;
|
||||
_triviaCancelSource.Cancel();
|
||||
|
||||
|
||||
if (Users[guildUser] == WinRequirement)
|
||||
{
|
||||
ShouldStopGame = true;
|
||||
try
|
||||
{
|
||||
var embedS = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.WithDescription(GetText("trivia_win",
|
||||
guildUser.Mention,
|
||||
Format.Bold(CurrentQuestion.Answer)));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
await Channel.EmbedAsync(embedS).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
var reward = _bc.BotConfig.TriviaCurrencyReward;
|
||||
if (reward > 0)
|
||||
await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var embed = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.WithDescription(GetText("trivia_guess", guildUser.Mention, Format.Bold(CurrentQuestion.Answer)));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
await Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) { _log.Warn(ex); }
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetLeaderboard()
|
||||
{
|
||||
if (Users.Count == 0)
|
||||
return GetText("no_results");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value))
|
||||
{
|
||||
sb.AppendLine(GetText("trivia_points", Format.Bold(kvp.Key.ToString()), kvp.Value).SnPl(kvp.Value));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
110
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaQuestion.cs
Normal file
110
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaQuestion.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
// THANKS @ShoMinamimoto for suggestions and coding help
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaQuestion
|
||||
{
|
||||
//represents the min size to judge levDistance with
|
||||
private static readonly HashSet<Tuple<int, int>> strictness = new HashSet<Tuple<int, int>> {
|
||||
new Tuple<int, int>(9, 0),
|
||||
new Tuple<int, int>(14, 1),
|
||||
new Tuple<int, int>(19, 2),
|
||||
new Tuple<int, int>(22, 3),
|
||||
};
|
||||
public static int maxStringLength = 22;
|
||||
|
||||
public string Category { get; set; }
|
||||
public string Question { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string AnswerImageUrl { get; set; }
|
||||
public string Answer { get; set; }
|
||||
private string _cleanAnswer;
|
||||
public string CleanAnswer => _cleanAnswer ?? (_cleanAnswer = Clean(Answer));
|
||||
|
||||
public TriviaQuestion(string q, string a, string c, string img = null, string answerImage = null)
|
||||
{
|
||||
this.Question = q;
|
||||
this.Answer = a;
|
||||
this.Category = c;
|
||||
this.ImageUrl = img;
|
||||
this.AnswerImageUrl = answerImage ?? img;
|
||||
}
|
||||
|
||||
public string GetHint() => Scramble(Answer);
|
||||
|
||||
public bool IsAnswerCorrect(string guess)
|
||||
{
|
||||
if (Answer.Equals(guess))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var cleanGuess = Clean(guess);
|
||||
if (CleanAnswer.Equals(cleanGuess))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
int levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess);
|
||||
int levDistanceNormal = Answer.LevenshteinDistance(guess);
|
||||
return JudgeGuess(CleanAnswer.Length, cleanGuess.Length, levDistanceClean)
|
||||
|| JudgeGuess(Answer.Length, guess.Length, levDistanceNormal);
|
||||
}
|
||||
|
||||
private bool JudgeGuess(int guessLength, int answerLength, int levDistance)
|
||||
{
|
||||
foreach (Tuple<int, int> level in strictness)
|
||||
{
|
||||
if (guessLength <= level.Item1 || answerLength <= level.Item1)
|
||||
{
|
||||
if (levDistance <= level.Item2)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private string Clean(string str)
|
||||
{
|
||||
str = " " + str.ToLower() + " ";
|
||||
str = Regex.Replace(str, "\\s+", " ");
|
||||
str = Regex.Replace(str, "[^\\w\\d\\s]", "");
|
||||
//Here's where custom modification can be done
|
||||
str = Regex.Replace(str, "\\s(a|an|the|of|in|for|to|as|at|be)\\s", " ");
|
||||
//End custom mod and cleanup whitespace
|
||||
str = Regex.Replace(str, "^\\s+", "");
|
||||
str = Regex.Replace(str, "\\s+$", "");
|
||||
//Trim the really long answers
|
||||
str = str.Length <= maxStringLength ? str : str.Substring(0, maxStringLength);
|
||||
return str;
|
||||
}
|
||||
|
||||
private static string Scramble(string word)
|
||||
{
|
||||
var letters = word.ToCharArray();
|
||||
var count = 0;
|
||||
for (var i = 0; i < letters.Length; i++)
|
||||
{
|
||||
if (letters[i] == ' ')
|
||||
continue;
|
||||
|
||||
count++;
|
||||
if (count <= letters.Length / 5)
|
||||
continue;
|
||||
|
||||
if (count % 3 == 0)
|
||||
continue;
|
||||
|
||||
if (letters[i] != ' ')
|
||||
letters[i] = '_';
|
||||
}
|
||||
return string.Join(" ", new string(letters).Replace(" ", " \u2000").AsEnumerable());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaQuestionPool
|
||||
{
|
||||
public class PokemonNameId
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private static TriviaQuestionPool _instance;
|
||||
public static TriviaQuestionPool Instance { get; } = _instance ?? (_instance = new TriviaQuestionPool());
|
||||
|
||||
private const string questionsFile = "data/trivia_questions.json";
|
||||
private const string pokemonMapPath = "data/pokemon/name-id_map4.json";
|
||||
private readonly int maxPokemonId;
|
||||
|
||||
private Random rng { get; } = new NadekoRandom();
|
||||
|
||||
private TriviaQuestion[] pool { get; }
|
||||
private ImmutableDictionary<int, string> map { get; }
|
||||
|
||||
static TriviaQuestionPool() { }
|
||||
|
||||
private TriviaQuestionPool()
|
||||
{
|
||||
pool = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(questionsFile));
|
||||
map = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(pokemonMapPath))
|
||||
.ToDictionary(x => x.Id, x => x.Name)
|
||||
.ToImmutableDictionary();
|
||||
|
||||
maxPokemonId = 721; //xd
|
||||
}
|
||||
|
||||
public TriviaQuestion GetRandomQuestion(HashSet<TriviaQuestion> exclude, bool isPokemon)
|
||||
{
|
||||
if (pool.Length == 0)
|
||||
return null;
|
||||
|
||||
if (isPokemon)
|
||||
{
|
||||
var num = rng.Next(1, maxPokemonId + 1);
|
||||
return new TriviaQuestion("Who's That Pokémon?",
|
||||
map[num].ToTitleCase(),
|
||||
"Pokemon",
|
||||
$@"http://nadekobot.me/images/pokemon/shadows/{num}.png",
|
||||
$@"http://nadekobot.me/images/pokemon/real/{num}.png");
|
||||
}
|
||||
TriviaQuestion randomQuestion;
|
||||
while (exclude.Contains(randomQuestion = pool[rng.Next(0, pool.Length)])) ;
|
||||
|
||||
return randomQuestion;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user