427 lines
16 KiB
C#
427 lines
16 KiB
C#
/* Shaun Marquardt
|
|
* TINFO 200
|
|
* CS3: TicTacToe
|
|
* ****************************************************
|
|
* Change History
|
|
* Date Developer Description
|
|
* 2020-02-18 marqusa File creation and initial implementation,
|
|
* Martyr2 (Coders Lexicon), Microsoft, Chuck Costarella L5Life
|
|
* 2020-02-22 marqusa Implement TakeCPUTurn.
|
|
* Implement draw game scenario (Thanks, Wargames).
|
|
* 2020-02-23 marqusa Add functionality to have CPU play as player 1 or player 2.
|
|
*
|
|
* REFERENCES
|
|
* https://www.coderslexicon.com/passing-data-between-forms-using-delegates-and-events/
|
|
* https://docs.microsoft.com/en-us/dotnet/api/system.eventargs?view=netframework-4.8
|
|
*
|
|
* AND NOW In English, a more descriptive part of what is going on here.
|
|
* -----------------
|
|
* So the first thing that happens on Form1_Load, is the form will instantiate
|
|
* a new TicTacToe class and pass in the form to the constructor.
|
|
* The constructor will attach the form delegate and initialize the game board
|
|
* along with setting the GameOver variable to False.
|
|
*
|
|
* When a label is clicked, a specific row and column is passed to TakePlayerTurn
|
|
* along with the current player's turn identifier (by PlayerTurn enum type).
|
|
*
|
|
* TakePlayerTurn will check for a winner using CheckForWinner.
|
|
*
|
|
* CheckForWinner will set GameOver to TRUE if win or draw.
|
|
* if win, returns winning player's X or O symbol using XorO enum type.
|
|
* If draw, still sets GameOver to TRUE, but returns NONE XorO enum type.
|
|
*
|
|
* After that, the player turn will be toggled using TogglePlayerTurn, to
|
|
* signal to the players and the game whose turn it is.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace TicTacToe
|
|
{
|
|
class TicTacToe
|
|
{
|
|
|
|
/* This is the class to play the classic game,
|
|
* Tic-Tac-Toe. The form you see displayed (Form1.cs)
|
|
* is primarily a driver for this TicTacToe class
|
|
* which actually plays the game behind the scenes.
|
|
*
|
|
* Requirements:
|
|
* 1) Class TicTacToe enables us to create a complete app to play
|
|
* the game of Tic-Tac-Toe.
|
|
* 2) This class contains a private 3x3 rectangular array of integers.
|
|
* 3) The constructor initializes the empty board.
|
|
* 4) Each move should be in an empty square.
|
|
* 5) Determine win, lose, or draw.
|
|
*
|
|
* Extra Credit:
|
|
* The app is developed using a Windows Form application.
|
|
* It is possible to play vs CPU as CPU player 1 or player 2.
|
|
*
|
|
* Extra Fun Stuff:
|
|
* The game actively tracks the current player's turn.
|
|
* I used an enum for both player turn and X and O.
|
|
* I included a delegate and custom event args to pass
|
|
* data from the TicTacToe class and update the correct label.
|
|
* The game is really handled by basically everything in this
|
|
* TicTacToe class with the form as a GUI frontend.
|
|
*/
|
|
|
|
//Game Over auto-implemented property
|
|
//with internal set
|
|
public bool GameOver { get; internal set; }
|
|
|
|
//Current player turn is of type enum PlayerTurn.
|
|
//with internal set
|
|
//This helps the game track whose turn it currently is.
|
|
public PlayerTurn CurrentPlayerTurn { get; internal set; }
|
|
|
|
//The class contains a private 3-by-3 rectangular array of integers.
|
|
private int[,] gameBoard;
|
|
|
|
//Enum XorO describes X as 1, O as 2, and none as 0.
|
|
//We need a 'none' value to help pass along 'nothing'
|
|
//or the winning player back to the form on TogglePlayerTurn.
|
|
public enum XorO
|
|
{
|
|
none = 0,
|
|
x = 1,
|
|
o = 2
|
|
}
|
|
|
|
//Enum PlayerTurn helps track which player's turn it is
|
|
//using Player 1=1, Player2=2, and CPU=3.
|
|
public enum PlayerTurn
|
|
{
|
|
Player1 = 1,
|
|
Player2 = 2,
|
|
CPU = 3
|
|
}
|
|
|
|
//Delegates to pass data to the GUI Form. Ref: Coder's Lexicon
|
|
public delegate void SendMessage(object obj, EventArgs e);
|
|
public event SendMessage OnSendMessage;
|
|
|
|
//An eventarg class to send to the GUI form via the delegate. Ref: Microsoft
|
|
public class RowColumnEventArgs : EventArgs
|
|
{
|
|
public int row { get; set; }
|
|
public int col { get; set; }
|
|
}
|
|
|
|
// Class constructor
|
|
// 1 - Create the object - chunk out memory on the heap for the actual storage.
|
|
// 2 - initialize all relevant variables to reasonable defaults.
|
|
public TicTacToe(frmTicTacToe frm)
|
|
{
|
|
//Construct a 3x3 game board.
|
|
gameBoard = new int[3, 3];
|
|
|
|
//The constructor should initialize the empty board to all blank's.
|
|
InitializeGameBoard();
|
|
|
|
//Set the game to be "over"
|
|
GameOver = true;
|
|
|
|
//form driver, says "attach its MessageReceived function to the event"
|
|
OnSendMessage += frm.MessageReceived;
|
|
}
|
|
|
|
//Initializes the game board to the start state of raw storage
|
|
//Preconditions: not yet
|
|
//Inputs: no args
|
|
//Outputs: void return
|
|
//Postconditions: not yet
|
|
//ref: Chuck Costarella, L5Life
|
|
private void InitializeGameBoard()
|
|
{
|
|
//iterate through each row
|
|
for (int r = 0; r < 3; r++)
|
|
{
|
|
//iterate through each column in an individual row
|
|
for (int c = 0; c < 3; c++)
|
|
{
|
|
//init all cells in the game board to the init start status - none
|
|
gameBoard[r,c] = (int)XorO.none;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Starts a new game.
|
|
* Preconditions: not yet
|
|
* Inputs: no args
|
|
* Outputs: void return
|
|
* Postconditions: not yet
|
|
*/
|
|
internal void StartNewGame()
|
|
{
|
|
//Start the game.
|
|
GameOver = false;
|
|
|
|
//Reset the game board.
|
|
InitializeGameBoard();
|
|
|
|
//Set to the first player's CurrentPlayerTurn.
|
|
CurrentPlayerTurn = PlayerTurn.Player1;
|
|
}
|
|
|
|
/* Take a given player's turn.
|
|
* Preconditions: not yet
|
|
* Inputs: PlayerTurn, row, col
|
|
* Outputs: void return
|
|
* Postconditions: not yet
|
|
*/
|
|
internal void TakePlayerTurn(PlayerTurn playerTurn, int row, int col)
|
|
{
|
|
//If the game's over, don't take any more turns.
|
|
if (GameOver)
|
|
return;
|
|
|
|
//If the requested row or column is already taken, don't make a move.
|
|
//Do not allow placing a move in a spot that already has been placed.
|
|
if (gameBoard[row,col] != 0)
|
|
return;
|
|
|
|
//ref: Microsoft
|
|
RowColumnEventArgs args = new RowColumnEventArgs();
|
|
args.row = row;
|
|
args.col = col;
|
|
|
|
switch (playerTurn)
|
|
{
|
|
case PlayerTurn.Player1: //Take the first player's CurrentPlayerTurn.
|
|
if(gameBoard[row,col] == (int)XorO.none)
|
|
{
|
|
gameBoard[row, col] = (int)XorO.x; //The first player is X.
|
|
OnSendMessage?.Invoke(this, args); //ref: Coder's Lexicon
|
|
}
|
|
break;
|
|
case PlayerTurn.Player2: //Take the second player's turn.
|
|
if (gameBoard[row, col] == (int)XorO.none)
|
|
{
|
|
gameBoard[row, col] = (int)XorO.o; //The second player is O.
|
|
OnSendMessage?.Invoke(this, args); //ref: Coder's Lexicon
|
|
}
|
|
break;
|
|
case PlayerTurn.CPU: //Take the CPU's turn.
|
|
TakeCPUTurn();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Take the CPU's turn. Uses Random.Next
|
|
* which automatically seeds the random number generator
|
|
* to the current system time.
|
|
* Preconditions: not yet
|
|
* Inputs: no args
|
|
* Outputs: void return
|
|
* Postconditions: not yet
|
|
*/
|
|
public void TakeCPUTurn()
|
|
{
|
|
//If the game is over, do nothing.
|
|
if (GameOver)
|
|
return;
|
|
|
|
//Create a new instance of the System.Random class.
|
|
//It is automatically seeded with the current system time.
|
|
Random rnd = new Random();
|
|
|
|
//Create a string list of available indexes.
|
|
//as I won't know how big the collection will be.
|
|
List<string> availableIndexes = new List<string>();
|
|
|
|
//Parse through the gameBoard and find all
|
|
//the indexes that are still 0.
|
|
//For each row in the gameboard
|
|
for (int r = 0; r < 3; r++)
|
|
{
|
|
//Iterate through each column.
|
|
for(int c = 0; c < 3; c++)
|
|
{
|
|
//If there is nothing in the current position
|
|
if(gameBoard[r,c] == 0)
|
|
{
|
|
//add it to the availableIndexes list collection.
|
|
availableIndexes.Add($"{r}{ c}");
|
|
}
|
|
}
|
|
} //end for
|
|
|
|
//no more spaces left on the game board.
|
|
if (availableIndexes.Count == 0)
|
|
{
|
|
GameOver = true;
|
|
return;
|
|
}
|
|
|
|
//Now determine where the computer is going to
|
|
//place the move.
|
|
int randomIndex = rnd.Next(0, availableIndexes.Count);
|
|
string computerMove = availableIndexes[randomIndex];
|
|
int row = int.Parse(computerMove[0].ToString());
|
|
int col = int.Parse(computerMove[1].ToString());
|
|
|
|
//Setup the event args to pass back
|
|
//to the main game form.
|
|
//ref: Microsoft
|
|
RowColumnEventArgs args = new RowColumnEventArgs();
|
|
args.row = row;
|
|
args.col = col;
|
|
|
|
//Now place the computer move in the determined
|
|
//row and column of the game board
|
|
//and update the form.
|
|
if(Properties.Settings.Default.CPUAsPlayer == 1)
|
|
gameBoard[row, col] = (int)XorO.x;
|
|
else
|
|
gameBoard[row, col] = (int)XorO.o; //The second player is O.
|
|
|
|
OnSendMessage?.Invoke(this, args); //ref: Coder's Lexicon
|
|
} //TakeCPUTurn
|
|
|
|
/* Toggles a player turn between player 1 and player 2
|
|
* or player 1 and CPU.
|
|
* Also runs the CheckForWinner function before toggling player turn.
|
|
* Preconditions: not yet
|
|
* Inputs: PlayerTurn enum type
|
|
* Outputs: XorO enum type
|
|
* Postconditions: not yet
|
|
*/
|
|
internal XorO TogglePlayerTurn(PlayerTurn playerTurn)
|
|
{
|
|
//Check for winning player before toggling player turn.
|
|
XorO winningPlayer = CheckForWinner(playerTurn);
|
|
|
|
//If we do not have a winner
|
|
if (!GameOver)
|
|
{
|
|
//Swap the player turns.
|
|
//First is the easy case: If we are not playing vs CPU
|
|
//simply swap player turn.
|
|
if(!Properties.Settings.Default.PlayVsCPU)
|
|
{
|
|
if (CurrentPlayerTurn == PlayerTurn.Player1)
|
|
CurrentPlayerTurn = PlayerTurn.Player2;
|
|
else
|
|
CurrentPlayerTurn = PlayerTurn.Player1;
|
|
}
|
|
else //We are playing against CPU.
|
|
{
|
|
//Determine if CPU is Player 1 or Player 2.
|
|
if(Properties.Settings.Default.CPUAsPlayer == 1) //CPU is player 1.
|
|
{
|
|
if (CurrentPlayerTurn == PlayerTurn.Player2)
|
|
{
|
|
CurrentPlayerTurn = PlayerTurn.CPU;
|
|
}
|
|
else
|
|
{
|
|
CurrentPlayerTurn = PlayerTurn.Player2;
|
|
}
|
|
}
|
|
else //CPU is player 2.
|
|
{
|
|
if (CurrentPlayerTurn == PlayerTurn.Player1)
|
|
{
|
|
CurrentPlayerTurn = PlayerTurn.CPU;
|
|
}
|
|
else
|
|
{
|
|
CurrentPlayerTurn = PlayerTurn.Player1;
|
|
}
|
|
}
|
|
}
|
|
} //end if !GameOver
|
|
|
|
//Return 'none' if no winning player,
|
|
//else return the winning player.
|
|
return winningPlayer;
|
|
} // TogglePlayerTurn
|
|
|
|
/* Checks for a winner.
|
|
* If we have a winner, set GameOver to True
|
|
* and return the winning player's game piece.
|
|
* Preconditions: not yet
|
|
* Inputs: PlayerTurn enum
|
|
* Outputs: XorO enum
|
|
* Postconditions: not yet
|
|
*/
|
|
private XorO CheckForWinner(PlayerTurn playerTurn)
|
|
{
|
|
//chicken dinner
|
|
bool bWinnerWinner = true;
|
|
XorO winningPlayer = XorO.none;
|
|
|
|
//Draw Game. In this foreach loop,
|
|
//bWinnerWinner is most likely set false.
|
|
foreach (var value in gameBoard)
|
|
{
|
|
//It's not a draw game.
|
|
if (value == 0)
|
|
{
|
|
bWinnerWinner = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//Winner on a column.
|
|
for (int row = 0; row < 3; row++)
|
|
{
|
|
if(gameBoard[row,0] != 0 //rowN col0 has something in it
|
|
&& gameBoard[row,0] == gameBoard[row,1] //rowN col0 is equal to col1
|
|
&& gameBoard[row,0] == gameBoard[row,2])//and rowN col0 is equal to col2
|
|
{
|
|
bWinnerWinner = true;
|
|
|
|
//ref: chuck
|
|
Enum.TryParse<XorO>(gameBoard[row, 0].ToString(), out winningPlayer);
|
|
}
|
|
}
|
|
|
|
//Winner on a row.
|
|
for(int col = 0; col < 3; col++)
|
|
{
|
|
if (gameBoard[0, col] != 0 //rowN col0 has something in it
|
|
&& gameBoard[0, col] == gameBoard[1, col] //colN row0 is equan to row1
|
|
&& gameBoard[0, col] == gameBoard[2, col])//and colN row0 is equal to row2
|
|
{
|
|
bWinnerWinner = true;
|
|
|
|
//ref: chuck
|
|
Enum.TryParse<XorO>(gameBoard[0, col].ToString(), out winningPlayer);
|
|
}
|
|
}
|
|
|
|
//Winner diagonally.
|
|
if(gameBoard[1,1] != 0 //the middle has something in it.
|
|
&& ( (gameBoard[0,0] == gameBoard[1,1] //Winner from top left to btm right
|
|
&& gameBoard[1,1] == gameBoard[2,2])
|
|
||(gameBoard[0,2] == gameBoard[1,1] //OR winner from top rt to btm lft
|
|
&& gameBoard[2,0] == gameBoard[1,1]) )
|
|
) //end if
|
|
{
|
|
bWinnerWinner = true;
|
|
Enum.TryParse<XorO>(gameBoard[1, 1].ToString(), out winningPlayer);
|
|
}
|
|
|
|
//We have a winner or draw game.
|
|
if (bWinnerWinner)
|
|
{
|
|
//Set the game to be over.
|
|
GameOver = true;
|
|
|
|
//Return the winning player's piece.
|
|
return winningPlayer;
|
|
}
|
|
|
|
//Return no game piece as the game is not over.
|
|
return XorO.none;
|
|
}
|
|
} //CheckForWinner()
|
|
}
|