added some functions to ms_game.c

This commit is contained in:
MarcUs7i 2025-01-29 20:05:22 +01:00
parent 5e22e2d056
commit e3a4919714

449
ms_game.c
View file

@ -10,11 +10,460 @@
* ----------------------------------------------------------
*/
#include <stdlib.h>
#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;
}