Restructured folders and project names, ci should be fixed

This commit is contained in:
Kwoth
2021-06-17 23:40:48 +02:00
parent 7aca29ae8a
commit 91ecf9ca41
788 changed files with 204 additions and 146 deletions

View File

@@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using NadekoBot.Common;
using NadekoBot.Core.Common;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Games.Common.Acrophobia
{
public sealed class AcrophobiaGame : IDisposable
{
public class Options : INadekoCommandOptions
{
[Option('s', "submission-time", Required = false, Default = 60, HelpText = "Time after which the submissions are closed and voting starts.")]
public int SubmissionTime { get; set; } = 60;
[Option('v', "vote-time", Required = false, Default = 60, HelpText = "Time after which the voting is closed and the winner is declared.")]
public int VoteTime { get; set; } = 30;
public void NormalizeOptions()
{
if (SubmissionTime < 15 || SubmissionTime > 300)
SubmissionTime = 60;
if (VoteTime < 15 || VoteTime > 120)
VoteTime = 30;
}
}
public enum Phase
{
Submission,
Voting,
Ended
}
public enum UserInputResult
{
Submitted,
SubmissionFailed,
Voted,
VotingFailed,
Failed
}
public Phase CurrentPhase { get; private set; } = Phase.Submission;
public ImmutableArray<char> StartingLetters { get; private set; }
private readonly Dictionary<AcrophobiaUser, int> submissions = new Dictionary<AcrophobiaUser, int>();
private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
public Options Opts { get; }
private readonly NadekoRandom _rng;
public event Func<AcrophobiaGame, Task> OnStarted = delegate { return Task.CompletedTask; };
public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnVotingStarted = delegate { return Task.CompletedTask; };
public event Func<string, Task> OnUserVoted = delegate { return Task.CompletedTask; };
public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnEnded = delegate { return Task.CompletedTask; };
private readonly HashSet<ulong> _usersWhoVoted = new HashSet<ulong>();
public AcrophobiaGame(Options options)
{
Opts = options;
_rng = new NadekoRandom();
InitializeStartingLetters();
}
public async Task Run()
{
await OnStarted(this).ConfigureAwait(false);
await Task.Delay(Opts.SubmissionTime * 1000).ConfigureAwait(false);
await locker.WaitAsync().ConfigureAwait(false);
try
{
if (submissions.Count == 0)
{
CurrentPhase = Phase.Ended;
await OnVotingStarted(this, ImmutableArray.Create<KeyValuePair<AcrophobiaUser, int>>()).ConfigureAwait(false);
return;
}
if (submissions.Count == 1)
{
CurrentPhase = Phase.Ended;
await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
return;
}
CurrentPhase = Phase.Voting;
await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
}
finally { locker.Release(); }
await Task.Delay(Opts.VoteTime * 1000).ConfigureAwait(false);
await locker.WaitAsync().ConfigureAwait(false);
try
{
CurrentPhase = Phase.Ended;
await OnEnded(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
}
finally { locker.Release(); }
}
private void InitializeStartingLetters()
{
var wordCount = _rng.Next(3, 6);
var lettersArr = new char[wordCount];
for (int i = 0; i < wordCount; i++)
{
var randChar = (char)_rng.Next(65, 91);
lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar;
}
StartingLetters = lettersArr.ToImmutableArray();
}
public async Task<bool> UserInput(ulong userId, string userName, string input)
{
var user = new AcrophobiaUser(userId, userName, input.ToLowerInvariant().ToTitleCase());
await locker.WaitAsync().ConfigureAwait(false);
try
{
switch (CurrentPhase)
{
case Phase.Submission:
if (submissions.ContainsKey(user) || !IsValidAnswer(input))
break;
submissions.Add(user, 0);
return true;
case Phase.Voting:
AcrophobiaUser toVoteFor;
if (!int.TryParse(input, out var index)
|| --index < 0
|| index >= submissions.Count
|| (toVoteFor = submissions.ToArray()[index].Key).UserId == user.UserId
|| !_usersWhoVoted.Add(userId))
break;
++submissions[toVoteFor];
var _ = Task.Run(() => OnUserVoted(userName));
return true;
default:
break;
}
return false;
}
finally
{
locker.Release();
}
}
private bool IsValidAnswer(string input)
{
input = input.ToUpperInvariant();
var inputWords = input.Split(' ');
if (inputWords.Length != StartingLetters.Length) // number of words must be the same as the number of the starting letters
return false;
for (int i = 0; i < StartingLetters.Length; i++)
{
var letter = StartingLetters[i];
if (!inputWords[i].StartsWith(letter.ToString(), StringComparison.InvariantCulture)) // all first letters must match
return false;
}
return true;
}
public void Dispose()
{
this.CurrentPhase = Phase.Ended;
OnStarted = null;
OnEnded = null;
OnUserVoted = null;
OnVotingStarted = null;
_usersWhoVoted.Clear();
submissions.Clear();
locker.Dispose();
}
}
}

View File

@@ -0,0 +1,28 @@
namespace NadekoBot.Modules.Games.Common.Acrophobia
{
public class AcrophobiaUser
{
public string UserName { get; }
public ulong UserId { get; }
public string Input { get; }
public AcrophobiaUser(ulong userId, string userName, string input)
{
this.UserName = userName;
this.UserId = userId;
this.Input = input;
}
public override int GetHashCode()
{
return UserId.GetHashCode();
}
public override bool Equals(object obj)
{
return obj is AcrophobiaUser x
? x.UserId == this.UserId
: false;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public class ChatterBotResponse
{
public string Convo_id { get; set; }
public string BotSay { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json;
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public class ChatterBotSession : IChatterBotSession
{
private static NadekoRandom Rng { get; } = new NadekoRandom();
private readonly string _chatterBotId;
private readonly IHttpClientFactory _httpFactory;
private readonly int _botId = 6;
public ChatterBotSession(IHttpClientFactory httpFactory)
{
_chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64();
_httpFactory = httpFactory;
}
private string ApiEndpoint => "http://api.program-o.com/v2/chatbot/" +
$"?bot_id={_botId}&" +
"say={0}&" +
$"convo_id=nadekobot_{_chatterBotId}&" +
"format=json";
public async Task<string> Think(string message)
{
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync(string.Format(ApiEndpoint, message)).ConfigureAwait(false);
var cbr = JsonConvert.DeserializeObject<ChatterBotResponse>(res);
return cbr.BotSay.Replace("<br/>", "\n", StringComparison.InvariantCulture);
}
}
}
}

View File

@@ -0,0 +1,20 @@
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public class CleverbotResponse
{
public string Cs { get; set; }
public string Output { get; set; }
}
public class CleverbotIOCreateResponse
{
public string Status { get; set; }
public string Nick { get; set; }
}
public class CleverbotIOAskResponse
{
public string Status { get; set; }
public string Response { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public interface IChatterBotSession
{
Task<string> Think(string input);
}
}

View File

@@ -0,0 +1,109 @@
using NadekoBot.Common;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public class OfficialCleverbotSession : IChatterBotSession
{
private readonly string _apiKey;
private readonly IHttpClientFactory _httpFactory;
private string _cs = null;
private string QueryString => $"https://www.cleverbot.com/getreply?key={_apiKey}" +
"&wrapper=nadekobot" +
"&input={0}" +
"&cs={1}";
public OfficialCleverbotSession(string apiKey, IHttpClientFactory factory)
{
this._apiKey = apiKey;
this._httpFactory = factory;
}
public async Task<string> Think(string input)
{
using (var http = _httpFactory.CreateClient())
{
var dataString = await http.GetStringAsync(string.Format(QueryString, input, _cs ?? "")).ConfigureAwait(false);
try
{
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
_cs = data?.Cs;
return data?.Output;
}
catch
{
Log.Warning("Unexpected cleverbot response received: ");
Log.Warning(dataString);
return null;
}
}
}
}
public class CleverbotIOSession : IChatterBotSession
{
private readonly string _key;
private readonly string _user;
private readonly IHttpClientFactory _httpFactory;
private readonly AsyncLazy<string> _nick;
private readonly string _createEndpoint = $"https://cleverbot.io/1.0/create";
private readonly string _askEndpoint = $"https://cleverbot.io/1.0/ask";
public CleverbotIOSession(string user, string key, IHttpClientFactory factory)
{
this._key = key;
this._user = user;
this._httpFactory = factory;
_nick = new AsyncLazy<string>((Func<Task<string>>)GetNick);
}
private async Task<string> GetNick()
{
using (var _http = _httpFactory.CreateClient())
using (var msg = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("user", _user),
new KeyValuePair<string, string>("key", _key),
}))
using (var data = await _http.PostAsync(_createEndpoint, msg).ConfigureAwait(false))
{
var str = await data.Content.ReadAsStringAsync().ConfigureAwait(false);
var obj = JsonConvert.DeserializeObject<CleverbotIOCreateResponse>(str);
if (obj.Status != "success")
throw new OperationCanceledException(obj.Status);
return obj.Nick;
}
}
public async Task<string> Think(string input)
{
using (var _http = _httpFactory.CreateClient())
using (var msg = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("user", _user),
new KeyValuePair<string, string>("key", _key),
new KeyValuePair<string, string>("nick", await _nick),
new KeyValuePair<string, string>("text", input),
}))
using (var data = await _http.PostAsync(_askEndpoint, msg).ConfigureAwait(false))
{
var str = await data.Content.ReadAsStringAsync().ConfigureAwait(false);
var obj = JsonConvert.DeserializeObject<CleverbotIOAskResponse>(str);
if (obj.Status != "success")
throw new OperationCanceledException(obj.Status);
return obj.Response;
}
}
}
}

View File

@@ -0,0 +1,72 @@
using System.Collections.Generic;
using NadekoBot.Common.Yml;
namespace NadekoBot.Modules.Games.Common
{
public sealed class GamesConfig
{
[Comment("Trivia related settings (.t command)")]
public TriviaConfig Trivia { get; set; } = new TriviaConfig()
{
CurrencyReward = 0,
MinimumWinReq = 1,
};
[Comment("List of responses for the .8ball command. A random one will be selected every time")]
public List<string> EightBallResponses { get; set; } = new List<string>()
{
"Most definitely yes.",
"For sure.",
"Totally!",
"Of course!",
"As I see it, yes.",
"My sources say yes.",
"Yes.",
"Most likely.",
"Perhaps...",
"Maybe...",
"Hm, not sure.",
"It is uncertain.",
"Ask me again later.",
"Don't count on it.",
"Probably not.",
"Very doubtful.",
"Most likely no.",
"Nope.",
"No.",
"My sources say no.",
"Don't even think about it.",
"Definitely no.",
"NO - It may cause disease contraction!"
};
[Comment("List of animals which will be used for the animal race game (.race)")]
public List<RaceAnimal> RaceAnimals { get; set; } = new List<RaceAnimal>()
{
new RaceAnimal {Icon = "🐼", Name = "Panda"},
new RaceAnimal {Icon = "🐻", Name = "Bear"},
new RaceAnimal {Icon = "🐧", Name = "Pengu"},
new RaceAnimal {Icon = "🐨", Name = "Koala"},
new RaceAnimal {Icon = "🐬", Name = "Dolphin"},
new RaceAnimal {Icon = "🐞", Name = "Ladybird"},
new RaceAnimal {Icon = "🦀", Name = "Crab"},
new RaceAnimal {Icon = "🦄", Name = "Unicorn"}
};
}
public sealed class TriviaConfig
{
[Comment("The amount of currency awarded to the winner of the trivia game.")]
public long CurrencyReward { get; set; } = 0;
[Comment(@"Users won't be able to start trivia games which have
a smaller win requirement than the one specified by this setting.")]
public int MinimumWinReq { get; set; } = 1;
}
public sealed class RaceAnimal
{
public string Icon { get; set; }
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.IO;
using System.Net.Http;
using NadekoBot.Common;
using NadekoBot.Core.Services;
using Serilog;
using Image = SixLabors.ImageSharp.Image;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
namespace NadekoBot.Modules.Games.Common
{
public class GirlRating
{
private readonly IImageCache _images;
public double Crazy { get; }
public double Hot { get; }
public int Roll { get; }
public string Advice { get; }
private readonly IHttpClientFactory _httpFactory;
public AsyncLazy<Stream> Stream { get; }
public GirlRating(IImageCache images, IHttpClientFactory factory, double crazy, double hot, int roll, string advice)
{
_images = images;
Crazy = crazy;
Hot = hot;
Roll = roll;
Advice = advice; // convenient to have it here, even though atm there are only few different ones.
_httpFactory = factory;
Stream = new AsyncLazy<Stream>(() =>
{
try
{
using (var img = Image.Load(_images.RategirlMatrix))
{
const int minx = 35;
const int miny = 385;
const int length = 345;
var pointx = (int)(minx + length * (Hot / 10));
var pointy = (int)(miny - length * ((Crazy - 4) / 6));
using (var pointImg = Image.Load(_images.RategirlDot))
{
img.Mutate(x => x.DrawImage(pointImg, new Point(pointx - 10, pointy - 10), new GraphicsOptions()));
}
var imgStream = new MemoryStream();
img.SaveAsPng(imgStream);
return imgStream;
//using (var byteContent = new ByteArrayContent(imgStream.ToArray()))
//{
// http.AddFakeHeaders();
// using (var reponse = await http.PutAsync("https://transfer.sh/img.png", byteContent).ConfigureAwait(false))
// {
// url = await reponse.Content.ReadAsStringAsync().ConfigureAwait(false);
// }
//}
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error getting RateGirl image");
return null;
}
});
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
namespace NadekoBot.Modules.Games.Common.Hangman.Exceptions
{
public class TermNotFoundException : Exception
{
public TermNotFoundException() : base("Term of that type couldn't be found")
{
}
public TermNotFoundException(string message) : base(message)
{
}
public TermNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public sealed class Hangman : IDisposable
{
public string TermType { get; }
public TermPool TermPool { get; }
public HangmanObject Term { get; }
public string ScrambledWord => "`" + String.Concat(Term.Word.Select(c =>
{
if (c == ' ')
return " \u2000";
if (!(char.IsLetter(c) || char.IsDigit(c)))
return $" {c}";
c = char.ToLowerInvariant(c);
return _previousGuesses.Contains(c) ? $" {c}" : " ◯";
})) + "`";
private Phase _currentPhase = Phase.Active;
public Phase CurrentPhase
{
get => _currentPhase;
set
{
if (value == Phase.Ended)
_endingCompletionSource.TrySetResult(true);
_currentPhase = value;
}
}
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private readonly HashSet<ulong> _recentUsers = new HashSet<ulong>();
public uint Errors { get; private set; } = 0;
public uint MaxErrors { get; } = 6;
public event Func<Hangman, string, Task> OnGameEnded = delegate { return Task.CompletedTask; };
public event Func<Hangman, string, char, Task> OnLetterAlreadyUsed = delegate { return Task.CompletedTask; };
public event Func<Hangman, string, char, Task> OnGuessFailed = delegate { return Task.CompletedTask; };
public event Func<Hangman, string, char, Task> OnGuessSucceeded = delegate { return Task.CompletedTask; };
private readonly HashSet<char> _previousGuesses = new HashSet<char>();
public ImmutableArray<char> PreviousGuesses => _previousGuesses.ToImmutableArray();
private readonly TaskCompletionSource<bool> _endingCompletionSource = new TaskCompletionSource<bool>();
public Task EndedTask => _endingCompletionSource.Task;
public Hangman(string type, TermPool tp = null)
{
this.TermType = type.Trim().ToLowerInvariant().ToTitleCase();
this.TermPool = tp ?? new TermPool();
this.Term = this.TermPool.GetTerm(type);
}
private void AddError()
{
if (++Errors > MaxErrors)
{
var _ = OnGameEnded(this, null);
CurrentPhase = Phase.Ended;
}
}
public string GetHangman() => $@". ┌─────┐
.┃...............┋
.┃...............┋
.┃{(Errors > 0 ? ".............😲" : "")}
.{(Errors > 1 ? "............./" : "")} {(Errors > 2 ? "|" : "")} {(Errors > 3 ? "\\" : "")}
.{(Errors > 4 ? "............../" : "")} {(Errors > 5 ? "\\" : "")}
/-\";
public async Task Input(ulong userId, string userName, string input)
{
if (CurrentPhase == Phase.Ended)
return;
if (string.IsNullOrWhiteSpace(input))
return;
input = input.Trim().ToLowerInvariant();
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase == Phase.Ended)
return;
if (input.Length > 1) // tried to guess the whole word
{
if (input != Term.Word) // failed
return;
var _ = OnGameEnded?.Invoke(this, userName);
CurrentPhase = Phase.Ended;
return;
}
var ch = input[0];
if (!char.IsLetterOrDigit(ch))
return;
if (!_recentUsers.Add(userId)) // don't let a single user spam guesses
return;
if (!_previousGuesses.Add(ch)) // that letter was already guessed
{
var _ = OnLetterAlreadyUsed?.Invoke(this, userName, ch);
}
else if (!Term.Word.Contains(ch)) // guessed letter doesn't exist
{
var _ = OnGuessFailed?.Invoke(this, userName, ch);
AddError();
}
else if (Term.Word.All(x => _previousGuesses.IsSupersetOf(Term.Word.ToLowerInvariant()
.Where(char.IsLetterOrDigit))))
{
var _ = OnGameEnded.Invoke(this, userName); // if all letters are guessed
CurrentPhase = Phase.Ended;
}
else // guessed but not last letter
{
var _ = OnGuessSucceeded?.Invoke(this, userName, ch);
_recentUsers.Remove(userId); // he can guess again right away
return;
}
var clearSpam = Task.Run(async () =>
{
await Task.Delay(3000).ConfigureAwait(false); // remove the user from the spamlist after 5 seconds
_recentUsers.Remove(userId);
});
}
finally { _locker.Release(); }
}
public async Task Stop()
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
CurrentPhase = Phase.Ended;
}
finally { _locker.Release(); }
}
public void Dispose()
{
OnGameEnded = null;
OnGuessFailed = null;
OnGuessSucceeded = null;
OnLetterAlreadyUsed = null;
_previousGuesses.Clear();
_recentUsers.Clear();
// _locker.Dispose();
}
}
}

View File

@@ -0,0 +1,17 @@
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public class HangmanObject
{
public string Word { get; set; }
public string ImageUrl { get; set; }
public string GetWord()
{
var term = Word.ToTitleCase();
return $"[{term}](https://en.wikipedia.org/wiki/{term.Replace(' ', '_')})";
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Games.Common.Hangman
{
public enum Phase
{
Active,
Ended,
}
}

View File

@@ -0,0 +1,50 @@
using NadekoBot.Common;
using NadekoBot.Modules.Games.Common.Hangman.Exceptions;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Serilog;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public class TermPool
{
const string termsPath = "data/hangman.json";
public IReadOnlyDictionary<string, HangmanObject[]> Data { get; } = new Dictionary<string, HangmanObject[]>();
public TermPool()
{
try
{
Data = JsonConvert.DeserializeObject<Dictionary<string, HangmanObject[]>>(File.ReadAllText(termsPath));
Data = Data.ToDictionary(
x => x.Key.ToLowerInvariant(),
x => x.Value);
}
catch (Exception ex)
{
Log.Warning(ex, "Error loading Hangman Term pool");
}
}
public HangmanObject GetTerm(string type)
{
type = type?.Trim().ToLowerInvariant();
var rng = new NadekoRandom();
if (type == "random")
{
type = Data.Keys.ToArray()[rng.Next(0, Data.Keys.Count())];
}
if (!Data.TryGetValue(type, out var termTypes) || termTypes.Length == 0)
throw new TermNotFoundException();
var obj = termTypes[rng.Next(0, termTypes.Length)];
obj.Word = obj.Word.Trim().ToLowerInvariant();
return obj;
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace NadekoBot.Modules.Games.Common.Hangman
{
[Flags]
public enum TermTypes
{
Countries = 0,
Movies = 1,
Animals = 2,
Things = 4,
Random = 8,
}
}

View File

@@ -0,0 +1,185 @@
using NadekoBot.Common;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Nunchi
{
public sealed class NunchiGame : IDisposable
{
public enum Phase
{
Joining,
Playing,
WaitingForNextRound,
Ended,
}
public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100);
public Phase CurrentPhase { get; private set; } = Phase.Joining;
public event Func<NunchiGame, Task> OnGameStarted;
public event Func<NunchiGame, int, Task> OnRoundStarted;
public event Func<NunchiGame, Task> OnUserGuessed;
public event Func<NunchiGame, (ulong Id, string Name)?, Task> OnRoundEnded; // tuple of the user who failed
public event Func<NunchiGame, string, Task> OnGameEnded; // name of the user who won
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private HashSet<(ulong Id, string Name)> _participants = new HashSet<(ulong Id, string Name)>();
private HashSet<(ulong Id, string Name)> _passed = new HashSet<(ulong Id, string Name)>();
public ImmutableArray<(ulong Id, string Name)> Participants => _participants.ToImmutableArray();
public int ParticipantCount => _participants.Count;
private const int _killTimeout = 20 * 1000;
private const int _nextRoundTimeout = 5 * 1000;
private Timer _killTimer;
public NunchiGame(ulong creatorId, string creatorName)
{
_participants.Add((creatorId, creatorName));
}
public async Task<bool> Join(ulong userId, string userName)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase != Phase.Joining)
return false;
return _participants.Add((userId, userName));
}
finally { _locker.Release(); }
}
public async Task<bool> Initialize()
{
CurrentPhase = Phase.Joining;
await Task.Delay(30000).ConfigureAwait(false);
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (_participants.Count < 3)
{
CurrentPhase = Phase.Ended;
return false;
}
_killTimer = new Timer(async state =>
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase != Phase.Playing)
return;
//if some players took too long to type a number, boot them all out and start a new round
_participants = new HashSet<(ulong, string)>(_passed);
EndRound();
}
finally { _locker.Release(); }
}, null, _killTimeout, _killTimeout);
CurrentPhase = Phase.Playing;
var _ = OnGameStarted?.Invoke(this);
var __ = OnRoundStarted?.Invoke(this, CurrentNumber);
return true;
}
finally { _locker.Release(); }
}
public async Task Input(ulong userId, string userName, int input)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase != Phase.Playing)
return;
var userTuple = (Id: userId, Name: userName);
// if the user is not a member of the race,
// or he already successfully typed the number
// ignore the input
if (!_participants.Contains(userTuple) || !_passed.Add(userTuple))
return;
//if the number is correct
if (CurrentNumber == input - 1)
{
//increment current number
++CurrentNumber;
if (_passed.Count == _participants.Count - 1)
{
// if only n players are left, and n - 1 type the correct number, round is over
// if only 2 players are left, game is over
if (_participants.Count == 2)
{
_killTimer.Change(Timeout.Infinite, Timeout.Infinite);
CurrentPhase = Phase.Ended;
var _ = OnGameEnded?.Invoke(this, userTuple.Name);
}
else // else just start the new round without the user who was the last
{
var failure = _participants.Except(_passed).First();
OnUserGuessed?.Invoke(this);
EndRound(failure);
return;
}
}
OnUserGuessed?.Invoke(this);
}
else
{
//if the user failed
EndRound(userTuple);
}
}
finally { _locker.Release(); }
}
private void EndRound((ulong, string)? failure = null)
{
_killTimer.Change(_killTimeout, _killTimeout);
CurrentNumber = new NadekoRandom().Next(0, 100); // reset the counter
_passed.Clear(); // reset all users who passed (new round starts)
if(failure != null)
_participants.Remove(failure.Value); // remove the dude who failed from the list of players
var __ = OnRoundEnded?.Invoke(this, failure);
if (_participants.Count <= 1) // means we have a winner or everyone was booted out
{
_killTimer.Change(Timeout.Infinite, Timeout.Infinite);
CurrentPhase = Phase.Ended;
var _ = OnGameEnded?.Invoke(this, _participants.Count > 0 ? _participants.First().Name : null);
return;
}
CurrentPhase = Phase.WaitingForNextRound;
var throwawayDelay = Task.Run(async () =>
{
await Task.Delay(_nextRoundTimeout).ConfigureAwait(false);
CurrentPhase = Phase.Playing;
var ___ = OnRoundStarted?.Invoke(this, CurrentNumber);
});
}
public void Dispose()
{
OnGameEnded = null;
OnGameStarted = null;
OnRoundEnded = null;
OnRoundStarted = null;
OnUserGuessed = null;
}
}
}

View File

@@ -0,0 +1,72 @@
using System.Threading.Tasks;
using Discord;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services;
using System;
using System.Threading;
namespace NadekoBot.Modules.Games.Common
{
public class PollRunner
{
public Poll Poll { get; }
private readonly DbService _db;
public event Func<IUserMessage, IGuildUser, Task> OnVoted;
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
public PollRunner(DbService db, Poll poll)
{
_db = db;
Poll = poll;
}
public async Task<bool> TryVote(IUserMessage msg)
{
PollVote voteObj;
await _locker.WaitAsync().ConfigureAwait(false);
try
{
// has to be a user message
// channel must be the same the poll started in
if (msg == null || msg.Author.IsBot || msg.Channel.Id != Poll.ChannelId)
return false;
// has to be an integer
if (!int.TryParse(msg.Content, out int vote))
return false;
--vote;
if (vote < 0 || vote >= Poll.Answers.Count)
return false;
var usr = msg.Author as IGuildUser;
if (usr == null)
return false;
voteObj = new PollVote()
{
UserId = msg.Author.Id,
VoteIndex = vote,
};
if (!Poll.Votes.Add(voteObj))
return false;
var _ = OnVoted?.Invoke(msg, usr);
}
finally { _locker.Release(); }
using (var uow = _db.GetDbContext())
{
var trackedPoll = uow.Polls.GetById(Poll.Id);
trackedPoll.Votes.Add(voteObj);
uow.SaveChanges();
}
return true;
}
public void End()
{
OnVoted = null;
}
}
}

View File

@@ -0,0 +1,291 @@
using Discord;
using Discord.WebSocket;
using NadekoBot.Extensions;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using NadekoBot.Core.Common;
using NadekoBot.Core.Services;
namespace NadekoBot.Modules.Games.Common
{
public class TicTacToe
{
public class Options : INadekoCommandOptions
{
public void NormalizeOptions()
{
if (TurnTimer < 5 || TurnTimer > 60)
TurnTimer = 15;
}
[Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. Default 15.")]
public int TurnTimer { get; set; } = 15;
}
enum Phase
{
Starting,
Started,
Ended
}
private readonly ITextChannel _channel;
private readonly IGuildUser[] _users;
private readonly int?[,] _state;
private Phase _phase;
private int _curUserIndex;
private readonly SemaphoreSlim _moveLock;
private IGuildUser _winner;
private readonly string[] _numbers = { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:" };
public event Action<TicTacToe> OnEnded;
private IUserMessage _previousMessage;
private Timer _timeoutTimer;
private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client;
private readonly Options _options;
public TicTacToe(IBotStrings strings, DiscordSocketClient client, ITextChannel channel,
IGuildUser firstUser, Options options)
{
_channel = channel;
_strings = strings;
_client = client;
_options = options;
_users = new[] { firstUser, null };
_state = new int?[,] {
{ null, null, null },
{ null, null, null },
{ null, null, null },
};
_phase = Phase.Starting;
_moveLock = new SemaphoreSlim(1, 1);
}
private string GetText(string key, params object[] replacements)
=> _strings.GetText(key, _channel.GuildId, replacements);
public string GetState()
{
var sb = new StringBuilder();
for (var i = 0; i < _state.GetLength(0); i++)
{
for (var j = 0; j < _state.GetLength(1); j++)
{
sb.Append(_state[i, j] == null ? _numbers[i * 3 + j] : GetIcon(_state[i, j]));
if (j < _state.GetLength(1) - 1)
sb.Append("┃");
}
if (i < _state.GetLength(0) - 1)
sb.AppendLine("\n──────────");
}
return sb.ToString();
}
public EmbedBuilder GetEmbed(string title = null)
{
var embed = new EmbedBuilder()
.WithOkColor()
.WithDescription(Environment.NewLine + GetState())
.WithAuthor(eab => eab.WithName(GetText("vs", _users[0], _users[1])));
if (!string.IsNullOrWhiteSpace(title))
embed.WithTitle(title);
if (_winner == null)
{
if (_phase == Phase.Ended)
embed.WithFooter(efb => efb.WithText(GetText("ttt_no_moves")));
else
embed.WithFooter(efb => efb.WithText(GetText("ttt_users_move", _users[_curUserIndex])));
}
else
embed.WithFooter(efb => efb.WithText(GetText("ttt_has_won", _winner)));
return embed;
}
private static string GetIcon(int? val)
{
switch (val)
{
case 0:
return "❌";
case 1:
return "⭕";
case 2:
return "❎";
case 3:
return "🅾";
default:
return "⬛";
}
}
public async Task Start(IGuildUser user)
{
if (_phase == Phase.Started || _phase == Phase.Ended)
{
await _channel.SendErrorAsync(user.Mention + GetText("ttt_already_running")).ConfigureAwait(false);
return;
}
else if (_users[0] == user)
{
await _channel.SendErrorAsync(user.Mention + GetText("ttt_against_yourself")).ConfigureAwait(false);
return;
}
_users[1] = user;
_phase = Phase.Started;
_timeoutTimer = new Timer(async (_) =>
{
await _moveLock.WaitAsync().ConfigureAwait(false);
try
{
if (_phase == Phase.Ended)
return;
_phase = Phase.Ended;
if (_users[1] != null)
{
_winner = _users[_curUserIndex ^= 1];
var del = _previousMessage?.DeleteAsync();
try
{
await _channel.EmbedAsync(GetEmbed(GetText("ttt_time_expired"))).ConfigureAwait(false);
if (del != null)
await del.ConfigureAwait(false);
}
catch { }
}
OnEnded?.Invoke(this);
}
catch { }
finally
{
_moveLock.Release();
}
}, null, _options.TurnTimer * 1000, Timeout.Infinite);
_client.MessageReceived += Client_MessageReceived;
_previousMessage = await _channel.EmbedAsync(GetEmbed(GetText("game_started"))).ConfigureAwait(false);
}
private bool IsDraw()
{
for (var i = 0; i < 3; i++)
{
for (var j = 0; j < 3; j++)
{
if (_state[i, j] == null)
return false;
}
}
return true;
}
private Task Client_MessageReceived(SocketMessage msg)
{
var _ = Task.Run(async () =>
{
await _moveLock.WaitAsync().ConfigureAwait(false);
try
{
var curUser = _users[_curUserIndex];
if (_phase == Phase.Ended || msg.Author?.Id != curUser.Id)
return;
if (int.TryParse(msg.Content, out var index) &&
--index >= 0 &&
index <= 9 &&
_state[index / 3, index % 3] == null)
{
_state[index / 3, index % 3] = _curUserIndex;
// i'm lazy
if (_state[index / 3, 0] == _state[index / 3, 1] && _state[index / 3, 1] == _state[index / 3, 2])
{
_state[index / 3, 0] = _curUserIndex + 2;
_state[index / 3, 1] = _curUserIndex + 2;
_state[index / 3, 2] = _curUserIndex + 2;
_phase = Phase.Ended;
}
else if (_state[0, index % 3] == _state[1, index % 3] && _state[1, index % 3] == _state[2, index % 3])
{
_state[0, index % 3] = _curUserIndex + 2;
_state[1, index % 3] = _curUserIndex + 2;
_state[2, index % 3] = _curUserIndex + 2;
_phase = Phase.Ended;
}
else if (_curUserIndex == _state[0, 0] && _state[0, 0] == _state[1, 1] && _state[1, 1] == _state[2, 2])
{
_state[0, 0] = _curUserIndex + 2;
_state[1, 1] = _curUserIndex + 2;
_state[2, 2] = _curUserIndex + 2;
_phase = Phase.Ended;
}
else if (_curUserIndex == _state[0, 2] && _state[0, 2] == _state[1, 1] && _state[1, 1] == _state[2, 0])
{
_state[0, 2] = _curUserIndex + 2;
_state[1, 1] = _curUserIndex + 2;
_state[2, 0] = _curUserIndex + 2;
_phase = Phase.Ended;
}
var reason = "";
if (_phase == Phase.Ended) // if user won, stop receiving moves
{
reason = GetText("ttt_matched_three");
_winner = _users[_curUserIndex];
_client.MessageReceived -= Client_MessageReceived;
OnEnded?.Invoke(this);
}
else if (IsDraw())
{
reason = GetText("ttt_a_draw");
_phase = Phase.Ended;
_client.MessageReceived -= Client_MessageReceived;
OnEnded?.Invoke(this);
}
var sendstate = Task.Run(async () =>
{
var del1 = msg.DeleteAsync();
var del2 = _previousMessage?.DeleteAsync();
try { _previousMessage = await _channel.EmbedAsync(GetEmbed(reason)).ConfigureAwait(false); } catch { }
try { await del1.ConfigureAwait(false); } catch { }
try { if (del2 != null) await del2.ConfigureAwait(false); } catch { }
});
_curUserIndex ^= 1;
_timeoutTimer.Change(_options.TurnTimer * 1000, Timeout.Infinite);
}
}
finally
{
_moveLock.Release();
}
});
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,283 @@
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.Modules.Games.Common.Trivia;
using NadekoBot.Modules.Games.Services;
using Serilog;
namespace NadekoBot.Modules.Games.Common.Trivia
{
public class TriviaGame
{
private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1);
private readonly IDataCache _cache;
private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client;
private readonly GamesConfig _config;
private readonly ICurrencyService _cs;
private readonly TriviaOptions _options;
public IGuild Guild { get; }
public ITextChannel Channel { 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; }
private readonly TriviaQuestionPool _questionPool;
private int _timeoutCount = 0;
private readonly string _quitCommand;
public TriviaGame(IBotStrings strings, DiscordSocketClient client, GamesConfig config,
IDataCache cache, ICurrencyService cs, IGuild guild, ITextChannel channel,
TriviaOptions options, string quitCommand)
{
_cache = cache;
_questionPool = new TriviaQuestionPool(_cache);
_strings = strings;
_client = client;
_config = config;
_cs = cs;
_options = options;
_quitCommand = quitCommand;
Guild = guild;
Channel = channel;
}
private string GetText(string key, params object[] replacements)
=> _strings.GetText(key, Channel.GuildId, replacements);
public async Task StartGame()
{
var showHowToQuit = false;
while (!ShouldStopGame)
{
// reset the cancellation source
_triviaCancelSource = new CancellationTokenSource();
showHowToQuit = !showHowToQuit;
// load question
CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.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 (showHowToQuit)
questionEmbed.WithFooter(GetText("trivia_quit", _quitCommand));
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.Warning(ex, "Error sending trivia embed");
await Task.Delay(2000).ConfigureAwait(false);
continue;
}
//receive messages
try
{
_client.MessageReceived += PotentialGuess;
//allow people to guess
GameActive = true;
try
{
//hint
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false);
if (!_options.NoHint)
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.Warning(ex, "Error editing triva message"); }
//timeout
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false);
}
catch (TaskCanceledException) { _timeoutCount = 0; } //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);
if (_options.Timeout != 0 && ++_timeoutCount >= _options.Timeout)
await StopGame().ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia time's up message");
}
}
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.Warning(ex, "Error sending trivia stopping message");
}
}
}
private Task PotentialGuess(SocketMessage imsg)
{
var _ = Task.Run(async () =>
{
try
{
if (imsg.Author.IsBot)
return;
var umsg = imsg as SocketUserMessage;
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 (_options.WinRequirement != 0 && Users[guildUser] == _options.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 = _config.Trivia.CurrencyReward;
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.Warning(ex.ToString()); }
});
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();
}
}
}

View File

@@ -0,0 +1,30 @@
using CommandLine;
using NadekoBot.Core.Common;
namespace NadekoBot.Core.Modules.Games.Common.Trivia
{
public class TriviaOptions : INadekoCommandOptions
{
[Option('p', "pokemon", Required = false, Default = false, HelpText = "Whether it's 'Who's that pokemon?' trivia.")]
public bool IsPokemon { get; set; } = false;
[Option("nohint", Required = false, Default = false, HelpText = "Don't show any hints.")]
public bool NoHint { get; set; } = false;
[Option('w', "win-req", Required = false, Default = 10, HelpText = "Winning requirement. Set 0 for an infinite game. Default 10.")]
public int WinRequirement { get; set; } = 10;
[Option('q', "question-timer", Required = false, Default = 30, HelpText = "How long until the question ends. Default 30.")]
public int QuestionTimer { get; set; } = 30;
[Option('t', "timeout", Required = false, Default = 10, HelpText = "Number of questions of inactivity in order stop. Set 0 for never. Default 10.")]
public int Timeout { get; set; } = 10;
public void NormalizeOptions()
{
if (WinRequirement < 0)
WinRequirement = 10;
if (QuestionTimer < 10 || QuestionTimer > 300)
QuestionTimer = 30;
if (Timeout < 0 || Timeout > 20)
Timeout = 10;
}
}
}

View 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 const 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, StringComparison.InvariantCulture))
{
return true;
}
var cleanGuess = Clean(guess);
if (CleanAnswer.Equals(cleanGuess, StringComparison.InvariantCulture))
{
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 static 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 static string Clean(string str)
{
str = " " + str.ToLowerInvariant() + " ";
str = Regex.Replace(str, "\\s+", " ");
str = Regex.Replace(str, "[^\\w\\d\\s]", "");
//Here's where custom modification can be done
str = Regex.Replace(str, "\\s(a|an|the|of|in|for|to|as|at|be)\\s", " ");
//End custom mod and cleanup whitespace
str = Regex.Replace(str, "^\\s+", "");
str = Regex.Replace(str, "\\s+$", "");
//Trim the really long answers
str = str.Length <= maxStringLength ? str : str.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", StringComparison.InvariantCulture).AsEnumerable());
}
}
}

View File

@@ -0,0 +1,44 @@
using NadekoBot.Common;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using System.Collections.Generic;
namespace NadekoBot.Modules.Games.Common.Trivia
{
public class TriviaQuestionPool
{
private readonly IDataCache _cache;
private readonly int maxPokemonId;
private readonly NadekoRandom _rng = new NadekoRandom();
private TriviaQuestion[] Pool => _cache.LocalData.TriviaQuestions;
private IReadOnlyDictionary<int, string> Map => _cache.LocalData.PokemonMap;
public TriviaQuestionPool(IDataCache cache)
{
_cache = cache;
maxPokemonId = 721; //xd
}
public TriviaQuestion GetRandomQuestion(HashSet<TriviaQuestion> exclude, bool isPokemon)
{
if (Pool.Length == 0)
return null;
if (isPokemon)
{
var num = _rng.Next(1, maxPokemonId + 1);
return new TriviaQuestion("Who's That Pokémon?",
Map[num].ToTitleCase(),
"Pokemon",
$@"https://nadeko.bot/images/pokemon/shadows/{num}.png",
$@"https://nadeko.bot/images/pokemon/real/{num}.png");
}
TriviaQuestion randomQuestion;
while (exclude.Contains(randomQuestion = Pool[_rng.Next(0, Pool.Length)])) ;
return randomQuestion;
}
}
}

View File

@@ -0,0 +1,9 @@
namespace NadekoBot.Modules.Games.Common
{
public class TypingArticle
{
public string Source { get; set; }
public string Extra { get; set; }
public string Text { get; set; }
}
}

View File

@@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Services;
using CommandLine;
using NadekoBot.Core.Common;
using Serilog;
namespace NadekoBot.Modules.Games.Common
{
public class TypingGame
{
public class Options : INadekoCommandOptions
{
[Option('s', "start-time", Default = 5, Required = false, HelpText = "How long does it take for the race to start. Default 5.")]
public int StartTime { get; set; } = 5;
public void NormalizeOptions()
{
if (StartTime < 3 || StartTime > 30)
StartTime = 5;
}
}
public const float WORD_VALUE = 4.5f;
public ITextChannel Channel { get; }
public string CurrentSentence { get; private set; }
public bool IsActive { get; private set; }
private readonly Stopwatch sw;
private readonly List<ulong> finishedUserIds;
private readonly DiscordSocketClient _client;
private readonly GamesService _games;
private readonly string _prefix;
private readonly Options _options;
public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel,
string prefix, Options options)
{
_games = games;
_client = client;
_prefix = prefix;
_options = options;
this.Channel = channel;
IsActive = false;
sw = new Stopwatch();
finishedUserIds = new List<ulong>();
}
public async Task<bool> Stop()
{
if (!IsActive) return false;
_client.MessageReceived -= AnswerReceived;
finishedUserIds.Clear();
IsActive = false;
sw.Stop();
sw.Reset();
try
{
await Channel.SendConfirmAsync("Typing contest stopped.").ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex.ToString());
}
return true;
}
public async Task Start()
{
if (IsActive) return; // can't start running game
IsActive = true;
CurrentSentence = GetRandomSentence();
var i = (int)(CurrentSentence.Length / WORD_VALUE * 1.7f);
try
{
await Channel.SendConfirmAsync($@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can.").ConfigureAwait(false);
var time = _options.StartTime;
var msg = await Channel.SendMessageAsync($"Starting new typing contest in **{time}**...", options: new RequestOptions()
{
RetryMode = RetryMode.AlwaysRetry
}).ConfigureAwait(false);
do
{
await Task.Delay(2000).ConfigureAwait(false);
time -= 2;
try { await msg.ModifyAsync(m => m.Content = $"Starting new typing contest in **{time}**..").ConfigureAwait(false); } catch { }
} while (time > 2);
await msg.ModifyAsync(m => {
m.Content = CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture);
}).ConfigureAwait(false);
sw.Start();
HandleAnswers();
while (i > 0)
{
await Task.Delay(1000).ConfigureAwait(false);
i--;
if (!IsActive)
return;
}
}
catch { }
finally
{
await Stop().ConfigureAwait(false);
}
}
public string GetRandomSentence()
{
if (_games.TypingArticles.Any())
return _games.TypingArticles[new NadekoRandom().Next(0, _games.TypingArticles.Count)].Text;
else
return $"No typing articles found. Use {_prefix}typeadd command to add a new article for typing.";
}
private void HandleAnswers()
{
_client.MessageReceived += AnswerReceived;
}
private Task AnswerReceived(SocketMessage imsg)
{
var _ = Task.Run(async () =>
{
try
{
if (imsg.Author.IsBot)
return;
var msg = imsg as SocketUserMessage;
if (msg == null)
return;
if (this.Channel == null || this.Channel.Id != msg.Channel.Id) return;
var guess = msg.Content;
var distance = CurrentSentence.LevenshteinDistance(guess);
var decision = Judge(distance, guess.Length);
if (decision && !finishedUserIds.Contains(msg.Author.Id))
{
var elapsed = sw.Elapsed;
var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60;
finishedUserIds.Add(msg.Author.Id);
await this.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle($"{msg.Author} finished the race!")
.AddField(efb =>
efb.WithName("Place").WithValue($"#{finishedUserIds.Count}").WithIsInline(true))
.AddField(efb =>
efb.WithName("WPM").WithValue($"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*")
.WithIsInline(true))
.AddField(efb =>
efb.WithName("Errors").WithValue(distance.ToString()).WithIsInline(true)))
.ConfigureAwait(false);
if (finishedUserIds.Count % 4 == 0)
{
await this.Channel.SendConfirmAsync(
$":exclamation: A lot of people finished, here is the text for those still typing:" +
$"\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture)).SanitizeMentions(true)}**")
.ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
Log.Warning(ex.ToString());
}
});
return Task.CompletedTask;
}
private static bool Judge(int errors, int textLength) => errors <= textLength / 25;
}
}