Added proprietary user interface.
Added most game logic.
This commit is contained in:
@ -1,26 +1,299 @@
|
||||
namespace TicTacToe {
|
||||
public class Grid {
|
||||
private readonly Player[,] _field;
|
||||
private const int GRIDSIZE = 3;
|
||||
public bool GameOver => IsGameOver();
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
private bool IsGameOver() {
|
||||
for (var x = 0; x < GRIDSIZE; x++)
|
||||
for (var y = 0; y < GRIDSIZE; y++)
|
||||
if (IsFieldEmpty(x, y))
|
||||
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<Player> ExpectNextTurn;
|
||||
public event EventHandler<Player> 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();
|
||||
}
|
||||
/// <summary>
|
||||
/// Checks if a given field is empty.
|
||||
/// </summary>
|
||||
/// <param name="x">Column index</param>
|
||||
/// <param name="y">Row index</param>
|
||||
/// <returns>True if the field is empty</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given field is not empty.
|
||||
/// </summary>
|
||||
/// <param name="x">Column index</param>
|
||||
/// <param name="y">Row index</param>
|
||||
/// <returns>True if the field is set</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if the game is over and updates the properties.
|
||||
/// </summary>
|
||||
/// <see cref="IsGameOver"/>
|
||||
/// <see cref="Winner"/>
|
||||
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
|
||||
/// <summary>
|
||||
/// Checks if a field is full and there is no winner
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
private bool IsFieldEmpty(int x, int y) {
|
||||
return _field[x, y] == Player.Empty;
|
||||
/// <summary>
|
||||
/// Checks primary diagonal on a win.
|
||||
/// </summary>
|
||||
/// <returns>True if a win was found</returns>
|
||||
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;
|
||||
}
|
||||
/// <summary>
|
||||
/// Checks secondary diagonal on a win.
|
||||
/// </summary>
|
||||
/// <returns>True if a win was found</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
public Grid() {
|
||||
_field = new Player[GRIDSIZE, GRIDSIZE];
|
||||
/// <summary>
|
||||
/// Checks all columns on a win.
|
||||
/// </summary>
|
||||
/// <returns>True if a win was found</returns>
|
||||
private bool CheckColumns() {
|
||||
for (var x = 0; x < GRIDSIZE; x++) {
|
||||
if (CheckColumn(in x)) {
|
||||
WinningLine = ((x, 0), (x, GRIDSIZE));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool PickSpot() {
|
||||
/// <summary>
|
||||
/// Checks a specific column on a win.
|
||||
/// </summary>
|
||||
/// <param name="y">Column index</param>
|
||||
/// <returns>True if a win was found</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks all rows on a win.
|
||||
/// </summary>
|
||||
/// <returns>True if a win was found</returns>
|
||||
private bool CheckRows() {
|
||||
for (var y = 0; y < GRIDSIZE; y++) {
|
||||
if (CheckRow(in y)) {
|
||||
WinningLine = ((0, y), (GRIDSIZE, y));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks a specific row on a win.
|
||||
/// </summary>
|
||||
/// <param name="y">Row index</param>
|
||||
/// <returns>True if a win was found</returns>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Writes the grid data into a string. All rows are printed pipe ('|') separated and all row items are printed comma (',') separated.
|
||||
/// </summary>
|
||||
/// <returns>A string uniquely identifying the grid</returns>
|
||||
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!");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
namespace TicTacToe {
|
||||
enum Player {
|
||||
public enum Player {
|
||||
Empty = 0,
|
||||
Cross = 1,
|
||||
Circle = 2
|
||||
|
Reference in New Issue
Block a user