connect4 game added.

This commit is contained in:
Master Kwoth 2017-08-04 14:36:07 +02:00
parent 94e4c89564
commit ec7f69f1c0
7 changed files with 584 additions and 28 deletions

View File

@ -0,0 +1,356 @@
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
{
//todo: diagonal checking
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;
/* rows = 4, columns = 3, total = 12
* [][][][][][]
* [][][][][][]
* [][][][][][]
* [][][][][][]
* [][][][][][]
* [][][][][][]
* [][][][][][]
* */
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()
{
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)
{
EndGame(Result.CurrentPlayerWon);
break;
}
//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)
{
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)
{
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;
}
}
}

View File

@ -0,0 +1,180 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Common.Connect4;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
public class Connect4Commands : NadekoSubmodule
{
public static ConcurrentDictionary<ulong, Connect4Game> Games = new ConcurrentDictionary<ulong, Connect4Game>();
private readonly DiscordSocketClient _client;
//private readonly string[] numbers = new string[] { "⓪", " ①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨" };
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 = Games.GetOrAdd(Context.Channel.Id, newGame)) != newGame)
{
//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 (Games.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 (Games.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();
}
}
}
}

View File

@ -64,7 +64,9 @@ namespace NadekoBot.Modules.Games
await ConfirmLocalized("nunchi_failed_to_start").ConfigureAwait(false); await ConfirmLocalized("nunchi_failed_to_start").ConfigureAwait(false);
} }
async Task _client_MessageReceived(SocketMessage arg) Task _client_MessageReceived(SocketMessage arg)
{
var _ = Task.Run(async () =>
{ {
if (arg.Channel.Id != Context.Channel.Id) if (arg.Channel.Id != Context.Channel.Id)
return; return;
@ -79,6 +81,22 @@ namespace NadekoBot.Modules.Games
{ {
Console.WriteLine(ex); Console.WriteLine(ex);
} }
});
return Task.CompletedTask;
}
Task Nunchi_OnGameEnded(Nunchi arg1, string arg2)
{
if (Games.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));
} }
} }
@ -105,17 +123,6 @@ namespace NadekoBot.Modules.Games
{ {
return ConfirmLocalized("nunchi_started", Format.Bold(arg.ParticipantCount.ToString())); return ConfirmLocalized("nunchi_started", Format.Bold(arg.ParticipantCount.ToString()));
} }
private Task Nunchi_OnGameEnded(Nunchi arg1, string arg2)
{
if (Games.TryRemove(Context.Guild.Id, out var game))
game.Dispose();
if(arg2 == null)
return ConfirmLocalized("nunchi_ended_no_winner", Format.Bold(arg2));
else
return ConfirmLocalized("nunchi_ended", Format.Bold(arg2));
}
} }
} }
} }

View File

@ -101,19 +101,19 @@ namespace NadekoBot.Modules.Searches.Common
website = $"https://e621.net/post/index.json?limit=1000&tags={tag}"; website = $"https://e621.net/post/index.json?limit=1000&tags={tag}";
break; break;
case DapiSearchType.Danbooru: case DapiSearchType.Danbooru:
website = $"http://danbooru.donmai.us/posts.json?limit=200&tags={tag}"; website = $"http://danbooru.donmai.us/posts.json?limit=100&tags={tag}";
break; break;
case DapiSearchType.Gelbooru: case DapiSearchType.Gelbooru:
website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=1000&tags={tag}"; website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
break; break;
case DapiSearchType.Rule34: case DapiSearchType.Rule34:
website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}"; website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
break; break;
case DapiSearchType.Konachan: case DapiSearchType.Konachan:
website = $"https://konachan.com/post.json?s=post&q=index&limit=1000&tags={tag}"; website = $"https://konachan.com/post.json?s=post&q=index&limit=100&tags={tag}";
break; break;
case DapiSearchType.Yandere: case DapiSearchType.Yandere:
website = $"https://yande.re/post.json?limit=1000&tags={tag}"; website = $"https://yande.re/post.json?limit=100&tags={tag}";
break; break;
} }
@ -137,7 +137,7 @@ namespace NadekoBot.Modules.Searches.Common
private async Task<ImageCacherObject[]> LoadXmlAsync(string website, DapiSearchType type) private async Task<ImageCacherObject[]> LoadXmlAsync(string website, DapiSearchType type)
{ {
var list = new List<ImageCacherObject>(1000); var list = new List<ImageCacherObject>();
using (var http = new HttpClient()) using (var http = new HttpClient())
{ {
using (var reader = XmlReader.Create(await http.GetStreamAsync(website), new XmlReaderSettings() using (var reader = XmlReader.Create(await http.GetStreamAsync(website), new XmlReaderSettings()

View File

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using NadekoBot.Modules.Searches.Common; using NadekoBot.Modules.Searches.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View File

@ -1278,6 +1278,15 @@
<data name="nunchi_usage" xml:space="preserve"> <data name="nunchi_usage" xml:space="preserve">
<value>`{0}nunchi`</value> <value>`{0}nunchi`</value>
</data> </data>
<data name="connect4_cmd" xml:space="preserve">
<value>connect4 con4</value>
</data>
<data name="connect4_desc" xml:space="preserve">
<value>Creates or joins an existing connect4 game. 2 players are required for the game. Objective of the game is to get 4 of your pieces next to each other in a vertical, horizontal or diagonal line.</value>
</data>
<data name="connect4_usage" xml:space="preserve">
<value>`{0}connect4`</value>
</data>
<data name="raffle_cmd" xml:space="preserve"> <data name="raffle_cmd" xml:space="preserve">
<value>raffle</value> <value>raffle</value>
</data> </data>

View File

@ -349,6 +349,11 @@
"games_category": "Category", "games_category": "Category",
"games_cleverbot_disabled": "Disabled cleverbot on this server.", "games_cleverbot_disabled": "Disabled cleverbot on this server.",
"games_cleverbot_enabled": "Enabled cleverbot on this server.", "games_cleverbot_enabled": "Enabled cleverbot on this server.",
"games_connect4_created": "Created a Connect4 game. Waiting for a player to join.",
"games_connect4_player_to_move": "Player to move: {0}",
"games_connect4_failed_to_start": "Connect4 game failed to start because nobody joined.",
"games_connect4_draw": "Connect4 game ended in a draw.",
"games_connect4_won": "{0} won the game of Connect4 against {1}.",
"games_curgen_disabled": "Currency generation has been disabled on this channel.", "games_curgen_disabled": "Currency generation has been disabled on this channel.",
"games_curgen_enabled": "Currency generation has been enabled on this channel.", "games_curgen_enabled": "Currency generation has been enabled on this channel.",
"games_curgen_pl": "{0} random {1} appeared!", "games_curgen_pl": "{0} random {1} appeared!",