games module done too. Searches and music left.

This commit is contained in:
Kwoth 2017-02-23 23:30:38 +01:00
parent d0fa050c30
commit 00629fa45f
12 changed files with 544 additions and 170 deletions

View File

@ -19,9 +19,9 @@ namespace NadekoBot.Modules.Games
[Group]
public class CleverBotCommands : NadekoSubmodule
{
private static new Logger _log { get; }
private new static Logger _log { get; }
public static ConcurrentDictionary<ulong, Lazy<ChatterBotSession>> CleverbotGuilds { get; } = new ConcurrentDictionary<ulong, Lazy<ChatterBotSession>>();
public static ConcurrentDictionary<ulong, Lazy<ChatterBotSession>> CleverbotGuilds { get; }
static CleverBotCommands()
{
@ -96,7 +96,7 @@ namespace NadekoBot.Modules.Games
uow.GuildConfigs.SetCleverbotEnabled(Context.Guild.Id, false);
await uow.CompleteAsync().ConfigureAwait(false);
}
await Context.Channel.SendConfirmAsync($"{Context.User.Mention} Disabled cleverbot on this server.").ConfigureAwait(false);
await ReplyConfirmLocalized("cleverbot_disabled").ConfigureAwait(false);
return;
}
@ -110,7 +110,7 @@ namespace NadekoBot.Modules.Games
await uow.CompleteAsync().ConfigureAwait(false);
}
await Context.Channel.SendConfirmAsync($"{Context.User.Mention} Enabled cleverbot on this server.").ConfigureAwait(false);
await ReplyConfirmLocalized("cleverbot_enabled").ConfigureAwait(false);
}
}
}

View File

@ -1,8 +1,6 @@
using Discord.Commands;
using NadekoBot.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Commands.Hangman;
using NLog;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
@ -21,7 +19,7 @@ namespace NadekoBot.Modules.Games
[NadekoCommand, Usage, Description, Aliases]
public async Task Hangmanlist()
{
await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + String.Join(", ", HangmanTermPool.data.Keys));
await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join(", ", HangmanTermPool.data.Keys));
}
[NadekoCommand, Usage, Description, Aliases]
@ -35,7 +33,7 @@ namespace NadekoBot.Modules.Games
return;
}
hm.OnEnded += (g) =>
hm.OnEnded += g =>
{
HangmanGame throwaway;
HangmanGames.TryRemove(g.GameChannel.Id, out throwaway);

View File

@ -11,10 +11,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games
@ -31,7 +28,7 @@ namespace NadekoBot.Modules.Games
[Group]
public class PlantPickCommands : NadekoSubmodule
{
private static ConcurrentHashSet<ulong> generationChannels { get; } = new ConcurrentHashSet<ulong>();
private static ConcurrentHashSet<ulong> generationChannels { get; }
//channelid/message
private static ConcurrentDictionary<ulong, List<IUserMessage>> plantedFlowers { get; } = new ConcurrentDictionary<ulong, List<IUserMessage>>();
//channelId/last generation
@ -82,25 +79,17 @@ namespace NadekoBot.Modules.Games
if (dropAmount > 0)
{
var msgs = new IUserMessage[dropAmount];
string firstPart;
if (dropAmount == 1)
{
firstPart = $"A random { NadekoBot.BotConfig.CurrencyName } appeared!";
}
else
{
firstPart = $"{dropAmount} random { NadekoBot.BotConfig.CurrencyPluralName } appeared!";
}
var prefix = NadekoBot.ModulePrefixes[typeof(Games).Name];
var toSend = dropAmount == 1
? GetLocalText(channel, "curgen_sn", NadekoBot.BotConfig.CurrencySign, prefix)
: GetLocalText(channel, "curgen_pl", NadekoBot.BotConfig.CurrencySign, prefix);
var file = GetRandomCurrencyImage();
using (var fileStream = file.Value.ToStream())
{
var sent = await channel.SendFileAsync(
fileStream,
file.Key,
string.Format("❗ {0} Pick it up by typing `{1}pick`", firstPart,
NadekoBot.ModulePrefixes[typeof(Games).Name]))
.ConfigureAwait(false);
toSend).ConfigureAwait(false);
msgs[0] = sent;
}
@ -117,6 +106,12 @@ namespace NadekoBot.Modules.Games
return Task.CompletedTask;
}
public static string GetLocalText(ITextChannel channel, string key, params object[] replacements) =>
NadekoTopLevelModule.GetTextStatic(key,
NadekoBot.Localization.GetCultureInfo(channel.GuildId),
typeof(Games).Name.ToLowerInvariant(),
replacements);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Pick()
@ -135,7 +130,8 @@ namespace NadekoBot.Modules.Games
await Task.WhenAll(msgs.Where(m => m != null).Select(toDelete => toDelete.DeleteAsync())).ConfigureAwait(false);
await CurrencyHandler.AddCurrencyAsync((IGuildUser)Context.User, $"Picked {NadekoBot.BotConfig.CurrencyPluralName}", msgs.Count, false).ConfigureAwait(false);
var msg = await channel.SendConfirmAsync($"**{Context.User}** picked {msgs.Count}{NadekoBot.BotConfig.CurrencySign}!").ConfigureAwait(false);
var msg = await ReplyConfirmLocalized("picked", msgs.Count + NadekoBot.BotConfig.CurrencySign)
.ConfigureAwait(false);
msg.DeleteAfter(10);
}
@ -149,14 +145,19 @@ namespace NadekoBot.Modules.Games
var removed = await CurrencyHandler.RemoveCurrencyAsync((IGuildUser)Context.User, $"Planted a {NadekoBot.BotConfig.CurrencyName}", amount, false).ConfigureAwait(false);
if (!removed)
{
await Context.Channel.SendErrorAsync($"You don't have enough {NadekoBot.BotConfig.CurrencyPluralName}.").ConfigureAwait(false);
await ReplyErrorLocalized("not_enough", NadekoBot.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
var imgData = GetRandomCurrencyImage();
var vowelFirst = new[] { 'a', 'e', 'i', 'o', 'u' }.Contains(NadekoBot.BotConfig.CurrencyName[0]);
var msgToSend = $"Oh how Nice! **{Context.User.Username}** planted {(amount == 1 ? (vowelFirst ? "an" : "a") : amount.ToString())} {(amount > 1 ? NadekoBot.BotConfig.CurrencyPluralName : NadekoBot.BotConfig.CurrencyName)}. Pick it using {Prefix}pick";
//todo upload all currency images to transfer.sh and use that one as cdn
//and then
var msgToSend = GetText("planted",
Format.Bold(Context.User.ToString()),
amount + NadekoBot.BotConfig.CurrencySign,
Prefix);
IUserMessage msg;
using (var toSend = imgData.Value.ToStream())

View File

@ -3,12 +3,10 @@ using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Attributes;
using NadekoBot.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games
@ -24,20 +22,20 @@ namespace NadekoBot.Modules.Games
[RequireUserPermission(GuildPermission.ManageMessages)]
[RequireContext(ContextType.Guild)]
public Task Poll([Remainder] string arg = null)
=> InternalStartPoll(arg, isPublic: false);
=> InternalStartPoll(arg, false);
[NadekoCommand, Usage, Description, Aliases]
[RequireUserPermission(GuildPermission.ManageMessages)]
[RequireContext(ContextType.Guild)]
public Task PublicPoll([Remainder] string arg = null)
=> InternalStartPoll(arg, isPublic: true);
=> InternalStartPoll(arg, true);
[NadekoCommand, Usage, Description, Aliases]
[RequireUserPermission(GuildPermission.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task PollStats()
{
Games.Poll poll;
Poll poll;
if (!ActivePolls.TryGetValue(Context.Guild.Id, out poll))
return;
@ -78,27 +76,25 @@ namespace NadekoBot.Modules.Games
public class Poll
{
private readonly IUserMessage originalMessage;
private readonly IGuild guild;
private string[] Answers { get; }
private ConcurrentDictionary<ulong, int> participants = new ConcurrentDictionary<ulong, int>();
private readonly string question;
private DateTime started;
private CancellationTokenSource pollCancellationSource = new CancellationTokenSource();
private readonly IUserMessage _originalMessage;
private readonly IGuild _guild;
private string[] answers { get; }
private readonly ConcurrentDictionary<ulong, int> _participants = new ConcurrentDictionary<ulong, int>();
private readonly string _question;
public bool IsPublic { get; }
public Poll(IUserMessage umsg, string question, IEnumerable<string> enumerable, bool isPublic = false)
{
this.originalMessage = umsg;
this.guild = ((ITextChannel)umsg.Channel).Guild;
this.question = question;
this.Answers = enumerable as string[] ?? enumerable.ToArray();
this.IsPublic = isPublic;
_originalMessage = umsg;
_guild = ((ITextChannel)umsg.Channel).Guild;
_question = question;
answers = enumerable as string[] ?? enumerable.ToArray();
IsPublic = isPublic;
}
public EmbedBuilder GetStats(string title)
{
var results = participants.GroupBy(kvp => kvp.Value)
var results = _participants.GroupBy(kvp => kvp.Value)
.ToDictionary(x => x.Key, x => x.Sum(kvp => 1))
.OrderByDescending(kvp => kvp.Value)
.ToArray();
@ -106,7 +102,7 @@ namespace NadekoBot.Modules.Games
var eb = new EmbedBuilder().WithTitle(title);
var sb = new StringBuilder()
.AppendLine(Format.Bold(question))
.AppendLine(Format.Bold(_question))
.AppendLine();
var totalVotesCast = 0;
@ -119,7 +115,7 @@ namespace NadekoBot.Modules.Games
for (int i = 0; i < results.Length; i++)
{
var result = results[i];
sb.AppendLine($"`{i + 1}.` {Format.Bold(Answers[result.Key - 1])} with {Format.Bold(result.Value.ToString())} votes.");
sb.AppendLine($"`{i + 1}.` {Format.Bold(answers[result.Key - 1])} with {Format.Bold(result.Value.ToString())} votes.");
totalVotesCast += result.Value;
}
}
@ -133,22 +129,21 @@ namespace NadekoBot.Modules.Games
public async Task StartPoll()
{
started = DateTime.Now;
NadekoBot.Client.MessageReceived += Vote;
var msgToSend = $"📃**{originalMessage.Author.Username}** has created a poll which requires your attention:\n\n**{question}**\n";
var msgToSend = $"📃**{_originalMessage.Author.Username}** has created a poll which requires your attention:\n\n**{_question}**\n";
var num = 1;
msgToSend = Answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n");
msgToSend = answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n");
if (!IsPublic)
msgToSend += "\n**Private Message me with the corresponding number of the answer.**";
else
msgToSend += "\n**Send a Message here with the corresponding number of the answer.**";
await originalMessage.Channel.SendConfirmAsync(msgToSend).ConfigureAwait(false);
await _originalMessage.Channel.SendConfirmAsync(msgToSend).ConfigureAwait(false);
}
public async Task StopPoll()
{
NadekoBot.Client.MessageReceived -= Vote;
await originalMessage.Channel.EmbedAsync(GetStats("POLL CLOSED")).ConfigureAwait(false);
await _originalMessage.Channel.EmbedAsync(GetStats("POLL CLOSED")).ConfigureAwait(false);
}
private async Task Vote(SocketMessage imsg)
@ -164,14 +159,14 @@ namespace NadekoBot.Modules.Games
int vote;
if (!int.TryParse(imsg.Content, out vote))
return;
if (vote < 1 || vote > Answers.Length)
if (vote < 1 || vote > answers.Length)
return;
IMessageChannel ch;
if (IsPublic)
{
//if public, channel must be the same the poll started in
if (originalMessage.Channel.Id != imsg.Channel.Id)
if (_originalMessage.Channel.Id != imsg.Channel.Id)
return;
ch = imsg.Channel;
}
@ -182,13 +177,13 @@ namespace NadekoBot.Modules.Games
return;
// user must be a member of the guild this poll is in
var guildUsers = await guild.GetUsersAsync().ConfigureAwait(false);
if (!guildUsers.Any(u => u.Id == imsg.Author.Id))
var guildUsers = await _guild.GetUsersAsync().ConfigureAwait(false);
if (guildUsers.All(u => u.Id != imsg.Author.Id))
return;
}
//user can vote only once
if (participants.TryAdd(msg.Author.Id, vote))
if (_participants.TryAdd(msg.Author.Id, vote))
{
if (!IsPublic)
{

View File

@ -13,14 +13,13 @@ namespace NadekoBot.Modules.Games
{
public partial class Games
{
//todo timeout
[Group]
public class TicTacToeCommands : NadekoSubmodule
{
//channelId/game
private static readonly Dictionary<ulong, TicTacToe> _games = new Dictionary<ulong, TicTacToe>();
private readonly SemaphoreSlim sem = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
@ -28,7 +27,7 @@ namespace NadekoBot.Modules.Games
{
var channel = (ITextChannel)Context.Channel;
await sem.WaitAsync(1000);
await _sem.WaitAsync(1000);
try
{
TicTacToe game;
@ -42,7 +41,7 @@ namespace NadekoBot.Modules.Games
}
game = new TicTacToe(channel, (IGuildUser)Context.User);
_games.Add(channel.Id, game);
await Context.Channel.SendConfirmAsync($"{Context.User.Mention} Created a TicTacToe game.").ConfigureAwait(false);
await ReplyConfirmLocalized("ttt_created").ConfigureAwait(false);
game.OnEnded += (g) =>
{
@ -51,7 +50,7 @@ namespace NadekoBot.Modules.Games
}
finally
{
sem.Release();
_sem.Release();
}
}
}
@ -70,42 +69,47 @@ namespace NadekoBot.Modules.Games
private readonly IGuildUser[] _users;
private readonly int?[,] _state;
private Phase _phase;
int curUserIndex = 0;
private readonly SemaphoreSlim moveLock;
private int _curUserIndex;
private readonly SemaphoreSlim _moveLock;
private IGuildUser _winner = null;
private IGuildUser _winner;
private readonly string[] numbers = { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:" };
private readonly string[] _numbers = { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:" };
public Action<TicTacToe> OnEnded;
private IUserMessage previousMessage = null;
private Timer timeoutTimer;
private IUserMessage _previousMessage;
private Timer _timeoutTimer;
public TicTacToe(ITextChannel channel, IGuildUser firstUser)
{
_channel = channel;
_users = new IGuildUser[2] { firstUser, null };
_state = new int?[3, 3] {
_users = new[] { firstUser, null };
_state = new int?[,] {
{ null, null, null },
{ null, null, null },
{ null, null, null },
};
_log = LogManager.GetCurrentClassLogger();
_log.Warn($"User {firstUser} created a TicTacToe game.");
_phase = Phase.Starting;
moveLock = new SemaphoreSlim(1, 1);
_moveLock = new SemaphoreSlim(1, 1);
}
private string GetText(string key, params object[] replacements) =>
NadekoTopLevelModule.GetTextStatic(key,
NadekoBot.Localization.GetCultureInfo(_channel.GuildId),
typeof(Games).Name.ToLowerInvariant(),
replacements);
public string GetState()
{
var sb = new StringBuilder();
for (int i = 0; i < _state.GetLength(0); i++)
for (var i = 0; i < _state.GetLength(0); i++)
{
for (int j = 0; j < _state.GetLength(1); j++)
for (var j = 0; j < _state.GetLength(1); j++)
{
sb.Append(_state[i, j] == null ? numbers[i * 3 + j] : GetIcon(_state[i, j]));
sb.Append(_state[i, j] == null ? _numbers[i * 3 + j] : GetIcon(_state[i, j]));
if (j < _state.GetLength(1) - 1)
sb.Append("┃");
}
@ -121,7 +125,7 @@ namespace NadekoBot.Modules.Games
var embed = new EmbedBuilder()
.WithOkColor()
.WithDescription(Environment.NewLine + GetState())
.WithAuthor(eab => eab.WithName($"{_users[0]} vs {_users[1]}"));
.WithAuthor(eab => eab.WithName(GetText("vs", _users[0], _users[1])));
if (!string.IsNullOrWhiteSpace(title))
embed.WithTitle(title);
@ -129,12 +133,12 @@ namespace NadekoBot.Modules.Games
if (_winner == null)
{
if (_phase == Phase.Ended)
embed.WithFooter(efb => efb.WithText($"No moves left!"));
embed.WithFooter(efb => efb.WithText(GetText("ttt_no_moves")));
else
embed.WithFooter(efb => efb.WithText($"{_users[curUserIndex]}'s move"));
embed.WithFooter(efb => efb.WithText(GetText("users_move", _users[_curUserIndex])));
}
else
embed.WithFooter(efb => efb.WithText($"{_winner} Won!"));
embed.WithFooter(efb => efb.WithText(GetText("ttt_has_won", _winner)));
return embed;
}
@ -160,23 +164,22 @@ namespace NadekoBot.Modules.Games
{
if (_phase == Phase.Started || _phase == Phase.Ended)
{
await _channel.SendErrorAsync(user.Mention + " TicTacToe Game is already running in this channel.").ConfigureAwait(false);
await _channel.SendErrorAsync(user.Mention + GetText("ttt_already_running")).ConfigureAwait(false);
return;
}
else if (_users[0] == user)
{
await _channel.SendErrorAsync(user.Mention + " You can't play against yourself.").ConfigureAwait(false);
await _channel.SendErrorAsync(user.Mention + GetText("ttt_against_yourself")).ConfigureAwait(false);
return;
}
_users[1] = user;
_log.Warn($"User {user} joined a TicTacToe game.");
_phase = Phase.Started;
timeoutTimer = new Timer(async (_) =>
_timeoutTimer = new Timer(async (_) =>
{
await moveLock.WaitAsync();
await _moveLock.WaitAsync();
try
{
if (_phase == Phase.Ended)
@ -185,12 +188,13 @@ namespace NadekoBot.Modules.Games
_phase = Phase.Ended;
if (_users[1] != null)
{
_winner = _users[curUserIndex ^= 1];
var del = previousMessage?.DeleteAsync();
_winner = _users[_curUserIndex ^= 1];
var del = _previousMessage?.DeleteAsync();
try
{
await _channel.EmbedAsync(GetEmbed("Time Expired!")).ConfigureAwait(false);
await del.ConfigureAwait(false);
await _channel.EmbedAsync(GetEmbed(GetText("ttt_time_expired"))).ConfigureAwait(false);
if (del != null)
await del.ConfigureAwait(false);
}
catch { }
}
@ -200,21 +204,21 @@ namespace NadekoBot.Modules.Games
catch { }
finally
{
moveLock.Release();
_moveLock.Release();
}
}, null, 15000, Timeout.Infinite);
NadekoBot.Client.MessageReceived += Client_MessageReceived;
previousMessage = await _channel.EmbedAsync(GetEmbed("Game Started")).ConfigureAwait(false);
_previousMessage = await _channel.EmbedAsync(GetEmbed(GetText("game_started"))).ConfigureAwait(false);
}
private bool IsDraw()
{
for (int i = 0; i < 3; i++)
for (var i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
for (var j = 0; j < 3; j++)
{
if (_state[i, j] == null)
return false;
@ -227,10 +231,10 @@ namespace NadekoBot.Modules.Games
{
var _ = Task.Run(async () =>
{
await moveLock.WaitAsync().ConfigureAwait(false);
await _moveLock.WaitAsync().ConfigureAwait(false);
try
{
var curUser = _users[curUserIndex];
var curUser = _users[_curUserIndex];
if (_phase == Phase.Ended || msg.Author?.Id != curUser.Id)
return;
@ -240,53 +244,53 @@ namespace NadekoBot.Modules.Games
index <= 9 &&
_state[index / 3, index % 3] == null)
{
_state[index / 3, index % 3] = curUserIndex;
_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;
_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;
_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])
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;
_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])
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;
_state[0, 2] = _curUserIndex + 2;
_state[1, 1] = _curUserIndex + 2;
_state[2, 0] = _curUserIndex + 2;
_phase = Phase.Ended;
}
string reason = "";
var reason = "";
if (_phase == Phase.Ended) // if user won, stop receiving moves
{
reason = "Matched three!";
_winner = _users[curUserIndex];
reason = GetText("ttt_matched_three");
_winner = _users[_curUserIndex];
NadekoBot.Client.MessageReceived -= Client_MessageReceived;
OnEnded?.Invoke(this);
}
else if (IsDraw())
{
reason = "A draw!";
reason = GetText("ttt_a_draw");
_phase = Phase.Ended;
NadekoBot.Client.MessageReceived -= Client_MessageReceived;
OnEnded?.Invoke(this);
@ -295,19 +299,19 @@ namespace NadekoBot.Modules.Games
var sendstate = Task.Run(async () =>
{
var del1 = msg.DeleteAsync();
var del2 = previousMessage?.DeleteAsync();
try { previousMessage = await _channel.EmbedAsync(GetEmbed(reason)); } catch { }
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;
_curUserIndex ^= 1;
timeoutTimer.Change(15000, Timeout.Infinite);
_timeoutTimer.Change(15000, Timeout.Infinite);
}
}
finally
{
moveLock.Release();
_moveLock.Release();
}
});

View File

@ -17,36 +17,42 @@ namespace NadekoBot.Modules.Games.Trivia
public class TriviaGame
{
private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1);
private Logger _log { get; }
private readonly Logger _log;
public IGuild guild { get; }
public ITextChannel channel { get; }
public IGuild Guild { get; }
public ITextChannel Channel { get; }
private int QuestionDurationMiliseconds { get; } = 30000;
private int HintTimeoutMiliseconds { get; } = 6000;
public bool ShowHints { get; } = true;
private int questionDurationMiliseconds { get; } = 30000;
private int hintTimeoutMiliseconds { get; } = 6000;
public bool ShowHints { get; }
private CancellationTokenSource triviaCancelSource { get; set; }
public TriviaQuestion CurrentQuestion { get; private set; }
public HashSet<TriviaQuestion> oldQuestions { get; } = new HashSet<TriviaQuestion>();
public HashSet<TriviaQuestion> OldQuestions { get; } = new HashSet<TriviaQuestion>();
public ConcurrentDictionary<IGuildUser, int> Users { get; } = new ConcurrentDictionary<IGuildUser, int>();
public bool GameActive { get; private set; } = false;
public bool GameActive { get; private set; }
public bool ShouldStopGame { get; private set; }
public int WinRequirement { get; } = 10;
public int WinRequirement { get; }
public TriviaGame(IGuild guild, ITextChannel channel, bool showHints, int winReq)
{
this._log = LogManager.GetCurrentClassLogger();
_log = LogManager.GetCurrentClassLogger();
this.ShowHints = showHints;
this.guild = guild;
this.channel = channel;
this.WinRequirement = winReq;
ShowHints = showHints;
Guild = guild;
Channel = channel;
WinRequirement = winReq;
}
private string GetText(string key, params object[] replacements) =>
NadekoTopLevelModule.GetTextStatic(key,
NadekoBot.Localization.GetCultureInfo(Channel.GuildId),
typeof(Games).Name.ToLowerInvariant(),
replacements);
public async Task StartGame()
{
while (!ShouldStopGame)
@ -55,26 +61,24 @@ namespace NadekoBot.Modules.Games.Trivia
triviaCancelSource = new CancellationTokenSource();
// load question
CurrentQuestion = TriviaQuestionPool.Instance.GetRandomQuestion(oldQuestions);
if (CurrentQuestion == null ||
string.IsNullOrWhiteSpace(CurrentQuestion.Answer) ||
string.IsNullOrWhiteSpace(CurrentQuestion.Question))
CurrentQuestion = TriviaQuestionPool.Instance.GetRandomQuestion(OldQuestions);
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
{
await channel.SendErrorAsync("Trivia Game", "Failed loading a question.").ConfigureAwait(false);
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
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("Trivia Game")
.AddField(eab => eab.WithName("Category").WithValue(CurrentQuestion.Category))
.AddField(eab => eab.WithName("Question").WithValue(CurrentQuestion.Question));
.WithTitle(GetText("trivia_game"))
.AddField(eab => eab.WithName(GetText("category")).WithValue(CurrentQuestion.Category))
.AddField(eab => eab.WithName(GetText("question")).WithValue(CurrentQuestion.Question));
questionMessage = await channel.EmbedAsync(questionEmbed).ConfigureAwait(false);
questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound ||
ex.HttpCode == System.Net.HttpStatusCode.Forbidden ||
@ -99,7 +103,7 @@ namespace NadekoBot.Modules.Games.Trivia
try
{
//hint
await Task.Delay(HintTimeoutMiliseconds, triviaCancelSource.Token).ConfigureAwait(false);
await Task.Delay(hintTimeoutMiliseconds, triviaCancelSource.Token).ConfigureAwait(false);
if (ShowHints)
try
{
@ -113,7 +117,7 @@ namespace NadekoBot.Modules.Games.Trivia
catch (Exception ex) { _log.Warn(ex); }
//timeout
await Task.Delay(QuestionDurationMiliseconds - HintTimeoutMiliseconds, triviaCancelSource.Token).ConfigureAwait(false);
await Task.Delay(questionDurationMiliseconds - hintTimeoutMiliseconds, triviaCancelSource.Token).ConfigureAwait(false);
}
catch (TaskCanceledException) { } //means someone guessed the answer
@ -124,7 +128,7 @@ namespace NadekoBot.Modules.Games.Trivia
NadekoBot.Client.MessageReceived -= PotentialGuess;
}
if (!triviaCancelSource.IsCancellationRequested)
try { await channel.SendErrorAsync("Trivia Game", $"**Time's up!** The correct answer was **{CurrentQuestion.Answer}**").ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
try { await Channel.SendErrorAsync(GetText("trivia_game"), GetText("trivia_times_up", Format.Bold(CurrentQuestion.Answer))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
await Task.Delay(2000).ConfigureAwait(false);
}
}
@ -133,7 +137,7 @@ namespace NadekoBot.Modules.Games.Trivia
{
ShouldStopGame = true;
await channel.EmbedAsync(new EmbedBuilder().WithOkColor()
await Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName("Trivia Game Ended"))
.WithTitle("Final Results")
.WithDescription(GetLeaderboard())).ConfigureAwait(false);
@ -144,7 +148,7 @@ namespace NadekoBot.Modules.Games.Trivia
var old = ShouldStopGame;
ShouldStopGame = true;
if (!old)
try { await channel.SendConfirmAsync("Trivia Game", "Stopping after this question.").ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
try { await Channel.SendConfirmAsync(GetText("trivia_game"), GetText("trivia_stopping")).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
}
private async Task PotentialGuess(SocketMessage imsg)
@ -155,11 +159,9 @@ namespace NadekoBot.Modules.Games.Trivia
return;
var umsg = imsg as SocketUserMessage;
if (umsg == null)
return;
var textChannel = umsg.Channel as ITextChannel;
if (textChannel == null || textChannel.Guild != guild)
var textChannel = umsg?.Channel as ITextChannel;
if (textChannel == null || textChannel.Guild != Guild)
return;
var guildUser = (IGuildUser)umsg.Author;
@ -182,13 +184,24 @@ namespace NadekoBot.Modules.Games.Trivia
if (Users[guildUser] == WinRequirement)
{
ShouldStopGame = true;
try { await channel.SendConfirmAsync("Trivia Game", $"{guildUser.Mention} guessed it and WON the game! The answer was: **{CurrentQuestion.Answer}**").ConfigureAwait(false); } catch { }
try
{
await Channel.SendConfirmAsync(GetText("trivia_game"),
GetText("trivia_win",
guildUser.Mention,
Format.Bold(CurrentQuestion.Answer))).ConfigureAwait(false);
}
catch
{
// ignored
}
var reward = NadekoBot.BotConfig.TriviaCurrencyReward;
if (reward > 0)
await CurrencyHandler.AddCurrencyAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false);
return;
}
await channel.SendConfirmAsync("Trivia Game", $"{guildUser.Mention} guessed it! The answer was: **{CurrentQuestion.Answer}**").ConfigureAwait(false);
await Channel.SendConfirmAsync(GetText("trivia_game"),
GetText("guess", guildUser.Mention, Format.Bold(CurrentQuestion.Answer))).ConfigureAwait(false);
}
catch (Exception ex) { _log.Warn(ex); }
@ -197,13 +210,13 @@ namespace NadekoBot.Modules.Games.Trivia
public string GetLeaderboard()
{
if (Users.Count == 0)
return "No results.";
return GetText("no_results");
var sb = new StringBuilder();
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value))
{
sb.AppendLine($"**{kvp.Key.Username}** has {kvp.Value} points".ToString().SnPl(kvp.Value));
sb.AppendLine(GetText("trivia_points", Format.Bold(kvp.Key.ToString()), kvp.Value).SnPl(kvp.Value));
}
return sb.ToString();

View File

@ -3,9 +3,7 @@ using Discord.Commands;
using NadekoBot.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Trivia;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
@ -31,7 +29,7 @@ namespace NadekoBot.Modules.Games
var showHints = !additionalArgs.Contains("nohint");
TriviaGame trivia = new TriviaGame(channel.Guild, channel, showHints, winReq);
var trivia = new TriviaGame(channel.Guild, channel, showHints, winReq);
if (RunningTrivias.TryAdd(channel.Guild.Id, trivia))
{
try
@ -45,8 +43,9 @@ namespace NadekoBot.Modules.Games
}
return;
}
else
await Context.Channel.SendErrorAsync("Trivia game is already running on this server.\n" + trivia.CurrentQuestion).ConfigureAwait(false);
await Context.Channel.SendErrorAsync(GetText("trivia_already_running") + "\n" + trivia.CurrentQuestion)
.ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
@ -58,11 +57,11 @@ namespace NadekoBot.Modules.Games
TriviaGame trivia;
if (RunningTrivias.TryGetValue(channel.Guild.Id, out trivia))
{
await channel.SendConfirmAsync("Leaderboard", trivia.GetLeaderboard()).ConfigureAwait(false);
await channel.SendConfirmAsync(GetText("leaderboard"), trivia.GetLeaderboard()).ConfigureAwait(false);
return;
}
await channel.SendErrorAsync("No trivia is running on this server.").ConfigureAwait(false);
await ReplyErrorLocalized("trivia_none").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
@ -78,7 +77,7 @@ namespace NadekoBot.Modules.Games
return;
}
await channel.SendErrorAsync("No trivia is running on this server.").ConfigureAwait(false);
await ReplyErrorLocalized("trivia_none").ConfigureAwait(false);
}
}
}

View File

@ -23,9 +23,9 @@ namespace NadekoBot.Modules.Permissions
[Group]
public class BlacklistCommands : ModuleBase
{
public static ConcurrentHashSet<ulong> BlacklistedUsers { get; set; } = new ConcurrentHashSet<ulong>();
public static ConcurrentHashSet<ulong> BlacklistedGuilds { get; set; } = new ConcurrentHashSet<ulong>();
public static ConcurrentHashSet<ulong> BlacklistedChannels { get; set; } = new ConcurrentHashSet<ulong>();
public static ConcurrentHashSet<ulong> BlacklistedUsers { get; set; }
public static ConcurrentHashSet<ulong> BlacklistedGuilds { get; set; }
public static ConcurrentHashSet<ulong> BlacklistedChannels { get; set; }
static BlacklistCommands()
{
@ -115,7 +115,7 @@ namespace NadekoBot.Modules.Permissions
}
break;
case BlacklistType.Channel:
var item = Games.Games.TriviaCommands.RunningTrivias.FirstOrDefault(kvp => kvp.Value.channel.Id == id);
var item = Games.Games.TriviaCommands.RunningTrivias.FirstOrDefault(kvp => kvp.Value.Channel.Id == id);
Games.Games.TriviaCommands.RunningTrivias.TryRemove(item.Key, out tg);
if (tg != null)
{

View File

@ -145,7 +145,7 @@ namespace NadekoBot.Modules.Utility
uow.Quotes.Remove(q);
await uow.CompleteAsync().ConfigureAwait(false);
sucess = true;
response = GetText("deleted_quote");
response = GetText("quote_deleted");
}
}
if(sucess)

View File

@ -2,4 +2,5 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=modules_005Cadministration_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=modules_005Cgambling_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=modules_005Cgames_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=modules_005Cpermissions_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=modules_005Cutility_005Ccommands/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -2860,6 +2860,33 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Category.
/// </summary>
public static string games_category {
get {
return ResourceManager.GetString("games_category", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Disabled cleverbot on this server..
/// </summary>
public static string games_cleverbot_disabled {
get {
return ResourceManager.GetString("games_cleverbot_disabled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Enabled cleverbot on this server..
/// </summary>
public static string games_cleverbot_enabled {
get {
return ResourceManager.GetString("games_cleverbot_enabled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Currency generation has been disabled on this channel..
/// </summary>
@ -2878,6 +2905,42 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to {0} random {1} appeared! Pick them up by typing `{2}pick`.
/// </summary>
public static string games_curgen_pl {
get {
return ResourceManager.GetString("games_curgen_pl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A random {0} appeared! Pick it up by typing `{1}pick`.
/// </summary>
public static string games_curgen_sn {
get {
return ResourceManager.GetString("games_curgen_sn", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed loading a question..
/// </summary>
public static string games_failed_loading_question {
get {
return ResourceManager.GetString("games_failed_loading_question", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game Started.
/// </summary>
public static string games_game_started {
get {
return ResourceManager.GetString("games_game_started", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hangman game started.
/// </summary>
@ -2914,6 +2977,51 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Leaderboard.
/// </summary>
public static string games_leaderboard {
get {
return ResourceManager.GetString("games_leaderboard", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No results.
/// </summary>
public static string games_no_results {
get {
return ResourceManager.GetString("games_no_results", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You don&apos;t have enough {0}.
/// </summary>
public static string games_not_enough {
get {
return ResourceManager.GetString("games_not_enough", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to picked {0}.
/// </summary>
public static string games_picked {
get {
return ResourceManager.GetString("games_picked", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} planted {1}.
/// </summary>
public static string games_planted {
get {
return ResourceManager.GetString("games_planted", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Question.
/// </summary>
@ -2950,6 +3058,168 @@ namespace NadekoBot.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Trivia game is already running on this server..
/// </summary>
public static string games_trivia_already_running {
get {
return ResourceManager.GetString("games_trivia_already_running", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Trivia Game.
/// </summary>
public static string games_trivia_game {
get {
return ResourceManager.GetString("games_trivia_game", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} guessed it! The answer was: {1}.
/// </summary>
public static string games_trivia_guess {
get {
return ResourceManager.GetString("games_trivia_guess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No trivia is running on this server..
/// </summary>
public static string games_trivia_none {
get {
return ResourceManager.GetString("games_trivia_none", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} has {1} points.
/// </summary>
public static string games_trivia_points {
get {
return ResourceManager.GetString("games_trivia_points", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Stopping after this question..
/// </summary>
public static string games_trivia_stopping {
get {
return ResourceManager.GetString("games_trivia_stopping", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Time&apos;s up! The correct answer was {0}.
/// </summary>
public static string games_trivia_times_up {
get {
return ResourceManager.GetString("games_trivia_times_up", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} guessed it and WON the game! The answer was: {1}.
/// </summary>
public static string games_trivia_win {
get {
return ResourceManager.GetString("games_trivia_win", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A draw!.
/// </summary>
public static string games_ttt_a_draw {
get {
return ResourceManager.GetString("games_ttt_a_draw", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You can&apos;t play against yourself..
/// </summary>
public static string games_ttt_against_yourself {
get {
return ResourceManager.GetString("games_ttt_against_yourself", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TicTacToe Game is already running in this channel..
/// </summary>
public static string games_ttt_already_running {
get {
return ResourceManager.GetString("games_ttt_already_running", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to has created a game of TicTacToe..
/// </summary>
public static string games_ttt_created {
get {
return ResourceManager.GetString("games_ttt_created", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} has Won!.
/// </summary>
public static string games_ttt_has_won {
get {
return ResourceManager.GetString("games_ttt_has_won", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Matched Three.
/// </summary>
public static string games_ttt_matched_three {
get {
return ResourceManager.GetString("games_ttt_matched_three", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No moves left!.
/// </summary>
public static string games_ttt_no_moves {
get {
return ResourceManager.GetString("games_ttt_no_moves", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Time Expired!.
/// </summary>
public static string games_ttt_time_expired {
get {
return ResourceManager.GetString("games_ttt_time_expired", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}&apos;s move.
/// </summary>
public static string games_ttt_users_move {
get {
return ResourceManager.GetString("games_ttt_users_move", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} vs {1}.
/// </summary>
public static string games_vs {
get {
return ResourceManager.GetString("games_vs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Back to ToC.
/// </summary>

View File

@ -1221,12 +1221,34 @@ Don't forget to leave your discord name or id in the message.
<data name="gambling_total_average" xml:space="preserve">
<value>Total: {0} Average: {1}</value>
</data>
<data name="games_category" xml:space="preserve">
<value>Category</value>
</data>
<data name="games_cleverbot_disabled" xml:space="preserve">
<value>Disabled cleverbot on this server.</value>
</data>
<data name="games_cleverbot_enabled" xml:space="preserve">
<value>Enabled cleverbot on this server.</value>
</data>
<data name="games_curgen_disabled" xml:space="preserve">
<value>Currency generation has been disabled on this channel.</value>
</data>
<data name="games_curgen_enabled" xml:space="preserve">
<value>Currency generation has been enabled on this channel.</value>
</data>
<data name="games_curgen_pl" xml:space="preserve">
<value>{0} random {1} appeared! Pick them up by typing `{2}pick`</value>
<comment>plural</comment>
</data>
<data name="games_curgen_sn" xml:space="preserve">
<value>A random {0} appeared! Pick it up by typing `{1}pick`</value>
</data>
<data name="games_failed_loading_question" xml:space="preserve">
<value>Failed loading a question.</value>
</data>
<data name="games_game_started" xml:space="preserve">
<value>Game Started</value>
</data>
<data name="games_hangman_game_started" xml:space="preserve">
<value>Hangman game started</value>
</data>
@ -1239,6 +1261,77 @@ Don't forget to leave your discord name or id in the message.
<data name="games_hangman_types" xml:space="preserve">
<value>List of "{0}hangman" term types:</value>
</data>
<data name="games_leaderboard" xml:space="preserve">
<value>Leaderboard</value>
</data>
<data name="games_not_enough" xml:space="preserve">
<value>You don't have enough {0}</value>
</data>
<data name="games_no_results" xml:space="preserve">
<value>No results</value>
</data>
<data name="games_picked" xml:space="preserve">
<value>picked {0}</value>
<comment>Kwoth picked 5*</comment>
</data>
<data name="games_planted" xml:space="preserve">
<value>{0} planted {1}</value>
<comment>Kwoth planted 5*</comment>
</data>
<data name="games_trivia_already_running" xml:space="preserve">
<value>Trivia game is already running on this server.</value>
</data>
<data name="games_trivia_game" xml:space="preserve">
<value>Trivia Game</value>
</data>
<data name="games_trivia_guess" xml:space="preserve">
<value>{0} guessed it! The answer was: {1}</value>
</data>
<data name="games_trivia_none" xml:space="preserve">
<value>No trivia is running on this server.</value>
</data>
<data name="games_trivia_points" xml:space="preserve">
<value>{0} has {1} points</value>
</data>
<data name="games_trivia_stopping" xml:space="preserve">
<value>Stopping after this question.</value>
</data>
<data name="games_trivia_times_up" xml:space="preserve">
<value>Time's up! The correct answer was {0}</value>
</data>
<data name="games_trivia_win" xml:space="preserve">
<value>{0} guessed it and WON the game! The answer was: {1}</value>
</data>
<data name="games_ttt_against_yourself" xml:space="preserve">
<value>You can't play against yourself.</value>
</data>
<data name="games_ttt_already_running" xml:space="preserve">
<value>TicTacToe Game is already running in this channel.</value>
</data>
<data name="games_ttt_a_draw" xml:space="preserve">
<value>A draw!</value>
</data>
<data name="games_ttt_created" xml:space="preserve">
<value>has created a game of TicTacToe.</value>
</data>
<data name="games_ttt_has_won" xml:space="preserve">
<value>{0} has Won!</value>
</data>
<data name="games_ttt_matched_three" xml:space="preserve">
<value>Matched Three</value>
</data>
<data name="games_ttt_no_moves" xml:space="preserve">
<value>No moves left!</value>
</data>
<data name="games_ttt_time_expired" xml:space="preserve">
<value>Time Expired!</value>
</data>
<data name="games_ttt_users_move" xml:space="preserve">
<value>{0}'s move</value>
</data>
<data name="games_vs" xml:space="preserve">
<value>{0} vs {1}</value>
</data>
<data name="utiliity_joined" xml:space="preserve">
<value>Joined</value>
</data>