2017-10-04 22:51:12 +00:00
|
|
|
|
using Discord;
|
|
|
|
|
using Discord.WebSocket;
|
|
|
|
|
using NadekoBot.Extensions;
|
2017-10-13 04:14:54 +00:00
|
|
|
|
using NadekoBot.Core.Services.Impl;
|
2017-10-04 22:51:12 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|