From e3a4919714d5fd454702e1e1f9a938701ee4422a Mon Sep 17 00:00:00 2001 From: MarcUs7i Date: Wed, 29 Jan 2025 20:05:22 +0100 Subject: [PATCH] added some functions to ms_game.c --- ms_game.c | 449 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) diff --git a/ms_game.c b/ms_game.c index 39f66f4..cbb69c9 100644 --- a/ms_game.c +++ b/ms_game.c @@ -10,11 +10,460 @@ * ---------------------------------------------------------- */ +#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; +} \ No newline at end of file