eibx.com | articles | about

Exercise: Minesweeper in under 100 lines of C

I stumbled upon this post "Exercise: Minesweeper in 100 lines of clean Ruby" by Radan Skorić.

I like the premise of trying to make Minesweeper in 100 lines of code while still maintaining readability. So I took up the challenge myself.

The end result looks like this:

Generating the field

In order to simplify some calculations later I've chosen to add a border around the map. To do so we add 2 to the width and height.

So a selected field size of 5x5 becomes 7x7 - that means we'll be allocating 49 bytes.

While the bytes are linear in memory we can visualize them with every 7th byte on a new row.

This will be our minefield.

After allocating we have to clear the memory and add our border.

The tiles will be filled with '#' and the border will be '+'.

+ + + + + + + +
+ # # # # # # +
+ # # # # # # +
+ # # # # # # +
+ # # # # # # +
+ # # # # # # +
+ + + + + + + +

            
                field = (char*)malloc(width*height);
                flags = (char*)malloc(width*height);
                for (int y = 0; y < height; y++) {
                    for (int x = 0; x < width; x++) {
                        int is_border = (x == 0 || y == 0 || x == width-1 || y == height-1);
                        field[x+y*width] = is_border ? '+' : '#';
                        flags[x+y*width] = 0;
                    }
                }
            
        

Placing mines

The simplest way is to keep trying to place the mine at a random position until we have placed all our mines.

This is also a bad solution since it could theoretically run forever.

+ + + + + + + +
+ # # # # # # +
+ # # # M M M +
+ # # # # # # +
+ M M # # # # +
+ M # # # # # +
+ + + + + + + +

            
                while (mines > 0) {
                    unsigned int tile_index = rand() % (width*height);
                    if (field[tile_index] == '#') {
                        field[tile_index] = 'M';
                        mines--;
                    }
                }
            
        

Printing the map

Now Minesweeper is not so much fun, if you can see where the mines are.

So when we print the map we replace certain characters.

If a tile contains a mine ('M') - render it as a blank ('#').
If our flag array contains a flag on the tile - render it as a flag ('.')
If our cursor is placed on the tile - render it as a cursor ('X')

+ + + + + + + +
+ X # # # # # +
+ # # # # # # +
+ # # # # # # +
+ # # # # # # +
+ # # # # # # +
+ + + + + + + +

            
                for (int y = 0; y < height; y++) {
                    for (int x = 0; x < width; x++) {
                        char c = field[x+y*width];

                        // If the tile is a mine - print it as a regular tile
                        if (c == 'M') c = '#';
                        // If the tile has flag on it show it.
                        if (c == '#' && flags[x+y*width] == 1) c = '.';
                        // Print the cursor instead of the tile
                        if (x == cursor_x && y == cursor_y) c = 'X';

                        printf("%c", c);
                    }
                    printf("\n");
                }
            
        

Selecting a tile

The thrill of Minesweeper comes from selecting a tile. If the tile you select happens to be a mine you're done.

But if you hit an empty tile a larger portion of the field might be revealed.

            
                if (input == ' ' && !*flag_tile) {
                    // Hit a mine
                    if (*cursor_tile == 'M') break;

                    // Hit an empty tile
                    if (*cursor_tile == '#') {
                        flood_fill(cursor_x, cursor_y);

                        empty_tiles_left = 0;
                        for (int i = 0; i < width*height; i++) { 
                            if (field[i] == '#') empty_tiles_left++; 
                        }
                    }
                }
            
        

Revealing the field

In order to reveal a larger portion of the map I've chosen to do a simple flood fill.

It's implemented as a simple recursive function that recursively goes north, west, east, and south.

If a tile has no surrounding mines it marks the tile with '_' and recursion continues.

If a tile has any mines surrounding it - the number of mines will be written to the field and the recursion stops.

Here we're selecting the top left tile:

+ + + + + + + +
+ X _ 1 # # # +
+ _ _ 1 # # # +
+ 2 2 # # # # +
+ # # # # # # +
+ # # # # # # +
+ + + + + + + +

Here we're selecting the bottom right tile:

+ + + + + + + +
+ _ _ 1 # # # +
+ _ _ 1 # # # +
+ 2 2 2 2 3 2 +
+ # # 1 _ _ _ +
+ # 3 1 _ _ X +
+ + + + + + + +

            
                void flood_fill(int x, int y) {
                    char* tile = &field[x + y * width];
                    if (*tile != '#') return;
                
                    // Count the number of mines around the tile
                    int neighbour_mines = 0;
                    for (int x_offset = -1; x_offset <= 1; x_offset++) {
                        for (int y_offset = -1; y_offset <= 1; y_offset++) {
                            if (tile[x_offset + y_offset * width] == 'M') neighbour_mines++;
                        }
                    }
                
                    // If we don't find any mines around the tile
                    // We mark the tile as empty and continue the search
                    if (neighbour_mines == 0) {
                        *tile = '_';
                
                        flood_fill(x,   y-1);
                        flood_fill(x-1, y);
                        flood_fill(x+1, y);
                        flood_fill(x,   y+1);
                        return;
                    }
                
                    // If we do find mines around the tile
                    // We write the number of mines found
                    *tile = neighbour_mines + '0';
                }
            
        

The end

That's most of what I want to show.

The full source is available below, where you can read how the cursor movement, placing flags, and the instantaneous getchar() works.

Full source

It ended at 90 lines of code (excluding comments and new lines).

Download main.c

    
        // gcc main.c -o minesweeper
        #include <stdio.h>
        #include <stdlib.h>
        #include <termios.h>

        int width, height, mines;
        char *field, *flags;

        void flood_fill(int x, int y) {
            char* tile = &field[x + y * width];
            if (*tile != '#') return;

            // Count the number of mines around the tile
            int neighbour_mines = 0;
            for (int x_offset = -1; x_offset <= 1; x_offset++) {
                for (int y_offset = -1; y_offset <= 1; y_offset++) {
                    if (tile[x_offset + y_offset * width] == 'M') neighbour_mines++;
                }
            }

            // If we don't find any mines around the tile
            // We mark the tile as empty and continue the search
            if (neighbour_mines == 0) {
                *tile = '_';

                flood_fill(x,   y-1);
                flood_fill(x-1, y);
                flood_fill(x+1, y);
                flood_fill(x,   y+1);
                return;
            }

            // If we do find mines around the tile
            // We write the number of mines found
            *tile = neighbour_mines + '0';
        }

        int main(int argc, char** argv) {
            if (argc != 4) {
                printf("Run ./minesweeper [width] [height] [mines]\n");
                return 1;
            }

            // Setup
            // Converting the 3 parameters to integers
            width = atoi(argv[1]) + 2;
            height = atoi(argv[2]) + 2;
            mines = atoi(argv[3]);
            int cursor_x = 1, cursor_y = 1;

            // Prevent the need for pressing enter
            struct termios termconf;
            tcgetattr(0, &termconf);
            termconf.c_lflag &= ~(ICANON);
            tcsetattr(0, TCSANOW, &termconf);

            // Allocate field and clear
            field = (char*)malloc(width*height);
            flags = (char*)malloc(width*height);
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    int is_border = (x == 0 || y == 0 || x == width-1 || y == height-1);
                    field[x+y*width] = is_border ? '+' : '#';
                    flags[x+y*width] = 0;
                }
            }

            // Place mines randomly
            while (mines > 0) {
                unsigned int tile_index = rand() % (width*height);
                if (field[tile_index] == '#') { field[tile_index] = 'M'; mines--; }
            }

            int empty_tiles_left = -1;
            while (1) {
                // Clear terminal
                printf("\033c");

                // Print field
                for (int y = 0; y < height; y++) {
                    for (int x = 0; x < width; x++) {
                        char c = field[x+y*width];

                        // If the tile is a bomb - print it as a regular tile
                        if (c == 'M') c = '#';
                        // If the tile has flag on it show it.
                        if (c == '#' && flags[x+y*width] == 1) c = '.';
                        // Print the cursor instead of the tile
                        if (x == cursor_x && y == cursor_y) c = 'X';

                        printf("%c", c);
                    }
                    printf("\n");
                }
                printf("Movement: Arrowkeys\nSelect tile: Spacebar\nFlag tile: f\n");

                if (empty_tiles_left == 0) {
                    printf("Success!");
                    return 0;
                }

                // Get input
                char input = getchar();

                // Handle movement
                cursor_x += (input == 'D') ? -1 : (input == 'C') ? 1 : 0;
                cursor_y += (input == 'A') ? -1 : (input == 'B') ? 1 : 0;

                // Clamping cursor_x and cursor_y to within the field
                cursor_x = (cursor_x < 0) ? 0 : (cursor_x >= width)  ? width-1  : cursor_x;
                cursor_y = (cursor_y < 0) ? 0 : (cursor_y >= height) ? height-1 : cursor_y;

                char* cursor_tile = &field[cursor_x+cursor_y*width];
                char* flag_tile   = &flags[cursor_x+cursor_y*width];

                // Handle placing flag
                if (input == 'f') *flag_tile = !*flag_tile;

                // Handle "digging"
                else if (input == ' ' && !*flag_tile) {
                    // Hit a mine
                    if (*cursor_tile == 'M') break;

                    // Hit an empty tile
                    if (*cursor_tile == '#') {
                        flood_fill(cursor_x, cursor_y);

                        empty_tiles_left = 0;
                        for (int i = 0; i < width*height; i++) {
                            if (field[i] == '#') empty_tiles_left++;
                        }
                    }
                }
            }

            printf("Boom!");

            return 0;
        }
        
    

Thanks!

Thank you for sticking around till the end. I hope it has been informative and perhaps you've learned a thing or two. :)

If you have any questions, thoughts, or comments, feel free to reach me at hello@eibx.com