// Calls minimax for each possible move and returns best move public static int FindBestMove(Connect4Game connect4, Connect4Board startBoard, int maxDepth) { MiniMaxVanilla.maxDepth = maxDepth; MiniMaxVanilla.connect4 = connect4; int player = MAXIMIZING; List <MoveScore> results = new List <MoveScore>(); Console.WriteLine("Move scores {move, score}: "); foreach (int move in connect4.GetPossibleMoves(startBoard)) { Connect4Board nextBoard = connect4.SimulateMove(startBoard, player, move); int searchScore = MiniMaxSearch(nextBoard, player * -1, 1); Console.Write("{" + move + ", " + searchScore + "} , "); results.Add(new MoveScore() { move = move, score = searchScore }); } int decidedMove = DecideMove(results); Console.WriteLine("Move chosen: " + decidedMove); return(decidedMove); }
public void TestMethodCanWinDiagonallyDown() { // AAA // arrange Connect4Game game = new Connect4Game(); // act game.dropPieceInColumn(3); game.dropPieceInColumn(2); game.dropPieceInColumn(2); game.dropPieceInColumn(2); game.dropPieceInColumn(1); game.dropPieceInColumn(1); game.dropPieceInColumn(1); game.dropPieceInColumn(0); game.dropPieceInColumn(0); game.dropPieceInColumn(0); game.dropPieceInColumn(0); bool actualIsGameOver = game.isGameOver(); // assert Assert.IsTrue(actualIsGameOver); }
public void ProcessNextPlayerCallsCorrectMethods() { var player1 = MockRepository.GenerateMock<Connect4Player>(); var player2 = MockRepository.GenerateMock<Connect4Player>(); var game = new Connect4Game(6, 7, player1, player2); var writer = MockRepository.GenerateMock<WithWriteMethod>(); game.WriteLineToDisplay = writer.WriteLineToDisplay; //expect a write to screen writer.Expect(x => x.WriteLineToDisplay(Arg<object>.Is.Anything)); //expect a query ofplayer one player1.Expect(x => x.GetColumnSelected(Arg<Func<string>>.Is.Anything, Arg<Action<object>>.Is.Anything, Arg<Connect4Board>.Is.Anything)).Return(1); //expect no query pf player 2 player2.Expect(x => x.GetColumnSelected(Arg<Func<string>>.Is.Anything, Arg<Action<object>>.Is.Anything, Arg<Connect4Board>.Is.Anything)) .Repeat.Never().Return(1); game.ProcessNextPlayer(); writer.VerifyAllExpectations(); player1.VerifyAllExpectations(); player2.VerifyAllExpectations(); }
public void CanCreateGame() { IPlayer player1 = MockRepository.GenerateMock<Connect4Player>(); IPlayer player2 = MockRepository.GenerateMock<Connect4Player>(); var game = new Connect4Game(rows: 7, columns: 6, player1: player1, player2: player2); Assert.IsNotNull(game); Assert.IsInstanceOfType(game, typeof(IGame)); Assert.IsInstanceOfType(game, typeof(Connect4Game)); }
public void TestMethodNewGameIsNotOver() { // AAA // arrange Connect4Game game = new Connect4Game(); // act bool isGameOver = game.isGameOver(); // assert Assert.IsFalse(isGameOver); }
public void TestMethodGetCurrentPlayer() { // AAA // arrange Connect4Game game = new Connect4Game(); Piece expectedCurrentPlayer = Piece.Red; // act Piece actualCurrentPlayer = game.getCurrentPlayer(); // assert Assert.AreEqual(expectedCurrentPlayer, actualCurrentPlayer); }
public void TestMethodGetPieceAt() { // AAA // arrange Connect4Game game = new Connect4Game(); Piece expectedPieceColor = Piece.Red; // act game.dropPieceInColumn(0); Piece actualPieceColor = game.getPieceAt(5, 0); // assert Assert.AreEqual(expectedPieceColor, actualPieceColor); }
public void TestMethodDropPieceAndCurrentPlayerChanges() { // AAA // arrange Connect4Game game = new Connect4Game(); Piece expectedCurrentPlayer = Piece.Green; // act game.dropPieceInColumn(0); Piece actualCurrentPlayer = game.getCurrentPlayer(); // assert Assert.AreEqual(expectedCurrentPlayer, actualCurrentPlayer); }
public void TestMethodCanWinVertically() { // AAA // arrange Connect4Game game = new Connect4Game(); // act for (int columnIndex = 0; columnIndex < 8; columnIndex++) { game.dropPieceInColumn(columnIndex % 2); } bool actualIsGameOver = game.isGameOver(); // assert Assert.IsTrue(actualIsGameOver); }
public async Task Connect4PreGameSetup(GameUser p2, int bet, Connect4Game currentLobby) { var player1 = GameService.GetGameUser(Context.User.Id, Context.Guild.Id); var player2 = GameService.GetGameUser(p2.UserId, Context.Guild.Id); var lines = new int[6, 7]; var embed = new EmbedBuilder(); // Here we build the initial game board with all empty squares for (var r = 0; r < 6; r++) { if (r == 0) { for (var c = 0; c < 7; c++) { embed.Description += $":{numlist[c]}:"; } embed.Description += "\n"; } for (var c = 0; c < 7; c++) { embed.Description += $"{none}"; } embed.Description += "\n"; } embed.Description += "Usage:\n" + "`connect4 [column]`\n" + $":large_blue_circle: - {Context.User.Mention} <-\n" + $":red_circle: - {Context.Guild.GetUser(player2.UserId)?.Mention}"; embed.Footer = new EmbedFooterBuilder { Text = $"it is {Context.User.Username}'s turn" }; var gamemessage = await ReplyAsync("", false, embed.Build()); await Connect4PlayingTask(lines, gamemessage, player1, player2, bet, embed, currentLobby); }
public async Task ExecuteGroupAsync(CommandContext ctx, [Description("desc-game-movetime")] TimeSpan?moveTime = null) { if (moveTime?.TotalSeconds is < 2 or > 120) { throw new InvalidCommandUsageException(ctx, "cmd-err-game-movetime", 2, 120); } if (this.Service.IsEventRunningInChannel(ctx.Channel.Id)) { throw new CommandFailedException(ctx, "cmd-err-evt-dup"); } DiscordUser?opponent = await ctx.WaitForGameOpponentAsync(); if (opponent is null) { throw new CommandFailedException(ctx, "cmd-err-game-op-none", ctx.User.Mention); } var game = new Connect4Game(ctx.Client.GetInteractivity(), ctx.Channel, ctx.User, opponent, moveTime); this.Service.RegisterEventInChannel(game, ctx.Channel.Id); try { await game.RunAsync(this.Localization); if (game.Winner is { }) { if (game.IsTimeoutReached) { await ctx.ImpInfoAsync(this.ModuleColor, Emojis.Trophy, "str-game-timeout", game.Winner.Mention); } else { await ctx.ImpInfoAsync(this.ModuleColor, Emojis.Trophy, "fmt-winners", game.Winner.Mention); } GameStatsService gss = ctx.Services.GetRequiredService <GameStatsService>(); await gss.UpdateStatsAsync(game.Winner.Id, s => s.Connect4Won++); await gss.UpdateStatsAsync(game.Winner == ctx.User?opponent.Id : ctx.User.Id, s => s.Connect4Lost++); }
// Calls minimax for each possible move and returns best move public static int FindBestMove(Connect4Game connect4, Connect4Board startBoard, int maxDepth) { MiniMaxAlphaBeta.maxDepth = maxDepth; MiniMaxAlphaBeta.connect4 = connect4; int player = MAXIMIZING; int a = int.MinValue; int b = int.MaxValue; List<MoveScore> results = new List<MoveScore>(); Console.WriteLine("\nMove scores {move, score, a}: "); foreach (int move in connect4.GetPossibleMoves(startBoard).OrderBy(x => rand.Next()).ToList()) { Connect4Board nextBoard = connect4.SimulateMove(startBoard, player, move); int searchScore = MiniMaxSearch(nextBoard, MINIMIZING, 1, a, b); // UPDATE ALPHA a = Math.Max(a, searchScore); Console.Write("{" + move + ", " + searchScore + ", " + a + "} , "); results.Add(new MoveScore() { move = move, score = searchScore }); } int decidedMove = DecideMove(results); Console.WriteLine("Move chosen: " + decidedMove); return decidedMove; }
public Connect4GameController(ILogger <Connect4GameController> logger, Connect4Game game) { _game = game; _logger = logger; }
public async Task Connect4PlayingTask(int[, ] lines, IUserMessage gamemessage, GameUser player1, GameUser player2, int bet, EmbedBuilder embed, Connect4Game currentlobby) { // LastX and LastY are used to check for horizontal and vertical wins var lastx = 0; var lasty = 0; var playinggame = true; // We always begin the game with whoever ran the initial command. var currentplayer = 1; var winmethod = ""; // MSGTime is used to ensure that only a single minute passes in between turns, if it goes past a minute between turns then we count it as a player forfeiting. var msgtime = DateTime.UtcNow + TimeSpan.FromMinutes(1); var errormsgs = ""; while (playinggame) { var errormsgs1 = errormsgs; if (!string.IsNullOrEmpty(errormsgs1)) { await gamemessage.ModifyAsync(x => x.Content = errormsgs1); } else { await gamemessage.ModifyAsync(x => x.Content = " "); } errormsgs = ""; // Using InteractiveBase we wait until a message is sent in the current game var next = await NextMessageAsync(async (x, y) => x.Channel.Id == y.Channel.Id, TimeSpan.FromMinutes(1)); // If the player doesn't show up mark them as forfeiting and award a win to the other player. if (next == null || msgtime < DateTime.UtcNow) { await ReplyAsync( $"{(currentplayer == 1 ? Context.Guild.GetUser(player1.UserId)?.Mention : Context.Guild.GetUser(player2.UserId)?.Mention)} Did not reply fast enough. Auto forfeiting"); var w = currentplayer == 1 ? player2.UserId : player1.UserId; var l = currentplayer == 1 ? player1.UserId : player2.UserId; await Connect4WinAsync(w, l, bet, "Player Forfeited."); return; } // filter out non game messages by ignoring ones if (!next.Content.ToLower().StartsWith("connect4")) { continue; } // Ensure that we only accept messages from players that are in the game if (next.Author.Id != player1.UserId && next.Author.Id != player2.UserId) { // await ReplyAsync("You are not part of this game."); errormsgs = $"{next.Author.Mention} You are not part of this game."; await next.DeleteAsync(); continue; } // Ensure that the current message is from a player AND it is also their turn. if ((next.Author.Id == player1.UserId && currentplayer == 1) || (next.Author.Id == player2.UserId && currentplayer == 2)) { // filter out invalid line submissions var parameters = next.Content.Split(" "); // Make sure that the message is in the correct format of connect4 [line] if (parameters.Length != 2 || !int.TryParse(parameters[1], out var Column)) { errormsgs = $"{(currentplayer == 2 ? Context.Guild.GetUser(player1.UserId)?.Mention : Context.Guild.GetUser(player2.UserId)?.Mention)} \n" + $"Invalid Line input, here is an example input:\n" + "`connect4 3` - this will place a counter in line 3.\n" + "NOTE: Do not use the bot's prefix, just write `connect4 [line]`"; await next.DeleteAsync(); continue; } // as there are only 7 columns to pick from, filter out values outside of this range. if (Column < 0 || Column > 6) { // error invalid line. errormsgs = $"{(currentplayer == 2 ? Context.Guild.GetUser(player1.UserId)?.Mention : Context.Guild.GetUser(player2.UserId)?.Mention)}\n" + $"Invalid input, line number must be from 0-6 message in the format:\n" + "`connect4 [line]`"; await next.DeleteAsync(); continue; } var success = false; // moving from the top of the board downwards for (var row = 5; row >= 0; row--) { if (lines[row, Column] != 0) { continue; } lines[row, Column] = currentplayer; lastx = Column; lasty = row; success = true; break; } // Ensure that we only move to the next player's turn IF the current player actually makes a move in an available column. if (!success) { errormsgs = $"{(currentplayer == 2 ? Context.Guild.GetUser(player1.UserId)?.Mention : Context.Guild.GetUser(player2.UserId)?.Mention)}\n" + $"Error, please specify a line that isn't full"; await next.DeleteAsync(); continue; } // Update the embed message embed.Description = ""; for (var r = 0; r < 6; r++) { if (r == 0) { for (var c = 0; c < 7; c++) { embed.Description += $":{numlist[c]}:"; } embed.Description += "\n"; } for (var c = 0; c < 7; c++) { if (lines[r, c] == 0) { embed.Description += $"{none}"; } else if (lines[r, c] == 1) { embed.Description += $"{blue}"; } else if (lines[r, c] == 2) { embed.Description += $"{red}"; } } embed.Description += "\n"; } embed.Description += "Usage:\n" + "`connect4 [column]`\n" + $":large_blue_circle: - {Context.User.Mention} {(currentplayer == 1 ? "" : "<-")}\n" + $":red_circle: - {Context.Guild.GetUser(player2.UserId)?.Mention} {(currentplayer == 2 ? "" : "<-")}"; embed.Footer = new EmbedFooterBuilder { Text = $"it is {(currentplayer == 2 ? Context.Guild.GetUser(player1.UserId)?.Username : Context.Guild.GetUser(player2.UserId)?.Username)}'s turn" }; await gamemessage.ModifyAsync(x => x.Embed = embed.Build()); // Check If it is a win here. var connectioncount = 0; // Checking Horizontally (Rows) for (var i = 0; i <= 6; i++) { if (lines[lasty, i] == currentplayer) { connectioncount++; } else { connectioncount = 0; } if (connectioncount < 4) { continue; } // await ReplyAsync($"Player {currentplayer} Wins! Horizontal"); playinggame = false; winmethod = "Horizontal"; break; } // Checking Vertically (Columns) connectioncount = 0; for (var i = 0; i <= 5; i++) { if (lines[i, lastx] == currentplayer) { connectioncount++; } else { connectioncount = 0; } if (connectioncount >= 4) { // await ReplyAsync($"Player {currentplayer} Wins! Vertical"); playinggame = false; winmethod = "Vertical"; break; } } /* C O L U M N S * R [0,0][0,1][0,2][0,3][0,4][0,5][0,6] * O [1,0][1,1][1,2][1,3][1,4][1,5][1,6] * W [2,0][2,1][2,2][2,3][2,4][2,5][2,6] * S [3,0][3,1][3,2][3,3][3,4][3,5][3,6] * [4,0][4,1][4,2][4,3][4,4][4,5][4,6] * [5,0][5,1][5,2][5,3][5,4][5,5][5,6] */ // Checking Diagonally int colinit, rowinit; // Top Left => Bottom Right (from top row diagonals) for (rowinit = 0; rowinit <= 5; rowinit++) { connectioncount = 0; int row, col; for (row = rowinit, col = 0; col <= 6 && row <= 5; col++, row++) { if (lines[row, col] == currentplayer) { connectioncount++; if (connectioncount < 4) { continue; } playinggame = false; winmethod = "Diagonal"; break; } connectioncount = 0; } } // Top Left => Bottom Right (from columns) for (colinit = 0; colinit <= 6; colinit++) { connectioncount = 0; int row, col; for (row = 0, col = colinit; col <= 6 && row <= 5; col++, row++) { if (lines[row, col] == currentplayer) { connectioncount++; if (connectioncount < 4) { continue; } playinggame = false; winmethod = "Diagonal"; break; } connectioncount = 0; } } // Checking other Diagonal. // Top Right => Bottom Left for (rowinit = 0; rowinit <= 5; rowinit++) { connectioncount = 0; int row, col; for (row = rowinit, col = 6; col >= 0 && row <= 5; col--, row++) { if (lines[row, col] == currentplayer) { connectioncount++; if (connectioncount < 4) { continue; } playinggame = false; winmethod = "Diagonal"; break; } connectioncount = 0; } } for (colinit = 6; colinit >= 0; colinit--) { connectioncount = 0; int row, col; for (row = 0, col = colinit; col >= 0 && row <= 5; col--, row++) { if (lines[row, col] == currentplayer) { connectioncount++; if (connectioncount < 4) { continue; } playinggame = false; winmethod = "Diagonal"; break; } connectioncount = 0; } } // If we have a win, do don't switch the current player. if (!playinggame) { continue; } currentplayer = currentplayer == 1 ? 2 : 1; // To reduce the amount of messages after the game, delete the connect4 message. await next.DeleteAsync(); msgtime = DateTime.UtcNow + TimeSpan.FromMinutes(1); if (!embed.Description.Contains($"{none}")) { // This means all spaces are filled on the board // ie. a tie. await ReplyAsync( "The Game is a draw. User Balances have not been modified. Good Game!"); currentlobby.GameRunning = false; return; } } else { errormsgs = $"{(currentplayer == 2 ? Context.Guild.GetUser(player1.UserId)?.Mention : Context.Guild.GetUser(player2.UserId)?.Mention)}\n" + "Unknown Player/Not your turn."; await next.DeleteAsync(); } } await Connect4GetResultsAsync(currentplayer, player1, player2, bet, winmethod); }
public async Task Connect4(params string[] args) { var(options, _) = OptionsParser.Default.ParseFrom(new Connect4Game.Options(), args); if (!await CheckBetOptional(options.Bet)) { return; } var newGame = new Connect4Game(Context.User.Id, Context.User.ToString(), options, _cs); 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(), options.Bet).ConfigureAwait(false); return; } if (options.Bet > 0) { if (!await _cs.RemoveAsync(Context.User.Id, "Connect4-bet", options.Bet, true)) { await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false); _service.Connect4Games.TryRemove(Context.Channel.Id, out _); game.Dispose(); return; } } game.OnGameStateUpdated += Game_OnGameStateUpdated; game.OnGameFailedToStart += Game_OnGameFailedToStart; game.OnGameEnded += Game_OnGameEnded; _client.MessageReceived += _client_MessageReceived; game.Initialize(); if (options.Bet == 0) { await ReplyConfirmLocalized("connect4_created").ConfigureAwait(false); } else { await ReplyConfirmLocalized("connect4_created_bet", options.Bet + _bc.BotConfig.CurrencySign).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.Username), Format.Bold(arg.OtherPlayer.Username)); } else if (result == Connect4Game.Result.OtherPlayerWon) { title = GetText("connect4_won", Format.Bold(arg.OtherPlayer.Username), Format.Bold(arg.CurrentPlayer.Username)); } else { title = GetText("connect4_draw"); } return(msg.ModifyAsync(x => x.Embed = new EmbedBuilder() .WithTitle(title) .WithDescription(GetGameStateText(game)) .WithOkColor() .Build())); } }
// Calls minimax for each possible move and returns best move public static int FindBestMove(Connect4Game connect4, Con4Board startBoard, int maxDepth) { MiniMaxAlphaBetaHeuristic.maxDepth = maxDepth; MiniMaxAlphaBetaHeuristic.connect4 = connect4; int player = MAXIMIZING; int a = int.MinValue; int b = int.MaxValue; List <MoveBoardNode> moveBoardNodes = new List <MoveBoardNode>(); foreach (int move in startBoard.GetPossibleMoves()) { Con4Board nextBoard = startBoard.SimulateMove(player, move); Evaluation nextEval = CalculateBoardScore(nextBoard, 1); BoardNode boardNode = new BoardNode { board = nextBoard, eval = nextEval }; moveBoardNodes.Add(new MoveBoardNode { move = move, boardNode = boardNode }); } moveBoardNodes = moveBoardNodes.OrderByDescending(x => x.boardNode.eval.score).ToList(); // player is maximizing Console.WriteLine("\tMove scores {move, score}: "); Console.Write("\t"); List <MoveScore> results = new List <MoveScore>(); foreach (MoveBoardNode moveBoardNode in moveBoardNodes) { int searchScore = MiniMaxSearch(moveBoardNode.boardNode, MINIMIZING, 1, a, b); a = Math.Max(a, searchScore); if (a >= b) { break; } Console.Write("{" + moveBoardNode.move + ", " + searchScore + "} , "); results.Add(new MoveScore() { move = moveBoardNode.move, score = searchScore }); } int maxScore = results.Max(x => x.score); if (Math.Abs(maxScore) > 900) // Either confident in win or in loss, don't search deeper { MainClass.gameOutcomeFinal = true; } if (results.Count(x => x.score > -900) == 1) // Only 1 viable option, just do it! { MainClass.gameOutcomeFinal = true; } int bestMove = results.Find(x => x.score == maxScore).move; //int decidedMove = DecideMove(results); Console.WriteLine("\tBest move: " + bestMove); Console.WriteLine("\tSimulations: " + Con4Board.simulations); Con4Board.simulations = 0; return(bestMove); }
public void CannotCreateGame_WrongPlayerType2() { IPlayer player2 = MockRepository.GenerateMock<IPlayer>(); IPlayer player1 = MockRepository.GenerateMock<Connect4Player>(); var game = new Connect4Game(rows: 7, columns: 6, player1: player1, player2: player2); }
public void CannotCreateCreateGame_BoardTooManyRows() { IPlayer player1 = MockRepository.GenerateMock<Connect4Player>(); IPlayer player2 = MockRepository.GenerateMock<Connect4Player>(); var game = new Connect4Game(rows: 10, columns: 6, player1: player1, player2: player2); }
public Connect4Form() : base() { InitializeComponent(); game = new Connect4Game(); }
public async Task Connect4AcceptTask(int bet, Connect4Game currentlobby) { var accepted = false; var timeoutattempts = 0; var messagewashout = 0; GameUser p2 = null; // Here we wait until another player accepts the game while (!accepted) { var next = await NextMessageAsync(async (x, y) => x.Channel.Id == y.Channel.Id, TimeSpan.FromSeconds(10)); if (next?.Author.Id == Context.User.Id) { // Ignore author messages for the game until another player accepts } else if (string.Equals(next?.Content, "connect4 accept", StringComparison.InvariantCultureIgnoreCase)) { p2 = GameService.GetGameUser(next.Author.Id, Context.Guild.Id); if (p2.Points < bet) { await ReplyAsync( $"{next.Author.Mention} - You do not have enough points to play this game, you need a minimum of {bet}. Your balance is {p2.Points} points"); } else { accepted = true; p2 = GameService.GetGameUser(next.Author.Id, Context.Guild.Id); } } // Overload for is a message is not sent within the timeout if (next == null) { // if more than 6 timeouts (1 minute of no messages) occur without a user accepting, we quit the game timeoutattempts++; if (timeoutattempts < 6) { continue; } await ReplyAsync("Connect4: Timed out!"); currentlobby.GameRunning = false; return; } // In case people are talking over the game, we also make the game quit after 25 messages are sent so it is not waiting indefinitely for a player to accept. messagewashout++; if (messagewashout <= 25) { continue; } await ReplyAsync("Connect4: Timed out!"); currentlobby.GameRunning = false; return; } await Connect4PreGameSetup(p2, bet, currentlobby); }