Python Intermediate Algorithms Tutorial

Command Line Basic Minesweeper

In this implementation, the minefield grid is always 4x4 and there's just one mine on the field.

Additional work could be done on this Minesweeper implementation to allow for more adjustable field sizes, additional mines on the field and automatic reveal of coordinates that don't contain any adjacent mines.

For a more robust code solution, I'd recommend more modularity with functions and the use of object-oriented programming to easily allow for reuse of code with inheritance, extensability to various platforms and additional benefits.

from random import randint
import itertools
import logging
import time

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)-16s %(levelname)-12s %(lineno)-6s %(funcName)-28s %(message)-8s',
                    datefmt='%H:%M:%SS'
                    )


def play_minesweeper():
    minefield_rows = 4
    minefield_columns = 4

    logging.debug("set random mine row and column index given minefield size")
    mine_row_index = randint(0, minefield_rows - 1)
    mine_column_index = randint(0, minefield_columns - 1)
    logging.debug("mine is at: ({0}, {1})".format(mine_row_index, mine_column_index))

    def find_ones_possible_indices(mine_index, minefield_length):
        """
        Finds all possible indices of 1s that surround mine
        Function only works under assumption that minefield is square (all sides equal)
        :param mine_index: index of the mine for its corresponding row or column
        :param minefield_length: count of squares along one edge of minefield
        :return: one_indices
        """
        one_indices = []
        if mine_index == minefield_length - 1:
            logging.debug("mine is located in the bottom row of hidden_minefield")
            logging.debug("1s can only be in 2nd to bottom row and bottom row of hidden_minefield")
            for one_index in range(mine_index - 1, mine_index + 1):
                one_indices.append(one_index)
        elif mine_index == 0:
            logging.debug("mine is in the first aka top row of hidden_minefield")
            logging.debug("1s can only be in the top and 2nd to the top row of hidden_minefield")
            for one_index in range(mine_index, mine_index + 2):
                one_indices.append(one_index)
        else:
            logging.debug("mine is located in one of the middle rows")
            logging.debug("1s can be above, on same and below the row of the mine in hidden_minefield")
            for one_index in range(mine_index - 1, mine_index + 2):
                one_indices.append(one_index)
        return one_indices

    row_ones_indices = find_ones_possible_indices(mine_row_index, minefield_rows)
    logging.debug("row_ones_indices: {0}".format(row_ones_indices))
    column_ones_indices = find_ones_possible_indices(mine_column_index, minefield_columns)
    logging.debug("column_ones_indices: {0}".format(column_ones_indices))

    logging.debug("create list with list items of potential row indices and column indices of 1s")
    coordinate_indices_lists = [row_ones_indices, column_ones_indices]
    logging.debug("coordinate_indices_lists: {0}".format(coordinate_indices_lists))

    logging.debug("create empty list to store all possible coordinates of positions of 1s; not all coord. on field")
    ones_coordinates = []
    logging.debug("find cartesian product of values in coordinate_indices_lists")
    for coordinate in itertools.product(*coordinate_indices_lists):
        logging.debug("append coordinate of 1 to ones_coordinates")
        ones_coordinates.append(coordinate)
    logging.debug("ones_coordinates: {0}".format(ones_coordinates))

    logging.debug("generate hidden_minefield variable as a list of lists")
    hidden_minefield = [[] for _ in range(minefield_rows)]

    logging.debug("create counter for all 1s and 0s")
    count_ones_and_zeros = 0

    logging.debug("loop over all values in hidden_minefield; fill in with appropriate values")
    for row_index in range(minefield_rows):
        for column_index in range(minefield_columns):

            # check if this value on the hidden_minefield is the value of the mine
            if (row_index, column_index) == (mine_row_index, mine_column_index):
                hidden_minefield[row_index].append(9)

            # check if this value on the hidden_minefield is in the ones_coordinates list
            elif (row_index, column_index) in ones_coordinates:
                hidden_minefield[row_index].append(1)
                count_ones_and_zeros += 1

            # the value is not the mine or adjacent to the mine
            else:
                hidden_minefield[row_index].append(0)
                count_ones_and_zeros += 1

    logging.debug("output hidden_minefield with each row on a newline")
    for row in hidden_minefield:
        logging.debug(row)

    logging.debug("create the user interface minefield of all ? values")
    user_interface_minefield = [[] for _ in range(minefield_rows)]
    for empty_list in user_interface_minefield:
        for i in range(minefield_columns):
            empty_list.append('?')

    logging.debug("sleep so print messages appear after logging messages")
    time.sleep(0.5)

    print("Time for the user to play! Try to avoid the mines!")
    # only enough guesses for user to guess all 1s and 0s; otherwise, user guesses mine and game ends
    for guess_counter, guess in enumerate(range(count_ones_and_zeros)):
        print("\nIn minefield, row values start at 1 and go to {0}.".format(minefield_rows))
        print("In minefield, column values start at 1 and go to {0}.".format(minefield_columns))
        print("\nThis is the user_interface_minefield:")
        for row in user_interface_minefield:
            print(row)

        # prompt user to guess row and column
        def get_player_input():
            guess_row = int(input("Guess row: "))
            guess_column = int(input("Guess column: "))
            return guess_row, guess_column

        guess_row, guess_column = get_player_input()

        def check_condition_guess(guess_index, minefield_edge_size):
            if isinstance(guess_row, int) is False:
                print("You didn't guess an integer")
                bad_guess_status = True
            elif guess_index < 1 or guess_index > minefield_edge_size:
                print("Your guess integer is out of range of 1-4")
                bad_guess_status = True
            else:
                bad_guess_status = False
            return bad_guess_status

        bad_guess_status = check_condition_guess(guess_row, minefield_rows)
        bad_guess_status = check_condition_guess(guess_column, minefield_columns)

        if bad_guess_status is True:
            print("Please make a more appropriate guess")
            continue

        print("Your guessed coordinate: ({0}, {1})".format(guess_row, guess_column))

        # create new variables for offsets of 1 less than guesses because we use zero-based numbering in Python
        user_guess_row_offset = guess_row - 1
        user_guess_column_offset = guess_column - 1

        # make reveal of board and notify user with text change
        if hidden_minefield[user_guess_row_offset][user_guess_column_offset] == 1:
            print("You guessed a 1 so you're nearby a mine.")
            user_interface_minefield[user_guess_row_offset][user_guess_column_offset] = '1'

        elif hidden_minefield[user_guess_row_offset][user_guess_column_offset] == 0:
            print("You guessed far from a mine. You're ok!")
            user_interface_minefield[user_guess_row_offset][user_guess_column_offset] = '0'

        else:
            print("You guessed the mine! You lose!\nHere's what the minefield looks like:")
            for row in hidden_minefield:
                print(row)
            break

        if guess_counter == 0:
            print("You have made {0} guess".format(guess_counter+1))
        else:
            print("You have made {0} guesses".format(guess_counter + 1))

play_minesweeper()
19:47:35S        DEBUG        16     play_minesweeper             set random mine row and column index given minefield size
19:47:35S        DEBUG        19     play_minesweeper             mine is at: (1, 1)
19:47:35S        DEBUG        41     find_ones_possible_indices   mine is located in one of the middle rows
19:47:35S        DEBUG        42     find_ones_possible_indices   1s can be above, on same and below the row of the mine in hidden_minefield
19:47:35S        DEBUG        48     play_minesweeper             row_ones_indices: [0, 1, 2]
19:47:35S        DEBUG        41     find_ones_possible_indices   mine is located in one of the middle rows
19:47:35S        DEBUG        42     find_ones_possible_indices   1s can be above, on same and below the row of the mine in hidden_minefield
19:47:35S        DEBUG        50     play_minesweeper             column_ones_indices: [0, 1, 2]
19:47:35S        DEBUG        52     play_minesweeper             create list with list items of potential row indices and column indices of 1s
19:47:35S        DEBUG        54     play_minesweeper             coordinate_indices_lists: [[0, 1, 2], [0, 1, 2]]
19:47:35S        DEBUG        56     play_minesweeper             create empty list to store all possible coordinates of positions of 1s; not all coord. on field
19:47:35S        DEBUG        58     play_minesweeper             find cartesian product of values in coordinate_indices_lists
19:47:35S        DEBUG        60     play_minesweeper             append coordinate of 1 to ones_coordinates
19:47:35S        DEBUG        60     play_minesweeper             append coordinate of 1 to ones_coordinates
19:47:35S        DEBUG        60     play_minesweeper             append coordinate of 1 to ones_coordinates
19:47:35S        DEBUG        60     play_minesweeper             append coordinate of 1 to ones_coordinates
19:47:35S        DEBUG        60     play_minesweeper             append coordinate of 1 to ones_coordinates
19:47:35S        DEBUG        60     play_minesweeper             append coordinate of 1 to ones_coordinates
19:47:35S        DEBUG        60     play_minesweeper             append coordinate of 1 to ones_coordinates
19:47:35S        DEBUG        60     play_minesweeper             append coordinate of 1 to ones_coordinates
19:47:35S        DEBUG        60     play_minesweeper             append coordinate of 1 to ones_coordinates
19:47:35S        DEBUG        62     play_minesweeper             ones_coordinates: [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
19:47:35S        DEBUG        64     play_minesweeper             generate hidden_minefield variable as a list of lists
19:47:35S        DEBUG        67     play_minesweeper             create counter for all 1s and 0s
19:47:35S        DEBUG        70     play_minesweeper             loop over all values in hidden_minefield; fill in with appropriate values
19:47:35S        DEBUG        88     play_minesweeper             output hidden_minefield with each row on a newline
19:47:35S        DEBUG        90     play_minesweeper             [1, 1, 1, 0]
19:47:35S        DEBUG        90     play_minesweeper             [1, 9, 1, 0]
19:47:35S        DEBUG        90     play_minesweeper             [1, 1, 1, 0]
19:47:35S        DEBUG        90     play_minesweeper             [0, 0, 0, 0]
19:47:35S        DEBUG        92     play_minesweeper             create the user interface minefield of all ? values
19:47:35S        DEBUG        98     play_minesweeper             sleep so print messages appear after logging messages


Time for the user to play! Try to avoid the mines!

In minefield, row values start at 1 and go to 4.
In minefield, column values start at 1 and go to 4.

This is the user_interface_minefield:
['?', '?', '?', '?']
['?', '?', '?', '?']
['?', '?', '?', '?']
['?', '?', '?', '?']


Guess row:  1
Guess column:  1


Your guessed coordinate: (1, 1)
You guessed a 1 so you're nearby a mine.
You have made 1 guess

In minefield, row values start at 1 and go to 4.
In minefield, column values start at 1 and go to 4.

This is the user_interface_minefield:
['1', '?', '?', '?']
['?', '?', '?', '?']
['?', '?', '?', '?']
['?', '?', '?', '?']


Guess row:  4
Guess column:  4


Your guessed coordinate: (4, 4)
You guessed far from a mine. You're ok!
You have made 2 guesses

In minefield, row values start at 1 and go to 4.
In minefield, column values start at 1 and go to 4.

This is the user_interface_minefield:
['1', '?', '?', '?']
['?', '?', '?', '?']
['?', '?', '?', '?']
['?', '?', '?', '0']