I've been asked to attach a C++ code sample to my application for an entry-level C++ programmer position in a videogame company. I've created a simple, yet complete console application to solve Sudoku problems. How can I improve the code that I have?
Please do comment on the algorithm, the code practices, presentation, or anything else. It's one of my first attempts to also set up unit tests with my code.
Also, I am interested in whether you think this would work well as a code sample in particular – i.e. does it show what people who look at the code samples want to see?
#include <set>
#include <string>
#include <fstream>
#include <iostream>
#include <cassert>
// ======================================================================================
enum SolutionResult {
// The position has been solved OK.
SR_SOLVED,
// The current position in invalid.
// For that reason, it is useless to even attempt to solve it.
SR_INVALID,
// The current position is valid in its current state, but there
// exists no complete solution that would uphold the sudoku rules.
SR_UNSOLVABLE
};
// ======================================================================================
class Sudoku {
protected:
// 0 = Empty cell
short values[9][9];
// Returns whether all the rows are currently valid,
// i.e. whether no rows contain any duplicate values.
bool checkValidRows() {
for (short row = 0; row < 9; row++) {
std::set<short> valuesInRow;
for (short col = 0; col < 9; col++) {
short val = this->values[row][col];
if (!val) continue;
if (valuesInRow.find(val) != valuesInRow.end()) {
return false;
}
valuesInRow.insert(val);
}
}
return true;
}
// Returns whether all the columns are currently valid,
// i.e. whether no columns contain any duplicate values.
bool checkValidCols() {
for (short col = 0; col < 9; col++) {
std::set<short> valuesInCol;
for (short row = 0; row < 9; row++) {
short val = this->values[row][col];
if (!val) continue;
if (valuesInCol.find(val) != valuesInCol.end()) {
return false;
}
valuesInCol.insert(val);
}
}
return true;
}
// Returns whether all the partial squares are currently valid,
// i.e. whether no 3x3 square contains a duplicate value.
bool checkValidSquares() {
for (short squareX = 0; squareX < 3; squareX++) {
for (short squareY = 0; squareY < 3; squareY++) {
std::set<short> valuesInSquare;
for (short m = 0; m < 3; m++) {
for (short n = 0; n < 3; n++) {
short val = this->values[squareX*3+m][squareY*3+n];
if (val == 0) continue;
if (valuesInSquare.find(val) != valuesInSquare.end()) {
return false;
}
valuesInSquare.insert(val);
}
}
}
}
return true;
}
// Returns whether the current position is valid, i.e., whether its rows, columns,
// and squares are all valid and do not contain a duplicate value.
bool checkValid() {
return this->checkValidRows() && this->checkValidCols() && this->checkValidSquares();
}
public:
Sudoku() {
// Start with a blank position.
for (short row = 0; row < 9; row++) {
for (short col = 0; col < 9; col++) {
this->values[row][col] = 0;
}
}
}
Sudoku (const short values[9][9]) {
for (short row = 0; row < 9; row++) {
for (short col = 0; col < 9; col++) {
// Values that are not between [0 .. 9]
// are discarded and replaced with zeroes.
short val = values[row][col];
if (val >= 0 && val <= 9) {
this->values[row][col] = val;
}
else {
this->values[row][col] = 0;
}
}
}
}
// This is the solver method.
// The way to solution is a standard backtrack exercise.
//
// For the next empty space, we put the lowest possible value in, and recursively call the solver
// to see whether it can complete all the other spaces in a valid manner – in which case, we are done.
// In case that no possible value in the chosen space leads to a valid and complete solution, we clear
// the space entirely and declare the position as unsolvable. In recursion terms, this means that
// we need to attempt to put another value to the previous space, and so on.
SolutionResult solve() {
// Check that the situation is valid.
if (!this->checkValid()) return SR_INVALID;
// For the next empty space:
for (short row = 0; row < 9; row++) {
for (short col = 0; col < 9; col++) {
if (this->values[row][col] > 0) continue;
// Put an arbitrary value into the space:
for (short val = 1; val <= 9; val++) {
this->values[row][col] = val;
// Recursively call the solver and see whether it can complete all
// the other spaces in the same manner. In case that it can, we're done.
if (this->solve() == SR_SOLVED) {
return SR_SOLVED;
}
}
// We tried all the possible values, and none has lead to a complete and
// valid solution. Clear the space, and return that the position is unsolvable.
this->values[row][col] = 0;
return SR_UNSOLVABLE;
}
}
// There are no more empty spaces.
// We're done.
return SR_SOLVED;
}
// Formatted output.
friend std::ostream& operator<< (std::ostream &os, const Sudoku& s) {
for (short row = 0; row < 9; row++) {
os << std::endl;
for (short col = 0; col < 9; col++) {
os << ' ';
if (s.values[row][col] > 0) {
os << s.values[row][col];
} else {
os << '.';
}
if (col == 2 || col == 5) {
os << " |";
}
}
if (row == 2 || row == 5) {
os << std::endl << " ------+-------+------";
}
}
os << std::endl;
return os;
}
//
static void runUnitTests();
};
// ======================================================================================
int main (int argc, char* argv[])
{
short pos[9][9] = {
{0, 0, 4, 0, 0, 0, 0, 0, 0},
{6, 0, 2, 1, 0, 0, 0, 0, 0},
{1, 0, 8, 3, 4, 2, 5, 6, 7},
{0, 0, 9, 7, 0, 0, 4, 2, 3},
{4, 0, 6, 8, 0, 0, 0, 0, 1},
{7, 1, 3, 9, 2, 0, 8, 0, 0},
{0, 0, 0, 5, 3, 0, 2, 8, 4},
{0, 0, 0, 4, 1, 0, 6, 0, 0},
{3, 0, 0, 0, 0, 6, 1, 0, 0}
};
// Let's test the entire unit.
Sudoku::runUnitTests();
// Let's solve the particular position above:
Sudoku s (pos);
std::cout << s << std::endl;
std::cout << "Solver starts, please wait..." << std::endl;
switch (s.solve()) {
case SR_SOLVED:
std::cout << "Solution: " << std:: endl;
std::cout << s << std::endl;
break;
case SR_INVALID:
std::cout << "The position is not valid." << std:: endl;
break;
case SR_UNSOLVABLE:
std::cout << "The position cannot be solved." << std:: endl;
break;
}
std::cout << "Press any key to exit...";
getchar();
return 0;
}
// ======================================================================================
void Sudoku::runUnitTests() {
short dataZeroes[9][9] = {
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0}
};
short dataValidPos1[9][9] = {
{5, 3, 4, 6, 7, 8, 9, 1, 2},
{6, 7, 2, 1, 9, 5, 3, 4, 8},
{1, 9, 8, 3, 4, 2, 5, 6, 7},
{8, 5, 9, 7, 6, 1, 4, 2, 3},
{4, 2, 6, 8, 5, 3, 7, 9, 1},
{7, 1, 3, 9, 2, 4, 8, 5, 6},
{9, 6, 1, 5, 3, 7, 2, 8, 4},
{2, 8, 7, 4, 1, 9, 6, 3, 5},
{3, 4, 5, 2, 8, 6, 1, 7, 9}
};
short dataValidPos2[9][9] = {
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{6, 0, 2, 1, 0, 0, 0, 0, 0},
{1, 0, 8, 3, 4, 2, 5, 6, 7},
{0, 0, 9, 7, 0, 0, 4, 2, 3},
{4, 0, 6, 8, 0, 0, 0, 0, 1},
{7, 1, 3, 9, 2, 0, 8, 0, 0},
{0, 0, 0, 5, 3, 0, 2, 8, 4},
{0, 0, 0, 4, 1, 0, 6, 0, 0},
{3, 0, 0, 0, 0, 6, 1, 0, 0}
};
short dataInvalidRows[9][9] = {
{5, 3, 4, 6, 7, 8, 9, 1, 5},
{6, 7, 2, 1, 9, 5, 3, 4, 8},
{1, 9, 8, 3, 4, 2, 0, 6, 7},
{8, 5, 9, 7, 6, 1, 4, 2, 3},
{4, 2, 6, 8, 5, 3, 7, 9, 1},
{7, 1, 3, 9, 2, 4, 8, 5, 6},
{9, 6, 1, 5, 3, 7, 2, 8, 4},
{2, 8, 7, 4, 1, 9, 6, 3, 0},
{3, 4, 5, 2, 8, 6, 1, 7, 9}
};
short dataInvalidCols[9][9] = {
{5, 3, 4, 6, 7, 8, 9, 1, 2},
{6, 7, 2, 1, 9, 0, 3, 4, 8},
{1, 9, 8, 3, 4, 2, 5, 6, 7},
{8, 5, 9, 7, 6, 1, 4, 2, 3},
{4, 2, 6, 8, 5, 3, 7, 9, 1},
{7, 1, 3, 9, 2, 4, 8, 5, 6},
{9, 6, 1, 5, 3, 7, 2, 8, 4},
{2, 8, 7, 4, 1, 9, 6, 3, 5},
{5, 4, 0, 2, 8, 6, 1, 7, 9}
};
short dataInvalidSquares[9][9] = {
{5, 3, 4, 6, 7, 8, 9, 1, 2},
{6, 5, 2, 1, 9, 0, 3, 4, 8},
{1, 9, 8, 3, 4, 2, 5, 6, 7},
{8, 0, 9, 7, 6, 1, 4, 2, 3},
{4, 2, 6, 8, 5, 3, 7, 9, 1},
{7, 1, 3, 9, 2, 4, 8, 5, 6},
{9, 6, 1, 5, 3, 7, 2, 8, 4},
{2, 8, 7, 4, 1, 9, 6, 3, 5},
{3, 4, 5, 2, 8, 6, 1, 7, 9}
};
Sudoku zeroes (dataZeroes);
Sudoku validPos1 (dataValidPos1);
Sudoku validPos2 (dataValidPos2);
Sudoku invalidRows (dataInvalidRows);
Sudoku invalidCols (dataInvalidCols);
Sudoku invalidSquares (dataInvalidSquares);
std::cout << "Unit tests start..." << std::endl;
assert(zeroes.checkValidRows());
assert(zeroes.checkValidCols());
assert(zeroes.checkValidSquares());
assert(zeroes.checkValid());
assert(zeroes.solve() == SR_SOLVED);
assert(validPos1.checkValidRows());
assert(validPos1.checkValidCols());
assert(validPos1.checkValidSquares());
assert(validPos1.checkValid());
assert(validPos1.solve() == SR_SOLVED);
assert(validPos2.checkValidRows());
assert(validPos2.checkValidCols());
assert(validPos2.checkValidSquares());
assert(validPos2.checkValid());
assert(validPos2.solve() == SR_SOLVED);
assert(validPos2.values[0][0] == 5);
assert(validPos2.values[0][1] == 3);
assert(validPos2.values[0][2] == 4);
assert(validPos2.values[0][3] == 6);
assert(validPos2.values[0][4] == 7);
assert(validPos2.values[0][5] == 8);
assert(validPos2.values[0][6] == 9);
assert(validPos2.values[0][7] == 1);
assert(validPos2.values[0][8] == 2);
assert(!invalidRows.checkValidRows());
assert( invalidRows.checkValidCols());
assert( invalidRows.checkValidSquares());
assert(!invalidRows.checkValid());
assert( invalidRows.solve() == SR_INVALID);
assert( invalidCols.checkValidRows());
assert(!invalidCols.checkValidCols());
assert( invalidCols.checkValidSquares());
assert(!invalidCols.checkValid());
assert( invalidCols.solve() == SR_INVALID);
assert( invalidSquares.checkValidRows());
assert( invalidSquares.checkValidCols());
assert(!invalidSquares.checkValidSquares());
assert(!invalidSquares.checkValid());
assert( invalidSquares.solve() == SR_INVALID);
std::cout << "All unit tests OK." << std::endl;
}