diff --git a/TicTacToe/Field.cs b/TicTacToe/Field.cs index a7cf23a..0bad1e8 100644 --- a/TicTacToe/Field.cs +++ b/TicTacToe/Field.cs @@ -28,6 +28,7 @@ namespace TicTacToe } } + //Helper Class to call GetDescription on Variables of Type FieldState, to get the Names from those Types public static class FieldStateHelper { public static string GetDescription(this Enum value){ diff --git a/TicTacToe/Game.cs b/TicTacToe/Game.cs index f7efb54..940f690 100644 --- a/TicTacToe/Game.cs +++ b/TicTacToe/Game.cs @@ -3,6 +3,7 @@ using TicTacToe.Players; namespace TicTacToe { + //Class on which every game bases on (Only TicTacToe atm) abstract class Game { protected readonly List Players = new List(); diff --git a/TicTacToe/Players/AdvancedAiPlayer.cs b/TicTacToe/Players/AdvancedAiPlayer.cs index 97430da..5eb94e9 100644 --- a/TicTacToe/Players/AdvancedAiPlayer.cs +++ b/TicTacToe/Players/AdvancedAiPlayer.cs @@ -2,6 +2,7 @@ namespace TicTacToe.Players { + // The advanced Player uses the MiniMax Algorithm to chose its best Placement option public class AdvancedAiPlayer : Player { private FieldState _enemyFieldState; @@ -20,17 +21,27 @@ namespace TicTacToe.Players public override int MakeMove(TicTacToeBoard b) { + // The best Value for the Player is decided if it wants to min or to max the Score int bestVal = (int) Symbol * -1000; + // The Move it makes on default | TODO: maybe this should be random 0-8, if the algorithm goes into a loop int bestMove = -1; + // For every Field on the board Calculate a value of how good it would be to place there for (int i = 0; i < 9; i++) { - if (b._fields[i].State == FieldState.Empty) + if (b.Fields[i].State == FieldState.Empty) { - b._fields[i].State = Symbol; + // Place the Piece on the Field + b.Fields[i].State = Symbol; + + // See how likely it is to win with this move int val = MiniMax(b, -1, _enemyFieldState); - b._fields[i].State = FieldState.Empty; + + // Undo the move + b.Fields[i].State = FieldState.Empty; + + // If it is the best move so far, mark it and continue on to the next field if (Symbol == FieldState.PlayerX && val > bestVal) { bestMove = i; @@ -43,41 +54,63 @@ namespace TicTacToe.Players } } } + return bestMove; } + // MiniMax is a tree based Algorithm to find the best Outcome for the starting Player, + // if both Players play perfectly + // Reference: https://www.youtube.com/watch?v=l-hh51ncgDI private int MiniMax(TicTacToeBoard b, int depth, FieldState player) { + // If the game is Over return the winner value O = -1, No Winner = 0, X = 1 if (depth == 0 | b.IsGameFinished(out var winner)) { return (int) winner; } - + + // The PlayerX wants to maximize the Value if (player == FieldState.PlayerX) { int maxEval = int.MinValue; for (int i = 0; i < 9; i++) { - if (b._fields[i].State == FieldState.Empty) + if (b.Fields[i].State == FieldState.Empty) { - b._fields[i].State = FieldState.PlayerX; + // Place the Piece on the Field + b.Fields[i].State = FieldState.PlayerX; + + // See how the perfect Enemy will respond to that placement int eval = MiniMax(b, depth - 1, FieldState.PlayerO); - b._fields[i].State = FieldState.Empty; + + // Undo the placement + b.Fields[i].State = FieldState.Empty; + + // Update the max evaluated Value maxEval = Math.Max(maxEval, eval); } } + return maxEval; } + // The PlayerO wants to minimize the Value else { int minEval = int.MaxValue; for (int i = 0; i < 9; i++) { - if (b._fields[i].State == FieldState.Empty) + if (b.Fields[i].State == FieldState.Empty) { - b._fields[i].State = FieldState.PlayerO; + // Place the Piece on the Field + b.Fields[i].State = FieldState.PlayerO; + + // See how the perfect Enemy will respond to that placement int eval = MiniMax(b, depth - 1, FieldState.PlayerX); - b._fields[i].State = FieldState.Empty; + + // Undo the placement + b.Fields[i].State = FieldState.Empty; + + // Update the min evaluated Value minEval = Math.Min(minEval, eval); } } @@ -87,6 +120,7 @@ namespace TicTacToe.Players } + // No Cleanup needed public override void CleanUp() { } diff --git a/TicTacToe/Players/BasicAiPlayer.cs b/TicTacToe/Players/BasicAiPlayer.cs index d3643c7..c921257 100644 --- a/TicTacToe/Players/BasicAiPlayer.cs +++ b/TicTacToe/Players/BasicAiPlayer.cs @@ -12,9 +12,12 @@ namespace TicTacToe.Players public override int MakeMove(TicTacToeBoard _) { + // Just make a random move + // It doesnt need to get checked because the TicTacToe Class handles the input by itself return _random.Next(9); } + // No Cleanup is needed public override void CleanUp() { } } } \ No newline at end of file diff --git a/TicTacToe/Players/HumanPlayer.cs b/TicTacToe/Players/HumanPlayer.cs index 031e83f..96a8487 100644 --- a/TicTacToe/Players/HumanPlayer.cs +++ b/TicTacToe/Players/HumanPlayer.cs @@ -15,10 +15,14 @@ namespace TicTacToe.Players do { Console.WriteLine($"Where do you want to put your {Symbol} ?"); + + // This is because the highest number is 9 so we always only need one Key to play var key = Console.ReadKey(true); loop = int.TryParse(key.KeyChar.ToString(), out result); } while (!loop); + // Moves are Zero based, but are Shown One based + // So we subtract one to get to the Zero based Move return result-1; } diff --git a/TicTacToe/Players/LearningAiPlayer.cs b/TicTacToe/Players/LearningAiPlayer.cs index e2c97e0..8ddbd48 100644 --- a/TicTacToe/Players/LearningAiPlayer.cs +++ b/TicTacToe/Players/LearningAiPlayer.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Newtonsoft.Json; +// TODO: Comment this, ooh god ohh fuck ooh god pls no namespace TicTacToe.Players { diff --git a/TicTacToe/TicTacToe.cs b/TicTacToe/TicTacToe.cs index 0d8efc3..8603a7f 100644 --- a/TicTacToe/TicTacToe.cs +++ b/TicTacToe/TicTacToe.cs @@ -29,9 +29,9 @@ namespace TicTacToe { Console.WriteLine($"Player {player.Name}: {Scores[player]}"); } - } + //Do CleanUp for every Player (Mostly needed for LearningAi to update the brain) private void CleanUp() { foreach (var player in Players) @@ -40,18 +40,24 @@ namespace TicTacToe } } + // Check if the game is finished, and if so add Scores and more for LearningAi public override void CheckGameState() { if (!_board.IsGameFinished(out var winnerState)) return; _gameFinished = true; + + // Get the winner (Player? <- ? because the player could be null) var winner = Players.FirstOrDefault(x => x.Symbol == winnerState); if (winner is not null) { AddScore(winner); + + //If one of the Players was a LearningAiPlayer add the Match to the Ai Brain var player = Players.FirstOrDefault(x => x.GetType() == typeof(LearningAiPlayer)); var learningAiPlayer = player as LearningAiPlayer; - learningAiPlayer?.SaveToJson(_board); + if (learningAiPlayer is not null) + learningAiPlayer.SaveToJson(_board); } CleanUp(); } @@ -60,15 +66,20 @@ namespace TicTacToe { if (Players.Count != 2) throw new Exception("Not enough, or too many Players"); _board = new TicTacToeBoard(); + + // Draw the Board so the first Player can see it _board.DrawBoard(); _gameFinished = false; int round = 0; while (_gameFinished is not true) { + // For both Players for (int i = 0; i < 2; i++) { - bool placed = true; + // As long as the Player has not played an allowed move, + // he is asked to repeat his move + bool placed; do { placed = _board.TryPlace(Players[i].MakeMove(_board), Players[i], round); @@ -77,6 +88,7 @@ namespace TicTacToe round++; _board.DrawBoard(); CheckGameState(); + //Dont let the second Player make his turn if the game is already finished if (_gameFinished) break; } } diff --git a/TicTacToe/TicTacToeBoard.cs b/TicTacToe/TicTacToeBoard.cs index 0b46cc4..25d2070 100644 --- a/TicTacToe/TicTacToeBoard.cs +++ b/TicTacToe/TicTacToeBoard.cs @@ -1,19 +1,18 @@ using System; -using System.Collections.Generic; -using System.Linq; using TicTacToe.Players; namespace TicTacToe { public class TicTacToeBoard { - public Field[] _fields = new Field[9]; + public Field[] Fields = new Field[9]; public TicTacToeBoard() { - for (int i = 0; i < _fields.Length; i++) + for (int i = 0; i < Fields.Length; i++) { - _fields[i] = new Field(Convert.ToChar((i+1).ToString())); + //Initialise all Fields and give them the Symbol on which Position they are + Fields[i] = new Field(Convert.ToChar((i+1).ToString())); } } @@ -21,33 +20,34 @@ namespace TicTacToe { Console.Clear(); Console.WriteLine( - $"{_fields[0].Symbol} | " + - $"{_fields[1].Symbol} | " + - $"{_fields[2].Symbol}\n--------\n" + - $"{_fields[3].Symbol} | " + - $"{_fields[4].Symbol} | " + - $"{_fields[5].Symbol}\n--------\n" + - $"{_fields[6].Symbol} | " + - $"{_fields[7].Symbol} | " + - $"{_fields[8].Symbol}"); + $"{Fields[0].Symbol} | " + + $"{Fields[1].Symbol} | " + + $"{Fields[2].Symbol}\n--------\n" + + $"{Fields[3].Symbol} | " + + $"{Fields[4].Symbol} | " + + $"{Fields[5].Symbol}\n--------\n" + + $"{Fields[6].Symbol} | " + + $"{Fields[7].Symbol} | " + + $"{Fields[8].Symbol}"); } - + + //Tries to mark a position, if the position is not free it returns a false public bool TryPlace(int pos, Player player, int round) { if (pos < 0 || pos > 9) return false; - if (_fields[pos].State != FieldState.Empty) return false; + if (Fields[pos].State != FieldState.Empty) return false; switch (player.Symbol) { case FieldState.PlayerX: - _fields[pos].State = FieldState.PlayerX; - _fields[pos].Symbol = 'X'; - _fields[pos].Round = round; + Fields[pos].State = FieldState.PlayerX; + Fields[pos].Symbol = 'X'; + Fields[pos].Round = round; break; case FieldState.PlayerO: - _fields[pos].State = FieldState.PlayerO; - _fields[pos].Symbol = 'O'; - _fields[pos].Round = round; + Fields[pos].State = FieldState.PlayerO; + Fields[pos].Symbol = 'O'; + Fields[pos].Round = round; break; default: throw new Exception("Player Symbol was not recognised"); @@ -58,14 +58,14 @@ namespace TicTacToe public int[] GetBoardHistory() { - int[] history = new int[_fields.Length]; + int[] history = new int[Fields.Length]; for (int i = 0; i < history.Length; i++) { history[i] = -1; } int index = 0; - foreach (var field in _fields) + foreach (var field in Fields) { if (field.Round >= 0 && field.Round <= history.Length) { @@ -79,9 +79,8 @@ namespace TicTacToe private bool IsBoardFull() { - //return !_fields.Any(x => x.State == FieldState.Empty); bool isBoardFull = true; - foreach (var field in _fields) + foreach (var field in Fields) { if (field.State == FieldState.Empty) { @@ -97,12 +96,12 @@ namespace TicTacToe winner = FieldState.Empty; for (int i = 0; i < 3; i++) { - FieldState first = _fields[i * 3].State; + FieldState first = Fields[i * 3].State; winner = first; if (first == FieldState.Empty) continue; for (int j = 0; j < 3; j++) { - if (_fields[i * 3 + j].State == first) + if (Fields[i * 3 + j].State == first) { if (j == 2) { @@ -124,12 +123,12 @@ namespace TicTacToe winner = FieldState.Empty; for (int i = 0; i < 3; i++) { - FieldState first = _fields[i].State; + FieldState first = Fields[i].State; winner = first; if (first == FieldState.Empty) continue; for (int j = 0; j < 3; j++) { - if (_fields[j * 3 + i].State == first) + if (Fields[j * 3 + i].State == first) { if (j == 2) { @@ -148,10 +147,10 @@ namespace TicTacToe private bool Diagonal(out FieldState winner) { - winner = _fields[4].State; - return _fields[4].State != FieldState.Empty && - (_fields[0].State == _fields[4].State && _fields[4].State == _fields[8].State || - _fields[2].State == _fields[4].State && _fields[4].State == _fields[6].State); + winner = Fields[4].State; + return Fields[4].State != FieldState.Empty && + (Fields[0].State == Fields[4].State && Fields[4].State == Fields[8].State || + Fields[2].State == Fields[4].State && Fields[4].State == Fields[6].State); } public bool IsGameFinished(out FieldState winner) @@ -175,12 +174,11 @@ namespace TicTacToe return IsBoardFull(); } - public FieldState[] GetFieldStates() { var output = new FieldState[9]; int index = 0; - foreach (var field in _fields) + foreach (var field in Fields) { output[index] = field.State; index++; @@ -188,22 +186,5 @@ namespace TicTacToe return output; } - /*public IList GetBoardCopy() - { - return Array.AsReadOnly(_fields); - }*/ - - public object GetBoardCopy() - { - return this.MemberwiseClone(); - } - - public void SetBoard(IList fields) - { - for (int i = 0; i < 9; i++) - { - _fields[i] = fields[i]; - } - } } } \ No newline at end of file