Example #1
0
    def find_by_exclusion_zone(exclusion_zones: Dict[FrozenSet[Optional[Tuple[
        int, int]]], int],
                               display: bool = False) -> bool:
        """
        Find & open safe-spaces by comparing zones of mutual exclusivity based off exposed hints.

        :param exclusion_zones: a dictionary of zones & the # of mines within those zones
        :param display: Prints board state after execution if True
        :return: True if board state was altered. (Updates param exclusion_zones & solve_mine.lookup by side-effect)
        """

        opened = set()
        updated = False
        # Check for zones which are entirely within a different, larger zone
        for zone, level in sorted(list(exclusion_zones.items()),
                                  key=lambda pair: len(pair[0]),
                                  reverse=False):
            for other, other_level in sorted(list(exclusion_zones.items()),
                                             key=lambda pair: len(pair[0]),
                                             reverse=True):
                if len(zone) > len(other):
                    # Only perform check when zone is small enough
                    #  to fit inside other
                    break
                elif zone < other:  # set operation: 'Proper-subset' (aka. "zone.issubset(other) and zone != other")
                    exclusion_zones.pop(
                        other)  # Pull out larger group for modification
                    new_zone = frozenset(
                        other - zone)  # Remove smaller group from larger group

                    if other_level - level > 0:
                        # Update # of mines in 'larger' group after removing smaller group;
                        #  Put back modified 'larger' group
                        exclusion_zones[new_zone] = other_level - level
                    else:
                        # OR,
                        #   If the # of mines in the modified group is 0,
                        #   we can safely open all spaces in that group, instead
                        for pos in new_zone:
                            if lookup[pos].hint == '?':
                                lookup[pos].hint = f'{open(*pos)}'
                                opened.add(pos)
                                updated = True
                                if display:
                                    clearscreen()
                                    print(hashmaptostring(
                                        lookup, nrows, ncols))
                                    print()

        for zone, freq in list(exclusion_zones.items()):
            new_zone = zone - opened
            exclusion_zones.pop(zone)
            if new_zone:
                exclusion_zones[new_zone] = freq

        if display:
            clearscreen()
            print(hashmaptostring(lookup, nrows, ncols))
            print()
        return updated
Example #2
0
    def open_zeros(display: bool = False) -> None:
        """
        Unveil hints of squares neighboring a square with no surrounding mines.

        :param display: Prints board state after execution if True
        :return: None
        """

        if display:
            print(hashmaptostring(lookup, nrows, ncols), '\n')
        for pos, space in lookup.items():
            if space.hint == '0':
                for neighbor in space.neighbors.values():
                    if neighbor and lookup[neighbor].hint == '?':
                        lookup[neighbor].hint = f"{open(*neighbor)}"
        if display:
            clearscreen()
            print(hashmaptostring(lookup, nrows, ncols))
            print()
Example #3
0
    def update_zones(
        exclusion_zones: Dict[FrozenSet[Optional[Tuple[int, int]]], int]
    ) -> Tuple[bool, bool]:
        """
        Deduces additional exclusion zones from visible hints & known zones. (May also find & mark mines).

        :param exclusion_zones: a dictionary of zones & the # of mines within those zones
        :return: Whether any additional zones were discovered + whether any mines were discovered. (Side-effect: updates solve_mine.lookup)
        """

        if len(exclusion_zones) == 3:
            print(end='')

        by_coord = group_by_coord(exclusion_zones)
        mine_found, zone_added = False, False

        for coord, zones_by_frequency in by_coord.items():
            for freq, zones in zones_by_frequency.items():
                for other_freq, other_zones in zones_by_frequency.items():
                    for zone in zones:
                        for other_zone in other_zones:
                            if freq == other_freq:
                                new_zone = zone ^ other_zone
                                if new_zone and new_zone not in exclusion_zones:
                                    exclusion_zones[new_zone] = 1
                                    zone_added = True
                            else:
                                new_zone = zone - other_zone if freq > other_freq else other_zone - zone
                                if len(new_zone) > 1:
                                    exclusion_zones[new_zone] = abs(freq -
                                                                    other_freq)
                                else:
                                    lookup[set(new_zone).pop()].hint = 'x'
                                    mine_found = True
                                    if display:
                                        clearscreen()
                                        print(
                                            hashmaptostring(
                                                lookup, nrows, ncols))
                                        print()
                                    return mine_found, mine_found
        return zone_added, mine_found
Example #4
0
    def open_safe_spaces(display: bool = False) -> bool:
        """
        Deduce which squares ARE NOT holding mines based off visible hints & 'open' them.

        :param display: Prints board state after execution if True
        :return: True if board state was altered during this invocation
        """

        space_unpacked = False
        for space in lookup.values():
            if space.hint.isnumeric():
                proximity = int(space.hint)
                if proximity > 0 and proximity == sum(
                        1 for neighbor in space.neighbors.values()
                        if neighbor and lookup[neighbor].hint == 'x'):
                    for neighbor in space.neighbors.values():
                        if neighbor and lookup[neighbor].hint == '?':
                            lookup[neighbor].hint = f"{open(*neighbor)}"
                            space_unpacked = True
        if display and space_unpacked:
            clearscreen()
            print(hashmaptostring(lookup, nrows, ncols))
            print()
        return space_unpacked
Example #5
0
    def mark_spaces(display: bool = False) -> int:
        """
        Deduce which squares are holding mines based off visible hints & mark them w/ an 'x'.

        :param display: Prints board state after execution if True
        :return: # of mines found during this invocation
        """

        nfound = 0
        for space in lookup.values():
            if space.hint.isnumeric():
                proximity = int(space.hint)
                if proximity > 0 and proximity == sum(
                        1 for neighbor in space.neighbors.values()
                        if neighbor and lookup[neighbor].hint in 'x?'):
                    for neighbor in space.neighbors.values():
                        if neighbor and lookup[neighbor].hint == '?':
                            lookup[neighbor].hint = 'x'
                            nfound += 1
        if display and nfound:
            clearscreen()
            print(hashmaptostring(lookup, nrows, ncols))
            print()
        return nfound
Example #6
0
def solve_mine(map: str, n: int) -> str:
    """
    Plays a game of Minesweeper as far as possible without making a guess. (Kata mandated entry point for program.)

    :param map: a string representation of the 2d-board
    :param n: # of mines on the gameboard
    :return: a string representation of the solved 2d-board or a single '?' if board is unsolvable.
    """
    def get_frontier() -> Dict[Tuple[int, int], Gridspace]:
        """
        Get '?'-adjacent hint spaces

        :return: a dictionary of coordinates pairs & Gridspace objects identifying spaces w/ a neighboring '?'
        """

        return {
            pos: space
            for pos, space in lookup.items() if space.hint.isnumeric() and any(
                lookup[neighbor].hint == '?'
                for neighbor in space.neighbors.values() if neighbor)
        }

    def get_exclusion_zones(
        frontier: Dict[Tuple[int, int], Gridspace]
    ) -> Dict[FrozenSet[Optional[Tuple[int, int]]], int]:
        """
        Group the frontier-adjacent '?' spaces into pairs of "zone, frequency", where 'zone' is a group of spaces & 'frequency' is the # of mines hiding in zone.

        :param frontier: a dictionary representing the squares immediately surrounded by '?'s
        :return: a dictionary of zones & the # of mines within those zones
        """

        exclusion_zones = {}
        for pos, space in frontier.items():
            nunkown = int(space.hint) - sum(
                1 for neighbor in space.neighbors.values()
                if neighbor and lookup[neighbor].hint == 'x')
            exclusion_zones.update(
                {
                    frozenset(neighbor for neighbor in space.neighbors.values(
                    ) if neighbor and lookup[neighbor].hint == '?'):
                    nunkown
                })
        return exclusion_zones

    def group_by_coord(exclusion_zones: Dict[FrozenSet[Optional[Tuple[int,
                                                                      int]]],
                                             int]):
        """
        Get a nested dictionary tracking which squares are in which zones.

        :param exclusion_zones: a dictionary of zones & the # of mines within those zones
        :return: Nested dictionary as such, {(row, col): {frequency: {zone0, zone1, ...}}}
        """
        by_coord = {}
        for zone in exclusion_zones:
            freq = exclusion_zones[zone]
            for spot in zone:
                by_coord[spot] = by_coord.setdefault(spot, dict())
                by_coord[spot][freq] = by_coord[spot].setdefault(freq, set())
                by_coord[spot][freq].add(zone)
        return by_coord

    def update_zones(
        exclusion_zones: Dict[FrozenSet[Optional[Tuple[int, int]]], int]
    ) -> Tuple[bool, bool]:
        """
        Deduces additional exclusion zones from visible hints & known zones. (May also find & mark mines).

        :param exclusion_zones: a dictionary of zones & the # of mines within those zones
        :return: Whether any additional zones were discovered + whether any mines were discovered. (Side-effect: updates solve_mine.lookup)
        """

        if len(exclusion_zones) == 3:
            print(end='')

        by_coord = group_by_coord(exclusion_zones)
        mine_found, zone_added = False, False

        for coord, zones_by_frequency in by_coord.items():
            for freq, zones in zones_by_frequency.items():
                for other_freq, other_zones in zones_by_frequency.items():
                    for zone in zones:
                        for other_zone in other_zones:
                            if freq == other_freq:
                                new_zone = zone ^ other_zone
                                if new_zone and new_zone not in exclusion_zones:
                                    exclusion_zones[new_zone] = 1
                                    zone_added = True
                            else:
                                new_zone = zone - other_zone if freq > other_freq else other_zone - zone
                                if len(new_zone) > 1:
                                    exclusion_zones[new_zone] = abs(freq -
                                                                    other_freq)
                                else:
                                    lookup[set(new_zone).pop()].hint = 'x'
                                    mine_found = True
                                    if display:
                                        clearscreen()
                                        print(
                                            hashmaptostring(
                                                lookup, nrows, ncols))
                                        print()
                                    return mine_found, mine_found
        return zone_added, mine_found

    def find_by_exclusion_zone(exclusion_zones: Dict[FrozenSet[Optional[Tuple[
        int, int]]], int],
                               display: bool = False) -> bool:
        """
        Find & open safe-spaces by comparing zones of mutual exclusivity based off exposed hints.

        :param exclusion_zones: a dictionary of zones & the # of mines within those zones
        :param display: Prints board state after execution if True
        :return: True if board state was altered. (Updates param exclusion_zones & solve_mine.lookup by side-effect)
        """

        opened = set()
        updated = False
        # Check for zones which are entirely within a different, larger zone
        for zone, level in sorted(list(exclusion_zones.items()),
                                  key=lambda pair: len(pair[0]),
                                  reverse=False):
            for other, other_level in sorted(list(exclusion_zones.items()),
                                             key=lambda pair: len(pair[0]),
                                             reverse=True):
                if len(zone) > len(other):
                    # Only perform check when zone is small enough
                    #  to fit inside other
                    break
                elif zone < other:  # set operation: 'Proper-subset' (aka. "zone.issubset(other) and zone != other")
                    exclusion_zones.pop(
                        other)  # Pull out larger group for modification
                    new_zone = frozenset(
                        other - zone)  # Remove smaller group from larger group

                    if other_level - level > 0:
                        # Update # of mines in 'larger' group after removing smaller group;
                        #  Put back modified 'larger' group
                        exclusion_zones[new_zone] = other_level - level
                    else:
                        # OR,
                        #   If the # of mines in the modified group is 0,
                        #   we can safely open all spaces in that group, instead
                        for pos in new_zone:
                            if lookup[pos].hint == '?':
                                lookup[pos].hint = f'{open(*pos)}'
                                opened.add(pos)
                                updated = True
                                if display:
                                    clearscreen()
                                    print(hashmaptostring(
                                        lookup, nrows, ncols))
                                    print()

        for zone, freq in list(exclusion_zones.items()):
            new_zone = zone - opened
            exclusion_zones.pop(zone)
            if new_zone:
                exclusion_zones[new_zone] = freq

        if display:
            clearscreen()
            print(hashmaptostring(lookup, nrows, ncols))
            print()
        return updated

    def open_zeros(display: bool = False) -> None:
        """
        Unveil hints of squares neighboring a square with no surrounding mines.

        :param display: Prints board state after execution if True
        :return: None
        """

        if display:
            print(hashmaptostring(lookup, nrows, ncols), '\n')
        for pos, space in lookup.items():
            if space.hint == '0':
                for neighbor in space.neighbors.values():
                    if neighbor and lookup[neighbor].hint == '?':
                        lookup[neighbor].hint = f"{open(*neighbor)}"
        if display:
            clearscreen()
            print(hashmaptostring(lookup, nrows, ncols))
            print()

    def mark_spaces(display: bool = False) -> int:
        """
        Deduce which squares are holding mines based off visible hints & mark them w/ an 'x'.

        :param display: Prints board state after execution if True
        :return: # of mines found during this invocation
        """

        nfound = 0
        for space in lookup.values():
            if space.hint.isnumeric():
                proximity = int(space.hint)
                if proximity > 0 and proximity == sum(
                        1 for neighbor in space.neighbors.values()
                        if neighbor and lookup[neighbor].hint in 'x?'):
                    for neighbor in space.neighbors.values():
                        if neighbor and lookup[neighbor].hint == '?':
                            lookup[neighbor].hint = 'x'
                            nfound += 1
        if display and nfound:
            clearscreen()
            print(hashmaptostring(lookup, nrows, ncols))
            print()
        return nfound

    def open_safe_spaces(display: bool = False) -> bool:
        """
        Deduce which squares ARE NOT holding mines based off visible hints & 'open' them.

        :param display: Prints board state after execution if True
        :return: True if board state was altered during this invocation
        """

        space_unpacked = False
        for space in lookup.values():
            if space.hint.isnumeric():
                proximity = int(space.hint)
                if proximity > 0 and proximity == sum(
                        1 for neighbor in space.neighbors.values()
                        if neighbor and lookup[neighbor].hint == 'x'):
                    for neighbor in space.neighbors.values():
                        if neighbor and lookup[neighbor].hint == '?':
                            lookup[neighbor].hint = f"{open(*neighbor)}"
                            space_unpacked = True
        if display and space_unpacked:
            clearscreen()
            print(hashmaptostring(lookup, nrows, ncols))
            print()
        return space_unpacked

    def find_safe_spaces(display: bool = False) -> Tuple[bool, int]:
        """
        Use set operations to further deduce which squares ARE NOT holding mines based off visible hints & 'open' them.

        :param display: Prints board state after execution if True
        :return: True if board state was altered + the # of mines marked during this invocation
        """

        zone_found = True
        mine_found = False
        updated = False

        frontier = get_frontier()
        exclusion_zones = get_exclusion_zones(frontier)
        """while not updated:
            updated |= find_by_exclusion_zone(exclusion_zones, display)
            if not updated:
                updated, mine_found = update_zones(exclusion_zones)
                if not updated:
                    break"""
        while zone_found:
            updated |= find_by_exclusion_zone(exclusion_zones, display)
            if len(exclusion_zones) == 3:
                print(end='')
            old_zones = exclusion_zones
            zone_found, mine_found = update_zones(exclusion_zones)
            if old_zones == exclusion_zones:
                print(end='')
            updated |= mine_found
        return updated, mine_found

    board_2d = splitgrid(map)
    nrows, ncols = len(board_2d), len(board_2d[0])
    lookup = boardtohashmap(board_2d)
    nfound = 0
    display = True

    # Start by opening all spaces around those marked '0'
    open_zeros(display)

    while nfound < n:
        updated = False

        # Mark discernible mines
        nmarked = mark_spaces(display)
        updated |= nmarked
        nfound += nmarked

        # Open discernible safe spaces
        updated |= open_safe_spaces(display)

        if not updated:
            # Find & open additional safe spaces using set operations
            if nfound == 82:
                print(end='')
            space_updated, mine_found = find_safe_spaces(display)
            nfound += mine_found
            if space_updated:
                continue

            if display:
                clearscreen()
                print(hashmaptostring(lookup, nrows, ncols))
                print()
            return '?'

    # All mines found; Open all remaining '?'s
    for pos, space in lookup.items():
        if space.hint == '?':
            space.hint = f'{open(*pos)}'

    if display:
        clearscreen()
        print(hashmaptostring(lookup, nrows, ncols))
        print()
    return hashmaptostring(lookup, nrows, ncols)