From 27f6efb58ad51d05f308744c3e790b9ac83b9beb Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Sat, 4 Jan 2020 13:13:13 +0100 Subject: [PATCH] Added proprietary user interface. Added most game logic. --- CleanTicTacToe/CleanTicTacToe.csproj | 8 +- CleanTicTacToe/MainForm.Designer.cs | 26 +++ CleanTicTacToe/MainForm.cs | 52 ++++- CleanTicTacToe/MainForm.resx | 3 + CleanTicTacToe/Properties/Grid.cs | 26 --- CleanTicTacToe/Properties/Player.cs | 7 - TicTacToe/Grid.cs | 303 +++++++++++++++++++++++++-- TicTacToe/Player.cs | 2 +- 8 files changed, 369 insertions(+), 58 deletions(-) delete mode 100644 CleanTicTacToe/Properties/Grid.cs delete mode 100644 CleanTicTacToe/Properties/Player.cs 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