-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
361 lines (303 loc) · 16.5 KB
/
Program.cs
File metadata and controls
361 lines (303 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
using System;
namespace TicTacToe
{
class Program
{
// Declaring a multidimensional array with a fixed size
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/arrays/multidimensional-arrays
static char[,] emptyBoard = new char[3, 3] { { '1', '2', '3' }, { '4', '5', '6' }, { '7', '8', '9' } };
static int gameCount = 1; // Will be used to decide which player's turn is next
static char currentPlayer; //= (gameCount % 2 == 0) ? 'O' : 'X';
// There has got to be a better way of creating this var - investigate
// It would have been easier to use an array with single dimension instead though
static char[,] gameState = new char[3, 3] { { ' ', ' ', ' ' }, { ' ', ' ', ' ' }, { ' ', ' ', ' ' } };
static void Main(string[] args)
{
bool continueGame = true; // Game continues until this is false
// Game on auto-repeat unless continueGame is false
do
{
// Continue game prompt boolean var, when false "play again" question will repeat. Needs to be in this context to reset on every repeat game
bool validContinueGameResponse = false;
// Change player on each run, start with X
currentPlayer = (gameCount % 2 == 0) ? 'O' : 'X';
// Clear the gameState variable when starting a new game
gameState = new char[3, 3] { { ' ', ' ', ' ' }, { ' ', ' ', ' ' }, { ' ', ' ', ' ' } };
// Start a round of tic-tac-toe
PlayGame();
// Play again on auto-repeat - allows for prompting the player and repeating a game should the user choose so
do
{
Console.Write("Would you like to play again? (Y/N): ");
// It would have been better to use ReadKey(), then compare the key to the array of acceptable key responses, and have a generic message
// providing instruction(s) to the user. This takes care of (playAgain.Length > 1).However, as one must allow user to
// potentially enter more than one key ("Yes" response in a scrnshot) for this exercise,
// ReadLine() must be used
string playAgain = Console.ReadLine();
// Checking the input (playAgain) and processing errors as required as well as the correct responses (y/n)
if (playAgain.Length > 1) // First check if the user typed in a sentence out at random for the y/n prompt
{
Console.WriteLine("Error: String must be exactly one character long. Please try again.");
}
// Process a valid "no" response.
else if (playAgain.ToLower() == "n")
{
continueGame = false;
validContinueGameResponse = true;
}
// Process a valid "yes" response.
else if (playAgain.ToLower() == "y")
{
continueGame = true;
validContinueGameResponse = true;
}
else
// It's not longer than 1 in length, it's not a 'y' or 'n', so it has to be some other single char. This will also catch empty/blank input
{
Console.WriteLine("Error: Must be 'Y' or 'N'. Please try again.");
}
} while (!validContinueGameResponse);
// Increase the game count to change the starting player for the next game
gameCount++;
} while (continueGame);
}
// Procedure that handles game mechanics (user inputs and associated errors)
static void PlayGame()
{
bool gameOver = false;
bool validPlayerMovement = false; // Test for valid response received for user input
// Let's print the board for starters. This is where the logic check for game win will be
// PrintGameBoard plus the procedure for entering new positions plus the logic check.
// gameState variable should probably go here to avoid having to reset on new game.
int moveCount = 1; // Used to change players between moves
do
{
// This loop allows for the game to carry on until an end-state has been reached (one of the players wins, or the game is a draw)
// Print the game board
PrintGameBoard();
// Handle the player input. This loop controls the prompts in case incorrect input has been provided. Contains a function/(method) for
// checking if a position has already been taken. Depends on the checkGameState to verify game state, i.e. (win/draw).
do
{
validPlayerMovement = false; // Resolution for Bug 1. Resets the validPlayerMovement var after a successful move
Console.Write($"\nPlayer {currentPlayer}, please enter a square number to place your token in: ");
string currentPlayerMove = Console.ReadLine();
// Function for checking if the position in the grid has been taken by a symbol (player)
// If taken, returns true. If not taken, returns false and sets the value at the appropriate position in the gameState array equal
// to the currentPlayer symbol.
bool checkSetPlayerPosition(int playerMove)
{
if (playerMove <= 3)
{
// Compensate for the 0-based index by subtracting 1
if (gameState[0, playerMove - 1] != ' ')
{
return (true);
}
else
{
gameState[0, playerMove - 1] = currentPlayer;
return (false);
}
} //playerMove > 3 and <= 6
else if (playerMove <= 6)
{
// Rebase for the number of columns (subtract 3 from playerMove) and the 0-based index (subtract 1)
if (gameState[1, playerMove - 4] != ' ')
{
return (true);
}
else
{
gameState[1, playerMove - 4] = currentPlayer;
return (false);
}
}
else
{
// Rebase for the number of columns (subtract 6 because it's in row 3, subtract 1 to correct for index)
if (gameState[2, playerMove - 7] != ' ')
{
return (true);
}
else
{
gameState[2, playerMove - 7] = currentPlayer;
return (false);
}
}
}
// Simple way to detect if response is a number and return a boolean
// https://docs.microsoft.com/en-us/dotnet/api/system.int32.tryparse?view=netcore-3.1#System_Int32_TryParse_System_String_System_Int32__
if (Int32.TryParse(currentPlayerMove, out int intCurrentPlayerMove))
{
// Elegant, since result is produced by TryParse() above
if (intCurrentPlayerMove < 1 || intCurrentPlayerMove > 9)
{
// The number entered is not between 1 and 9 (inclusive), display error
Console.WriteLine("Error: That square number does not exist. Please try again.");
}
else if (checkSetPlayerPosition(intCurrentPlayerMove)) // See above for function that checks if position is taken
{
// The number chosen has already been taken
Console.WriteLine("Error: That square number is already occupied. Please try again.");
}
else
{
// Increase the move count to change the current player
moveCount++;
// Change the player for the next move
currentPlayer = (currentPlayer == 'X') ? 'O' : 'X';
// Check game state (are there any winners, is there a draw?)
gameOver = checkGameState();
// Current move is over
validPlayerMovement = true;
}
}
else
{
// String entered was not a number
Console.WriteLine("Error: Input string was not in the correct format. Please try again.");
}
} while (!validPlayerMovement);
} while (!gameOver);
} // End of PlayGame()
// Procedure for generating an empty gameboard as well as the game status board.
static void PrintGameBoard()
{
// Create the numbered grid, with 2 blanks/spaces around the number/symbol.
// I wasn't happy with just manually entering the req'd number of dashes, but rather opted to use
// the string constructor to allow for a bit of flexibility in terms of grid spacing. This may come in
// handy should one want to change the spacing/sizing in the future
// ref: https://stackoverflow.com/questions/3754582/is-there-an-easy-way-to-return-a-string-repeated-x-number-of-times
string horizLine = new string('-', 13); // 13 dashes
string blankSpace = new string(' ', 1); // Whitespace around the number, a single space.
char vertDelim = '|'; // Symbol for vertical lines
// Clear console
// https://docs.microsoft.com/en-us/dotnet/api/system.console.clear?view=netcore-3.1
Console.Clear();
// Passing a character array as an argument to a procedure, idea being to recycle the code for drawing the board and substitute numbers
// or symbols using an array. Using a for loop which draws row per row.
void genBoard(char[,] boardVar)
{
// Horizontal line above the grid
Console.WriteLine(horizLine);
// Let's go row by row of the array
for (int arrRow = 0; arrRow < 3; arrRow++)
{
// Interpolated string. It's more concise. I like concise
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated
Console.Write($"{vertDelim}");
// Column by column
for (int arrCol = 0; arrCol < 3; arrCol++)
{
// Allows for passing boardVar to Write, which can't be done using interpolated strings (see above for ref.)
Console.Write("{0}{1}{0}{2}", blankSpace, boardVar[arrRow, arrCol], vertDelim);
}
Console.WriteLine($"\n{horizLine}");
}
}
// Generate blank board
Console.WriteLine("Game Board Positions:");
genBoard(emptyBoard);
// Generate status board
Console.WriteLine("\nGame Board Status:");
genBoard(gameState);
}
// Function checkGameState looks at the rows, columns and two diagonals of the gameState array.
// If a sequence of 3 consecutive 'X' or 'O' characters has been found, it prints the game board,
// declares a winner and returns true. Return value is set to the gameOver variable which stops
// the execution of the specific game currently in progress.
static bool checkGameState()
{
// Check if game is a draw, the cheesy way
int fullBoard = 0;
foreach (char playerMove in gameState)
{
if (playerMove != ' ') { fullBoard++; }
}
if (fullBoard == 9)
{
PrintGameBoard();
Console.WriteLine("The game was a draw.");
return (true);
}
// Check gameState columns. The idea is to concat the values in across the row for each column, then compare to a
// string XXX or OOO.
for (int arrCol = 0; arrCol < 3; arrCol++)
{
string colCheck = "";
for (int arrRow = 0; arrRow < 3; arrRow++)
{
colCheck = colCheck + gameState[arrRow, arrCol].ToString();
}
switch (colCheck)
{
case "XXX":
PrintGameBoard();
Console.WriteLine("Player X was the winner!");
return (true);
case "OOO":
PrintGameBoard();
Console.WriteLine("Player O was the winner!");
return (true);
default:
break;
}
}
// Check gameState rows
for (int arrRow = 0; arrRow < 3; arrRow++)
{
string rowCheck = "";
for (int arrCol = 0; arrCol < 3; arrCol++)
{
rowCheck = rowCheck + gameState[arrRow, arrCol].ToString();
}
switch (rowCheck)
{
case "XXX":
PrintGameBoard();
Console.WriteLine("Player X was the winner!");
return (true);
case "OOO":
PrintGameBoard();
Console.WriteLine("Player O was the winner!");
return (true);
default:
break;
}
}
// Check gameState diagonals. I don't like having too many for loops, so two diagonals are determined
// using a single loop.
string diagonalCheck1 = "", diagonalCheck2 = "";
for (int arrRow = 0; arrRow < 3; arrRow++)
{
diagonalCheck1 = diagonalCheck1 + gameState[arrRow, arrRow].ToString();
diagonalCheck2 = diagonalCheck2 + gameState[arrRow, 2 - arrRow].ToString();
}
// I don't like repeating chunks of code unless necessary, so here's a foreach loop to determine if either of
// the two variables with diagonals (diagonalCheck1 and diagonalCheck2) contains a winning combination.
// I could do this since I did not have a loop within a loop like for column/row checks above
// Note: This is also a bug fix for Bug 2: Initial version only checked one diagonal for a winner.
// Reference https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/arrays/using-foreach-with-arrays
foreach (string selDiag in new string[] { diagonalCheck1, diagonalCheck2 } )
{
switch (selDiag)
{
case "XXX":
PrintGameBoard();
Console.WriteLine("Player X was the winner!");
return (true);
case "OOO":
PrintGameBoard();
Console.WriteLine("Player O was the winner!");
return (true);
default:
break;
}
}
// Covers fail-safe scenario
return (false);
}
} // End of Program
}