TINFO200/Cs3Apps/TicTacToe/TicTacToe.cs

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