diff --git a/CleanTicTacToe/CleanTicTacToe.csproj b/CleanTicTacToe/CleanTicTacToe.csproj
index db5d4ab..a946bb1 100644
--- a/CleanTicTacToe/CleanTicTacToe.csproj
+++ b/CleanTicTacToe/CleanTicTacToe.csproj
@@ -62,8 +62,6 @@
Resources.Designer.cs
Designer
-
-
True
Resources.resx
@@ -81,5 +79,11 @@
+
+
+ {8EB373A9-D083-4BFE-9396-E9A40E75AAC2}
+ TicTacToe
+
+
\ No newline at end of file
diff --git a/CleanTicTacToe/MainForm.Designer.cs b/CleanTicTacToe/MainForm.Designer.cs
index 173242a..bb4e093 100644
--- a/CleanTicTacToe/MainForm.Designer.cs
+++ b/CleanTicTacToe/MainForm.Designer.cs
@@ -23,20 +23,46 @@
/// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden.
///
private void InitializeComponent() {
+ this.statusStrip1 = new System.Windows.Forms.StatusStrip();
+ this.TslStatus = new System.Windows.Forms.ToolStripStatusLabel();
+ this.statusStrip1.SuspendLayout();
this.SuspendLayout();
//
+ // statusStrip1
+ //
+ this.statusStrip1.ImageScalingSize = new System.Drawing.Size(32, 32);
+ this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.TslStatus});
+ this.statusStrip1.Location = new System.Drawing.Point(0, 853);
+ this.statusStrip1.Name = "statusStrip1";
+ this.statusStrip1.Size = new System.Drawing.Size(1010, 38);
+ this.statusStrip1.TabIndex = 0;
+ this.statusStrip1.Text = "statusStrip1";
+ //
+ // TslStatus
+ //
+ this.TslStatus.Name = "TslStatus";
+ this.TslStatus.Size = new System.Drawing.Size(0, 28);
+ //
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 25F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1010, 891);
+ this.Controls.Add(this.statusStrip1);
this.Name = "MainForm";
this.Text = "Form1";
+ this.statusStrip1.ResumeLayout(false);
+ this.statusStrip1.PerformLayout();
this.ResumeLayout(false);
+ this.PerformLayout();
}
#endregion
+
+ private System.Windows.Forms.StatusStrip statusStrip1;
+ private System.Windows.Forms.ToolStripStatusLabel TslStatus;
}
}
diff --git a/CleanTicTacToe/MainForm.cs b/CleanTicTacToe/MainForm.cs
index b1bcf74..8f1dd48 100644
--- a/CleanTicTacToe/MainForm.cs
+++ b/CleanTicTacToe/MainForm.cs
@@ -1,21 +1,59 @@
using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Data;
using System.Drawing;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using System.Windows.Forms;
using TicTacToe;
namespace CleanTicTacToe {
public partial class MainForm : Form {
+ private const int BUTTONSIZE = 100;
private readonly Grid _grid;
-
+ private readonly Button[,] _pbxs = new Button[Grid.GRIDSIZE, Grid.GRIDSIZE];
public MainForm() {
InitializeComponent();
_grid = new Grid();
+ _grid.ExpectNextTurn += Grid_ExpectNextTurn;
+ _grid.GameOver += Grid_GameOver;
+ for (var x = 0; x < Grid.GRIDSIZE; x++)
+ for (var y = 0; y < Grid.GRIDSIZE; y++) {
+ var pictureBox = new Button() {
+ Location = new Point(x * BUTTONSIZE, y * BUTTONSIZE),
+ Width = BUTTONSIZE,
+ Height = BUTTONSIZE,
+ Tag = (x, y)
+ };
+ pictureBox.Click += Grid_Click;
+ Controls.Add(pictureBox);
+ _pbxs[x, y] = pictureBox;
+ }
+ }
+
+ private void Grid_GameOver(object sender, Player e) {
+ TslStatus.Text = $"Player {e} has won!";
+ _grid.Reset();
+ }
+
+ private void Grid_ExpectNextTurn(object sender, Player e) {
+ TslStatus.Text = $"Player {e} must now pick a spot!";
+ }
+
+ private void Grid_Click(object sender, EventArgs e) {
+ if (!(sender is Button pictureBox))
+ throw new InvalidProgramException("Expected picturebox as sender!");
+ (var x, var y) = ((int x, int y))pictureBox.Tag;
+ TslStatus.Text = $"Spot {x},{y} clicked!";
+ try {
+ _grid.PickSpot(x, y);
+ } catch (Exception ex) {
+ TslStatus.Text = ex.Message;
+ }
+ RefreshPbxs();
+ }
+
+ private void RefreshPbxs() {
+ for (var x = 0; x < Grid.GRIDSIZE; x++)
+ for (var y = 0; y < Grid.GRIDSIZE; y++) {
+ _pbxs[x, y].Text = $"{Grid.GetPlayerChar(_grid[x, y])}";
+ }
}
}
}
diff --git a/CleanTicTacToe/MainForm.resx b/CleanTicTacToe/MainForm.resx
index 1af7de1..174ebc7 100644
--- a/CleanTicTacToe/MainForm.resx
+++ b/CleanTicTacToe/MainForm.resx
@@ -117,4 +117,7 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ 17, 17
+
\ No newline at end of file
diff --git a/CleanTicTacToe/Properties/Grid.cs b/CleanTicTacToe/Properties/Grid.cs
deleted file mode 100644
index 2c739fb..0000000
--- a/CleanTicTacToe/Properties/Grid.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-namespace TicTacToe {
- public class Grid {
- private readonly Player[,] _field;
- private const int GRIDSIZE = 3;
- public bool GameOver => IsGameOver();
-
- private bool IsGameOver() {
- for (var x = 0; x < GRIDSIZE; x++)
- for (var y = 0; y < GRIDSIZE; y++)
- if (IsFieldEmpty(x, y))
- return false;
- return true;
- }
-
- private bool IsFieldEmpty(int x, int y) {
- return _field[x, y] == Player.Empty;
- }
-
- public Grid() {
- _field = new Player[GRIDSIZE, GRIDSIZE];
- }
-
- public bool PickSpot() {
- }
- }
-}
diff --git a/CleanTicTacToe/Properties/Player.cs b/CleanTicTacToe/Properties/Player.cs
deleted file mode 100644
index 5ddc33c..0000000
--- a/CleanTicTacToe/Properties/Player.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace TicTacToe {
- enum Player {
- Empty = 0,
- Cross = 1,
- Circle = 2
- }
-}
\ No newline at end of file
diff --git a/TicTacToe/Grid.cs b/TicTacToe/Grid.cs
index 2c739fb..d42337a 100644
--- a/TicTacToe/Grid.cs
+++ b/TicTacToe/Grid.cs
@@ -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 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;
}
-
- private bool IsFieldEmpty(int x, int y) {
- return _field[x, y] == Player.Empty;
+ ///
+ /// 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;
}
- public Grid() {
- _field = new Player[GRIDSIZE, GRIDSIZE];
+ ///
+ /// 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;
}
- public bool PickSpot() {
+ ///
+ /// 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!");
+ };
}
}
}
diff --git a/TicTacToe/Player.cs b/TicTacToe/Player.cs
index 5ddc33c..5f2a335 100644
--- a/TicTacToe/Player.cs
+++ b/TicTacToe/Player.cs
@@ -1,5 +1,5 @@
namespace TicTacToe {
- enum Player {
+ public enum Player {
Empty = 0,
Cross = 1,
Circle = 2