13-minesweeper/ms_game.c
2025-01-29 20:05:22 +01:00

469 lines
No EOL
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*----------------------------------------------------------
* HTBLA-Leonding / Class: 2IHIF
* ---------------------------------------------------------
* Exercise Number: B1
* Title: Mine Sweeper Game implementation
* Author: Marc Tismonar
* ----------------------------------------------------------
* Description:
* Implementation of ms_game.h.
* ----------------------------------------------------------
*/
#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;
}