Minesweeper in Python
Intermediate · Games · about 90 minutes to build · 150 lines
Open squares by typing their coordinates, flag the mines you find and clear the board without stepping on one.
The program
Press Run and answer its questions in the box that appears under the code.
Open it in the full editor to save it, share it and keep building.
Sample run
What the program printed, and what was typed (in yellow).
Minesweeper
Open every safe square. A number counts the mines touching it.
1) Small 6 x 6, 4 mines
2) Medium 9 x 9, 10 mines
3) Large 10 x 12, 18 mines
Choose a level (1-3): 1
A B C D E F Mines: 4 Flags: 0
1 # # # # # #
2 # # # # # #
3 # # # # # #
4 # # # # # #
5 # # # # # #
6 # # # # # #
Move (like B3, or f B3 to flag): C5
A B C D E F Mines: 4 Flags: 0
1 . . . 2 # #
2 1 1 . 2 # #
3 # 1 . 1 2 #
4 1 1 . . 1 #
5 . . . . 1 1
6 . . . . . .
Move (like B3, or f B3 to flag): f A3
A B C D E F Mines: 4 Flags: 1
1 . . . 2 # #
2 1 1 . 2 # #
3 F 1 . 1 2 #
4 1 1 . . 1 #
5 . . . . 1 1
6 . . . . . .
Move (like B3, or f B3 to flag): F3
A B C D E F Mines: 4 Flags: 1
1 . . . 2 # #
2 1 1 . 2 # #
3 F 1 . 1 2 2
4 1 1 . . 1 #
5 . . . . 1 1
6 . . . . . .
Move (like B3, or f B3 to flag): G2
Type a letter and a number that are on the board, like C4.
Move (like B3, or f B3 to flag): F2
A B C D E F Mines: 4 Flags: 1
1 . . . 2 # #
2 1 1 . 2 # 2
3 F 1 . 1 2 2
4 1 1 . . 1 #
5 . . . . 1 1
6 . . . . . .
Move (like B3, or f B3 to flag): F1
A B C D E F Mines: 4 Flags: 1
1 . . . 2 * 2
2 1 1 . 2 * 2
3 * 1 . 1 2 2
4 1 1 . . 1 *
5 . . . . 1 1
6 . . . . . .
You cleared the board in 4 moves.
Play again? (y/n) n
Goodbye.How it is built
1. The game in one dictionary
One dictionary holds a whole game: the board size, the mines, the open squares and the flags. The last three are sets of (row, col) pairs, so checking a square is quick.
def new_game(rows, cols, mines): """Return a fresh game. The mines are placed after the first move.""" return {"rows": rows, "cols": cols, "mine_count": mines, "mines": set(), "open": set(), "flags": set()}
2. A safe first move
The mines are placed only after the first move, and never on that square or next to it, so the first move always opens an area. random.sample() picks different squares, never the same one twice.
def place_mines(game, row, col): """Scatter the mines, keeping the first square and its neighbours clear.""" safe = set(neighbours(game, row, col)) | {(row, col)} squares = [(r, c) for r in range(game["rows"]) for c in range(game["cols"]) if (r, c) not in safe] game["mines"] = set(random.sample(squares, game["mine_count"]))
3. Counting mines
& between two sets gives the squares that are in both, so the neighbours that are also mines. len() of that is the number shown on the square.
def mines_around(game, row, col): """Count the mines touching a square.""" return len(set(neighbours(game, row, col)) & game["mines"])
4. Flood fill
When a square has no mines around it, all its neighbours are safe, so they are opened too, and so on outwards. A list used as a stack holds the squares still to open; squares already open or flagged are skipped.
def open_square(game, row, col): """Open a square; around a 0, keep opening the neighbours (flood fill).""" to_open = [(row, col)] while to_open: square = to_open.pop() if square in game["open"] or square in game["flags"]: continue game["open"].add(square) if mines_around(game, *square) == 0: to_open.extend(neighbours(game, *square))
5. Reading a move
The pattern accepts b3, B3 or f b3. The letter becomes a column with index() and the number a row, and anything off the board gives None.
def parse_move(game, text): """Turn "b3" into (False, 2, 1) and "f b3" into (True, 2, 1), or None.""" match = MOVE.fullmatch(text.strip().lower()) if not match: return None row, col = int(match.group(3)) - 1, LETTERS.lower().index(match.group(2)) if 0 <= row < game["rows"] and col < game["cols"]: return bool(match.group(1)), row, col return None
6. The game loop
Each move is checked before it is used: an open square or a flagged one gets a message. ^= toggles a flag, adding the square if it is not in the set and removing it if it is.
def play(game): """Play one game until it is won or lost.""" moves = 0 show(game) while True: move = parse_move(game, input("Move (like B3, or f B3 to flag): ")) if move is None: print("Type a letter and a number that are on the board, like C4.") continue flag, row, col = move if (row, col) in game["open"]: print("That square is already open.") elif flag: game["flags"] ^= {(row, col)} # add the flag, or take it away show(game) elif (row, col) in game["flags"]: print("Flagged. Type f and the square again to unflag it.") else: moves += 1 result = reveal(game, row, col) show(game, reveal_mines=result is not None) if result == "lost": print(f"Boom. {LETTERS[col]}{row + 1} was a mine. Game over.") return if result == "won": print(f"You cleared the board in {moves} moves.") return
Make it bigger
- Mark the mine that ended the game with an X and wrongly flagged squares with a dash.
- Add chording: typing a number that already has that many flags around it opens all its other neighbours.
- Time each game with time.perf_counter and keep a best time for each level.
- Let the player choose the board size and the number of mines.
What it practises
- sets
- dictionaries
- flood fill with a stack
- random.sample
- regular expressions