using System; using System.Text; namespace TicTacToe { public class Grid { public const int GRIDSIZE = 3; private const Player GameBeginner = Player.Cross; private Player NextGameBeginner = GameBeginner; private readonly Player[,] _field = new Player[GRIDSIZE, GRIDSIZE]; private Player? winner; private ((int x, int y) p1, (int x, int y) p2)? winningLine; public event EventHandler ExpectNextTurn; public event EventHandler GameOver; public Player this[int x, int y] { get => _field[x, y]; } public bool IsGameOver { get; private set; } = false; public Player ActivePlayer { get; private set; } = GameBeginner; public Player Winner { get => winner.Value; private set => winner = value; } public ((int x, int y) p1, (int x, int y) p2) WinningLine { get => winningLine.Value; private set => winningLine = value; } public Grid() { Reset(); } /// /// Checks if a given field is empty. /// /// Column index /// Row index /// True if the field is empty public bool IsEmpty(int x, int y) => _field[x, y] == Player.Empty; public void Reset() { ActivePlayer = NextGameBeginner; NextGameBeginner = EnemyOf(NextGameBeginner); IsGameOver = false; winner = null; winningLine = null; for (var y = 0; y < GRIDSIZE; y++) for (var x = 0; x < GRIDSIZE; x++) _field[x, y] = Player.Empty; ExpectNextTurn?.Invoke(this, ActivePlayer); } /// /// Checks if a given field is not empty. /// /// Column index /// Row index /// True if the field is set public bool IsSet(int x, int y) => _field[x, y] != Player.Empty; public void PickSpot(int x, int y) { if (IsGameOver) throw new InvalidOperationException("Cannot pick spot on finished game!"); if (!IsEmpty(x, y)) throw new InvalidOperationException("Cannot pick already picked spot!"); _field[x, y] = ActivePlayer; TurnOver(); if (!IsGameOver) ExpectNextTurn?.Invoke(this, ActivePlayer); } /// /// Tests if the game is over and updates the properties. /// /// /// private void TurnOver() { if (CheckRows() || CheckColumns() || CheckDiagonalPrimary() || CheckDiagonalSecondary() || CheckDraw()) { // Important to check after all other cases as there might be a full field with a winner IsGameOver = true; GameOver?.Invoke(this, Winner); } else { IsGameOver = false; ActivePlayer = EnemyOf(ActivePlayer); } } private static Player EnemyOf(Player player) { switch (player) { case Player.Cross: return Player.Circle; case Player.Circle: return Player.Cross; default: throw new Exception("Unexpected enumerable state!"); } } #region GameOverTests /// /// Checks if a field is full and there is no winner /// /// private bool CheckDraw() { for (var y = 0; y < GRIDSIZE; y++) { for (var x = 0; x < GRIDSIZE; x++) { if (IsEmpty(x, y)) return false; } } Winner = Player.Empty; return true; } /// /// Checks primary diagonal on a win. /// /// True if a win was found private bool CheckDiagonalPrimary() { var circlecount = 0; var crosscount = 0; for (var x = 0; x < GRIDSIZE; x++) { switch (_field[x, x]) { case Player.Circle: circlecount++; break; case Player.Cross: crosscount++; break; case Player.Empty: return false; default: throw new InvalidProgramException("Unexpected enumerable state!"); } } if (crosscount == GRIDSIZE) { Winner = Player.Cross; WinningLine = ((0, 0), (GRIDSIZE, GRIDSIZE)); return true; } if (circlecount == GRIDSIZE) { Winner = Player.Circle; WinningLine = ((0, 0), (GRIDSIZE, GRIDSIZE)); return true; } return false; } /// /// Checks secondary diagonal on a win. /// /// True if a win was found private bool CheckDiagonalSecondary() { var circlecount = 0; var crosscount = 0; for (var x = 0; x < GRIDSIZE; x++) { switch (_field[x, GRIDSIZE - 1 - x]) { case Player.Circle: circlecount++; break; case Player.Cross: crosscount++; break; case Player.Empty: return false; default: throw new InvalidProgramException("Unexpected enumerable state!"); } } if (crosscount == GRIDSIZE) { Winner = Player.Cross; WinningLine = ((0, GRIDSIZE), (GRIDSIZE, 0)); return true; } if (circlecount == GRIDSIZE) { Winner = Player.Circle; WinningLine = ((0, GRIDSIZE), (GRIDSIZE, 0)); return true; } return false; } /// /// Checks all columns on a win. /// /// True if a win was found private bool CheckColumns() { for (var x = 0; x < GRIDSIZE; x++) { if (CheckColumn(in x)) { WinningLine = ((x, 0), (x, GRIDSIZE)); return true; } } return false; } /// /// Checks a specific column on a win. /// /// Column index /// True if a win was found private bool CheckColumn(in int y) { var circlecount = 0; var crosscount = 0; for (var x = 0; x < GRIDSIZE; x++) { switch (_field[y, x]) { case Player.Circle: circlecount++; break; case Player.Cross: crosscount++; break; case Player.Empty: return false; default: throw new InvalidProgramException("Unexpected enumerable state!"); } } if (crosscount == GRIDSIZE) { Winner = Player.Cross; return true; } if (circlecount == GRIDSIZE) { Winner = Player.Circle; return true; } return false; } /// /// Checks all rows on a win. /// /// True if a win was found private bool CheckRows() { for (var y = 0; y < GRIDSIZE; y++) { if (CheckRow(in y)) { WinningLine = ((0, y), (GRIDSIZE, y)); return true; } } return false; } /// /// Checks a specific row on a win. /// /// Row index /// True if a win was found private bool CheckRow(in int y) { var circlecount = 0; var crosscount = 0; for (var x = 0; x < GRIDSIZE; x++) { switch (_field[x, y]) { case Player.Circle: circlecount++; break; case Player.Cross: crosscount++; break; case Player.Empty: return false; default: throw new InvalidProgramException("Unexpected enumerable state!"); } } if (crosscount == GRIDSIZE) { Winner = Player.Cross; return true; } if (circlecount == GRIDSIZE) { Winner = Player.Circle; return true; } return false; } #endregion /// /// Writes the grid data into a string. All rows are printed pipe ('|') separated and all row items are printed comma (',') separated. /// /// A string uniquely identifying the grid public override string ToString() { var sb = new StringBuilder(); for (var y = 0; y < GRIDSIZE; y++) { if (y > 0) sb.Append("|"); for (var x = 0; x < GRIDSIZE; x++) { if (x > 0) sb.Append(","); sb.Append(GetPlayerChar(_field[x, y])); } } return sb.ToString(); } public static char GetPlayerChar(Player player) { switch (player) { case Player.Empty: return ' '; case Player.Cross: return 'X'; case Player.Circle: return 'O'; default: throw new Exception("Unexpected enumerable state!"); }; } } }