Project Details
Make the code of the paper game Tic Tac Toe, with a 3x3 board and a 2 player system, including a menu interface and a help/controls screen.
If a player wins, or a draw is achieved, a win/draw screen must be displayed.
Project Information
- Date Started: October 21, 2024
- Date Finished: October 27, 2024
- Status: Completed
Strategy
One strategy for a project like this is to break the problem down into smaller parts that make up as a whole. Then, we build on to these smaller parts that are much easier to build from scratch. Identifying what parts we need to build and how they connect from one another sequentially allows us to get a perfect idea of how the code should go.
Identifying the Components
For the code to work, I identified some of the main components that is the bare minimum that should allow our game to work.
- Board Setup Mechanism. The first thing to do is to set up the board. This is to reset the game state to a state where a new game can be played again.
- Key Detection Mechanism. The computer should detect if a key is pressed, and when it does, it should execute out a specific set of instructions depending on the key pressed.
- Cursor Mechanism. A cursor must be displayed on the board which would allow the players to choose where to place their next move. This should be paired with the Key Detection Component and executes when a directional key, like WASD is pressed.
- Move Set Mechanism. If a player chooses a tile which they will place their move, the program should store it and update the console that their move is already placed. Again, this can be paired with the Key Detection Mechanism, and executes when the Enter key is pressed.
- Win/Draw Detect Mechanism. From the moves from either players stored by the Move Set Mechanism, the program should detect if a player had already won the game or both players had a draw.
- Player Switch Mechanism. If a program does not detect a win or draw, neither player has forfeited, the game is still in effect, and one player had already made their move, the players/users should be let know through a console update that it is the other player’s turn.
Identifying the Flow
Identifying the components are just the beginning, the next thing we can do is to set up the flow of our code: what goes first, what goes next, and when it loops back to some part of itself. This can be done using a flowcharting process, for a more visual way.
flowchart TD board[[Board Set]] keyPress[[Await Key Press]] ifMove{If key pressed: WASD} ifEnter{If key pressed: Enter} cursorMove[[Move Cursor]] curUpdate[[Cursor Update]] gameUpdate[Game Update] valStore[[Store Move]] wdDetect{Detect if win/draw} wdScr[[Win/Draw Screen]] playerChange[[Player Change]] start((Start)) stop((End)) start --> board board --> keyPress keyPress --> ifMove ifMove --> |Yes|cursorMove cursorMove --> curUpdate curUpdate --> keyPress ifMove --> |No|ifEnter ifEnter --> |Yes|valStore valStore --> gameUpdate gameUpdate --> wdDetect wdDetect --> |Yes|wdScr wdScr --> stop wdDetect --> |No|playerChange playerChange --> keyPress ifEnter --> |No|keyPress
Here, we see that the program terminates after a result screen.
We can make the program loop after a result screen as an added feature if they wanted to make an another game.
Since, the flow of the program simply captures the entire process, we can simply leave it off like this so that the flowchart would not be unnecessarily complicated.
Building the Program
In this section, we’ll talk on building each section and the strategy on how they are made.
Board Setup
When setting up the board, we can use some of the functions over to my previous projects Cuya - Performance Task 3 on display setup.
// For console padding
int lpadding = 0;
int rpadding = 0;
// Sets padding
void setpd (int left, int right) {
lpadding = left;
rpadding = right;
}void createBorder (int starty, int endy, char vertChar, char horChar, char vertex) {
int i, startx, endx;
startx = lpadding;
endx = 80 - rpadding;
for (i = startx; i <= endx; i++) {
gotoxy(i, starty); cout << horChar;
}
for (i = startx; i <= endx; i++) {
gotoxy(i, endy); cout << horChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(startx, i); cout << vertChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(endx, i); cout << vertChar;
}
gotoxy(startx, starty); cout << vertex;
gotoxy(startx, endy); cout << vertex;
gotoxy(endx, starty); cout << vertex;
gotoxy(endx, endy); cout << vertex;
}Here, the border is created by manually setting the console with cout and gotoxy, paired with a loop. This uses a global variable lpadding and rpadding, which is used by the function to automatically align the border in the left and the right.
This can then be improved by setting two extra global variables, upadding and dpadding, which can be used to automatically align the border from the top to bottom.
The padding and border setup now goes like this:
/// padding variables
int lpadding = 0;
int rpadding = 0;
int upadding = 0;
int dpadding = 0;
void setpd (int left, int right, int up, int down) {
lpadding = left;
rpadding = right;
upadding = up;
dpadding = down;
}
void border (char vertChar, char horChar, char vertex) {
int i, startx, endx, starty, endy;
startx = lpadding;
endx = 80 - rpadding;
starty = upadding;
endy = 25 - dpadding;
for (i = startx; i <= endx; i++) {
gotoxy(i, starty); cout << horChar;
}
for (i = startx; i <= endx; i++) {
gotoxy(i, endy); cout << horChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(startx, i); cout << vertChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(endx, i); cout << vertChar;
}
gotoxy(startx, starty); cout << vertex;
gotoxy(startx, endy); cout << vertex;
gotoxy(endx, starty); cout << vertex;
gotoxy(endx, endy); cout << vertex;
}This would remove the starty and endy argument in the previous function.
It would also make the setpd() function more useful.
The new border() can then be used to set up the main board.
This alone will only output a single box in the center of the console.
We can then utilize these next few functions that can be used to make up the cells of our board.
void hr (int y, char lineChar, char endChar) {
int i, startx, endx;
startx = lpadding;
endx = 80 - rpadding;
gotoxy(startx, y); cout << endChar;
for (i = startx + 1; i <= endx - 1; i++) {
gotoxy(i, y); cout << lineChar;
}
gotoxy(endx, y); cout << endChar;
}This function is from the Cuya - Performance Task 3 project. It creates a straight horizontal line that spans between the border boundaries.
void vr (int x, char lineChar, char endChar) {
int i, starty, endy;
starty = upadding;
endy = 25 - dpadding;
gotoxy(x, starty); cout << endChar;
for (i = starty + 1; i <= endy - 1; i++) {
gotoxy(x, i); cout << lineChar;
}
gotoxy(x, endy); cout << endChar;
}This function, unique to this project creates a vertical line that spans between the border boundaries. This is now made possible due to the existence of upadding and dpadding.
Both functions accept two characters as the argument. The character lineChar is used on the line itself, endChar is the character used to its endpoints.
Altogether, these functions can then be used to set up the board.
// Resets the board border state/Removes any cell select indicators
void setBoard () {
setpd(25, 25, 6, 7);
border('|', '-', char(254));
hr(10, '-', char(254));
hr(14, '-', char(254));
vr(35, '|', char(254));
vr(45, '|', char(254));
createBox(35, 10, 45, 14, '|','-', char(254));
}Basically, this function simply resets the border state.
Here, the char() function is an another predefined function that outputs a square character □ that is used for corners and design.
Notice that an another function, createBox() was called.
It is used so that the lines where hr() and vr() intersect are displayed with the same square character □.
It is defined in the same manner as createBorder() but independent from any padding restrictions.
void createBox (int startx, int starty, int endx, int endy, char vertChar, char horChar, char vertex) {
int i;
for (i = startx; i <= endx; i++) {
gotoxy(i, starty); cout << horChar;
}
for (i = startx; i <= endx; i++) {
gotoxy(i, endy); cout << horChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(startx, i); cout << vertChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(endx, i); cout << vertChar;
}
gotoxy(startx, starty); cout << vertex;
gotoxy(startx, endy); cout << vertex;
gotoxy(endx, starty); cout << vertex;
gotoxy(endx, endy); cout << vertex;
}Here, two points are used to define the box, whose coordinates are then used to draw the box itself, unlike the border() function draws a box that is dependent on the padding variables.
Key Press Detect
For the key press detection, the mechanism is the same as the key press detection in Cuya - Performance Task 3.
The concept is that getch(), aside from awaiting user key input, it returns a number/character that can be stored by assigning it to a variable.
int key = getch();When a specific key is pressed, an integer is assigned to the variable key.
As specific keys return specific values, these values can then be used in a conditional statement like if...else to execute a specific code when a key is pressed.
Some of these values are displayed below:
| Integer | Key Pressed |
|---|---|
| 13 | Enter Key |
| 26 | Esc Key |
| 32 | Space Key |
| 97 | A Key |
| 100 | D Key |
| 115 | S Key |
| 119 | W Key |
Basically, if we set up a conditional expression like this:
if (key == num) {
statement;
}It acts as a detector that executes code when a specific key is pressed.
To summarize the behavior of this function, here is a flowchart for that process.
flowchart TD start[[Start: Await Key Press]] init{{keyPressed = 0}} keyPress[/"Wait for key press keyPressed = getch();"/] key1{keyPressed == key1} key2{keyPressed == key2} key3{keyPressed == key3} block1[code to execute when key1 is pressed] block2[code to execute when key2 is pressed] block3[code to execute when key3 is pressed] stop(Exit) start --> init init --> keyPress keyPress --> key1 key1 --> |Yes|block1 key1 --> |No|key2 key2 --> |Yes| block2 key2 --> |No| key3 key3 --> |Yes| block3 key3 --> |No| keyPress block1 & block2 & block3 --> stop
In general, this mechanism is managed by the keyPress() function, which is defined as:
int keyPress() {
int cmd = 0;
int keyPressed = getch();
// Updates current selected cell value
if (keyPressed == 119) boardy--;
else if (keyPressed == 115) boardy++;
else if (keyPressed == 97) boardx--;
else if (keyPressed == 100) boardx++;
else if (keyPressed == 13) cmd = 1;
else if (keyPressed == 27) cmd = 2;
// Handles overflow for boardx
if (boardx > 3) boardx = 1;
else if (boardx < 1) boardx = 3;
// Handles overflow for boardy
if (boardy > 3) boardy = 1;
else if (boardy < 1) boardy = 3;
return cmd;
}The function is not a void, but an int data type, meaning, it returns an integer cmd that can be used on other functions when other special keys are pressed, like Enter or Esc for move select and pause functionality.
It also has other functions which will be covered in Cell Selection and Update
Cell Selection and Update
Once a board is set up, the player has to choose where they want to place their move by selecting a cell.
To provide this functionality, the board is first initialized as a 2D integer array.
int board[3][3] = {
{0, 0, 0},
{0, 0, 0},
{0, 0, 0}
};This would then allow us to store a player’s move depending on where in the board they played their move.
Another useful variable is the boardx and boardy.
This is then used to store the coordinates of the current selected cell.
// Selected cell coordinates
int boardx = 1;
int boardy = 1;These variables can only be manipulated using specific keys in keyPress().
// Updates current selected cell value
if (keyPressed == 119) boardy--;
else if (keyPressed == 115) boardy++;
else if (keyPressed == 97) boardx--;
else if (keyPressed == 100) boardx++;The numbers 119, 115, 97, and 100 correspond to W, A, S, and D respectively.
If any of these directional keys are pressed, then it can then increment or decrement the coordinates of the selected cell.
One drawback with this approach is that it can increase or decrease boardx and boardy beyond the size of our 3x3 board.
Since we want it to be within the boundaries of the board, the conditionals in the same keyPress() function does the job.
// Handles overflow for boardx
if (boardx > 2) boardx = 0;
else if (boardx < 0) boardx = 2;
// Handles overflow for boardy
if (boardy > 2) boardy = 0;
else if (boardy < 0) boardy = 2;This means that if boardx goes above 2, it automatically goes to the lowest possible value 0. If it goes below 0, it goes to the highest possible value instead, 2. The same process goes for boardy.
This is overflow handling. We do this so that if the user keeps pressing the key, the possible choices simply cycles over and over again.
We also set the boardx and boardy to be from 0 to 2 as arrays start from 0 instead of 1.
This means that the first entry start in 0, then the second entry in 1, and so on.
In practice, this is how the keyPress() cell selection system function would work.
graph TD start[["Start: keyPress()"]] init{{cmd = 0, keyPressed = 0}} getkey[/"keyPressed = getch()"/] ifW{if W is pressed keyPressed == 119} ifA{if A is pressed keyPressed == 115} ifS{if S is pressed keyPressed == 97} ifD{if D is pressed keyPressed == 100} incX[boardx++] decX[boardx--] incY[boardy++] decY[boardy--] goto((A)) overflowX{Test for overflow boardx > 2} underflowX{Test for underflow boardx < 0} overflowY{Test for overflow boardy > 2} underflowY{Test for underflow boardy > 0} setminX[boardx = 0] setmaxX[boardx = 2] setminY[boardy = 0] setmaxY[boardy = 2] return[\"Output cmd"\] exit(Exit) start --> init init --> getkey getkey --> ifW ifW --> |Yes|decY ifW --> |No|ifA ifA --> |Yes|decX ifA --> |No|ifS ifS --> |Yes|incY ifS --> |No|ifD ifD --> |Yes|incX ifD --> |No|goto decX & decY & incX & incY --> overflowX overflowX --> |Yes| setminX overflowX --> |No| underflowX underflowX --> |Yes| setmaxX underflowX --> |No| overflowY setminX & setmaxX --> overflowY overflowY --> |Yes| setminY overflowY --> |No| underflowY underflowY --> |Yes| setmaxY underflowY --> |No| return setminY & setmaxY --> return return --> exit
After the WASD decision tree, there should be two more that was omitted in the flowchart: ```cpp else if (keyPressed == 13) cmd = 1; else if (keyPressed == 27) cmd = 2; else if (keyPressed == 32) cmd = 3; ```
It concerns on how we can use the keyPress() function to perform other tasks.
For example, if the Enter key was pressed, it outputs a numeric value, which is why there is a return cmd; statement at the end.
It can then be used for other functions or purposes that require detecting key presses, such as in Program Flow and Handling, and the cmd ensures that a specific key is pressed, like 1 for Enter in this case
Once the variables of the selected cell is updated, the user should be notified which cell is selected in the user interface/console, which is the central functionality of the cellSelect() function.
void cellSelect() {
int startx, starty, endx, endy;
// Initializes coordinates in the board
startx = 25; starty = 6;
endx = 35; endy = 10;
setBoard();
// Modifiers to coords based on current cell selected
switch (boardx){
case 1:
startx += 10;
endx += 10;
break;
case 2:
startx += 20;
endx += 20;
break;
default:
startx += 0;
endx += 0;
break;
}
switch (boardy){
case 1:
starty += 4;
endy += 4;
break;
case 2:
starty+= 8;
endy += 8;
break;
default:
starty += 0;
endy += 0;
break;
}
createBox(startx, starty, endx, endy, char(186), char(205), char(254));
gotoxy(1, 1); cout << startx << " ," << starty;
gotoxy(1, 2); cout << endx << " ," << endy;
}This simply changes the borders of each cell in the UI to show that a specific cell is selected.
Managing Player Moves
Now that we can freely select any cell we want, we now need to take care what happens if a player places their move.
All aspects of player inputs are managed by the playerInput() function.
When setting up the player input, we store the player input first.
This is in order to:
- indicate that a cell is filled and not empty
- keep track of the entire board, which allows us to detect if a winning move is made.
We do this using this line of code in playerInput().
board[boardx][boardy] = currentPlayer;The way this works is like this:
- Using the
boardarray, we store the player’s move using the selected coordinatesboardxandboardy. - The
currentPlayervariable indicates the current player making the move.1indicates player 1 had made the move, and-1indicates that player 2 made the move.
One problem is that we might overwrite moves that previous players had already placed in the board.
To prevent this, we first detect if that specific cell is 0, otherwise, we simply ignore the player input.
This is also the same reason why we initialized the board variable as 0 in Cell Selection and Update
if (board[boardx][boardy] == 0) {
board[boardx][boardy] = currentPlayer;
}The next component of playerInput() is letting the player know that their move is already placed.
if (currentPlayer == 1) {
currentPlayer = -1;
char message1[6] = {' ', char(220), char(220), char(220), ' '};
char message2[6] = {char(219), char(219), char(219), char(219), char(219)};
char message3[6] = {' ', char(220), char(219), char(220), ' '};
int startx = 28;
int starty = 7;
switch (boardx){
case 1:
startx += 10;
break;
case 2:
startx += 20;
break;
default:
startx += 0;
break;
}
switch (boardy){
case 1:
starty += 4;
break;
case 2:
starty+= 8;
break;
default:
starty += 0;
break;
}
gotoxy(startx, starty); cout << message1;
gotoxy(startx, starty + 1); cout << message2;
gotoxy(startx, starty + 2); cout << message3;
} else if (currentPlayer == -1) {
currentPlayer = 1;
int startx, starty;
char message1[6] = {char(201), char(205), char(205), char(205), char(187)};
char message2[6] = {char(186), ' ', ' ', ' ', char(186)};
char message3[6] = {char(200), char(205), char(205), char(205), char(188)};
startx = 28;
starty = 7;
switch (boardx){
case 1:
startx += 10;
break;
case 2:
startx += 20;
break;
default:
startx += 0;
break;
}
switch (boardy){
case 1:
starty += 4;
break;
case 2:
starty+= 8;
break;
default:
starty += 0;
break;
}
gotoxy(startx, starty); cout << message1;
gotoxy(startx, starty + 1); cout << message2;
gotoxy(startx, starty + 2); cout << message3;
}This might be a little complex but what it essentially does is it places a unique symbol in the board to indicate that a move is placed by that player.
By default, player 1 outputs a club symbol, while player 2 outputs a box.
We place it inside the if statement from earlier so that it won’t overwrite the screen as well.
How it is displayed is that for each line, a string is stored in variables message1, message2, and message3.
It then displays the string line by line.
The switch statements are there to ensure that the symbols stay aligned even if placed in an another cell.
Before the symbol is generated, we also see these statements first.
if (currentPlayer == 1) {
currentPlayer = -1;
// ...
} else if (currentPlayer == -1) {
currentPlayer = 1;
// ...
}This ensures that after a player makes their move, they will take turns and so the next player can get a chance in placing their move next.
To summarize, the flow of the playerInput() function is:
graph TD start[["Start: playerInput()"]] emptyCell{"board[boardx][boardy] == 0"} assign["board[boardx][boardy] = currentPlayer"] detectPlayer{currentPlayer == <<number>>} displayHeart[[Display Heart at current location]] displayBox[[Display Box at current location]] switchAtoB[currentPlayer = -1] switchBtoA[currentPlayer = 1] stop((Stop)) start --> emptyCell emptyCell --> |Yes|assign emptyCell --> |No|stop assign --> detectPlayer detectPlayer --> |1| switchAtoB detectPlayer --> |-1| switchBtoA switchAtoB --> displayHeart switchBtoA --> displayBox displayHeart & displayBox --> stop
Game State Detection
After each move is placed, we must test if the current board if someone had already won or the game is a draw.
But first, we must identify when a set of moves is winning or not.
- A player wins the game if they placed their pieces three in a row, either horizontally, vertically, or diagonally.
- A game is a draw when both players have filled every possible cell and none of their pieces match a three in a row pattern.
This portion of the game is encapsulated by the testforWins() function, displayed as follows.
int testforWins () {
int i = 0, j = 0;
// Check for horizontal matches
for (i = 0; i < 3; i++) {
if ((board[i][0] == board[i][1]) && (board[i][1] == board[i][2]) && (board[i][1] != 0)) return 1;
}
// Check for vertical matches
for (i = 0; i < 3; i++) {
if ((board[0][i] == board[1][i]) && (board[1][i] == board[2][i]) && (board[1][i] != 0)) return 1;
}
// Check for diagonal matches
if ((board[0][0] == board[1][1]) && (board[1][1] == board[2][2]) && (board[1][1] != 0)) return 1;
else if ((board[0][2] == board[1][1]) && (board[1][1] == board[2][0]) && (board[1][1] != 0)) return 1;
// Check for draw
i = 0;
j = 0;
while (j < 3) {
if (board[i][j] == 0) {
return 0;
}
i++;
if (i >= 2) {
i = 0;
j++;
}
if (j == 3) return -1;
}
return 0;
}The first loop checks if each cell in a row are equal.
If they are equal, they output the integer 1.
The same goes for the second row, but it checks if each cell in a column are equal.
// Check for horizontal matches
for (i = 0; i < 3; i++) {
if ((board[i][0] == board[i][1]) && (board[i][1] == board[i][2]) && (board[i][1] != 0)) return 1;
}
// Check for vertical matches
for (i = 0; i < 3; i++) {
if ((board[0][i] == board[1][i]) && (board[1][i] == board[2][i]) && (board[1][i] != 0)) return 1;
}When using a return statement here, it automatically breaks outside the loop and proceeds to the output.
The rest of the function is not executed.
The same goes for diagonal matches.
// Check for diagonal matches
if ((board[0][0] == board[1][1]) && (board[1][1] == board[2][2]) && (board[1][1] != 0)) return 1;
else if ((board[0][2] == board[1][1]) && (board[1][1] == board[2][0]) && (board[1][1] != 0)) return 1;Finally, we want to detect if the game is a draw.
This loop should take care of it.
// Check for draw
i = 0;
j = 0;
while (j <= 2) {
if (board[i][j] == 0) {
return 0;
}
i++;
if (i == 3) {
i = 0;
j++;
}
if (j == 3) return -1;
}
return 0;Basically, what this does is it scans the board if there are cells that are yet to be filled, or cells with a value 0.
Since all winning positions have been tested, this will only execute if none of the three tests pass, and therefore, will not return a value after this point.
In summary, if it finds a 0, then a game is still ongoing.
However, if it detects that all cells are filled, it returns -1 and thus the game is a draw.
The summary of the game state system should look like this:
graph TD start[["Start: testforWins()"]] ifHorizontal{If cells in row i match} ifVertical{If cells in column i match} ifDiagonal{If cells in diagonals match} ifDraw{If all cells are filled} outContinue[/return 0/] outWin[/return 1/] outDraw[/return -1/] stop((Stop)) start --> ifHorizontal ifHorizontal --> |Yes| outWin ifHorizontal --> |No| ifVertical ifVertical --> |Yes| outWin ifVertical --> |No| ifDiagonal ifDiagonal --> |Yes| outWin ifDiagonal --> |No| ifDraw ifDraw --> |Yes| outDraw ifDraw --> |No| outContinue outDraw & outContinue & outWin --> stop
Main Game
Now that we’ve built each component, what we need to just do is to connect them together.
We first refer to our previous program flow we built in Identifying the Flow.
Then we simply build our code based on this.
flowchart TD board[[Board Set]] keyPress[[Await Key Press]] ifMove{If key pressed: WASD} ifEnter{If key pressed: Enter} cursorMove[[Move Cursor]] curUpdate[[Cursor Update]] gameUpdate[Game Update] valStore[[Store Move]] wdDetect{Detect if win/draw} wdScr[[Win/Draw Screen]] playerChange[[Player Change]] start((Start)) stop((End)) start --> board board --> keyPress keyPress --> ifMove ifMove --> |Yes|cursorMove cursorMove --> curUpdate curUpdate --> keyPress ifMove --> |No|ifEnter ifEnter --> |Yes|valStore valStore --> gameUpdate gameUpdate --> wdDetect wdDetect --> |Yes|wdScr wdScr --> stop wdDetect --> |No|playerChange playerChange --> keyPress ifEnter --> |No|keyPress
The code, now in game(), should go like this.
void game () {
int cmd = 0;
int gameState = 0;
clrscr();
clrBoard();
setBoard(); // Board Set
while (cmd == 0) {
if (currentPlayer == 1) ccout(22, "Player 1's turn!");
else if (currentPlayer == -1) ccout(22, "Player 2's turn!");
cellSelect(); // If WASD pressed
cmd = keyPress(); // Await key press
// If Enter key pressed
if (cmd == 1) {
playerInput(); // Store input and update game
gameState = testforWins(); // Test if winning/draw
// If win
if (gameState == 1) {
// Win Screen
if (currentPlayer == 1) ccout(22, "Player 1 won the game!");
else if (currentPlayer == -1) ccout(22, "Player 2 won the game!");
getch();
clrscr();
break;
} else if (gameState == -1) { // If draw
// Draw Screen
ccout(22, "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t");
ccout(22, "It's a draw!");
getch();
clrscr();
break;
}
cmd = 0;
} else if (cmd == 2) { // If Esc pressed
clrscr();
return;
}
};
}Application Screens
For our program to be a proper application, we then build the rest of the interface.
Here, we introduce the two other screens, help() and the menu screen.
void help () {
setpd(3, 3, 2, 2);
ccout(5, "Game Controls");
ccout(7, "Use WASD to pick a cell.");
ccout(8, "Use Enter to place your move.");
ccout(9, "Use E to pause the game.");
ccout(10, "Use Space to navigate the menu.");
ccout(13, "How to Play");
ccout(15, "Each players take turns in placing marks on an empty cell in the board.");
ccout(16, "The first player whose gets three of their marks in a row wins.");
ccout(17, "If neither player fills up the board without making a ");
ccout(18, "three in a row, the game ends in a draw.");
getch();
clrscr();
}The help screen simply displays how to play the game and the controls of the game.
After a key press, the help screen should go away.
void main() {
int cmd = 0, selected = 0;
clrscr();
setpd(5, 5, 5, 5);
ccout(11, "Tic Tac Toe!" );
ccout(16, "> Start ");
ccout(17, " Controls/Help ");
ccout(18, " Exit ");
while (1 == 1) {
setpd(5, 5, 5, 5);
ccout(11, "Tic Tac Toe!" );
cmd = keyPress();
if (cmd == 3) selected++;
else if (cmd == 1) {
clrscr();
switch (selected) {
case 0:
game();
break;
case 1:
help();
break;
case 2:
return;
}
}
if (selected > 2) selected = 0;
if (selected == 0) {
ccout(16, "> Start ");
ccout(17, " Controls/Help ");
ccout(18, " Exit ");
} else if (selected == 1) {
ccout(16, " Start ");
ccout(17, "> Controls/Help ");
ccout(18, " Exit ");
} else if (selected == 2) {
ccout(16, " Start ");
ccout(17, " Controls/Help ");
ccout(18, "> Exit ");
}
};
}The function main() should join each component together.
We see some of its parts that are worth explaining.
int cmd = 0, selected = 0;
clrscr();
setpd(5, 5, 5, 5);
ccout(11, "Tic Tac Toe!" );
ccout(16, "> Start ");
ccout(17, " Controls/Help ");
ccout(18, " Exit ");This at the beginning sets the screen at start up.
It displays the initial options so that it would immediately appear in the console/UI.
while (1 == 1) {
setpd(5, 5, 5, 5);
ccout(11, "Tic Tac Toe!" );
cmd = keyPress();
if (cmd == 3) selected++;
else if (cmd == 1) {
clrscr();
switch (selected) {
case 0:
game();
break;
case 1:
help();
break;
case 2:
return;
}
}
if (selected > 2) selected = 0;
if (selected == 0) {
ccout(16, "> Start ");
ccout(17, " Controls/Help ");
ccout(18, " Exit ");
} else if (selected == 1) {
ccout(16, " Start ");
ccout(17, "> Controls/Help ");
ccout(18, " Exit ");
} else if (selected == 2) {
ccout(16, " Start ");
ccout(17, " Controls/Help ");
ccout(18, "> Exit ");
}
}; This loop contains some parts which include a section that calls the functions game(), which consist of the game code, and the help() screen from earlier.
It first detects if the Space key is pressed, which then allows you to cycle through the other options.
Selecting the Enter key would then proceed to the switch statement, which would then call the other application screens.
In addition, the final option would then force the function to terminate (as it is a return statement), thus properly terminating the program.
This is also the only way that this loop would terminate as the condition in the while () loop is always true, thus it is an infinite loop.
Source Code
#include <iostream.h>
#include <conio.h>
#include <string.h>
/// padding variables
int lpadding = 0;
int rpadding = 0;
int upadding = 0;
int dpadding = 0;
/// Cell containers
int board[3][3] = {
{0, 0, 0},
{0, 0, 0},
{0, 0, 0}
};
// Selected cell coordinates
int boardx = 1;
int boardy = 1;
// Current Player
int currentPlayer = 1;
//// Design and Display
void setpd (int left, int right, int up, int down) {
lpadding = left;
rpadding = right;
upadding = up;
dpadding = down;
}
int center (char* string) {
int length = strlen(string);
return (80 - length) / 2;
}
// Outputs string with a left alignment
void lcout(int x, int y, char* message, int type) {
int xpos;
switch (type) {
case 1: // Relative to padding
xpos = x + lpadding;
break;
case 0: // Relative to left
xpos = x;
break;
}
gotoxy(xpos, y);
cout << message;
}
// Outputs string with center alignment
void ccout(int y, char* message) {
gotoxy(center(message), y);
cout << message;
}
// Outputs string with right alignment
void rcout(int x, int y, char* message, int type) {
int xpos;
int length = strlen(message);
switch (type) {
case 1: // Relative to padding
xpos = 80 - (x + length + rpadding);
break;
case 0: // Relative to right
xpos = 80 - (x + length);
break;
}
gotoxy(xpos, y);
cout << message;
}
void border (char vertChar, char horChar, char vertex) {
int i, startx, endx, starty, endy;
startx = lpadding;
endx = 80 - rpadding;
starty = upadding;
endy = 25 - dpadding;
for (i = startx; i <= endx; i++) {
gotoxy(i, starty); cout << horChar;
}
for (i = startx; i <= endx; i++) {
gotoxy(i, endy); cout << horChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(startx, i); cout << vertChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(endx, i); cout << vertChar;
}
gotoxy(startx, starty); cout << vertex;
gotoxy(startx, endy); cout << vertex;
gotoxy(endx, starty); cout << vertex;
gotoxy(endx, endy); cout << vertex;
}
void createBox (int startx, int starty, int endx, int endy, char vertChar, char horChar, char vertex) {
int i;
for (i = startx; i <= endx; i++) {
gotoxy(i, starty); cout << horChar;
}
for (i = startx; i <= endx; i++) {
gotoxy(i, endy); cout << horChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(startx, i); cout << vertChar;
}
for (i = starty; i <= endy; i++) {
gotoxy(endx, i); cout << vertChar;
}
gotoxy(startx, starty); cout << vertex;
gotoxy(startx, endy); cout << vertex;
gotoxy(endx, starty); cout << vertex;
gotoxy(endx, endy); cout << vertex;
}
void hr (int y, char lineChar, char endChar) {
int i, startx, endx;
startx = lpadding;
endx = 80 - rpadding;
gotoxy(startx, y); cout << endChar;
for (i = startx + 1; i <= endx - 1; i++) {
gotoxy(i, y); cout << lineChar;
}
gotoxy(endx, y); cout << endChar;
}
void hr (int y, char lineChar, char endChar, int startx, int endx) {
int i;
gotoxy(startx, y); cout << endChar;
for (i = startx + 1; i <= endx - 1; i++) {
gotoxy(i, y); cout << lineChar;
}
gotoxy(endx, y); cout << endChar;
}
/// vr() with default params; no start/end coords
void vr (int x, char lineChar, char endChar) {
int i, starty, endy;
starty = upadding;
endy = 25 - dpadding;
gotoxy(x, starty); cout << endChar;
for (i = starty + 1; i <= endy - 1; i++) {
gotoxy(x, i); cout << lineChar;
}
gotoxy(x, endy); cout << endChar;
}
/// vr with start and end coords
void vr (int x, char lineChar, char endChar, int starty, int endy) {
int i;
gotoxy(x, starty); cout << endChar;
for (i = starty + 1; i <= endy - 1; i++) {
gotoxy(x, i); cout << lineChar;
}
gotoxy(x, endy); cout << endChar;
}
//// Game Functions
// Resets the board border state/Removes any cell select indicators
void setBoard () {
setpd(25, 25, 6, 7);
border('|', '-', char(254));
hr(10, '-', char(254));
hr(14, '-', char(254));
vr(35, '|', char(254));
vr(45, '|', char(254));
createBox(35, 10, 45, 14, '|','-', char(254));
}
// Detects key press
// It assigns a code depending on the key press
// For W, A, S, D keys, internal code is 0 and is used for movement
// For Enter key, internal code is 1 and is used for assigning/storing player moves
// For Esc key, internal code is 2 and is used for force quit
// It also handles overflow and selected cell variable updates for cmd 0.
int keyPress() {
int cmd = 0;
int keyPressed = getch();
// Updates current selected cell value
if (keyPressed == 119) boardy--;
else if (keyPressed == 115) boardy++;
else if (keyPressed == 97) boardx--;
else if (keyPressed == 100) boardx++;
else if (keyPressed == 13) cmd = 1;
else if (keyPressed == 27) cmd = 2;
else if (keyPressed == 32) cmd = 3;
// Handles overflow for boardx
if (boardx > 2) boardx = 0;
else if (boardx < 0) boardx = 2;
// Handles overflow for boardy
if (boardy > 2) boardy = 0;
else if (boardy < 0) boardy = 2;
return cmd;
}
// Updates the board to indicate the current selected cell
void cellSelect() {
int startx, starty, endx, endy;
// Initializes coordinates in the board
startx = 25; starty = 6;
endx = 35; endy = 10;
setBoard();
// Modifiers to coords based on current cell selected
switch (boardx){
case 1:
startx += 10;
endx += 10;
break;
case 2:
startx += 20;
endx += 20;
break;
default:
startx += 0;
endx += 0;
break;
}
switch (boardy){
case 1:
starty += 4;
endy += 4;
break;
case 2:
starty+= 8;
endy += 8;
break;
default:
starty += 0;
endy += 0;
break;
}
createBox(startx, starty, endx, endy, char(186), char(205), char(254));
}
int testforWins () {
int i = 0, j = 0;
// Check for horizontal matches
for (i = 0; i < 3; i++) {
if ((board[i][0] == board[i][1]) && (board[i][1] == board[i][2]) && (board[i][1] != 0)) return 1;
}
// Check for vertical matches
for (i = 0; i < 3; i++) {
if ((board[0][i] == board[1][i]) && (board[1][i] == board[2][i]) && (board[1][i] != 0)) return 1;
}
// Check for diagonal matches
if ((board[0][0] == board[1][1]) && (board[1][1] == board[2][2]) && (board[1][1] != 0)) return 1;
else if ((board[0][2] == board[1][1]) && (board[1][1] == board[2][0]) && (board[1][1] != 0)) return 1;
// Check for draw
i = 0;
j = 0;
while (j <= 2) {
if (board[i][j] == 0) {
return 0;
}
i++;
if (i == 3) {
i = 0;
j++;
}
if (j == 3) return -1;
}
return 0;
}
void playerInput () {
if (board[boardx][boardy] == 0) {
board[boardx][boardy] = currentPlayer;
if (currentPlayer == 1) {
currentPlayer = -1;
char message1[6] = {' ', char(220), char(220), char(220), ' '};
char message2[6] = {char(219), char(219), char(219), char(219), char(219)};
char message3[6] = {' ', char(220), char(219), char(220), ' '};
int startx = 28;
int starty = 7;
switch (boardx){
case 1:
startx += 10;
break;
case 2:
startx += 20;
break;
default:
startx += 0;
break;
}
switch (boardy){
case 1:
starty += 4;
break;
case 2:
starty+= 8;
break;
default:
starty += 0;
break;
}
gotoxy(startx, starty); cout << message1;
gotoxy(startx, starty + 1); cout << message2;
gotoxy(startx, starty + 2); cout << message3;
} else if (currentPlayer == -1) {
currentPlayer = 1;
int startx, starty;
char message1[6] = {char(201), char(205), char(205), char(205), char(187)};
char message2[6] = {char(186), ' ', ' ', ' ', char(186)};
char message3[6] = {char(200), char(205), char(205), char(205), char(188)};
startx = 28;
starty = 7;
switch (boardx){
case 1:
startx += 10;
break;
case 2:
startx += 20;
break;
default:
startx += 0;
break;
}
switch (boardy){
case 1:
starty += 4;
break;
case 2:
starty+= 8;
break;
default:
starty += 0;
break;
}
gotoxy(startx, starty); cout << message1;
gotoxy(startx, starty + 1); cout << message2;
gotoxy(startx, starty + 2); cout << message3;
}
}
}
void clrBoard() {
int i = 0, j = 0;
currentPlayer = 1;
while (j < 3) {
board[i][j] = 0;
if (i == 2) {
i = -1;
j++;
}
i++;
};
}
//// Game Screens
void help () {
setpd(3, 3, 2, 2);
ccout(5, "Game Controls");
ccout(7, "Use WASD to pick a cell.");
ccout(8, "Use Enter to place your move.");
ccout(9, "Use E to pause the game.");
ccout(10, "Use Space to navigate the menu.");
ccout(13, "How to Play");
ccout(15, "Each players take turns in placing marks on an empty cell in the board.");
ccout(16, "The first player whose gets three of their marks in a row wins.");
ccout(17, "If neither player fills up the board without making a ");
ccout(18, "three in a row, the game ends in a draw.");
getch();
clrscr();
}
void game () {
int cmd = 0;
int gameState = 0;
clrscr();
clrBoard();
setBoard();
while (cmd == 0) {
if (currentPlayer == 1) ccout(22, "Player 1's turn!");
else if (currentPlayer == -1) ccout(22, "Player 2's turn!");
cellSelect();
cmd = keyPress();
if (cmd == 1) {
playerInput();
gameState = testforWins();
if (gameState == 1) {
if (currentPlayer == 1) ccout(22, "Player 1 won the game!");
else if (currentPlayer == -1) ccout(22, "Player 2 won the game!");
getch();
clrscr();
break;
} else if (gameState == -1) {
ccout(22, "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t");
ccout(22, "It's a draw!");
getch();
clrscr();
break;
}
cmd = 0;
} else if (cmd == 2) {
clrscr();
return;
}
};
}
void main() {
int cmd = 0, selected = 0;
clrscr();
setpd(5, 5, 5, 5);
ccout(11, "Tic Tac Toe!" );
ccout(16, "> Start ");
ccout(17, " Controls/Help ");
ccout(18, " Exit ");
while (1 == 1) {
setpd(5, 5, 5, 5);
ccout(11, "Tic Tac Toe!" );
cmd = keyPress();
if (cmd == 3) selected++;
else if (cmd == 1) {
clrscr();
switch (selected) {
case 0:
game();
break;
case 1:
help();
break;
case 2:
return;
}
}
if (selected > 2) selected = 0;
if (selected == 0) {
ccout(16, "> Start ");
ccout(17, " Controls/Help ");
ccout(18, " Exit ");
} else if (selected == 1) {
ccout(16, " Start ");
ccout(17, "> Controls/Help ");
ccout(18, " Exit ");
} else if (selected == 2) {
ccout(16, " Start ");
ccout(17, " Controls/Help ");
ccout(18, "> Exit ");
}
};
}