From c0c64885475bf574b0d0c33246da498b0d406e66 Mon Sep 17 00:00:00 2001 From: EndilWayfare Date: Tue, 14 Jun 2016 14:54:56 -0700 Subject: [PATCH] Class Project status The actual submission source files for my C Programming class final project --- sudoku.c | 120 +++++++ sudoku_assistant.c | 432 +++++++++++++++++++++++ sudoku_assistant.h | 25 ++ sudoku_board.c | 145 ++++++++ sudoku_board.h | 29 ++ sudoku_commands.c | 814 +++++++++++++++++++++++++++++++++++++++++++ sudoku_commands.h | 60 ++++ sudoku_help.h | 76 ++++ sudoku_test_digits.c | 245 +++++++++++++ sudoku_test_digits.h | 52 +++ sudoku_undo.c | 325 +++++++++++++++++ sudoku_undo.h | 40 +++ sudoku_utility.c | 420 ++++++++++++++++++++++ sudoku_utility.h | 107 ++++++ 14 files changed, 2890 insertions(+) create mode 100644 sudoku.c create mode 100644 sudoku_assistant.c create mode 100644 sudoku_assistant.h create mode 100644 sudoku_board.c create mode 100644 sudoku_board.h create mode 100644 sudoku_commands.c create mode 100644 sudoku_commands.h create mode 100644 sudoku_help.h create mode 100644 sudoku_test_digits.c create mode 100644 sudoku_test_digits.h create mode 100644 sudoku_undo.c create mode 100644 sudoku_undo.h create mode 100644 sudoku_utility.c create mode 100644 sudoku_utility.h diff --git a/sudoku.c b/sudoku.c new file mode 100644 index 0000000..8c298f1 --- /dev/null +++ b/sudoku.c @@ -0,0 +1,120 @@ +/****************************************************************************** + * Program: sudoku.c + * + * Purpose: To implement a sudoku application, with assistant functions to help + * user solve sudoku puzzles + * + * Developer: Philip Ormand + * + * Date: 5/13/16 + * + ****************************************************************************/ +#include +#include +#include + +#include "sudoku_board.h" +#include "sudoku_commands.h" +#include "sudoku_test_digits.h" +#include "sudoku_utility.h" + +/* + * ======= SUDOKU SEMANTICS ======= (AKA, my chosen definitions for terms in variable names, etc.) + * 'board': the sudoku puzzle space. The actual matrix that makes up a given puzzle or solution. + * 'square': individual space for a digit on a sudoku board. A sudoku board is 9 squares by 9 squares. + * 'row' / 'column': self-explanitory. A horizontal or vertical row of squares on the sudoku board. + * 'block' : the 9 3x3 blocks of squares that must each contain the digits 1-9 on the sudoku board. + * + * Which block is which? + * +---+---+---+ + * | 1 | 2 | 3 | + * +---+---+---+ + * | 4 | 5 | 6 | + * +---+---+---+ + * | 7 | 8 | 9 | + * +---+---+---+ + */ + +int main(int argc, char* argv[]) +{ + bool running = true; + struct SudokuBoard board = { 0 }; + struct SudokuCommandInput commandInput = { 0 }; + const struct SudokuCommand *command = NULL; + SudokuCommandResult status = SUDOKU_COMMAND_SUCCESS; + + initializeSudokuBoard(&board); + initializeString(&commandInput.string); + + // if one argument was provided, go ahead and load sudoku board from textfile + if (argc > 1) + { + // if filename provided by argument cannot be found/read, loadSudokuBoard will + // print an error statement, and execution will continue with a blank board + loadSudokuBoard(argv[1], &board); + } + + puts("========== Sudoku Game =========="); + puts(showAllCommandsPrompt); + putchar('\n'); + printSudokuBoard(&board); + + while (running) + { + // put extra space between previous output and command prompt + putchar('\n'); + + command = getCommand(&commandInput); + + status = command->commandFunction(&board, &commandInput); + + if (status == SUDOKU_COMMAND_USAGE) + { + printf("Usage: '%s'\n", command->usagePrompt); + } + else if (status == SUDOKU_COMMAND_EXIT) + { + running = false; + } + } + + freeHistory(&board.history); + + return 0; +} + + +/* +int main(int argc, char **argv) +{ + char *fileName = "sudoku_text_file.txt"; + FILE *sudokuTextFile; + struct SudokuBoard board; + + if (argc == 2) + { + // if an argument was provided + fileName = argv[1]; + } + + initializeSudokuBoard(&board); + + if ( (sudokuTextFile = fopen(fileName, "r")) == NULL ) + { + terminate("ERROR: couldn't open sudoku file"); + } + + if ( !readSudokuBoard(sudokuTextFile, &board) ) + { + exit(EXIT_FAILURE); // error message printed by readSudokuBoard + } + + printSudokuBoard(&board); + putchar('\n'); + + puts("Evaluating solution:"); + evaluateDigitsPresent(&board); + + exit(EXIT_SUCCESS); +} +*/ \ No newline at end of file diff --git a/sudoku_assistant.c b/sudoku_assistant.c new file mode 100644 index 0000000..1d996ef --- /dev/null +++ b/sudoku_assistant.c @@ -0,0 +1,432 @@ +/****************************************************************************** + * Program: sudoku_assistant.c + * + * Purpose: Defines the assistant functions that help solve sudoku puzzles + * + * Developer: Philip Ormand + * + * Date: 5/13/16 + * + *****************************************************************************/ +#include +#include + +#include "sudoku_assistant.h" +#include "sudoku_board.h" +#include "sudoku_test_digits.h" + + +HistoryStep assistantCrosshatch(SudokuBoard *board, bool verbose); +HistoryStep assistantLocked(SudokuBoard *board, bool verbose); +char scanForSingleCandidate(SudokuDigitTestField digitsPossible[][SUDOKU_COL_COUNT], Coord2D *suggestedSquare); + + +const SudokuAssistant assistants[] = { + {"crosshatch", "Uses cross - hatch scanning to identify 'hidden singles'", assistantCrosshatch}, + {"locked", "Uses row/column range checking to identify 'locked' candidates", assistantLocked} +}; + +#define SUDOKU_ASSISTANT_COUNT sizeof(assistants)/sizeof(*assistants) + +const char *sudokuAssistantNoSuggestionMessage = "Sorry, no recommendations found using this assistant\n"; + +HistoryStep assistantCrosshatch(SudokuBoard * board, bool verbose) +{ + HistoryStep suggestion = { 0 }; + DigitsPresent digitsPresent; + SudokuDigitTestField testFlag; + int row, column, block, digit; + + // make sure we know which digits are present in each row, column, and block + evaluateDigitsPresent(board, &digitsPresent, false); + + // NOTE: 'suggestion.newValue' is used to represent whether or not the assistant has found a + // value to suggest. If 0 (i.e. false), the loops should continue running. Otherwise, + // suggestion has been found and we can break out. + + for (row = 0; !suggestion.newValue && row < SUDOKU_ROW_COUNT; ++row) + { + // check if each digit is in the row in question + for (digit = 1; !suggestion.newValue && digit <= SUDOKU_DIGIT_MAX; ++digit) + { + testFlag = SUDOKU_TEST_FLAG_SHIFT(digit); + + // if row contains that digit... + if (digitsPresent.rows[row] & testFlag) + { + // ...check which columns also have it + for (column = 0; !suggestion.newValue && column < SUDOKU_COL_COUNT; ++column) + { + if (digitsPresent.columns[column] & testFlag) + { + // determine which block the intersection occurs in + block = SUDOKU_BLOCK_FROM_INTERSECTION(row, column); + + // if block doesn't already contain that digit... + if (!(digitsPresent.blocks[block] & testFlag)) + { + // array of Coord2D structs to store blank squares in block + // only 4 possible squares per block, due to row/col intersection + Coord2D candidates[4] = { 0 }; + + int blockRow, blockColumn, blockRowOffset, blockColumnOffset, + candidateCount = 0; + + // iterate through block squares + blockRowOffset = (block / 3) * 3; + + for (blockRow = blockRowOffset; + blockRow < SUDOKU_BLOCK_HEIGHT + blockRowOffset; ++blockRow) + { + blockColumnOffset = (block % 3) * 3; + + for (blockColumn = blockColumnOffset; + blockColumn < SUDOKU_BLOCK_WIDTH + blockColumnOffset; ++blockColumn) + { + if ( // if square's value is 0, + board->contents[blockRow][blockColumn] == 0 && + // AND if square is not inside outer row/column, + blockRow != row && blockColumn != column && + // AND square's row doesn't already contain digit, + !(digitsPresent.rows[blockRow] & testFlag) && + // AND square's column doesnt already contain digit... + !(digitsPresent.columns[blockColumn] & testFlag)) + { + // ...then square is valid candidate + candidates[candidateCount].row = blockRow; + candidates[candidateCount].col = blockColumn; + ++candidateCount; + } + } + } + + // if there was only one possible square + if (candidateCount == 1) + { + // populate the struct for returning values + suggestion.location = *candidates; + suggestion.newValue = digit; + + // recommend that the user fill that square with the digit + if (verbose) + { + printf("Try changing square %c%d to %d\n", + colLabels[candidates->col], + candidates->row + 1, digit); + } + } + } + } + } + } + } + } + + // when all cross-hatches have been processed, notify the user if we didn't find any suggestions + if (!suggestion.newValue && verbose) + { + printf(sudokuAssistantNoSuggestionMessage); + } + + return suggestion; +} + +HistoryStep assistantLocked(SudokuBoard * board, bool verbose) +{ + HistoryStep suggestion = { 0 }; + DigitsPresent digitsPresent; + SudokuDigitTestField digitsPossible[SUDOKU_ROW_COUNT][SUDOKU_COL_COUNT] = { 0 }; + SudokuDigitTestField testFlag; + int row, column, block, digit; + + // make sure we know which digits are present in each row, column, and block + evaluateDigitsPresent(board, &digitsPresent, false); + + // make initial analysis of possible digits for each square + for (row = 0; row < SUDOKU_ROW_COUNT; ++row) + { + for (column = 0; column < SUDOKU_COL_COUNT; ++column) + { + // only process squares that have no digit already + if (board->contents[row][column] == 0) + { + // determine which block we're in currently + block = SUDOKU_BLOCK_FROM_INTERSECTION(row, column); + + // initialize square with all digits possible + digitsPossible[row][column] = SUDOKU_TEST_ALLDIGITS; + + // unset any digits PRESENT in the square's row + digitsPossible[row][column] &= ~digitsPresent.rows[row]; + + // unset any digits PRESENT in the square's column + digitsPossible[row][column] &= ~digitsPresent.columns[column]; + + // unset any digits PRESENT in the square's block + digitsPossible[row][column] &= ~digitsPresent.blocks[block]; + } + } + } + + // SHORTCUT: If any squares now have only 1 possible candidate, suggest that candidate + suggestion.newValue = scanForSingleCandidate(digitsPossible, &suggestion.location); + + // if suggestion value is still 0, keep looking + if (!suggestion.newValue) + { + // use "locked candidate rule" to eliminate candidates + + // There are two forms of the locked candidate rule, but they are logically equivalent; + // checking with form #1 is the same as checking with form #2. + + // This program implements form #1: + // "When a candidate is possible in a certain block and row/column, and it is not possible + // anywhere else in the same row/column, then it is also not possible anywhere else in the + // same block." + // (source: https://www.stolaf.edu/people/hansonr/sudoku/explain.htm). + + // "lock" candidates by block, processing rows + for (row = 0; row < SUDOKU_ROW_COUNT; ++row) + { + for (digit = 1; digit <= SUDOKU_DIGIT_MAX; ++digit) + { + bool digitPossibleInBlock[3] = { 0 }; + int i, possibleBlockCount = 0; + testFlag = SUDOKU_TEST_FLAG_SHIFT(digit); + + for (column = 0; column < SUDOKU_COL_COUNT; ++column) + { + block = SUDOKU_BLOCK_FROM_INTERSECTION(row, column); + + // if digit is possible in this square of current row... + if (digitsPossible[row][column] & testFlag) + { + // ...record which block-column it was possible in + digitPossibleInBlock[block % 3] = true; + } + } + + // once all columns in current row have been broken down into their blocks, + // check if current digit is possible ONLY within one block + for (i = 0; i < 3; ++i) + { + if (digitPossibleInBlock[i]) + { + ++possibleBlockCount; + block = i + ((row / 3) * 3); + } + } + + if (possibleBlockCount == 1) + { + // unset the digitsPossible for squares inside selected bloock that are + // OUTSIDE current row + int blockRow, blockColumn, blockRowOffset, blockColumnOffset; + + blockRowOffset = (block / 3) * 3; + + for (blockRow = blockRowOffset; + blockRow < SUDOKU_BLOCK_HEIGHT + blockRowOffset; ++blockRow) + { + blockColumnOffset = (block % 3) * 3; + + for (blockColumn = blockColumnOffset; + blockColumn < SUDOKU_BLOCK_WIDTH + blockColumnOffset; ++blockColumn) + { + // if current row is NOT the same as outer row... + if (blockRow != row) + { + // ... remove current digit from square's digitsPossible + digitsPossible[blockRow][blockColumn] &= ~testFlag; + } + } + } + } + } + } + + // "lock" candidates by block, processing columns + for (column = 0; column < SUDOKU_COL_COUNT; ++column) + { + for (digit = 1; digit <= SUDOKU_DIGIT_MAX; ++digit) + { + bool digitPossibleInBlock[3] = { 0 }; + int i, possibleBlockCount = 0; + testFlag = SUDOKU_TEST_FLAG_SHIFT(digit); + + for (row = 0; row < SUDOKU_COL_COUNT; ++row) + { + block = SUDOKU_BLOCK_FROM_INTERSECTION(row, column); + + // if digit is possible in this square of current column... + if (digitsPossible[row][column] & testFlag) + { + // ...record which block-row it was possible in + digitPossibleInBlock[block / 3] = true; + } + } + + // once all rows in current column have been broken down into their blocks, + // check if current digit is possible ONLY within one block + for (i = 0; i < 3; ++i) + { + if (digitPossibleInBlock[i]) + { + ++possibleBlockCount; + block = i * 3 + (column / 3); + } + } + + if (possibleBlockCount == 1) + { + // unset the digitsPossible for squares inside selected bloock that are + // OUTSIDE current column + int blockRow, blockColumn, blockRowOffset, blockColumnOffset; + + blockRowOffset = (block / 3) * 3; + + for (blockRow = blockRowOffset; + blockRow < SUDOKU_BLOCK_HEIGHT + blockRowOffset; ++blockRow) + { + blockColumnOffset = (block % 3) * 3; + + for (blockColumn = blockColumnOffset; + blockColumn < SUDOKU_BLOCK_WIDTH + blockColumnOffset; ++blockColumn) + { + // if current column is NOT the same as outer column... + if (blockColumn != column) + { + // ... remove current digit from square's digitsPossible + digitsPossible[blockRow][blockColumn] &= ~testFlag; + } + } + } + } + } + } + + // Now that locked candidates have been identified, scan again + suggestion.newValue = scanForSingleCandidate(digitsPossible, &suggestion.location); + } + + // After all steps, if a suggestion was found, recommend it + if (suggestion.newValue && verbose) + { + printf("Try changing square %c%d to %d\n", + colLabels[suggestion.location.col], + suggestion.location.row + 1, suggestion.newValue); + } + // Otherwise, notify the user that no suggestions were found + else if (verbose) + { + printf("Sorry, no recommendations found using this assistant\n"); + } + + return suggestion; +} + +char scanForSingleCandidate(SudokuDigitTestField digitsPossible[][SUDOKU_COL_COUNT], Coord2D *suggestedSquare) +{ + char suggestionValue = 0; + bool suggestionFound = false; + int row, column, block, digit, bit, bitFlagged, numberFound; + + // check all squares to see if only one candidate is possible + for (row = 0; !suggestionFound && row < SUDOKU_ROW_COUNT; ++row) + { + for (column = 0; !suggestionFound && column < SUDOKU_COL_COUNT; ++column) + { + numberFound = 0; + + // count how many '1' bits are set for current square + for (bit = 0; bit < SUDOKU_DIGIT_MAX; ++bit) + { + // shift current square's SudokuDigitTestField a certain number of bits + // if the least-significant-bit is '1', that is a flagged value + if ((digitsPossible[row][column] >> bit) & 1) + { + // we found a flag, so increment the counter + ++numberFound; + + // save the position of the set bit, in case we can recommend it + bitFlagged = bit; + } + } + + // if numberFound is 1, then only one candidate exists for this square + if (numberFound == 1) + { + suggestionFound = true; + suggestedSquare->row = row; + suggestedSquare->col = column; + + // since only one digit was flagged, 'bitFlagged' is safe to use + // add 1, because bitFlagged represents place-value, which starts at 0 + suggestionValue = bitFlagged + 1; + } + } + } + + // check if any block has only one square possible for a candidate + for (digit = 1; !suggestionFound && digit <= SUDOKU_DIGIT_MAX; ++digit) + { + bit = SUDOKU_TEST_FLAG_SHIFT(digit); + + for (block = 0; !suggestionFound && block < SUDOKU_BLOCK_COUNT; ++block) + { + numberFound = 0; + + for (row = (block / SUDOKU_BLOCK_HEIGHT) * SUDOKU_BLOCK_HEIGHT; + !suggestionFound && + row < (block / SUDOKU_BLOCK_HEIGHT) * SUDOKU_BLOCK_HEIGHT + SUDOKU_BLOCK_HEIGHT; + ++row) + { + for (column = (block % SUDOKU_BLOCK_WIDTH) * SUDOKU_BLOCK_WIDTH; + !suggestionFound && + column < (block % SUDOKU_BLOCK_WIDTH) * SUDOKU_BLOCK_WIDTH + SUDOKU_BLOCK_WIDTH; + ++column) + { + // count candidates for current digit inside current block + if (digitsPossible[row][column] & bit) + { + // found a candidate, so increment counter + ++numberFound; + + // save the square, in case we can recommend it + suggestedSquare->row = row; + suggestedSquare->col = column; + } + } + } + + // if only one candidate for that digit exists in current block... + if (numberFound == 1) + { + // ...we have a recommendation + suggestionFound = true; + suggestionValue = digit; + } + } + } + + // if no recommendation was found, 'recommendationValue' is still 0 (i.e. false) + // otherwise, function returns recommended value for square + return suggestionValue; +} + + +const SudokuAssistant *matchAssistant(char *name) +{ + const SudokuAssistant *assistant = NULL; + int i; + + for (i = 0; !assistant && i < SUDOKU_ASSISTANT_COUNT; ++i) + { + if (strcmp(name, assistants[i].name) == 0) + { + assistant = &assistants[i]; + } + } + + return assistant; +} \ No newline at end of file diff --git a/sudoku_assistant.h b/sudoku_assistant.h new file mode 100644 index 0000000..340200a --- /dev/null +++ b/sudoku_assistant.h @@ -0,0 +1,25 @@ +#ifndef SUDOKU_ASSISTANT_H +#define SUDOKU_ASSISTANT_H + +#include "sudoku_commands.h" +#include "sudoku_undo.h" + +#define SUDOKU_ASSISTANT_NAME_LENGTH_MAX 16 + +struct SudokuAssistant +{ + char *name; + char *description; + HistoryStep(*assistantFunction)(struct SudokuBoard *board, bool verbose); +}; + +typedef struct SudokuAssistant SudokuAssistant; + +extern const SudokuAssistant assistants[]; + +extern const char *sudokuAssistantNoSuggestionMessage; + +const SudokuAssistant *matchAssistant(char *name); + +#endif // SUDOKU_ASSISTANT_H + diff --git a/sudoku_board.c b/sudoku_board.c new file mode 100644 index 0000000..fd39fef --- /dev/null +++ b/sudoku_board.c @@ -0,0 +1,145 @@ +/****************************************************************************** + * Program: sudoku_board.c + * + * Purpose: Defines struct describing the state of a sudoku puzzle board, along + * with functions for initializing, loading, copying, displaying and + * freeing the board's contents + * + * Developer: Philip Ormand + * + * Date: 5/13/16 + * + *****************************************************************************/ +#include +#include +#include +#include +#include + +#include "sudoku_board.h" +#include "sudoku_test_digits.h" +#include "sudoku_undo.h" + + +void initializeSudokuBoard(struct SudokuBoard *board) +{ + // allocate new history stack for this fresh board + if (board->history) + { + // if there is an existing history, free it so it's not leaked + freeHistory(&board->history); + } + + board->history = createHistory(board); + + // initialize board contents to 0; + memset((void*)&(board->contents), 0, SUDOKU_ROW_COUNT * SUDOKU_COL_COUNT); +} + +void freeSudokuBoardResources(struct SudokuBoard *board) +{ + freeHistory(&board->history); +} + +bool loadSudokuBoard(char *fileName, struct SudokuBoard* board) +{ + FILE *file; + + if (file = fopen(fileName, "r")) + { + // reset sudoku board + initializeSudokuBoard(board); + + char *currentSquare = board->contents[0], + *boardEnd = board->contents[0] + SUDOKU_ROW_COUNT * SUDOKU_COL_COUNT, + input; // we can use char for input instead of int, because we're checking feof() + + while (currentSquare < boardEnd && !feof(file)) + { + // scan file stream until we encounter a digit + while (!isdigit(input = getc(file)) && !feof(file)) // break if EOF is encountered + { + ; + } + + if (!feof(file)) + { + // convert ASCII digit to binary integer by subtracting the ASCII code for 0 + *currentSquare++ = input - '0'; + } + } + } + else { + printf("Sorry, file \"%s\" not found\n", fileName); + // return false instead of terminating. program might try to recover. + return false; + } + + return true; +} + +void copySudokuBoardContents(const char source[SUDOKU_ROW_COUNT][SUDOKU_COL_COUNT], char destination[SUDOKU_ROW_COUNT][SUDOKU_COL_COUNT]) +{ + const char *currentSourceSquare = source[0], + *sourceEnd = currentSourceSquare + SUDOKU_ROW_COUNT * SUDOKU_COL_COUNT; + char *currentDestinationSquare = destination[0]; + + while (currentSourceSquare < sourceEnd) + { + *currentDestinationSquare++ = *currentSourceSquare++; + } +} + +void printSudokuBoard(struct SudokuBoard *board) +{ + // strings, not #defines: reduce size of compiled code (due to macro expansions) + // static variables: only one copy of each string exists across all calls + static char *rowDivider = " ++---+---+---++---+---+---++---+---+---++\n", + *rowDividerThick = " ++===+===+===++===+===+===++===+===+===++\n", + *rowFormatString = " %d - || %d | %d | %d || %d | %d | %d || %d | %d | %d ||\n", + *xAxisLabel = " A B C D E F G H I\n"; + int i, j; + + fputs(xAxisLabel, stdout); + for (i = 0; i < SUDOKU_ROW_COUNT; ++i) + { + if ( (i % 3) == 0 ) + { + fputs(rowDividerThick, stdout); + } + else + { + fputs(rowDivider, stdout); + } + + // row label + printf(" %d - ", i + 1); + + for (j = 0; j < SUDOKU_COL_COUNT; ++j) + { + // print "thick" border if on a block boundary + if ( (j % 3) == 0 ) + { + fputs("||", stdout); + } + else + { + putchar('|'); + } + + // print blank if 0, otherwise print digit + if (board->contents[i][j] > 0) + { + printf(" %d ", board->contents[i][j]); + } + else + { + fputs(" ", stdout); + } + } + + fputs("||\n", stdout); + } + fputs(rowDividerThick, stdout); + +} \ No newline at end of file diff --git a/sudoku_board.h b/sudoku_board.h new file mode 100644 index 0000000..3359f9c --- /dev/null +++ b/sudoku_board.h @@ -0,0 +1,29 @@ +#ifndef SUDOKU_BOARD_H +#define SUDOKU_BOARD_H + +#include +#include +#include + +#include "sudoku_undo.h" +#include "sudoku_utility.h" + +struct SudokuBoard { + char contents[SUDOKU_ROW_COUNT][SUDOKU_COL_COUNT]; /**< NOT a null-terminated string; a collection of small integers */ + History history; +}; + +typedef struct SudokuBoard SudokuBoard; + +//"initialize", not "create", since it is not allocated +void initializeSudokuBoard(struct SudokuBoard *board); + +void freeSudokuBoardResources(struct SudokuBoard *board); + +bool loadSudokuBoard(char *fileName, struct SudokuBoard *board); + +void copySudokuBoardContents(const char source[SUDOKU_ROW_COUNT][SUDOKU_COL_COUNT], char destination[SUDOKU_ROW_COUNT][SUDOKU_COL_COUNT]); + +void printSudokuBoard(struct SudokuBoard *board); + +#endif // !SUDOKU_BOARD_H \ No newline at end of file diff --git a/sudoku_commands.c b/sudoku_commands.c new file mode 100644 index 0000000..a97bb93 --- /dev/null +++ b/sudoku_commands.c @@ -0,0 +1,814 @@ +/****************************************************************************** + * Program: sudoku_commands.c + * + * Purpose: Implements the menu-command system for users to interact with the + * program + * + * Developer: Philip Ormand + * + * Date: 5/13/16 + * + *****************************************************************************/ +#include +#include +#include +#include + +#include "sudoku_commands.h" +#include "sudoku_test_digits.h" +#include "sudoku_assistant.h" +#include "sudoku_help.h" + + +#define IS_ANY_INPUT_REMAINING(inputPtr) input->currentIndex + 1 < input->string.length +#define GET_CHAR_FROM_INPUT_STRING(inputPtr, loopVar) inputPtr->string.array[loopVar + inputPtr->currentIndex] + + +// Forward declarations, so function names are visible for definition of 'commands' array +SudokuCommandResult commandNew(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandLoad(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandCheck(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandChange(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandAssist(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandSolve(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandDisplay(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandUndo(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandRedo(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandHelp(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandExit(SudokuBoard *board, SudokuCommandInput *input); +SudokuCommandResult commandCommands(SudokuBoard *board, SudokuCommandInput *input); + + +#define SUDOKU_COMMAND_COUNT sizeof(commands)/sizeof(*commands) + +const struct SudokuCommand commands[] = { + { "new", "Begin new sudoku game", "new ", SUDOKU_HELP_NEW, commandNew }, + { "load", "Load sudoku game from text file", "load ", SUDOKU_HELP_LOAD, commandLoad }, + { "check", "Checks sudoku board to see if solution is correct", "check", SUDOKU_HELP_CHECK, commandCheck }, + { "change", "Change a square's value", "change ", SUDOKU_HELP_CHANGE, commandChange }, + { "assist", "Use an assistant to get suggestion", "assist ", SUDOKU_HELP_ASSIST, commandAssist }, + { "solve", "Let an assistant automatically fill as many squares as it can", "solve ", SUDOKU_HELP_SOLVE, commandSolve }, + { "display", "Displays the current state of the sudoku board", "display", SUDOKU_HELP_DISPLAY, commandDisplay }, + { "undo", "Undoes changes made to the board", "undo ", SUDOKU_HELP_UNDO, commandUndo }, + { "redo", "Redoes changes that were undone", "redo ", SUDOKU_HELP_REDO, commandRedo }, + { "help", "Offers details about a particular command", "help ", SUDOKU_HELP_HELP, commandHelp }, + { "exit", "Exit program", "exit", SUDOKU_HELP_EXIT, commandExit }, + { "commands", "Displays available commands", "commands", SUDOKU_HELP_COMMANDS, commandCommands }, +}; + +const char *showAllCommandsPrompt = "Type 'commands' to list all available commands."; + +struct SudokuBoardPreset +{ + char *name; + char board[SUDOKU_ROW_COUNT][SUDOKU_COL_COUNT]; +}; + +typedef struct SudokuBoardPreset SudokuBoardPreset; + +#define SUDOKU_BOARDPRESET_NAME_LENGTH_MAX 16 +#define SUDOKU_BOARDPRESET_COUNT sizeof(boardPresets)/sizeof(*boardPresets) + +const SudokuBoardPreset boardPresets[] = { + { "blank", { 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, } + }, + { "easy", { 0, 0, 3, 0, 4, 2, 0, 9, 0, + 0, 9, 0, 0, 6, 0, 5, 0, 0, + 5, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 1, 7, 0, 0, 2, 8, 5, + 0, 0, 8, 0, 0, 0, 1, 0, 0, + 3, 2, 9, 0, 0, 8, 7, 0, 0, + 0, 3, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 5, 0, 9, 0, 0, 2, 0, + 0, 8, 0, 2, 1, 0, 6, 0, 0, } + }, + { "supereasy", { 7, 0, 5, 1, 0, 4, 8, 0, 6, + 1, 0, 8, 0, 5, 0, 4, 9, 0, + 4, 0, 3, 6, 2, 0, 0, 5, 7, + 0, 7, 0, 0, 4, 2, 3, 8, 0, + 0, 3, 0, 0, 1, 7, 6, 2, 9, + 2, 5, 9, 3, 0, 0, 0, 0, 1, + 3, 0, 0, 0, 0, 9, 5, 1, 8, + 9, 1, 6, 8, 0, 5, 0, 0, 0, + 0, 8, 2, 4, 7, 0, 9, 0, 3, } + }, + { "moderate", { 4, 0, 0, 9, 0, 3, 5, 0, 6, + 2, 9, 1, 0, 4, 0, 0, 0, 0, + 0, 0, 6, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 6, 3, 0, 7, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + 6, 0, 2, 5, 0, 0, 0, 0, 0, + 0, 0, 0, 7, 0, 0, 8, 0, 0, + 0, 0, 0, 0, 6, 0, 9, 7, 1, + 9, 0, 8, 1, 0, 4, 0, 0, 2, } + }, +}; + +SudokuCommandResult commandNew(SudokuBoard *board, SudokuCommandInput *input) +{ + SudokuCommandResult status = SUDOKU_COMMAND_SUCCESS; + const SudokuBoardPreset *preset = NULL; + char presetName[SUDOKU_BOARDPRESET_NAME_LENGTH_MAX]; + int i; + + // if no argument was provided for preset name + if (!getStringArgument(input, presetName, sizeof(presetName))) + { + // preset defaults to "blank" + strcpy(presetName, "blank"); + } + + for (i = 0; !preset && i < SUDOKU_BOARDPRESET_COUNT; ++i) + { + if (strcmp(presetName, boardPresets[i].name) == 0) + { + preset = &boardPresets[i]; + } + } + + // if match was found for the preset name provided + if (preset) + { + // put sudoku board back to clean, default state + initializeSudokuBoard(board); + + // copy board contents from preset into sudoku board + copySudokuBoardContents(preset->board, board->contents); + + // display new state of the board + printSudokuBoard(board); + } + // if no match was found + else + { + printf("Sorry, no preset found with name \"%s\"\n", presetName); + status = SUDOKU_COMMAND_FAILURE; + } + + return status; +} + +SudokuCommandResult commandLoad(SudokuBoard *board, SudokuCommandInput *input) +{ + SudokuCommandResult status = SUDOKU_COMMAND_SUCCESS; + char fileName[FILENAME_MAX]; + + // if argument provided for filename of sudoku input file + if (getStringArgument(input, fileName, sizeof(fileName))) + { + // if filename exists and board was loaded successfully + if (loadSudokuBoard(fileName, board)) + { + printf("Successfully loaded sudoku board \"%s\"\n\n", fileName); + + // display new state of the board + printSudokuBoard(board); + } + else + { + status = SUDOKU_COMMAND_FAILURE; + } + } + // if no argument was provided, that's an invalid command + else + { + status = SUDOKU_COMMAND_USAGE; + } + + return status; +} + +SudokuCommandResult commandCheck(SudokuBoard *board, SudokuCommandInput *input) +{ + struct DigitsPresent digitsPresent; + + evaluateDigitsPresent(board, &digitsPresent, true); + + return SUDOKU_COMMAND_SUCCESS; +} + +SudokuCommandResult commandChange(SudokuBoard *board, SudokuCommandInput *input) +{ + SudokuCommandResult status = SUDOKU_COMMAND_SUCCESS; + char column, row, value; + + // if there are arguments to read + if (IS_ANY_INPUT_REMAINING(input)) + { + // if any argument not present OR any arguments were invalid + if (!getColumnArgument(input, &column) || + !getRowArgument(input, &row) || + !getSudokuDigitArgument(input, &value)) + { + status = SUDOKU_COMMAND_USAGE; + } + } + // no arguments were provided. Prompt for input + else + { + promptForColumn(&column); + promptForRow(&row); + promptForSudokuDigit(&value); + } + + // all input collected, regardless of method. If we're still okay, make changes to board + if (status == SUDOKU_COMMAND_SUCCESS) + { + // all arguments read and validated + int oldValue = board->contents[row][column]; + + // create history item + Coord2D square; + square.col = column; + square.row = row; + addUndoStep(board->history, &square, value); + + // change square value + board->contents[row][column] = value; + + // if there were undo steps past this point in the stack, they are now invalidated + invalidateSubsequentRedoSteps(board->history); + + printf("Changed square %c%d from %d to %d\n\n", + colLabels[column], row + 1, oldValue, value); + + // display new state of board + printSudokuBoard(board); + } + + return status; +} + +SudokuCommandResult commandAssist(SudokuBoard *board, SudokuCommandInput *input) +{ + SudokuCommandResult status = SUDOKU_COMMAND_SUCCESS; + + const SudokuAssistant *assistant = NULL; + char assistantName[SUDOKU_ASSISTANT_NAME_LENGTH_MAX]; + + // if argument provided for type of assistant to use + if (getStringArgument(input, assistantName, sizeof(assistantName))) + { + // if assistant exists + if (assistant = matchAssistant(assistantName)) + { + assistant->assistantFunction(board, true); + } + else + { + printf("Sorry, \"%s\" is not a valid assistant.\n", assistantName); + status = SUDOKU_COMMAND_FAILURE; + } + } + // if no argument was provided, that's an invalid command + else + { + status = SUDOKU_COMMAND_USAGE; + } + + return status; +} + +SudokuCommandResult commandSolve(SudokuBoard * board, SudokuCommandInput * input) +{ + SudokuCommandResult status = SUDOKU_COMMAND_SUCCESS; + + const SudokuAssistant *assistant = NULL; + char assistantName[SUDOKU_ASSISTANT_NAME_LENGTH_MAX]; + + // if argument provided for type of assistant to use + if (getStringArgument(input, assistantName, sizeof(assistantName))) + { + // if assistant exists + if (assistant = matchAssistant(assistantName)) + { + HistoryStep currentChange = assistant->assistantFunction(board, false); + int i; + + // if we got at least one suggestion + if (currentChange.newValue) + { + // only need to invalidate subsequent history items ONCE + invalidateSubsequentRedoSteps(board->history); + } + + // as long as assistant keeps returning suggestions: + for (i = 1; currentChange.newValue; + currentChange = assistant->assistantFunction(board, false), ++i) + { + // create history item + addUndoStep(board->history, ¤tChange.location, currentChange.newValue); + + // change square value + board->contents + [currentChange.location.row] + [currentChange.location.col] = currentChange.newValue; + + // let the user know what changes are being made + printf("%2d: Changed square %c%d to %d\n", + i, colLabels[currentChange.location.col], + currentChange.location.row + 1, currentChange.newValue); + } + + // if we got at least one suggestion... + if (i > 1) + { + // put gap between change-log and board-printout + putchar('\n'); + + // display present state of the board + printSudokuBoard(board); + } + // otherwise, let the user know + else + { + fputs(sudokuAssistantNoSuggestionMessage, stdout); + } + } + else + { + printf("Sorry, \"%s\" is not a valid assistant.\n", assistantName); + status = SUDOKU_COMMAND_FAILURE; + } + } + // if no argument was provided, that's an invalid command + else + { + status = SUDOKU_COMMAND_USAGE; + } + + return status; +} + +SudokuCommandResult commandDisplay(SudokuBoard *board, SudokuCommandInput *input) +{ + // display present state of the board + printSudokuBoard(board); + + return SUDOKU_COMMAND_SUCCESS; +} + +SudokuCommandResult commandUndo(SudokuBoard *board, SudokuCommandInput *input) +{ + SudokuCommandResult status = SUDOKU_COMMAND_SUCCESS; + unsigned stepsToUndo; + + // if argument provided for stepsToUndo is missing or invalid, assume 1 undo step + if (!getUnsignedArgument(input, &stepsToUndo)) + { + stepsToUndo = 1; + } + + // all arguments read and validated + if (undoStep(board->history, stepsToUndo)) + { + // display new state of board + putchar('\n'); + printSudokuBoard(board); + } + else + { + // if undo fails, specific error message provided by restoreUndoStep + status = SUDOKU_COMMAND_FAILURE; + } + + return status; +} + +SudokuCommandResult commandRedo(SudokuBoard *board, SudokuCommandInput *input) +{ + SudokuCommandResult status = SUDOKU_COMMAND_SUCCESS; + unsigned stepsToRedo; + + // if argument provided for stepsToRedo is missing or invalid, assume 1 redo step + if (!getUnsignedArgument(input, &stepsToRedo)) + { + stepsToRedo = 1; + } + + // all arguments read and validated + if (redoStep(board->history, stepsToRedo)) + { + // display new state of board + putchar('\n'); + printSudokuBoard(board); + } + else + { + // if undo fails, specific error message provided by redoUndoStep + status = SUDOKU_COMMAND_FAILURE; + } + + return status; +} + +SudokuCommandResult commandHelp(SudokuBoard *board, SudokuCommandInput *input) +{ + SudokuCommandResult status = SUDOKU_COMMAND_SUCCESS; + const struct SudokuCommand *command = NULL; + char commandName[SUDOKU_COMMAND_NAME_LENGTH_MAX]; + + // if argument provided for command to help with + if (getStringArgument(input, commandName, sizeof(commandName))) + { + // if command exists + if (command = matchCommand(commandName)) + { + printf("\"%s\": %s\n", command->name, command->description); + printf("Usage: '%s'\n", command->usagePrompt); + printf(command->helpText); + } + else + { + printf("Sorry, \"%s\" is not a valid command.\n", commandName); + status = SUDOKU_COMMAND_FAILURE; + } + } + // if no argument was provided, that's an invalid command + else + { + status = SUDOKU_COMMAND_USAGE; + } + + return status; +} + +SudokuCommandResult commandExit(SudokuBoard *board, SudokuCommandInput *input) +{ + puts("Goodbye!"); + return SUDOKU_COMMAND_EXIT; +} + +SudokuCommandResult commandCommands(SudokuBoard * board, SudokuCommandInput * input) +{ + printCommands(); + + return SUDOKU_COMMAND_SUCCESS; +} + +bool getStringArgument(SudokuCommandInput * input, char * argument, size_t maxLength) +{ + bool status = true; + + if (input) + { + // if the string's array is not NULL and it contains characters + if (input->string.array && input->string.length > 0) + { + // maxLength gets 1 subtracted from it in the loop (to leave room for null-terminator) + // this will cause unsigned overflow it maxLength == 0 + if (maxLength > 0) + { + // if there is still input to read + if (IS_ANY_INPUT_REMAINING(input)) + { + size_t i; // for comparison with maxLength, i should be unsigned type + int ch; + + for (i = 0, ch = GET_CHAR_FROM_INPUT_STRING(input, i); + i < maxLength - 1 && ch != 0 && !isspace(ch); + ++i, ch = GET_CHAR_FROM_INPUT_STRING(input, i)) + { + argument[i] = ch; + } + + // null-terminate the argument + argument[i] = 0; + + // skip to the end of current argument + while (!isspace(ch) && ch != 0) + { + ++i; + ch = input->string.array[i + input->currentIndex]; + } + + // skip the whitespace until next argument + while (isspace(ch)) + { + ++i; + ch = input->string.array[i + input->currentIndex]; + } + + // i + currentIndex is now positioned over the next valid character of + // input from the input string. + input->currentIndex += i; + } + else + { + status = false; + } + } + else + { + terminate("ERROR: tried to get string argument with a maxLength of 0"); + } + } + else + { + terminate("ERROR: tried to get string argument from an invalid String"); + } + } + else + { + terminate("ERROR: tried to get string argument from a null SudokuCommandInput"); + } + + return status; +} + +bool getUnsignedArgument(SudokuCommandInput * input, unsigned * argument) +{ + bool status = true; + + if (input) + { + // if the string's array is not NULL and it contains characters + if (input->string.array && input->string.length > 0) + { + // if there is still input to read + if (IS_ANY_INPUT_REMAINING(input)) + { + String numberCharacters = { 0 }; + initializeString(&numberCharacters); + + size_t i; // for comparison with maxLength, i should be unsigned type + int ch; + + for (i = 0, ch = GET_CHAR_FROM_INPUT_STRING(input, i); + ch != 0 && !isspace(ch); + ++i, ch = GET_CHAR_FROM_INPUT_STRING(input, i)) + { + // get only digits from the argument + if (isdigit(ch)) + { + addCharToString(&numberCharacters, ch); + } + } + + // skip the whitespace until next argument + while (isspace(ch)) + { + ++i; + ch = input->string.array[i + input->currentIndex]; + } + + // i + currentIndex is now positioned over the next valid character of + // input from the input string. + input->currentIndex += i; + + // convert the digit characters read into numeric form + // because we read only digits, number should never be negative + *argument = (unsigned)atoi(numberCharacters.array); + + // NOTE: if conversion fails, *argument == 0 + } + else + { + status = false; + } + } + else + { + terminate("ERROR: tried to get unsigned argument from an invalid String"); + } + } + else + { + terminate("ERROR: tried to get unsigned argument from a null SudokuCommandInput"); + } + + return status; +} + +/** + * returns the INTEGER value of the column, starting from 0, NOT the letter that was provided + */ +bool getColumnArgument(SudokuCommandInput * input, char *argument) +{ + bool status = true; + + if (input) + { + // if the string's array is not NULL and it contains characters + if (input->string.array && input->string.length > 0) + { + // if there is still input to read + if (IS_ANY_INPUT_REMAINING(input)) + { + int i = 0; + + *argument = GET_CHAR_FROM_INPUT_STRING(input, i); + ++i; + + // skip the whitespace until next argument, if any exists + while (isspace(GET_CHAR_FROM_INPUT_STRING(input, i))) + { + ++i; + } + + // i + currentIndex is now positioned over the next valid character of + // input from the input string. + input->currentIndex += i; + + // input read successfully, now validate it + + // turn ASCII code into integer value + *argument = SUDOKU_COL_LETTER_TO_INDEX(*argument); + + // is argument in range [0, 8] (inclusive)? + status = validateColIndex(*argument); + } + else + { + puts("No argument provided for column letter"); + status = false; + } + } + else + { + terminate("ERROR: tried to get column argument from an invalid String"); + } + } + else + { + terminate("ERROR: tried to get column argument from a null SudokuCommandInput"); + } + + return status; +} + +bool getRowArgument(SudokuCommandInput * input, char * argument) +{ + bool status = true; + + if (input) + { + // if the string's array is not NULL and it contains characters + if (input->string.array && input->string.length > 0) + { + // if there is still input to read + if (IS_ANY_INPUT_REMAINING(input)) + { + int i = 0; + + *argument = GET_CHAR_FROM_INPUT_STRING(input, i); + ++i; + + // skip the whitespace until next argument, if any exists + while (isspace(GET_CHAR_FROM_INPUT_STRING(input, i))) + { + ++i; + } + + // i + currentIndex is now positioned over the next valid character of + // input from the input string. + input->currentIndex += i; + + // input read successfully, now validate it + + // turn ASCII code into integer value + *argument = SUDOKU_ROW_NUMBER_TO_INDEX(*argument); + + // is argument inside range [0, 8] (inclusive)? + status = validateRowIndex(*argument); + } + else + { + puts("No argument provided for row number"); + status = false; + } + } + else + { + terminate("ERROR: tried to get row argument from an invalid String"); + } + } + else + { + terminate("ERROR: tried to get row argument from a null SudokuCommandInput"); + } + + return status; +} + +bool getSudokuDigitArgument(SudokuCommandInput * input, char * argument) +{ + bool status = true; + + if (input) + { + // if the string's array is not NULL and it contains characters + if (input->string.array && input->string.length > 0) + { + // if there is still input to read + if (IS_ANY_INPUT_REMAINING(input)) + { + int i = 0; + + *argument = GET_CHAR_FROM_INPUT_STRING(input, i); + ++i; + + // skip the whitespace until next argument, if any exists + while (isspace(GET_CHAR_FROM_INPUT_STRING(input, i))) + { + ++i; + } + + // i + currentIndex is now positioned over the next valid character of + // input from the input string. + input->currentIndex += i; + + // input read successfully, now validate it + + // turn ASCII code into integer value + *argument = SUDOKU_DIGIT_CHAR_TO_VALUE(*argument); + + // is argument in range [0, 9] (inclusive)? + status = validateSudokuDigit(*argument); + } + else + { + puts("No argument provided for new sudoku digit"); + status = false; + } + } + else + { + terminate("ERROR: tried to get sudoku digit argument from an invalid String"); + } + } + else + { + terminate("ERROR: tried to get sudoku digit argument from a null SudokuCommandInput"); + } + + return status; +} + +void printCommands() +{ + int i; + + puts("Available commands:"); + + for (i = 0; i < SUDOKU_COMMAND_COUNT; ++i) + { + printf("'%s' \t(%s)\n", commands[i].name, commands[i].description); + } +} + +const struct SudokuCommand *getCommand(SudokuCommandInput *input) +{ + const struct SudokuCommand *command = NULL; + + do + { + char commandName[SUDOKU_COMMAND_NAME_LENGTH_MAX]; + + clearString(&input->string); + input->currentIndex = 0; + ensureCleanInput(); + + fputs("Enter command: ", stdout); + readString(&input->string); + putchar('\n'); + + if (getStringArgument(input, commandName, SUDOKU_COMMAND_NAME_LENGTH_MAX)) + { + command = matchCommand(commandName); + } + + // command will be null if getCommandNameArgument failed OR if no match was found + if (!command) + { + printf("'%s' is not a valid command. \n%s\n\n", commandName, showAllCommandsPrompt); + } + + } while (!command); + + return command; +} + +const struct SudokuCommand *matchCommand(char *name) +{ + const struct SudokuCommand *command = NULL; + int i; + + for (i = 0; !command && i < SUDOKU_COMMAND_COUNT; ++i) + { + if (strcmp(name, commands[i].name) == 0) + { + command = &commands[i]; + } + } + + return command; +} diff --git a/sudoku_commands.h b/sudoku_commands.h new file mode 100644 index 0000000..b2fc50c --- /dev/null +++ b/sudoku_commands.h @@ -0,0 +1,60 @@ +#ifndef SUDOKU_COMMANDS_H +#define SUDOKU_COMMANDS_H + +#include + +#include "sudoku_board.h" +#include "sudoku_utility.h" + +#define SUDOKU_COMMAND_NAME_LENGTH_MAX 16 + +enum SudokuCommandResult { + SUDOKU_COMMAND_SUCCESS, + SUDOKU_COMMAND_USAGE, + SUDOKU_COMMAND_FAILURE, + SUDOKU_COMMAND_EXIT, +}; +typedef enum SudokuCommandResult SudokuCommandResult; + +struct SudokuCommandInput +{ + String string; + size_t currentIndex; +}; + +typedef struct SudokuCommandInput SudokuCommandInput; + +struct SudokuCommand +{ + char *name; + char *description; + char *usagePrompt; + char *helpText; + SudokuCommandResult(*commandFunction)(SudokuBoard *board, SudokuCommandInput *input); +}; + +typedef struct SudokuCommmand SudokuCommand; + +extern const struct SudokuCommand commands[]; + +extern const char *showAllCommandsPrompt; + + +bool getStringArgument(SudokuCommandInput *input, char *argument, size_t maxLength); + +bool getUnsignedArgument(SudokuCommandInput *input, unsigned *argument); + +bool getColumnArgument(SudokuCommandInput *input, char *argument); + +bool getRowArgument(SudokuCommandInput *input, char *argument); + +bool getSudokuDigitArgument(SudokuCommandInput *input, char *argument); + + +void printCommands(); + +const struct SudokuCommand *getCommand(SudokuCommandInput *input); + +const struct SudokuCommand *matchCommand(char *name); + +#endif // !SUDOKU_COMMANDS_H \ No newline at end of file diff --git a/sudoku_help.h b/sudoku_help.h new file mode 100644 index 0000000..d4e1257 --- /dev/null +++ b/sudoku_help.h @@ -0,0 +1,76 @@ +#ifndef SUDOKU_HELP_H +#define SUDOKU_HELP_H + +#define SUDOKU_HELP_NEW \ +"\nClears current sudoku board and replaces it with a preset.\n" \ +"Default preset (when no argument is provided) is a blank sudoku board.\n" \ +"\nArguments:\n" \ +" - : Name of a preset board to start from:\n" \ +" - \"blank\": A blank sudoku board\n" + +#define SUDOKU_HELP_LOAD \ +"\nDigits may be separated by commas, spaces, any delimiting character, or nothing at all. " \ +"Digits are loaded into the board from left to right, starting at the top-left square\n" \ +"If there is a zero in the file, the corresponding square will be treated as blank. " \ +"If there are not enough digits in the file to fill the whole sudoku board, the remaining " \ +"squares will be blank. If there are too many digits in the file, additional digits will " \ +"be ignored once the board is full. Nonsense characters that don't correspond to digits " \ +"will be automatically ignored.\n" \ +"\nArguments:\n" \ +" - : Name of the text file to load\n" + +#define SUDOKU_HELP_CHECK \ +"\nDisplays feedback if any problems are encountered.\n" + +#define SUDOKU_HELP_CHANGE \ +"\nUsing a value of '0' means the square will be blank.\n" \ +"\nArguments:\n" \ +" - : Letter identifying the column of the square\n" \ +" - : Number identifying the row of the square\n" \ +" - : New value for the square\n" + +#define SUDOKU_HELP_ASSIST \ +"\nAn assistant is a helper that looks for possible additions to the board based on the " \ +"digits that are already present.\n" \ +"\nArguments:\n" \ +" - : Name of the assistant to use:\n" \ +" - \"crosshatch\": Uses cross-hatch scanning to identify 'hidden singles'\n" \ +" - \"locked\": Uses row/column and block exclusion to 'lock' candidates\n" + +#define SUDOKU_HELP_SOLVE \ +"\nAutomatically applies the suggestions of an assistant until no more suggestions are available. " \ +"This will either solve the sudoku puzzle or exhaust the help of the given assistant.\n" \ +"\nArguments:\n" \ +" - : Name of the assistant to use:\n" \ +" - \"crosshatch\": Uses cross-hatch scanning to identify 'hidden singles'\n" \ +" - \"locked\": Uses row/column and block exclusion to 'lock' candidates\n" + +#define SUDOKU_HELP_DISPLAY \ +"" + +#define SUDOKU_HELP_UNDO \ +"\nEvery time you use the 'change' command to alter a square, an undo step is created. \n" \ +"Calling the command with no arguments rolls back a single undo step, as long as there " \ +"are steps to undo.\n" \ +"\nArguments:\n" \ +" - : Number of undo steps to roll back\n" + +#define SUDOKU_HELP_REDO \ +"\nIf you used the 'undo' command to roll back changes, the 'redo' command makes the changes " \ +"again.\n" \ +"\nArguments:\n" \ +" - : Number of steps to redo\n" + +#define SUDOKU_HELP_HELP \ +"\nThis is the command you're using right now.\n" \ +"\nArguments:\n" \ +" - : Name of the command to get help with\n" + +#define SUDOKU_HELP_EXIT \ +"\nCheerfully bids you adieu, until the next time you desire to tangle with the mathematical " \ +"and logical arcana of sudoku\n" + +#define SUDOKU_HELP_COMMANDS \ +"" + +#endif // !SUDOKU_HELP_H diff --git a/sudoku_test_digits.c b/sudoku_test_digits.c new file mode 100644 index 0000000..49035d0 --- /dev/null +++ b/sudoku_test_digits.c @@ -0,0 +1,245 @@ +/****************************************************************************** + * Program: sudoku_test_digits.c + * + * Purpose: Functions for evaluating the sudoku board solution: which digits + * are present and if solution is valid + * + * Developer: Philip Ormand + * + * Date: 5/13/16 + * + *****************************************************************************/ +#include +#include +#include + +#include "sudoku_board.h" +#include "sudoku_test_digits.h" + +/** + * Required for the functionality of the generic 'EVALUATE_DIGIT' macro + */ +enum SUDOKU_COLLECTION_TYPE { + // strange capitalization = parallelism with members of DigitsPresent struct + SUDOKU_IS_rows, + SUDOKU_IS_columns, + SUDOKU_IS_blocks, +}; + + +void reportIllegalDigit(int row, int col, int value); +void reportRepeatedDigit(enum SUDOKU_COLLECTION_TYPE type, int row, int col, int block, int value); +void reportBlankSquare(int row, int col); + +/** + * Business logic of a single iteration, checking if a single digit is present in a single row, + * column, or block. + */ +#define EVALUATE_DIGIT(type, collectionNumber) \ +if (validateSudokuDigit(value)) \ +{ \ + /* if square is blank */ \ + if (value == 0) \ + { \ + /* if caller wants printed feedback AND we haven't reported on this square yet */ \ + if (verbose && !digitsPresent->squaresIllegalOrBlank[i][j]) \ + { \ + reportBlankSquare(i, j); \ + } \ + \ + digitsPresent->squaresIllegalOrBlank[i][j] = true; \ + sudokuValid = false; \ + } \ + /* square has a valid sudoku digit */ \ + else \ + { \ + currentTestFlag = SUDOKU_TEST_FLAG_SHIFT(value); \ + \ + /* if SUDOKU_TEST_ flag is set */ \ + if (digitsPresent->type[collectionNumber] & currentTestFlag) \ + { \ + /* this digit is repeated, so solution is invalid */ \ + sudokuValid = false; \ + \ + /* print feedback message if caller wants it */ \ + if (verbose) { \ + reportRepeatedDigit(SUDOKU_IS_##type, i, j, k, value); \ + } \ + } \ + /* we haven't encountered this digit yet in the current row */ \ + else \ + { \ + /* mark the digit as present */ \ + digitsPresent->type[collectionNumber] |= currentTestFlag; \ + } \ + } \ +} \ +/* illegal sudoku digit */ \ +else \ +{ \ + /* if caller wants printed feedback AND we haven't reported on this square yet */ \ + if (verbose && !digitsPresent->squaresIllegalOrBlank[i][j]) \ + { \ + reportIllegalDigit(i, j, value); \ + } \ + \ + digitsPresent->squaresIllegalOrBlank[i][j] = true; \ + sudokuValid = false; \ +} + +/** + * Macro to retreive the value of the current sudoku square + */ +#define CURRENT_ITEM board->contents[i][j] + +/** + * Populates a DigitsPresent struct with the correct values for the current state of a sudoku board. + * Analyzes sudoku board contents to determine which digits are present in each row, column, and + * block, whether any squares are blank or have illegal values, and whether any digits were repeated + * within a row, column, or block (invalidating the sudoku solution). + * + * @param board Pointer to the SudokuBoard to analyze + * @param digitsPresent Pointer to the DigitsPresent struct that will store the results + * @param verbose If true, results of analysis will be printed to the screen. + */ +void evaluateDigitsPresent(struct SudokuBoard *board, struct DigitsPresent *digitsPresent, bool verbose) +{ + int i, j, k = 0, value; + bool sudokuValid = true; + SudokuDigitTestField currentTestFlag = 0; + + // reset DigitsPresent member + memset((void*)digitsPresent, 0, sizeof(DigitsPresent)); + + // rows + for (i = 0; i < SUDOKU_ROW_COUNT; ++i) + { + for (j = 0; j < SUDOKU_COL_COUNT; ++j) + { + value = CURRENT_ITEM; + + EVALUATE_DIGIT(rows, i) + } + + // at this point, the whole row should be evaluated + if (verbose && digitsPresent->rows[i] != SUDOKU_TEST_ALLDIGITS) + { + // condition is 0 (if-statement fails) if all the same bits are set/unset in current row + // and in SUDOKU_TEST_ALLDIGITS + // contition is non-0 (if-statement succeeds) if there is a bit set in SUDOKU_TEST_ALLDIGITS + // that is not set in current row, meaning there is a digit NOT present in the row + printf("OOPS: row %d doesn't contain all digits from 1 to 9\n", i + 1); + } + } + + // columns + for (j = 0; j < SUDOKU_COL_COUNT; ++j) + { + for (i = 0; i < SUDOKU_ROW_COUNT; ++i) + { + value = CURRENT_ITEM; + + EVALUATE_DIGIT(columns, j) + } + + // at this point, the whole column should be evaluated + if (verbose && digitsPresent->columns[j] != SUDOKU_TEST_ALLDIGITS) + { + printf("OOPS: column %c doesn't contain all digits from 1 to 9\n", colLabels[j]); + } + } + + // Blocks + for (k = 0; k < SUDOKU_BLOCK_COUNT; ++k) + { + for (i = (k / SUDOKU_BLOCK_HEIGHT) * SUDOKU_BLOCK_HEIGHT; + i < (k / SUDOKU_BLOCK_HEIGHT) * SUDOKU_BLOCK_HEIGHT + SUDOKU_BLOCK_HEIGHT; + ++i) + { + for (j = (k % SUDOKU_BLOCK_WIDTH) * SUDOKU_BLOCK_WIDTH; + j < (k % SUDOKU_BLOCK_WIDTH) * SUDOKU_BLOCK_WIDTH + SUDOKU_BLOCK_WIDTH; + ++j) + { + value = CURRENT_ITEM; + + EVALUATE_DIGIT(blocks, k) + } + } + + // at this point, the whole block should be evaluated + if (verbose && digitsPresent->blocks[k] != SUDOKU_TEST_ALLDIGITS) + { + printf("OOPS: block %d doesn't contain all digits from 1 to 9\n", k + 1); + } + } + + if (verbose && sudokuValid) + { + printf("Sudoku solution is valid!\n"); + } +} + +/** + * Prints feedback to STDOUT, reporting that a square contains an illegal value + * Inner function of 'evaluateDigitsPresent' + * + * @param row Index of square's row + * @param col Index of square's column + * @param value Square's current value, which is illegal + */ +inline void reportIllegalDigit(int row, int col, int value) +{ + printf("OOPS: the value '%d' is not a legal sudoku number (%c%d)\n", value, colLabels[col], row + 1); +} + +/** +* Prints feedback to STDOUT, reporting that a square contains a digit repeated elsewhere in its row, +* colulmn, or block. +* Inner function of 'evaluateDigitsPresent' +* +* @param type Reporting problem with a row, column, or block? +* @param row Index of square's row +* @param col Index of square's column +* @param block Index of square's block +* @param value Square's current value, which is illegal +*/ +inline void reportRepeatedDigit(enum SUDOKU_COLLECTION_TYPE type, int row, int col, int block, int value) +{ + printf("OOPS: the digit '%d' was repeated in ", value); + + // different message, depending on if we're testing a row, column, or block + switch (type) + { + case SUDOKU_IS_rows: + printf("row %d ", row + 1); + break; + + case SUDOKU_IS_columns: + printf("column %c ", colLabels[col]); + break; + + case SUDOKU_IS_blocks: + printf("block %d ", block + 1); + break; + + default: + // there should never be a default case, because 'type' is an enum value + puts("...just kidding. Something went terribly wrong."); + break; + } + + // print coordinates of the square in question + printf("(%c%d)\n", colLabels[col], row + 1); +} + +/** +* Prints feedback to STDOUT, reporting that a square is blank. +* Inner function of 'evaluateDigitsPresent' +* +* @param row Index of square's row +* @param col Index of square's column +*/ +inline void reportBlankSquare(int row, int col) +{ + printf("OOPS: square %c%d is blank\n", colLabels[col], row + 1); +} \ No newline at end of file diff --git a/sudoku_test_digits.h b/sudoku_test_digits.h new file mode 100644 index 0000000..e5f8b2f --- /dev/null +++ b/sudoku_test_digits.h @@ -0,0 +1,52 @@ +#ifndef SUDOKU_TEST_DIGITS_H +#define SUDOKU_TEST_DIGITS_H + +#include + +#include "sudoku_board.h" +#include "sudoku_utility.h" + +struct SudokuBoard; + +/** + * Bit-flags to indicate if a sudoku row, column, or block contains a particular digit + */ +typedef enum { + SUDOKU_TEST_1 = 1, // 00000000 00000001 + SUDOKU_TEST_2 = 2, // 00000000 00000010 + SUDOKU_TEST_3 = 4, // 00000000 00000100 + SUDOKU_TEST_4 = 8, // 00000000 00001000 + SUDOKU_TEST_5 = 16, // 00000000 00010000 + SUDOKU_TEST_6 = 32, // 00000000 00100000 + SUDOKU_TEST_7 = 64, // 00000000 01000000 + SUDOKU_TEST_8 = 128, // 00000000 10000000 + SUDOKU_TEST_9 = 256, // 00000001 00000000 + SUDOKU_TEST_ALLDIGITS = 511, // 00000001 11111111 +} DigitTestFlags; + +#define SUDOKU_TEST_FLAG_SHIFT(number) (1 << ((number) - 1)) + +/** + * Bit-field for the digits contained in an inidividual row, column, or block. + * Value reflects the bit-wise combination of various flags from the DigitTestFlag enum + */ +typedef int SudokuDigitTestField; + +/** + * All the bit-fields neccessary to determine if each row, column, and block in a particular + * sudoku board is valid + */ +struct DigitsPresent { + SudokuDigitTestField columns[9]; /**< records the digits present in each column */ + SudokuDigitTestField rows[9]; /**< records the digits present in each row */ + SudokuDigitTestField blocks[9]; /**< records the digits present in each block */ + bool squaresIllegalOrBlank[SUDOKU_ROW_COUNT][SUDOKU_COL_COUNT]; + /**< records whether a square has been reported to have illegal or blank contents */ +}; + +typedef struct DigitsPresent DigitsPresent; + +void evaluateDigitsPresent(struct SudokuBoard *board, struct DigitsPresent *digitsPresent, bool verbose); + +#endif // !SUDOKU_TEST_DIGITS_H + diff --git a/sudoku_undo.c b/sudoku_undo.c new file mode 100644 index 0000000..c39217f --- /dev/null +++ b/sudoku_undo.c @@ -0,0 +1,325 @@ +/****************************************************************************** + * Program: sudoku.c + * + * Purpose: To implement a history system that keeps track of changes made to + * a sudoku board over time. Allows program to undo or redo steps. + * + * Developer: Philip Ormand + * + * Date: 5/13/16 + * + *****************************************************************************/ +#include + +#include "sudoku_board.h" +#include "sudoku_undo.h" +#include "sudoku_utility.h" + +// internal structs defined in source file, not header file, for information-hiding +// purposes. "clients" of the History ADT don't need to know how it's implemented, +// only the functions they can call to make use of its functionality + +/** +* Struct representing the history of a sudoku board +*/ +struct HistoryStruct { + HistoryStep *steps; /**< dynamically allocated array of HistorySteps */ + HistoryStep *currentStep; /**< off-the-end pointer to top of History stack */ + size_t length; /**< number of steps currently in history */ + size_t capacity; /**< number of elements allocated in 'steps' */ + + struct SudokuBoard *owner; /**< whose history is this? So we don't have to pass + it as a parameter to any "member" functions */ +}; + +typedef struct HistoryStruct HistoryStruct; + + +/** + * Factory function that creates and initializes a dynamic History object for a + * SudokuBoard. The SudokuBoard calls this function with itself as a parameter + * and stores the return value in its history member. + * + * @param owner SudokuBoard that will own the created History stack + */ +History createHistory(struct SudokuBoard *owner) +{ + // allocate space for a HistoryStruct + History history = malloc(sizeof(*history)); + + if (history == NULL) // failed to allocate HistoryStruct + { + terminate("ERROR: could not create History"); + } + + // allocate a dynamic array for holding HistorySteps + history->capacity = 1; + history->length = 0; + history->steps = malloc(sizeof(HistoryStep) * history->capacity); + + if (history->steps == NULL) + { + terminate("ERROR: could not initialize History steps"); + } + + // position currentStep to be ready for recording the first history event + history->currentStep = history->steps; + + // record owner of this History stack for future undo/redo operations + history->owner = owner; + + return history; +} + +/** +* Frees a dynamically-allocated HistoryStruct to prevent memory leaks. +* Pointer passed to function will be set to NULL at the end, indicating that it was freed. +* +* @param historyPtr Pointer to HistoryStruct to be freed +*/ +void freeHistory(History *historyPtr) +{ + if (historyPtr && *historyPtr) + { + History history = *historyPtr; + + // deallocate memory used to store all the HistorySteps + free(history->steps); + + // deallocate the History object itself + free(history); + + // make pointer null, so it can't be accidently freed again + *historyPtr = NULL; + } + else + { + terminate("ERROR: tried to free a null History pointer"); + } +} + +/** +* Reallocates a HistoryStruct's 'steps' member with a larger size if there not enough room to add +* the specified number of HistorySteps +* No change occurs if capacity is large enough already. +* +* @param history Pointer to HistoryStruct that will be modified +* @param stepsToAdd Number of additional HistorySteps to ensure space for +*/ +void growHistory(History history, size_t stepsToAdd) +{ + if (history) + { + if (history->steps) + { + if (stepsToAdd > (history->capacity - history->length)) + { + int currentStepIndex = history->currentStep - history->steps; + history->capacity *= 2; + history->steps = realloc(history->steps, sizeof(HistoryStep) * history->capacity); + + if (history->steps == NULL) + { + terminate("ERROR: unable to grow history"); + } + + history->currentStep = history->steps + currentStepIndex; + } + } + else + { + terminate("ERROR: tried to grow a History with null steps array"); + } + } + else + { + terminate("ERROR: tried to grow a null History"); + } +} + +/** +* Add a HistoryStep to a HistoryStruct stack +* +* @param history Pointer to HistoryStruct that will receive step +* @param square Row/column address of the change +* @param value New value for the square +*/ +void addUndoStep(History history, Coord2D *square, int value) +{ + if (history) + { + if (validateSudokuCoord2D(square)) + { + if (validateSudokuDigit(value)) + { + // store location and current value of sudoku square + history->currentStep->location = *square; + history->currentStep->newValue = value; + history->currentStep->oldValue = + history->owner->contents[square->row][square->col]; + + // advance history + ++history->currentStep; + ++history->length; + growHistory(history, 1); + } + else + { + terminate("ERROR: tried to add undo step with invalid sudoku digit"); + } + } + else + { + terminate("ERROR: tried to add undo step with invalid square address"); + } + } + else + { + terminate("ERROR: tried to add undo step to null History"); + } +} + +/** +* Roll back the changes associated with the most recent HistoryStep(s) in a HistoryStruct +* Automatically recognizes if bottom of the history stack is reached. +* +* @param history Pointer to HistoryStruct +* @param stepsToUndo Number of HistorySteps to roll back +* @return True if any steps were undone, false if no steps were undone +*/ +bool undoStep(History history, size_t stepsToUndo) +{ + bool status = true; + + if (history) + { + if (history->currentStep) + { + // only take action if the number of steps is not 0 + if (stepsToUndo) + { + // if currentStep is the bottom of the stack, there are no steps to undo + if (history->currentStep == history->steps) + { + puts(""); + status = false; + } + else + { + Coord2D *currentSquare = &(history->currentStep - 1)->location; + size_t i, existingValue; + + printf("Undoing %d steps:\n", stepsToUndo); + + for (i = 0; i < stepsToUndo && history->currentStep != history->steps; ++i) + { + --history->currentStep; + currentSquare = &history->currentStep->location; + existingValue = history->owner->contents[currentSquare->row][currentSquare->col]; + history->owner->contents[currentSquare->row][currentSquare->col] = + history->currentStep->oldValue; + + printf(" %d: changed %c%d from %d back to %d\n", i + 1, + colLabels[currentSquare->col], currentSquare->row + 1, + existingValue, history->currentStep->oldValue); + } + + // if function was asked to undo more steps than were available + if (i < stepsToUndo) + { + puts(""); + } + } + } + } + else + { + terminate("ERROR: can't restore undo step if currentStep is null"); + } + } + else + { + terminate("ERROR: tried to restore undo step from a null History"); + } + + return status; +} + +/** +* Reapplies the changes associated with the most recent undone HistoryStep(s) in a HistoryStruct +* Automatically recognizes when the top of history stack is reached. +* +* @param history Pointer to HistoryStruct +* @param stepsToUndo Number of HistorySteps to reapply +* @return True if any steps were redone, false if no steps were redone +*/ +bool redoStep(History history, size_t stepsToRedo) +{ + bool status = true; + + if (history) + { + if (history->currentStep) + { + // only take action if the number of steps is not 0 + if (stepsToRedo) + { + // if currentStep is the top of the stack, there are no steps to redo + if (history->currentStep == history->steps + history->length) + { + puts(""); + status = false; + } + else + { + Coord2D *currentSquare = &(history->currentStep - 1)->location; + size_t i, existingValue; + + printf("Redoing %d steps:\n", stepsToRedo); + + for (i = 0; i < stepsToRedo && history->currentStep != history->steps + history->length; ++i) + { + currentSquare = &history->currentStep->location; + existingValue = history->owner->contents[currentSquare->row][currentSquare->col]; + history->owner->contents[currentSquare->row][currentSquare->col] = + history->currentStep->newValue; + + printf(" %d: changed %c%d from %d back to %d\n", i + 1, + colLabels[currentSquare->col], currentSquare->row + 1, + existingValue, history->currentStep->newValue); + + ++history->currentStep; + } + + // if function was asked to undo more steps than were available + if (i < stepsToRedo) + { + puts(""); + } + } + } + } + else + { + terminate("ERROR: can't restore undo step if currentStep is null"); + } + } + else + { + terminate("ERROR: tried to restore undo step from a null History"); + } + + return status; +} + +/** +* If there are any HistorySteps PAST the current history location (i.e. steps that could be redone), +* make them inaccessible, because they are no longer valid. +* +* @param history Pointer to HistoryStruct +*/ +void invalidateSubsequentRedoSteps(History history) +{ + // set current state of board as final available redo step + history->length = history->currentStep - history->steps; +} diff --git a/sudoku_undo.h b/sudoku_undo.h new file mode 100644 index 0000000..a8ceac8 --- /dev/null +++ b/sudoku_undo.h @@ -0,0 +1,40 @@ +#ifndef SUDOKU_UNDO_H +#define SUDOKU_UNDO_H + +#include + +#include "sudoku_board.h" +#include "sudoku_utility.h" + +struct SudokuBoard; + +/** +* Struct representing a single change to a sudoku board. +*/ +struct HistoryStep { + Coord2D location; /**< row/column of the square that changed */ + char newValue; /**< value of the square after change */ + char oldValue; /**< value of the square before changed */ +}; + +typedef struct HistoryStep HistoryStep; + + +struct HistoryStruct; + +typedef struct HistoryStruct *History; + +History createHistory(struct SudokuBoard *owner); + +void freeHistory(History *historyPtr); + +void addUndoStep(History history, struct Coord2D *square, int value); + + +bool undoStep(History history, size_t stepsToUndo); + +bool redoStep(History history, size_t stepsToRedo); + +void invalidateSubsequentRedoSteps(History history); + +#endif // !SUDOKU_UNDO_H diff --git a/sudoku_utility.c b/sudoku_utility.c new file mode 100644 index 0000000..4366658 --- /dev/null +++ b/sudoku_utility.c @@ -0,0 +1,420 @@ +/****************************************************************************** + * Program: sudoku.c + * + * Purpose: Defining miscellaneous data structs, validation functions, and + * constants + * + * Developer: Philip Ormand + * + * Date: 5/13/16 + * + *****************************************************************************/ +#include +#include +#include +#include +#include + +#include "sudoku_utility.h" + + + /** + * Array containing the letters 'A' through 'I'. + * Retreiving an item from this array with a column index will return the corresponding letter + * of that column + */ +const char colLabels[] = "ABCDEFGHI"; + + +/** + * Calling this function guarantees that the subsequent read will be at the beginning of STDIN + * or at the beginning of a new line of input. + * (no isolated '\n' at the beginning of input, which might throw off the subsequent read) + */ +void ensureCleanInput() +{ + // if stdin is at EOF, either it hasn't been read from yet, or we're on an implementation + // that sets EOF when 'Enter' is pressed at the end of a read. + // hasStdinBeenRead returns true if it has ever been called with 'true' as an argument + // we did not just read from stdin, so we call it with 'false'. + if (!feof(stdin) && hasStdinBeenRead(false)) + { + // otherwise, we've got to clear it to the next newline + char ch = 0; + while ((ch = getchar()) != EOF && ch != '\n') { } + + // last char read was EOF or '\n'. We're clean! + } +} + +/** + * Is STDIN clean, having never been read from since program start? + * Calling this function with 'false' says "I'm not using STDIN, I just want to know if it is clean"; + * Calling this function with 'true' says "I read from STDIN, so it's not guaranteed to be clean anymore". + * Until function is called with 'true', return will always be 'false'. + * After calling with 'true', subsequent calls to the function will always return 'true'. + * + * @param wasJustRead 'true' if program read from STDIN, 'false' if just checking status +*/ +bool hasStdinBeenRead(bool wasJustRead) +{ + static bool hasBeenRead = false; + + // if hasBeenRead == false && wasJustRead == false, hasBeenRead will still be false + // the first time the function is called with wasJustRead == true, hasBeenRead will == true + // from there, function will always return true. + hasBeenRead |= wasJustRead; + + return hasBeenRead; +} + + +/** +* Displays prompt asking user to input a column letter. +* Function returns the ARRAY INDEX for a sudoku column (NOT the ASCII letter) +* +* @param index Pointer to a char that will contain the result +*/ +void promptForColumn(char *index) +{ + do + { + fputs("Enter a column letter: ", stdout); + + ensureCleanInput(); + hasStdinBeenRead(true); + scanf(" %c", (char*) index); + *index = SUDOKU_COL_LETTER_TO_INDEX(*index); + + } while (!validateColIndex(*index)); +} + +/** +* Displays prompt asking user to input a row number. +* Function returns the ARRAY INDEX for a sudoku row (begins at 0, not 1) +* +* @param index Pointer to a char that will contain the result +*/ +void promptForRow(char *index) +{ + do + { + fputs("Enter a row number: ", stdout); + + ensureCleanInput(); + hasStdinBeenRead(true); + scanf(" %c", (char*) index); + *index = SUDOKU_ROW_NUMBER_TO_INDEX(*index); + + } while (!validateRowIndex(*index)); +} + +/** +* Displays prompt asking user to input a sudoku digit. +* Function returns the actual integer value (NOT the ASCII code for the number's symbol). +* A value of 0 represents a blank square. +* +* @param digit Pointer to a char that will contain the result +*/ +void promptForSudokuDigit(char * digit) +{ + do + { + fputs("Enter a sudoku digit: ", stdout); + + ensureCleanInput(); + hasStdinBeenRead(true); + scanf(" %c", (char*) digit); + *digit = SUDOKU_DIGIT_CHAR_TO_VALUE(*digit); + + } while (!validateSudokuDigit(*digit)); +} + +/** +* Ensures that an index is within the range of a sudoku row. +* +* @param row Index to be validated +* @return True if valid, false if invalid +*/ +bool validateRowIndex(size_t row) +{ + // is index in range [0, 8] (inclusive)? + if (row >= 0 && row < SUDOKU_ROW_COUNT) + { + return true; + } + else + { + char asciiValue = (char) SUDOKU_ROW_INDEX_TO_NUMBER(row); + printf("Sorry, '%d' (character '%c') is not a valid row number\n", + asciiValue, asciiValue); + return false; + } +} + +/** +* Ensures that an index is within the range of a sudoku column. +* +* @param column Index to be validated +* @return True if valid, false if invalid +*/ +bool validateColIndex(size_t column) +{ + // is index in range [0, 8] (inclusive)? + if (column >= 0 && column < SUDOKU_COL_COUNT) + { + return true; + } + else + { + printf("Sorry, '%c' is not a valid column letter\n", + SUDOKU_COL_INDEX_TO_LETTER(column)); + return false; + } +} + +/** +* Ensures that a digit is a valid sudoku digit. +* +* @param digit Digit to be validated +* @return True if valid, false if invalid +*/ +bool validateSudokuDigit(size_t digit) +{ + // is index in range [0, 9] (inclusive)? + if (digit >= 0 && digit <= SUDOKU_DIGIT_MAX) + { + return true; + } + else + { + char asciiValue = (char) SUDOKU_DIGIT_VALUE_TO_CHAR(digit); + printf("Sorry, '%d' (character '%c') is not a valid sudoku digit\n", + asciiValue, asciiValue); + return false; + } +} + +/** +* Ensures that a Coord2D struct's coordinates are in the correct range of a sudoku board. +* +* @param coord Pointer to struct that will be validated +* @return True if valid, false if invalid +*/ +bool validateSudokuCoord2D(Coord2D * coord) +{ + return validateColIndex(coord->col) && + validateRowIndex(coord->row); +} + +/** +* Initializes a String ADT with a new dynamically-allocated char array and default member values +* +* @param string Pointer to String that will be initialized +*/ +void initializeString(struct String *string) +{ + if (string) + { + if (string->array != NULL) + { + // if, by some chance, we try to initialize a string that was already + // initialized, make sure to free the old array, so it is not leaked + free(string); + } + + string->array = calloc(1, sizeof(char)); + if (string->array == NULL) + { + terminate("ERROR: could not create String"); + } + + string->capacity = 1; + string->length = 1; + } + else + { + terminate("ERROR: tried to initialize a null String pointer"); + } +} + +/** +* Frees resources of a String ADT to prevent memory leaks +* Member pointing to the dynamic char array is set to NULL, signifying the string is freed +* +* @param string Pointer to String that will be freed +*/ +void freeString(struct String *string) +{ + if (string) + { + // if string's array is NULL, you can't free that. + if (string->array) + { + free(string->array); + string->array = NULL; + } + + // even if the array is NULL, zero out the other members just in case + string->length = 0; + string->capacity = 0; + } + else + { + terminate("ERROR: tried to free a null String pointer"); + } +} + +/** +* Reallocates dynamic char array with a larger size if there is not enough room to add the number +* of characters specified. No change occurs if String's capacity is large enough already. +* +* @param string Pointer to String that will be modified +* @param charsToAdd Number of additional characters to ensure space for +*/ +void growString(struct String *string, size_t charsToAdd) +{ + if (string) + { + if (string->array) + { + if (charsToAdd > (string->capacity - string->length)) + { + string->capacity *= 2; + string->array = realloc(string->array, string->capacity); + + if (string->array == NULL) + { + terminate("ERROR: unable to grow string"); + } + } + } + else + { + terminate("ERROR: tried to grow a String with a null array"); + } + } + else + { + terminate("ERROR: tried to grow a null String"); + } +} + +/** +* Adds a single character to a String ADT. +* Calls 'growString' automatically to make sure there is enough room +* +* @param string Pointer to String receiving characters +* @param ch Character to add to String +*/ +char addCharToString(struct String * string, char ch) +{ + if (string) + { + if (string->array) + { + growString(string, 1); + string->array[string->length - 1] = ch; + string->array[string->length++] = 0; + } + else + { + terminate("ERROR: tried to add a character to a String with a null array"); + } + } + else + { + terminate("ERROR: tried to add character to a null String"); + } + + return ch; +} + +/** +* Reads STDIN, saving characters into a String ADT. +* Reads until newline/EOF is encountered +* +* @param string Pointer to String that will receive input +*/ +int readString(struct String * string) +{ + int ch, charsAdded = 0; + + // we are about to read from stdin, so let hasStdinBeenRead know that, yes, stdin has been read + hasStdinBeenRead(true); + + while ((ch = getchar()) == '\n') + { + // If the first character we read is a newline, that's just the residue from a + // previous input. (This is the pattern established by scanf, so all my functions + // follow the pattern to ensure maximum compatibility.) + + // If the first character encountered is NOT a newline, the loop will terminate, and + // ch holds the first character of real input. + // Otherwise, the loop will run again. Assuming the character following the first + // newline is not another newline, the loop will terminate then. Regardless, this + // loop should chew through any number of consecutive newlines and leave ch with the + // correct value: real input. + } + + while (ch != EOF && ch != '\n') + { + addCharToString(string, ch); + ++charsAdded; + + // ch was primed with the first character outside this loop. + // this statement reads ch for NEXT iteration: + ch = getchar(); + } + + if (ch == '\n') + { + // put back '\n' as a courtesy to the next function that tries to read input. + // if last character read is EOF, this is not neccessary. + ungetc(ch, stdin); + } + + // return number of chars added to string + return charsAdded; +} + +/** +* Makes a String ADT empty, ready to receive new contents. +* Dynamic char array is NOT reallocated; String retains whatever capacity it had before function call. +* +* @param string Pointer to String that will be cleared +*/ +void clearString(struct String * string) +{ + if (string) + { + if (string->array) + { + // first character null-terminated + string->array[0] = 0; + // length back to 1 + string->length = 1; + } + else + { + terminate("ERROR: tried to clear a String with a null array"); + } + } + else + { + terminate("ERROR: tried to clear a null String"); + } +} + + +/** +* Displays error message and exits irregularly if the program encounters a fatal error +* +* @param message Error message to display +*/ +void terminate(const char *message) +{ + puts(message); + exit(EXIT_FAILURE); +} + diff --git a/sudoku_utility.h b/sudoku_utility.h new file mode 100644 index 0000000..b66aebd --- /dev/null +++ b/sudoku_utility.h @@ -0,0 +1,107 @@ +#ifndef SUDOKU_UTILITY_H +#define SUDOKU_UTILITY_H + +#include + +#define SUDOKU_ROW_COUNT 9 +#define SUDOKU_COL_COUNT 9 +#define SUDOKU_BLOCK_COUNT 9 +#define SUDOKU_BLOCK_WIDTH 3 +#define SUDOKU_BLOCK_HEIGHT 3 +#define SUDOKU_DIGIT_MAX 9 + +/** turn ASCII code into integer, subtracting 1 to convert the human-readable column number to an array index */ +#define SUDOKU_COL_LETTER_TO_INDEX(letter) toupper(letter) - 'A' +/** turn column index integer value back into ASCII */ +#define SUDOKU_COL_INDEX_TO_LETTER(index) (index) + 'A' +/** turn ASCII code into integer, subtracting 1 to convert the human-readable row number to an array index */ +#define SUDOKU_ROW_NUMBER_TO_INDEX(number) (number) - '0' - 1; +/** turn row index integer value back into ASCII */ +#define SUDOKU_ROW_INDEX_TO_NUMBER(index) (index) + '0' + 1 +/** turn ASCII code into integer */ +#define SUDOKU_DIGIT_CHAR_TO_VALUE(character) (character) - '0' +/** turn sudoku digit integer value back into ASCII */ +#define SUDOKU_DIGIT_VALUE_TO_CHAR(digit) (digit) + '0' +/** given a row and column, find which block contains the intersection. Numbering begins at 0 */ +#define SUDOKU_BLOCK_FROM_INTERSECTION(row, column) ((row) / 3) * 3 + (column) / 3 + + +extern const char colLabels[]; + + +void ensureCleanInput(); + +bool hasStdinBeenRead(bool wasJustRead); + + +void promptForColumn(char *index); + +void promptForRow(char *index); + +void promptForSudokuDigit(char *digit); + + +bool validateRowIndex(size_t row); + +bool validateColIndex(size_t col); + +bool validateSudokuDigit(size_t digit); + + +/** +* Struct representing the coordinates of a 2-Dimensional point +* (Origin of coordiate system is top-left) +*/ +struct Coord2D { + /** + * Union of anonymous structs. + * allows 'Coord2D.col' and 'Coord2D.x' to mean the same thing + */ + union { + struct { + size_t col; /**< horizontal coordinate */ + size_t row; /**< vertical coordinate */ + }; + struct { + size_t x; /**< horizontal coordinate */ + size_t y; /**< vertical coordinate */ + }; + }; +}; + +typedef struct Coord2D Coord2D; + +bool validateSudokuCoord2D(Coord2D *coord); + + +/** + * Dynamically-allocated c-string ADT with a growth strategy similar to C++'s vector class + * + * If remaining capacity is not enough to hold the characters a user wants to add, the array + * is reallocated with double the current capacity. + * + * ADT currently supports only adding a character at a time, to allow reading from text input + * like STDIN. No other functionality presently necessary, as the ADT is only used for reading + * command + argument strings. + */ +struct String { + char *array; /**< dynamically-allocated char array to hold contents */ + size_t length; /**< number of characters currently in string. INCLUDES null-terminator */ + size_t capacity; /**< number of elements allocated in 'array' */ +}; + +typedef struct String String; + +void initializeString(String *string); + +void freeString(String *string); + +char addCharToString(String *string, char ch); + +int readString(String *string); + +void clearString(String *string); + +void terminate(const char *message); + +#endif // !SUDOKU_UTILITY_H