Minesweeper in C: Complete Solution & Deep Dive Guide

a laptop computer sitting on top of a desk

Mastering Minesweeper Logic in C: The Ultimate Developer's Guide

This guide provides a complete solution for creating a Minesweeper board annotator in C. We'll explore how to take a 2D character array representing a board with mines ('*') and empty spaces (' ') and correctly populate the empty squares with the number of adjacent mines, a foundational skill in grid-based problem-solving.


The Challenge: More Than Just a Game

Remember the sheer panic of the classic Minesweeper game? You'd stare at a grid of gray squares, your mouse hovering, making a calculated guess. A click could reveal a helpful number, guiding your next move, or it could unveil a dreaded mine, ending your game instantly. That simple click hides a fascinating logical puzzle that developers can deconstruct and solve with code.

Many of us have played the game, but have you ever stopped to think about the logic that powers it? How does the game know what number to display in each empty square? This isn't just a trivial game mechanic; it's a classic programming problem that tests your understanding of arrays, loops, boundary conditions, and algorithmic thinking. It's a perfect challenge for sharpening your skills in a language like C, where you manage the details yourself.

In this comprehensive guide, we will transform you from a player into the architect. We'll break down the Minesweeper annotation problem step-by-step, build a robust C solution from the ground up, and uncover the elegant logic behind one of computing's most iconic puzzles. This is a core exercise from the kodikra.com C learning path, designed to solidify your fundamental programming abilities.


What is the Minesweeper Annotation Problem?

At its core, the problem is about data transformation. You are given an input, which is a representation of a Minesweeper board, and you must produce an annotated output. Let's define the components clearly.

The Input

The input is a 2D grid or a "board." In C, this is typically represented as an array of strings (char **) or a 2D array (char[][]). Each cell in this grid can have one of two states:

  • '*': Represents a mine.
  • ' ': Represents an empty square that needs to be annotated.

For example, a simple 3x4 board might look like this:


+---+---+---+---+
| * |   |   |   |
+---+---+---+---+
|   |   | * |   |
+---+---+---+---+
|   | * |   |   |
+---+---+---+---+

This translates to an array of strings: {"* ", " * ", " * "}.

The Goal (The Output)

The task is to process this input board and produce a new board of the same dimensions. In the output board:

  • Mine squares ('*') remain unchanged.
  • Each empty square (' ') must be updated to show the count of all mines in its eight adjacent squares (horizontally, vertically, and diagonally).
  • If an empty square has zero adjacent mines, it should remain an empty space (' ').

For the example input above, the correct annotated output would be:


+---+---+---+---+
| * | 2 | 1 | 1 |
+---+---+---+---+
| 2 | 3 | * | 1 |
+---+---+---+---+
| 1 | * | 2 | 1 |
+---+---+---+---+

This translates to the output array of strings: {"*211", "23*1", "1*21"}.


Why C is the Perfect Language for This Challenge

While you could solve this problem in any language, C offers a unique learning experience. It forces you to confront concepts that higher-level languages often abstract away, making you a more disciplined and knowledgeable programmer. This is a cornerstone of the kodikra.com C curriculum.

  • Memory Management: You will be responsible for allocating and freeing memory for the output board. This builds a deep understanding of pointers and dynamic memory allocation with malloc and free.
  • Array and Pointer Arithmetic: Manipulating 2D grids in C provides excellent practice with array indexing and, optionally, pointer arithmetic, which are fundamental to performance-critical applications.
  • Algorithmic Efficiency: C's low-level nature encourages you to think about the efficiency of your loops and function calls. You're closer to the hardware, so you can reason more clearly about performance.
  • Boundary Checking: There are no built-in "out of bounds" exceptions that will save you. You must manually and carefully implement logic to ensure you don't access memory outside your array, a critical skill for writing secure and stable code.

How to Design the Annotation Algorithm

The most intuitive and efficient way to solve this is to iterate through the board, find the mines, and then update the neighbors of each mine. Let's break down this strategy.

The core idea is simple: a number appears in a square because it's a neighbor of a mine. Therefore, it makes sense to use the mines as the starting point of our logic.

The High-Level Plan

Our main function, let's call it annotate, will perform the following steps:

  1. Receive the input board (dimensions and content).
  2. Allocate new memory for an output board of the same size. This is crucial to avoid modifying the input data while we are still reading from it.
  3. Copy the contents of the input board to our new output board. Now we have a clean slate to work on.
  4. Iterate through every single cell of the input board using nested loops (one for rows, one for columns).
  5. Inside the loop, check if the current cell contains a mine ('*').
  6. If it is a mine, trigger a helper function that will increment the count of all its valid neighbors on the output board.
  7. After the loops complete, return the fully annotated output board.

This approach ensures a clean separation of concerns and prevents logical errors that can arise from modifying a data structure while iterating over it.

ASCII Art: Main Annotation Logic Flow

Here is a visual representation of the main algorithm's flow, showing how we process the board to find mines and then update their surroundings.

    ● Start `annotate(input_board)`
    │
    ▼
  ┌────────────────────────┐
  │ Allocate `result_board` │
  │ Copy `input_board`      │
  └────────────┬───────────┘
               │
               ▼
    ┌─ Loop rows `r` = 0 to `height-1` ─┐
    │          │                       │
    │          ▼                       │
    │ ┌─ Loop cols `c` = 0 to `width-1` ─┐
    │ │        │                        │
    │ │        ▼                        │
    │ │  ◆ Is `board[r][c]` a mine? ◆   │
    │ │     ╱          ╲              │
    │ │   Yes           No            │
    │ │    │             │            │
    │ │    ▼             ▼            │
    │ │ [Increment      [Continue]    │
    │ │  Neighbors]      │            │
    │ │    └─────────────┤            │
    │ └──────────────────┘            │
    └─────────────────────────────────┘
               │
               ▼
  ┌────────────────────────┐
  │ Return `result_board`  │
  └────────────────────────┘
               │
               ▼
            ● End

The Neighbor Increment Logic

The real work happens when we find a mine. For a mine at position (row, col), we need to check its 8 neighbors. A neighbor's coordinates can be calculated by adding offsets from -1 to +1 to the mine's row and column.

For each of the 8 potential neighbors, we must perform two critical checks:

  1. Boundary Check: Is the neighbor's coordinate within the board's dimensions? For example, if a mine is at (0, 0), its top-left neighbor at (-1, -1) is out of bounds and must be ignored.
  2. Content Check: Is the neighbor square itself a mine? We should only increment the count on non-mine squares.

If a neighbor passes both checks, we can safely increment its value on our output board. Since we store counts as characters, we handle this by converting the character to a number, incrementing it, and converting it back. For example, '1' becomes '2', '2' becomes '3', and an initial ' ' becomes '1'.

ASCII Art: Neighbor Increment Logic Flow

This diagram details the logic for processing the 3x3 grid around a detected mine.

    ● Start `increment_neighbors(board, r, c)`
    │
    ▼
  ┌─ Loop `dr` from -1 to 1 ──────────┐
  │          │                        │
  │          ▼                        │
  │ ┌─ Loop `dc` from -1 to 1 ────────┐
  │ │        │                        │
  │ │        ▼                        │
  │ │ ◆ Is `(dr, dc)` the center (0,0)? ◆
  │ │     ╱          ╲              │
  │ │   Yes           No            │
  │ │    │             │            │
  │ │    ▼             ▼            │
  │ │ [Skip]    ┌─────────────────┐ │
  │ │           │ Check Boundaries  │ │
  │ │           └────────┬──────────┘ │
  │ │                    │            │
  │ │                    ▼            │
  │ │              ◆ Is neighbor valid & not a mine? ◆
  │ │                  ╱          ╲   │
  │ │                Yes           No │
  │ │                 │             │ │
  │ │                 ▼             ▼ │
  │ │             [Increment      [Skip]
  │ │              Neighbor Count]  │ │
  │ │                 └─────────────┤ │
  │ └───────────────────────────────┘ │
  └───────────────────────────────────┘
               │
               ▼
            ● End

The Complete C Solution: A Detailed Code Walkthrough

Now, let's dive into a complete and robust C implementation based on the exclusive learning materials at kodikra.com. We will analyze each function and explain its purpose, syntax, and logic line-by-line.

First, here is the full code, which we will then dissect.


#include "minesweeper.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// Helper function to increment the count on a single, non-mine square.
static void set_single_field(char **minefield, const size_t row, const size_t col)
{
    // If the target cell is a mine, do nothing.
    if (minefield[row][col] == '*')
        return;

    // Get the current character.
    const char c = minefield[row][col];
    
    // If it's a space, set it to '1'. Otherwise, increment the digit.
    minefield[row][col] = (c == ' ') ? '1' : c + 1;
}

// Helper function to find all valid neighbors of a mine and increment their counts.
static void increment_neighbors(char **minefield, const size_t row_count,
                                const size_t col_count, const size_t row,
                                const size_t col)
{
    // Iterate over the 3x3 grid centered on the mine at (row, col).
    for (int r = (int)row - 1; r <= (int)row + 1; ++r) {
        for (int c = (int)col - 1; c <= (int)col + 1; ++c) {
            // Boundary check: ensure the neighbor is within the board.
            if (r >= 0 && r < (int)row_count && c >= 0 && c < (int)col_count) {
                // Call the helper to safely increment the cell's value.
                set_single_field(minefield, r, c);
            }
        }
    }
}

// Main annotation function.
char **annotate(const char **minefield, const size_t rows)
{
    // Handle empty input board.
    if (rows == 0)
        return NULL;

    const size_t cols = strlen(minefield[0]);
    if (cols == 0) {
        // Allocate a structure for a board with rows but no columns.
        char **result = malloc(rows * sizeof(char *));
        if (!result) return NULL;
        for (size_t i = 0; i < rows; ++i) {
            result[i] = malloc(1 * sizeof(char));
            if (!result[i]) return NULL; // Handle allocation failure
            result[i][0] = '\0';
        }
        return result;
    }

    // Allocate memory for the array of row pointers.
    char **annotated_field = malloc(rows * sizeof(char *));
    if (!annotated_field) return NULL; // Always check malloc result

    // Allocate memory for each row and copy the input board.
    for (size_t i = 0; i < rows; ++i) {
        annotated_field[i] = malloc((cols + 1) * sizeof(char));
        if (!annotated_field[i]) return NULL; // Handle allocation failure
        strcpy(annotated_field[i], minefield[i]);
    }

    // Iterate through the input board to find mines.
    for (size_t r = 0; r < rows; ++r) {
        for (size_t c = 0; c < cols; ++c) {
            if (minefield[r][c] == '*') {
                // If a mine is found, increment its neighbors on the copied board.
                increment_neighbors(annotated_field, rows, cols, r, c);
            }
        }
    }

    return annotated_field;
}

// Don't forget to implement a free_annotation function for the caller.
void free_annotation(char **annotation, const size_t rows) {
    if (!annotation) return;
    for (size_t i = 0; i < rows; ++i) {
        free(annotation[i]);
    }
    free(annotation);
}

Code Analysis: set_single_field

This is a small, focused helper function declared as static, meaning it's only visible within this file. Its job is to increment the count on one specific square.

  • if (minefield[row][col] == '*') return;: This is a safety check. The logic in increment_neighbors should already prevent this from being called on a mine, but it's good practice. We never modify a mine.
  • const char c = minefield[row][col];: We read the current value of the cell.
  • minefield[row][col] = (c == ' ') ? '1' : c + 1;: This is the core logic, using a ternary operator. If the cell is a blank space, its first "increment" is to become '1'. If it's already a digit (like '1', '2', etc.), we perform character arithmetic. In ASCII, digits '0' through '9' are contiguous, so adding 1 to the character '1' results in the character '2'.

Code Analysis: increment_neighbors

This function orchestrates the updates for the 8 neighbors of a mine.

  • for (int r = (int)row - 1; r <= (int)row + 1; ++r): This loop iterates through the row above the mine (row - 1), the mine's own row (row), and the row below the mine (row + 1). We cast to int to allow for negative starting values, which are handled by our boundary check.
  • for (int c = (int)col - 1; c <= (int)col + 1; ++c): Similarly, this nested loop iterates through the three relevant columns.
  • if (r >= 0 && r < (int)row_count && c >= 0 && c < (int)col_count): This is the crucial boundary check. It ensures that the neighbor coordinates (r, c) are actually on the board before we try to access them. This prevents segmentation faults and undefined behavior.
  • set_single_field(minefield, r, c);: If the neighbor is within bounds, we call our helper to update its value. Notice that this logic also calls set_single_field on the mine itself. However, the guard clause inside set_single_field handles this gracefully by doing nothing.

Code Analysis: The Main annotate Function

This is the public-facing function that ties everything together.

  • Edge Case Handling: The function starts by checking for empty boards (zero rows or zero columns) and handles them correctly by returning an appropriately allocated structure or NULL. This demonstrates robust programming.
  • Memory Allocation:
    • char **annotated_field = malloc(rows * sizeof(char *));: First, we allocate memory for the top-level array, which is an array of pointers to characters (our rows).
    • annotated_field[i] = malloc((cols + 1) * sizeof(char));: Inside a loop, we allocate memory for each individual row. We allocate cols + 1 bytes to accommodate the string's characters plus the null terminator (\0).
    • Error Checking: Crucially, we check the return value of malloc. If it returns NULL, memory allocation failed, and we must exit gracefully to avoid crashing.
  • Board Copying: strcpy(annotated_field[i], minefield[i]);: We use strcpy to copy the original board data into our new memory block. This gives us a safe copy to modify.
  • Main Loop: The nested for loops iterate through every cell of the *original* minefield. This is key. We read from the original and write to the copy.
  • Finding Mines: if (minefield[r][c] == '*'): When a mine is found, we call increment_neighbors, passing it our annotated_field (the copy) to be modified.
  • Return Value: Finally, the function returns the pointer to the newly created and fully annotated board. The caller is now responsible for freeing this memory.

Code Analysis: free_annotation

Since annotate allocates memory, it's good practice to provide a corresponding function to free it. This is essential for preventing memory leaks.

  • It iterates through each row and calls free() on the row pointer.
  • After all rows are freed, it calls free() on the top-level pointer (the array of pointers).

Potential Pitfalls and Alternative Approaches

While the presented solution is robust, it's useful to understand common mistakes and alternative designs. This knowledge deepens your problem-solving skills.

Common Pitfalls

  • Off-by-One Errors: Forgetting to allocate space for the null terminator (cols + 1) is a classic C error that can lead to buffer overflows.
  • Modifying the Input Board: If you try to modify the board you are iterating over, you can get incorrect counts. For example, if you change a ' ' to a '1', a later part of your algorithm might misinterpret that '1'. Always work on a copy.
  • Forgetting Boundary Checks: Accessing an array index like minefield[-1] is undefined behavior in C and will likely crash your program. Boundary checks are non-negotiable.
  • Memory Leaks: Forgetting to free the memory allocated by malloc will cause your program's memory usage to grow indefinitely over time.

Alternative Strategy vs. Our Approach

An alternative strategy exists: instead of iterating to find mines, you could iterate through every empty square and then check its 8 neighbors to count how many are mines. Let's compare.

Aspect Our Approach (Iterate Mines, Increment Neighbors) Alternative (Iterate Empty Cells, Count Neighbor Mines)
Logic Find a mine, then "push" updates to its neighbors. Find an empty cell, then "pull" information from its neighbors.
Performance Highly efficient, especially on sparse boards (few mines). Each cell is visited a constant number of times. The complexity is O(rows * cols). Can be less efficient on dense boards. For every empty cell, you perform 8 reads. The complexity is also O(rows * cols), but with a potentially higher constant factor.
Read/Write Pattern Reads from the original board, writes to the new board. This is a very clean and safe pattern. Also requires reading from an original and writing to a copy to avoid logical errors. The pattern is similar.
Conclusion Generally preferred. It's conceptually clean and performs excellently. It directly models the cause-and-effect relationship: a mine *causes* its neighbors to have a number. A valid approach that also works correctly if implemented carefully. Can be slightly less intuitive for some developers.

Frequently Asked Questions (FAQ)

What if the input board is invalid, e.g., contains characters other than '*' or ' '?

The current solution doesn't explicitly validate the input. It would treat any non-mine character as an empty space and potentially increment it (e.g., 'a' might become 'b'). A production-grade solution should include an initial validation step that scans the board and returns an error if invalid characters are found.

How is memory managed in this C solution? Who is responsible for freeing it?

The annotate function uses malloc to dynamically allocate memory for the new board. This memory resides on the heap. The function that calls annotate is responsible for later calling the free_annotation function to release this memory and prevent leaks. This is a common pattern in C library design known as "caller frees".

Why not modify the input board directly instead of creating a copy?

Modifying the input board directly is risky and can lead to incorrect results. Imagine your algorithm changes a space to '1'. Later, when processing a different mine, your code might see that '1' and misinterpret it. Creating a copy ensures you are always reading from a pristine, unmodified source while writing to a separate destination, eliminating this class of bugs.

What's the difference between `const char **minefield` and `char minefield[][COLS]` in a function signature?

const char **minefield declares a pointer to a pointer to a char. This is used for "jagged arrays" where each row can be of a different length and is allocated separately in memory. char minefield[][COLS] is for a true 2D array where all rows are contiguous in memory, but it requires the column dimension (COLS) to be known at compile time. The ** approach is more flexible for handling boards of arbitrary width.

Can this algorithm be parallelized to run faster?

Partially. The initial board scan to find mines is easily parallelizable. However, the step where multiple threads increment neighbor counts can be tricky. If two different mines have an overlapping neighbor, multiple threads might try to write to the same memory location simultaneously, creating a "race condition." This would require synchronization mechanisms like mutexes, which could negate the performance gains. For typical board sizes, parallelization is likely overkill.

How would this logic change for a game on an infinite or wrapping (toroidal) grid?

The core logic of iterating through neighbors would remain, but the boundary checks would change significantly. For a toroidal grid, a neighbor at an x-coordinate of -1 would "wrap around" to the maximum x-coordinate. This would be implemented using the modulo operator (%) on the neighbor coordinates to ensure they always fall within the board's dimensions.


Conclusion: Beyond the Game

You have now successfully deconstructed the logic of Minesweeper and implemented a clean, efficient, and robust solution in C. This exercise from the kodikra.com curriculum is more than just a game; it's a practical lesson in some of the most critical aspects of programming: algorithmic design, memory management, pointer manipulation, and the importance of handling edge cases.

The skills you've honed here—iterating over 2D data structures, performing boundary checks, and managing dynamic memory—are directly applicable to a vast range of real-world problems. They are the foundation for tasks in image processing, scientific computing, game development, and any domain that involves grid-based data. By mastering these fundamentals, you are well-equipped to tackle more complex challenges.

Ready to test your skills further? Explore the complete C learning path on kodikra.com for more challenges that will push your abilities and deepen your understanding. Or, if you want to broaden your knowledge, see our complete guide to the C programming language.

Disclaimer: The code provided is compatible with standard C compilers (like GCC and Clang) supporting the C99 standard or newer. All memory allocation and deallocation patterns follow standard C practices.


Published by Kodikra — Your trusted C learning resource.