Merge pull request #1 from Pablu23/Development

Development
This commit is contained in:
Pablu23
2021-10-25 18:00:16 +02:00
committed by GitHub
8 changed files with 103 additions and 66 deletions

View File

@@ -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 class FieldStateHelper
{ {
public static string GetDescription(this Enum value){ public static string GetDescription(this Enum value){

View File

@@ -3,6 +3,7 @@ using TicTacToe.Players;
namespace TicTacToe namespace TicTacToe
{ {
//Class on which every game bases on (Only TicTacToe atm)
abstract class Game abstract class Game
{ {
protected readonly List<Player> Players = new List<Player>(); protected readonly List<Player> Players = new List<Player>();

View File

@@ -2,6 +2,7 @@
namespace TicTacToe.Players namespace TicTacToe.Players
{ {
// The advanced Player uses the MiniMax Algorithm to chose its best Placement option
public class AdvancedAiPlayer : Player public class AdvancedAiPlayer : Player
{ {
private FieldState _enemyFieldState; private FieldState _enemyFieldState;
@@ -20,17 +21,27 @@ namespace TicTacToe.Players
public override int MakeMove(TicTacToeBoard b) 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; 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; 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++) 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); 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) if (Symbol == FieldState.PlayerX && val > bestVal)
{ {
bestMove = i; bestMove = i;
@@ -43,41 +54,63 @@ namespace TicTacToe.Players
} }
} }
} }
return bestMove; 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) 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)) if (depth == 0 | b.IsGameFinished(out var winner))
{ {
return (int) winner; return (int) winner;
} }
// The PlayerX wants to maximize the Value
if (player == FieldState.PlayerX) if (player == FieldState.PlayerX)
{ {
int maxEval = int.MinValue; int maxEval = int.MinValue;
for (int i = 0; i < 9; i++) 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); 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); maxEval = Math.Max(maxEval, eval);
} }
} }
return maxEval; return maxEval;
} }
// The PlayerO wants to minimize the Value
else else
{ {
int minEval = int.MaxValue; int minEval = int.MaxValue;
for (int i = 0; i < 9; i++) 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); 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); minEval = Math.Min(minEval, eval);
} }
} }
@@ -87,6 +120,7 @@ namespace TicTacToe.Players
} }
// No Cleanup needed
public override void CleanUp() public override void CleanUp()
{ {
} }

View File

@@ -12,9 +12,12 @@ namespace TicTacToe.Players
public override int MakeMove(TicTacToeBoard _) 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); return _random.Next(9);
} }
// No Cleanup is needed
public override void CleanUp() { } public override void CleanUp() { }
} }
} }

View File

@@ -15,10 +15,14 @@ namespace TicTacToe.Players
do do
{ {
Console.WriteLine($"Where do you want to put your {Symbol} ?"); 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); var key = Console.ReadKey(true);
loop = int.TryParse(key.KeyChar.ToString(), out result); loop = int.TryParse(key.KeyChar.ToString(), out result);
} while (!loop); } 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; return result-1;
} }

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
// TODO: Comment this, ooh god ohh fuck ooh god pls no
namespace TicTacToe.Players namespace TicTacToe.Players
{ {

View File

@@ -29,9 +29,9 @@ namespace TicTacToe
{ {
Console.WriteLine($"Player {player.Name}: {Scores[player]}"); Console.WriteLine($"Player {player.Name}: {Scores[player]}");
} }
} }
//Do CleanUp for every Player (Mostly needed for LearningAi to update the brain)
private void CleanUp() private void CleanUp()
{ {
foreach (var player in Players) 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() public override void CheckGameState()
{ {
if (!_board.IsGameFinished(out var winnerState)) return; if (!_board.IsGameFinished(out var winnerState)) return;
_gameFinished = true; _gameFinished = true;
// Get the winner (Player? <- ? because the player could be null)
var winner = Players.FirstOrDefault(x => x.Symbol == winnerState); var winner = Players.FirstOrDefault(x => x.Symbol == winnerState);
if (winner is not null) if (winner is not null)
{ {
AddScore(winner); 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 player = Players.FirstOrDefault(x => x.GetType() == typeof(LearningAiPlayer));
var learningAiPlayer = player as LearningAiPlayer; var learningAiPlayer = player as LearningAiPlayer;
learningAiPlayer?.SaveToJson(_board); if (learningAiPlayer is not null)
learningAiPlayer.SaveToJson(_board);
} }
CleanUp(); CleanUp();
} }
@@ -60,15 +66,20 @@ namespace TicTacToe
{ {
if (Players.Count != 2) throw new Exception("Not enough, or too many Players"); if (Players.Count != 2) throw new Exception("Not enough, or too many Players");
_board = new TicTacToeBoard(); _board = new TicTacToeBoard();
// Draw the Board so the first Player can see it
_board.DrawBoard(); _board.DrawBoard();
_gameFinished = false; _gameFinished = false;
int round = 0; int round = 0;
while (_gameFinished is not true) while (_gameFinished is not true)
{ {
// For both Players
for (int i = 0; i < 2; i++) 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 do
{ {
placed = _board.TryPlace(Players[i].MakeMove(_board), Players[i], round); placed = _board.TryPlace(Players[i].MakeMove(_board), Players[i], round);
@@ -77,6 +88,7 @@ namespace TicTacToe
round++; round++;
_board.DrawBoard(); _board.DrawBoard();
CheckGameState(); CheckGameState();
//Dont let the second Player make his turn if the game is already finished
if (_gameFinished) break; if (_gameFinished) break;
} }
} }

View File

@@ -1,19 +1,18 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using TicTacToe.Players; using TicTacToe.Players;
namespace TicTacToe namespace TicTacToe
{ {
public class TicTacToeBoard public class TicTacToeBoard
{ {
public Field[] _fields = new Field[9]; public Field[] Fields = new Field[9];
public TicTacToeBoard() 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.Clear();
Console.WriteLine( Console.WriteLine(
$"{_fields[0].Symbol} | " + $"{Fields[0].Symbol} | " +
$"{_fields[1].Symbol} | " + $"{Fields[1].Symbol} | " +
$"{_fields[2].Symbol}\n--------\n" + $"{Fields[2].Symbol}\n--------\n" +
$"{_fields[3].Symbol} | " + $"{Fields[3].Symbol} | " +
$"{_fields[4].Symbol} | " + $"{Fields[4].Symbol} | " +
$"{_fields[5].Symbol}\n--------\n" + $"{Fields[5].Symbol}\n--------\n" +
$"{_fields[6].Symbol} | " + $"{Fields[6].Symbol} | " +
$"{_fields[7].Symbol} | " + $"{Fields[7].Symbol} | " +
$"{_fields[8].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) public bool TryPlace(int pos, Player player, int round)
{ {
if (pos < 0 || pos > 9) return false; 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) switch (player.Symbol)
{ {
case FieldState.PlayerX: case FieldState.PlayerX:
_fields[pos].State = FieldState.PlayerX; Fields[pos].State = FieldState.PlayerX;
_fields[pos].Symbol = 'X'; Fields[pos].Symbol = 'X';
_fields[pos].Round = round; Fields[pos].Round = round;
break; break;
case FieldState.PlayerO: case FieldState.PlayerO:
_fields[pos].State = FieldState.PlayerO; Fields[pos].State = FieldState.PlayerO;
_fields[pos].Symbol = 'O'; Fields[pos].Symbol = 'O';
_fields[pos].Round = round; Fields[pos].Round = round;
break; break;
default: default:
throw new Exception("Player Symbol was not recognised"); throw new Exception("Player Symbol was not recognised");
@@ -58,14 +58,14 @@ namespace TicTacToe
public int[] GetBoardHistory() public int[] GetBoardHistory()
{ {
int[] history = new int[_fields.Length]; int[] history = new int[Fields.Length];
for (int i = 0; i < history.Length; i++) for (int i = 0; i < history.Length; i++)
{ {
history[i] = -1; history[i] = -1;
} }
int index = 0; int index = 0;
foreach (var field in _fields) foreach (var field in Fields)
{ {
if (field.Round >= 0 && field.Round <= history.Length) if (field.Round >= 0 && field.Round <= history.Length)
{ {
@@ -79,9 +79,8 @@ namespace TicTacToe
private bool IsBoardFull() private bool IsBoardFull()
{ {
//return !_fields.Any(x => x.State == FieldState.Empty);
bool isBoardFull = true; bool isBoardFull = true;
foreach (var field in _fields) foreach (var field in Fields)
{ {
if (field.State == FieldState.Empty) if (field.State == FieldState.Empty)
{ {
@@ -97,12 +96,12 @@ namespace TicTacToe
winner = FieldState.Empty; winner = FieldState.Empty;
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
FieldState first = _fields[i * 3].State; FieldState first = Fields[i * 3].State;
winner = first; winner = first;
if (first == FieldState.Empty) continue; if (first == FieldState.Empty) continue;
for (int j = 0; j < 3; j++) for (int j = 0; j < 3; j++)
{ {
if (_fields[i * 3 + j].State == first) if (Fields[i * 3 + j].State == first)
{ {
if (j == 2) if (j == 2)
{ {
@@ -124,12 +123,12 @@ namespace TicTacToe
winner = FieldState.Empty; winner = FieldState.Empty;
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
FieldState first = _fields[i].State; FieldState first = Fields[i].State;
winner = first; winner = first;
if (first == FieldState.Empty) continue; if (first == FieldState.Empty) continue;
for (int j = 0; j < 3; j++) for (int j = 0; j < 3; j++)
{ {
if (_fields[j * 3 + i].State == first) if (Fields[j * 3 + i].State == first)
{ {
if (j == 2) if (j == 2)
{ {
@@ -148,10 +147,10 @@ namespace TicTacToe
private bool Diagonal(out FieldState winner) private bool Diagonal(out FieldState winner)
{ {
winner = _fields[4].State; winner = Fields[4].State;
return _fields[4].State != FieldState.Empty && return Fields[4].State != FieldState.Empty &&
(_fields[0].State == _fields[4].State && _fields[4].State == _fields[8].State || (Fields[0].State == Fields[4].State && Fields[4].State == Fields[8].State ||
_fields[2].State == _fields[4].State && _fields[4].State == _fields[6].State); Fields[2].State == Fields[4].State && Fields[4].State == Fields[6].State);
} }
public bool IsGameFinished(out FieldState winner) public bool IsGameFinished(out FieldState winner)
@@ -175,12 +174,11 @@ namespace TicTacToe
return IsBoardFull(); return IsBoardFull();
} }
public FieldState[] GetFieldStates() public FieldState[] GetFieldStates()
{ {
var output = new FieldState[9]; var output = new FieldState[9];
int index = 0; int index = 0;
foreach (var field in _fields) foreach (var field in Fields)
{ {
output[index] = field.State; output[index] = field.State;
index++; index++;
@@ -188,22 +186,5 @@ namespace TicTacToe
return output; return output;
} }
/*public IList<Field> GetBoardCopy()
{
return Array.AsReadOnly(_fields);
}*/
public object GetBoardCopy()
{
return this.MemberwiseClone();
}
public void SetBoard(IList<Field> fields)
{
for (int i = 0; i < 9; i++)
{
_fields[i] = fields[i];
}
}
} }
} }