~n0mn0m/minesweeper

5fa97ea8be6df468720d958d543f24a634d96d40 — n0mn0m 2 years ago master
Minesweeper in the terminal.

Python implementation of minesweeper.
4 files changed, 346 insertions(+), 0 deletions(-)

A .gitignore
A LICENSE
A README.md
A minesweeper.py
A  => .gitignore +140 -0
@@ 1,140 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/


A  => LICENSE +11 -0
@@ 1,11 @@
Copyright 2020 Alexander (n0mn0m) Hagerman

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file

A  => README.md +45 -0
@@ 1,45 @@
## About

A simple implementation of Minesweeper using python and the terminal.

Requires Python >= 3.6 for fstrings no other packages required.

May be interesting to others as I used set coordinates to perform the mine
and flag handling instead of nested looping.


```
~/projects/minesweeper$ python3 minesweeper.py

Mine Sweeper

Input coordinates in xyc format where:

x = the position on the x axis
y = the position on the y axis
c = the command type. f for flag c for clear

Rules:
There are a set number of mines on the board.
Mark them all with a flag (f) while not attempting
to clear any coordinates that contain a mine.

Each time you clear an empty tile that tile will
show you how many mines are neighboring that tile.

To remove a placed flag enter the coordinates of
the tile with a flag and the flag command (eg. 23f)
and the flag will be removed.

Enter 'help' at any time to see this screen again.

Flags placed 0
Total mines 7
Mines left 7
[['x', 'x', 'x', 'x', 'x', 'x'],
 ['x', 'x', 'x', 'x', 'x', 'x'],
 ['x', 'x', 'x', 'x', 'x', 'x'],
 ['x', 'x', 'x', 'x', 'x', 'x'],
 ['x', 'x', 'x', 'x', 'x', 'x'],
 ['x', 'x', 'x', 'x', 'x', 'x']]
```
\ No newline at end of file

A  => minesweeper.py +150 -0
@@ 1,150 @@
"""
Mine Sweeper

Input coordinates in xyc format where:

x = the position on the x axis
y = the position on the y axis
c = the command type. f for flag c for clear

Rules:
There are a set number of mines on the board.
Mark them all with a flag (f) while not attempting
to clear any coordinates that contain a mine.

Each time you clear an empty tile that tile will
show you how many mines are neighboring that tile.

To remove a placed flag enter the coordinates of
the tile with a flag and the flag command (eg. 23f)
and the flag will be removed.

Enter 'help' at any time to see this screen again.
"""

import itertools
import random
import sys
from pprint import pprint as pp


def setup_gameboard(grid_size, mine_ratio=.2):
    grid = [["x"] * grid_size for i in range(grid_size) ]
    mine_count = int(grid_size * grid_size * .2)
    # We don't have to actually put the moves on the board.
    # The board is nothing but a set of coordinates that we
    # are tracking the state of. Since the mines are relatively
    # Sparse we can have then in a set, and do set operations
    # against the mines instead of have to loop a matrix over
    # and over.
    mine_coords = set()

    # This could in theory be infinite by really really
    # bad luck, but doubtful
    while len(mine_coords) < mine_count:
        x = random.randint(0, grid_size - 1)
        y = random.randint(0, grid_size - 1)

        mine = f"{x}{y}"

        if mine not in mine_coords: 
            mine_coords.add(mine)

    return grid, mine_coords

def set_flag(grid, flags, move):
    coord = move[:2]
    x = int(coord[0])
    y = int(coord[1])
    if coord in flags:
        flags.remove(coord)
        grid[x][y] = "x"
    else:
        flags.add(coord)
        grid[x][y] = "f"

    return grid

def clear_tile(grid, mines, move):
    """
    Clear the tile, and display how many neighboring tiles are mines. If the cleared
    tile is a mine see if they user wants to play again.
    """
    coord = move[:2]
    pp(coord)
    if coord in mines:
        print("BOOM! You hit a mine.")
        again = input("\nPlay again (y/n)? ")
        if again == "y":
            play_game()
        else:
            sys.exit()
    else:
        x = int(coord[0])
        y = int(coord[1])
        # Neighbors in rows above and below, we are not checking indices in
        # the grid otherwise you would need to guard these.
        neighbors = 0
        row_neighbords = (x-1, x, x+1)
        column_neighbords = (y-1, y, y+1)
        possible_mines = itertools.product(row_neighbords, column_neighbords)
        for neighbor in possible_mines:
            possible_mine = f"{neighbor[0]}{neighbor[1]}"
            if possible_mine in mines:
                neighbors += 1
        grid[x][y] = f"{neighbors}"
        return grid

def play_game():
    """
    Top level function that initializes game variables
    and puts us into the user input eval loop.
    """
    grid_size = 6
    grid, mines = setup_gameboard(grid_size)
    mine_count = len(mines)
    flag_count = 0
    flags = set()
    # Uncomment to make testing easier.
    # print(f"DEBUG: {mines}\n\n")
    
    while flags != mines:
        print(f"Flags placed {len(flags)}")
        print(f"Total mines {len(mines)}")
        print(f"Mines left {len(mines - flags)}")
        pp(grid)

        move = input(f"\nCoordinate (xy from 0 to {grid_size -1} and command: ")

        if move == "exit":
            sys.exit()
        elif len(move) != 3 or move == "help":
            print(__doc__)
        elif move[2] == "f":
            try:
                grid = set_flag(grid, flags, move)
            except IndexError:
                print(f"Invalid coordinates. Coordinates should be between 0 and {grid_size-1}")
        elif move[2] == "c":
            try:
                grid = clear_tile(grid, mines, move)
            except IndexError:
                print(f"Invalid coordinates. Coordinates should be between 0 and {grid_size-1}")
        else:
            print("Unknown command type.\n\nPlease enter coord (xy) follow by f or c (flag or clear).\n\nEnter 'help' for more information.\n\n")

    print("\n\nCongratulations you've found all the mines!")
    pp(mines)
    pp(grid)

    again = input("\n\nPlay again (y/n)? ")
    if again == "y":
        play_game()
    else:
        sys.exit()


if __name__ == "__main__":
    print(__doc__)
    play_game()