Global usings and file scoped namespaces

This commit is contained in:
Kwoth
2021-12-19 05:14:11 +01:00
parent bc31dae965
commit ee33313519
548 changed files with 47528 additions and 49115 deletions

View File

@@ -2,146 +2,143 @@
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Common.Acrophobia;
using NadekoBot.Modules.Games.Services;
using NadekoBot.Common;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class AcropobiaCommands : NadekoSubmodule<GamesService>
{
private readonly DiscordSocketClient _client;
namespace NadekoBot.Modules.Games;
public AcropobiaCommands(DiscordSocketClient client)
public partial class Games
{
[Group]
public class AcropobiaCommands : NadekoSubmodule<GamesService>
{
private readonly DiscordSocketClient _client;
public AcropobiaCommands(DiscordSocketClient client)
{
_client = client;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptions(typeof(AcrophobiaGame.Options))]
public async Task Acrophobia(params string[] args)
{
var (options, _) = OptionsParser.ParseFrom(new AcrophobiaGame.Options(), args);
var channel = (ITextChannel)ctx.Channel;
var game = new AcrophobiaGame(options);
if (_service.AcrophobiaGames.TryAdd(channel.Id, game))
{
_client = client;
try
{
game.OnStarted += Game_OnStarted;
game.OnEnded += Game_OnEnded;
game.OnVotingStarted += Game_OnVotingStarted;
game.OnUserVoted += Game_OnUserVoted;
_client.MessageReceived += _client_MessageReceived;
await game.Run().ConfigureAwait(false);
}
finally
{
_client.MessageReceived -= _client_MessageReceived;
_service.AcrophobiaGames.TryRemove(channel.Id, out game);
game.Dispose();
}
}
else
{
await ReplyErrorLocalizedAsync(strs.acro_running).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptions(typeof(AcrophobiaGame.Options))]
public async Task Acrophobia(params string[] args)
Task _client_MessageReceived(SocketMessage msg)
{
var (options, _) = OptionsParser.ParseFrom(new AcrophobiaGame.Options(), args);
var channel = (ITextChannel)ctx.Channel;
if (msg.Channel.Id != ctx.Channel.Id)
return Task.CompletedTask;
var game = new AcrophobiaGame(options);
if (_service.AcrophobiaGames.TryAdd(channel.Id, game))
var _ = Task.Run(async () =>
{
try
{
game.OnStarted += Game_OnStarted;
game.OnEnded += Game_OnEnded;
game.OnVotingStarted += Game_OnVotingStarted;
game.OnUserVoted += Game_OnUserVoted;
_client.MessageReceived += _client_MessageReceived;
await game.Run().ConfigureAwait(false);
var success = await game.UserInput(msg.Author.Id, msg.Author.ToString(), msg.Content)
.ConfigureAwait(false);
if (success)
await msg.DeleteAsync().ConfigureAwait(false);
}
finally
{
_client.MessageReceived -= _client_MessageReceived;
_service.AcrophobiaGames.TryRemove(channel.Id, out game);
game.Dispose();
}
}
else
{
await ReplyErrorLocalizedAsync(strs.acro_running).ConfigureAwait(false);
}
catch { }
});
Task _client_MessageReceived(SocketMessage msg)
{
if (msg.Channel.Id != ctx.Channel.Id)
return Task.CompletedTask;
return Task.CompletedTask;
}
}
var _ = Task.Run(async () =>
{
try
{
var success = await game.UserInput(msg.Author.Id, msg.Author.ToString(), msg.Content)
.ConfigureAwait(false);
if (success)
await msg.DeleteAsync().ConfigureAwait(false);
}
catch { }
});
private Task Game_OnStarted(AcrophobiaGame game)
{
var embed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(GetText(strs.acro_started(Format.Bold(string.Join(".", game.StartingLetters)))))
.WithFooter(GetText(strs.acro_started_footer(game.Opts.SubmissionTime)));
return Task.CompletedTask;
}
return ctx.Channel.EmbedAsync(embed);
}
private Task Game_OnUserVoted(string user)
{
return SendConfirmAsync(
GetText(strs.acrophobia),
GetText(strs.acro_vote_cast(Format.Bold(user))));
}
private async Task Game_OnVotingStarted(AcrophobiaGame game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> submissions)
{
if (submissions.Length == 0)
{
await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_ended_no_sub)).ConfigureAwait(false);
return;
}
if (submissions.Length == 1)
{
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription(GetText(strs.acro_winner_only(Format.Bold(submissions.First().Key.UserName))))
.WithFooter(submissions.First().Key.Input))
.ConfigureAwait(false);
return;
}
private Task Game_OnStarted(AcrophobiaGame game)
{
var embed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(GetText(strs.acro_started(Format.Bold(string.Join(".", game.StartingLetters)))))
.WithFooter(GetText(strs.acro_started_footer(game.Opts.SubmissionTime)));
return ctx.Channel.EmbedAsync(embed);
}
private Task Game_OnUserVoted(string user)
{
return SendConfirmAsync(
GetText(strs.acrophobia),
GetText(strs.acro_vote_cast(Format.Bold(user))));
}
private async Task Game_OnVotingStarted(AcrophobiaGame game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> submissions)
{
if (submissions.Length == 0)
{
await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_ended_no_sub)).ConfigureAwait(false);
return;
}
if (submissions.Length == 1)
{
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription(GetText(strs.acro_winner_only(Format.Bold(submissions.First().Key.UserName))))
.WithFooter(submissions.First().Key.Input))
.ConfigureAwait(false);
return;
}
var i = 0;
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed))
.WithDescription(GetText(strs.acro_nym_was(Format.Bold(string.Join(".", game.StartingLetters)) + "\n" +
$@"--
var i = 0;
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed))
.WithDescription(GetText(strs.acro_nym_was(Format.Bold(string.Join(".", game.StartingLetters)) + "\n" +
$@"--
{submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.Input}**\n")}
--")))
.WithFooter(GetText(strs.acro_vote));
.WithFooter(GetText(strs.acro_vote));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
private async Task Game_OnEnded(AcrophobiaGame game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> votes)
private async Task Game_OnEnded(AcrophobiaGame game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> votes)
{
if (!votes.Any() || votes.All(x => x.Value == 0))
{
if (!votes.Any() || votes.All(x => x.Value == 0))
{
await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_no_votes_cast)).ConfigureAwait(false);
return;
}
var table = votes.OrderByDescending(v => v.Value);
var winner = table.First();
var embed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName),
Format.Bold(winner.Value.ToString()))))
.WithFooter(winner.Key.Input);
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_no_votes_cast)).ConfigureAwait(false);
return;
}
var table = votes.OrderByDescending(v => v.Value);
var winner = table.First();
var embed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName),
Format.Bold(winner.Value.ToString()))))
.WithFooter(winner.Key.Input);
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}

View File

@@ -1,60 +1,57 @@
using Discord;
using Discord.Commands;
using System;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Services;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
using NadekoBot.Modules.Games.Services;
using NadekoBot.Modules.Games.Common.ChatterBot;
namespace NadekoBot.Modules.Games
namespace NadekoBot.Modules.Games;
public partial class Games
{
public partial class Games
[Group]
public class ChatterBotCommands : NadekoSubmodule<ChatterBotService>
{
[Group]
public class ChatterBotCommands : NadekoSubmodule<ChatterBotService>
private readonly DbService _db;
public ChatterBotCommands(DbService db)
{
private readonly DbService _db;
public ChatterBotCommands(DbService db)
{
_db = db;
}
[NoPublicBot]
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task Cleverbot()
{
var channel = (ITextChannel)ctx.Channel;
if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _))
{
using (var uow = _db.GetDbContext())
{
uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, false);
await uow.SaveChangesAsync();
}
await ReplyConfirmLocalizedAsync(strs.cleverbot_disabled).ConfigureAwait(false);
return;
}
_service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy<IChatterBotSession>(() => _service.CreateSession(), true));
using (var uow = _db.GetDbContext())
{
uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, true);
await uow.SaveChangesAsync();
}
await ReplyConfirmLocalizedAsync(strs.cleverbot_enabled).ConfigureAwait(false);
}
_db = db;
}
[NoPublicBot]
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task Cleverbot()
{
var channel = (ITextChannel)ctx.Channel;
if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _))
{
using (var uow = _db.GetDbContext())
{
uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, false);
await uow.SaveChangesAsync();
}
await ReplyConfirmLocalizedAsync(strs.cleverbot_disabled).ConfigureAwait(false);
return;
}
_service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy<IChatterBotSession>(() => _service.CreateSession(), true));
using (var uow = _db.GetDbContext())
{
uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, true);
await uow.SaveChangesAsync();
}
await ReplyConfirmLocalizedAsync(strs.cleverbot_enabled).ConfigureAwait(false);
}
}
}

View File

@@ -1,189 +1,185 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using NadekoBot.Common;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Games.Common.Acrophobia
namespace NadekoBot.Modules.Games.Common.Acrophobia;
public sealed class AcrophobiaGame : IDisposable
{
public sealed class AcrophobiaGame : IDisposable
public class Options : INadekoCommandOptions
{
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()
{
[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();
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

@@ -1,28 +1,27 @@
namespace NadekoBot.Modules.Games.Common.Acrophobia
namespace NadekoBot.Modules.Games.Common.Acrophobia;
public class AcrophobiaUser
{
public class AcrophobiaUser
public string UserName { get; }
public ulong UserId { get; }
public string Input { get; }
public AcrophobiaUser(ulong userId, string userName, string input)
{
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;
}
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

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

View File

@@ -1,40 +1,38 @@
using System;
using System.Net.Http;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json;
namespace NadekoBot.Modules.Games.Common.ChatterBot
namespace NadekoBot.Modules.Games.Common.ChatterBot;
public class ChatterBotSession : IChatterBotSession
{
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)
{
private static NadekoRandom Rng { get; } = new NadekoRandom();
_chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64();
_httpFactory = httpFactory;
}
private readonly string _chatterBotId;
private readonly IHttpClientFactory _httpFactory;
private readonly int _botId = 6;
private string ApiEndpoint => "http://api.program-o.com/v2/chatbot/" +
$"?bot_id={_botId}&" +
"say={0}&" +
$"convo_id=nadekobot_{_chatterBotId}&" +
"format=json";
public ChatterBotSession(IHttpClientFactory httpFactory)
public async Task<string> Think(string message)
{
using (var http = _httpFactory.CreateClient())
{
_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);
}
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

@@ -1,20 +1,19 @@
namespace NadekoBot.Modules.Games.Common.ChatterBot
namespace NadekoBot.Modules.Games.Common.ChatterBot;
public class CleverbotResponse
{
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; }
}
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

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

View File

@@ -1,109 +1,105 @@
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
namespace NadekoBot.Modules.Games.Common.ChatterBot;
public class OfficialCleverbotSession : IChatterBotSession
{
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)
{
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;
}
}
}
this._apiKey = apiKey;
this._httpFactory = factory;
}
public class CleverbotIOSession : IChatterBotSession
public async Task<string> Think(string input)
{
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)
using (var http = _httpFactory.CreateClient())
{
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[]
var dataString = await http.GetStringAsync(string.Format(QueryString, input, _cs ?? "")).ConfigureAwait(false);
try
{
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);
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
return obj.Nick;
_cs = data?.Cs;
return data?.Output;
}
}
public async Task<string> Think(string input)
{
using (var _http = _httpFactory.CreateClient())
using (var msg = new FormUrlEncodedContent(new[]
catch
{
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;
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

@@ -1,93 +1,91 @@
using System.Collections.Generic;
using Cloneable;
using Cloneable;
using NadekoBot.Common;
using NadekoBot.Common.Yml;
namespace NadekoBot.Modules.Games.Common
namespace NadekoBot.Modules.Games.Common;
[Cloneable]
public sealed partial class GamesConfig : ICloneable<GamesConfig>
{
[Cloneable]
public sealed partial class GamesConfig : ICloneable<GamesConfig>
{
[Comment("DO NOT CHANGE")]
public int Version { get; set; }
[Comment("DO NOT CHANGE")]
public int Version { get; set; }
[Comment("Hangman related settings (.hangman command)")]
public HangmanConfig Hangman { get; set; } = new HangmanConfig()
{
CurrencyReward = 0
};
[Comment("Hangman related settings (.hangman command)")]
public HangmanConfig Hangman { get; set; } = new HangmanConfig()
{
CurrencyReward = 0
};
[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" }
};
}
[Cloneable]
public sealed partial class HangmanConfig
[Comment("Trivia related settings (.t command)")]
public TriviaConfig Trivia { get; set; } = new TriviaConfig()
{
[Comment("The amount of currency awarded to the winner of a hangman game")]
public long CurrencyReward { get; set; }
}
CurrencyReward = 0,
MinimumWinReq = 1,
};
[Cloneable]
public sealed partial class TriviaConfig
[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>()
{
[Comment("The amount of currency awarded to the winner of the trivia game.")]
public long CurrencyReward { get; set; } = 0;
"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(@"Users won't be able to start trivia games which have
[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" }
};
}
[Cloneable]
public sealed partial class HangmanConfig
{
[Comment("The amount of currency awarded to the winner of a hangman game")]
public long CurrencyReward { get; set; }
}
[Cloneable]
public sealed partial 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 int MinimumWinReq { get; set; } = 1;
}
[Cloneable]
public sealed partial class RaceAnimal
{
public string Icon { get; set; }
public string Name { get; set; }
}
[Cloneable]
public sealed partial class RaceAnimal
{
public string Icon { get; set; }
public string Name { get; set; }
}

View File

@@ -1,75 +1,72 @@
using System;
using System.IO;
using System.IO;
using System.Net.Http;
using NadekoBot.Common;
using NadekoBot.Services;
using Serilog;
using Image = SixLabors.ImageSharp.Image;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
namespace NadekoBot.Modules.Games.Common
namespace NadekoBot.Modules.Games.Common;
public class GirlRating
{
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)
{
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;
_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>(() =>
Stream = new AsyncLazy<Stream>(() =>
{
try
{
try
using (var img = Image.Load(_images.RategirlMatrix))
{
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))
{
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);
// }
//}
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;
}
});
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error getting RateGirl image");
return null;
}
});
}
}
}

View File

@@ -1,72 +1,67 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using NadekoBot.Common;
using NadekoBot.Common.Yml;
using Serilog;
namespace NadekoBot.Modules.Games.Hangman
namespace NadekoBot.Modules.Games.Hangman;
public sealed class DefaultHangmanSource : IHangmanSource
{
public sealed class DefaultHangmanSource : IHangmanSource
private IReadOnlyDictionary<string, HangmanTerm[]> _terms = new Dictionary<string, HangmanTerm[]>();
private readonly Random _rng;
public DefaultHangmanSource()
{
private IReadOnlyDictionary<string, HangmanTerm[]> _terms = new Dictionary<string, HangmanTerm[]>();
private readonly Random _rng;
_rng = new NadekoRandom();
Reload();
}
public DefaultHangmanSource()
public void Reload()
{
if (!Directory.Exists("data/hangman"))
{
_rng = new NadekoRandom();
Reload();
Log.Error("Hangman game won't work. Folder 'data/hangman' is missing.");
return;
}
public void Reload()
var qs = new Dictionary<string, HangmanTerm[]>();
foreach (var file in Directory.EnumerateFiles("data/hangman/", "*.yml"))
{
if (!Directory.Exists("data/hangman"))
try
{
Log.Error("Hangman game won't work. Folder 'data/hangman' is missing.");
return;
var data = Yaml.Deserializer.Deserialize<HangmanTerm[]>(File.ReadAllText(file));
qs[Path.GetFileNameWithoutExtension(file).ToLowerInvariant()] = data;
}
var qs = new Dictionary<string, HangmanTerm[]>();
foreach (var file in Directory.EnumerateFiles("data/hangman/", "*.yml"))
catch (Exception ex)
{
try
{
var data = Yaml.Deserializer.Deserialize<HangmanTerm[]>(File.ReadAllText(file));
qs[Path.GetFileNameWithoutExtension(file).ToLowerInvariant()] = data;
}
catch (Exception ex)
{
Log.Error(ex, "Loading {HangmanFile} failed.", file);
}
Log.Error(ex, "Loading {HangmanFile} failed.", file);
}
}
_terms = qs;
_terms = qs;
Log.Information("Loaded {HangmanCategoryCount} hangman categories.", qs.Count);
}
Log.Information("Loaded {HangmanCategoryCount} hangman categories.", qs.Count);
}
public IReadOnlyCollection<string> GetCategories()
=> _terms.Keys.ToList();
public IReadOnlyCollection<string> GetCategories()
=> _terms.Keys.ToList();
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term)
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term)
{
if (category is null)
{
if (category is null)
{
var cats = GetCategories();
category = cats.ElementAt(_rng.Next(0, cats.Count));
}
if (_terms.TryGetValue(category, out var terms))
{
term = terms[_rng.Next(0, terms.Length)];
return true;
}
term = null;
return false;
var cats = GetCategories();
category = cats.ElementAt(_rng.Next(0, cats.Count));
}
if (_terms.TryGetValue(category, out var terms))
{
term = terms[_rng.Next(0, terms.Length)];
return true;
}
term = null;
return false;
}
}

View File

@@ -1,121 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AngleSharp.Text;
using AngleSharp.Text;
namespace NadekoBot.Modules.Games.Hangman
namespace NadekoBot.Modules.Games.Hangman;
public sealed class HangmanGame
{
public sealed class HangmanGame
public enum Phase { Running, Ended }
public enum GuessResult { NoAction, AlreadyTried, Incorrect, Guess, Win }
public record State(
int Errors,
Phase Phase,
string Word,
GuessResult GuessResult,
List<char> missedLetters,
string ImageUrl)
{
public enum Phase { Running, Ended }
public enum GuessResult { NoAction, AlreadyTried, Incorrect, Guess, Win }
public bool Failed => Errors > 5;
}
public record State(
int Errors,
Phase Phase,
string Word,
GuessResult GuessResult,
List<char> missedLetters,
string ImageUrl)
{
public bool Failed => Errors > 5;
}
private Phase CurrentPhase { get; set; }
private Phase CurrentPhase { get; set; }
private readonly HashSet<char> _incorrect = new();
private readonly HashSet<char> _correct = new();
private readonly HashSet<char> _remaining = new();
private readonly HashSet<char> _incorrect = new();
private readonly HashSet<char> _correct = new();
private readonly HashSet<char> _remaining = new();
private readonly string _word;
private readonly string _imageUrl;
private readonly string _word;
private readonly string _imageUrl;
public HangmanGame(HangmanTerm term)
{
_word = term.Word;
_imageUrl = term.ImageUrl;
public HangmanGame(HangmanTerm term)
{
_word = term.Word;
_imageUrl = term.ImageUrl;
_remaining = _word
.ToLowerInvariant()
.Where(x => x.IsLetter())
.Select(char.ToLowerInvariant)
.ToHashSet();
_remaining = _word
.ToLowerInvariant()
.Where(x => x.IsLetter())
.Select(char.ToLowerInvariant)
.ToHashSet();
}
public State GetState(GuessResult guessResult = GuessResult.NoAction)
=> new State(_incorrect.Count,
CurrentPhase,
CurrentPhase == Phase.Ended
? _word
: GetScrambledWord(),
guessResult,
_incorrect.ToList(),
CurrentPhase == Phase.Ended
? _imageUrl
: string.Empty);
private string GetScrambledWord()
{
Span<char> output = stackalloc char[_word.Length * 2];
for (var i = 0; i < _word.Length; i++)
{
var ch = _word[i];
if (ch == ' ')
output[i*2] = '';
if (!ch.IsLetter() || !_remaining.Contains(char.ToLowerInvariant(ch)))
output[i*2] = ch;
else
output[i*2] = '_';
output[i * 2 + 1] = ' ';
}
public State GetState(GuessResult guessResult = GuessResult.NoAction)
=> new State(_incorrect.Count,
CurrentPhase,
CurrentPhase == Phase.Ended
? _word
: GetScrambledWord(),
guessResult,
_incorrect.ToList(),
CurrentPhase == Phase.Ended
? _imageUrl
: string.Empty);
return new(output);
}
private string GetScrambledWord()
public State Guess(string guess)
{
if (CurrentPhase != Phase.Running)
return GetState(GuessResult.NoAction);
guess = guess.Trim();
if (guess.Length > 1)
{
Span<char> output = stackalloc char[_word.Length * 2];
for (var i = 0; i < _word.Length; i++)
{
var ch = _word[i];
if (ch == ' ')
output[i*2] = '';
if (!ch.IsLetter() || !_remaining.Contains(char.ToLowerInvariant(ch)))
output[i*2] = ch;
else
output[i*2] = '_';
output[i * 2 + 1] = ' ';
}
return new(output);
}
public State Guess(string guess)
{
if (CurrentPhase != Phase.Running)
return GetState(GuessResult.NoAction);
guess = guess.Trim();
if (guess.Length > 1)
{
if (guess.Equals(_word, StringComparison.InvariantCultureIgnoreCase))
{
CurrentPhase = Phase.Ended;
return GetState(GuessResult.Win);
}
return GetState(GuessResult.NoAction);
}
var charGuess = guess[0];
if (!char.IsLetter(charGuess))
return GetState(GuessResult.NoAction);
if (_incorrect.Contains(charGuess) || _correct.Contains(charGuess))
return GetState(GuessResult.AlreadyTried);
if (_remaining.Remove(charGuess))
{
if (_remaining.Count == 0)
{
CurrentPhase = Phase.Ended;
return GetState(GuessResult.Win);
}
return GetState(GuessResult.Guess);
}
_incorrect.Add(charGuess);
if (_incorrect.Count > 5)
if (guess.Equals(_word, StringComparison.InvariantCultureIgnoreCase))
{
CurrentPhase = Phase.Ended;
return GetState(GuessResult.Incorrect);
return GetState(GuessResult.Win);
}
return GetState(GuessResult.NoAction);
}
var charGuess = guess[0];
if (!char.IsLetter(charGuess))
return GetState(GuessResult.NoAction);
if (_incorrect.Contains(charGuess) || _correct.Contains(charGuess))
return GetState(GuessResult.AlreadyTried);
if (_remaining.Remove(charGuess))
{
if (_remaining.Count == 0)
{
CurrentPhase = Phase.Ended;
return GetState(GuessResult.Win);
}
return GetState(GuessResult.Guess);
}
_incorrect.Add(charGuess);
if (_incorrect.Count > 5)
{
CurrentPhase = Phase.Ended;
return GetState(GuessResult.Incorrect);
}
return GetState(GuessResult.Incorrect);
}
}

View File

@@ -1,7 +1,5 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Discord;
@@ -11,137 +9,136 @@ using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Services;
using NadekoBot.Services;
namespace NadekoBot.Modules.Games.Hangman
namespace NadekoBot.Modules.Games.Hangman;
public sealed class HangmanService : IHangmanService, ILateExecutor
{
public sealed class HangmanService : IHangmanService, ILateExecutor
private readonly ConcurrentDictionary<ulong, HangmanGame> _hangmanGames = new();
private readonly IHangmanSource _source;
private readonly IEmbedBuilderService _eb;
private readonly GamesConfigService _gcs;
private readonly ICurrencyService _cs;
private readonly IMemoryCache _cdCache;
private readonly object _locker = new();
public HangmanService(IHangmanSource source, IEmbedBuilderService eb, GamesConfigService gcs,
ICurrencyService cs, IMemoryCache cdCache)
{
private readonly ConcurrentDictionary<ulong, HangmanGame> _hangmanGames = new();
private readonly IHangmanSource _source;
private readonly IEmbedBuilderService _eb;
private readonly GamesConfigService _gcs;
private readonly ICurrencyService _cs;
private readonly IMemoryCache _cdCache;
private readonly object _locker = new();
_source = source;
_eb = eb;
_gcs = gcs;
_cs = cs;
_cdCache = cdCache;
}
public HangmanService(IHangmanSource source, IEmbedBuilderService eb, GamesConfigService gcs,
ICurrencyService cs, IMemoryCache cdCache)
public bool StartHangman(
ulong channelId,
string? category,
[NotNullWhen(true)] out HangmanGame.State? state)
{
state = null;
if (!_source.GetTerm(category, out var term))
return false;
var game = new HangmanGame(term);
lock (_locker)
{
_source = source;
_eb = eb;
_gcs = gcs;
_cs = cs;
_cdCache = cdCache;
}
public bool StartHangman(
ulong channelId,
string? category,
[NotNullWhen(true)] out HangmanGame.State? state)
{
state = null;
if (!_source.GetTerm(category, out var term))
return false;
var game = new HangmanGame(term);
lock (_locker)
var hc = _hangmanGames.GetOrAdd(channelId, game);
if (hc == game)
{
var hc = _hangmanGames.GetOrAdd(channelId, game);
if (hc == game)
{
state = hc.GetState();
return true;
}
return false;
}
}
public ValueTask<bool> StopHangman(ulong channelId)
{
lock (_locker)
{
if (_hangmanGames.TryRemove(channelId, out var game))
{
return new(true);
}
state = hc.GetState();
return true;
}
return new(false);
}
public IReadOnlyCollection<string> GetHangmanTypes()
=> _source.GetCategories();
public async Task LateExecute(IGuild guild, IUserMessage msg)
{
if (_hangmanGames.ContainsKey(msg.Channel.Id))
{
if (string.IsNullOrWhiteSpace(msg.Content))
return;
if (_cdCache.TryGetValue(msg.Author.Id, out _))
return;
HangmanGame.State state;
long rew = 0;
lock (_locker)
{
if (!_hangmanGames.TryGetValue(msg.Channel.Id, out var game))
return;
state = game.Guess(msg.Content.ToLowerInvariant());
if (state.GuessResult == HangmanGame.GuessResult.NoAction)
return;
if (state.GuessResult == HangmanGame.GuessResult.Incorrect
|| state.GuessResult == HangmanGame.GuessResult.AlreadyTried)
{
_cdCache.Set(msg.Author.Id, string.Empty, new MemoryCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3)
});
}
if (state.Phase == HangmanGame.Phase.Ended)
if (_hangmanGames.TryRemove(msg.Channel.Id, out _))
rew = _gcs.Data.Hangman.CurrencyReward;
}
if (rew > 0)
await _cs.AddAsync(msg.Author, "hangman win", rew, gamble: true);
await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state);
}
}
private Task<IUserMessage> SendState(ITextChannel channel, IUser user, string content, HangmanGame.State state)
{
var embed = Games.HangmanCommands.GetEmbed(_eb, state);
if (state.GuessResult == HangmanGame.GuessResult.Guess)
embed.WithDescription($"{user} guessed the letter {content}!")
.WithOkColor();
else if (state.GuessResult == HangmanGame.GuessResult.Incorrect && state.Failed)
embed.WithDescription($"{user} Letter {content} doesn't exist! Game over!")
.WithErrorColor();
else if (state.GuessResult == HangmanGame.GuessResult.Incorrect)
embed.WithDescription($"{user} Letter {content} doesn't exist!")
.WithErrorColor();
else if (state.GuessResult == HangmanGame.GuessResult.AlreadyTried)
embed.WithDescription($"{user} Letter {content} has already been used.")
.WithPendingColor();
else if (state.GuessResult == HangmanGame.GuessResult.Win)
embed.WithDescription($"{user} won!")
.WithOkColor();
if (!string.IsNullOrWhiteSpace(state.ImageUrl)
&& Uri.IsWellFormedUriString(state.ImageUrl, UriKind.Absolute))
{
embed.WithImageUrl(state.ImageUrl);
}
return channel.EmbedAsync(embed);
return false;
}
}
public ValueTask<bool> StopHangman(ulong channelId)
{
lock (_locker)
{
if (_hangmanGames.TryRemove(channelId, out var game))
{
return new(true);
}
}
return new(false);
}
public IReadOnlyCollection<string> GetHangmanTypes()
=> _source.GetCategories();
public async Task LateExecute(IGuild guild, IUserMessage msg)
{
if (_hangmanGames.ContainsKey(msg.Channel.Id))
{
if (string.IsNullOrWhiteSpace(msg.Content))
return;
if (_cdCache.TryGetValue(msg.Author.Id, out _))
return;
HangmanGame.State state;
long rew = 0;
lock (_locker)
{
if (!_hangmanGames.TryGetValue(msg.Channel.Id, out var game))
return;
state = game.Guess(msg.Content.ToLowerInvariant());
if (state.GuessResult == HangmanGame.GuessResult.NoAction)
return;
if (state.GuessResult == HangmanGame.GuessResult.Incorrect
|| state.GuessResult == HangmanGame.GuessResult.AlreadyTried)
{
_cdCache.Set(msg.Author.Id, string.Empty, new MemoryCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3)
});
}
if (state.Phase == HangmanGame.Phase.Ended)
if (_hangmanGames.TryRemove(msg.Channel.Id, out _))
rew = _gcs.Data.Hangman.CurrencyReward;
}
if (rew > 0)
await _cs.AddAsync(msg.Author, "hangman win", rew, gamble: true);
await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state);
}
}
private Task<IUserMessage> SendState(ITextChannel channel, IUser user, string content, HangmanGame.State state)
{
var embed = Games.HangmanCommands.GetEmbed(_eb, state);
if (state.GuessResult == HangmanGame.GuessResult.Guess)
embed.WithDescription($"{user} guessed the letter {content}!")
.WithOkColor();
else if (state.GuessResult == HangmanGame.GuessResult.Incorrect && state.Failed)
embed.WithDescription($"{user} Letter {content} doesn't exist! Game over!")
.WithErrorColor();
else if (state.GuessResult == HangmanGame.GuessResult.Incorrect)
embed.WithDescription($"{user} Letter {content} doesn't exist!")
.WithErrorColor();
else if (state.GuessResult == HangmanGame.GuessResult.AlreadyTried)
embed.WithDescription($"{user} Letter {content} has already been used.")
.WithPendingColor();
else if (state.GuessResult == HangmanGame.GuessResult.Win)
embed.WithDescription($"{user} won!")
.WithOkColor();
if (!string.IsNullOrWhiteSpace(state.ImageUrl)
&& Uri.IsWellFormedUriString(state.ImageUrl, UriKind.Absolute))
{
embed.WithImageUrl(state.ImageUrl);
}
return channel.EmbedAsync(embed);
}
}

View File

@@ -1,8 +1,7 @@
namespace NadekoBot.Modules.Games.Hangman
namespace NadekoBot.Modules.Games.Hangman;
public sealed class HangmanTerm
{
public sealed class HangmanTerm
{
public string Word { get; set; }
public string ImageUrl { get; set; }
}
}
public string Word { get; set; }
public string ImageUrl { get; set; }
}

View File

@@ -1,14 +1,12 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Hangman
namespace NadekoBot.Modules.Games.Hangman;
public interface IHangmanService
{
public interface IHangmanService
{
bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? hangmanController);
ValueTask<bool> StopHangman(ulong channelId);
IReadOnlyCollection<string> GetHangmanTypes();
}
bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? hangmanController);
ValueTask<bool> StopHangman(ulong channelId);
IReadOnlyCollection<string> GetHangmanTypes();
}

View File

@@ -1,14 +1,12 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NadekoBot.Services;
namespace NadekoBot.Modules.Games.Hangman
namespace NadekoBot.Modules.Games.Hangman;
public interface IHangmanSource : INService
{
public interface IHangmanSource : INService
{
public IReadOnlyCollection<string> GetCategories();
public void Reload();
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term);
}
public IReadOnlyCollection<string> GetCategories();
public void Reload();
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term);
}

View File

@@ -1,185 +1,181 @@
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
namespace NadekoBot.Modules.Games.Common.Nunchi;
public sealed class NunchiGame : IDisposable
{
public sealed class NunchiGame : IDisposable
public enum Phase
{
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
{
Joining,
Playing,
WaitingForNextRound,
Ended,
if (CurrentPhase != Phase.Joining)
return false;
return _participants.Add((userId, userName));
}
finally { _locker.Release(); }
}
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)
public async Task<bool> Initialize()
{
CurrentPhase = Phase.Joining;
await Task.Delay(30000).ConfigureAwait(false);
await _locker.WaitAsync().ConfigureAwait(false);
try
{
_participants.Add((creatorId, creatorName));
}
public async Task<bool> Join(ulong userId, string userName)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
if (_participants.Count < 3)
{
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;
return false;
}
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;
_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

@@ -1,74 +1,70 @@
using System.Threading.Tasks;
using Discord;
using NadekoBot.Db.Models;
using System;
using System.Linq;
using System.Threading;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Games.Common
namespace NadekoBot.Modules.Games.Common;
public class PollRunner
{
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)
{
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 is 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 is 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.Poll.FirstOrDefault(x => x.Id == Poll.Id);
trackedPoll.Votes.Add(voteObj);
uow.SaveChanges();
}
return true;
}
public void End()
{
OnVoted = null;
}
_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 is 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 is 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.Poll.FirstOrDefault(x => x.Id == Poll.Id);
trackedPoll.Votes.Add(voteObj);
uow.SaveChanges();
}
return true;
}
public void End()
{
OnVoted = null;
}
}

View File

@@ -1,7 +1,6 @@
using Discord;
using Discord.WebSocket;
using NadekoBot.Extensions;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -9,285 +8,284 @@ using CommandLine;
using NadekoBot.Common;
using NadekoBot.Services;
namespace NadekoBot.Modules.Games.Common
namespace NadekoBot.Modules.Games.Common;
public class TicTacToe
{
public class TicTacToe
public class Options : INadekoCommandOptions
{
public class Options : INadekoCommandOptions
public void NormalizeOptions()
{
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;
private readonly IEmbedBuilderService _eb;
public TicTacToe(IBotStrings strings, DiscordSocketClient client, ITextChannel channel,
IGuildUser firstUser, Options options, IEmbedBuilderService eb)
{
_channel = channel;
_strings = strings;
_client = client;
_options = options;
_eb = eb;
_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(LocStr key)
=> _strings.GetText(key, _channel.GuildId);
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++)
{
if (TurnTimer < 5 || TurnTimer > 60)
TurnTimer = 15;
sb.Append(_state[i, j] is null ? _numbers[i * 3 + j] : GetIcon(_state[i, j]));
if (j < _state.GetLength(1) - 1)
sb.Append("┃");
}
[Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. Default 15.")]
public int TurnTimer { get; set; } = 15;
if (i < _state.GetLength(0) - 1)
sb.AppendLine("\n──────────");
}
enum Phase
return sb.ToString();
}
public IEmbedBuilder GetEmbed(string title = null)
{
var embed = _eb.Create()
.WithOkColor()
.WithDescription(Environment.NewLine + GetState())
.WithAuthor(GetText(strs.vs(_users[0], _users[1])));
if (!string.IsNullOrWhiteSpace(title))
embed.WithTitle(title);
if (_winner is null)
{
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;
private readonly IEmbedBuilderService _eb;
public TicTacToe(IBotStrings strings, DiscordSocketClient client, ITextChannel channel,
IGuildUser firstUser, Options options, IEmbedBuilderService eb)
{
_channel = channel;
_strings = strings;
_client = client;
_options = options;
_eb = eb;
_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(LocStr key)
=> _strings.GetText(key, _channel.GuildId);
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] is 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 IEmbedBuilder GetEmbed(string title = null)
{
var embed = _eb.Create()
.WithOkColor()
.WithDescription(Environment.NewLine + GetState())
.WithAuthor(GetText(strs.vs(_users[0], _users[1])));
if (!string.IsNullOrWhiteSpace(title))
embed.WithTitle(title);
if (_winner is null)
{
if (_phase == Phase.Ended)
embed.WithFooter(GetText(strs.ttt_no_moves));
else
embed.WithFooter(GetText(strs.ttt_users_move(_users[_curUserIndex])));
}
if (_phase == Phase.Ended)
embed.WithFooter(GetText(strs.ttt_no_moves));
else
embed.WithFooter(GetText(strs.ttt_has_won(_winner)));
return embed;
embed.WithFooter(GetText(strs.ttt_users_move(_users[_curUserIndex])));
}
else
embed.WithFooter(GetText(strs.ttt_has_won(_winner)));
private static string GetIcon(int? val)
return embed;
}
private static string GetIcon(int? val)
{
switch (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(_eb, user.Mention + GetText(strs.ttt_already_running)).ConfigureAwait(false);
return;
}
else if (_users[0] == user)
{
await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.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(strs.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(strs.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] is 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] is 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(strs.ttt_matched_three);
_winner = _users[_curUserIndex];
_client.MessageReceived -= Client_MessageReceived;
OnEnded?.Invoke(this);
}
else if (IsDraw())
{
reason = GetText(strs.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;
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(_eb, user.Mention + GetText(strs.ttt_already_running)).ConfigureAwait(false);
return;
}
else if (_users[0] == user)
{
await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.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(strs.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(strs.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] is 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] is 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(strs.ttt_matched_three);
_winner = _users[_curUserIndex];
_client.MessageReceived -= Client_MessageReceived;
OnEnded?.Invoke(this);
}
else if (IsDraw())
{
reason = GetText(strs.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

@@ -1,7 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -10,277 +7,273 @@ using Discord.Net;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Services;
using NadekoBot.Modules.Games.Common.Trivia;
using NadekoBot.Modules.Games.Services;
using Serilog;
namespace NadekoBot.Modules.Games.Common.Trivia
namespace NadekoBot.Modules.Games.Common.Trivia;
public class TriviaGame
{
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;
private readonly IEmbedBuilderService _eb;
public TriviaGame(IBotStrings strings, DiscordSocketClient client, GamesConfig config,
IDataCache cache, ICurrencyService cs, IGuild guild, ITextChannel channel,
TriviaOptions options, string quitCommand, IEmbedBuilderService eb)
{
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;
_cache = cache;
_questionPool = new TriviaQuestionPool(_cache);
_strings = strings;
_client = client;
_config = config;
_cs = cs;
_options = options;
_quitCommand = quitCommand;
_eb = eb;
public IGuild Guild { get; }
public ITextChannel Channel { get; }
Guild = guild;
Channel = channel;
}
private CancellationTokenSource _triviaCancelSource;
private string GetText(in LocStr key)
=> _strings.GetText(key, Channel.GuildId);
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;
private readonly IEmbedBuilderService _eb;
public TriviaGame(IBotStrings strings, DiscordSocketClient client, GamesConfig config,
IDataCache cache, ICurrencyService cs, IGuild guild, ITextChannel channel,
TriviaOptions options, string quitCommand, IEmbedBuilderService eb)
public async Task StartGame()
{
var showHowToQuit = false;
while (!ShouldStopGame)
{
_cache = cache;
_questionPool = new TriviaQuestionPool(_cache);
_strings = strings;
_client = client;
_config = config;
_cs = cs;
_options = options;
_quitCommand = quitCommand;
_eb = eb;
// reset the cancellation source
_triviaCancelSource = new CancellationTokenSource();
showHowToQuit = !showHowToQuit;
Guild = guild;
Channel = channel;
}
private string GetText(in LocStr key)
=> _strings.GetText(key, Channel.GuildId);
public async Task StartGame()
{
var showHowToQuit = false;
while (!ShouldStopGame)
// load question
CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.IsPokemon);
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
{
// reset the cancellation source
_triviaCancelSource = new CancellationTokenSource();
showHowToQuit = !showHowToQuit;
await Channel.SendErrorAsync(_eb, GetText(strs.trivia_game), GetText(strs.failed_loading_question)).ConfigureAwait(false);
return;
}
OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again
// load question
CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.IsPokemon);
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
{
await Channel.SendErrorAsync(_eb, GetText(strs.trivia_game), GetText(strs.failed_loading_question)).ConfigureAwait(false);
return;
}
OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again
IEmbedBuilder questionEmbed;
IUserMessage questionMessage;
try
{
questionEmbed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.AddField(GetText(strs.category), CurrentQuestion.Category)
.AddField(GetText(strs.question), CurrentQuestion.Question);
IEmbedBuilder questionEmbed;
IUserMessage questionMessage;
try
{
questionEmbed = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.AddField(GetText(strs.category), CurrentQuestion.Category)
.AddField(GetText(strs.question), CurrentQuestion.Question);
if (showHowToQuit)
questionEmbed.WithFooter(GetText(strs.trivia_quit(_quitCommand)));
if (showHowToQuit)
questionEmbed.WithFooter(GetText(strs.trivia_quit(_quitCommand)));
if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute))
questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl);
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(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 = _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).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);
questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false);
}
}
public async Task EnsureStopped()
{
ShouldStopGame = true;
await Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithAuthor("Trivia Game Ended")
.WithTitle("Final Results")
.WithDescription(GetLeaderboard())).ConfigureAwait(false);
}
public async Task StopGame()
{
var old = ShouldStopGame;
ShouldStopGame = true;
if (!old)
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound ||
ex.HttpCode == System.Net.HttpStatusCode.Forbidden ||
ex.HttpCode == System.Net.HttpStatusCode.BadRequest)
{
try
{
await Channel.SendConfirmAsync(_eb,
GetText(strs.trivia_game),
GetText(strs.trivia_stopping));
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia stopping message");
}
return;
}
}
private Task PotentialGuess(SocketMessage imsg)
{
var _ = Task.Run(async () =>
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
{
if (imsg.Author.IsBot)
return;
var umsg = imsg as SocketUserMessage;
var textChannel = umsg?.Channel as ITextChannel;
if (textChannel is 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;
//hint
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false);
if (!_options.NoHint)
try
{
var embedS = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_win(
guildUser.Mention,
Format.Bold(CurrentQuestion.Answer))));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl);
await Channel.EmbedAsync(embedS).ConfigureAwait(false);
await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(CurrentQuestion.GetHint()).Build())
.ConfigureAwait(false);
}
catch
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
// ignored
break;
}
var reward = _config.Trivia.CurrencyReward;
if (reward > 0)
await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false);
return;
}
var embed = _eb.Create().WithOkColor()
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 = _eb.Create().WithErrorColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_guess(guildUser.Mention, Format.Bold(CurrentQuestion.Answer))));
.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).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");
}
catch (Exception ex) { Log.Warning(ex.ToString()); }
});
return Task.CompletedTask;
}
public string GetLeaderboard()
{
if (Users.Count == 0)
return GetText(strs.no_results);
var sb = new StringBuilder();
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value))
{
sb.AppendLine(GetText(strs.trivia_points(Format.Bold(kvp.Key.ToString()), kvp.Value)).SnPl(kvp.Value));
}
return sb.ToString();
await Task.Delay(5000).ConfigureAwait(false);
}
}
public async Task EnsureStopped()
{
ShouldStopGame = true;
await Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithAuthor("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(_eb,
GetText(strs.trivia_game),
GetText(strs.trivia_stopping));
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia stopping message");
}
}
}
private Task PotentialGuess(SocketMessage imsg)
{
var _ = Task.Run(async () =>
{
try
{
if (imsg.Author.IsBot)
return;
var umsg = imsg as SocketUserMessage;
var textChannel = umsg?.Channel as ITextChannel;
if (textChannel is 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 = _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).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 = _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).ConfigureAwait(false);
}
catch (Exception ex) { Log.Warning(ex.ToString()); }
});
return Task.CompletedTask;
}
public string GetLeaderboard()
{
if (Users.Count == 0)
return GetText(strs.no_results);
var sb = new StringBuilder();
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value))
{
sb.AppendLine(GetText(strs.trivia_points(Format.Bold(kvp.Key.ToString()), kvp.Value)).SnPl(kvp.Value));
}
return sb.ToString();
}
}

View File

@@ -1,30 +1,29 @@
using CommandLine;
using NadekoBot.Common;
namespace NadekoBot.Modules.Games.Common.Trivia
namespace NadekoBot.Modules.Games.Common.Trivia;
public class TriviaOptions : INadekoCommandOptions
{
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()
{
[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;
if (WinRequirement < 0)
WinRequirement = 10;
if (QuestionTimer < 10 || QuestionTimer > 300)
QuestionTimer = 30;
if (Timeout < 0 || Timeout > 20)
Timeout = 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

@@ -1,110 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using NadekoBot.Extensions;
// THANKS @ShoMinamimoto for suggestions and coding help
namespace NadekoBot.Modules.Games.Common.Trivia
namespace NadekoBot.Modules.Games.Common.Trivia;
public class TriviaQuestion
{
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)
{
//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());
}
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

@@ -1,44 +1,42 @@
using NadekoBot.Common;
using NadekoBot.Services;
using NadekoBot.Extensions;
using System.Collections.Generic;
namespace NadekoBot.Modules.Games.Common.Trivia
namespace NadekoBot.Modules.Games.Common.Trivia;
public class TriviaQuestionPool
{
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)
{
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;
}
_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

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

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
@@ -10,180 +7,178 @@ using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Services;
using CommandLine;
using NadekoBot.Services;
using Serilog;
namespace NadekoBot.Modules.Games.Common
namespace NadekoBot.Modules.Games.Common;
public class TypingGame
{
public class TypingGame
public class Options : INadekoCommandOptions
{
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;
[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()
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;
private readonly IEmbedBuilderService _eb;
public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel,
string prefix, Options options, IEmbedBuilderService eb)
{
_games = games;
_client = client;
_prefix = prefix;
_options = options;
_eb = eb;
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(_eb, "Typing contest stopped.");
}
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(_eb,
$@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can.");
var time = _options.StartTime;
var msg = await Channel.SendMessageAsync($"Starting new typing contest in **{time}**...", options: new RequestOptions()
{
if (StartTime < 3 || StartTime > 30)
StartTime = 5;
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;
}
}
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;
private readonly IEmbedBuilderService _eb;
public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel,
string prefix, Options options, IEmbedBuilderService eb)
catch { }
finally
{
_games = games;
_client = client;
_prefix = prefix;
_options = options;
_eb = eb;
this.Channel = channel;
IsActive = false;
sw = new Stopwatch();
finishedUserIds = new List<ulong>();
await Stop().ConfigureAwait(false);
}
}
public async Task<bool> Stop()
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 () =>
{
if (!IsActive) return false;
_client.MessageReceived -= AnswerReceived;
finishedUserIds.Clear();
IsActive = false;
sw.Stop();
sw.Reset();
try
{
await Channel.SendConfirmAsync(_eb, "Typing contest stopped.");
if (imsg.Author.IsBot)
return;
var msg = imsg as SocketUserMessage;
if (msg is null)
return;
if (this.Channel is 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(_eb.Create().WithOkColor()
.WithTitle($"{msg.Author} finished the race!")
.AddField("Place", $"#{finishedUserIds.Count}", true)
.AddField("WPM", $"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*", true)
.AddField("Errors", distance.ToString(), true));
if (finishedUserIds.Count % 4 == 0)
{
await this.Channel.SendConfirmAsync(_eb,
$":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 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(_eb,
$@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can.");
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 is null)
return;
if (this.Channel is 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(_eb.Create().WithOkColor()
.WithTitle($"{msg.Author} finished the race!")
.AddField("Place", $"#{finishedUserIds.Count}", true)
.AddField("WPM", $"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*", true)
.AddField("Errors", distance.ToString(), true));
if (finishedUserIds.Count % 4 == 0)
{
await this.Channel.SendConfirmAsync(_eb,
$":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;
});
return Task.CompletedTask;
}
private static bool Judge(int errors, int textLength) => errors <= textLength / 25;
}

View File

@@ -6,159 +6,157 @@ using NadekoBot.Services;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Modules.Games.Services;
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games
namespace NadekoBot.Modules.Games;
/* more games
- Shiritori
- Simple RPG adventure
*/
public partial class Games : NadekoModule<GamesService>
{
/* more games
- Shiritori
- Simple RPG adventure
*/
public partial class Games : NadekoModule<GamesService>
private readonly IImageCache _images;
private readonly IHttpClientFactory _httpFactory;
private readonly Random _rng = new Random();
public Games(IDataCache data, IHttpClientFactory factory)
{
private readonly IImageCache _images;
private readonly IHttpClientFactory _httpFactory;
private readonly Random _rng = new Random();
_images = data.LocalImages;
_httpFactory = factory;
}
public Games(IDataCache data, IHttpClientFactory factory)
[NadekoCommand, Aliases]
public async Task Choose([Leftover] string list = null)
{
if (string.IsNullOrWhiteSpace(list))
return;
var listArr = list.Split(';');
if (listArr.Length < 2)
return;
var rng = new NadekoRandom();
await SendConfirmAsync("🤔", listArr[rng.Next(0, listArr.Length)]).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
public async Task EightBall([Leftover] string question = null)
{
if (string.IsNullOrWhiteSpace(question))
return;
var res = _service.GetEightballResponse(ctx.User.Id, question);
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription(ctx.User.ToString())
.AddField("❓ " + GetText(strs.question), question, false)
.AddField("🎱 " + GetText(strs._8ball), res, false));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task RateGirl([Leftover] IGuildUser usr)
{
var gr = _service.GirlRatings.GetOrAdd(usr.Id, GetGirl);
var originalStream = await gr.Stream;
if (originalStream is null)
{
_images = data.LocalImages;
_httpFactory = factory;
await ReplyErrorLocalizedAsync(strs.something_went_wrong).ConfigureAwait(false);
return;
}
[NadekoCommand, Aliases]
public async Task Choose([Leftover] string list = null)
using (var imgStream = new MemoryStream())
{
if (string.IsNullOrWhiteSpace(list))
return;
var listArr = list.Split(';');
if (listArr.Length < 2)
return;
var rng = new NadekoRandom();
await SendConfirmAsync("🤔", listArr[rng.Next(0, listArr.Length)]).ConfigureAwait(false);
lock (gr)
{
originalStream.Position = 0;
originalStream.CopyTo(imgStream);
}
imgStream.Position = 0;
await ctx.Channel.SendFileAsync(stream: imgStream,
filename: $"girl_{usr}.png",
text: Format.Bold($"{ctx.User.Mention} Girl Rating For {usr}"),
embed: _eb.Create()
.WithOkColor()
.AddField("Hot", gr.Hot.ToString("F2"), true)
.AddField("Crazy", gr.Crazy.ToString("F2"), true)
.AddField("Advice", gr.Advice, false)
.Build()).ConfigureAwait(false);
}
}
private double NextDouble(double x, double y)
{
return _rng.NextDouble() * (y - x) + x;
}
private GirlRating GetGirl(ulong uid)
{
var rng = new NadekoRandom();
var roll = rng.Next(1, 1001);
var ratings = _service.Ratings.GetAwaiter().GetResult();
double hot;
double crazy;
string advice;
if (roll < 500)
{
hot = NextDouble(0, 5);
crazy = NextDouble(4, 10);
advice = ratings.Nog;
}
else if (roll < 750)
{
hot = NextDouble(5, 8);
crazy = NextDouble(4, .6 * hot + 4);
advice = ratings.Fun;
}
else if (roll < 900)
{
hot = NextDouble(5, 10);
crazy = NextDouble(.61 * hot + 4, 10);
advice = ratings.Dan;
}
else if (roll < 951)
{
hot = NextDouble(8, 10);
crazy = NextDouble(7, .6 * hot + 4);
advice = ratings.Dat;
}
else if (roll < 990)
{
hot = NextDouble(8, 10);
crazy = NextDouble(5, 7);
advice = ratings.Wif;
}
else if (roll < 999)
{
hot = NextDouble(8, 10);
crazy = NextDouble(2, 3.99d);
advice = ratings.Tra;
}
else
{
hot = NextDouble(8, 10);
crazy = NextDouble(4, 5);
advice = ratings.Uni;
}
[NadekoCommand, Aliases]
public async Task EightBall([Leftover] string question = null)
{
if (string.IsNullOrWhiteSpace(question))
return;
return new GirlRating(_images, _httpFactory, crazy, hot, roll, advice);
}
var res = _service.GetEightballResponse(ctx.User.Id, question);
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription(ctx.User.ToString())
.AddField("❓ " + GetText(strs.question), question, false)
.AddField("🎱 " + GetText(strs._8ball), res, false));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task RateGirl([Leftover] IGuildUser usr)
{
var gr = _service.GirlRatings.GetOrAdd(usr.Id, GetGirl);
var originalStream = await gr.Stream;
if (originalStream is null)
{
await ReplyErrorLocalizedAsync(strs.something_went_wrong).ConfigureAwait(false);
return;
}
using (var imgStream = new MemoryStream())
{
lock (gr)
{
originalStream.Position = 0;
originalStream.CopyTo(imgStream);
}
imgStream.Position = 0;
await ctx.Channel.SendFileAsync(stream: imgStream,
filename: $"girl_{usr}.png",
text: Format.Bold($"{ctx.User.Mention} Girl Rating For {usr}"),
embed: _eb.Create()
.WithOkColor()
.AddField("Hot", gr.Hot.ToString("F2"), true)
.AddField("Crazy", gr.Crazy.ToString("F2"), true)
.AddField("Advice", gr.Advice, false)
.Build()).ConfigureAwait(false);
}
}
private double NextDouble(double x, double y)
{
return _rng.NextDouble() * (y - x) + x;
}
private GirlRating GetGirl(ulong uid)
{
var rng = new NadekoRandom();
var roll = rng.Next(1, 1001);
var ratings = _service.Ratings.GetAwaiter().GetResult();
double hot;
double crazy;
string advice;
if (roll < 500)
{
hot = NextDouble(0, 5);
crazy = NextDouble(4, 10);
advice = ratings.Nog;
}
else if (roll < 750)
{
hot = NextDouble(5, 8);
crazy = NextDouble(4, .6 * hot + 4);
advice = ratings.Fun;
}
else if (roll < 900)
{
hot = NextDouble(5, 10);
crazy = NextDouble(.61 * hot + 4, 10);
advice = ratings.Dan;
}
else if (roll < 951)
{
hot = NextDouble(8, 10);
crazy = NextDouble(7, .6 * hot + 4);
advice = ratings.Dat;
}
else if (roll < 990)
{
hot = NextDouble(8, 10);
crazy = NextDouble(5, 7);
advice = ratings.Wif;
}
else if (roll < 999)
{
hot = NextDouble(8, 10);
crazy = NextDouble(2, 3.99d);
advice = ratings.Tra;
}
else
{
hot = NextDouble(8, 10);
crazy = NextDouble(4, 5);
advice = ratings.Uni;
}
return new GirlRating(_images, _httpFactory, crazy, hot, roll, advice);
}
[NadekoCommand, Aliases]
public async Task Linux(string guhnoo, string loonix)
{
await SendConfirmAsync(
$@"I'd just like to interject for moment. What you're refering to as {loonix}, is in fact, {guhnoo}/{loonix}, or as I've recently taken to calling it, {guhnoo} plus {loonix}. {loonix} is not an operating system unto itself, but rather another free component of a fully functioning {guhnoo} system made useful by the {guhnoo} corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX.
[NadekoCommand, Aliases]
public async Task Linux(string guhnoo, string loonix)
{
await SendConfirmAsync(
$@"I'd just like to interject for moment. What you're refering to as {loonix}, is in fact, {guhnoo}/{loonix}, or as I've recently taken to calling it, {guhnoo} plus {loonix}. {loonix} is not an operating system unto itself, but rather another free component of a fully functioning {guhnoo} system made useful by the {guhnoo} corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX.
Many computer users run a modified version of the {guhnoo} system every day, without realizing it. Through a peculiar turn of events, the version of {guhnoo} which is widely used today is often called {loonix}, and many of its users are not aware that it is basically the {guhnoo} system, developed by the {guhnoo} Project.
There really is a {loonix}, and these people are using it, but it is just a part of the system they use. {loonix} is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. {loonix} is normally used in combination with the {guhnoo} operating system: the whole system is basically {guhnoo} with {loonix} added, or {guhnoo}/{loonix}. All the so-called {loonix} distributions are really distributions of {guhnoo}/{loonix}."
).ConfigureAwait(false);
}
).ConfigureAwait(false);
}
}
}

View File

@@ -7,82 +7,81 @@ using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Hangman;
using NadekoBot.Services;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class HangmanCommands : NadekoSubmodule<IHangmanService>
{
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Hangmanlist()
{
await SendConfirmAsync(
GetText(strs.hangman_types(Prefix)),
_service.GetHangmanTypes().JoinWith('\n'));
}
namespace NadekoBot.Modules.Games;
private static string Draw(HangmanGame.State state)
{
return $@". ┌─────┐
public partial class Games
{
[Group]
public class HangmanCommands : NadekoSubmodule<IHangmanService>
{
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Hangmanlist()
{
await SendConfirmAsync(
GetText(strs.hangman_types(Prefix)),
_service.GetHangmanTypes().JoinWith('\n'));
}
private static string Draw(HangmanGame.State state)
{
return $@". ┌─────┐
.┃...............┋
.┃...............┋
.┃{(state.Errors > 0 ? ".............😲" : "")}
.{(state.Errors > 1 ? "............./" : "")} {(state.Errors > 2 ? "|" : "")} {(state.Errors > 3 ? "\\" : "")}
.{(state.Errors > 4 ? "............../" : "")} {(state.Errors > 5 ? "\\" : "")}
/-\";
}
}
public static IEmbedBuilder GetEmbed(IEmbedBuilderService eb, HangmanGame.State state)
{
if (state.Phase == HangmanGame.Phase.Running)
return eb.Create()
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.JoinWith(' '));
public static IEmbedBuilder GetEmbed(IEmbedBuilderService eb, HangmanGame.State state)
{
if (state.Phase == HangmanGame.Phase.Running)
return eb.Create()
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.JoinWith(' '));
if (state.Phase == HangmanGame.Phase.Ended && state.Failed)
return eb.Create()
.WithErrorColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.JoinWith(' '));
else
{
return eb.Create()
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.JoinWith(' '));
}
if (state.Phase == HangmanGame.Phase.Ended && state.Failed)
return eb.Create()
.WithErrorColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.JoinWith(' '));
else
{
return eb.Create()
.WithOkColor()
.AddField("Hangman", Draw(state))
.AddField("Guess", Format.Code(state.Word))
.WithFooter(state.missedLetters.JoinWith(' '));
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Hangman([Leftover] string? type = null)
{
if (!_service.StartHangman(ctx.Channel.Id, type, out var hangman))
{
await ReplyErrorLocalizedAsync(strs.hangman_running);
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Hangman([Leftover] string? type = null)
{
if (!_service.StartHangman(ctx.Channel.Id, type, out var hangman))
{
await ReplyErrorLocalizedAsync(strs.hangman_running);
return;
}
var eb = GetEmbed(_eb, hangman);
eb.WithDescription(GetText(strs.hangman_game_started));
await ctx.Channel.EmbedAsync(eb);
}
var eb = GetEmbed(_eb, hangman);
eb.WithDescription(GetText(strs.hangman_game_started));
await ctx.Channel.EmbedAsync(eb);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task HangmanStop()
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task HangmanStop()
{
if (await _service.StopHangman(ctx.Channel.Id))
{
if (await _service.StopHangman(ctx.Channel.Id))
{
await ReplyConfirmLocalizedAsync(strs.hangman_stopped).ConfigureAwait(false);
}
await ReplyConfirmLocalizedAsync(strs.hangman_stopped).ConfigureAwait(false);
}
}
}
}
}

View File

@@ -4,123 +4,121 @@ using Discord.WebSocket;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Common.Nunchi;
using NadekoBot.Modules.Games.Services;
using System.Linq;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games
namespace NadekoBot.Modules.Games;
public partial class Games
{
public partial class Games
[Group]
public class NunchiCommands : NadekoSubmodule<GamesService>
{
[Group]
public class NunchiCommands : NadekoSubmodule<GamesService>
private readonly DiscordSocketClient _client;
public NunchiCommands(DiscordSocketClient client)
{
private readonly DiscordSocketClient _client;
_client = client;
}
public NunchiCommands(DiscordSocketClient client)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Nunchi()
{
var newNunchi = new NunchiGame(ctx.User.Id, ctx.User.ToString());
NunchiGame nunchi;
//if a game was already active
if ((nunchi = _service.NunchiGames.GetOrAdd(ctx.Guild.Id, newNunchi)) != newNunchi)
{
_client = client;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Nunchi()
{
var newNunchi = new NunchiGame(ctx.User.Id, ctx.User.ToString());
NunchiGame nunchi;
//if a game was already active
if ((nunchi = _service.NunchiGames.GetOrAdd(ctx.Guild.Id, newNunchi)) != newNunchi)
// join it
if (!await nunchi.Join(ctx.User.Id, ctx.User.ToString()).ConfigureAwait(false))
{
// join it
if (!await nunchi.Join(ctx.User.Id, ctx.User.ToString()).ConfigureAwait(false))
{
// if you failed joining, that means game is running or just ended
// await ReplyErrorLocalized("nunchi_already_started").ConfigureAwait(false);
return;
}
await ReplyErrorLocalizedAsync(strs.nunchi_joined(nunchi.ParticipantCount));
// if you failed joining, that means game is running or just ended
// await ReplyErrorLocalized("nunchi_already_started").ConfigureAwait(false);
return;
}
await ReplyErrorLocalizedAsync(strs.nunchi_joined(nunchi.ParticipantCount));
return;
}
try { await ConfirmLocalizedAsync(strs.nunchi_created); } catch { }
nunchi.OnGameEnded += Nunchi_OnGameEnded;
//nunchi.OnGameStarted += Nunchi_OnGameStarted;
nunchi.OnRoundEnded += Nunchi_OnRoundEnded;
nunchi.OnUserGuessed += Nunchi_OnUserGuessed;
nunchi.OnRoundStarted += Nunchi_OnRoundStarted;
_client.MessageReceived += _client_MessageReceived;
try { await ConfirmLocalizedAsync(strs.nunchi_created); } catch { }
nunchi.OnGameEnded += Nunchi_OnGameEnded;
//nunchi.OnGameStarted += Nunchi_OnGameStarted;
nunchi.OnRoundEnded += Nunchi_OnRoundEnded;
nunchi.OnUserGuessed += Nunchi_OnUserGuessed;
nunchi.OnRoundStarted += Nunchi_OnRoundStarted;
_client.MessageReceived += _client_MessageReceived;
var success = await nunchi.Initialize().ConfigureAwait(false);
if (!success)
{
if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game))
game.Dispose();
await ConfirmLocalizedAsync(strs.nunchi_failed_to_start).ConfigureAwait(false);
}
var success = await nunchi.Initialize().ConfigureAwait(false);
if (!success)
{
if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game))
game.Dispose();
await ConfirmLocalizedAsync(strs.nunchi_failed_to_start).ConfigureAwait(false);
}
Task _client_MessageReceived(SocketMessage arg)
Task _client_MessageReceived(SocketMessage arg)
{
var _ = Task.Run(async () =>
{
var _ = Task.Run(async () =>
if (arg.Channel.Id != ctx.Channel.Id)
return;
if (!int.TryParse(arg.Content, out var number))
return;
try
{
if (arg.Channel.Id != ctx.Channel.Id)
return;
if (!int.TryParse(arg.Content, out var number))
return;
try
{
await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number).ConfigureAwait(false);
}
catch
{
}
});
return Task.CompletedTask;
}
Task Nunchi_OnGameEnded(NunchiGame arg1, string arg2)
{
if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game))
{
_client.MessageReceived -= _client_MessageReceived;
game.Dispose();
await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number).ConfigureAwait(false);
}
catch
{
}
});
return Task.CompletedTask;
}
if (arg2 is null)
return ConfirmLocalizedAsync(strs.nunchi_ended_no_winner);
else
return ConfirmLocalizedAsync(strs.nunchi_ended(Format.Bold(arg2)));
Task Nunchi_OnGameEnded(NunchiGame arg1, string arg2)
{
if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game))
{
_client.MessageReceived -= _client_MessageReceived;
game.Dispose();
}
}
private Task Nunchi_OnRoundStarted(NunchiGame arg, int cur)
{
return ConfirmLocalizedAsync(strs.nunchi_round_started(
Format.Bold(arg.ParticipantCount.ToString()),
Format.Bold(cur.ToString())));
}
private Task Nunchi_OnUserGuessed(NunchiGame arg)
{
return ConfirmLocalizedAsync(strs.nunchi_next_number(Format.Bold(arg.CurrentNumber.ToString())));
}
private Task Nunchi_OnRoundEnded(NunchiGame arg1, (ulong Id, string Name)? arg2)
{
if(arg2.HasValue)
return ConfirmLocalizedAsync(strs.nunchi_round_ended(Format.Bold(arg2.Value.Name)));
if (arg2 is null)
return ConfirmLocalizedAsync(strs.nunchi_ended_no_winner);
else
return ConfirmLocalizedAsync(strs.nunchi_round_ended_boot(
Format.Bold("\n" + string.Join("\n, ", arg1.Participants.Select(x => x.Name))))); // this won't work if there are too many users
return ConfirmLocalizedAsync(strs.nunchi_ended(Format.Bold(arg2)));
}
}
private Task Nunchi_OnGameStarted(NunchiGame arg)
{
return ConfirmLocalizedAsync(strs.nunchi_started(Format.Bold(arg.ParticipantCount.ToString())));
}
private Task Nunchi_OnRoundStarted(NunchiGame arg, int cur)
{
return ConfirmLocalizedAsync(strs.nunchi_round_started(
Format.Bold(arg.ParticipantCount.ToString()),
Format.Bold(cur.ToString())));
}
private Task Nunchi_OnUserGuessed(NunchiGame arg)
{
return ConfirmLocalizedAsync(strs.nunchi_next_number(Format.Bold(arg.CurrentNumber.ToString())));
}
private Task Nunchi_OnRoundEnded(NunchiGame arg1, (ulong Id, string Name)? arg2)
{
if(arg2.HasValue)
return ConfirmLocalizedAsync(strs.nunchi_round_ended(Format.Bold(arg2.Value.Name)));
else
return ConfirmLocalizedAsync(strs.nunchi_round_ended_boot(
Format.Bold("\n" + string.Join("\n, ", arg1.Participants.Select(x => x.Name))))); // this won't work if there are too many users
}
private Task Nunchi_OnGameStarted(NunchiGame arg)
{
return ConfirmLocalizedAsync(strs.nunchi_started(Format.Bold(arg.ParticipantCount.ToString())));
}
}
}

View File

@@ -7,118 +7,116 @@ using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Services;
using NadekoBot.Services.Database.Models;
using System.Text;
using System.Linq;
namespace NadekoBot.Modules.Games
namespace NadekoBot.Modules.Games;
public partial class Games
{
public partial class Games
[Group]
public class PollCommands : NadekoSubmodule<PollService>
{
[Group]
public class PollCommands : NadekoSubmodule<PollService>
private readonly DiscordSocketClient _client;
public PollCommands(DiscordSocketClient client)
{
private readonly DiscordSocketClient _client;
_client = client;
}
public PollCommands(DiscordSocketClient client)
[NadekoCommand, Aliases]
[UserPerm(GuildPerm.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task Poll([Leftover] string arg)
{
if (string.IsNullOrWhiteSpace(arg))
return;
var poll = _service.CreatePoll(ctx.Guild.Id,
ctx.Channel.Id, arg);
if(poll is null)
{
_client = client;
await ReplyErrorLocalizedAsync(strs.poll_invalid_input).ConfigureAwait(false);
return;
}
[NadekoCommand, Aliases]
[UserPerm(GuildPerm.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task Poll([Leftover] string arg)
if (_service.StartPoll(poll))
{
if (string.IsNullOrWhiteSpace(arg))
return;
var poll = _service.CreatePoll(ctx.Guild.Id,
ctx.Channel.Id, arg);
if(poll is null)
{
await ReplyErrorLocalizedAsync(strs.poll_invalid_input).ConfigureAwait(false);
return;
}
if (_service.StartPoll(poll))
{
await ctx.Channel
.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.poll_created(ctx.User.ToString())))
.WithDescription(
Format.Bold(poll.Question) + "\n\n" +
await ctx.Channel
.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.poll_created(ctx.User.ToString())))
.WithDescription(
Format.Bold(poll.Question) + "\n\n" +
string.Join("\n", poll.Answers
.Select(x => $"`{x.Index + 1}.` {Format.Bold(x.Text)}"))))
.ConfigureAwait(false);
}
else
{
await ReplyErrorLocalizedAsync(strs.poll_already_running).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[UserPerm(GuildPerm.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task PollStats()
{
if (!_service.ActivePolls.TryGetValue(ctx.Guild.Id, out var pr))
return;
await ctx.Channel.EmbedAsync(GetStats(pr.Poll, GetText(strs.current_poll_results))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[UserPerm(GuildPerm.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task Pollend()
{
var channel = (ITextChannel)ctx.Channel;
Poll p;
if ((p = _service.StopPoll(ctx.Guild.Id)) is null)
return;
var embed = GetStats(p, GetText(strs.poll_closed));
await ctx.Channel.EmbedAsync(embed)
.ConfigureAwait(false);
}
public IEmbedBuilder GetStats(Poll poll, string title)
else
{
var results = poll.Votes.GroupBy(kvp => kvp.VoteIndex)
.ToDictionary(x => x.Key, x => x.Sum(kvp => 1));
var totalVotesCast = results.Sum(x => x.Value);
var eb = _eb.Create().WithTitle(title);
var sb = new StringBuilder()
.AppendLine(Format.Bold(poll.Question))
.AppendLine();
var stats = poll.Answers
.Select(x =>
{
results.TryGetValue(x.Index, out var votes);
return (x.Index, votes, x.Text);
})
.OrderByDescending(x => x.votes)
.ToArray();
for (int i = 0; i < stats.Length; i++)
{
var (Index, votes, Text) = stats[i];
sb.AppendLine(GetText(strs.poll_result(
Index + 1,
Format.Bold(Text),
Format.Bold(votes.ToString()))));
}
return eb.WithDescription(sb.ToString())
.WithFooter(GetText(strs.x_votes_cast(totalVotesCast)))
.WithOkColor();
await ReplyErrorLocalizedAsync(strs.poll_already_running).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[UserPerm(GuildPerm.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task PollStats()
{
if (!_service.ActivePolls.TryGetValue(ctx.Guild.Id, out var pr))
return;
await ctx.Channel.EmbedAsync(GetStats(pr.Poll, GetText(strs.current_poll_results))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[UserPerm(GuildPerm.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task Pollend()
{
var channel = (ITextChannel)ctx.Channel;
Poll p;
if ((p = _service.StopPoll(ctx.Guild.Id)) is null)
return;
var embed = GetStats(p, GetText(strs.poll_closed));
await ctx.Channel.EmbedAsync(embed)
.ConfigureAwait(false);
}
public IEmbedBuilder GetStats(Poll poll, string title)
{
var results = poll.Votes.GroupBy(kvp => kvp.VoteIndex)
.ToDictionary(x => x.Key, x => x.Sum(kvp => 1));
var totalVotesCast = results.Sum(x => x.Value);
var eb = _eb.Create().WithTitle(title);
var sb = new StringBuilder()
.AppendLine(Format.Bold(poll.Question))
.AppendLine();
var stats = poll.Answers
.Select(x =>
{
results.TryGetValue(x.Index, out var votes);
return (x.Index, votes, x.Text);
})
.OrderByDescending(x => x.votes)
.ToArray();
for (int i = 0; i < stats.Length; i++)
{
var (Index, votes, Text) = stats[i];
sb.AppendLine(GetText(strs.poll_result(
Index + 1,
Format.Bold(Text),
Format.Bold(votes.ToString()))));
}
return eb.WithDescription(sb.ToString())
.WithFooter(GetText(strs.x_votes_cast(totalVotesCast)))
.WithOkColor();
}
}
}

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
@@ -11,142 +9,140 @@ using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Services;
using NadekoBot.Modules.Games.Common.ChatterBot;
using System.Net.Http;
using Serilog;
namespace NadekoBot.Modules.Games.Services
namespace NadekoBot.Modules.Games.Services;
public class ChatterBotService : IEarlyBehavior
{
public class ChatterBotService : IEarlyBehavior
private readonly DiscordSocketClient _client;
private readonly PermissionService _perms;
private readonly CommandHandler _cmd;
private readonly IBotStrings _strings;
private readonly IBotCredentials _creds;
private readonly IEmbedBuilderService _eb;
private readonly IHttpClientFactory _httpFactory;
public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
public int Priority => 1;
public ChatterBotService(DiscordSocketClient client, PermissionService perms,
Bot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory,
IBotCredentials creds, IEmbedBuilderService eb)
{
private readonly DiscordSocketClient _client;
private readonly PermissionService _perms;
private readonly CommandHandler _cmd;
private readonly IBotStrings _strings;
private readonly IBotCredentials _creds;
private readonly IEmbedBuilderService _eb;
private readonly IHttpClientFactory _httpFactory;
_client = client;
_perms = perms;
_cmd = cmd;
_strings = strings;
_creds = creds;
_eb = eb;
_httpFactory = factory;
public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
ChatterBotGuilds = new ConcurrentDictionary<ulong, Lazy<IChatterBotSession>>(
bot.AllGuildConfigs
.Where(gc => gc.CleverbotEnabled)
.ToDictionary(gc => gc.GuildId, gc => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
}
public int Priority => 1;
public IChatterBotSession CreateSession()
{
if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory);
else
return new CleverbotIOSession("GAh3wUfzDCpDpdpT", "RStKgqn7tcO9blbrv4KbXM8NDlb7H37C", _httpFactory);
}
public ChatterBotService(DiscordSocketClient client, PermissionService perms,
Bot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory,
IBotCredentials creds, IEmbedBuilderService eb)
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
{
var channel = msg.Channel as ITextChannel;
cleverbot = null;
if (channel is null)
return null;
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out Lazy<IChatterBotSession> lazyCleverbot))
return null;
cleverbot = lazyCleverbot.Value;
var nadekoId = _client.CurrentUser.Id;
var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{nadekoId}> ";
string message;
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
{
_client = client;
_perms = perms;
_cmd = cmd;
_strings = strings;
_creds = creds;
_eb = eb;
_httpFactory = factory;
ChatterBotGuilds = new ConcurrentDictionary<ulong, Lazy<IChatterBotSession>>(
bot.AllGuildConfigs
.Where(gc => gc.CleverbotEnabled)
.ToDictionary(gc => gc.GuildId, gc => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
message = msg.Content.Substring(normalMention.Length).Trim();
}
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
{
message = msg.Content.Substring(nickMention.Length).Trim();
}
else
{
return null;
}
public IChatterBotSession CreateSession()
return message;
}
public async Task<bool> TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message)
{
await channel.TriggerTypingAsync().ConfigureAwait(false);
var response = await cleverbot.Think(message).ConfigureAwait(false);
try
{
if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory);
else
return new CleverbotIOSession("GAh3wUfzDCpDpdpT", "RStKgqn7tcO9blbrv4KbXM8NDlb7H37C", _httpFactory);
await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)).ConfigureAwait(false);
}
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
catch
{
var channel = msg.Channel as ITextChannel;
cleverbot = null;
if (channel is null)
return null;
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out Lazy<IChatterBotSession> lazyCleverbot))
return null;
cleverbot = lazyCleverbot.Value;
var nadekoId = _client.CurrentUser.Id;
var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{nadekoId}> ";
string message;
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
{
message = msg.Content.Substring(normalMention.Length).Trim();
}
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
{
message = msg.Content.Substring(nickMention.Length).Trim();
}
else
{
return null;
}
return message;
await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)).ConfigureAwait(false); // try twice :\
}
return true;
}
public async Task<bool> TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message)
public async Task<bool> RunBehavior(IGuild guild, IUserMessage usrMsg)
{
if (!(guild is SocketGuild sg))
return false;
try
{
await channel.TriggerTypingAsync().ConfigureAwait(false);
var response = await cleverbot.Think(message).ConfigureAwait(false);
try
{
await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)).ConfigureAwait(false);
}
catch
{
await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)).ConfigureAwait(false); // try twice :\
}
return true;
}
public async Task<bool> RunBehavior(IGuild guild, IUserMessage usrMsg)
{
if (!(guild is SocketGuild sg))
var message = PrepareMessage(usrMsg, out IChatterBotSession cbs);
if (message is null || cbs is null)
return false;
try
{
var message = PrepareMessage(usrMsg, out IChatterBotSession cbs);
if (message is null || cbs is null)
return false;
var pc = _perms.GetCacheFor(guild.Id);
if (!pc.Permissions.CheckPermissions(usrMsg,
var pc = _perms.GetCacheFor(guild.Id);
if (!pc.Permissions.CheckPermissions(usrMsg,
"cleverbot",
"Games".ToLowerInvariant(),
out int index))
{
if (pc.Verbose)
{
if (pc.Verbose)
{
var returnMsg = _strings.GetText(strs.perm_prevent(index + 1,
Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))));
var returnMsg = _strings.GetText(strs.perm_prevent(index + 1,
Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))));
try { await usrMsg.Channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); } catch { }
Log.Information(returnMsg);
}
return true;
try { await usrMsg.Channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); } catch { }
Log.Information(returnMsg);
}
return true;
}
var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message).ConfigureAwait(false);
if (cleverbotExecuted)
{
Log.Information($@"CleverBot Executed
var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message).ConfigureAwait(false);
if (cleverbotExecuted)
{
Log.Information($@"CleverBot Executed
Server: {guild.Name} [{guild.Id}]
Channel: {usrMsg.Channel?.Name} [{usrMsg.Channel?.Id}]
UserId: {usrMsg.Author} [{usrMsg.Author.Id}]
Message: {usrMsg.Content}");
return true;
}
return true;
}
catch (Exception ex)
{
Log.Warning(ex,"Error in cleverbot");
}
return false;
}
catch (Exception ex)
{
Log.Warning(ex,"Error in cleverbot");
}
return false;
}
}
}

View File

@@ -3,40 +3,39 @@ using NadekoBot.Common.Configs;
using NadekoBot.Services;
using NadekoBot.Modules.Games.Common;
namespace NadekoBot.Modules.Games.Services
{
public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
{
public override string Name { get; } = "games";
private const string FilePath = "data/games.yml";
private static TypedKey<GamesConfig> changeKey = new TypedKey<GamesConfig>("config.games.updated");
namespace NadekoBot.Modules.Games.Services;
public GamesConfigService(IConfigSeria serializer, IPubSub pubSub)
: base(FilePath, serializer, pubSub, changeKey)
{
AddParsedProp("trivia.min_win_req", gs => gs.Trivia.MinimumWinReq, int.TryParse,
ConfigPrinters.ToString, val => val > 0);
AddParsedProp("trivia.currency_reward", gs => gs.Trivia.CurrencyReward, long.TryParse,
ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("hangman.currency_reward", gs => gs.Hangman.CurrencyReward, long.TryParse,
ConfigPrinters.ToString, val => val >= 0);
public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
{
public override string Name { get; } = "games";
private const string FilePath = "data/games.yml";
private static TypedKey<GamesConfig> changeKey = new TypedKey<GamesConfig>("config.games.updated");
public GamesConfigService(IConfigSeria serializer, IPubSub pubSub)
: base(FilePath, serializer, pubSub, changeKey)
{
AddParsedProp("trivia.min_win_req", gs => gs.Trivia.MinimumWinReq, int.TryParse,
ConfigPrinters.ToString, val => val > 0);
AddParsedProp("trivia.currency_reward", gs => gs.Trivia.CurrencyReward, long.TryParse,
ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("hangman.currency_reward", gs => gs.Hangman.CurrencyReward, long.TryParse,
ConfigPrinters.ToString, val => val >= 0);
Migrate();
}
Migrate();
}
private void Migrate()
private void Migrate()
{
if (_data.Version < 1)
{
if (_data.Version < 1)
ModifyConfig(c =>
{
ModifyConfig(c =>
c.Version = 1;
c.Hangman = new HangmanConfig()
{
c.Version = 1;
c.Hangman = new HangmanConfig()
{
CurrencyReward = 0
};
});
}
CurrencyReward = 0
};
});
}
}
}

View File

@@ -7,127 +7,123 @@ using NadekoBot.Modules.Games.Common.Acrophobia;
using NadekoBot.Modules.Games.Common.Nunchi;
using NadekoBot.Modules.Games.Common.Trivia;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Serilog;
namespace NadekoBot.Modules.Games.Services
namespace NadekoBot.Modules.Games.Services;
public class GamesService : INService
{
public class GamesService : INService
private readonly GamesConfigService _gamesConfig;
public ConcurrentDictionary<ulong, GirlRating> GirlRatings { get; } = new ConcurrentDictionary<ulong, GirlRating>();
public IReadOnlyList<string> EightBallResponses => _gamesConfig.Data.EightBallResponses;
private readonly Timer _t;
private readonly IHttpClientFactory _httpFactory;
private readonly IMemoryCache _8BallCache;
private readonly Random _rng;
private const string TypingArticlesPath = "data/typing_articles3.json";
public List<TypingArticle> TypingArticles { get; } = new List<TypingArticle>();
//channelId, game
public ConcurrentDictionary<ulong, AcrophobiaGame> AcrophobiaGames { get; } = new ConcurrentDictionary<ulong, AcrophobiaGame>();
public ConcurrentDictionary<ulong, TriviaGame> RunningTrivias { get; } = new ConcurrentDictionary<ulong, TriviaGame>();
public Dictionary<ulong, TicTacToe> TicTacToeGames { get; } = new Dictionary<ulong, TicTacToe>();
public ConcurrentDictionary<ulong, TypingGame> RunningContests { get; } = new ConcurrentDictionary<ulong, TypingGame>();
public ConcurrentDictionary<ulong, NunchiGame> NunchiGames { get; } = new ConcurrentDictionary<ulong, NunchiGame>();
public AsyncLazy<RatingTexts> Ratings { get; }
public class RatingTexts
{
private readonly GamesConfigService _gamesConfig;
public string Nog { get; set; }
public string Tra { get; set; }
public string Fun { get; set; }
public string Uni { get; set; }
public string Wif { get; set; }
public string Dat { get; set; }
public string Dan { get; set; }
}
public ConcurrentDictionary<ulong, GirlRating> GirlRatings { get; } = new ConcurrentDictionary<ulong, GirlRating>();
public IReadOnlyList<string> EightBallResponses => _gamesConfig.Data.EightBallResponses;
private readonly Timer _t;
private readonly IHttpClientFactory _httpFactory;
private readonly IMemoryCache _8BallCache;
private readonly Random _rng;
private const string TypingArticlesPath = "data/typing_articles3.json";
public List<TypingArticle> TypingArticles { get; } = new List<TypingArticle>();
//channelId, game
public ConcurrentDictionary<ulong, AcrophobiaGame> AcrophobiaGames { get; } = new ConcurrentDictionary<ulong, AcrophobiaGame>();
public ConcurrentDictionary<ulong, TriviaGame> RunningTrivias { get; } = new ConcurrentDictionary<ulong, TriviaGame>();
public Dictionary<ulong, TicTacToe> TicTacToeGames { get; } = new Dictionary<ulong, TicTacToe>();
public ConcurrentDictionary<ulong, TypingGame> RunningContests { get; } = new ConcurrentDictionary<ulong, TypingGame>();
public ConcurrentDictionary<ulong, NunchiGame> NunchiGames { get; } = new ConcurrentDictionary<ulong, NunchiGame>();
public AsyncLazy<RatingTexts> Ratings { get; }
public class RatingTexts
public GamesService(GamesConfigService gamesConfig, IHttpClientFactory httpFactory)
{
_gamesConfig = gamesConfig;
_httpFactory = httpFactory;
_8BallCache = new MemoryCache(new MemoryCacheOptions()
{
public string Nog { get; set; }
public string Tra { get; set; }
public string Fun { get; set; }
public string Uni { get; set; }
public string Wif { get; set; }
public string Dat { get; set; }
public string Dan { get; set; }
SizeLimit = 500_000
});
Ratings = new AsyncLazy<RatingTexts>(GetRatingTexts);
_rng = new NadekoRandom();
//girl ratings
_t = new Timer((_) =>
{
GirlRatings.Clear();
}, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1));
try
{
TypingArticles = JsonConvert.DeserializeObject<List<TypingArticle>>(File.ReadAllText(TypingArticlesPath));
}
public GamesService(GamesConfigService gamesConfig, IHttpClientFactory httpFactory)
catch (Exception ex)
{
_gamesConfig = gamesConfig;
_httpFactory = httpFactory;
_8BallCache = new MemoryCache(new MemoryCacheOptions()
{
SizeLimit = 500_000
});
Ratings = new AsyncLazy<RatingTexts>(GetRatingTexts);
_rng = new NadekoRandom();
//girl ratings
_t = new Timer((_) =>
{
GirlRatings.Clear();
}, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1));
try
{
TypingArticles = JsonConvert.DeserializeObject<List<TypingArticle>>(File.ReadAllText(TypingArticlesPath));
}
catch (Exception ex)
{
Log.Warning("Error while loading typing articles {0}", ex.ToString());
TypingArticles = new List<TypingArticle>();
}
}
private async Task<RatingTexts> GetRatingTexts()
{
using (var http = _httpFactory.CreateClient())
{
var text = await http.GetStringAsync("https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/rates.json");
return JsonConvert.DeserializeObject<RatingTexts>(text);
}
}
public void AddTypingArticle(IUser user, string text)
{
TypingArticles.Add(new TypingArticle
{
Source = user.ToString(),
Extra = $"Text added on {DateTime.UtcNow} by {user}.",
Text = text.SanitizeMentions(true),
});
File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(TypingArticles));
}
public string GetEightballResponse(ulong userId, string question)
{
return _8BallCache.GetOrCreate($"8ball:{userId}:{question}", e =>
{
e.Size = question.Length;
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
return EightBallResponses[_rng.Next(0, EightBallResponses.Count)];;
});
}
public TypingArticle RemoveTypingArticle(int index)
{
var articles = TypingArticles;
if (index < 0 || index >= articles.Count)
return null;
var removed = articles[index];
TypingArticles.RemoveAt(index);
File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(articles));
return removed;
Log.Warning("Error while loading typing articles {0}", ex.ToString());
TypingArticles = new List<TypingArticle>();
}
}
}
private async Task<RatingTexts> GetRatingTexts()
{
using (var http = _httpFactory.CreateClient())
{
var text = await http.GetStringAsync("https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/rates.json");
return JsonConvert.DeserializeObject<RatingTexts>(text);
}
}
public void AddTypingArticle(IUser user, string text)
{
TypingArticles.Add(new TypingArticle
{
Source = user.ToString(),
Extra = $"Text added on {DateTime.UtcNow} by {user}.",
Text = text.SanitizeMentions(true),
});
File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(TypingArticles));
}
public string GetEightballResponse(ulong userId, string question)
{
return _8BallCache.GetOrCreate($"8ball:{userId}:{question}", e =>
{
e.Size = question.Length;
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
return EightBallResponses[_rng.Next(0, EightBallResponses.Count)];;
});
}
public TypingArticle RemoveTypingArticle(int index)
{
var articles = TypingArticles;
if (index < 0 || index >= articles.Count)
return null;
var removed = articles[index];
TypingArticles.RemoveAt(index);
File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(articles));
return removed;
}
}

View File

@@ -1,140 +1,135 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using Discord;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Db.Models;
using NadekoBot.Common.Collections;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using NadekoBot.Extensions;
using Serilog;
namespace NadekoBot.Modules.Games.Services
namespace NadekoBot.Modules.Games.Services;
public class PollService : IEarlyBehavior
{
public class PollService : IEarlyBehavior
public ConcurrentDictionary<ulong, PollRunner> ActivePolls { get; } = new ConcurrentDictionary<ulong, PollRunner>();
public int Priority => 5;
private readonly DbService _db;
private readonly IBotStrings _strs;
private readonly IEmbedBuilderService _eb;
public PollService(DbService db, IBotStrings strs, IEmbedBuilderService eb)
{
public ConcurrentDictionary<ulong, PollRunner> ActivePolls { get; } = new ConcurrentDictionary<ulong, PollRunner>();
_db = db;
_strs = strs;
_eb = eb;
public int Priority => 5;
private readonly DbService _db;
private readonly IBotStrings _strs;
private readonly IEmbedBuilderService _eb;
public PollService(DbService db, IBotStrings strs, IEmbedBuilderService eb)
using (var uow = db.GetDbContext())
{
_db = db;
_strs = strs;
_eb = eb;
using (var uow = db.GetDbContext())
{
ActivePolls = uow.Poll.GetAllPolls()
.ToDictionary(x => x.GuildId, x =>
{
var pr = new PollRunner(db, x);
pr.OnVoted += Pr_OnVoted;
return pr;
})
.ToConcurrent();
}
}
public Poll CreatePoll(ulong guildId, ulong channelId, string input)
{
if (string.IsNullOrWhiteSpace(input) || !input.Contains(";"))
return null;
var data = input.Split(';');
if (data.Length < 3)
return null;
var col = new IndexedCollection<PollAnswer>(data.Skip(1)
.Select(x => new PollAnswer() { Text = x }));
return new Poll()
{
Answers = col,
Question = data[0],
ChannelId = channelId,
GuildId = guildId,
Votes = new System.Collections.Generic.HashSet<PollVote>()
};
}
public bool StartPoll(Poll p)
{
var pr = new PollRunner(_db, p);
if (ActivePolls.TryAdd(p.GuildId, pr))
{
using (var uow = _db.GetDbContext())
ActivePolls = uow.Poll.GetAllPolls()
.ToDictionary(x => x.GuildId, x =>
{
uow.Poll.Add(p);
uow.SaveChanges();
}
pr.OnVoted += Pr_OnVoted;
return true;
}
return false;
}
public Poll StopPoll(ulong guildId)
{
if (ActivePolls.TryRemove(guildId, out var pr))
{
pr.OnVoted -= Pr_OnVoted;
using var uow = _db.GetDbContext();
uow.RemovePoll(pr.Poll.Id);
uow.SaveChanges();
return pr.Poll;
}
return null;
}
private async Task Pr_OnVoted(IUserMessage msg, IGuildUser usr)
{
var toDelete = await msg.Channel.SendConfirmAsync(_eb,
_strs.GetText(strs.poll_voted(Format.Bold(usr.ToString())), usr.GuildId))
.ConfigureAwait(false);
toDelete.DeleteAfter(5);
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
}
public async Task<bool> RunBehavior(IGuild guild, IUserMessage msg)
{
if (guild is null)
return false;
if (!ActivePolls.TryGetValue(guild.Id, out var poll))
return false;
try
{
var voted = await poll.TryVote(msg).ConfigureAwait(false);
if (voted)
{
Log.Information("User {UserName} [{UserId}] voted in a poll on {GuildName} [{GuildId}] server",
msg.Author.ToString(),
msg.Author.Id,
guild.Name,
guild.Id);
}
return voted;
}
catch (Exception ex)
{
Log.Warning(ex, "Error voting");
}
return false;
var pr = new PollRunner(db, x);
pr.OnVoted += Pr_OnVoted;
return pr;
})
.ToConcurrent();
}
}
}
public Poll CreatePoll(ulong guildId, ulong channelId, string input)
{
if (string.IsNullOrWhiteSpace(input) || !input.Contains(";"))
return null;
var data = input.Split(';');
if (data.Length < 3)
return null;
var col = new IndexedCollection<PollAnswer>(data.Skip(1)
.Select(x => new PollAnswer() { Text = x }));
return new Poll()
{
Answers = col,
Question = data[0],
ChannelId = channelId,
GuildId = guildId,
Votes = new System.Collections.Generic.HashSet<PollVote>()
};
}
public bool StartPoll(Poll p)
{
var pr = new PollRunner(_db, p);
if (ActivePolls.TryAdd(p.GuildId, pr))
{
using (var uow = _db.GetDbContext())
{
uow.Poll.Add(p);
uow.SaveChanges();
}
pr.OnVoted += Pr_OnVoted;
return true;
}
return false;
}
public Poll StopPoll(ulong guildId)
{
if (ActivePolls.TryRemove(guildId, out var pr))
{
pr.OnVoted -= Pr_OnVoted;
using var uow = _db.GetDbContext();
uow.RemovePoll(pr.Poll.Id);
uow.SaveChanges();
return pr.Poll;
}
return null;
}
private async Task Pr_OnVoted(IUserMessage msg, IGuildUser usr)
{
var toDelete = await msg.Channel.SendConfirmAsync(_eb,
_strs.GetText(strs.poll_voted(Format.Bold(usr.ToString())), usr.GuildId))
.ConfigureAwait(false);
toDelete.DeleteAfter(5);
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
}
public async Task<bool> RunBehavior(IGuild guild, IUserMessage msg)
{
if (guild is null)
return false;
if (!ActivePolls.TryGetValue(guild.Id, out var poll))
return false;
try
{
var voted = await poll.TryVote(msg).ConfigureAwait(false);
if (voted)
{
Log.Information("User {UserName} [{UserId}] voted in a poll on {GuildName} [{GuildId}] server",
msg.Author.ToString(),
msg.Author.Id,
guild.Name,
guild.Id);
}
return voted;
}
catch (Exception ex)
{
Log.Warning(ex, "Error voting");
}
return false;
}
}

View File

@@ -2,116 +2,112 @@
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Modules.Games.Services;
using NadekoBot.Common;
namespace NadekoBot.Modules.Games
namespace NadekoBot.Modules.Games;
public partial class Games
{
public partial class Games
[Group]
public class SpeedTypingCommands : NadekoSubmodule<GamesService>
{
[Group]
public class SpeedTypingCommands : NadekoSubmodule<GamesService>
private readonly GamesService _games;
private readonly DiscordSocketClient _client;
public SpeedTypingCommands(DiscordSocketClient client, GamesService games)
{
private readonly GamesService _games;
private readonly DiscordSocketClient _client;
_games = games;
_client = client;
}
public SpeedTypingCommands(DiscordSocketClient client, GamesService games)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptionsAttribute(typeof(TypingGame.Options))]
public async Task TypeStart(params string[] args)
{
var (options, _) = OptionsParser.ParseFrom(new TypingGame.Options(), args);
var channel = (ITextChannel)ctx.Channel;
var game = _service.RunningContests.GetOrAdd(ctx.Guild.Id, id => new TypingGame(_games, _client, channel, Prefix, options, _eb));
if (game.IsActive)
{
_games = games;
_client = client;
await SendErrorAsync($"Contest already running in {game.Channel.Mention} channel.");
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptionsAttribute(typeof(TypingGame.Options))]
public async Task TypeStart(params string[] args)
else
{
var (options, _) = OptionsParser.ParseFrom(new TypingGame.Options(), args);
var channel = (ITextChannel)ctx.Channel;
var game = _service.RunningContests.GetOrAdd(ctx.Guild.Id, id => new TypingGame(_games, _client, channel, Prefix, options, _eb));
if (game.IsActive)
{
await SendErrorAsync($"Contest already running in {game.Channel.Mention} channel.");
}
else
{
await game.Start().ConfigureAwait(false);
}
await game.Start().ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task TypeStop()
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task TypeStop()
{
if (_service.RunningContests.TryRemove(ctx.Guild.Id, out TypingGame game))
{
if (_service.RunningContests.TryRemove(ctx.Guild.Id, out TypingGame game))
{
await game.Stop().ConfigureAwait(false);
return;
}
await game.Stop().ConfigureAwait(false);
return;
}
await SendErrorAsync("No contest to stop on this channel.").ConfigureAwait(false);
}
await SendErrorAsync("No contest to stop on this channel.").ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Typeadd([Leftover] string text)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Typeadd([Leftover] string text)
{
if (string.IsNullOrWhiteSpace(text))
return;
_games.AddTypingArticle(ctx.User, text);
await SendConfirmAsync("Added new article for typing game.").ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Typelist(int page = 1)
{
if (page < 1)
return;
var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray();
if (!articles.Any())
{
if (string.IsNullOrWhiteSpace(text))
return;
_games.AddTypingArticle(ctx.User, text);
await SendConfirmAsync("Added new article for typing game.").ConfigureAwait(false);
await SendErrorAsync($"{ctx.User.Mention} `No articles found on that page.`").ConfigureAwait(false);
return;
}
var i = (page - 1) * 15;
await SendConfirmAsync("List of articles for Type Race", string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}")))
.ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Typelist(int page = 1)
{
if (page < 1)
return;
var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray();
if (!articles.Any())
{
await SendErrorAsync($"{ctx.User.Mention} `No articles found on that page.`").ConfigureAwait(false);
return;
}
var i = (page - 1) * 15;
await SendConfirmAsync("List of articles for Type Race", string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}")))
.ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Typedel(int index)
{
var removed = _service.RemoveTypingArticle(--index);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Typedel(int index)
{
var removed = _service.RemoveTypingArticle(--index);
if (removed is null)
{
return;
}
var embed = _eb.Create()
.WithTitle($"Removed typing article #{index + 1}")
.WithDescription(removed.Text.TrimTo(50))
.WithOkColor();
await ctx.Channel.EmbedAsync(embed);
if (removed is null)
{
return;
}
var embed = _eb.Create()
.WithTitle($"Removed typing article #{index + 1}")
.WithDescription(removed.Text.TrimTo(50))
.WithOkColor();
await ctx.Channel.EmbedAsync(embed);
}
}
}

View File

@@ -8,54 +8,53 @@ using NadekoBot.Modules.Games.Services;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games
namespace NadekoBot.Modules.Games;
public partial class Games
{
public partial class Games
[Group]
public class TicTacToeCommands : NadekoSubmodule<GamesService>
{
[Group]
public class TicTacToeCommands : NadekoSubmodule<GamesService>
private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1);
private readonly DiscordSocketClient _client;
public TicTacToeCommands(DiscordSocketClient client)
{
private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1);
private readonly DiscordSocketClient _client;
_client = client;
}
public TicTacToeCommands(DiscordSocketClient client)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptions(typeof(TicTacToe.Options))]
public async Task TicTacToe(params string[] args)
{
var (options, _) = OptionsParser.ParseFrom(new TicTacToe.Options(), args);
var channel = (ITextChannel)ctx.Channel;
await _sem.WaitAsync(1000).ConfigureAwait(false);
try
{
_client = client;
if (_service.TicTacToeGames.TryGetValue(channel.Id, out TicTacToe game))
{
var _ = Task.Run(async () =>
{
await game.Start((IGuildUser)ctx.User).ConfigureAwait(false);
});
return;
}
game = new TicTacToe(base.Strings, this._client, channel, (IGuildUser)ctx.User, options, _eb);
_service.TicTacToeGames.Add(channel.Id, game);
await ReplyConfirmLocalizedAsync(strs.ttt_created).ConfigureAwait(false);
game.OnEnded += (g) =>
{
_service.TicTacToeGames.Remove(channel.Id);
_sem.Dispose();
};
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[NadekoOptions(typeof(TicTacToe.Options))]
public async Task TicTacToe(params string[] args)
finally
{
var (options, _) = OptionsParser.ParseFrom(new TicTacToe.Options(), args);
var channel = (ITextChannel)ctx.Channel;
await _sem.WaitAsync(1000).ConfigureAwait(false);
try
{
if (_service.TicTacToeGames.TryGetValue(channel.Id, out TicTacToe game))
{
var _ = Task.Run(async () =>
{
await game.Start((IGuildUser)ctx.User).ConfigureAwait(false);
});
return;
}
game = new TicTacToe(base.Strings, this._client, channel, (IGuildUser)ctx.User, options, _eb);
_service.TicTacToeGames.Add(channel.Id, game);
await ReplyConfirmLocalizedAsync(strs.ttt_created).ConfigureAwait(false);
game.OnEnded += (g) =>
{
_service.TicTacToeGames.Remove(channel.Id);
_sem.Dispose();
};
}
finally
{
_sem.Release();
}
_sem.Release();
}
}
}

View File

@@ -1,7 +1,6 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Services;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
@@ -9,93 +8,92 @@ using NadekoBot.Modules.Games.Common.Trivia;
using NadekoBot.Modules.Games.Services;
using NadekoBot.Common;
namespace NadekoBot.Modules.Games
namespace NadekoBot.Modules.Games;
public partial class Games
{
public partial class Games
[Group]
public class TriviaCommands : NadekoSubmodule<GamesService>
{
[Group]
public class TriviaCommands : NadekoSubmodule<GamesService>
private readonly IDataCache _cache;
private readonly ICurrencyService _cs;
private readonly GamesConfigService _gamesConfig;
private readonly DiscordSocketClient _client;
public TriviaCommands(DiscordSocketClient client, IDataCache cache, ICurrencyService cs,
GamesConfigService gamesConfig)
{
private readonly IDataCache _cache;
private readonly ICurrencyService _cs;
private readonly GamesConfigService _gamesConfig;
private readonly DiscordSocketClient _client;
_cache = cache;
_cs = cs;
_gamesConfig = gamesConfig;
_client = client;
}
public TriviaCommands(DiscordSocketClient client, IDataCache cache, ICurrencyService cs,
GamesConfigService gamesConfig)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
[NadekoOptionsAttribute(typeof(TriviaOptions))]
public Task Trivia(params string[] args)
=> InternalTrivia(args);
public 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)
{
_cache = cache;
_cs = cs;
_gamesConfig = gamesConfig;
_client = client;
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
[NadekoOptionsAttribute(typeof(TriviaOptions))]
public Task Trivia(params string[] args)
=> InternalTrivia(args);
public async Task InternalTrivia(params string[] args)
var trivia = new TriviaGame(Strings, _client, config, _cache, _cs, channel.Guild, channel, opts,
Prefix + "tq", _eb);
if (_service.RunningTrivias.TryAdd(channel.Guild.Id, trivia))
{
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)
try
{
return;
await trivia.StartGame().ConfigureAwait(false);
}
var trivia = new TriviaGame(Strings, _client, config, _cache, _cs, channel.Guild, channel, opts,
Prefix + "tq", _eb);
if (_service.RunningTrivias.TryAdd(channel.Guild.Id, trivia))
finally
{
try
{
await trivia.StartGame().ConfigureAwait(false);
}
finally
{
_service.RunningTrivias.TryRemove(channel.Guild.Id, out trivia);
await trivia.EnsureStopped().ConfigureAwait(false);
}
return;
_service.RunningTrivias.TryRemove(channel.Guild.Id, out trivia);
await trivia.EnsureStopped().ConfigureAwait(false);
}
await SendErrorAsync(GetText(strs.trivia_already_running) + "\n" + trivia.CurrentQuestion)
.ConfigureAwait(false);
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Tl()
{
if (_service.RunningTrivias.TryGetValue(ctx.Guild.Id, out TriviaGame trivia))
{
await SendConfirmAsync(GetText(strs.leaderboard), trivia.GetLeaderboard()).ConfigureAwait(false);
return;
}
await SendErrorAsync(GetText(strs.trivia_already_running) + "\n" + trivia.CurrentQuestion)
.ConfigureAwait(false);
}
await ReplyErrorLocalizedAsync(strs.trivia_none).ConfigureAwait(false);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Tl()
{
if (_service.RunningTrivias.TryGetValue(ctx.Guild.Id, out TriviaGame trivia))
{
await SendConfirmAsync(GetText(strs.leaderboard), trivia.GetLeaderboard()).ConfigureAwait(false);
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Tq()
await ReplyErrorLocalizedAsync(strs.trivia_none).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Tq()
{
var channel = (ITextChannel)ctx.Channel;
if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia))
{
var channel = (ITextChannel)ctx.Channel;
if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia))
{
await trivia.StopGame().ConfigureAwait(false);
return;
}
await ReplyErrorLocalizedAsync(strs.trivia_none).ConfigureAwait(false);
await trivia.StopGame().ConfigureAwait(false);
return;
}
await ReplyErrorLocalizedAsync(strs.trivia_none).ConfigureAwait(false);
}
}
}