Added proprietary user interface.

Added most game logic.
This commit is contained in:
Michael Chen 2020-01-04 13:13:13 +01:00 committed by Michael Chen
parent 71c1a4d20c
commit 27f6efb58a
Signed by: cnml
GPG Key ID: 5845BF3F82D5F629
8 changed files with 369 additions and 58 deletions

View File

@ -62,8 +62,6 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput> <LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</EmbeddedResource> </EmbeddedResource>
<Compile Include="Properties\Grid.cs" />
<Compile Include="Properties\Player.cs" />
<Compile Include="Properties\Resources.Designer.cs"> <Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon> <DependentUpon>Resources.resx</DependentUpon>
@ -81,5 +79,11 @@
<ItemGroup> <ItemGroup>
<None Include="App.config" /> <None Include="App.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TicTacToe\TicTacToe.csproj">
<Project>{8EB373A9-D083-4BFE-9396-E9A40E75AAC2}</Project>
<Name>TicTacToe</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@ -23,20 +23,46 @@
/// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden. /// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden.
/// </summary> /// </summary>
private void InitializeComponent() { private void InitializeComponent() {
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.TslStatus = new System.Windows.Forms.ToolStripStatusLabel();
this.statusStrip1.SuspendLayout();
this.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 // MainForm
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 25F); this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 25F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1010, 891); this.ClientSize = new System.Drawing.Size(1010, 891);
this.Controls.Add(this.statusStrip1);
this.Name = "MainForm"; this.Name = "MainForm";
this.Text = "Form1"; this.Text = "Form1";
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.ResumeLayout(false); this.ResumeLayout(false);
this.PerformLayout();
} }
#endregion #endregion
private System.Windows.Forms.StatusStrip statusStrip1;
private System.Windows.Forms.ToolStripStatusLabel TslStatus;
} }
} }

View File

@ -1,21 +1,59 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing; using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using TicTacToe; using TicTacToe;
namespace CleanTicTacToe { namespace CleanTicTacToe {
public partial class MainForm : Form { public partial class MainForm : Form {
private const int BUTTONSIZE = 100;
private readonly Grid _grid; private readonly Grid _grid;
private readonly Button[,] _pbxs = new Button[Grid.GRIDSIZE, Grid.GRIDSIZE];
public MainForm() { public MainForm() {
InitializeComponent(); InitializeComponent();
_grid = new Grid(); _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])}";
}
} }
} }
} }

View File

@ -117,4 +117,7 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root> </root>

View File

@ -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() {
}
}
}

View File

@ -1,7 +0,0 @@
namespace TicTacToe {
enum Player {
Empty = 0,
Cross = 1,
Circle = 2
}
}

View File

@ -1,26 +1,299 @@
namespace TicTacToe { using System;
public class Grid { using System.Text;
private readonly Player[,] _field;
private const int GRIDSIZE = 3;
public bool GameOver => IsGameOver();
private bool IsGameOver() { namespace TicTacToe {
for (var x = 0; x < GRIDSIZE; x++) 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 y = 0; y < GRIDSIZE; y++)
if (IsFieldEmpty(x, 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; return false;
}
}
Winner = Player.Empty;
return true; return true;
} }
/// <summary>
private bool IsFieldEmpty(int x, int y) { /// Checks primary diagonal on a win.
return _field[x, y] == Player.Empty; /// </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() { /// <summary>
_field = new Player[GRIDSIZE, GRIDSIZE]; /// 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!");
};
} }
} }
} }

View File

@ -1,5 +1,5 @@
namespace TicTacToe { namespace TicTacToe {
enum Player { public enum Player {
Empty = 0, Empty = 0,
Cross = 1, Cross = 1,
Circle = 2 Circle = 2