For coding since 10 days, this program is really impressive. If I had to learn something new and complicated as C++, I'd probably make many more mistakes.
One thing that is always tricky is input and output. Especially input since you have to deal with funny users who enter 1,4
for the coordinates since they don't know that you expect 1 4
.
I tried this by accident, and your program responded with:
Please enter the row and column
1,4
_|_|_|_|
a|_|_|_|
_|_|_|_|
_|_|_|_|
Your turn,Roland
Please enter the row and column
Position is taken
Please enter the row and column
Position is taken
Please enter the row and column
Position is taken
Please enter the row and column
Position is taken
...
So there is obviously something wrong. I solved this using the following code, which takes care of many possible edge cases:
void play(Board &board, Player &player) {
while (true) {
std::cout << "Please enter the row and column (1..4): ";
// If a simple string cannot be read, something is seriously
// broken. Stop the whole program.
std::string line;
if (!std::getline(std::cin, line)) {
std::exit(std::cin.eof() ? 0 : 1);
}
// Let the user type the coordinates either as "1 4"
// or as "1,4" or "1, 4" or any other variant.
std::replace(line.begin(), line.end(), ',', ' ');
int row, column;
if (!(std::stringstream(line) >> row >> column)) {
std::cout << "Please enter two numbers\n";
continue;
}
// Make sure that the coordinates are correct.
// Otherwise the program may crash or do something entirely different.
// This is called "undefined behavior" and it should frighten you.
row--;
column--;
if (!(0 <= row && row < 4 && 0 <= column && column < 4)) {
std::cout << "Please enter only numbers in the range 1..4\n";
continue;
}
if (board.at(row, column) != '_') {
std::cout << "Position is already taken\n";
continue;
}
board.playAt(row, column, player);
return;
}
}
As you can see, I also did some other changes to the code. But the most important idea here is to read the input line-by-line and then extract the coordinates from the line. In your current code, when I press Enter for many times before entering any number, the program will not give me any feedback.
A similar case is when entering the player data. I rewrote your code to:
Player input(const std::string &defaultName, char defaultSymbol) {
std::string line;
Player player;
std::cout << defaultName << ", please enter your name: ";
if (std::getline(std::cin, line) && !line.empty()) {
player.name = line;
} else {
player.name = defaultName;
}
std::cout << player.name << ", please enter a character to use: ";
if (std::getline(std::cin, line) && !line.empty()) {
player.symbol = line[0];
} else {
player.symbol = defaultSymbol;
}
return player;
}
Here, I also changed from using the >>
operator to using std::getline
since that makes the program's behavior much more predictable. The operator cin >> choice
in your code doesn't consume the Enter key for example. Therefore, when I changed the program to ask for name1, choice1, name2, choice2
, the name2 was always entered automatically and was an empty string. This cannot happen when you consistently read all input as lines.
I also provided useful default values, so that the user can just press Enter four times in a row. The above function is called like this:
int main() {
Player p1 = input("Player 1", 'x');
Player p2 = input("Player 2", 'o');
Board board;
...
}
This looks really short and brief, just as the program in main
should.
For all the above changes, you obviously have to know that these functions like std::getline
or std::replace
exist and that you need to include another header with #include <algorithm>
at the top of your program. This cannot be expected from a beginner, therefore it's always good to ask, as you did here.
For reference, here is the complete program that I wrote based on your really good code.
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <sstream>
class Player {
public:
std::string name;
char symbol{};
};
class Board {
static constexpr int boardSize = 4;
static constexpr int winLength = 4;
char board[boardSize][boardSize]{};
public:
Board() {
for (auto &row : board) {
for (char &cell : row) {
cell = '_';
}
}
}
bool isWin(const Player &player) const {
auto symbol = player.symbol;
return checkHorizontal(symbol)
|| checkVertical(symbol)
|| checkPrincipalDiagonal(symbol)
|| checkOtherDiagonal(symbol);
}
bool isDraw() const {
for (auto &row : board) {
for (char cell : row) {
if (cell == '_') {
return false;
}
}
}
return true;
}
int size() const { return boardSize; }
char at(int row, int col) const { return board[row][col]; }
void playAt(int row, int col, const Player &player) {
board[row][col] = player.symbol;
}
private:
bool checkHorizontal(char symbol) const {
for (auto &row : board) {
int count = 0;
for (char cell : row) {
if (cell == symbol) {
count++;
}
}
if (count == winLength) {
return true;
}
}
return false;
}
bool checkVertical(char symbol) const {
for (int i = 0; i < boardSize; i++) {
int count = 0;
for (auto &row : board) {
if (row[i] == symbol) {
count++;
if (count == winLength) {
return true;
}
} else {
count = 0;
}
}
}
return false;
}
bool checkPrincipalDiagonal(char symbol) const {
int count = 0;
for (int i = 0; i < boardSize; i++) {
if (board[i][i] == symbol) {
count++;
if (count == winLength) {
return true;
}
} else {
count = 0;
}
}
return false;
}
bool checkOtherDiagonal(char symbol) const {
int count = 0;
for (int i = 0; i < boardSize; i++) {
if (board[i][boardSize - 1 - i] == symbol) {
count++;
if (count == winLength) {
return true;
}
} else {
count = 0;
}
}
return false;
}
};
void print(const Board &board) {
for (int row = 0; row < board.size(); ++row) {
for (int col = 0; col < board.size(); ++col) {
std::cout << '|' << board.at(row, col);
}
std::cout << "|\n";
}
}
void play(Board &board, Player &player) {
while (true) {
std::cout << "Please enter the row and column (1.." << board.size() << "): ";
std::string line;
if (!std::getline(std::cin, line)) {
std::exit(std::cin.eof() ? 0 : 1);
}
std::replace(line.begin(), line.end(), ',', ' ');
int row, column;
if (!(std::stringstream(line) >> row >> column)) {
std::cout << "Please enter two numbers\n";
continue;
}
row--;
column--;
if (!(0 <= row && row < board.size() && 0 <= column && column < board.size())) {
std::cout << "Please enter only numbers in the range 1.." << board.size() << "\n";
continue;
}
if (board.at(row, column) != '_') {
std::cout << "Position is already taken\n";
continue;
}
board.playAt(row, column, player);
return;
}
}
Player input(const std::string &defaultName, char defaultSymbol) {
std::string line;
Player player;
std::cout << defaultName << ", please enter your name: ";
player.name = std::getline(std::cin, line) && !line.empty() ? line : defaultName;
std::cout << player.name << ", please enter a character to use: ";
player.symbol = std::getline(std::cin, line) && !line.empty() ? line[0] : defaultSymbol;
return player;
}
int main() {
Player player1 = input("Player 1", 'x');
Player player2 = input("Player 2", 'o');
Board board;
print(board);
Player *turn = &player1;
while (true) {
std::cout << "Your turn, " << turn->name << ".\n";
play(board, *turn);
print(board);
if (board.isWin(*turn)) {
std::cout << "Congrats, " << turn->name << ", you have won.\n";
break;
}
if (board.isDraw()) {
std::cout << "The match is a draw!\n";
break;
}
turn = turn == &player1 ? &player2 : &player1;
}
}
Feel free to change the two constants boardSize
and winLength
. A board size of 19 and a win length of 5 make an interesting game. Since these two meanings of 4
were not easily distinguishable in your code, it is good style to give such numbers meaningful names.