Removed module projects because it can't work like that atm. Commented out package commands.
This commit is contained in:
NadekoBot.Core
Migrations
20171013055008_clear-and-loadedpackage.Designer.cs20171013055008_clear-and-loadedpackage.csNadekoSqliteContextModelSnapshot.cs
Modules
Administration
Gambling
AnimalRacingCommands.cs
Common
CurrencyEventsCommands.csDiceRollCommands.csDrawCommands.csFlipCoinCommands.csFlowerShopCommands.csGambling.csServices
SlotCommands.csWaifuClaimCommands.csWheelOfFortuneCommands.csGames
AcropobiaCommands.csCleverBotCommands.csConnect4Commands.csGames.csHangmanCommands.csLeetCommands.csNunchiCommands.csPlantAndPickCommands.csPollCommands.cs
Common
Acrophobia
ChatterBot
ChatterBotResponse.csChatterBotSession.csCleverbotResponse.csIChatterBotSession.csOfficialCleverbotSession.cs
Connect4
GirlRating.csHangman
Nunchi
Poll.csTicTacToe.csTrivia
TypingArticle.csTypingGame.csServices
SpeedTypingCommands.csTicTacToeCommands.csTriviaCommands.csMusic
Common
Extensions
Music.csServices
Nsfw
Pokemon
Searches
AnimeSearchCommands.cs
Common
AnimeResult.csDefineModel.cs
Exceptions
GoogleSearchResult.csMagicItem.csMangaResult.csOmdbProvider.csOverwatchApiModel.csSearchImageCacher.csSearchPokemon.csStreamResponses.csTimeModels.csWeatherModels.csWikipediaApiModel.csWoWJoke.csExceptions
FeedCommands.csJokeCommands.csLoLCommands.csMemegenCommands.csOsuCommands.csOverwatchCommands.csPlaceCommands.csPokemonSearchCommands.csSearches.csServices
StreamNotificationCommands.csTranslatorCommands.csXkcdCommands.csUtility
BotConfigCommands.csCalcCommands.csCommandMapCommands.cs
Common
Extensions
InfoCommands.csNadekoBot.Modules.Searches.csprojPatreonCommands.csQuoteCommands.csRemindCommands.csRepeatCommands.csServices
CommandMapService.csConverterService.csMessageRepeaterService.csPatreonRewardsService.csRemindService.csStreamRoleService.csVerboseErrorsService.cs
StreamRoleCommands.csUnitConversionCommands.csUtility.csVerboseErrorCommands.csXp
Services
NadekoBot.Modules.Games
NadekoBot.slndocs
mkdocs.ymlsrc/NadekoBot
148
NadekoBot.Core/Modules/Games/AcropobiaCommands.cs
Normal file
148
NadekoBot.Core/Modules/Games/AcropobiaCommands.cs
Normal file
@ -0,0 +1,148 @@
|
||||
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;
|
||||
|
||||
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)]
|
||||
public async Task Acro(int submissionTime = 30)
|
||||
{
|
||||
if (submissionTime < 10 || submissionTime > 120)
|
||||
return;
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
|
||||
var game = new Acrophobia(submissionTime);
|
||||
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 ReplyErrorLocalized("acro_running").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Task _client_MessageReceived(SocketMessage msg)
|
||||
{
|
||||
if (msg.Channel.Id != Context.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(Acrophobia 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.SubmissionPhaseLength)));
|
||||
|
||||
return Context.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
private Task Game_OnUserVoted(string user)
|
||||
{
|
||||
return Context.Channel.SendConfirmAsync(
|
||||
GetText("acrophobia"),
|
||||
GetText("acro_vote_cast", Format.Bold(user)));
|
||||
}
|
||||
|
||||
private async Task Game_OnVotingStarted(Acrophobia game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> submissions)
|
||||
{
|
||||
if (submissions.Length == 0)
|
||||
{
|
||||
await Context.Channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_ended_no_sub"));
|
||||
return;
|
||||
}
|
||||
if (submissions.Length == 1)
|
||||
{
|
||||
await Context.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 Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task Game_OnEnded(Acrophobia game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> votes)
|
||||
{
|
||||
if (!votes.Any() || votes.All(x => x.Value == 0))
|
||||
{
|
||||
await Context.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 Context.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)]
|
||||
[RequireUserPermission(GuildPermission.ManageMessages)]
|
||||
public async Task Cleverbot()
|
||||
{
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
|
||||
if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _))
|
||||
{
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
uow.GuildConfigs.SetCleverbotEnabled(Context.Guild.Id, false);
|
||||
await uow.CompleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
await ReplyConfirmLocalized("cleverbot_disabled").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy<IChatterBotSession>(() => _service.CreateSession(), true));
|
||||
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
uow.GuildConfigs.SetCleverbotEnabled(Context.Guild.Id, true);
|
||||
await uow.CompleteAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ReplyConfirmLocalized("cleverbot_enabled").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
177
NadekoBot.Core/Modules/Games/Common/Acrophobia/Acrophobia.cs
Normal file
177
NadekoBot.Core/Modules/Games/Common/Acrophobia/Acrophobia.cs
Normal file
@ -0,0 +1,177 @@
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Extensions;
|
||||
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.Acrophobia
|
||||
{
|
||||
/// <summary>
|
||||
/// Platform-agnostic acrophobia game
|
||||
/// </summary>
|
||||
public class Acrophobia : IDisposable
|
||||
{
|
||||
private const int VotingPhaseLength = 30;
|
||||
|
||||
public enum Phase
|
||||
{
|
||||
Submission,
|
||||
Voting,
|
||||
Ended
|
||||
}
|
||||
|
||||
public enum UserInputResult
|
||||
{
|
||||
Submitted,
|
||||
SubmissionFailed,
|
||||
Voted,
|
||||
VotingFailed,
|
||||
Failed
|
||||
}
|
||||
|
||||
public int SubmissionPhaseLength { get; }
|
||||
|
||||
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);
|
||||
private readonly NadekoRandom _rng;
|
||||
|
||||
public event Func<Acrophobia, Task> OnStarted = delegate { return Task.CompletedTask; };
|
||||
public event Func<Acrophobia, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnVotingStarted = delegate { return Task.CompletedTask; };
|
||||
public event Func<string, Task> OnUserVoted = delegate { return Task.CompletedTask; };
|
||||
public event Func<Acrophobia, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnEnded = delegate { return Task.CompletedTask; };
|
||||
|
||||
private readonly HashSet<ulong> _usersWhoVoted = new HashSet<ulong>();
|
||||
|
||||
public Acrophobia(int submissionPhaseLength = 30)
|
||||
{
|
||||
_rng = new NadekoRandom();
|
||||
SubmissionPhaseLength = submissionPhaseLength;
|
||||
InitializeStartingLetters();
|
||||
}
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
await OnStarted(this).ConfigureAwait(false);
|
||||
await Task.Delay(SubmissionPhaseLength * 1000);
|
||||
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(VotingPhaseLength * 1000);
|
||||
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();
|
||||
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())) // 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,37 @@
|
||||
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 int _botId = 6;
|
||||
|
||||
public ChatterBotSession()
|
||||
{
|
||||
_chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64();
|
||||
}
|
||||
|
||||
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 = new HttpClient())
|
||||
{
|
||||
var res = await http.GetStringAsync(string.Format(apiEndpoint, message)).ConfigureAwait(false);
|
||||
var cbr = JsonConvert.DeserializeObject<ChatterBotResponse>(res);
|
||||
return cbr.BotSay.Replace("<br/>", "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
|
||||
{
|
||||
public class CleverbotResponse
|
||||
{
|
||||
public string Cs { get; set; }
|
||||
public string Output { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
|
||||
{
|
||||
public interface IChatterBotSession
|
||||
{
|
||||
Task<string> Think(string input);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot
|
||||
{
|
||||
public class OfficialCleverbotSession : IChatterBotSession
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
private string _cs = null;
|
||||
|
||||
private string queryString => $"https://www.cleverbot.com/getreply?key={_apiKey}" +
|
||||
"&wrapper=nadekobot" +
|
||||
"&input={0}" +
|
||||
"&cs={1}";
|
||||
|
||||
public OfficialCleverbotSession(string apiKey)
|
||||
{
|
||||
this._apiKey = apiKey;
|
||||
}
|
||||
|
||||
public async Task<string> Think(string input)
|
||||
{
|
||||
using (var http = new HttpClient())
|
||||
{
|
||||
var dataString = await http.GetStringAsync(string.Format(queryString, input, _cs ?? "")).ConfigureAwait(false);
|
||||
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
|
||||
_cs = data?.Cs;
|
||||
return data?.Output;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
364
NadekoBot.Core/Modules/Games/Common/Connect4/Connect4.cs
Normal file
364
NadekoBot.Core/Modules/Games/Common/Connect4/Connect4.cs
Normal file
@ -0,0 +1,364 @@
|
||||
using NadekoBot.Common;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Connect4
|
||||
{
|
||||
public class Connect4Game : IDisposable
|
||||
{
|
||||
public enum Phase
|
||||
{
|
||||
Joining, // waiting for second player to join
|
||||
P1Move,
|
||||
P2Move,
|
||||
Ended,
|
||||
}
|
||||
|
||||
public enum Field //temporary most likely
|
||||
{
|
||||
Empty,
|
||||
P1,
|
||||
P2,
|
||||
}
|
||||
|
||||
public enum Result
|
||||
{
|
||||
Draw,
|
||||
CurrentPlayerWon,
|
||||
OtherPlayerWon,
|
||||
}
|
||||
|
||||
public const int NumberOfColumns = 6;
|
||||
public const int NumberOfRows = 7;
|
||||
|
||||
public Phase CurrentPhase { get; private set; } = Phase.Joining;
|
||||
|
||||
//state is bottom to top, left to right
|
||||
private readonly Field[] _gameState = new Field[NumberOfRows * NumberOfColumns];
|
||||
private readonly (ulong UserId, string Username)?[] _players = new(ulong, string)?[2];
|
||||
|
||||
public ImmutableArray<Field> GameState => _gameState.ToImmutableArray();
|
||||
public ImmutableArray<(ulong UserId, string Username)?> Players => _players.ToImmutableArray();
|
||||
|
||||
public string CurrentPlayer => CurrentPhase == Phase.P1Move
|
||||
? _players[0].Value.Username
|
||||
: _players[1].Value.Username;
|
||||
|
||||
public string OtherPlayer => CurrentPhase == Phase.P2Move
|
||||
? _players[0].Value.Username
|
||||
: _players[1].Value.Username;
|
||||
|
||||
//public event Func<Connect4Game, Task> OnGameStarted;
|
||||
public event Func<Connect4Game, Task> OnGameStateUpdated;
|
||||
public event Func<Connect4Game, Task> OnGameFailedToStart;
|
||||
public event Func<Connect4Game, Result, Task> OnGameEnded;
|
||||
|
||||
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
|
||||
private readonly NadekoRandom _rng;
|
||||
|
||||
private Timer _playerTimeoutTimer;
|
||||
|
||||
/* [ ][ ][ ][ ][ ][ ]
|
||||
* [ ][ ][ ][ ][ ][ ]
|
||||
* [ ][ ][ ][ ][ ][ ]
|
||||
* [ ][ ][ ][ ][ ][ ]
|
||||
* [ ][ ][ ][ ][ ][ ]
|
||||
* [ ][ ][ ][ ][ ][ ]
|
||||
* [ ][ ][ ][ ][ ][ ]
|
||||
*/
|
||||
|
||||
public Connect4Game(ulong userId, string userName)
|
||||
{
|
||||
_players[0] = (userId, userName);
|
||||
|
||||
_rng = new NadekoRandom();
|
||||
for (int i = 0; i < NumberOfColumns * NumberOfRows; i++)
|
||||
{
|
||||
_gameState[i] = Field.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (CurrentPhase != Phase.Joining)
|
||||
return;
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(15000).ConfigureAwait(false);
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_players[1] == null)
|
||||
{
|
||||
var __ = OnGameFailedToStart?.Invoke(this);
|
||||
CurrentPhase = Phase.Ended;
|
||||
return;
|
||||
}
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> Join(ulong userId, string userName)
|
||||
{
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (CurrentPhase != Phase.Joining) //can't join if its not a joining phase
|
||||
return false;
|
||||
|
||||
if (_players[0].Value.UserId == userId) // same user can't join own game
|
||||
return false;
|
||||
|
||||
if (_rng.Next(0, 2) == 0) //rolling from 0-1, if number is 0, join as first player
|
||||
{
|
||||
_players[1] = _players[0];
|
||||
_players[0] = (userId, userName);
|
||||
}
|
||||
else //else join as a second player
|
||||
_players[1] = (userId, userName);
|
||||
|
||||
CurrentPhase = Phase.P1Move; //start the game
|
||||
_playerTimeoutTimer = new Timer(async state =>
|
||||
{
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
EndGame(Result.OtherPlayerWon);
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
|
||||
var __ = OnGameStateUpdated?.Invoke(this);
|
||||
|
||||
return true;
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}
|
||||
|
||||
public async Task<bool> Input(ulong userId, string userName, int inputCol)
|
||||
{
|
||||
await _locker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
inputCol -= 1;
|
||||
if (CurrentPhase == Phase.Ended || CurrentPhase == Phase.Joining)
|
||||
return false;
|
||||
|
||||
if (!((_players[0].Value.UserId == userId && CurrentPhase == Phase.P1Move)
|
||||
|| (_players[1].Value.UserId == userId && CurrentPhase == Phase.P2Move)))
|
||||
return false;
|
||||
|
||||
if (inputCol < 0 || inputCol > NumberOfColumns) //invalid input
|
||||
return false;
|
||||
|
||||
if (IsColumnFull(inputCol)) //can't play there event?
|
||||
return false;
|
||||
|
||||
var start = NumberOfRows * inputCol;
|
||||
for (int i = start; i < start + NumberOfRows; i++)
|
||||
{
|
||||
if (_gameState[i] == Field.Empty)
|
||||
{
|
||||
_gameState[i] = GetPlayerPiece(userId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//check winnning condition
|
||||
// ok, i'll go from [0-2] in rows (and through all columns) and check upward if 4 are connected
|
||||
|
||||
for (int i = 0; i < NumberOfRows - 3; i++)
|
||||
{
|
||||
if (CurrentPhase == Phase.Ended)
|
||||
break;
|
||||
|
||||
for (int j = 0; j < NumberOfColumns; j++)
|
||||
{
|
||||
if (CurrentPhase == Phase.Ended)
|
||||
break;
|
||||
|
||||
var first = _gameState[i + j * NumberOfRows];
|
||||
if (first != Field.Empty)
|
||||
{
|
||||
//Console.WriteLine(i + j * NumberOfRows);
|
||||
for (int k = 1; k < 4; k++)
|
||||
{
|
||||
var next = _gameState[i + k + j * NumberOfRows];
|
||||
if (next == first)
|
||||
{
|
||||
//Console.WriteLine(i + k + j * NumberOfRows);
|
||||
if (k == 3)
|
||||
EndGame(Result.CurrentPlayerWon);
|
||||
else
|
||||
continue;
|
||||
}
|
||||
else break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// i'll go [0-1] in columns (and through all rows) and check to the right if 4 are connected
|
||||
for (int i = 0; i < NumberOfColumns - 3; i++)
|
||||
{
|
||||
if (CurrentPhase == Phase.Ended)
|
||||
break;
|
||||
|
||||
for (int j = 0; j < NumberOfRows; j++)
|
||||
{
|
||||
if (CurrentPhase == Phase.Ended)
|
||||
break;
|
||||
|
||||
var first = _gameState[j + i * NumberOfRows];
|
||||
if (first != Field.Empty)
|
||||
{
|
||||
for (int k = 1; k < 4; k++)
|
||||
{
|
||||
var next = _gameState[j + (i + k) * NumberOfRows];
|
||||
if (next == first)
|
||||
if (k == 3)
|
||||
EndGame(Result.CurrentPlayerWon);
|
||||
else
|
||||
continue;
|
||||
else break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//need to check diagonal now
|
||||
for (int col = 0; col < NumberOfColumns; col++)
|
||||
{
|
||||
if (CurrentPhase == Phase.Ended)
|
||||
break;
|
||||
|
||||
for (int row = 0; row < NumberOfRows; row++)
|
||||
{
|
||||
if (CurrentPhase == Phase.Ended)
|
||||
break;
|
||||
|
||||
var first = _gameState[row + col * NumberOfRows];
|
||||
|
||||
if (first != Field.Empty)
|
||||
{
|
||||
var same = 1;
|
||||
|
||||
//top left
|
||||
for (int i = 1; i < 4; i++)
|
||||
{
|
||||
//while going top left, rows are increasing, columns are decreasing
|
||||
var curRow = row + i;
|
||||
var curCol = col - i;
|
||||
|
||||
//check if current values are in range
|
||||
if (curRow >= NumberOfRows || curRow < 0)
|
||||
break;
|
||||
if (curCol < 0 || curCol >= NumberOfColumns)
|
||||
break;
|
||||
|
||||
var cur = _gameState[curRow + curCol * NumberOfRows];
|
||||
if (cur == first)
|
||||
same++;
|
||||
else break;
|
||||
}
|
||||
|
||||
if (same == 4)
|
||||
{
|
||||
Console.WriteLine($"Won top left diagonal starting from {row + col * NumberOfRows}");
|
||||
EndGame(Result.CurrentPlayerWon);
|
||||
break;
|
||||
}
|
||||
|
||||
same = 1;
|
||||
|
||||
//top right
|
||||
for (int i = 1; i < 4; i++)
|
||||
{
|
||||
//while going top right, rows are increasing, columns are increasing
|
||||
var curRow = row + i;
|
||||
var curCol = col + i;
|
||||
|
||||
//check if current values are in range
|
||||
if (curRow >= NumberOfRows || curRow < 0)
|
||||
break;
|
||||
if (curCol < 0 || curCol >= NumberOfColumns)
|
||||
break;
|
||||
|
||||
var cur = _gameState[curRow + curCol * NumberOfRows];
|
||||
if (cur == first)
|
||||
same++;
|
||||
else break;
|
||||
}
|
||||
|
||||
if (same == 4)
|
||||
{
|
||||
Console.WriteLine($"Won top right diagonal starting from {row + col * NumberOfRows}");
|
||||
EndGame(Result.CurrentPlayerWon);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//check draw? if it's even possible
|
||||
if (_gameState.All(x => x != Field.Empty))
|
||||
{
|
||||
EndGame(Result.Draw);
|
||||
}
|
||||
|
||||
if (CurrentPhase != Phase.Ended)
|
||||
{
|
||||
if (CurrentPhase == Phase.P1Move)
|
||||
CurrentPhase = Phase.P2Move;
|
||||
else
|
||||
CurrentPhase = Phase.P1Move;
|
||||
|
||||
ResetTimer();
|
||||
}
|
||||
var _ = OnGameStateUpdated?.Invoke(this);
|
||||
return true;
|
||||
}
|
||||
finally { _locker.Release(); }
|
||||
}
|
||||
|
||||
private void ResetTimer()
|
||||
{
|
||||
_playerTimeoutTimer.Change(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
private void EndGame(Result result)
|
||||
{
|
||||
if (CurrentPhase == Phase.Ended)
|
||||
return;
|
||||
var _ = OnGameEnded?.Invoke(this, result);
|
||||
CurrentPhase = Phase.Ended;
|
||||
}
|
||||
|
||||
private Field GetPlayerPiece(ulong userId) => _players[0].Value.UserId == userId
|
||||
? Field.P1
|
||||
: Field.P2;
|
||||
|
||||
//column is full if there are no empty fields
|
||||
private bool IsColumnFull(int column)
|
||||
{
|
||||
var start = NumberOfRows * column;
|
||||
for (int i = start; i < start + NumberOfRows; i++)
|
||||
{
|
||||
if (_gameState[i] == Field.Empty)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
OnGameFailedToStart = null;
|
||||
OnGameStateUpdated = null;
|
||||
OnGameEnded = null;
|
||||
_playerTimeoutTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
73
NadekoBot.Core/Modules/Games/Common/GirlRating.cs
Normal file
73
NadekoBot.Core/Modules/Games/Common/GirlRating.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using ImageSharp;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using NLog;
|
||||
using SixLabors.Primitives;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class GirlRating
|
||||
{
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public double Crazy { get; }
|
||||
public double Hot { get; }
|
||||
public int Roll { get; }
|
||||
public string Advice { get; }
|
||||
public AsyncLazy<string> Url { get; }
|
||||
|
||||
public GirlRating(IImagesService _images, double crazy, double hot, int roll, string advice)
|
||||
{
|
||||
Crazy = crazy;
|
||||
Hot = hot;
|
||||
Roll = roll;
|
||||
Advice = advice; // convenient to have it here, even though atm there are only few different ones.
|
||||
|
||||
Url = new AsyncLazy<string>(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var ms = new MemoryStream(_images.WifeMatrix.ToArray(), false))
|
||||
using (var img = Image.Load(ms))
|
||||
{
|
||||
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 pointMs = new MemoryStream(_images.RategirlDot.ToArray(), false))
|
||||
using (var pointImg = Image.Load(pointMs))
|
||||
{
|
||||
img.DrawImage(pointImg, 100, default(Size), new Point(pointx - 10, pointy - 10));
|
||||
}
|
||||
|
||||
string url;
|
||||
using (var http = new HttpClient())
|
||||
using (var imgStream = new MemoryStream())
|
||||
{
|
||||
img.SaveAsPng(imgStream);
|
||||
var byteContent = new ByteArrayContent(imgStream.ToArray());
|
||||
http.AddFakeHeaders();
|
||||
|
||||
var reponse = await http.PutAsync("https://transfer.sh/img.png", byteContent);
|
||||
url = await reponse.Content.ReadAsStringAsync();
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman.Exceptions
|
||||
{
|
||||
public class TermNotFoundException : Exception
|
||||
{
|
||||
public TermNotFoundException() : base("Term of that type couldn't be found")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
172
NadekoBot.Core/Modules/Games/Common/Hangman/Hangman.cs
Normal file
172
NadekoBot.Core/Modules/Games/Common/Hangman/Hangman.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using NadekoBot.Extensions;
|
||||
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.Hangman
|
||||
{
|
||||
public 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()
|
||||
{
|
||||
Errors++;
|
||||
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 latter was already guessed
|
||||
{
|
||||
var _ = OnLetterAlreadyUsed?.Invoke(this, userName, ch);
|
||||
AddError();
|
||||
}
|
||||
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(c => char.IsLetterOrDigit(c)))))
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman
|
||||
{
|
||||
public class HangmanObject
|
||||
{
|
||||
public string Word { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
}
|
||||
}
|
14
NadekoBot.Core/Modules/Games/Common/Hangman/Phase.cs
Normal file
14
NadekoBot.Core/Modules/Games/Common/Hangman/Phase.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman
|
||||
{
|
||||
public enum Phase
|
||||
{
|
||||
Active,
|
||||
Ended,
|
||||
}
|
||||
}
|
54
NadekoBot.Core/Modules/Games/Common/Hangman/TermPool.cs
Normal file
54
NadekoBot.Core/Modules/Games/Common/Hangman/TermPool.cs
Normal file
@ -0,0 +1,54 @@
|
||||
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 NLog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman
|
||||
{
|
||||
public class TermPool
|
||||
{
|
||||
const string termsPath = "data/hangman3.json";
|
||||
private readonly Logger _log;
|
||||
|
||||
public IReadOnlyDictionary<string, HangmanObject[]> Data { get; } = new Dictionary<string, HangmanObject[]>();
|
||||
public TermPool()
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
try
|
||||
{
|
||||
Data = JsonConvert.DeserializeObject<Dictionary<string, HangmanObject[]>>(File.ReadAllText(termsPath));
|
||||
Data = Data.ToDictionary(
|
||||
x => x.Key.ToLowerInvariant(),
|
||||
x => x.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public HangmanObject GetTerm(string type)
|
||||
{
|
||||
type = type?.Trim().ToLowerInvariant();
|
||||
var rng = new NadekoRandom();
|
||||
|
||||
if (type == "random")
|
||||
{
|
||||
var keys = Data.Keys.ToArray();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
18
NadekoBot.Core/Modules/Games/Common/Hangman/TermType.cs
Normal file
18
NadekoBot.Core/Modules/Games/Common/Hangman/TermType.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Hangman
|
||||
{
|
||||
[Flags]
|
||||
public enum TermType
|
||||
{
|
||||
Countries = 0,
|
||||
Movies = 1,
|
||||
Animals = 2,
|
||||
Things = 4,
|
||||
Random = 8,
|
||||
}
|
||||
}
|
181
NadekoBot.Core/Modules/Games/Common/Nunchi/Nunchi.cs
Normal file
181
NadekoBot.Core/Modules/Games/Common/Nunchi/Nunchi.cs
Normal file
@ -0,0 +1,181 @@
|
||||
using NadekoBot.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Nunchi
|
||||
{
|
||||
public class Nunchi : 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<Nunchi, Task> OnGameStarted;
|
||||
public event Func<Nunchi, int, Task> OnRoundStarted;
|
||||
public event Func<Nunchi, Task> OnUserGuessed;
|
||||
public event Func<Nunchi, (ulong Id, string Name)?, Task> OnRoundEnded; // tuple of the user who failed
|
||||
public event Func<Nunchi, 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 Nunchi(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();
|
||||
EndRound(failure);
|
||||
}
|
||||
}
|
||||
var __ = 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;
|
||||
}
|
||||
}
|
||||
}
|
129
NadekoBot.Core/Modules/Games/Common/Poll.cs
Normal file
129
NadekoBot.Core/Modules/Games/Common/Poll.cs
Normal file
@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class Poll
|
||||
{
|
||||
private readonly IUserMessage _originalMessage;
|
||||
private readonly IGuild _guild;
|
||||
private readonly string[] answers;
|
||||
private readonly ConcurrentDictionary<ulong, int> _participants = new ConcurrentDictionary<ulong, int>();
|
||||
private readonly string _question;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly NadekoStrings _strings;
|
||||
private bool running = false;
|
||||
|
||||
public event Action<ulong> OnEnded = delegate { };
|
||||
|
||||
public Poll(DiscordSocketClient client, NadekoStrings strings, IUserMessage umsg, string question, IEnumerable<string> enumerable)
|
||||
{
|
||||
_client = client;
|
||||
_strings = strings;
|
||||
|
||||
_originalMessage = umsg;
|
||||
_guild = ((ITextChannel)umsg.Channel).Guild;
|
||||
_question = question;
|
||||
answers = enumerable as string[] ?? enumerable.ToArray();
|
||||
}
|
||||
|
||||
public EmbedBuilder GetStats(string title)
|
||||
{
|
||||
var results = _participants.GroupBy(kvp => kvp.Value)
|
||||
.ToDictionary(x => x.Key, x => x.Sum(kvp => 1))
|
||||
.OrderByDescending(kvp => kvp.Value)
|
||||
.ToArray();
|
||||
|
||||
var eb = new EmbedBuilder().WithTitle(title);
|
||||
|
||||
var sb = new StringBuilder()
|
||||
.AppendLine(Format.Bold(_question))
|
||||
.AppendLine();
|
||||
|
||||
var totalVotesCast = 0;
|
||||
if (results.Length == 0)
|
||||
{
|
||||
sb.AppendLine(GetText("no_votes_cast"));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < results.Length; i++)
|
||||
{
|
||||
var result = results[i];
|
||||
sb.AppendLine(GetText("poll_result",
|
||||
result.Key,
|
||||
Format.Bold(answers[result.Key - 1]),
|
||||
Format.Bold(result.Value.ToString())));
|
||||
totalVotesCast += result.Value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
eb.WithDescription(sb.ToString())
|
||||
.WithFooter(efb => efb.WithText(GetText("x_votes_cast", totalVotesCast)));
|
||||
|
||||
return eb;
|
||||
}
|
||||
|
||||
public async Task StartPoll()
|
||||
{
|
||||
var msgToSend = GetText("poll_created", Format.Bold(_originalMessage.Author.Username)) + "\n\n" + Format.Bold(_question) + "\n";
|
||||
var num = 1;
|
||||
msgToSend = answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n");
|
||||
msgToSend += "\n" + Format.Bold(GetText("poll_vote_public"));
|
||||
|
||||
await _originalMessage.Channel.SendConfirmAsync(msgToSend).ConfigureAwait(false);
|
||||
running = true;
|
||||
}
|
||||
|
||||
public async Task StopPoll()
|
||||
{
|
||||
running = false;
|
||||
OnEnded(_guild.Id);
|
||||
await _originalMessage.Channel.EmbedAsync(GetStats("POLL CLOSED")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> TryVote(IUserMessage msg)
|
||||
{
|
||||
// has to be a user message
|
||||
if (msg == null || msg.Author.IsBot || !running)
|
||||
return false;
|
||||
|
||||
// has to be an integer
|
||||
if (!int.TryParse(msg.Content, out int vote))
|
||||
return false;
|
||||
if (vote < 1 || vote > answers.Length)
|
||||
return false;
|
||||
|
||||
IMessageChannel ch;
|
||||
//if public, channel must be the same the poll started in
|
||||
if (_originalMessage.Channel.Id != msg.Channel.Id)
|
||||
return false;
|
||||
ch = msg.Channel;
|
||||
|
||||
//user can vote only once
|
||||
if (_participants.TryAdd(msg.Author.Id, vote))
|
||||
{
|
||||
var toDelete = await ch.SendConfirmAsync(GetText("poll_voted", Format.Bold(msg.Author.ToString()))).ConfigureAwait(false);
|
||||
toDelete.DeleteAfter(5);
|
||||
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetText(string key, params object[] replacements)
|
||||
=> _strings.GetText(key,
|
||||
_guild.Id,
|
||||
"Games".ToLowerInvariant(),
|
||||
replacements);
|
||||
}
|
||||
}
|
277
NadekoBot.Core/Modules/Games/Common/TicTacToe.cs
Normal file
277
NadekoBot.Core/Modules/Games/Common/TicTacToe.cs
Normal file
@ -0,0 +1,277 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class TicTacToe
|
||||
{
|
||||
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 Action<TicTacToe> OnEnded;
|
||||
|
||||
private IUserMessage _previousMessage;
|
||||
private Timer _timeoutTimer;
|
||||
private readonly NadekoStrings _strings;
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public TicTacToe(NadekoStrings strings, DiscordSocketClient client, ITextChannel channel, IGuildUser firstUser)
|
||||
{
|
||||
_channel = channel;
|
||||
_strings = strings;
|
||||
_client = client;
|
||||
|
||||
_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,
|
||||
typeof(Games).Name.ToLowerInvariant(),
|
||||
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();
|
||||
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, 15000, 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)); } catch { }
|
||||
try { await del1; } catch { }
|
||||
try { if (del2 != null) await del2; } catch { }
|
||||
});
|
||||
_curUserIndex ^= 1;
|
||||
|
||||
_timeoutTimer.Change(15000, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_moveLock.Release();
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
266
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaGame.cs
Normal file
266
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaGame.cs
Normal file
@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using NLog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaGame
|
||||
{
|
||||
private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1);
|
||||
private readonly Logger _log;
|
||||
private readonly NadekoStrings _strings;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotConfigProvider _bc;
|
||||
private readonly CurrencyService _cs;
|
||||
|
||||
public IGuild Guild { get; }
|
||||
public ITextChannel Channel { get; }
|
||||
|
||||
private readonly int _questionDurationMiliseconds = 30000;
|
||||
private readonly int _hintTimeoutMiliseconds = 6000;
|
||||
public bool ShowHints { get; }
|
||||
public bool IsPokemon { get; }
|
||||
private CancellationTokenSource _triviaCancelSource;
|
||||
|
||||
public TriviaQuestion CurrentQuestion { get; private set; }
|
||||
public HashSet<TriviaQuestion> OldQuestions { get; } = new HashSet<TriviaQuestion>();
|
||||
|
||||
public ConcurrentDictionary<IGuildUser, int> Users { get; } = new ConcurrentDictionary<IGuildUser, int>();
|
||||
|
||||
public bool GameActive { get; private set; }
|
||||
public bool ShouldStopGame { get; private set; }
|
||||
|
||||
public int WinRequirement { get; }
|
||||
|
||||
public TriviaGame(NadekoStrings strings, DiscordSocketClient client, IBotConfigProvider bc,
|
||||
CurrencyService cs, IGuild guild, ITextChannel channel,
|
||||
bool showHints, int winReq, bool isPokemon)
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
_strings = strings;
|
||||
_client = client;
|
||||
_bc = bc;
|
||||
_cs = cs;
|
||||
|
||||
ShowHints = showHints;
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
WinRequirement = winReq;
|
||||
IsPokemon = isPokemon;
|
||||
}
|
||||
|
||||
private string GetText(string key, params object[] replacements) =>
|
||||
_strings.GetText(key,
|
||||
Channel.GuildId,
|
||||
typeof(Games).Name.ToLowerInvariant(),
|
||||
replacements);
|
||||
|
||||
public async Task StartGame()
|
||||
{
|
||||
while (!ShouldStopGame)
|
||||
{
|
||||
// reset the cancellation source
|
||||
_triviaCancelSource = new CancellationTokenSource();
|
||||
|
||||
// load question
|
||||
CurrentQuestion = TriviaQuestionPool.Instance.GetRandomQuestion(OldQuestions, IsPokemon);
|
||||
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
|
||||
{
|
||||
await Channel.SendErrorAsync(GetText("trivia_game"), GetText("failed_loading_question")).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again
|
||||
|
||||
EmbedBuilder questionEmbed;
|
||||
IUserMessage questionMessage;
|
||||
try
|
||||
{
|
||||
questionEmbed = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.AddField(eab => eab.WithName(GetText("category")).WithValue(CurrentQuestion.Category))
|
||||
.AddField(eab => eab.WithName(GetText("question")).WithValue(CurrentQuestion.Question));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute))
|
||||
questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl);
|
||||
|
||||
questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound ||
|
||||
ex.HttpCode == System.Net.HttpStatusCode.Forbidden ||
|
||||
ex.HttpCode == System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
//receive messages
|
||||
try
|
||||
{
|
||||
_client.MessageReceived += PotentialGuess;
|
||||
|
||||
//allow people to guess
|
||||
GameActive = true;
|
||||
try
|
||||
{
|
||||
//hint
|
||||
await Task.Delay(_hintTimeoutMiliseconds, _triviaCancelSource.Token).ConfigureAwait(false);
|
||||
if (ShowHints)
|
||||
try
|
||||
{
|
||||
await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(efb => efb.WithText(CurrentQuestion.GetHint())).Build())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) { _log.Warn(ex); }
|
||||
|
||||
//timeout
|
||||
await Task.Delay(_questionDurationMiliseconds - _hintTimeoutMiliseconds, _triviaCancelSource.Token).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (TaskCanceledException) { } //means someone guessed the answer
|
||||
}
|
||||
finally
|
||||
{
|
||||
GameActive = false;
|
||||
_client.MessageReceived -= PotentialGuess;
|
||||
}
|
||||
if (!_triviaCancelSource.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var embed = new EmbedBuilder().WithErrorColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.WithDescription(GetText("trivia_times_up", Format.Bold(CurrentQuestion.Answer)));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
|
||||
await Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn(ex);
|
||||
}
|
||||
}
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureStopped()
|
||||
{
|
||||
ShouldStopGame = true;
|
||||
|
||||
await Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName("Trivia Game Ended"))
|
||||
.WithTitle("Final Results")
|
||||
.WithDescription(GetLeaderboard())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StopGame()
|
||||
{
|
||||
var old = ShouldStopGame;
|
||||
ShouldStopGame = true;
|
||||
if (!old)
|
||||
try { await Channel.SendConfirmAsync(GetText("trivia_game"), GetText("trivia_stopping")).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
|
||||
}
|
||||
|
||||
private Task PotentialGuess(SocketMessage imsg)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (imsg.Author.IsBot)
|
||||
return;
|
||||
|
||||
var umsg = imsg as SocketUserMessage;
|
||||
|
||||
var textChannel = umsg?.Channel as ITextChannel;
|
||||
if (textChannel == null || textChannel.Guild != Guild)
|
||||
return;
|
||||
|
||||
var guildUser = (IGuildUser)umsg.Author;
|
||||
|
||||
var guess = false;
|
||||
await _guessLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (GameActive && CurrentQuestion.IsAnswerCorrect(umsg.Content) && !_triviaCancelSource.IsCancellationRequested)
|
||||
{
|
||||
Users.AddOrUpdate(guildUser, 1, (gu, old) => ++old);
|
||||
guess = true;
|
||||
}
|
||||
}
|
||||
finally { _guessLock.Release(); }
|
||||
if (!guess) return;
|
||||
_triviaCancelSource.Cancel();
|
||||
|
||||
|
||||
if (Users[guildUser] == WinRequirement)
|
||||
{
|
||||
ShouldStopGame = true;
|
||||
try
|
||||
{
|
||||
var embedS = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.WithDescription(GetText("trivia_win",
|
||||
guildUser.Mention,
|
||||
Format.Bold(CurrentQuestion.Answer)));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
await Channel.EmbedAsync(embedS).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
var reward = _bc.BotConfig.TriviaCurrencyReward;
|
||||
if (reward > 0)
|
||||
await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
var embed = new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("trivia_game"))
|
||||
.WithDescription(GetText("trivia_guess", guildUser.Mention, Format.Bold(CurrentQuestion.Answer)));
|
||||
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
|
||||
await Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) { _log.Warn(ex); }
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetLeaderboard()
|
||||
{
|
||||
if (Users.Count == 0)
|
||||
return GetText("no_results");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value))
|
||||
{
|
||||
sb.AppendLine(GetText("trivia_points", Format.Bold(kvp.Key.ToString()), kvp.Value).SnPl(kvp.Value));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
110
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaQuestion.cs
Normal file
110
NadekoBot.Core/Modules/Games/Common/Trivia/TriviaQuestion.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NadekoBot.Extensions;
|
||||
|
||||
// THANKS @ShoMinamimoto for suggestions and coding help
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaQuestion
|
||||
{
|
||||
//represents the min size to judge levDistance with
|
||||
private static readonly HashSet<Tuple<int, int>> strictness = new HashSet<Tuple<int, int>> {
|
||||
new Tuple<int, int>(9, 0),
|
||||
new Tuple<int, int>(14, 1),
|
||||
new Tuple<int, int>(19, 2),
|
||||
new Tuple<int, int>(22, 3),
|
||||
};
|
||||
public static int maxStringLength = 22;
|
||||
|
||||
public string Category { get; set; }
|
||||
public string Question { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string AnswerImageUrl { get; set; }
|
||||
public string Answer { get; set; }
|
||||
private string _cleanAnswer;
|
||||
public string CleanAnswer => _cleanAnswer ?? (_cleanAnswer = Clean(Answer));
|
||||
|
||||
public TriviaQuestion(string q, string a, string c, string img = null, string answerImage = null)
|
||||
{
|
||||
this.Question = q;
|
||||
this.Answer = a;
|
||||
this.Category = c;
|
||||
this.ImageUrl = img;
|
||||
this.AnswerImageUrl = answerImage ?? img;
|
||||
}
|
||||
|
||||
public string GetHint() => Scramble(Answer);
|
||||
|
||||
public bool IsAnswerCorrect(string guess)
|
||||
{
|
||||
if (Answer.Equals(guess))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var cleanGuess = Clean(guess);
|
||||
if (CleanAnswer.Equals(cleanGuess))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
int levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess);
|
||||
int levDistanceNormal = Answer.LevenshteinDistance(guess);
|
||||
return JudgeGuess(CleanAnswer.Length, cleanGuess.Length, levDistanceClean)
|
||||
|| JudgeGuess(Answer.Length, guess.Length, levDistanceNormal);
|
||||
}
|
||||
|
||||
private bool JudgeGuess(int guessLength, int answerLength, int levDistance)
|
||||
{
|
||||
foreach (Tuple<int, int> level in strictness)
|
||||
{
|
||||
if (guessLength <= level.Item1 || answerLength <= level.Item1)
|
||||
{
|
||||
if (levDistance <= level.Item2)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private string Clean(string str)
|
||||
{
|
||||
str = " " + str.ToLower() + " ";
|
||||
str = Regex.Replace(str, "\\s+", " ");
|
||||
str = Regex.Replace(str, "[^\\w\\d\\s]", "");
|
||||
//Here's where custom modification can be done
|
||||
str = Regex.Replace(str, "\\s(a|an|the|of|in|for|to|as|at|be)\\s", " ");
|
||||
//End custom mod and cleanup whitespace
|
||||
str = Regex.Replace(str, "^\\s+", "");
|
||||
str = Regex.Replace(str, "\\s+$", "");
|
||||
//Trim the really long answers
|
||||
str = str.Length <= maxStringLength ? str : str.Substring(0, maxStringLength);
|
||||
return str;
|
||||
}
|
||||
|
||||
private static string Scramble(string word)
|
||||
{
|
||||
var letters = word.ToCharArray();
|
||||
var count = 0;
|
||||
for (var i = 0; i < letters.Length; i++)
|
||||
{
|
||||
if (letters[i] == ' ')
|
||||
continue;
|
||||
|
||||
count++;
|
||||
if (count <= letters.Length / 5)
|
||||
continue;
|
||||
|
||||
if (count % 3 == 0)
|
||||
continue;
|
||||
|
||||
if (letters[i] != ' ')
|
||||
letters[i] = '_';
|
||||
}
|
||||
return string.Join(" ", new string(letters).Replace(" ", " \u2000").AsEnumerable());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.Trivia
|
||||
{
|
||||
public class TriviaQuestionPool
|
||||
{
|
||||
public class PokemonNameId
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private static TriviaQuestionPool _instance;
|
||||
public static TriviaQuestionPool Instance { get; } = _instance ?? (_instance = new TriviaQuestionPool());
|
||||
|
||||
private const string questionsFile = "data/trivia_questions.json";
|
||||
private const string pokemonMapPath = "data/pokemon/name-id_map4.json";
|
||||
private readonly int maxPokemonId;
|
||||
|
||||
private Random rng { get; } = new NadekoRandom();
|
||||
|
||||
private TriviaQuestion[] pool { get; }
|
||||
private ImmutableDictionary<int, string> map { get; }
|
||||
|
||||
static TriviaQuestionPool() { }
|
||||
|
||||
private TriviaQuestionPool()
|
||||
{
|
||||
pool = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(questionsFile));
|
||||
map = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(pokemonMapPath))
|
||||
.ToDictionary(x => x.Id, x => x.Name)
|
||||
.ToImmutableDictionary();
|
||||
|
||||
maxPokemonId = 721; //xd
|
||||
}
|
||||
|
||||
public TriviaQuestion GetRandomQuestion(HashSet<TriviaQuestion> exclude, bool isPokemon)
|
||||
{
|
||||
if (pool.Length == 0)
|
||||
return null;
|
||||
|
||||
if (isPokemon)
|
||||
{
|
||||
var num = rng.Next(1, maxPokemonId + 1);
|
||||
return new TriviaQuestion("Who's That Pokémon?",
|
||||
map[num].ToTitleCase(),
|
||||
"Pokemon",
|
||||
$@"http://nadekobot.me/images/pokemon/shadows/{num}.png",
|
||||
$@"http://nadekobot.me/images/pokemon/real/{num}.png");
|
||||
}
|
||||
TriviaQuestion randomQuestion;
|
||||
while (exclude.Contains(randomQuestion = pool[rng.Next(0, pool.Length)])) ;
|
||||
|
||||
return randomQuestion;
|
||||
}
|
||||
}
|
||||
}
|
8
NadekoBot.Core/Modules/Games/Common/TypingArticle.cs
Normal file
8
NadekoBot.Core/Modules/Games/Common/TypingArticle.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class TypingArticle
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
153
NadekoBot.Core/Modules/Games/Common/TypingGame.cs
Normal file
153
NadekoBot.Core/Modules/Games/Common/TypingGame.cs
Normal file
@ -0,0 +1,153 @@
|
||||
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 NLog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common
|
||||
{
|
||||
public class TypingGame
|
||||
{
|
||||
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 Logger _log { get; }
|
||||
|
||||
public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel, string prefix) //kek@prefix
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
_games = games;
|
||||
_client = client;
|
||||
_prefix = prefix;
|
||||
|
||||
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.Warn(ex); }
|
||||
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 msg = await Channel.SendMessageAsync("Starting new typing contest in **3**...").ConfigureAwait(false);
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await msg.ModifyAsync(m => m.Content = "Starting new typing contest in **2**...").ConfigureAwait(false);
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
await msg.ModifyAsync(m => m.Content = "Starting new typing contest in **1**...").ConfigureAwait(false);
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) { _log.Warn(ex); }
|
||||
|
||||
await msg.ModifyAsync(m => m.Content = Format.Bold(Format.Sanitize(CurrentSentence.Replace(" ", " \x200B")).SanitizeMentions())).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")).SanitizeMentions()}**").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { _log.Warn(ex); }
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool Judge(int errors, int textLength) => errors <= textLength / 25;
|
||||
|
||||
}
|
||||
}
|
183
NadekoBot.Core/Modules/Games/Connect4Commands.cs
Normal file
183
NadekoBot.Core/Modules/Games/Connect4Commands.cs
Normal file
@ -0,0 +1,183 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Games.Common.Connect4;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class Connect4Commands : NadekoSubmodule<GamesService>
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
private readonly string[] numbers = new string[] { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:"};
|
||||
|
||||
public Connect4Commands(DiscordSocketClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Connect4()
|
||||
{
|
||||
var newGame = new Connect4Game(Context.User.Id, Context.User.ToString());
|
||||
Connect4Game game;
|
||||
if ((game = _service.Connect4Games.GetOrAdd(Context.Channel.Id, newGame)) != newGame)
|
||||
{
|
||||
if (game.CurrentPhase != Connect4Game.Phase.Joining)
|
||||
return;
|
||||
|
||||
newGame.Dispose();
|
||||
//means game already exists, try to join
|
||||
var joined = await game.Join(Context.User.Id, Context.User.ToString()).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
game.OnGameStateUpdated += Game_OnGameStateUpdated;
|
||||
game.OnGameFailedToStart += Game_OnGameFailedToStart;
|
||||
game.OnGameEnded += Game_OnGameEnded;
|
||||
_client.MessageReceived += _client_MessageReceived;
|
||||
|
||||
game.Initialize();
|
||||
|
||||
await ReplyConfirmLocalized("connect4_created").ConfigureAwait(false);
|
||||
|
||||
Task _client_MessageReceived(SocketMessage arg)
|
||||
{
|
||||
if (Context.Channel.Id != arg.Channel.Id)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
bool success = false;
|
||||
if (int.TryParse(arg.Content, out var col))
|
||||
{
|
||||
success = await game.Input(arg.Author.Id, arg.Author.ToString(), col).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (success)
|
||||
try { await arg.DeleteAsync().ConfigureAwait(false); } catch { }
|
||||
else
|
||||
{
|
||||
if (game.CurrentPhase == Connect4Game.Phase.Joining
|
||||
|| game.CurrentPhase == Connect4Game.Phase.Ended)
|
||||
{
|
||||
return;
|
||||
}
|
||||
RepostCounter++;
|
||||
if (RepostCounter == 0)
|
||||
try { msg = await Context.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()); } catch { }
|
||||
}
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task Game_OnGameFailedToStart(Connect4Game arg)
|
||||
{
|
||||
if (_service.Connect4Games.TryRemove(Context.Channel.Id, out var toDispose))
|
||||
{
|
||||
_client.MessageReceived -= _client_MessageReceived;
|
||||
toDispose.Dispose();
|
||||
}
|
||||
return ErrorLocalized("connect4_failed_to_start");
|
||||
}
|
||||
|
||||
Task Game_OnGameEnded(Connect4Game arg, Connect4Game.Result result)
|
||||
{
|
||||
if (_service.Connect4Games.TryRemove(Context.Channel.Id, out var toDispose))
|
||||
{
|
||||
_client.MessageReceived -= _client_MessageReceived;
|
||||
toDispose.Dispose();
|
||||
}
|
||||
|
||||
string title;
|
||||
if (result == Connect4Game.Result.CurrentPlayerWon)
|
||||
{
|
||||
title = GetText("connect4_won", Format.Bold(arg.CurrentPlayer), Format.Bold(arg.OtherPlayer));
|
||||
}
|
||||
else if (result == Connect4Game.Result.OtherPlayerWon)
|
||||
{
|
||||
title = GetText("connect4_won", Format.Bold(arg.OtherPlayer), Format.Bold(arg.CurrentPlayer));
|
||||
}
|
||||
else
|
||||
title = GetText("connect4_draw");
|
||||
|
||||
return msg.ModifyAsync(x => x.Embed = new EmbedBuilder()
|
||||
.WithTitle(title)
|
||||
.WithDescription(GetGameStateText(game))
|
||||
.WithOkColor()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
private IUserMessage msg;
|
||||
|
||||
private int _repostCounter = 0;
|
||||
private int RepostCounter
|
||||
{
|
||||
get => _repostCounter;
|
||||
set
|
||||
{
|
||||
if (value < 0 || value > 7)
|
||||
_repostCounter = 0;
|
||||
else _repostCounter = value;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Game_OnGameStateUpdated(Connect4Game game)
|
||||
{
|
||||
var embed = new EmbedBuilder()
|
||||
.WithTitle($"{game.CurrentPlayer} vs {game.OtherPlayer}")
|
||||
.WithDescription(GetGameStateText(game))
|
||||
.WithOkColor();
|
||||
|
||||
|
||||
if (msg == null)
|
||||
msg = await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
else
|
||||
await msg.ModifyAsync(x => x.Embed = embed.Build()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string GetGameStateText(Connect4Game game)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (game.CurrentPhase == Connect4Game.Phase.P1Move ||
|
||||
game.CurrentPhase == Connect4Game.Phase.P2Move)
|
||||
sb.AppendLine(GetText("connect4_player_to_move", Format.Bold(game.CurrentPlayer)));
|
||||
|
||||
for (int i = Connect4Game.NumberOfRows; i > 0; i--)
|
||||
{
|
||||
for (int j = 0; j < Connect4Game.NumberOfColumns; j++)
|
||||
{
|
||||
//Console.WriteLine(i + (j * Connect4Game.NumberOfRows) - 1);
|
||||
var cur = game.GameState[i + (j * Connect4Game.NumberOfRows) - 1];
|
||||
|
||||
if (cur == Connect4Game.Field.Empty)
|
||||
sb.Append("⚫"); //black circle
|
||||
else if (cur == Connect4Game.Field.P1)
|
||||
sb.Append("🔴"); //red circle
|
||||
else
|
||||
sb.Append("🔵"); //blue circle
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
for (int i = 0; i < Connect4Game.NumberOfColumns; i++)
|
||||
{
|
||||
sb.Append(/*new string(' ', 1 + ((i + 1) / 2)) + */numbers[i]);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
209
NadekoBot.Core/Modules/Games/Games.cs
Normal file
209
NadekoBot.Core/Modules/Games/Games.cs
Normal file
@ -0,0 +1,209 @@
|
||||
using Discord.Commands;
|
||||
using Discord;
|
||||
using NadekoBot.Core.Services;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
/* more games
|
||||
- Blackjack
|
||||
- Shiritori
|
||||
- Simple RPG adventure
|
||||
*/
|
||||
public partial class Games : NadekoTopLevelModule<GamesService>
|
||||
{
|
||||
private readonly IImagesService _images;
|
||||
|
||||
public Games(IImagesService images)
|
||||
{
|
||||
_images = images;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task Choose([Remainder] string list = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(list))
|
||||
return;
|
||||
var listArr = list.Split(';');
|
||||
if (listArr.Length < 2)
|
||||
return;
|
||||
var rng = new NadekoRandom();
|
||||
await Context.Channel.SendConfirmAsync("🤔", listArr[rng.Next(0, listArr.Length)]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task EightBall([Remainder] string question = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(question))
|
||||
return;
|
||||
|
||||
await Context.Channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor)
|
||||
.AddField(efb => efb.WithName("test❓ " + GetText("question") ).WithValue(question).WithIsInline(false))
|
||||
.AddField(efb => efb.WithName("🎱 " + GetText("8ball")).WithValue(_service.EightBallResponses[new NadekoRandom().Next(0, _service.EightBallResponses.Length)]).WithIsInline(false)));
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task Rps(string input)
|
||||
{
|
||||
Func<int,string> getRpsPick = (p) =>
|
||||
{
|
||||
switch (p)
|
||||
{
|
||||
case 0:
|
||||
return "🚀";
|
||||
case 1:
|
||||
return "📎";
|
||||
default:
|
||||
return "✂️";
|
||||
}
|
||||
};
|
||||
|
||||
int pick;
|
||||
switch (input)
|
||||
{
|
||||
case "r":
|
||||
case "rock":
|
||||
case "rocket":
|
||||
pick = 0;
|
||||
break;
|
||||
case "p":
|
||||
case "paper":
|
||||
case "paperclip":
|
||||
pick = 1;
|
||||
break;
|
||||
case "scissors":
|
||||
case "s":
|
||||
pick = 2;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
var nadekoPick = new NadekoRandom().Next(0, 3);
|
||||
string msg;
|
||||
if (pick == nadekoPick)
|
||||
msg = GetText("rps_draw", getRpsPick(pick));
|
||||
else if ((pick == 0 && nadekoPick == 1) ||
|
||||
(pick == 1 && nadekoPick == 2) ||
|
||||
(pick == 2 && nadekoPick == 0))
|
||||
msg = GetText("rps_win", Context.Client.CurrentUser.Mention,
|
||||
getRpsPick(nadekoPick), getRpsPick(pick));
|
||||
else
|
||||
msg = GetText("rps_win", Context.User.Mention, getRpsPick(pick),
|
||||
getRpsPick(nadekoPick));
|
||||
|
||||
await Context.Channel.SendConfirmAsync(msg).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task RateGirl(IGuildUser usr)
|
||||
{
|
||||
var gr = _service.GirlRatings.GetOrAdd(usr.Id, GetGirl);
|
||||
var img = await gr.Url;
|
||||
await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithTitle("Girl Rating For " + usr)
|
||||
.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))
|
||||
.WithImageUrl(img)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private double NextDouble(double x, double y)
|
||||
{
|
||||
var rng = new Random();
|
||||
return rng.NextDouble() * (y - x) + x;
|
||||
}
|
||||
|
||||
private GirlRating GetGirl(ulong uid)
|
||||
{
|
||||
var rng = new NadekoRandom();
|
||||
|
||||
var roll = rng.Next(1, 1001);
|
||||
|
||||
if ((uid == 185968432783687681 ||
|
||||
uid == 265642040950390784) && roll >= 900)
|
||||
roll = 1000;
|
||||
|
||||
|
||||
double hot;
|
||||
double crazy;
|
||||
string advice;
|
||||
if (roll < 500)
|
||||
{
|
||||
hot = NextDouble(0, 5);
|
||||
crazy = NextDouble(4, 10);
|
||||
advice =
|
||||
"This is your NO-GO ZONE. We do not hang around, and date, and marry women who are at least, in our mind, a 5. " +
|
||||
"So, this is your no-go zone. You don't go here. You just rule this out. Life is better this way, that's the way it is.";
|
||||
}
|
||||
else if (roll < 750)
|
||||
{
|
||||
hot = NextDouble(5, 8);
|
||||
crazy = NextDouble(4, .6 * hot + 4);
|
||||
advice = "Above a 5, and to about an 8, and below the crazy line - this is your FUN ZONE. You can " +
|
||||
"hang around here, and meet these girls and spend time with them. Keep in mind, while you're " +
|
||||
"in the fun zone, you want to move OUT of the fun zone to a more permanent location. " +
|
||||
"These girls are most of the time not crazy.";
|
||||
}
|
||||
else if (roll < 900)
|
||||
{
|
||||
hot = NextDouble(5, 10);
|
||||
crazy = NextDouble(.61 * hot + 4, 10);
|
||||
advice = "Above the crazy line - it's the DANGER ZONE. This is redheads, strippers, anyone named Tiffany, " +
|
||||
"hairdressers... This is where your car gets keyed, you get bunny in the pot, your tires get slashed, " +
|
||||
"and you wind up in jail.";
|
||||
}
|
||||
else if (roll < 951)
|
||||
{
|
||||
hot = NextDouble(8, 10);
|
||||
crazy = NextDouble(7, .6 * hot + 4);
|
||||
advice = "Below the crazy line, above an 8 hot, but still about 7 crazy. This is your DATE ZONE. " +
|
||||
"You can stay in the date zone indefinitely. These are the girls you introduce to your friends and your family. " +
|
||||
"They're good looking, and they're reasonably not crazy most of the time. You can stay here indefinitely.";
|
||||
}
|
||||
else if (roll < 990)
|
||||
{
|
||||
hot = NextDouble(8, 10);
|
||||
crazy = NextDouble(5, 7);
|
||||
advice = "Above an 8 hot, and between about 7 and a 5 crazy - this is WIFE ZONE. If you meet this girl, you should consider long-term " +
|
||||
"relationship. Rare.";
|
||||
}
|
||||
else if (roll < 999)
|
||||
{
|
||||
hot = NextDouble(8, 10);
|
||||
crazy = NextDouble(2, 3.99d);
|
||||
advice = "You've met a girl she's above 8 hot, and not crazy at all (below 4)... totally cool?" +
|
||||
" You should be careful. That's a dude. You're talking to a tranny!";
|
||||
}
|
||||
else
|
||||
{
|
||||
hot = NextDouble(8, 10);
|
||||
crazy = NextDouble(4, 5);
|
||||
advice = "Below 5 crazy, and above 8 hot, this is the UNICORN ZONE, these things don't exist." +
|
||||
"If you find a unicorn, please capture it safely, keep it alive, we'd like to study it, " +
|
||||
"and maybe look at how to replicate that.";
|
||||
}
|
||||
|
||||
return new GirlRating(_images, crazy, hot, roll, advice);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task Linux(string guhnoo, string loonix)
|
||||
{
|
||||
await Context.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);
|
||||
}
|
||||
}
|
||||
}
|
135
NadekoBot.Core/Modules/Games/HangmanCommands.cs
Normal file
135
NadekoBot.Core/Modules/Games/HangmanCommands.cs
Normal file
@ -0,0 +1,135 @@
|
||||
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;
|
||||
|
||||
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 Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join("\n", _service.TermPool.Data.Keys));
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Hangman([Remainder]string type = "random")
|
||||
{
|
||||
var hm = new Hangman(type, _service.TermPool);
|
||||
|
||||
if (!_service.HangmanGames.TryAdd(Context.Channel.Id, hm))
|
||||
{
|
||||
hm.Dispose();
|
||||
await ReplyErrorLocalized("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 Context.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(Context.Channel.Id, out _);
|
||||
hm.Dispose();
|
||||
|
||||
Task _client_MessageReceived(SocketMessage msg)
|
||||
{
|
||||
var _ = Task.Run(() =>
|
||||
{
|
||||
if (Context.Channel.Id == msg.Channel.Id)
|
||||
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.Word.ToTitleCase()))
|
||||
.WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses)))
|
||||
.WithErrorColor();
|
||||
|
||||
if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute))
|
||||
loseEmbed.WithImageUrl(game.Term.ImageUrl);
|
||||
|
||||
return Context.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.Word.ToTitleCase()))
|
||||
.WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses)))
|
||||
.WithOkColor();
|
||||
|
||||
if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute))
|
||||
winEmbed.WithImageUrl(game.Term.ImageUrl);
|
||||
|
||||
return Context.Channel.EmbedAsync(winEmbed);
|
||||
}
|
||||
|
||||
private Task Hm_OnLetterAlreadyUsed(Hangman game, string user, char guess)
|
||||
{
|
||||
return Context.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 Context.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 Context.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(Context.Channel.Id, out var removed))
|
||||
{
|
||||
await removed.Stop().ConfigureAwait(false);
|
||||
await ReplyConfirmLocalized("hangman_stopped").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
305
NadekoBot.Core/Modules/Games/LeetCommands.cs
Normal file
305
NadekoBot.Core/Modules/Games/LeetCommands.cs
Normal file
@ -0,0 +1,305 @@
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Extensions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
|
||||
// taken from
|
||||
// http://www.codeproject.com/Tips/207582/L-t-Tr-nsl-t-r-Leet-Translator (thanks)
|
||||
// because i don't want to waste my time on this cancerous command
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task Leet(int level, [Remainder] string text = null)
|
||||
{
|
||||
text = text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
await Context.Channel.SendConfirmAsync("L33t", ToLeet(text, level).SanitizeMentions()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Translate text to Leet - Extension methods for string class
|
||||
/// </summary>
|
||||
/// <param name="text">Orginal text</param>
|
||||
/// <param name="degree">Degree of translation (1 - 3)</param>
|
||||
/// <returns>Leet translated text</returns>
|
||||
private static string ToLeet(string text, int degree = 1) =>
|
||||
Translate(text, degree);
|
||||
|
||||
/// <summary>
|
||||
/// Translate text to Leet
|
||||
/// </summary>
|
||||
/// <param name="text">Orginal text</param>
|
||||
/// <param name="degree">Degree of translation (1 - 3)</param>
|
||||
/// <returns>Leet translated text</returns>
|
||||
private static string Translate(string text, int degree = 1)
|
||||
{
|
||||
if (degree > 6)
|
||||
degree = 6;
|
||||
if (degree <= 0)
|
||||
return text;
|
||||
|
||||
// StringBuilder to store result.
|
||||
StringBuilder sb = new StringBuilder(text.Length);
|
||||
foreach (char c in text)
|
||||
{
|
||||
#region Degree 1
|
||||
if (degree == 1)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case 'a': sb.Append("4"); break;
|
||||
case 'e': sb.Append("3"); break;
|
||||
case 'i': sb.Append("1"); break;
|
||||
case 'o': sb.Append("0"); break;
|
||||
case 'A': sb.Append("4"); break;
|
||||
case 'E': sb.Append("3"); break;
|
||||
case 'I': sb.Append("1"); break;
|
||||
case 'O': sb.Append("0"); break;
|
||||
default: sb.Append(c); break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region Degree 2
|
||||
else if (degree == 2)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case 'a': sb.Append("4"); break;
|
||||
case 'e': sb.Append("3"); break;
|
||||
case 'i': sb.Append("1"); break;
|
||||
case 'o': sb.Append("0"); break;
|
||||
case 'A': sb.Append("4"); break;
|
||||
case 'E': sb.Append("3"); break;
|
||||
case 'I': sb.Append("1"); break;
|
||||
case 'O': sb.Append("0"); break;
|
||||
case 's': sb.Append("$"); break;
|
||||
case 'S': sb.Append("$"); break;
|
||||
case 'l': sb.Append("£"); break;
|
||||
case 'L': sb.Append("£"); break;
|
||||
case 'c': sb.Append("("); break;
|
||||
case 'C': sb.Append("("); break;
|
||||
case 'y': sb.Append("¥"); break;
|
||||
case 'Y': sb.Append("¥"); break;
|
||||
case 'u': sb.Append("µ"); break;
|
||||
case 'U': sb.Append("µ"); break;
|
||||
case 'd': sb.Append("Ð"); break;
|
||||
case 'D': sb.Append("Ð"); break;
|
||||
default: sb.Append(c); break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region Degree 3
|
||||
else if (degree == 3)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case 'a': sb.Append("4"); break;
|
||||
case 'e': sb.Append("3"); break;
|
||||
case 'i': sb.Append("1"); break;
|
||||
case 'o': sb.Append("0"); break;
|
||||
case 'A': sb.Append("4"); break;
|
||||
case 'E': sb.Append("3"); break;
|
||||
case 'I': sb.Append("1"); break;
|
||||
case 'O': sb.Append("0"); break;
|
||||
case 'k': sb.Append("|{"); break;
|
||||
case 'K': sb.Append("|{"); break;
|
||||
case 's': sb.Append("$"); break;
|
||||
case 'S': sb.Append("$"); break;
|
||||
case 'g': sb.Append("9"); break;
|
||||
case 'G': sb.Append("9"); break;
|
||||
case 'l': sb.Append("£"); break;
|
||||
case 'L': sb.Append("£"); break;
|
||||
case 'c': sb.Append("("); break;
|
||||
case 'C': sb.Append("("); break;
|
||||
case 't': sb.Append("7"); break;
|
||||
case 'T': sb.Append("7"); break;
|
||||
case 'z': sb.Append("2"); break;
|
||||
case 'Z': sb.Append("2"); break;
|
||||
case 'y': sb.Append("¥"); break;
|
||||
case 'Y': sb.Append("¥"); break;
|
||||
case 'u': sb.Append("µ"); break;
|
||||
case 'U': sb.Append("µ"); break;
|
||||
case 'f': sb.Append("ƒ"); break;
|
||||
case 'F': sb.Append("ƒ"); break;
|
||||
case 'd': sb.Append("Ð"); break;
|
||||
case 'D': sb.Append("Ð"); break;
|
||||
default: sb.Append(c); break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region Degree 4
|
||||
else if (degree == 4)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case 'a': sb.Append("4"); break;
|
||||
case 'e': sb.Append("3"); break;
|
||||
case 'i': sb.Append("1"); break;
|
||||
case 'o': sb.Append("0"); break;
|
||||
case 'A': sb.Append("4"); break;
|
||||
case 'E': sb.Append("3"); break;
|
||||
case 'I': sb.Append("1"); break;
|
||||
case 'O': sb.Append("0"); break;
|
||||
case 'k': sb.Append("|{"); break;
|
||||
case 'K': sb.Append("|{"); break;
|
||||
case 's': sb.Append("$"); break;
|
||||
case 'S': sb.Append("$"); break;
|
||||
case 'g': sb.Append("9"); break;
|
||||
case 'G': sb.Append("9"); break;
|
||||
case 'l': sb.Append("£"); break;
|
||||
case 'L': sb.Append("£"); break;
|
||||
case 'c': sb.Append("("); break;
|
||||
case 'C': sb.Append("("); break;
|
||||
case 't': sb.Append("7"); break;
|
||||
case 'T': sb.Append("7"); break;
|
||||
case 'z': sb.Append("2"); break;
|
||||
case 'Z': sb.Append("2"); break;
|
||||
case 'y': sb.Append("¥"); break;
|
||||
case 'Y': sb.Append("¥"); break;
|
||||
case 'u': sb.Append("µ"); break;
|
||||
case 'U': sb.Append("µ"); break;
|
||||
case 'f': sb.Append("ƒ"); break;
|
||||
case 'F': sb.Append("ƒ"); break;
|
||||
case 'd': sb.Append("Ð"); break;
|
||||
case 'D': sb.Append("Ð"); break;
|
||||
case 'n': sb.Append(@"|\\|"); break;
|
||||
case 'N': sb.Append(@"|\\|"); break;
|
||||
case 'w': sb.Append(@"\\/\\/"); break;
|
||||
case 'W': sb.Append(@"\\/\\/"); break;
|
||||
case 'h': sb.Append(@"|-|"); break;
|
||||
case 'H': sb.Append(@"|-|"); break;
|
||||
case 'v': sb.Append(@"\\/"); break;
|
||||
case 'V': sb.Append(@"\\/"); break;
|
||||
case 'm': sb.Append(@"|\\/|"); break;
|
||||
case 'M': sb.Append(@"|\/|"); break;
|
||||
default: sb.Append(c); break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region Degree 5
|
||||
else if (degree == 5)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case 'a': sb.Append("4"); break;
|
||||
case 'e': sb.Append("3"); break;
|
||||
case 'i': sb.Append("1"); break;
|
||||
case 'o': sb.Append("0"); break;
|
||||
case 'A': sb.Append("4"); break;
|
||||
case 'E': sb.Append("3"); break;
|
||||
case 'I': sb.Append("1"); break;
|
||||
case 'O': sb.Append("0"); break;
|
||||
case 's': sb.Append("$"); break;
|
||||
case 'S': sb.Append("$"); break;
|
||||
case 'g': sb.Append("9"); break;
|
||||
case 'G': sb.Append("9"); break;
|
||||
case 'l': sb.Append("£"); break;
|
||||
case 'L': sb.Append("£"); break;
|
||||
case 'c': sb.Append("("); break;
|
||||
case 'C': sb.Append("("); break;
|
||||
case 't': sb.Append("7"); break;
|
||||
case 'T': sb.Append("7"); break;
|
||||
case 'z': sb.Append("2"); break;
|
||||
case 'Z': sb.Append("2"); break;
|
||||
case 'y': sb.Append("¥"); break;
|
||||
case 'Y': sb.Append("¥"); break;
|
||||
case 'u': sb.Append("µ"); break;
|
||||
case 'U': sb.Append("µ"); break;
|
||||
case 'f': sb.Append("ƒ"); break;
|
||||
case 'F': sb.Append("ƒ"); break;
|
||||
case 'd': sb.Append("Ð"); break;
|
||||
case 'D': sb.Append("Ð"); break;
|
||||
case 'n': sb.Append(@"|\\|"); break;
|
||||
case 'N': sb.Append(@"|\\|"); break;
|
||||
case 'w': sb.Append(@"\\/\\/"); break;
|
||||
case 'W': sb.Append(@"\\/\\/"); break;
|
||||
case 'h': sb.Append("|-|"); break;
|
||||
case 'H': sb.Append("|-|"); break;
|
||||
case 'v': sb.Append("\\/"); break;
|
||||
case 'V': sb.Append(@"\\/"); break;
|
||||
case 'k': sb.Append("|{"); break;
|
||||
case 'K': sb.Append("|{"); break;
|
||||
case 'r': sb.Append("®"); break;
|
||||
case 'R': sb.Append("®"); break;
|
||||
case 'm': sb.Append(@"|\\/|"); break;
|
||||
case 'M': sb.Append(@"|\\/|"); break;
|
||||
case 'b': sb.Append("ß"); break;
|
||||
case 'B': sb.Append("ß"); break;
|
||||
case 'q': sb.Append("Q"); break;
|
||||
case 'Q': sb.Append("Q¸"); break;
|
||||
case 'x': sb.Append(")("); break;
|
||||
case 'X': sb.Append(")("); break;
|
||||
default: sb.Append(c); break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region Degree 6
|
||||
else if (degree == 6)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case 'a': sb.Append("4"); break;
|
||||
case 'e': sb.Append("3"); break;
|
||||
case 'i': sb.Append("1"); break;
|
||||
case 'o': sb.Append("0"); break;
|
||||
case 'A': sb.Append("4"); break;
|
||||
case 'E': sb.Append("3"); break;
|
||||
case 'I': sb.Append("1"); break;
|
||||
case 'O': sb.Append("0"); break;
|
||||
case 's': sb.Append("$"); break;
|
||||
case 'S': sb.Append("$"); break;
|
||||
case 'g': sb.Append("9"); break;
|
||||
case 'G': sb.Append("9"); break;
|
||||
case 'l': sb.Append("£"); break;
|
||||
case 'L': sb.Append("£"); break;
|
||||
case 'c': sb.Append("("); break;
|
||||
case 'C': sb.Append("("); break;
|
||||
case 't': sb.Append("7"); break;
|
||||
case 'T': sb.Append("7"); break;
|
||||
case 'z': sb.Append("2"); break;
|
||||
case 'Z': sb.Append("2"); break;
|
||||
case 'y': sb.Append("¥"); break;
|
||||
case 'Y': sb.Append("¥"); break;
|
||||
case 'u': sb.Append("µ"); break;
|
||||
case 'U': sb.Append("µ"); break;
|
||||
case 'f': sb.Append("ƒ"); break;
|
||||
case 'F': sb.Append("ƒ"); break;
|
||||
case 'd': sb.Append("Ð"); break;
|
||||
case 'D': sb.Append("Ð"); break;
|
||||
case 'n': sb.Append(@"|\\|"); break;
|
||||
case 'N': sb.Append(@"|\\|"); break;
|
||||
case 'w': sb.Append(@"\\/\\/"); break;
|
||||
case 'W': sb.Append(@"\\/\\/"); break;
|
||||
case 'h': sb.Append("|-|"); break;
|
||||
case 'H': sb.Append("|-|"); break;
|
||||
case 'v': sb.Append(@"\\/"); break;
|
||||
case 'V': sb.Append(@"\\/"); break;
|
||||
case 'k': sb.Append("|{"); break;
|
||||
case 'K': sb.Append("|{"); break;
|
||||
case 'r': sb.Append("®"); break;
|
||||
case 'R': sb.Append("®"); break;
|
||||
case 'm': sb.Append(@"|\\/|"); break;
|
||||
case 'M': sb.Append(@"|\\/|"); break;
|
||||
case 'b': sb.Append("ß"); break;
|
||||
case 'B': sb.Append("ß"); break;
|
||||
case 'j': sb.Append("_|"); break;
|
||||
case 'J': sb.Append("_|"); break;
|
||||
case 'P': sb.Append("|°"); break;
|
||||
case 'q': sb.Append("¶"); break;
|
||||
case 'Q': sb.Append("¶¸"); break;
|
||||
case 'x': sb.Append(")("); break;
|
||||
case 'X': sb.Append(")("); break;
|
||||
default: sb.Append(c); break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
return sb.ToString().TrimTo(1995); // Return result.
|
||||
}
|
||||
}
|
||||
}
|
128
NadekoBot.Core/Modules/Games/NunchiCommands.cs
Normal file
128
NadekoBot.Core/Modules/Games/NunchiCommands.cs
Normal file
@ -0,0 +1,128 @@
|
||||
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;
|
||||
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 Nunchi(Context.User.Id, Context.User.ToString());
|
||||
Nunchi nunchi;
|
||||
|
||||
//if a game was already active
|
||||
if ((nunchi = _service.NunchiGames.GetOrAdd(Context.Guild.Id, newNunchi)) != newNunchi)
|
||||
{
|
||||
// join it
|
||||
if (!await nunchi.Join(Context.User.Id, Context.User.ToString()))
|
||||
{
|
||||
// if you failed joining, that means game is running or just ended
|
||||
// await ReplyErrorLocalized("nunchi_already_started").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyConfirmLocalized("nunchi_joined", nunchi.ParticipantCount).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try { await ConfirmLocalized("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(Context.Guild.Id, out var game))
|
||||
game.Dispose();
|
||||
await ConfirmLocalized("nunchi_failed_to_start").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Task _client_MessageReceived(SocketMessage arg)
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
if (arg.Channel.Id != Context.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 (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task Nunchi_OnGameEnded(Nunchi arg1, string arg2)
|
||||
{
|
||||
if (_service.NunchiGames.TryRemove(Context.Guild.Id, out var game))
|
||||
{
|
||||
_client.MessageReceived -= _client_MessageReceived;
|
||||
game.Dispose();
|
||||
}
|
||||
|
||||
if (arg2 == null)
|
||||
return ConfirmLocalized("nunchi_ended_no_winner", Format.Bold(arg2));
|
||||
else
|
||||
return ConfirmLocalized("nunchi_ended", Format.Bold(arg2));
|
||||
}
|
||||
}
|
||||
|
||||
private Task Nunchi_OnRoundStarted(Nunchi arg, int cur)
|
||||
{
|
||||
return ConfirmLocalized("nunchi_round_started",
|
||||
Format.Bold(arg.ParticipantCount.ToString()),
|
||||
Format.Bold(cur.ToString()));
|
||||
}
|
||||
|
||||
private Task Nunchi_OnUserGuessed(Nunchi arg)
|
||||
{
|
||||
return ConfirmLocalized("nunchi_next_number", Format.Bold(arg.CurrentNumber.ToString()));
|
||||
}
|
||||
|
||||
private Task Nunchi_OnRoundEnded(Nunchi arg1, (ulong Id, string Name)? arg2)
|
||||
{
|
||||
if(arg2.HasValue)
|
||||
return ConfirmLocalized("nunchi_round_ended", Format.Bold(arg2.Value.Name));
|
||||
else
|
||||
return ConfirmLocalized("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(Nunchi arg)
|
||||
{
|
||||
return ConfirmLocalized("nunchi_started", Format.Bold(arg.ParticipantCount.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
144
NadekoBot.Core/Modules/Games/PlantAndPickCommands.cs
Normal file
144
NadekoBot.Core/Modules/Games/PlantAndPickCommands.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
/// <summary>
|
||||
/// Flower picking/planting idea is given to me by its
|
||||
/// inceptor Violent Crumble from Game Developers League discord server
|
||||
/// (he has !cookie and !nom) Thanks a lot Violent!
|
||||
/// Check out GDL (its a growing gamedev community):
|
||||
/// https://discord.gg/0TYNJfCU4De7YIk8
|
||||
/// </summary>
|
||||
[Group]
|
||||
public class PlantPickCommands : NadekoSubmodule<GamesService>
|
||||
{
|
||||
private readonly CurrencyService _cs;
|
||||
private readonly IBotConfigProvider _bc;
|
||||
private readonly DbService _db;
|
||||
|
||||
public PlantPickCommands(IBotConfigProvider bc, CurrencyService cs,
|
||||
DbService db)
|
||||
{
|
||||
_bc = bc;
|
||||
_cs = cs;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Pick()
|
||||
{
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
|
||||
if (!(await channel.Guild.GetCurrentUserAsync()).GetPermissions(channel).ManageMessages)
|
||||
return;
|
||||
|
||||
|
||||
try { await Context.Message.DeleteAsync().ConfigureAwait(false); } catch { }
|
||||
if (!_service.PlantedFlowers.TryRemove(channel.Id, out List<IUserMessage> msgs))
|
||||
return;
|
||||
|
||||
await Task.WhenAll(msgs.Where(m => m != null).Select(toDelete => toDelete.DeleteAsync())).ConfigureAwait(false);
|
||||
|
||||
await _cs.AddAsync((IGuildUser)Context.User, $"Picked {_bc.BotConfig.CurrencyPluralName}", msgs.Count, false).ConfigureAwait(false);
|
||||
var msg = await ReplyConfirmLocalized("picked", msgs.Count + _bc.BotConfig.CurrencySign)
|
||||
.ConfigureAwait(false);
|
||||
msg.DeleteAfter(10);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Plant(int amount = 1)
|
||||
{
|
||||
if (amount < 1)
|
||||
return;
|
||||
|
||||
var removed = await _cs.RemoveAsync((IGuildUser)Context.User, $"Planted a {_bc.BotConfig.CurrencyName}", amount, false).ConfigureAwait(false);
|
||||
if (!removed)
|
||||
{
|
||||
await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var imgData = _service.GetRandomCurrencyImage();
|
||||
|
||||
var msgToSend = GetText("planted",
|
||||
Format.Bold(Context.User.ToString()),
|
||||
amount + _bc.BotConfig.CurrencySign,
|
||||
Prefix);
|
||||
|
||||
if (amount > 1)
|
||||
msgToSend += " " + GetText("pick_pl", Prefix);
|
||||
else
|
||||
msgToSend += " " + GetText("pick_sn", Prefix);
|
||||
|
||||
IUserMessage msg;
|
||||
using (var toSend = imgData.Data.ToStream())
|
||||
{
|
||||
msg = await Context.Channel.SendFileAsync(toSend, imgData.Name, msgToSend).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var msgs = new IUserMessage[amount];
|
||||
msgs[0] = msg;
|
||||
|
||||
_service.PlantedFlowers.AddOrUpdate(Context.Channel.Id, msgs.ToList(), (id, old) =>
|
||||
{
|
||||
old.AddRange(msgs);
|
||||
return old;
|
||||
});
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[RequireUserPermission(GuildPermission.ManageMessages)]
|
||||
#if GLOBAL_NADEKO
|
||||
[OwnerOnly]
|
||||
#endif
|
||||
public async Task GenCurrency()
|
||||
{
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
|
||||
bool enabled;
|
||||
using (var uow = _db.UnitOfWork)
|
||||
{
|
||||
var guildConfig = uow.GuildConfigs.For(channel.Guild.Id, set => set.Include(gc => gc.GenerateCurrencyChannelIds));
|
||||
|
||||
var toAdd = new GCChannelId() { ChannelId = channel.Id };
|
||||
if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd))
|
||||
{
|
||||
guildConfig.GenerateCurrencyChannelIds.Add(toAdd);
|
||||
_service.GenerationChannels.Add(channel.Id);
|
||||
enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
guildConfig.GenerateCurrencyChannelIds.Remove(toAdd);
|
||||
_service.GenerationChannels.TryRemove(channel.Id);
|
||||
enabled = false;
|
||||
}
|
||||
await uow.CompleteAsync();
|
||||
}
|
||||
if (enabled)
|
||||
{
|
||||
await ReplyConfirmLocalized("curgen_enabled").ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalized("curgen_disabled").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
60
NadekoBot.Core/Modules/Games/PollCommands.cs
Normal file
60
NadekoBot.Core/Modules/Games/PollCommands.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
|
||||
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]
|
||||
[RequireUserPermission(GuildPermission.ManageMessages)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public Task Poll([Remainder] string arg = null)
|
||||
=> InternalStartPoll(arg);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireUserPermission(GuildPermission.ManageMessages)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task PollStats()
|
||||
{
|
||||
if (!_service.ActivePolls.TryGetValue(Context.Guild.Id, out var poll))
|
||||
return;
|
||||
|
||||
await Context.Channel.EmbedAsync(poll.GetStats(GetText("current_poll_results")));
|
||||
}
|
||||
|
||||
private async Task InternalStartPoll(string arg)
|
||||
{
|
||||
if(await _service.StartPoll((ITextChannel)Context.Channel, Context.Message, arg) == false)
|
||||
await ReplyErrorLocalized("poll_already_running").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireUserPermission(GuildPermission.ManageMessages)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Pollend()
|
||||
{
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
|
||||
_service.ActivePolls.TryRemove(channel.Guild.Id, out var poll);
|
||||
await poll.StopPoll().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
145
NadekoBot.Core/Modules/Games/Services/ChatterbotService.cs
Normal file
145
NadekoBot.Core/Modules/Games/Services/ChatterbotService.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
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.Core.Services.Database.Models;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using NLog;
|
||||
using NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Services
|
||||
{
|
||||
public class ChatterBotService : IEarlyBlockingExecutor, INService
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly Logger _log;
|
||||
private readonly PermissionService _perms;
|
||||
private readonly CommandHandler _cmd;
|
||||
private readonly NadekoStrings _strings;
|
||||
private readonly IBotCredentials _creds;
|
||||
|
||||
public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
|
||||
|
||||
public ChatterBotService(DiscordSocketClient client, PermissionService perms,
|
||||
NadekoBot bot, CommandHandler cmd, NadekoStrings strings,
|
||||
IBotCredentials creds)
|
||||
{
|
||||
_client = client;
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
_perms = perms;
|
||||
_cmd = cmd;
|
||||
_strings = strings;
|
||||
_creds = creds;
|
||||
|
||||
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 ChatterBotSession();
|
||||
else
|
||||
return new OfficialCleverbotSession(_creds.CleverbotApiKey);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
message = msg.Content.Substring(normalMention.Length).Trim();
|
||||
}
|
||||
else if (msg.Content.StartsWith(nickMention))
|
||||
{
|
||||
message = msg.Content.Substring(nickMention.Length).Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public async Task<bool> TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message)
|
||||
{
|
||||
await channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
|
||||
var response = await cleverbot.Think(message).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await channel.SendConfirmAsync(response.SanitizeMentions()).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await channel.SendConfirmAsync(response.SanitizeMentions()).ConfigureAwait(false); // try twice :\
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> TryExecuteEarly(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.GetCache(guild.Id);
|
||||
if (!pc.Permissions.CheckPermissions(usrMsg,
|
||||
"cleverbot",
|
||||
"Games".ToLowerInvariant(),
|
||||
out int index))
|
||||
{
|
||||
if (pc.Verbose)
|
||||
{
|
||||
var returnMsg = _strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild)));
|
||||
try { await usrMsg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { }
|
||||
_log.Info(returnMsg);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message).ConfigureAwait(false);
|
||||
if (cleverbotExecuted)
|
||||
{
|
||||
_log.Info($@"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.Warn(ex, "Error in cleverbot"); }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
215
NadekoBot.Core/Modules/Games/Services/GamesService.cs
Normal file
215
NadekoBot.Core/Modules/Games/Services/GamesService.cs
Normal file
@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Collections;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NadekoBot.Modules.Games.Common.Acrophobia;
|
||||
using NadekoBot.Modules.Games.Common.Connect4;
|
||||
using NadekoBot.Modules.Games.Common.Hangman;
|
||||
using NadekoBot.Modules.Games.Common.Trivia;
|
||||
using NadekoBot.Modules.Games.Common.Nunchi;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Services
|
||||
{
|
||||
public class GamesService : INService, IUnloadableService
|
||||
{
|
||||
private readonly IBotConfigProvider _bc;
|
||||
|
||||
public readonly ConcurrentDictionary<ulong, GirlRating> GirlRatings = new ConcurrentDictionary<ulong, GirlRating>();
|
||||
public readonly ImmutableArray<string> EightBallResponses;
|
||||
|
||||
private readonly Timer _t;
|
||||
private readonly CommandHandler _cmd;
|
||||
private readonly NadekoStrings _strings;
|
||||
private readonly IImagesService _images;
|
||||
private readonly Logger _log;
|
||||
|
||||
public readonly string TypingArticlesPath = "data/typing_articles2.json";
|
||||
private readonly CommandHandler _cmdHandler;
|
||||
|
||||
public List<TypingArticle> TypingArticles { get; } = new List<TypingArticle>();
|
||||
|
||||
//channelId, game
|
||||
public ConcurrentDictionary<ulong, Acrophobia> AcrophobiaGames { get; } = new ConcurrentDictionary<ulong, Acrophobia>();
|
||||
public ConcurrentDictionary<ulong, Connect4Game> Connect4Games { get; } = new ConcurrentDictionary<ulong, Connect4Game>();
|
||||
|
||||
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, Nunchi> NunchiGames { get; } = new ConcurrentDictionary<ulong, Common.Nunchi.Nunchi>();
|
||||
|
||||
public GamesService(CommandHandler cmd, IBotConfigProvider bc, NadekoBot bot,
|
||||
NadekoStrings strings, IImagesService images, CommandHandler cmdHandler)
|
||||
{
|
||||
_bc = bc;
|
||||
_cmd = cmd;
|
||||
_strings = strings;
|
||||
_images = images;
|
||||
_cmdHandler = cmdHandler;
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
//8ball
|
||||
EightBallResponses = _bc.BotConfig.EightBallResponses.Select(ebr => ebr.Text).ToImmutableArray();
|
||||
|
||||
//girl ratings
|
||||
_t = new Timer((_) =>
|
||||
{
|
||||
GirlRatings.Clear();
|
||||
|
||||
}, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1));
|
||||
|
||||
//plantpick
|
||||
_cmd.OnMessageNoTrigger += PotentialFlowerGeneration;
|
||||
GenerationChannels = new ConcurrentHashSet<ulong>(bot
|
||||
.AllGuildConfigs
|
||||
.SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId)));
|
||||
|
||||
try
|
||||
{
|
||||
TypingArticles = JsonConvert.DeserializeObject<List<TypingArticle>>(File.ReadAllText(TypingArticlesPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn("Error while loading typing articles {0}", ex.ToString());
|
||||
TypingArticles = new List<TypingArticle>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Unload()
|
||||
{
|
||||
_t.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
_cmd.OnMessageNoTrigger -= PotentialFlowerGeneration;
|
||||
|
||||
AcrophobiaGames.ForEach(x => x.Value.Dispose());
|
||||
AcrophobiaGames.Clear();
|
||||
Connect4Games.ForEach(x => x.Value.Dispose());
|
||||
Connect4Games.Clear();
|
||||
HangmanGames.ForEach(x => x.Value.Dispose());
|
||||
HangmanGames.Clear();
|
||||
await Task.WhenAll(RunningTrivias.Select(x => x.Value.StopGame()));
|
||||
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();
|
||||
}
|
||||
|
||||
private void DisposeElems(IEnumerable<IDisposable> xs)
|
||||
{
|
||||
xs.ForEach(x => x.Dispose());
|
||||
}
|
||||
|
||||
public void AddTypingArticle(IUser user, string text)
|
||||
{
|
||||
TypingArticles.Add(new TypingArticle
|
||||
{
|
||||
Title = $"Text added on {DateTime.UtcNow} by {user}",
|
||||
Text = text.SanitizeMentions(),
|
||||
});
|
||||
|
||||
File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(TypingArticles));
|
||||
}
|
||||
|
||||
public ConcurrentHashSet<ulong> GenerationChannels { get; }
|
||||
//channelid/message
|
||||
public ConcurrentDictionary<ulong, List<IUserMessage>> PlantedFlowers { get; } = new ConcurrentDictionary<ulong, List<IUserMessage>>();
|
||||
//channelId/last generation
|
||||
public ConcurrentDictionary<ulong, DateTime> LastGenerations { get; } = new ConcurrentDictionary<ulong, DateTime>();
|
||||
|
||||
private ConcurrentDictionary<ulong, object> _locks { get; } = new ConcurrentDictionary<ulong, object>();
|
||||
|
||||
public (string Name, ImmutableArray<byte> Data) GetRandomCurrencyImage()
|
||||
{
|
||||
var rng = new NadekoRandom();
|
||||
return _images.Currency[rng.Next(0, _images.Currency.Length)];
|
||||
}
|
||||
|
||||
private string GetText(ITextChannel ch, string key, params object[] rep)
|
||||
=> _strings.GetText(key, ch.GuildId, "Games".ToLowerInvariant(), rep);
|
||||
|
||||
private Task PotentialFlowerGeneration(IUserMessage imsg)
|
||||
{
|
||||
var msg = imsg as SocketUserMessage;
|
||||
if (msg == null || msg.Author.IsBot)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var channel = imsg.Channel as ITextChannel;
|
||||
if (channel == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (!GenerationChannels.Contains(channel.Id))
|
||||
return Task.CompletedTask;
|
||||
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue);
|
||||
var rng = new NadekoRandom();
|
||||
|
||||
if (DateTime.UtcNow - TimeSpan.FromSeconds(_bc.BotConfig.CurrencyGenerationCooldown) < lastGeneration) //recently generated in this channel, don't generate again
|
||||
return;
|
||||
|
||||
var num = rng.Next(1, 101) + _bc.BotConfig.CurrencyGenerationChance * 100;
|
||||
if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration))
|
||||
{
|
||||
var dropAmount = _bc.BotConfig.CurrencyDropAmount;
|
||||
var dropAmountMax = _bc.BotConfig.CurrencyDropAmountMax;
|
||||
|
||||
if (dropAmountMax != null && dropAmountMax > dropAmount)
|
||||
dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax.Value + 1);
|
||||
|
||||
if (dropAmount > 0)
|
||||
{
|
||||
var msgs = new IUserMessage[dropAmount];
|
||||
var prefix = _cmdHandler.GetPrefix(channel.Guild.Id);
|
||||
var toSend = dropAmount == 1
|
||||
? GetText(channel, "curgen_sn", _bc.BotConfig.CurrencySign)
|
||||
+ " " + GetText(channel, "pick_sn", prefix)
|
||||
: GetText(channel, "curgen_pl", dropAmount, _bc.BotConfig.CurrencySign)
|
||||
+ " " + GetText(channel, "pick_pl", prefix);
|
||||
var file = GetRandomCurrencyImage();
|
||||
using (var fileStream = file.Data.ToStream())
|
||||
{
|
||||
var sent = await channel.SendFileAsync(
|
||||
fileStream,
|
||||
file.Name,
|
||||
toSend).ConfigureAwait(false);
|
||||
|
||||
msgs[0] = sent;
|
||||
}
|
||||
|
||||
PlantedFlowers.AddOrUpdate(channel.Id, msgs.ToList(), (id, old) => { old.AddRange(msgs); return old; });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.GetCurrentClassLogger().Warn(ex);
|
||||
}
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
71
NadekoBot.Core/Modules/Games/Services/PollService.cs
Normal file
71
NadekoBot.Core/Modules/Games/Services/PollService.cs
Normal file
@ -0,0 +1,71 @@
|
||||
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.Impl;
|
||||
using NLog;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Services
|
||||
{
|
||||
public class PollService : IEarlyBlockingExecutor, INService
|
||||
{
|
||||
public ConcurrentDictionary<ulong, Poll> ActivePolls = new ConcurrentDictionary<ulong, Poll>();
|
||||
private readonly Logger _log;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly NadekoStrings _strings;
|
||||
|
||||
public PollService(DiscordSocketClient client, NadekoStrings strings)
|
||||
{
|
||||
_log = LogManager.GetCurrentClassLogger();
|
||||
_client = client;
|
||||
_strings = strings;
|
||||
}
|
||||
|
||||
public async Task<bool?> StartPoll(ITextChannel channel, IUserMessage msg, string arg)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(arg) || !arg.Contains(";"))
|
||||
return null;
|
||||
var data = arg.Split(';');
|
||||
if (data.Length < 3)
|
||||
return null;
|
||||
|
||||
var poll = new Poll(_client, _strings, msg, data[0], data.Skip(1));
|
||||
if (ActivePolls.TryAdd(channel.Guild.Id, poll))
|
||||
{
|
||||
poll.OnEnded += (gid) =>
|
||||
{
|
||||
ActivePolls.TryRemove(gid, out _);
|
||||
};
|
||||
|
||||
await poll.StartPoll().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> TryExecuteEarly(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.Warn(ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
120
NadekoBot.Core/Modules/Games/SpeedTypingCommands.cs
Normal file
120
NadekoBot.Core/Modules/Games/SpeedTypingCommands.cs
Normal file
@ -0,0 +1,120 @@
|
||||
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;
|
||||
|
||||
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)]
|
||||
public async Task TypeStart()
|
||||
{
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
|
||||
var game = _service.RunningContests.GetOrAdd(channel.Guild.Id, id => new TypingGame(_games, _client, channel, Prefix));
|
||||
|
||||
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)Context.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([Remainder] string text)
|
||||
{
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
_games.AddTypingArticle(Context.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)Context.Channel;
|
||||
|
||||
if (page < 1)
|
||||
return;
|
||||
|
||||
var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray();
|
||||
|
||||
if (!articles.Any())
|
||||
{
|
||||
await channel.SendErrorAsync($"{Context.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 channel = (ITextChannel)Context.Channel;
|
||||
|
||||
index -= 1;
|
||||
if (index < 0 || index >= _games.TypingArticles.Count)
|
||||
return;
|
||||
|
||||
var removed = _games.TypingArticles[index];
|
||||
_games.TypingArticles.RemoveAt(index);
|
||||
|
||||
File.WriteAllText(_games.TypingArticlesPath, JsonConvert.SerializeObject(_games.TypingArticles));
|
||||
|
||||
await channel.SendConfirmAsync($"`Removed typing article:` #{index + 1} - {removed.Text.TrimTo(50)}")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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.Extensions;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using NadekoBot.Modules.Games.Services;
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
|
||||
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)]
|
||||
public async Task TicTacToe()
|
||||
{
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
|
||||
await _sem.WaitAsync(1000);
|
||||
try
|
||||
{
|
||||
if (_service.TicTacToeGames.TryGetValue(channel.Id, out TicTacToe game))
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
await game.Start((IGuildUser)Context.User);
|
||||
});
|
||||
return;
|
||||
}
|
||||
game = new TicTacToe(base._strings, this._client, channel, (IGuildUser)Context.User);
|
||||
_service.TicTacToeGames.Add(channel.Id, game);
|
||||
await ReplyConfirmLocalized("ttt_created").ConfigureAwait(false);
|
||||
|
||||
game.OnEnded += (g) =>
|
||||
{
|
||||
_service.TicTacToeGames.Remove(channel.Id);
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sem.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
98
NadekoBot.Core/Modules/Games/TriviaCommands.cs
Normal file
98
NadekoBot.Core/Modules/Games/TriviaCommands.cs
Normal file
@ -0,0 +1,98 @@
|
||||
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;
|
||||
|
||||
namespace NadekoBot.Modules.Games
|
||||
{
|
||||
public partial class Games
|
||||
{
|
||||
[Group]
|
||||
public class TriviaCommands : NadekoSubmodule<GamesService>
|
||||
{
|
||||
private readonly CurrencyService _cs;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotConfigProvider _bc;
|
||||
|
||||
public TriviaCommands(DiscordSocketClient client, IBotConfigProvider bc, CurrencyService cs)
|
||||
{
|
||||
_cs = cs;
|
||||
_client = client;
|
||||
_bc = bc;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public Task Trivia([Remainder] string additionalArgs = "")
|
||||
=> InternalTrivia(10, additionalArgs);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public Task Trivia(int winReq = 10, [Remainder] string additionalArgs = "")
|
||||
=> InternalTrivia(winReq, additionalArgs);
|
||||
|
||||
public async Task InternalTrivia(int winReq, string additionalArgs = "")
|
||||
{
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
|
||||
additionalArgs = additionalArgs?.Trim()?.ToLowerInvariant();
|
||||
|
||||
var showHints = !additionalArgs.Contains("nohint");
|
||||
var isPokemon = additionalArgs.Contains("pokemon");
|
||||
|
||||
var trivia = new TriviaGame(_strings, _client, _bc, _cs, channel.Guild, channel, showHints, winReq, isPokemon);
|
||||
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 Context.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)Context.Channel;
|
||||
|
||||
if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia))
|
||||
{
|
||||
await channel.SendConfirmAsync(GetText("leaderboard"), trivia.GetLeaderboard()).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyErrorLocalized("trivia_none").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Tq()
|
||||
{
|
||||
var channel = (ITextChannel)Context.Channel;
|
||||
|
||||
if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia))
|
||||
{
|
||||
await trivia.StopGame().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyErrorLocalized("trivia_none").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user