/*---------------------------------------------------------- * HTBLA-Leonding / Class: 2IHIF * --------------------------------------------------------- * Exercise Number: B1 * Title: Mine Sweeper Game implementation * Author: Marc Tismonar * ---------------------------------------------------------- * Description: * Implementation of ms_game.h. * ---------------------------------------------------------- */ #include #include "ms_game.h" #include "ms_board.h" #include "ms_cell.h" #include "ms_ui_utils.h" /** Encapsulate a cell and its board coordinates */ struct MsCellCoord { MsCell cell; ColAddr col; RowAddr row; }; /** The only instance of cell data */ static struct MsCellCoord cell_coords[MAX_BOARD_SIZE * MAX_BOARD_SIZE] = {0}; /** The struct holding the data of the game */ struct MsGameData { MsBoard board; GameState state; }; /** The instance of the game */ static MsGame the_game = 0; /** * Starts a new game using the given mode. If a mode with a * predefined configuration is used, this function shall actually start * the game with that configuration using function `msg_start_game`. * Otherwise, the user shall be prompted for required attributes * (board size, mines) and the game is started afterwards. * * @param mode The configuration mode of the game to start. * @return The game if if could be started with a predefined configuration, * or 0 if it was not started because a custom configuration is required. */ MsGame msg_start_configured_game(GameMode mode) { if (the_game != 0) { the_game->state = INVALID; } Count cols = 0; Count rows = 0; Count mines = 0; switch (mode) { case BEGINNER: cols = 9; rows = 9; mines = 10; break; case ADVANCED: cols = 16; rows = 16; mines = 40; break; case EXPERT: cols = 30; rows = 16; mines = 99; break; case CUSTOM: default: return 0; } return msg_start_game(cols, rows, mines); } /** * Starts a new game, regardless of the state of the current game. * Therefore, completed games or games in progress are aborted * and a new game is 'restarted'. * * Note: Mines are distributed when the first cell is uncovered * to ensure to uncover an empty cell. * * @param column_count The number of columns to use on the game board. * @param row_count The number of rows to use on the game board. * @param mine_count The number of mines distributed on the board. * It must be greater than 0; * @return The started game or 0 in case errors. */ MsGame msg_start_game(Count column_count, Count row_count, Count mine_count) { if (the_game != 0) { the_game->state = INVALID; } if (column_count == 0 || row_count == 0 || mine_count == 0 || column_count * row_count < mine_count + 9) { return 0; } the_game = (MsGame)calloc(1, sizeof(struct MsGameData)); the_game->board = msb_get_board(); msb_init_board(the_game->board, column_count, row_count); the_game->state = IN_PROGRESS; for (CellIdx row = 0; row < row_count; row++) { for (CellIdx col = 0; col < column_count; col++) { cell_coords[row * column_count + col].cell = msb_get_cell(the_game->board, col, row); cell_coords[row * column_count + col].col = msu_idx_to_col_address(col); cell_coords[row * column_count + col].row = msu_idx_to_row_address(row); } } return the_game; } /** * Determines whether or not the given game is valid. * A game is NOT valid, if: * + it is 0 * + or if it has an invalid board * + or if it has no mines * + or if it has less than 10 cells * + or if it has more mines than cells - 9. * * @param game The game instance in focus. * @return True if the given game is valid, false otherwise. */ bool msg_is_valid(MsGame game) { if (game == 0 || !msb_is_valid(game->board)) { return false; } Count cols = msb_get_column_count(game->board); Count rows = msb_get_row_count(game->board); Count mines = 0; bool any_uncovered = false; for (CellIdx row = 0; row < rows && !any_uncovered; row++) { for (CellIdx col = 0; col < cols && !any_uncovered; col++) { MsCell cell = msb_get_cell(game->board, col, row); if (!msc_is_covered(cell)) { any_uncovered = true; } } } if (any_uncovered) { Count mines = 0; for (CellIdx row = 0; row < rows; row++) { for (CellIdx col = 0; col < cols; col++) { if (msc_has_mine(msb_get_cell(game->board, col, row))) { mines++; } } } return cols * rows >= 10 && mines > 0 && mines < cols * rows - 9; } return cols * rows >= 10; } /** * Selects the addressed cell without performing any action on that cell. Only * covered cells can be selected. The function clears any previous selection * and selects the addressed cell. Therefore invalid addresses or already uncovered cells * has only the effect of clearing a previous selection. * * To avoid uncovering unwanted cells due to wrong user inputs, the cell to uncover * is selected in the first step. Only the selected cell can be uncovered. * * @param game The game instance in focus. * @param column The column of the cell to select in the format used on the UI (e.g. 'A', 'B'). * @param row The row of the cell to select in the format used on the UI (e.g. 1, 2). * @return True if the addressed cell is selected, false otherwise. */ bool msg_select_cell(MsGame game, ColAddr column, RowAddr row) { if (!msg_is_valid(game)) { return false; } Count cols = msb_get_column_count(game->board); Count rows = msb_get_row_count(game->board); CellIdx col_idx = msu_col_address_to_index(column); CellIdx row_idx = msu_row_address_to_index(row); if (col_idx >= cols || row_idx >= rows) { return false; } MsCell cell = msb_get_cell(game->board, col_idx, row_idx); if (!msc_is_covered(cell)) { return false; } for (CellIdx r = 0; r < rows; r++) { for (CellIdx c = 0; c < cols; c++) { cell_coords[r * cols + c].cell = 0; cell_coords[r * cols + c].col = 0; cell_coords[r * cols + c].row = 0; } } cell_coords[row_idx * cols + col_idx].cell = cell; cell_coords[row_idx * cols + col_idx].col = column; cell_coords[row_idx * cols + col_idx].row = row; return true; } /** * Provides the currently selected cell. * * @return The currently selected cell or 0, * if no cell is selected. */ MsCell msg_get_selected_cell(MsGame game) { if (!msg_is_valid(game)) { return 0; } Count cols = msb_get_column_count(game->board); Count rows = msb_get_row_count(game->board); for (CellIdx r = 0; r < rows; r++) { for (CellIdx c = 0; c < cols; c++) { if (cell_coords[r * cols + c].cell != 0) { return cell_coords[r * cols + c].cell; } } } return 0; } /** * Uncovers the currently selected cell. * To avoid uncovering unwanted cells due to wrong user inputs, the cell to uncover * must be selected in the first step via function ´msg_select_cell´. * * Only the selected cell can be uncovered, otherwise the invocation is ignored. * No cell is selected afterwards. * * Mines are randomly distributed on the board when the first cell of a game is uncovered. * Mines shall be distributed in a way that keeps the first uncovered cell empty. * Depending on the size of the board and the number of mines, the randomized mine distribution * may take long to find a proper cell for all mines. Therefore mine distribution shall be * aborted after 10 X mine-count trials. The number of mines on the board need to be updated * accordingly, if not all mines could be dropped. * * @param game The game instance in focus. */ void msg_uncover_selected_cell(MsGame game) { if (!msg_is_valid(game)) { return; } MsCell cell = msg_get_selected_cell(game); if (cell == 0) { return; } if (!msc_uncover(cell)) { return; } Count cols = msb_get_column_count(game->board); Count rows = msb_get_row_count(game->board); Count mines = 0; for (CellIdx r = 0; r < rows; r++) { for (CellIdx c = 0; c < cols; c++) { if (msc_has_mine(msb_get_cell(game->board, c, r))) { mines++; } } } if (mines == 0) { return; } for (CellIdx r = 0; r < rows; r++) { for (CellIdx c = 0; c < cols; c++) { MsCell current = msb_get_cell(game->board, c, r); if (msc_has_mine(current)) { msc_drop_mine(current); } } } } /** * Sets the marker on the addressed cell. To clear the marker, the marker ´NONE´ is used. * The cell to mark must be selected in the first step via function ´msg_select_cell´. * Only the selected cell can be marked, otherwise the invocation is ignored. * No cell is selected afterwards. * * Note: The number of set mine-detected markers must NOT be larger than the number * of mines on the board. * * @param game The game instance in focus. * @param marker The marker to apply. */ void msg_mark_selected_cell(MsGame game, CellMarker marker) { if (!msg_is_valid(game)) { return; } MsCell cell = msg_get_selected_cell(game); if (cell == 0) { return; } if (marker == MINE_DETECTED) { Count mines = msg_get_mines_left_count(game); if (mines == 0) { return; } } msc_set_marker(cell, marker); } /** * Determines the state of the current game. * * @param game The game instance in focus. * @return The state of the current game. */ GameState msg_get_state(MsGame game) { if (!msg_is_valid(game)) { return INVALID; } Count cols = msb_get_column_count(game->board); Count rows = msb_get_row_count(game->board); Count mines = 0; Count uncovered = 0; for (CellIdx r = 0; r < rows; r++) { for (CellIdx c = 0; c < cols; c++) { MsCell cell = msb_get_cell(game->board, c, r); if (msc_has_mine(cell) && msc_get_marker(cell) == MINE_DETECTED) { mines++; } if (!msc_is_covered(cell)) { uncovered++; } } } if (uncovered == cols * rows) { return SOLVED; } if (mines > 0) { return FAILED; } return IN_PROGRESS; } /** * Provides the number of mines that are detected. * This is the difference of total number of mines the board carries * minus the number of cells being marked as 'detected'. * Cells marked as 'suspected' does not contribute to this value. * * @param game The game instance in focus. * @return The number of undetected mines * or 0, if the game is not valid. */ Count msg_get_mines_left_count(MsGame game) { if (!msg_is_valid(game)) { return 0; } Count cols = msb_get_column_count(game->board); Count rows = msb_get_row_count(game->board); Count mines = 0; for (CellIdx r = 0; r < rows; r++) { for (CellIdx c = 0; c < cols; c++) { MsCell cell = msb_get_cell(game->board, c, r); if (msc_has_mine(cell) && msc_get_marker(cell) == MINE_DETECTED) { mines++; } } } return mines; } /** * Provides the number of cells that are suspected carrying mines. * * @param game The game instance in focus. * @return The number of suspected mines * or 0, if the game is not valid. */ Count msg_get_mines_suspected_count(MsGame game) { if (!msg_is_valid(game)) { return 0; } Count cols = msb_get_column_count(game->board); Count rows = msb_get_row_count(game->board); Count mines = 0; for (CellIdx r = 0; r < rows; r++) { for (CellIdx c = 0; c < cols; c++) { MsCell cell = msb_get_cell(game->board, c, r); if (msc_has_mine(cell) && msc_get_marker(cell) == MINE_SUSPECTED) { mines++; } } } return mines; } /** * Provides the number of times a cell was actively uncovered by the user. * Note: This is not (necessarily) equal to the number of uncovered cells, * because uncovering empty cells uncovers more than one cell. Such an * activity is counted as 'one action' only. * * @param game The game instance in focus. * @return The number of times the used uncovered a cell * or 0, if the game is not valid. */ Count msg_get_uncover_action_count(MsGame game) { if (!msg_is_valid(game)) { return 0; } Count cols = msb_get_column_count(game->board); Count rows = msb_get_row_count(game->board); Count actions = 0; for (CellIdx r = 0; r < rows; r++) { for (CellIdx c = 0; c < cols; c++) { MsCell cell = msb_get_cell(game->board, c, r); if (!msc_is_covered(cell)) { actions++; } } } return actions; } /** * Provides access to the underlying game board. This function is intended * for being used by game visualizer but should not be needed by the game * application. * * @param game The game instance in focus. * @return The board used by the game instance or 0, if the game is invalid. */ MsBoard msg_get_board(MsGame game) { return msg_is_valid(game) ? game->board : 0; }