mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Killed history
This commit is contained in:
149
NadekoBot.Core/Modules/Games/AcropobiaCommands.cs
Normal file
149
NadekoBot.Core/Modules/Games/AcropobiaCommands.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using Discord;
|
||||
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.Core.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class AcropobiaCommands : NadekoSubmodule<GamesService>
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public AcropobiaCommands(DiscordSocketClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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))
|
||||
{
|
||||
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("acro_running").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Task _client_MessageReceived(SocketMessage msg)
|
||||
{
|
||||
if (msg.Channel.Id != ctx.Channel.Id)
|
||||
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 { }
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private Task Game_OnStarted(AcrophobiaGame game)
|
||||
{
|
||||
var embed = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("acrophobia"))
|
||||
.WithDescription(GetText("acro_started", Format.Bold(string.Join(".", game.StartingLetters))))
|
||||
.WithFooter(efb => efb.WithText(GetText("acro_started_footer", game.Opts.SubmissionTime)));
|
||||
|
||||
return ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
private Task Game_OnUserVoted(string user)
|
||||
{
|
||||
return ctx.Channel.SendConfirmAsync(
|
||||
GetText("acrophobia"),
|
||||
GetText("acro_vote_cast", Format.Bold(user)));
|
||||
}
|
||||
|
||||
private async Task Game_OnVotingStarted(AcrophobiaGame game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> submissions)
|
||||
{
|
||||
if (submissions.Length == 0)
|
||||
{
|
||||
await ctx.Channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_ended_no_sub")).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
if (submissions.Length == 1)
|
||||
{
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithDescription(
|
||||
GetText("acro_winner_only",
|
||||
Format.Bold(submissions.First().Key.UserName)))
|
||||
.WithFooter(efb => efb.WithText(submissions.First().Key.Input)))
|
||||
.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var i = 0;
|
||||
var embed = new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText("acrophobia") + " - " + GetText("submissions_closed"))
|
||||
.WithDescription(GetText("acro_nym_was", Format.Bold(string.Join(".", game.StartingLetters)) + "\n" +
|
||||
$@"--
|
||||
{submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.Input}**\n")}
|
||||
--"))
|
||||
.WithFooter(efb => efb.WithText(GetText("acro_vote")));
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task Game_OnEnded(AcrophobiaGame game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> votes)
|
||||
{
|
||||
if (!votes.Any() || votes.All(x => x.Value == 0))
|
||||
{
|
||||
await ctx.Channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_no_votes_cast")).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var table = votes.OrderByDescending(v => v.Value);
|
||||
var winner = table.First();
|
||||
var embed = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("acrophobia"))
|
||||
.WithDescription(GetText("acro_winner", Format.Bold(winner.Key.UserName),
|
||||
Format.Bold(winner.Value.ToString())))
|
||||
.WithFooter(efb => efb.WithText(winner.Key.Input));
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
NadekoBot.Core/Modules/Games/CleverBotCommands.cs
Normal file
56
NadekoBot.Core/Modules/Games/CleverBotCommands.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Core.Services;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class ChatterBotCommands : NadekoSubmodule<ChatterBotService>
|
||||
{
|
||||
private readonly DbService _db;
|
||||
|
||||
public ChatterBotCommands(DbService db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("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("cleverbot_enabled").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
190
NadekoBot.Core/Modules/Games/Common/Acrophobia/Acrophobia.cs
Normal file
190
NadekoBot.Core/Modules/Games/Common/Acrophobia/Acrophobia.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Acrophobia
|
||||
{
|
||||
public sealed class AcrophobiaGame : IDisposable
|
||||
{
|
||||
public class Options : INadekoCommandOptions
|
||||
{
|
||||
[Option('s', "submission-time", Required = false, Default = 60, HelpText = "Time after which the submissions are closed and voting starts.")]
|
||||
public int SubmissionTime { get; set; } = 60;
|
||||
|
||||
[Option('v', "vote-time", Required = false, Default = 60, HelpText = "Time after which the voting is closed and the winner is declared.")]
|
||||
public int VoteTime { get; set; } = 30;
|
||||
|
||||
public void NormalizeOptions()
|
||||
{
|
||||
if (SubmissionTime < 15 || SubmissionTime > 300)
|
||||
SubmissionTime = 60;
|
||||
if (VoteTime < 15 || VoteTime > 120)
|
||||
VoteTime = 30;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Phase
|
||||
{
|
||||
Submission,
|
||||
Voting,
|
||||
Ended
|
||||
}
|
||||
|
||||
public enum UserInputResult
|
||||
{
|
||||
Submitted,
|
||||
SubmissionFailed,
|
||||
Voted,
|
||||
VotingFailed,
|
||||
Failed
|
||||
}
|
||||
|
||||
public Phase CurrentPhase { get; private set; } = Phase.Submission;
|
||||
public ImmutableArray<char> StartingLetters { get; private set; }
|
||||
|
||||
private readonly Dictionary<AcrophobiaUser, int> submissions = new Dictionary<AcrophobiaUser, int>();
|
||||
private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
|
||||
public Options Opts { get; }
|
||||
private readonly NadekoRandom _rng;
|
||||
|
||||
public event Func<AcrophobiaGame, Task> OnStarted = delegate { return Task.CompletedTask; };
|
||||
public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnVotingStarted = delegate { return Task.CompletedTask; };
|
||||
public event Func<string, Task> OnUserVoted = delegate { return Task.CompletedTask; };
|
||||
public event Func<AcrophobiaGame, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnEnded = delegate { return Task.CompletedTask; };
|
||||
|
||||
private readonly HashSet<ulong> _usersWhoVoted = new HashSet<ulong>();
|
||||
|
||||
public AcrophobiaGame(Options options)
|
||||
{
|
||||
Opts = options;
|
||||
_rng = new NadekoRandom();
|
||||
InitializeStartingLetters();
|
||||
}
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
await OnStarted(this).ConfigureAwait(false);
|
||||
await Task.Delay(Opts.SubmissionTime * 1000).ConfigureAwait(false);
|
||||
await locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (submissions.Count == 0)
|
||||
{
|
||||
CurrentPhase = Phase.Ended;
|
||||
await OnVotingStarted(this, ImmutableArray.Create<KeyValuePair<AcrophobiaUser, int>>()).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
if (submissions.Count == 1)
|
||||
{
|
||||
CurrentPhase = Phase.Ended;
|
||||
await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentPhase = Phase.Voting;
|
||||
|
||||
await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
|
||||
}
|
||||
finally { locker.Release(); }
|
||||
|
||||
await Task.Delay(Opts.VoteTime * 1000).ConfigureAwait(false);
|
||||
await locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
CurrentPhase = Phase.Ended;
|
||||
await OnEnded(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
|
||||
}
|
||||
finally { locker.Release(); }
|
||||
}
|
||||
|
||||
private void InitializeStartingLetters()
|
||||
{
|
||||
var wordCount = _rng.Next(3, 6);
|
||||
|
||||
var lettersArr = new char[wordCount];
|
||||
|
||||
for (int i = 0; i < wordCount; i++)
|
||||
{
|
||||
var randChar = (char)_rng.Next(65, 91);
|
||||
lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar;
|
||||
}
|
||||
StartingLetters = lettersArr.ToImmutableArray();
|
||||
}
|
||||
|
||||
public async Task<bool> UserInput(ulong userId, string userName, string input)
|
||||
{
|
||||
var user = new AcrophobiaUser(userId, userName, input.ToLowerInvariant().ToTitleCase());
|
||||
|
||||
await locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
switch (CurrentPhase)
|
||||
{
|
||||
case Phase.Submission:
|
||||
if (submissions.ContainsKey(user) || !IsValidAnswer(input))
|
||||
break;
|
||||
|
||||
submissions.Add(user, 0);
|
||||
return true;
|
||||
case Phase.Voting:
|
||||
AcrophobiaUser toVoteFor;
|
||||
if (!int.TryParse(input, out var index)
|
||||
|| --index < 0
|
||||
|| index >= submissions.Count
|
||||
|| (toVoteFor = submissions.ToArray()[index].Key).UserId == user.UserId
|
||||
|| !_usersWhoVoted.Add(userId))
|
||||
break;
|
||||
++submissions[toVoteFor];
|
||||
var _ = Task.Run(() => OnUserVoted(userName));
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidAnswer(string input)
|
||||
{
|
||||
input = input.ToUpperInvariant();
|
||||
|
||||
var inputWords = input.Split(' ');
|
||||
|
||||
if (inputWords.Length != StartingLetters.Length) // number of words must be the same as the number of the starting letters
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < StartingLetters.Length; i++)
|
||||
{
|
||||
var letter = StartingLetters[i];
|
||||
|
||||
if (!inputWords[i].StartsWith(letter.ToString(), StringComparison.InvariantCulture)) // all first letters must match
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.CurrentPhase = Phase.Ended;
|
||||
OnStarted = null;
|
||||
OnEnded = null;
|
||||
OnUserVoted = null;
|
||||
OnVotingStarted = null;
|
||||
_usersWhoVoted.Clear();
|
||||
submissions.Clear();
|
||||
locker.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
namespace NadekoBot.Modules.Games.Common.Acrophobia
|
||||
{
|
||||
public class AcrophobiaUser
|
||||
{
|
||||
public string UserName { get; }
|
||||
public ulong UserId { get; }
|
||||
public string Input { get; }
|
||||
|
||||
public AcrophobiaUser(ulong userId, string userName, string input)
|
||||
{
|
||||
this.UserName = userName;
|
||||
this.UserId = userId;
|
||||
this.Input = input;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return UserId.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is AcrophobiaUser x
|
||||
? x.UserId == this.UserId
|
||||
: false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
|
||||
{
|
||||
public class ChatterBotResponse
|
||||
{
|
||||
public string Convo_id { get; set; }
|
||||
public string BotSay { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
|
||||
{
|
||||
public class ChatterBotSession : IChatterBotSession
|
||||
{
|
||||
private static NadekoRandom Rng { get; } = new NadekoRandom();
|
||||
|
||||
private readonly string _chatterBotId;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly int _botId = 6;
|
||||
|
||||
public ChatterBotSession(IHttpClientFactory httpFactory)
|
||||
{
|
||||
_chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64();
|
||||
_httpFactory = httpFactory;
|
||||
}
|
||||
|
||||
private string ApiEndpoint => "http://api.program-o.com/v2/chatbot/" +
|
||||
$"?bot_id={_botId}&" +
|
||||
"say={0}&" +
|
||||
$"convo_id=nadekobot_{_chatterBotId}&" +
|
||||
"format=json";
|
||||
|
||||
public async Task<string> Think(string message)
|
||||
{
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
{
|
||||
var res = await http.GetStringAsync(string.Format(ApiEndpoint, message)).ConfigureAwait(false);
|
||||
var cbr = JsonConvert.DeserializeObject<ChatterBotResponse>(res);
|
||||
return cbr.BotSay.Replace("<br/>", "\n", StringComparison.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
|
||||
{
|
||||
public class CleverbotResponse
|
||||
{
|
||||
public string Cs { get; set; }
|
||||
public string Output { get; set; }
|
||||
}
|
||||
|
||||
public class CleverbotIOCreateResponse
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public string Nick { get; set; }
|
||||
}
|
||||
|
||||
public class CleverbotIOAskResponse
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public string Response { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
|
||||
{
|
||||
public interface IChatterBotSession
|
||||
{
|
||||
Task<string> Think(string input);
|
||||
}
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
using NadekoBot.Common;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
|
||||
{
|
||||
public class OfficialCleverbotSession : IChatterBotSession
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private string _cs = null;
|
||||
|
||||
private string QueryString => $"https://www.cleverbot.com/getreply?key={_apiKey}" +
|
||||
"&wrapper=nadekobot" +
|
||||
"&input={0}" +
|
||||
"&cs={1}";
|
||||
|
||||
public OfficialCleverbotSession(string apiKey, IHttpClientFactory factory)
|
||||
{
|
||||
this._apiKey = apiKey;
|
||||
this._httpFactory = factory;
|
||||
}
|
||||
|
||||
public async Task<string> Think(string input)
|
||||
{
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
{
|
||||
var dataString = await http.GetStringAsync(string.Format(QueryString, input, _cs ?? "")).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
|
||||
|
||||
_cs = data?.Cs;
|
||||
return data?.Output;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Log.Warning("Unexpected cleverbot response received: ");
|
||||
Log.Warning(dataString);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CleverbotIOSession : IChatterBotSession
|
||||
{
|
||||
private readonly string _key;
|
||||
private readonly string _user;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly AsyncLazy<string> _nick;
|
||||
|
||||
private readonly string _createEndpoint = $"https://cleverbot.io/1.0/create";
|
||||
private readonly string _askEndpoint = $"https://cleverbot.io/1.0/ask";
|
||||
|
||||
public CleverbotIOSession(string user, string key, IHttpClientFactory factory)
|
||||
{
|
||||
this._key = key;
|
||||
this._user = user;
|
||||
this._httpFactory = factory;
|
||||
|
||||
_nick = new AsyncLazy<string>((Func<Task<string>>)GetNick);
|
||||
}
|
||||
|
||||
private async Task<string> GetNick()
|
||||
{
|
||||
using (var _http = _httpFactory.CreateClient())
|
||||
using (var msg = new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("user", _user),
|
||||
new KeyValuePair<string, string>("key", _key),
|
||||
}))
|
||||
using (var data = await _http.PostAsync(_createEndpoint, msg).ConfigureAwait(false))
|
||||
{
|
||||
var str = await data.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var obj = JsonConvert.DeserializeObject<CleverbotIOCreateResponse>(str);
|
||||
if (obj.Status != "success")
|
||||
throw new OperationCanceledException(obj.Status);
|
||||
|
||||
return obj.Nick;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> Think(string input)
|
||||
{
|
||||
using (var _http = _httpFactory.CreateClient())
|
||||
using (var msg = new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("user", _user),
|
||||
new KeyValuePair<string, string>("key", _key),
|
||||
new KeyValuePair<string, string>("nick", await _nick),
|
||||
new KeyValuePair<string, string>("text", input),
|
||||
}))
|
||||
using (var data = await _http.PostAsync(_askEndpoint, msg).ConfigureAwait(false))
|
||||
{
|
||||
var str = await data.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var obj = JsonConvert.DeserializeObject<CleverbotIOAskResponse>(str);
|
||||
if (obj.Status != "success")
|
||||
throw new OperationCanceledException(obj.Status);
|
||||
|
||||
return obj.Response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
NadekoBot.Core/Modules/Games/Common/GamesConfig.cs
Normal file
72
NadekoBot.Core/Modules/Games/Common/GamesConfig.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using NadekoBot.Common.Yml;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public sealed class GamesConfig
|
||||
{
|
||||
[Comment("Trivia related settings (.t command)")]
|
||||
public TriviaConfig Trivia { get; set; } = new TriviaConfig()
|
||||
{
|
||||
CurrencyReward = 0,
|
||||
MinimumWinReq = 1,
|
||||
};
|
||||
|
||||
[Comment("List of responses for the .8ball command. A random one will be selected every time")]
|
||||
public List<string> EightBallResponses { get; set; } = new List<string>()
|
||||
{
|
||||
"Most definitely yes.",
|
||||
"For sure.",
|
||||
"Totally!",
|
||||
"Of course!",
|
||||
"As I see it, yes.",
|
||||
"My sources say yes.",
|
||||
"Yes.",
|
||||
"Most likely.",
|
||||
"Perhaps...",
|
||||
"Maybe...",
|
||||
"Hm, not sure.",
|
||||
"It is uncertain.",
|
||||
"Ask me again later.",
|
||||
"Don't count on it.",
|
||||
"Probably not.",
|
||||
"Very doubtful.",
|
||||
"Most likely no.",
|
||||
"Nope.",
|
||||
"No.",
|
||||
"My sources say no.",
|
||||
"Don't even think about it.",
|
||||
"Definitely no.",
|
||||
"NO - It may cause disease contraction!"
|
||||
};
|
||||
|
||||
[Comment("List of animals which will be used for the animal race game (.race)")]
|
||||
public List<RaceAnimal> RaceAnimals { get; set; } = new List<RaceAnimal>()
|
||||
{
|
||||
new RaceAnimal {Icon = "🐼", Name = "Panda"},
|
||||
new RaceAnimal {Icon = "🐻", Name = "Bear"},
|
||||
new RaceAnimal {Icon = "🐧", Name = "Pengu"},
|
||||
new RaceAnimal {Icon = "🐨", Name = "Koala"},
|
||||
new RaceAnimal {Icon = "🐬", Name = "Dolphin"},
|
||||
new RaceAnimal {Icon = "🐞", Name = "Ladybird"},
|
||||
new RaceAnimal {Icon = "🦀", Name = "Crab"},
|
||||
new RaceAnimal {Icon = "🦄", Name = "Unicorn"}
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class TriviaConfig
|
||||
{
|
||||
[Comment("The amount of currency awarded to the winner of the trivia game.")]
|
||||
public long CurrencyReward { get; set; } = 0;
|
||||
|
||||
[Comment(@"Users won't be able to start trivia games which have
|
||||
a smaller win requirement than the one specified by this setting.")]
|
||||
public int MinimumWinReq { get; set; } = 1;
|
||||
}
|
||||
|
||||
public sealed class RaceAnimal
|
||||
{
|
||||
public string Icon { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
75
NadekoBot.Core/Modules/Games/Common/GirlRating.cs
Normal file
75
NadekoBot.Core/Modules/Games/Common/GirlRating.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Core.Services;
|
||||
using Serilog;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class GirlRating
|
||||
{
|
||||
private readonly IImageCache _images;
|
||||
|
||||
public double Crazy { get; }
|
||||
public double Hot { get; }
|
||||
public int Roll { get; }
|
||||
public string Advice { get; }
|
||||
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public AsyncLazy<Stream> Stream { get; }
|
||||
|
||||
public GirlRating(IImageCache images, IHttpClientFactory factory, double crazy, double hot, int roll, string advice)
|
||||
{
|
||||
_images = images;
|
||||
Crazy = crazy;
|
||||
Hot = hot;
|
||||
Roll = roll;
|
||||
Advice = advice; // convenient to have it here, even though atm there are only few different ones.
|
||||
_httpFactory = factory;
|
||||
|
||||
Stream = new AsyncLazy<Stream>(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var img = Image.Load(_images.RategirlMatrix))
|
||||
{
|
||||
const int minx = 35;
|
||||
const int miny = 385;
|
||||
const int length = 345;
|
||||
|
||||
var pointx = (int)(minx + length * (Hot / 10));
|
||||
var pointy = (int)(miny - length * ((Crazy - 4) / 6));
|
||||
|
||||
using (var pointImg = Image.Load(_images.RategirlDot))
|
||||
{
|
||||
img.Mutate(x => x.DrawImage(pointImg, new Point(pointx - 10, pointy - 10), new GraphicsOptions()));
|
||||
}
|
||||
|
||||
var imgStream = new MemoryStream();
|
||||
img.SaveAsPng(imgStream);
|
||||
return imgStream;
|
||||
//using (var byteContent = new ByteArrayContent(imgStream.ToArray()))
|
||||
//{
|
||||
// http.AddFakeHeaders();
|
||||
|
||||
// using (var reponse = await http.PutAsync("https://transfer.sh/img.png", byteContent).ConfigureAwait(false))
|
||||
// {
|
||||
// url = await reponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error getting RateGirl image");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman.Exceptions
|
||||
{
|
||||
public class TermNotFoundException : Exception
|
||||
{
|
||||
public TermNotFoundException() : base("Term of that type couldn't be found")
|
||||
{
|
||||
}
|
||||
|
||||
public TermNotFoundException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public TermNotFoundException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
170
NadekoBot.Core/Modules/Games/Common/Hangman/Hangman.cs
Normal file
170
NadekoBot.Core/Modules/Games/Common/Hangman/Hangman.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman
|
||||
{
|
||||
public sealed class Hangman : IDisposable
|
||||
{
|
||||
public string TermType { get; }
|
||||
public TermPool TermPool { get; }
|
||||
public HangmanObject Term { get; }
|
||||
|
||||
public string ScrambledWord => "`" + String.Concat(Term.Word.Select(c =>
|
||||
{
|
||||
if (c == ' ')
|
||||
return " \u2000";
|
||||
if (!(char.IsLetter(c) || char.IsDigit(c)))
|
||||
return $" {c}";
|
||||
|
||||
c = char.ToLowerInvariant(c);
|
||||
return _previousGuesses.Contains(c) ? $" {c}" : " ◯";
|
||||
})) + "`";
|
||||
|
||||
private Phase _currentPhase = Phase.Active;
|
||||
public Phase CurrentPhase
|
||||
{
|
||||
get => _currentPhase;
|
||||
set
|
||||
{
|
||||
if (value == Phase.Ended)
|
||||
_endingCompletionSource.TrySetResult(true);
|
||||
|
||||
_currentPhase = value;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
|
||||
|
||||
private readonly HashSet<ulong> _recentUsers = new HashSet<ulong>();
|
||||
|
||||
public uint Errors { get; private set; } = 0;
|
||||
public uint MaxErrors { get; } = 6;
|
||||
|
||||
public event Func<Hangman, string, Task> OnGameEnded = delegate { return Task.CompletedTask; };
|
||||
public event Func<Hangman, string, char, Task> OnLetterAlreadyUsed = delegate { return Task.CompletedTask; };
|
||||
public event Func<Hangman, string, char, Task> OnGuessFailed = delegate { return Task.CompletedTask; };
|
||||
public event Func<Hangman, string, char, Task> OnGuessSucceeded = delegate { return Task.CompletedTask; };
|
||||
|
||||
private readonly HashSet<char> _previousGuesses = new HashSet<char>();
|
||||
public ImmutableArray<char> PreviousGuesses => _previousGuesses.ToImmutableArray();
|
||||
|
||||
private readonly TaskCompletionSource<bool> _endingCompletionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
public Task EndedTask => _endingCompletionSource.Task;
|
||||
|
||||
public Hangman(string type, TermPool tp = null)
|
||||
{
|
||||
this.TermType = type.Trim().ToLowerInvariant().ToTitleCase();
|
||||
this.TermPool = tp ?? new TermPool();
|
||||
this.Term = this.TermPool.GetTerm(type);
|
||||
}
|
||||
|
||||
private void AddError()
|
||||
{
|
||||
if (++Errors > MaxErrors)
|
||||
{
|
||||
var _ = OnGameEnded(this, null);
|
||||
CurrentPhase = Phase.Ended;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetHangman() => $@". ┌─────┐
|
||||
.┃...............┋
|
||||
.┃...............┋
|
||||
.┃{(Errors > 0 ? ".............😲" : "")}
|
||||
.┃{(Errors > 1 ? "............./" : "")} {(Errors > 2 ? "|" : "")} {(Errors > 3 ? "\\" : "")}
|
||||
.┃{(Errors > 4 ? "............../" : "")} {(Errors > 5 ? "\\" : "")}
|
||||
/-\";
|
||||
|
||||
public async Task Input(ulong userId, string userName, string input)
|
||||
{
|
||||
if (CurrentPhase == Phase.Ended)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return;
|
||||
|
||||
input = input.Trim().ToLowerInvariant();
|
||||
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (CurrentPhase == Phase.Ended)
|
||||
return;
|
||||
|
||||
if (input.Length > 1) // tried to guess the whole word
|
||||
{
|
||||
if (input != Term.Word) // failed
|
||||
return;
|
||||
|
||||
var _ = OnGameEnded?.Invoke(this, userName);
|
||||
CurrentPhase = Phase.Ended;
|
||||
return;
|
||||
}
|
||||
|
||||
var ch = input[0];
|
||||
|
||||
if (!char.IsLetterOrDigit(ch))
|
||||
return;
|
||||
|
||||
if (!_recentUsers.Add(userId)) // don't let a single user spam guesses
|
||||
return;
|
||||
|
||||
if (!_previousGuesses.Add(ch)) // that letter was already guessed
|
||||
{
|
||||
var _ = OnLetterAlreadyUsed?.Invoke(this, userName, ch);
|
||||
}
|
||||
else if (!Term.Word.Contains(ch)) // guessed letter doesn't exist
|
||||
{
|
||||
var _ = OnGuessFailed?.Invoke(this, userName, ch);
|
||||
AddError();
|
||||
}
|
||||
else if (Term.Word.All(x => _previousGuesses.IsSupersetOf(Term.Word.ToLowerInvariant()
|
||||
.Where(char.IsLetterOrDigit))))
|
||||
{
|
||||
var _ = OnGameEnded.Invoke(this, userName); // if all letters are guessed
|
||||
CurrentPhase = Phase.Ended;
|
||||
}
|
||||
else // guessed but not last letter
|
||||
{
|
||||
var _ = OnGuessSucceeded?.Invoke(this, userName, ch);
|
||||
_recentUsers.Remove(userId); // he can guess again right away
|
||||
return;
|
||||
}
|
||||
|
||||
var clearSpam = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(3000).ConfigureAwait(false); // remove the user from the spamlist after 5 seconds
|
||||
_recentUsers.Remove(userId);
|
||||
});
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}
|
||||
|
||||
public async Task Stop()
|
||||
{
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
CurrentPhase = Phase.Ended;
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
OnGameEnded = null;
|
||||
OnGuessFailed = null;
|
||||
OnGuessSucceeded = null;
|
||||
OnLetterAlreadyUsed = null;
|
||||
_previousGuesses.Clear();
|
||||
_recentUsers.Clear();
|
||||
// _locker.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
17
NadekoBot.Core/Modules/Games/Common/Hangman/HangmanObject.cs
Normal file
17
NadekoBot.Core/Modules/Games/Common/Hangman/HangmanObject.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman
|
||||
{
|
||||
public class HangmanObject
|
||||
{
|
||||
public string Word { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
public string GetWord()
|
||||
{
|
||||
var term = Word.ToTitleCase();
|
||||
|
||||
return $"[{term}](https://en.wikipedia.org/wiki/{term.Replace(' ', '_')})";
|
||||
}
|
||||
}
|
||||
}
|
8
NadekoBot.Core/Modules/Games/Common/Hangman/Phase.cs
Normal file
8
NadekoBot.Core/Modules/Games/Common/Hangman/Phase.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman
|
||||
{
|
||||
public enum Phase
|
||||
{
|
||||
Active,
|
||||
Ended,
|
||||
}
|
||||
}
|
50
NadekoBot.Core/Modules/Games/Common/Hangman/TermPool.cs
Normal file
50
NadekoBot.Core/Modules/Games/Common/Hangman/TermPool.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Modules.Games.Common.Hangman.Exceptions;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman
|
||||
{
|
||||
public class TermPool
|
||||
{
|
||||
const string termsPath = "data/hangman.json";
|
||||
|
||||
public IReadOnlyDictionary<string, HangmanObject[]> Data { get; } = new Dictionary<string, HangmanObject[]>();
|
||||
public TermPool()
|
||||
{
|
||||
try
|
||||
{
|
||||
Data = JsonConvert.DeserializeObject<Dictionary<string, HangmanObject[]>>(File.ReadAllText(termsPath));
|
||||
Data = Data.ToDictionary(
|
||||
x => x.Key.ToLowerInvariant(),
|
||||
x => x.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error loading Hangman Term pool");
|
||||
}
|
||||
}
|
||||
|
||||
public HangmanObject GetTerm(string type)
|
||||
{
|
||||
type = type?.Trim().ToLowerInvariant();
|
||||
var rng = new NadekoRandom();
|
||||
|
||||
if (type == "random")
|
||||
{
|
||||
type = Data.Keys.ToArray()[rng.Next(0, Data.Keys.Count())];
|
||||
}
|
||||
if (!Data.TryGetValue(type, out var termTypes) || termTypes.Length == 0)
|
||||
throw new TermNotFoundException();
|
||||
|
||||
var obj = termTypes[rng.Next(0, termTypes.Length)];
|
||||
|
||||
obj.Word = obj.Word.Trim().ToLowerInvariant();
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
14
NadekoBot.Core/Modules/Games/Common/Hangman/TermType.cs
Normal file
14
NadekoBot.Core/Modules/Games/Common/Hangman/TermType.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman
|
||||
{
|
||||
[Flags]
|
||||
public enum TermTypes
|
||||
{
|
||||
Countries = 0,
|
||||
Movies = 1,
|
||||
Animals = 2,
|
||||
Things = 4,
|
||||
Random = 8,
|
||||
}
|
||||
}
|
185
NadekoBot.Core/Modules/Games/Common/Nunchi/Nunchi.cs
Normal file
185
NadekoBot.Core/Modules/Games/Common/Nunchi/Nunchi.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using NadekoBot.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Nunchi
|
||||
{
|
||||
public sealed class NunchiGame : IDisposable
|
||||
{
|
||||
public enum Phase
|
||||
{
|
||||
Joining,
|
||||
Playing,
|
||||
WaitingForNextRound,
|
||||
Ended,
|
||||
}
|
||||
|
||||
public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100);
|
||||
public Phase CurrentPhase { get; private set; } = Phase.Joining;
|
||||
|
||||
public event Func<NunchiGame, Task> OnGameStarted;
|
||||
public event Func<NunchiGame, int, Task> OnRoundStarted;
|
||||
public event Func<NunchiGame, Task> OnUserGuessed;
|
||||
public event Func<NunchiGame, (ulong Id, string Name)?, Task> OnRoundEnded; // tuple of the user who failed
|
||||
public event Func<NunchiGame, string, Task> OnGameEnded; // name of the user who won
|
||||
|
||||
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
|
||||
|
||||
private HashSet<(ulong Id, string Name)> _participants = new HashSet<(ulong Id, string Name)>();
|
||||
private HashSet<(ulong Id, string Name)> _passed = new HashSet<(ulong Id, string Name)>();
|
||||
|
||||
public ImmutableArray<(ulong Id, string Name)> Participants => _participants.ToImmutableArray();
|
||||
public int ParticipantCount => _participants.Count;
|
||||
|
||||
private const int _killTimeout = 20 * 1000;
|
||||
private const int _nextRoundTimeout = 5 * 1000;
|
||||
private Timer _killTimer;
|
||||
|
||||
public NunchiGame(ulong creatorId, string creatorName)
|
||||
{
|
||||
_participants.Add((creatorId, creatorName));
|
||||
}
|
||||
|
||||
public async Task<bool> Join(ulong userId, string userName)
|
||||
{
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (CurrentPhase != Phase.Joining)
|
||||
return false;
|
||||
|
||||
return _participants.Add((userId, userName));
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}
|
||||
|
||||
public async Task<bool> Initialize()
|
||||
{
|
||||
CurrentPhase = Phase.Joining;
|
||||
await Task.Delay(30000).ConfigureAwait(false);
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_participants.Count < 3)
|
||||
{
|
||||
CurrentPhase = Phase.Ended;
|
||||
return false;
|
||||
}
|
||||
|
||||
_killTimer = new Timer(async state =>
|
||||
{
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (CurrentPhase != Phase.Playing)
|
||||
return;
|
||||
|
||||
//if some players took too long to type a number, boot them all out and start a new round
|
||||
_participants = new HashSet<(ulong, string)>(_passed);
|
||||
EndRound();
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}, null, _killTimeout, _killTimeout);
|
||||
|
||||
CurrentPhase = Phase.Playing;
|
||||
var _ = OnGameStarted?.Invoke(this);
|
||||
var __ = OnRoundStarted?.Invoke(this, CurrentNumber);
|
||||
return true;
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}
|
||||
|
||||
public async Task Input(ulong userId, string userName, int input)
|
||||
{
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (CurrentPhase != Phase.Playing)
|
||||
return;
|
||||
|
||||
var userTuple = (Id: userId, Name: userName);
|
||||
|
||||
// if the user is not a member of the race,
|
||||
// or he already successfully typed the number
|
||||
// ignore the input
|
||||
if (!_participants.Contains(userTuple) || !_passed.Add(userTuple))
|
||||
return;
|
||||
|
||||
//if the number is correct
|
||||
if (CurrentNumber == input - 1)
|
||||
{
|
||||
//increment current number
|
||||
++CurrentNumber;
|
||||
if (_passed.Count == _participants.Count - 1)
|
||||
{
|
||||
// if only n players are left, and n - 1 type the correct number, round is over
|
||||
|
||||
// if only 2 players are left, game is over
|
||||
if (_participants.Count == 2)
|
||||
{
|
||||
_killTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
CurrentPhase = Phase.Ended;
|
||||
var _ = OnGameEnded?.Invoke(this, userTuple.Name);
|
||||
}
|
||||
else // else just start the new round without the user who was the last
|
||||
{
|
||||
var failure = _participants.Except(_passed).First();
|
||||
|
||||
OnUserGuessed?.Invoke(this);
|
||||
EndRound(failure);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
OnUserGuessed?.Invoke(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
//if the user failed
|
||||
|
||||
EndRound(userTuple);
|
||||
}
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}
|
||||
|
||||
private void EndRound((ulong, string)? failure = null)
|
||||
{
|
||||
_killTimer.Change(_killTimeout, _killTimeout);
|
||||
CurrentNumber = new NadekoRandom().Next(0, 100); // reset the counter
|
||||
_passed.Clear(); // reset all users who passed (new round starts)
|
||||
if(failure != null)
|
||||
_participants.Remove(failure.Value); // remove the dude who failed from the list of players
|
||||
|
||||
var __ = OnRoundEnded?.Invoke(this, failure);
|
||||
if (_participants.Count <= 1) // means we have a winner or everyone was booted out
|
||||
{
|
||||
_killTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
CurrentPhase = Phase.Ended;
|
||||
var _ = OnGameEnded?.Invoke(this, _participants.Count > 0 ? _participants.First().Name : null);
|
||||
return;
|
||||
}
|
||||
CurrentPhase = Phase.WaitingForNextRound;
|
||||
var throwawayDelay = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(_nextRoundTimeout).ConfigureAwait(false);
|
||||
CurrentPhase = Phase.Playing;
|
||||
var ___ = OnRoundStarted?.Invoke(this, CurrentNumber);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
OnGameEnded = null;
|
||||
OnGameStarted = null;
|
||||
OnRoundEnded = null;
|
||||
OnRoundStarted = null;
|
||||
OnUserGuessed = null;
|
||||
}
|
||||
}
|
||||
}
|
72
NadekoBot.Core/Modules/Games/Common/PollRunner.cs
Normal file
72
NadekoBot.Core/Modules/Games/Common/PollRunner.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Core.Services;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class PollRunner
|
||||
{
|
||||
public Poll Poll { get; }
|
||||
private readonly DbService _db;
|
||||
|
||||
public event Func<IUserMessage, IGuildUser, Task> OnVoted;
|
||||
|
||||
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
|
||||
|
||||
public PollRunner(DbService db, Poll poll)
|
||||
{
|
||||
_db = db;
|
||||
Poll = poll;
|
||||
}
|
||||
|
||||
public async Task<bool> TryVote(IUserMessage msg)
|
||||
{
|
||||
PollVote voteObj;
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// has to be a user message
|
||||
// channel must be the same the poll started in
|
||||
if (msg == null || msg.Author.IsBot || msg.Channel.Id != Poll.ChannelId)
|
||||
return false;
|
||||
|
||||
// has to be an integer
|
||||
if (!int.TryParse(msg.Content, out int vote))
|
||||
return false;
|
||||
--vote;
|
||||
if (vote < 0 || vote >= Poll.Answers.Count)
|
||||
return false;
|
||||
|
||||
var usr = msg.Author as IGuildUser;
|
||||
if (usr == null)
|
||||
return false;
|
||||
|
||||
voteObj = new PollVote()
|
||||
{
|
||||
UserId = msg.Author.Id,
|
||||
VoteIndex = vote,
|
||||
};
|
||||
if (!Poll.Votes.Add(voteObj))
|
||||
return false;
|
||||
|
||||
var _ = OnVoted?.Invoke(msg, usr);
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var trackedPoll = uow.Polls.GetById(Poll.Id);
|
||||
trackedPoll.Votes.Add(voteObj);
|
||||
uow.SaveChanges();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void End()
|
||||
{
|
||||
OnVoted = null;
|
||||
}
|
||||
}
|
||||
}
|
291
NadekoBot.Core/Modules/Games/Common/TicTacToe.cs
Normal file
291
NadekoBot.Core/Modules/Games/Common/TicTacToe.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Core.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class TicTacToe
|
||||
{
|
||||
public class Options : INadekoCommandOptions
|
||||
{
|
||||
public void NormalizeOptions()
|
||||
{
|
||||
if (TurnTimer < 5 || TurnTimer > 60)
|
||||
TurnTimer = 15;
|
||||
}
|
||||
|
||||
[Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. Default 15.")]
|
||||
public int TurnTimer { get; set; } = 15;
|
||||
}
|
||||
|
||||
enum Phase
|
||||
{
|
||||
Starting,
|
||||
Started,
|
||||
Ended
|
||||
}
|
||||
|
||||
private readonly ITextChannel _channel;
|
||||
private readonly IGuildUser[] _users;
|
||||
private readonly int?[,] _state;
|
||||
private Phase _phase;
|
||||
private int _curUserIndex;
|
||||
private readonly SemaphoreSlim _moveLock;
|
||||
|
||||
private IGuildUser _winner;
|
||||
|
||||
private readonly string[] _numbers = { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:" };
|
||||
|
||||
public event Action<TicTacToe> OnEnded;
|
||||
|
||||
private IUserMessage _previousMessage;
|
||||
private Timer _timeoutTimer;
|
||||
private readonly IBotStrings _strings;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly Options _options;
|
||||
|
||||
public TicTacToe(IBotStrings strings, DiscordSocketClient client, ITextChannel channel,
|
||||
IGuildUser firstUser, Options options)
|
||||
{
|
||||
_channel = channel;
|
||||
_strings = strings;
|
||||
_client = client;
|
||||
_options = options;
|
||||
|
||||
_users = new[] { firstUser, null };
|
||||
_state = new int?[,] {
|
||||
{ null, null, null },
|
||||
{ null, null, null },
|
||||
{ null, null, null },
|
||||
};
|
||||
|
||||
_phase = Phase.Starting;
|
||||
_moveLock = new SemaphoreSlim(1, 1);
|
||||
}
|
||||
|
||||
private string GetText(string key, params object[] replacements)
|
||||
=> _strings.GetText(key, _channel.GuildId, replacements);
|
||||
|
||||
public string GetState()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (var i = 0; i < _state.GetLength(0); i++)
|
||||
{
|
||||
for (var j = 0; j < _state.GetLength(1); j++)
|
||||
{
|
||||
sb.Append(_state[i, j] == null ? _numbers[i * 3 + j] : GetIcon(_state[i, j]));
|
||||
if (j < _state.GetLength(1) - 1)
|
||||
sb.Append("┃");
|
||||
}
|
||||
if (i < _state.GetLength(0) - 1)
|
||||
sb.AppendLine("\n──────────");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public EmbedBuilder GetEmbed(string title = null)
|
||||
{
|
||||
var embed = new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithDescription(Environment.NewLine + GetState())
|
||||
.WithAuthor(eab => eab.WithName(GetText("vs", _users[0], _users[1])));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
embed.WithTitle(title);
|
||||
|
||||
if (_winner == null)
|
||||
{
|
||||
if (_phase == Phase.Ended)
|
||||
embed.WithFooter(efb => efb.WithText(GetText("ttt_no_moves")));
|
||||
else
|
||||
embed.WithFooter(efb => efb.WithText(GetText("ttt_users_move", _users[_curUserIndex])));
|
||||
}
|
||||
else
|
||||
embed.WithFooter(efb => efb.WithText(GetText("ttt_has_won", _winner)));
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
private static string GetIcon(int? val)
|
||||
{
|
||||
switch (val)
|
||||
{
|
||||
case 0:
|
||||
return "❌";
|
||||
case 1:
|
||||
return "⭕";
|
||||
case 2:
|
||||
return "❎";
|
||||
case 3:
|
||||
return "🅾";
|
||||
default:
|
||||
return "⬛";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Start(IGuildUser user)
|
||||
{
|
||||
if (_phase == Phase.Started || _phase == Phase.Ended)
|
||||
{
|
||||
await _channel.SendErrorAsync(user.Mention + GetText("ttt_already_running")).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
else if (_users[0] == user)
|
||||
{
|
||||
await _channel.SendErrorAsync(user.Mention + GetText("ttt_against_yourself")).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_users[1] = user;
|
||||
|
||||
_phase = Phase.Started;
|
||||
|
||||
_timeoutTimer = new Timer(async (_) =>
|
||||
{
|
||||
await _moveLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_phase == Phase.Ended)
|
||||
return;
|
||||
|
||||
_phase = Phase.Ended;
|
||||
if (_users[1] != null)
|
||||
{
|
||||
_winner = _users[_curUserIndex ^= 1];
|
||||
var del = _previousMessage?.DeleteAsync();
|
||||
try
|
||||
{
|
||||
await _channel.EmbedAsync(GetEmbed(GetText("ttt_time_expired"))).ConfigureAwait(false);
|
||||
if (del != null)
|
||||
await del.ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
OnEnded?.Invoke(this);
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
_moveLock.Release();
|
||||
}
|
||||
}, null, _options.TurnTimer * 1000, Timeout.Infinite);
|
||||
|
||||
_client.MessageReceived += Client_MessageReceived;
|
||||
|
||||
|
||||
_previousMessage = await _channel.EmbedAsync(GetEmbed(GetText("game_started"))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private bool IsDraw()
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
for (var j = 0; j < 3; j++)
|
||||
{
|
||||
if (_state[i, j] == null)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Task Client_MessageReceived(SocketMessage msg)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
await _moveLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var curUser = _users[_curUserIndex];
|
||||
if (_phase == Phase.Ended || msg.Author?.Id != curUser.Id)
|
||||
return;
|
||||
|
||||
if (int.TryParse(msg.Content, out var index) &&
|
||||
--index >= 0 &&
|
||||
index <= 9 &&
|
||||
_state[index / 3, index % 3] == null)
|
||||
{
|
||||
_state[index / 3, index % 3] = _curUserIndex;
|
||||
|
||||
// i'm lazy
|
||||
if (_state[index / 3, 0] == _state[index / 3, 1] && _state[index / 3, 1] == _state[index / 3, 2])
|
||||
{
|
||||
_state[index / 3, 0] = _curUserIndex + 2;
|
||||
_state[index / 3, 1] = _curUserIndex + 2;
|
||||
_state[index / 3, 2] = _curUserIndex + 2;
|
||||
|
||||
_phase = Phase.Ended;
|
||||
}
|
||||
else if (_state[0, index % 3] == _state[1, index % 3] && _state[1, index % 3] == _state[2, index % 3])
|
||||
{
|
||||
_state[0, index % 3] = _curUserIndex + 2;
|
||||
_state[1, index % 3] = _curUserIndex + 2;
|
||||
_state[2, index % 3] = _curUserIndex + 2;
|
||||
|
||||
_phase = Phase.Ended;
|
||||
}
|
||||
else if (_curUserIndex == _state[0, 0] && _state[0, 0] == _state[1, 1] && _state[1, 1] == _state[2, 2])
|
||||
{
|
||||
_state[0, 0] = _curUserIndex + 2;
|
||||
_state[1, 1] = _curUserIndex + 2;
|
||||
_state[2, 2] = _curUserIndex + 2;
|
||||
|
||||
_phase = Phase.Ended;
|
||||
}
|
||||
else if (_curUserIndex == _state[0, 2] && _state[0, 2] == _state[1, 1] && _state[1, 1] == _state[2, 0])
|
||||
{
|
||||
_state[0, 2] = _curUserIndex + 2;
|
||||
_state[1, 1] = _curUserIndex + 2;
|
||||
_state[2, 0] = _curUserIndex + 2;
|
||||
|
||||
_phase = Phase.Ended;
|
||||
}
|
||||
var reason = "";
|
||||
|
||||
if (_phase == Phase.Ended) // if user won, stop receiving moves
|
||||
{
|
||||
reason = GetText("ttt_matched_three");
|
||||
_winner = _users[_curUserIndex];
|
||||
_client.MessageReceived -= Client_MessageReceived;
|
||||
OnEnded?.Invoke(this);
|
||||
}
|
||||
else if (IsDraw())
|
||||
{
|
||||
reason = GetText("ttt_a_draw");
|
||||
_phase = Phase.Ended;
|
||||
_client.MessageReceived -= Client_MessageReceived;
|
||||
OnEnded?.Invoke(this);
|
||||
}
|
||||
|
||||
var sendstate = Task.Run(async () =>
|
||||
{
|
||||
var del1 = msg.DeleteAsync();
|
||||
var del2 = _previousMessage?.DeleteAsync();
|
||||
try { _previousMessage = await _channel.EmbedAsync(GetEmbed(reason)).ConfigureAwait(false); } catch { }
|
||||
try { await del1.ConfigureAwait(false); } catch { }
|
||||
try { if (del2 != null) await del2.ConfigureAwait(false); } catch { }
|
||||
});
|
||||
_curUserIndex ^= 1;
|
||||
|
||||
_timeoutTimer.Change(_options.TurnTimer * 1000, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_moveLock.Release();
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
283
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaGame.cs
Normal file
283
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaGame.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Modules.Games.Common.Trivia;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaGame
|
||||
{
|
||||
private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1);
|
||||
private readonly IDataCache _cache;
|
||||
private readonly IBotStrings _strings;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly GamesConfig _config;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly TriviaOptions _options;
|
||||
|
||||
public IGuild Guild { get; }
|
||||
public ITextChannel Channel { get; }
|
||||
|
||||
private CancellationTokenSource _triviaCancelSource;
|
||||
|
||||
public TriviaQuestion CurrentQuestion { get; private set; }
|
||||
public HashSet<TriviaQuestion> OldQuestions { get; } = new HashSet<TriviaQuestion>();
|
||||
|
||||
public ConcurrentDictionary<IGuildUser, int> Users { get; } = new ConcurrentDictionary<IGuildUser, int>();
|
||||
|
||||
public bool GameActive { get; private set; }
|
||||
public bool ShouldStopGame { get; private set; }
|
||||
|
||||
private readonly TriviaQuestionPool _questionPool;
|
||||
private int _timeoutCount = 0;
|
||||
private readonly string _quitCommand;
|
||||
|
||||
public TriviaGame(IBotStrings strings, DiscordSocketClient client, GamesConfig config,
|
||||
IDataCache cache, ICurrencyService cs, IGuild guild, ITextChannel channel,
|
||||
TriviaOptions options, string quitCommand)
|
||||
{
|
||||
_cache = cache;
|
||||
_questionPool = new TriviaQuestionPool(_cache);
|
||||
_strings = strings;
|
||||
_client = client;
|
||||
_config = config;
|
||||
_cs = cs;
|
||||
_options = options;
|
||||
_quitCommand = quitCommand;
|
||||
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
}
|
||||
|
||||
private string GetText(string key, params object[] replacements)
|
||||
=> _strings.GetText(key, Channel.GuildId, replacements);
|
||||
|
||||
public async Task StartGame()
|
||||
{
|
||||
var showHowToQuit = false;
|
||||
while (!ShouldStopGame)
|
||||
{
|
||||
// reset the cancellation source
|
||||
_triviaCancelSource = new CancellationTokenSource();
|
||||
showHowToQuit = !showHowToQuit;
|
||||
|
||||
// load question
|
||||
CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.IsPokemon);
|
||||
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
|
||||
{
|
||||
await Channel.SendErrorAsync(GetText("trivia_game"), GetText("failed_loading_question")).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again
|
||||
|
||||
EmbedBuilder questionEmbed;
|
||||
IUserMessage questionMessage;
|
||||
try
|
||||
{
|
||||
questionEmbed = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.AddField(eab => eab.WithName(GetText("category")).WithValue(CurrentQuestion.Category))
|
||||
.AddField(eab => eab.WithName(GetText("question")).WithValue(CurrentQuestion.Question));
|
||||
|
||||
if (showHowToQuit)
|
||||
questionEmbed.WithFooter(GetText("trivia_quit", _quitCommand));
|
||||
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute))
|
||||
questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl);
|
||||
|
||||
questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound ||
|
||||
ex.HttpCode == System.Net.HttpStatusCode.Forbidden ||
|
||||
ex.HttpCode == System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error sending trivia embed");
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
//receive messages
|
||||
try
|
||||
{
|
||||
_client.MessageReceived += PotentialGuess;
|
||||
|
||||
//allow people to guess
|
||||
GameActive = true;
|
||||
try
|
||||
{
|
||||
//hint
|
||||
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false);
|
||||
if (!_options.NoHint)
|
||||
try
|
||||
{
|
||||
await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(efb => efb.WithText(CurrentQuestion.GetHint())).Build())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) { Log.Warning(ex, "Error editing triva message"); }
|
||||
|
||||
//timeout
|
||||
await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (TaskCanceledException) { _timeoutCount = 0; } //means someone guessed the answer
|
||||
}
|
||||
finally
|
||||
{
|
||||
GameActive = false;
|
||||
_client.MessageReceived -= PotentialGuess;
|
||||
}
|
||||
if (!_triviaCancelSource.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var embed = new EmbedBuilder().WithErrorColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.WithDescription(GetText("trivia_times_up", Format.Bold(CurrentQuestion.Answer)));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
|
||||
await Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
|
||||
if (_options.Timeout != 0 && ++_timeoutCount >= _options.Timeout)
|
||||
await StopGame().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error sending trivia time's up message");
|
||||
}
|
||||
}
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureStopped()
|
||||
{
|
||||
ShouldStopGame = true;
|
||||
|
||||
await Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName("Trivia Game Ended"))
|
||||
.WithTitle("Final Results")
|
||||
.WithDescription(GetLeaderboard())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StopGame()
|
||||
{
|
||||
var old = ShouldStopGame;
|
||||
ShouldStopGame = true;
|
||||
if (!old)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Channel.SendConfirmAsync(GetText("trivia_game"), GetText("trivia_stopping"))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error sending trivia stopping message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task PotentialGuess(SocketMessage imsg)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (imsg.Author.IsBot)
|
||||
return;
|
||||
|
||||
var umsg = imsg as SocketUserMessage;
|
||||
|
||||
var textChannel = umsg?.Channel as ITextChannel;
|
||||
if (textChannel == null || textChannel.Guild != Guild)
|
||||
return;
|
||||
|
||||
var guildUser = (IGuildUser)umsg.Author;
|
||||
|
||||
var guess = false;
|
||||
await _guessLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (GameActive && CurrentQuestion.IsAnswerCorrect(umsg.Content) && !_triviaCancelSource.IsCancellationRequested)
|
||||
{
|
||||
Users.AddOrUpdate(guildUser, 1, (gu, old) => ++old);
|
||||
guess = true;
|
||||
}
|
||||
}
|
||||
finally { _guessLock.Release(); }
|
||||
if (!guess) return;
|
||||
_triviaCancelSource.Cancel();
|
||||
|
||||
|
||||
if (_options.WinRequirement != 0 && Users[guildUser] == _options.WinRequirement)
|
||||
{
|
||||
ShouldStopGame = true;
|
||||
try
|
||||
{
|
||||
var embedS = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.WithDescription(GetText("trivia_win",
|
||||
guildUser.Mention,
|
||||
Format.Bold(CurrentQuestion.Answer)));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
await Channel.EmbedAsync(embedS).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
var reward = _config.Trivia.CurrencyReward;
|
||||
if (reward > 0)
|
||||
await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var embed = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.WithDescription(GetText("trivia_guess", guildUser.Mention, Format.Bold(CurrentQuestion.Answer)));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
await Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) { Log.Warning(ex.ToString()); }
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetLeaderboard()
|
||||
{
|
||||
if (Users.Count == 0)
|
||||
return GetText("no_results");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value))
|
||||
{
|
||||
sb.AppendLine(GetText("trivia_points", Format.Bold(kvp.Key.ToString()), kvp.Value).SnPl(kvp.Value));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
30
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaOptions.cs
Normal file
30
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaOptions.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using CommandLine;
|
||||
using NadekoBot.Core.Common;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaOptions : INadekoCommandOptions
|
||||
{
|
||||
[Option('p', "pokemon", Required = false, Default = false, HelpText = "Whether it's 'Who's that pokemon?' trivia.")]
|
||||
public bool IsPokemon { get; set; } = false;
|
||||
[Option("nohint", Required = false, Default = false, HelpText = "Don't show any hints.")]
|
||||
public bool NoHint { get; set; } = false;
|
||||
[Option('w', "win-req", Required = false, Default = 10, HelpText = "Winning requirement. Set 0 for an infinite game. Default 10.")]
|
||||
public int WinRequirement { get; set; } = 10;
|
||||
[Option('q', "question-timer", Required = false, Default = 30, HelpText = "How long until the question ends. Default 30.")]
|
||||
public int QuestionTimer { get; set; } = 30;
|
||||
[Option('t', "timeout", Required = false, Default = 10, HelpText = "Number of questions of inactivity in order stop. Set 0 for never. Default 10.")]
|
||||
public int Timeout { get; set; } = 10;
|
||||
|
||||
public void NormalizeOptions()
|
||||
{
|
||||
if (WinRequirement < 0)
|
||||
WinRequirement = 10;
|
||||
if (QuestionTimer < 10 || QuestionTimer > 300)
|
||||
QuestionTimer = 30;
|
||||
if (Timeout < 0 || Timeout > 20)
|
||||
Timeout = 10;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
110
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaQuestion.cs
Normal file
110
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaQuestion.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
// THANKS @ShoMinamimoto for suggestions and coding help
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaQuestion
|
||||
{
|
||||
//represents the min size to judge levDistance with
|
||||
private static readonly HashSet<Tuple<int, int>> strictness = new HashSet<Tuple<int, int>> {
|
||||
new Tuple<int, int>(9, 0),
|
||||
new Tuple<int, int>(14, 1),
|
||||
new Tuple<int, int>(19, 2),
|
||||
new Tuple<int, int>(22, 3),
|
||||
};
|
||||
public 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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Extensions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaQuestionPool
|
||||
{
|
||||
private readonly IDataCache _cache;
|
||||
private readonly int maxPokemonId;
|
||||
|
||||
private readonly NadekoRandom _rng = new NadekoRandom();
|
||||
|
||||
private TriviaQuestion[] Pool => _cache.LocalData.TriviaQuestions;
|
||||
private IReadOnlyDictionary<int, string> Map => _cache.LocalData.PokemonMap;
|
||||
|
||||
public TriviaQuestionPool(IDataCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
maxPokemonId = 721; //xd
|
||||
}
|
||||
|
||||
public TriviaQuestion GetRandomQuestion(HashSet<TriviaQuestion> exclude, bool isPokemon)
|
||||
{
|
||||
if (Pool.Length == 0)
|
||||
return null;
|
||||
|
||||
if (isPokemon)
|
||||
{
|
||||
var num = _rng.Next(1, maxPokemonId + 1);
|
||||
return new TriviaQuestion("Who's That Pokémon?",
|
||||
Map[num].ToTitleCase(),
|
||||
"Pokemon",
|
||||
$@"https://nadeko.bot/images/pokemon/shadows/{num}.png",
|
||||
$@"https://nadeko.bot/images/pokemon/real/{num}.png");
|
||||
}
|
||||
TriviaQuestion randomQuestion;
|
||||
while (exclude.Contains(randomQuestion = Pool[_rng.Next(0, Pool.Length)])) ;
|
||||
|
||||
return randomQuestion;
|
||||
}
|
||||
}
|
||||
}
|
9
NadekoBot.Core/Modules/Games/Common/TypingArticle.cs
Normal file
9
NadekoBot.Core/Modules/Games/Common/TypingArticle.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class TypingArticle
|
||||
{
|
||||
public string Source { get; set; }
|
||||
public string Extra { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
190
NadekoBot.Core/Modules/Games/Common/TypingGame.cs
Normal file
190
NadekoBot.Core/Modules/Games/Common/TypingGame.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using CommandLine;
|
||||
using NadekoBot.Core.Common;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class TypingGame
|
||||
{
|
||||
public class Options : INadekoCommandOptions
|
||||
{
|
||||
[Option('s', "start-time", Default = 5, Required = false, HelpText = "How long does it take for the race to start. Default 5.")]
|
||||
public int StartTime { get; set; } = 5;
|
||||
|
||||
public void NormalizeOptions()
|
||||
{
|
||||
if (StartTime < 3 || StartTime > 30)
|
||||
StartTime = 5;
|
||||
}
|
||||
}
|
||||
|
||||
public const float WORD_VALUE = 4.5f;
|
||||
public ITextChannel Channel { get; }
|
||||
public string CurrentSentence { get; private set; }
|
||||
public bool IsActive { get; private set; }
|
||||
private readonly Stopwatch sw;
|
||||
private readonly List<ulong> finishedUserIds;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly GamesService _games;
|
||||
private readonly string _prefix;
|
||||
private readonly Options _options;
|
||||
|
||||
public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel,
|
||||
string prefix, Options options)
|
||||
{
|
||||
_games = games;
|
||||
_client = client;
|
||||
_prefix = prefix;
|
||||
_options = options;
|
||||
|
||||
this.Channel = channel;
|
||||
IsActive = false;
|
||||
sw = new Stopwatch();
|
||||
finishedUserIds = new List<ulong>();
|
||||
}
|
||||
|
||||
public async Task<bool> Stop()
|
||||
{
|
||||
if (!IsActive) return false;
|
||||
_client.MessageReceived -= AnswerReceived;
|
||||
finishedUserIds.Clear();
|
||||
IsActive = false;
|
||||
sw.Stop();
|
||||
sw.Reset();
|
||||
try
|
||||
{
|
||||
await Channel.SendConfirmAsync("Typing contest stopped.").ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex.ToString());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
{
|
||||
if (IsActive) return; // can't start running game
|
||||
IsActive = true;
|
||||
CurrentSentence = GetRandomSentence();
|
||||
var i = (int)(CurrentSentence.Length / WORD_VALUE * 1.7f);
|
||||
try
|
||||
{
|
||||
await Channel.SendConfirmAsync($@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can.").ConfigureAwait(false);
|
||||
|
||||
|
||||
var time = _options.StartTime;
|
||||
|
||||
var msg = await Channel.SendMessageAsync($"Starting new typing contest in **{time}**...", options: new RequestOptions()
|
||||
{
|
||||
RetryMode = RetryMode.AlwaysRetry
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
do
|
||||
{
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
time -= 2;
|
||||
try { await msg.ModifyAsync(m => m.Content = $"Starting new typing contest in **{time}**..").ConfigureAwait(false); } catch { }
|
||||
} while (time > 2);
|
||||
|
||||
await msg.ModifyAsync(m => {
|
||||
m.Content = CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture);
|
||||
}).ConfigureAwait(false);
|
||||
sw.Start();
|
||||
HandleAnswers();
|
||||
|
||||
while (i > 0)
|
||||
{
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
i--;
|
||||
if (!IsActive)
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
await Stop().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetRandomSentence()
|
||||
{
|
||||
if (_games.TypingArticles.Any())
|
||||
return _games.TypingArticles[new NadekoRandom().Next(0, _games.TypingArticles.Count)].Text;
|
||||
else
|
||||
return $"No typing articles found. Use {_prefix}typeadd command to add a new article for typing.";
|
||||
|
||||
}
|
||||
|
||||
private void HandleAnswers()
|
||||
{
|
||||
_client.MessageReceived += AnswerReceived;
|
||||
}
|
||||
|
||||
private Task AnswerReceived(SocketMessage imsg)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (imsg.Author.IsBot)
|
||||
return;
|
||||
var msg = imsg as SocketUserMessage;
|
||||
if (msg == null)
|
||||
return;
|
||||
|
||||
if (this.Channel == null || this.Channel.Id != msg.Channel.Id) return;
|
||||
|
||||
var guess = msg.Content;
|
||||
|
||||
var distance = CurrentSentence.LevenshteinDistance(guess);
|
||||
var decision = Judge(distance, guess.Length);
|
||||
if (decision && !finishedUserIds.Contains(msg.Author.Id))
|
||||
{
|
||||
var elapsed = sw.Elapsed;
|
||||
var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60;
|
||||
finishedUserIds.Add(msg.Author.Id);
|
||||
await this.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithTitle($"{msg.Author} finished the race!")
|
||||
.AddField(efb =>
|
||||
efb.WithName("Place").WithValue($"#{finishedUserIds.Count}").WithIsInline(true))
|
||||
.AddField(efb =>
|
||||
efb.WithName("WPM").WithValue($"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*")
|
||||
.WithIsInline(true))
|
||||
.AddField(efb =>
|
||||
efb.WithName("Errors").WithValue(distance.ToString()).WithIsInline(true)))
|
||||
.ConfigureAwait(false);
|
||||
if (finishedUserIds.Count % 4 == 0)
|
||||
{
|
||||
await this.Channel.SendConfirmAsync(
|
||||
$":exclamation: A lot of people finished, here is the text for those still typing:" +
|
||||
$"\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture)).SanitizeMentions(true)}**")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex.ToString());
|
||||
}
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static bool Judge(int errors, int textLength) => errors <= textLength / 25;
|
||||
|
||||
}
|
||||
}
|
1
NadekoBot.Core/Modules/Games/Connect4Commands.cs
Normal file
1
NadekoBot.Core/Modules/Games/Connect4Commands.cs
Normal file
@@ -0,0 +1 @@
|
||||
|
164
NadekoBot.Core/Modules/Games/Games.cs
Normal file
164
NadekoBot.Core/Modules/Games/Games.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Core.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
|
||||
{
|
||||
/* 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)
|
||||
{
|
||||
_images = data.LocalImages;
|
||||
_httpFactory = factory;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 ctx.Channel.SendConfirmAsync("🤔", listArr[rng.Next(0, listArr.Length)]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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(new EmbedBuilder().WithColor(NadekoBot.OkColor)
|
||||
.WithDescription(ctx.User.ToString())
|
||||
.AddField(efb => efb.WithName("❓ " + GetText("question")).WithValue(question).WithIsInline(false))
|
||||
.AddField("🎱 " + GetText("8ball"), res, false));
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 == null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("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: new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.AddField(efb => efb.WithName("Hot").WithValue(gr.Hot.ToString("F2")).WithIsInline(true))
|
||||
.AddField(efb => efb.WithName("Crazy").WithValue(gr.Crazy.ToString("F2")).WithIsInline(true))
|
||||
.AddField(efb => efb.WithName("Advice").WithValue(gr.Advice).WithIsInline(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, Usage, Description, Aliases]
|
||||
public async Task Linux(string guhnoo, string loonix)
|
||||
{
|
||||
await ctx.Channel.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);
|
||||
}
|
||||
}
|
||||
}
|
144
NadekoBot.Core/Modules/Games/HangmanCommands.cs
Normal file
144
NadekoBot.Core/Modules/Games/HangmanCommands.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Extensions;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Modules.Games.Common.Hangman;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using NadekoBot.Modules.Games.Common.Hangman.Exceptions;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class HangmanCommands : NadekoSubmodule<GamesService>
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public HangmanCommands(DiscordSocketClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Hangmanlist()
|
||||
{
|
||||
await ctx.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join("\n", _service.TermPool.Data.Keys)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Hangman([Leftover]string type = "random")
|
||||
{
|
||||
Hangman hm;
|
||||
try
|
||||
{
|
||||
hm = new Hangman(type, _service.TermPool);
|
||||
}
|
||||
catch (TermNotFoundException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_service.HangmanGames.TryAdd(ctx.Channel.Id, hm))
|
||||
{
|
||||
hm.Dispose();
|
||||
await ReplyErrorLocalizedAsync("hangman_running").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
hm.OnGameEnded += Hm_OnGameEnded;
|
||||
hm.OnGuessFailed += Hm_OnGuessFailed;
|
||||
hm.OnGuessSucceeded += Hm_OnGuessSucceeded;
|
||||
hm.OnLetterAlreadyUsed += Hm_OnLetterAlreadyUsed;
|
||||
_client.MessageReceived += _client_MessageReceived;
|
||||
|
||||
try
|
||||
{
|
||||
await ctx.Channel.SendConfirmAsync(GetText("hangman_game_started") + $" ({hm.TermType})",
|
||||
hm.ScrambledWord + "\n" + hm.GetHangman())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
|
||||
await hm.EndedTask.ConfigureAwait(false);
|
||||
|
||||
_client.MessageReceived -= _client_MessageReceived;
|
||||
_service.HangmanGames.TryRemove(ctx.Channel.Id, out _);
|
||||
hm.Dispose();
|
||||
|
||||
Task _client_MessageReceived(SocketMessage msg)
|
||||
{
|
||||
var _ = Task.Run(() =>
|
||||
{
|
||||
if (ctx.Channel.Id == msg.Channel.Id && !msg.Author.IsBot)
|
||||
return hm.Input(msg.Author.Id, msg.Author.ToString(), msg.Content);
|
||||
else
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
Task Hm_OnGameEnded(Hangman game, string winner)
|
||||
{
|
||||
if (winner == null)
|
||||
{
|
||||
var loseEmbed = new EmbedBuilder().WithTitle($"Hangman Game ({game.TermType}) - Ended")
|
||||
.WithDescription(Format.Bold("You lose."))
|
||||
.AddField(efb => efb.WithName("It was").WithValue(game.Term.GetWord()))
|
||||
.WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses)))
|
||||
.WithErrorColor();
|
||||
|
||||
if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute))
|
||||
loseEmbed.WithImageUrl(game.Term.ImageUrl);
|
||||
|
||||
return ctx.Channel.EmbedAsync(loseEmbed);
|
||||
}
|
||||
|
||||
var winEmbed = new EmbedBuilder().WithTitle($"Hangman Game ({game.TermType}) - Ended")
|
||||
.WithDescription(Format.Bold($"{winner} Won."))
|
||||
.AddField(efb => efb.WithName("It was").WithValue(game.Term.GetWord()))
|
||||
.WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses)))
|
||||
.WithOkColor();
|
||||
|
||||
if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute))
|
||||
winEmbed.WithImageUrl(game.Term.ImageUrl);
|
||||
|
||||
return ctx.Channel.EmbedAsync(winEmbed);
|
||||
}
|
||||
|
||||
private Task Hm_OnLetterAlreadyUsed(Hangman game, string user, char guess)
|
||||
{
|
||||
return ctx.Channel.SendErrorAsync($"Hangman Game ({game.TermType})", $"{user} Letter `{guess}` has already been used. You can guess again in 3 seconds.\n" + game.ScrambledWord + "\n" + game.GetHangman(),
|
||||
footer: string.Join(" ", game.PreviousGuesses));
|
||||
}
|
||||
|
||||
private Task Hm_OnGuessSucceeded(Hangman game, string user, char guess)
|
||||
{
|
||||
return ctx.Channel.SendConfirmAsync($"Hangman Game ({game.TermType})", $"{user} guessed a letter `{guess}`!\n" + game.ScrambledWord + "\n" + game.GetHangman(),
|
||||
footer: string.Join(" ", game.PreviousGuesses));
|
||||
}
|
||||
|
||||
private Task Hm_OnGuessFailed(Hangman game, string user, char guess)
|
||||
{
|
||||
return ctx.Channel.SendErrorAsync($"Hangman Game ({game.TermType})", $"{user} Letter `{guess}` does not exist. You can guess again in 3 seconds.\n" + game.ScrambledWord + "\n" + game.GetHangman(),
|
||||
footer: string.Join(" ", game.PreviousGuesses));
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task HangmanStop()
|
||||
{
|
||||
if (_service.HangmanGames.TryRemove(ctx.Channel.Id, out var removed))
|
||||
{
|
||||
await removed.Stop().ConfigureAwait(false);
|
||||
await ReplyConfirmLocalizedAsync("hangman_stopped").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
126
NadekoBot.Core/Modules/Games/NunchiCommands.cs
Normal file
126
NadekoBot.Core/Modules/Games/NunchiCommands.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
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
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class NunchiCommands : NadekoSubmodule<GamesService>
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public NunchiCommands(DiscordSocketClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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))
|
||||
{
|
||||
// if you failed joining, that means game is running or just ended
|
||||
// await ReplyErrorLocalized("nunchi_already_started").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyConfirmLocalizedAsync("nunchi_joined", nunchi.ParticipantCount).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try { await ConfirmLocalizedAsync("nunchi_created").ConfigureAwait(false); } 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("nunchi_failed_to_start").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Task _client_MessageReceived(SocketMessage arg)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
if (arg2 == null)
|
||||
return ConfirmLocalizedAsync("nunchi_ended_no_winner", Format.Bold(arg2));
|
||||
else
|
||||
return ConfirmLocalizedAsync("nunchi_ended", Format.Bold(arg2));
|
||||
}
|
||||
}
|
||||
|
||||
private Task Nunchi_OnRoundStarted(NunchiGame arg, int cur)
|
||||
{
|
||||
return ConfirmLocalizedAsync("nunchi_round_started",
|
||||
Format.Bold(arg.ParticipantCount.ToString()),
|
||||
Format.Bold(cur.ToString()));
|
||||
}
|
||||
|
||||
private Task Nunchi_OnUserGuessed(NunchiGame arg)
|
||||
{
|
||||
return ConfirmLocalizedAsync("nunchi_next_number", Format.Bold(arg.CurrentNumber.ToString()));
|
||||
}
|
||||
|
||||
private Task Nunchi_OnRoundEnded(NunchiGame arg1, (ulong Id, string Name)? arg2)
|
||||
{
|
||||
if(arg2.HasValue)
|
||||
return ConfirmLocalizedAsync("nunchi_round_ended", Format.Bold(arg2.Value.Name));
|
||||
else
|
||||
return ConfirmLocalizedAsync("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("nunchi_started", Format.Bold(arg.ParticipantCount.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
127
NadekoBot.Core/Modules/Games/PlantAndPickCommands.cs
Normal file
127
NadekoBot.Core/Modules/Games/PlantAndPickCommands.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Administration.Services;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Modules.Gambling.Common;
|
||||
using NadekoBot.Core.Modules.Gambling.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class PlantPickCommands : GamblingSubmodule<PlantPickService>
|
||||
{
|
||||
private readonly LogCommandService logService;
|
||||
|
||||
public PlantPickCommands(LogCommandService logService, GamblingConfigService gss) : base(gss)
|
||||
{
|
||||
this.logService = logService;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Pick(string pass = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var picked = await _service.PickAsync(ctx.Guild.Id, (ITextChannel)ctx.Channel, ctx.User.Id, pass);
|
||||
|
||||
if (picked > 0)
|
||||
{
|
||||
var msg = await ReplyConfirmLocalizedAsync("picked", picked + CurrencySign)
|
||||
.ConfigureAwait(false);
|
||||
msg.DeleteAfter(10);
|
||||
}
|
||||
|
||||
if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages)
|
||||
{
|
||||
try
|
||||
{
|
||||
logService.AddDeleteIgnore(ctx.Message.Id);
|
||||
await ctx.Message.DeleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Plant(int amount = 1, string pass = null)
|
||||
{
|
||||
if (amount < 1)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var success = await _service.PlantAsync(ctx.Guild.Id, ctx.Channel, ctx.User.Id, ctx.User.ToString(), amount, pass);
|
||||
if (!success)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("not_enough", CurrencySign).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages)
|
||||
{
|
||||
logService.AddDeleteIgnore(ctx.Message.Id);
|
||||
await ctx.Message.DeleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
#if GLOBAL_NADEKO
|
||||
[OwnerOnly]
|
||||
#endif
|
||||
public async Task GenCurrency()
|
||||
{
|
||||
bool enabled = _service.ToggleCurrencyGeneration(ctx.Guild.Id, ctx.Channel.Id);
|
||||
if (enabled)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("curgen_enabled").ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("curgen_disabled").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[OwnerOnly]
|
||||
public Task GenCurList(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return Task.CompletedTask;
|
||||
var enabledIn = _service.GetAllGeneratingChannels();
|
||||
|
||||
return ctx.SendPaginatedConfirmAsync(page, (cur) =>
|
||||
{
|
||||
var items = enabledIn.Skip(page * 9).Take(9);
|
||||
|
||||
if (!items.Any())
|
||||
{
|
||||
return new EmbedBuilder().WithErrorColor()
|
||||
.WithDescription("-");
|
||||
}
|
||||
|
||||
return items.Aggregate(new EmbedBuilder().WithOkColor(),
|
||||
(eb, i) => eb.AddField(i.GuildId.ToString(), i.ChannelId));
|
||||
}, enabledIn.Count(), 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
124
NadekoBot.Core/Modules/Games/PollCommands.cs
Normal file
124
NadekoBot.Core/Modules/Games/PollCommands.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class PollCommands : NadekoSubmodule<PollService>
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public PollCommands(DiscordSocketClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 == null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("poll_invalid_input").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
if (_service.StartPoll(poll))
|
||||
{
|
||||
await ctx.Channel
|
||||
.EmbedAsync(new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText("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("poll_already_running").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("current_poll_results"))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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)) == null)
|
||||
return;
|
||||
|
||||
var embed = GetStats(p, GetText("poll_closed"));
|
||||
await ctx.Channel.EmbedAsync(embed)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public EmbedBuilder 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 = new EmbedBuilder().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("poll_result",
|
||||
Index + 1,
|
||||
Format.Bold(Text),
|
||||
Format.Bold(votes.ToString())));
|
||||
}
|
||||
|
||||
return eb.WithDescription(sb.ToString())
|
||||
.WithFooter(efb => efb.WithText(GetText("x_votes_cast", totalVotesCast)))
|
||||
.WithOkColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
149
NadekoBot.Core/Modules/Games/Services/ChatterbotService.cs
Normal file
149
NadekoBot.Core/Modules/Games/Services/ChatterbotService.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Permissions.Common;
|
||||
using NadekoBot.Modules.Permissions.Services;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
using System.Net.Http;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Services
|
||||
{
|
||||
public class ChatterBotService : IEarlyBehavior, INService
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly PermissionService _perms;
|
||||
private readonly CommandHandler _cmd;
|
||||
private readonly IBotStrings _strings;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
|
||||
|
||||
public int Priority => -1;
|
||||
public ModuleBehaviorType BehaviorType => ModuleBehaviorType.Executor;
|
||||
|
||||
public ChatterBotService(DiscordSocketClient client, PermissionService perms,
|
||||
NadekoBot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory,
|
||||
IBotCredentials creds)
|
||||
{
|
||||
_client = client;
|
||||
_perms = perms;
|
||||
_cmd = cmd;
|
||||
_strings = strings;
|
||||
_creds = creds;
|
||||
_httpFactory = factory;
|
||||
|
||||
ChatterBotGuilds = new ConcurrentDictionary<ulong, Lazy<IChatterBotSession>>(
|
||||
bot.AllGuildConfigs
|
||||
.Where(gc => gc.CleverbotEnabled)
|
||||
.ToDictionary(gc => gc.GuildId, gc => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
|
||||
}
|
||||
|
||||
public IChatterBotSession CreateSession()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
|
||||
return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory);
|
||||
else
|
||||
return new CleverbotIOSession("GAh3wUfzDCpDpdpT", "RStKgqn7tcO9blbrv4KbXM8NDlb7H37C", _httpFactory);
|
||||
}
|
||||
|
||||
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
|
||||
{
|
||||
var channel = msg.Channel as ITextChannel;
|
||||
cleverbot = null;
|
||||
|
||||
if (channel == 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;
|
||||
}
|
||||
|
||||
public static async Task<bool> TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message)
|
||||
{
|
||||
await channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
|
||||
var response = await cleverbot.Think(message).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await channel.SendConfirmAsync(response.SanitizeMentions(true)).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await channel.SendConfirmAsync(response.SanitizeMentions(true)).ConfigureAwait(false); // try twice :\
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> RunBehavior(DiscordSocketClient client, IGuild guild, IUserMessage usrMsg)
|
||||
{
|
||||
if (!(guild is SocketGuild sg))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var message = PrepareMessage(usrMsg, out IChatterBotSession cbs);
|
||||
if (message == null || cbs == null)
|
||||
return false;
|
||||
|
||||
var pc = _perms.GetCacheFor(guild.Id);
|
||||
if (!pc.Permissions.CheckPermissions(usrMsg,
|
||||
"cleverbot",
|
||||
"Games".ToLowerInvariant(),
|
||||
out int index))
|
||||
{
|
||||
if (pc.Verbose)
|
||||
{
|
||||
var returnMsg = _strings.GetText("trigger", guild.Id, index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild)));
|
||||
try { await usrMsg.Channel.SendErrorAsync(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
|
||||
Server: {guild.Name} [{guild.Id}]
|
||||
Channel: {usrMsg.Channel?.Name} [{usrMsg.Channel?.Id}]
|
||||
UserId: {usrMsg.Author} [{usrMsg.Author.Id}]
|
||||
Message: {usrMsg.Content}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,"Error in cleverbot");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
173
NadekoBot.Core/Modules/Games/Services/GamesConfigService.cs
Normal file
173
NadekoBot.Core/Modules/Games/Services/GamesConfigService.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Core.Common.Configs;
|
||||
using NadekoBot.Core.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");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// public sealed class GamesConfigMigrator : IConfigMigrator
|
||||
// {
|
||||
// // private readonly DbService _db;
|
||||
// private readonly GamesConfigService _gss;
|
||||
//
|
||||
// public GamesConfigMigrator(DbService dbService, GamesConfigService gss)
|
||||
// {
|
||||
// _log = LogManager.GetCurrentClassLogger();
|
||||
// _db = dbService;
|
||||
// _gss = gss;
|
||||
// }
|
||||
//
|
||||
// public void EnsureMigrated()
|
||||
// {
|
||||
// using var uow = _db.GetDbContext();
|
||||
// using var conn = uow._context.Database.GetDbConnection();
|
||||
// MigrateRaceAnimals(conn);
|
||||
// MigrateEightBall(conn);
|
||||
// }
|
||||
//
|
||||
// private void MigrateTrivia(DbConnection conn)
|
||||
// {
|
||||
// using (var checkTableCommand = conn.CreateCommand())
|
||||
// {
|
||||
// // make sure table still exists
|
||||
// checkTableCommand.CommandText =
|
||||
// "SELECT name FROM sqlite_master WHERE type='table' AND name='BotConfig';";
|
||||
// var checkReader = checkTableCommand.ExecuteReader();
|
||||
// if (!checkReader.HasRows)
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// Log.Information("Migrating trivia...");
|
||||
//
|
||||
// using var com = conn.CreateCommand();
|
||||
// com.CommandText = $@"SELECT MinimumTriviaWinReq, TriviaCurrencyReward FROM BotConfig";
|
||||
// using var reader = com.ExecuteReader();
|
||||
//
|
||||
// if (!reader.Read())
|
||||
// return;
|
||||
//
|
||||
// _gss.ModifyConfig(ModifyTriviaAction(reader));
|
||||
//
|
||||
// Log.Information("Trivia config migrated to data/games.yml");
|
||||
// }
|
||||
//
|
||||
// private static Action<GamesConfig> ModifyTriviaAction(DbDataReader reader)
|
||||
// => realConfig =>
|
||||
// {
|
||||
// var val = (int) (long) reader["MinimumTriviaWinReq"];
|
||||
// realConfig.Trivia.MinimumWinReq = val <= 0 ? 1 : val;
|
||||
// realConfig.Trivia.CurrencyReward = (long) reader["TriviaCurrencyReward"];
|
||||
// };
|
||||
//
|
||||
// private void MigrateEightBall(DbConnection conn)
|
||||
// {
|
||||
// using (var checkTableCommand = conn.CreateCommand())
|
||||
// {
|
||||
// // make sure table still exists
|
||||
// checkTableCommand.CommandText =
|
||||
// "SELECT name FROM sqlite_master WHERE type='table' AND name='EightBallResponses';";
|
||||
// var checkReader = checkTableCommand.ExecuteReader();
|
||||
// if (!checkReader.HasRows)
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// using (var com = conn.CreateCommand())
|
||||
// {
|
||||
// com.CommandText = $@"SELECT Text FROM EightBallResponses";
|
||||
// using var reader = com.ExecuteReader();
|
||||
//
|
||||
// if (!reader.Read())
|
||||
// return;
|
||||
//
|
||||
// Log.Information("Migrating eightball...");
|
||||
// _gss.ModifyConfig(Modify8ballAction(reader));
|
||||
// }
|
||||
//
|
||||
// Log.Information("Eightball migrated to data/games.yml");
|
||||
// MigrateTrivia(conn);
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
//
|
||||
// using var deleteEightBallCommand = conn.CreateCommand();
|
||||
// deleteEightBallCommand.CommandText = "DROP TABLE IF EXISTS EightBallResponses";
|
||||
// deleteEightBallCommand.ExecuteNonQuery();
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// private static Action<GamesConfig> Modify8ballAction(DbDataReader reader)
|
||||
// => realConfig =>
|
||||
// {
|
||||
// realConfig.EightBallResponses.Clear();
|
||||
// do
|
||||
// {
|
||||
// realConfig.EightBallResponses.Add((string) reader["Text"]);
|
||||
// } while (reader.Read());
|
||||
// };
|
||||
//
|
||||
// private void MigrateRaceAnimals(DbConnection conn)
|
||||
// {
|
||||
// using (var checkTableCommand = conn.CreateCommand())
|
||||
// {
|
||||
// // make sure table still exists
|
||||
// checkTableCommand.CommandText =
|
||||
// "SELECT name FROM sqlite_master WHERE type='table' AND name='RaceAnimals';";
|
||||
// var checkReader = checkTableCommand.ExecuteReader();
|
||||
// if (!checkReader.HasRows)
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// using (var com = conn.CreateCommand())
|
||||
// {
|
||||
// com.CommandText = $@"SELECT Name, Icon FROM RaceAnimals";
|
||||
// using var reader = com.ExecuteReader();
|
||||
//
|
||||
// if (!reader.Read())
|
||||
// return;
|
||||
//
|
||||
// Log.Information("Migrating race animals...");
|
||||
// _gss.ModifyConfig(ModifyRaceAnimalsAction(reader));
|
||||
// }
|
||||
//
|
||||
// Log.Information("Race animals migrated to data/games.yml");
|
||||
//
|
||||
// using var deleteRaceAnimalsCommand = conn.CreateCommand();
|
||||
// deleteRaceAnimalsCommand.CommandText = "DROP TABLE IF EXISTS RaceAnimals";
|
||||
// deleteRaceAnimalsCommand.ExecuteNonQuery();
|
||||
// }
|
||||
//
|
||||
// private static Action<GamesConfig> ModifyRaceAnimalsAction(DbDataReader reader)
|
||||
// => realConfig =>
|
||||
// {
|
||||
// realConfig.RaceAnimals.Clear();
|
||||
//
|
||||
// do
|
||||
// {
|
||||
// realConfig.RaceAnimals.Add(new RaceAnimal()
|
||||
// {
|
||||
// Icon = (string) reader["Icon"],
|
||||
// Name = (string) reader["Name"]
|
||||
// });
|
||||
// } while (reader.Read());
|
||||
// };
|
||||
// }
|
||||
}
|
159
NadekoBot.Core/Modules/Games/Services/GamesService.cs
Normal file
159
NadekoBot.Core/Modules/Games/Services/GamesService.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using Discord;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
using NadekoBot.Modules.Games.Common.Acrophobia;
|
||||
using NadekoBot.Modules.Games.Common.Hangman;
|
||||
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.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Services
|
||||
{
|
||||
public class GamesService : INService, IUnloadableService
|
||||
{
|
||||
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, Hangman> HangmanGames { get; } = new ConcurrentDictionary<ulong, Hangman>();
|
||||
public TermPool TermPool { get; } = new TermPool();
|
||||
|
||||
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 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 GamesService(GamesConfigService gamesConfig, IHttpClientFactory httpFactory)
|
||||
{
|
||||
_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 async Task Unload()
|
||||
{
|
||||
_t.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
AcrophobiaGames.ForEach(x => x.Value.Dispose());
|
||||
AcrophobiaGames.Clear();
|
||||
HangmanGames.ForEach(x => x.Value.Dispose());
|
||||
HangmanGames.Clear();
|
||||
await Task.WhenAll(RunningTrivias.Select(x => x.Value.StopGame())).ConfigureAwait(false);
|
||||
RunningTrivias.Clear();
|
||||
|
||||
TicTacToeGames.Clear();
|
||||
|
||||
await Task.WhenAll(RunningContests.Select(x => x.Value.Stop()))
|
||||
.ConfigureAwait(false);
|
||||
RunningContests.Clear();
|
||||
NunchiGames.ForEach(x => x.Value.Dispose());
|
||||
NunchiGames.Clear();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
127
NadekoBot.Core/Modules/Games/Services/PollService.cs
Normal file
127
NadekoBot.Core/Modules/Games/Services/PollService.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Common.Collections;
|
||||
using NadekoBot.Extensions;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Services
|
||||
{
|
||||
public class PollService : IEarlyBehavior, INService
|
||||
{
|
||||
public ConcurrentDictionary<ulong, PollRunner> ActivePolls { get; } = new ConcurrentDictionary<ulong, PollRunner>();
|
||||
|
||||
public int Priority => -5;
|
||||
public ModuleBehaviorType BehaviorType => ModuleBehaviorType.Executor;
|
||||
|
||||
private readonly DbService _db;
|
||||
private readonly IBotStrings _strs;
|
||||
|
||||
public PollService(DbService db, IBotStrings strs)
|
||||
{
|
||||
_db = db;
|
||||
_strs = strs;
|
||||
|
||||
using (var uow = db.GetDbContext())
|
||||
{
|
||||
ActivePolls = uow.Polls.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())
|
||||
{
|
||||
uow.Polls.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.Polls.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(_strs.GetText("poll_voted",
|
||||
usr.Guild.Id, Format.Bold(usr.ToString())))
|
||||
.ConfigureAwait(false);
|
||||
toDelete.DeleteAfter(5);
|
||||
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
|
||||
}
|
||||
|
||||
public async Task<bool> RunBehavior(DiscordSocketClient client, IGuild guild, IUserMessage msg)
|
||||
{
|
||||
if (guild == null)
|
||||
return false;
|
||||
|
||||
if (!ActivePolls.TryGetValue(guild.Id, out var poll))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
return await poll.TryVote(msg).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error voting");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
123
NadekoBot.Core/Modules/Games/SpeedTypingCommands.cs
Normal file
123
NadekoBot.Core/Modules/Games/SpeedTypingCommands.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using Discord;
|
||||
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.Core.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class SpeedTypingCommands : NadekoSubmodule<GamesService>
|
||||
{
|
||||
private readonly GamesService _games;
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public SpeedTypingCommands(DiscordSocketClient client, GamesService games)
|
||||
{
|
||||
_games = games;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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(channel.Guild.Id, id => new TypingGame(_games, _client, channel, Prefix, options));
|
||||
|
||||
if (game.IsActive)
|
||||
{
|
||||
await channel.SendErrorAsync(
|
||||
$"Contest already running in " +
|
||||
$"{game.Channel.Mention} channel.")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await game.Start().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task TypeStop()
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
if (_service.RunningContests.TryRemove(channel.Guild.Id, out TypingGame game))
|
||||
{
|
||||
await game.Stop().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
await channel.SendErrorAsync("No contest to stop on this channel.").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
public async Task Typeadd([Leftover] string text)
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
_games.AddTypingArticle(ctx.User, text);
|
||||
|
||||
await channel.SendConfirmAsync("Added new article for typing game.").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Typelist(int page = 1)
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
|
||||
if (page < 1)
|
||||
return;
|
||||
|
||||
var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray();
|
||||
|
||||
if (!articles.Any())
|
||||
{
|
||||
await channel.SendErrorAsync($"{ctx.User.Mention} `No articles found on that page.`").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var i = (page - 1) * 15;
|
||||
await channel.SendConfirmAsync("List of articles for Type Race", string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}")))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
public async Task Typedel(int index)
|
||||
{
|
||||
var removed = _service.RemoveTypingArticle(--index);
|
||||
|
||||
if (removed is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithTitle($"Removed typing article #{index + 1}")
|
||||
.WithDescription(removed.Text.TrimTo(50))
|
||||
.WithOkColor();
|
||||
|
||||
await Context.Channel.EmbedAsync(embed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
NadekoBot.Core/Modules/Games/TicTacToeCommands.cs
Normal file
62
NadekoBot.Core/Modules/Games/TicTacToeCommands.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class TicTacToeCommands : NadekoSubmodule<GamesService>
|
||||
{
|
||||
private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1);
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public TicTacToeCommands(DiscordSocketClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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
|
||||
{
|
||||
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);
|
||||
_service.TicTacToeGames.Add(channel.Id, game);
|
||||
await ReplyConfirmLocalizedAsync("ttt_created").ConfigureAwait(false);
|
||||
|
||||
game.OnEnded += (g) =>
|
||||
{
|
||||
_service.TicTacToeGames.Remove(channel.Id);
|
||||
_sem.Dispose();
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sem.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
NadekoBot.Core/Modules/Games/TriviaCommands.cs
Normal file
102
NadekoBot.Core/Modules/Games/TriviaCommands.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Modules.Games.Common.Trivia;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Core.Modules.Games.Common.Trivia;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[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)
|
||||
{
|
||||
_cache = cache;
|
||||
_cs = cs;
|
||||
_gamesConfig = gamesConfig;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var trivia = new TriviaGame(Strings, _client, config, _cache, _cs, channel.Guild, channel, opts, Prefix + "tq");
|
||||
if (_service.RunningTrivias.TryAdd(channel.Guild.Id, trivia))
|
||||
{
|
||||
try
|
||||
{
|
||||
await trivia.StartGame().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_service.RunningTrivias.TryRemove(channel.Guild.Id, out trivia);
|
||||
await trivia.EnsureStopped().ConfigureAwait(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Channel.SendErrorAsync(GetText("trivia_already_running") + "\n" + trivia.CurrentQuestion)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Tl()
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
|
||||
if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia))
|
||||
{
|
||||
await channel.SendConfirmAsync(GetText("leaderboard"), trivia.GetLeaderboard()).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyErrorLocalizedAsync("trivia_none").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Tq()
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
|
||||
if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia))
|
||||
{
|
||||
await trivia.StopGame().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyErrorLocalizedAsync("trivia_none").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user