├── test ├── logic │ ├── __init__.py │ └── test_game.py └── play.py ├── requirements.txt ├── src ├── exceptions.py ├── logic │ ├── engine │ │ ├── base.py │ │ ├── fivefivekernel.py │ │ └── satsolver.py │ ├── player.py │ └── game.py └── trainer │ ├── gene.py │ ├── trainer_config.py │ └── breeder.py ├── README.md ├── LICENSE └── .gitignore /test/logic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.18.5 2 | PyQt5==5.15.0 3 | scipy==1.4.1 4 | tqdm==4.46.1 -------------------------------------------------------------------------------- /src/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidGame(Exception): 2 | def __init__(self, msg, *args): 3 | super().__init__(msg, args) 4 | -------------------------------------------------------------------------------- /src/logic/engine/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class BaseEngine(ABC): 5 | @abstractmethod 6 | def get_move(self, game_instance): 7 | return 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cool Minesweeper written in Python 2 | 3 | This project is mostly for me to test out various solver algorithms. The SAT based solver can reach 40% win rate for traditional 30x16/99 expert boards. 4 | -------------------------------------------------------------------------------- /src/trainer/gene.py: -------------------------------------------------------------------------------- 1 | class Gene: 2 | def __init__(self, kernel, fitness=0): 3 | self.kernel = kernel 4 | self.fitness = fitness 5 | 6 | def __eq__(self, other): 7 | return self.fitness == other.fitness 8 | 9 | def __lt__(self, other): 10 | return self.fitness < other.fitness 11 | -------------------------------------------------------------------------------- /src/trainer/trainer_config.py: -------------------------------------------------------------------------------- 1 | POPULATION = 1000 2 | NUMBER_OF_GENERATIONS = 100 3 | TURN_NUMBER_LIMIT = -1 # Set to -1 for unlimited 4 | NO_OF_THREADS = 250 # MUST DIVIDE POPULATION! 5 | NEW_TRAINING_SESSION = True 6 | STARTING_GENERATION = 0 7 | 8 | # BREEDER SETTINGS 9 | SELECTION_RATE = 0.1 # Two best genes from SELECTION_RATE * population will be chosen to cross over 10 | KILLOFF_RATE = 0.3 # KILLOFF_RATE * POP = no. of genes killed off 11 | MUTATTION_RATE = 0.1 # The probability of a gene getting a mutation 12 | MUTATION_AMOUNT = 2 # The max amount of mutation allowed 13 | -------------------------------------------------------------------------------- /src/logic/engine/fivefivekernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import signal 3 | 4 | from logic.engine.base import BaseEngine 5 | 6 | 7 | class FiveFiveKernelEngine(BaseEngine): 8 | 9 | def __init__(self, kernel): 10 | self.kernel = kernel 11 | 12 | def get_move(self, game_instance): 13 | convolved = signal.convolve2d(game_instance.get_map(), self.kernel, mode='same') 14 | average = signal.convolve2d(game_instance.get_map(), np.ones((5, 5)), mode='same') 15 | convolved[average == 0] = 1 16 | least_risk_masked_idx = convolved[game_instance.opened_map == 0].argmin() 17 | next_move_cell = list(zip(*np.where(game_instance.opened_map == 0)))[least_risk_masked_idx] 18 | return next_move_cell 19 | -------------------------------------------------------------------------------- /test/play.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import numpy as np 4 | 5 | from logic.engine.satsolver import SatSolverEngine 6 | from logic.game import Game 7 | from logic.player import Player 8 | 9 | # logging.basicConfig(level=logging.DEBUG) 10 | np.set_printoptions(linewidth=160) 11 | 12 | count = 0 13 | with open("foo.csv", 'w') as fw: 14 | while True: 15 | count += 1 16 | game = Game(24, 30, 217, (0, 0)) 17 | solver = SatSolverEngine() 18 | player = Player(game, solver) 19 | start = datetime.now() 20 | play = player.play() 21 | if play == 1: 22 | print("Done in {} tries".format(count)) 23 | count = 0 24 | elapsed = (datetime.now() - start).total_seconds() 25 | print("Clear {}% in {}".format(100 * play, elapsed)) 26 | fw.write("{},{}\n".format(play, elapsed)) 27 | -------------------------------------------------------------------------------- /src/logic/player.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from logic.engine.base import BaseEngine 4 | from logic.game import Game 5 | 6 | 7 | class Player: 8 | engine: BaseEngine 9 | game: Game 10 | 11 | def __init__(self, game, engine): 12 | self.game = game 13 | self.engine = engine 14 | 15 | def make_move(self): 16 | next_move_cell = self.engine.get_move(self.game) 17 | fail = False 18 | if not isinstance(next_move_cell, list): 19 | next_move_cell = [next_move_cell] 20 | for _ in next_move_cell: 21 | logging.debug("Making move {}".format(_)) 22 | res = self.game.open_cell(_) 23 | fail = fail and res 24 | return fail 25 | 26 | def play(self): 27 | original_3bvs = self.game.get_3bvs() 28 | while not self.game.check_clear_condition(): 29 | result = self.make_move() 30 | return 1 - self.game.get_3bvs() / original_3bvs 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Felix Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/trainer/breeder.py: -------------------------------------------------------------------------------- 1 | from random import sample, random 2 | 3 | import numpy as np 4 | 5 | from trainer.trainer_config import * 6 | 7 | 8 | class Breeder: 9 | NUMBER_NEED_TO_KILL_OFF = int(KILLOFF_RATE * POPULATION) 10 | 11 | def __init__(self, genes): 12 | self.initialize(genes) 13 | self.cross_over() 14 | self.mutate() 15 | 16 | def initialize(self, genes): 17 | self.genes = genes 18 | self.genes = list(reversed(sorted(self.genes))) 19 | self.new_kernels = [_.kernel for _ in self.genes[:POPULATION - self.NUMBER_NEED_TO_KILL_OFF]] 20 | 21 | def cross_over(self): 22 | for _ in range(self.NUMBER_NEED_TO_KILL_OFF): 23 | selected_genes = list(sample(self.genes, int(SELECTION_RATE * POPULATION))) 24 | sorted(selected_genes) 25 | if selected_genes[0].fitness == 0 and selected_genes[1].fitness == 0: 26 | self.new_kernels.append(selected_genes[0].kernel) 27 | else: 28 | self.new_kernels.append( 29 | (selected_genes[0].kernel * selected_genes[0].fitness + 30 | selected_genes[1].kernel * selected_genes[1].fitness) 31 | / (selected_genes[0].fitness + selected_genes[1].fitness) 32 | ) 33 | 34 | def mutate(self): 35 | for idx, kernel in enumerate(self.new_kernels): 36 | if random() > MUTATTION_RATE: 37 | continue 38 | self.new_kernels[idx] = kernel + np.random.rand(5, 5) * MUTATION_AMOUNT 39 | -------------------------------------------------------------------------------- /test/logic/test_game.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import numpy as np 4 | 5 | from exceptions import InvalidGame 6 | from logic.game import Game 7 | 8 | 9 | class TestGame(TestCase): 10 | def test_initialize_map(self): 11 | def test_no_init_cell(): 12 | for i in range(1, 16): 13 | game = Game(3, 6, i) 14 | self.assertEqual(game.mine_map.sum(), i) 15 | 16 | def test_init_cell(): 17 | for i in range(3): 18 | for j in range(6): 19 | for _ in range(1, 18): 20 | game = Game(3, 6, _, (i, j)) 21 | self.assertEqual(game.mine_map[i, j], 0) 22 | 23 | def test_exceptions(): 24 | # Too few mines 25 | self.assertRaises(InvalidGame, lambda: Game(10, 10, 0)) 26 | self.assertRaises(InvalidGame, lambda: Game(10, 10, -5)) 27 | # Too many mines 28 | self.assertRaises(InvalidGame, lambda: Game(10, 10, 100)) 29 | self.assertRaises(InvalidGame, lambda: Game(10, 10, 150)) 30 | # Init mine outside range 31 | self.assertRaises(InvalidGame, lambda: Game(10, 10, 50, (-5, 5))) 32 | self.assertRaises(InvalidGame, lambda: Game(10, 10, 50, (5, -5))) 33 | self.assertRaises(InvalidGame, lambda: Game(10, 10, 50, (5, 15))) 34 | self.assertRaises(InvalidGame, lambda: Game(10, 10, 50, (15, 5))) 35 | 36 | test_no_init_cell() 37 | test_init_cell() 38 | test_exceptions() 39 | 40 | def test_bfs_number_cell(self): 41 | game = Game(5, 5, 1) 42 | game.mine_map = np.array([ 43 | [1, 1, 0, 0, 1], 44 | [0, 0, 0, 1, 0], 45 | [0, 0, 0, 0, 0], 46 | [1, 0, 0, 0, 1], 47 | [0, 0, 0, 0, 0], 48 | ]) 49 | game.fill_numbers() 50 | expected = {(3, 2), (3, 3), (3, 1), (2, 1), (2, 3), (4, 3), (2, 2), (4, 2), (4, 1)} 51 | actual1 = game.bfs((4, 2)) 52 | actual2 = game.bfs((3, 2)) 53 | self.assertEqual(actual1, actual2) 54 | self.assertEqual(actual1, expected) 55 | 56 | def test_open_cell_and_3bvs(self): 57 | game = Game(5, 5, 1) 58 | game.mine_map = np.array([ 59 | [1, 1, 0, 0, 1], 60 | [0, 0, 0, 1, 0], 61 | [0, 0, 0, 0, 0], 62 | [1, 0, 0, 0, 1], 63 | [0, 0, 0, 0, 0], 64 | ]) 65 | game.fill_numbers() 66 | game.open_cell((4, 2)) 67 | self.assertEqual(game.get_3bvs(), 10) 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ -------------------------------------------------------------------------------- /src/logic/game.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import numpy as np 4 | from scipy import signal 5 | 6 | from exceptions import InvalidGame 7 | 8 | 9 | class CellState(Enum): 10 | UNOPENED = 0 11 | NUMBER = 1 12 | FLAG = 2 13 | 14 | 15 | class Game: 16 | """ 17 | Game object 18 | Contains minimum data to run a game, 0-based index. 19 | For 1D representation, concatenate the rows, iterate by columns. E.g. [[0,1,2],[3,4,5],[6,7,8]] 20 | """ 21 | 22 | def __init__(self, rows, cols, n_mines, init_cell=None): 23 | """ 24 | Ctor 25 | :type init_cell: (row, col) tuple 26 | """ 27 | if not (0 < n_mines < rows * cols): 28 | raise InvalidGame("Invalid value of mines {} for size {}x{}".format(n_mines, rows, cols)) 29 | 30 | self.rows = rows 31 | self.cols = cols 32 | self.n_mines = n_mines 33 | self.mine_map = np.zeros((rows, cols)) 34 | 35 | self.opened_map = np.zeros((rows, cols)) 36 | self.number_map = np.zeros((rows, cols)) 37 | 38 | self.is_done = False 39 | self.is_lost = False 40 | self.is_won = False 41 | 42 | if init_cell is not None: 43 | self.initialize(init_cell) 44 | 45 | def initialize(self, init_cell): 46 | if init_cell is not None: 47 | if not isinstance(init_cell, tuple) \ 48 | or len(init_cell) != 2 \ 49 | or not (0 <= init_cell[0] < self.rows) \ 50 | or not (0 <= init_cell[1] < self.cols): 51 | raise InvalidGame("Invalid init cell ", init_cell) 52 | self.initialize_map(init_cell) 53 | self.fill_numbers() 54 | if init_cell is not None: 55 | self.open_cell(init_cell) 56 | 57 | def initialize_map(self, init_cell=None): 58 | n_sample = self.cols * self.rows if init_cell is None else self.cols * self.rows - 1 59 | sampled_indices = np.random.choice(n_sample, self.n_mines, replace=False).tolist() 60 | if init_cell is not None: 61 | sampled_indices = [(_ + 1 + init_cell[0] * self.cols + init_cell[1]) % (self.cols * self.rows) for _ in 62 | sampled_indices] 63 | for sampled_index in sampled_indices: 64 | sampled_row, sampled_col = np.divmod(sampled_index, self.cols) 65 | self.mine_map[sampled_row, sampled_col] = 1 66 | 67 | def fill_numbers(self): 68 | self.number_map = signal.convolve2d(self.mine_map, 69 | np.array([[1, 1, 1], 70 | [1, 0, 1], 71 | [1, 1, 1]]), 72 | mode='same') 73 | self.number_map[self.mine_map == 1] = -1 74 | 75 | def flag_cell(self, cell): 76 | row, col = cell 77 | self.opened_map[row, col] = CellState.FLAG.value 78 | 79 | def open_cell(self, cell): 80 | row, col = cell 81 | if self.mine_map[row, col] == 1: 82 | self.is_lost = True 83 | self.is_done = True 84 | return True 85 | if self.opened_map[row, col] == 1: 86 | return False 87 | opened_cells = self.bfs(cell) 88 | for opened_cell in opened_cells: 89 | row, col = opened_cell 90 | self.opened_map[row, col] = CellState.NUMBER.value 91 | return False 92 | 93 | def check_clear_condition(self): 94 | if self.is_done: 95 | return True 96 | if self.get_3bvs() == 0: 97 | self.is_won = True 98 | self.is_done = True 99 | return True 100 | return False 101 | 102 | def get_3bvs(self): 103 | unopened_mask = self.opened_map == CellState.UNOPENED.value 104 | non_mine_mask = self.mine_map == 0 105 | merged_mask = np.logical_and(unopened_mask, non_mine_mask) 106 | rows, cols = np.where(merged_mask) 107 | unopened_number_cells = set(zip(rows, cols)) 108 | zeros_unopened_number_cells = { 109 | (row, col) 110 | for (row, col) in unopened_number_cells 111 | if self.number_map[row, col] == 0 112 | } 113 | 114 | threebv_count = 0 115 | cells_opened_with_zeros = set() 116 | while len(zeros_unopened_number_cells) > 0: 117 | cell = zeros_unopened_number_cells.pop() 118 | threebv_count += 1 119 | explored = self.bfs(cell) 120 | cells_opened_with_zeros = cells_opened_with_zeros.union(explored) 121 | zeros_unopened_number_cells = zeros_unopened_number_cells.difference(explored) 122 | unopened_number_cells = unopened_number_cells.difference(cells_opened_with_zeros) 123 | threebv_count += len(unopened_number_cells) 124 | return threebv_count 125 | 126 | def __bfs_internal(self, cell, visited): 127 | visited.add(cell) 128 | row, col = cell 129 | if self.number_map[row, col] == -1: 130 | return {} 131 | if self.number_map[row, col] > 0: 132 | return {cell} 133 | lr = row - 1 if row > 0 else row 134 | rr = row + 1 if row < self.rows - 1 else row 135 | lc = col - 1 if col > 0 else col 136 | rc = col + 1 if col < self.cols - 1 else col 137 | result_set = {cell} 138 | for r in range(lr, rr + 1): 139 | for c in range(lc, rc + 1): 140 | adj_cell = (r, c) 141 | if adj_cell in visited: 142 | continue 143 | result_set = result_set.union(self.__bfs_internal(adj_cell, visited)) 144 | return result_set 145 | 146 | def bfs(self, cell): 147 | return self.__bfs_internal(cell, set()) 148 | 149 | def get_map(self): 150 | return self.opened_map * self.number_map 151 | 152 | def clone(self): 153 | game = Game(self.rows, self.cols, self.n_mines) 154 | game.mine_map = self.mine_map.copy() 155 | game.opened_map = self.opened_map.copy() 156 | game.number_map = self.number_map.copy() 157 | return game 158 | 159 | def __str__(self): 160 | print_map = self.opened_map * self.number_map 161 | print_map[self.opened_map == 0] = np.nan 162 | return str(print_map) 163 | 164 | def save_game(self, filename): 165 | np.save(filename, self.mine_map) 166 | 167 | 168 | if __name__ == '__main__': 169 | game = Game(10, 10, 10, (4, 4)) 170 | # np.savetxt("foo.csv", game.mine_map.astype(int).tolist(), delimiter=",") 171 | game.mine_map = np.genfromtxt("foo.csv", delimiter=',') 172 | game.fill_numbers() 173 | while not game.check_clear_condition(): 174 | print(game) 175 | inp = input("Make your move: ") 176 | inp = inp.split() 177 | row = int(inp[0]) 178 | col = int(inp[1]) 179 | fail = game.open_cell((row, col)) 180 | if fail: 181 | print("Boom, you are dead") 182 | if not fail: 183 | print("You won!") 184 | -------------------------------------------------------------------------------- /src/logic/engine/satsolver.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import operator as op 3 | import random 4 | from functools import reduce 5 | 6 | import numpy as np 7 | from scipy.special import comb 8 | 9 | from logic.engine.base import BaseEngine 10 | from logic.game import Game 11 | 12 | 13 | def ncr(n, r): 14 | r = min(r, n - r) 15 | numer = reduce(op.mul, range(n, n - r, -1), 1) 16 | denom = reduce(op.mul, range(1, r + 1), 1) 17 | return numer / denom 18 | 19 | 20 | class SatSolverEngine(BaseEngine): 21 | 22 | def get_move(self, game_instance: Game): 23 | self.game = game_instance 24 | self.number_map = game_instance.get_map() 25 | self.number_map[game_instance.opened_map == 0] = -1 26 | self.original_number_map = self.number_map.copy() 27 | self.mine_map = np.zeros(self.number_map.shape) 28 | self.unfilled_map = 1 - game_instance.opened_map 29 | self.reduced = None 30 | self.total_unfilled = None 31 | self.nh = game_instance.rows 32 | self.nw = game_instance.cols 33 | 34 | check = True 35 | probabilities = dict() 36 | mine_set = set() 37 | safe_set = set() 38 | mine_dict = dict() 39 | safe_dict = dict() 40 | count = 0 41 | while check: 42 | temp_mine_set, temp_safe_set = self.solve_obvious_tiles() 43 | for _ in temp_mine_set: 44 | mine_set.add(_) 45 | self.mine_map[_[1], _[0]] = 1 46 | self.unfilled_map[_[1], _[0]] = 0 47 | mine_dict[_] = 1 + count * 2 48 | for _ in temp_safe_set: 49 | safe_set.add(_) 50 | self.unfilled_map[_[1], _[0]] = 0 51 | safe_dict[_] = 1 + count * 2 52 | temp_mine_set2, temp_safe_set2, blocks = self.solve_adjacents() 53 | temp_mine_set3, temp_safe_set3 = self.solve_blocks(blocks) 54 | for _ in temp_mine_set2.union(temp_mine_set3): 55 | mine_set.add(_) 56 | self.mine_map[_[1], _[0]] = 1 57 | self.unfilled_map[_[1], _[0]] = 0 58 | mine_dict[_] = 2 + count * 2 59 | for _ in temp_safe_set2.union(temp_safe_set3): 60 | safe_set.add(_) 61 | self.unfilled_map[_[1], _[0]] = 0 62 | safe_dict[_] = 2 + count * 2 63 | check = len(temp_mine_set) != 0 or len(temp_safe_set) != 0 \ 64 | or len(temp_mine_set2) != 0 or len(temp_safe_set2) != 0 \ 65 | or len(temp_mine_set3) != 0 or len(temp_safe_set3) != 0 66 | 67 | if not check and len(safe_set) == 0: 68 | temp_mine_set4, temp_safe_set4, probabilities = self.advanced_solve(blocks) 69 | for _ in temp_mine_set4: 70 | mine_set.add(_) 71 | self.mine_map[_[1], _[0]] = 1 72 | self.unfilled_map[_[1], _[0]] = 0 73 | mine_dict[_] = 10 74 | for _ in temp_safe_set4: 75 | safe_set.add(_) 76 | self.unfilled_map[_[1], _[0]] = 0 77 | safe_dict[_] = 10 78 | if len(temp_safe_set4) > 0 or len(temp_mine_set4) > 0: 79 | check = True 80 | 81 | count += 1 82 | 83 | out = None 84 | if len(safe_dict) == 0: 85 | # Make guess 86 | if len(probabilities) > 0: 87 | min_p = 1 88 | for p_list in probabilities: 89 | for cell, p in p_list.items(): 90 | if min_p > p: 91 | min_p = p 92 | out = [cell] 93 | if out is None: 94 | out = random.sample(list(zip(*np.where(game_instance.opened_map == 0))), 1) 95 | out[0] = (out[0][1], out[0][0]) 96 | else: 97 | out = list(safe_dict) 98 | 99 | for idx, _ in enumerate(out): 100 | out[idx] = _[1], _[0] 101 | return out 102 | 103 | def reduce_map(self): 104 | total_mines = np.zeros((self.nh, self.nw)) 105 | padded_map = np.zeros((self.nh + 2, self.nw + 2)) 106 | if self.mine_map is None: 107 | self.generate_maps() 108 | padded_map[1:self.nh + 1, 1:self.nw + 1] = self.mine_map 109 | for i in range(-1, 2): 110 | for j in range(-1, 2): 111 | if i == 0 and j == 0: 112 | continue 113 | total_mines += padded_map[1 + i:1 + i + self.nh, 1 + j:1 + j + self.nw] 114 | self.reduced = self.number_map.copy() 115 | idx = self.number_map >= 0 116 | self.reduced[idx] = self.reduced[idx] - total_mines[idx] 117 | 118 | def get_total_unfilled(self): 119 | result = np.zeros((self.nh, self.nw)) 120 | padded_map = np.zeros((self.nh + 2, self.nw + 2)) 121 | if self.unfilled_map is None: 122 | self.generate_maps() 123 | padded_map[1:self.nh + 1, 1:self.nw + 1] = self.unfilled_map 124 | for i in range(-1, 2): 125 | for j in range(-1, 2): 126 | if i == 0 and j == 0: 127 | continue 128 | result += padded_map[1 + i:1 + i + self.nh, 1 + j:1 + j + self.nw] 129 | self.total_unfilled = result 130 | 131 | def get_adjacent_cells(self, m, n, filled=False, get_all=False): 132 | if not filled: 133 | check_value = 1 134 | else: 135 | check_value = 0 136 | result = list() 137 | for i in range(-1, 2): 138 | for j in range(-1, 2): 139 | if i == 0 and j == 0: 140 | continue 141 | mm = m + i 142 | nn = n + j 143 | if mm < 0 or mm >= self.nh or nn < 0 or nn >= self.nw: 144 | continue 145 | if self.unfilled_map[mm, nn] == check_value or get_all: 146 | result.append((nn, mm)) 147 | return result 148 | 149 | def solve_obvious_tiles(self): 150 | self.reduce_map() 151 | self.get_total_unfilled() 152 | 153 | mine_set = set() 154 | safe_set = set() 155 | 156 | for i in range(self.nh): 157 | for j in range(self.nw): 158 | if self.reduced[i, j] > 0 and self.reduced[i, j] == self.total_unfilled[i, j]: 159 | for _ in self.get_adjacent_cells(i, j): 160 | mine_set.add(_) 161 | continue 162 | if self.reduced[i, j] == 0 and self.total_unfilled[i, j] > 0: 163 | for _ in self.get_adjacent_cells(i, j): 164 | safe_set.add(_) 165 | continue 166 | return mine_set, safe_set 167 | 168 | def solve_adjacents(self): 169 | self.reduce_map() 170 | self.get_total_unfilled() 171 | xs = np.multiply(self.unfilled_map, np.tile(np.arange(self.nw), (self.nh, 1)))[self.unfilled_map == 1].astype( 172 | int) 173 | ys = np.multiply(self.unfilled_map, np.tile(np.arange(self.nh), (self.nw, 1)).transpose())[ 174 | self.unfilled_map == 1].astype(int) 175 | filled_adjacent_list = [self.get_adjacent_cells(y, x, filled=True) for x, y in zip(xs, ys)] 176 | i_cells = set() 177 | for _ in filled_adjacent_list: 178 | for __ in _: 179 | if self.mine_map[__[1], __[0]] == 1 or self.number_map[__[1], __[0]] == -1: 180 | continue 181 | i_cells.add(__) 182 | i_cells = list(i_cells) 183 | i_unfilled_adjs = {(x, y): set(self.get_adjacent_cells(y, x, filled=False)) for x, y in i_cells} 184 | i_filled_adjs = dict() 185 | for x, y in i_cells: 186 | unfilled_adjacent_cells = i_unfilled_adjs[(x, y)] 187 | temp = set() 188 | for xx, yy in unfilled_adjacent_cells: 189 | temp = temp.union(set(self.get_adjacent_cells(yy, xx, filled=True)).intersection(set(i_cells))) 190 | i_filled_adjs[(x, y)] = temp 191 | 192 | mine_set = set() 193 | safe_set = set() 194 | blocks = list() 195 | 196 | for left, rights in i_filled_adjs.items(): 197 | l_unfilled = i_unfilled_adjs[left] 198 | # Fill blocks 199 | if len(l_unfilled) > self.reduced[left[1], left[0]]: 200 | blocks.append((l_unfilled, self.reduced[left[1], left[0]])) 201 | 202 | for right in rights: 203 | r_unfilled = i_unfilled_adjs[right] 204 | l_diff = l_unfilled.difference(r_unfilled) 205 | r_diff = r_unfilled.difference(l_unfilled) 206 | 207 | # 1n 208 | if self.reduced[left[1], left[0]] == 1 \ 209 | and len(r_diff) == self.reduced[right[1], right[0]] - 1: 210 | for _ in r_diff: 211 | mine_set.add(_) 212 | for _ in l_diff: 213 | safe_set.add(_) 214 | 215 | if len(l_diff) != 0: 216 | continue 217 | diff = self.reduced[right[1], right[0]] - self.reduced[left[1], left[0]] 218 | if diff == 0: 219 | for _ in r_diff: 220 | safe_set.add(_) 221 | elif len(r_diff) == diff: 222 | for _ in r_diff: 223 | mine_set.add(_) 224 | else: 225 | blocks.append((r_diff, diff)) 226 | return mine_set, safe_set, blocks 227 | 228 | def solve_blocks(self, blocks): 229 | mine_set = set() 230 | safe_set = set() 231 | checked_set = [_ for _, __ in blocks] 232 | checked_pair = list() 233 | while True: 234 | clone = blocks.copy() 235 | n = len(checked_set) 236 | for left, left_mines in clone: 237 | for right, right_mines in clone: 238 | if left == right or not right.issubset(left): 239 | continue 240 | if (left, right) in checked_pair: 241 | continue 242 | checked_pair.append((left, right)) 243 | diff_set = left.difference(right) 244 | diff = left_mines - right_mines 245 | if diff == 0 and len(diff_set) > 0: 246 | safe_set = safe_set.union(diff_set) 247 | elif len(diff_set) == diff: 248 | mine_set = mine_set.union(diff_set) 249 | else: 250 | if diff_set not in checked_set: 251 | checked_set.append(diff_set) 252 | blocks.append((diff_set, diff)) 253 | if left not in checked_set: 254 | checked_set.append(left) 255 | if len(checked_set) == n: 256 | break 257 | return mine_set, safe_set 258 | 259 | def advanced_solve(self, blocks): 260 | blocks = list(blocks) 261 | big_blocks = [_[0] for _ in blocks] 262 | blocks_list = [[_] for _ in blocks] 263 | while True: 264 | temp = list() 265 | temp_block_list = list() 266 | for idx0, cells in enumerate(big_blocks): 267 | flag = False 268 | for idx, big_block in enumerate(temp): 269 | if len(big_block.intersection(cells)) > 0: 270 | temp[idx] = temp[idx].union(cells) 271 | temp_block_list[idx].extend(blocks_list[idx0]) 272 | flag = True 273 | break 274 | if not flag: 275 | temp.append(cells) 276 | temp_block_list.append(blocks_list[idx0]) 277 | blocks_list = temp_block_list 278 | if len(big_blocks) == len(temp): 279 | break 280 | big_blocks = temp 281 | total_mine_set = set() 282 | total_safe_set = set() 283 | all_probabilities = list() 284 | for big_block, sub_blocks in zip(big_blocks, blocks_list): 285 | mine_set, safe_set, probabilities = self.evaluate_all_possibilities(big_block, sub_blocks) 286 | total_mine_set = total_mine_set.union(mine_set) 287 | total_safe_set = total_safe_set.union(safe_set) 288 | all_probabilities.append(probabilities) 289 | return total_mine_set, total_safe_set, all_probabilities 290 | 291 | def evaluate_all_possibilities(self, big_block, blocks): 292 | TESTS = 1000 293 | 294 | all_cells = list() 295 | for block, _ in blocks: 296 | for cell in block: 297 | if cell not in all_cells: 298 | all_cells.append(cell) 299 | idx_dict = {cell: idx for idx, cell in enumerate(all_cells)} 300 | reverse_idx_dict = {idx: cell for idx, cell in enumerate(all_cells)} 301 | blocks = [([idx_dict[cell] for cell in block], int(mines)) for block, mines in blocks] 302 | big_block = {idx_dict[cell] for cell in big_block} 303 | 304 | min_possibilities = float('inf') 305 | min_partitions = list() 306 | min_loose_cells = set() 307 | for _ in range(TESTS): 308 | permutation = np.random.permutation(blocks) 309 | partitions = list() 310 | clone = set(big_block) 311 | for cells, mines in permutation: 312 | if set(cells).issubset(clone): 313 | clone = clone.difference(cells) 314 | partitions.append((cells, mines)) 315 | loose_cells = clone 316 | n_possibilities = 2 ** len(loose_cells) 317 | for group, mine_count in partitions: 318 | n_possibilities *= ncr(len(group), int(mine_count)) 319 | if n_possibilities < min_possibilities: 320 | min_possibilities = n_possibilities 321 | min_partitions = partitions 322 | min_loose_cells = loose_cells 323 | 324 | if min_loose_cells is None or min_partitions is None: 325 | return set(), set(), dict() 326 | 327 | if min_possibilities > 1500000: 328 | return set(), set(), dict() 329 | 330 | if len(min_partitions) > 0: 331 | choices = [itertools.combinations(*partition) for partition in min_partitions] 332 | else: 333 | choices = list() 334 | temp = list() 335 | for i in range(len(min_loose_cells) + 1): 336 | temp.extend(itertools.combinations(min_loose_cells, i)) 337 | choices.append(temp) 338 | choices = itertools.product(*choices) 339 | choices = list(choices) 340 | if min_possibilities > 200000: 341 | min_possibilities = 200000 342 | choices = random.sample(choices, min_possibilities) 343 | 344 | for idx, choice in enumerate(choices): 345 | temp = list() 346 | for _ in choice: 347 | temp.extend(_) 348 | choices[idx] = temp 349 | simulation_array = np.zeros((int(min_possibilities), len(all_cells) + len(blocks) + 1)) 350 | for idx, choice in enumerate(choices): 351 | simulation_array[idx, choice] = 1 352 | for idx, (block, mines) in enumerate(blocks): 353 | simulation_array[:, len(all_cells) + idx] = simulation_array[:, block].sum(axis=1) == mines 354 | simulation_array[:, -1] = np.all(simulation_array[:, len(all_cells):len(all_cells) + len(blocks)], axis=1) 355 | possible_outcomes = simulation_array[simulation_array[:, -1] == 1, :len(all_cells)] 356 | mine_sum = possible_outcomes.sum(axis=1) 357 | unique_mine_sum, unique_mine_sum_count = np.unique(mine_sum, return_counts=True) 358 | unfilled_cell = self.unfilled_map.sum() - len(big_block) 359 | mine_left = self.game.n_mines - self.game.mine_map.sum() 360 | if mine_left <= 0: 361 | unique_mine_sum_max = unique_mine_sum.max() 362 | mine_left = int((unfilled_cell - unique_mine_sum_max) * 0.25 + unique_mine_sum_max) 363 | comb_list = [comb(unfilled_cell, mine_left - _) for _ in unique_mine_sum] 364 | comb_dict = {_: __ for _, __ in zip(unique_mine_sum, comb_list)} 365 | total_possibilities = 0 366 | for out_comb, in_comb in zip(comb_list, unique_mine_sum_count): 367 | total_possibilities += out_comb * in_comb 368 | possible_outcomes *= np.array([comb_dict[_] for _ in mine_sum])[:, None] 369 | possible_outcomes /= total_possibilities 370 | 371 | predictions = np.sum(possible_outcomes, axis=0) 372 | mine_indices = np.arange(len(all_cells))[predictions == 1] 373 | safe_indices = np.arange(len(all_cells))[predictions == 0] 374 | 375 | if min_possibilities != 200000: 376 | mine_set = {reverse_idx_dict[idx] for idx in mine_indices} 377 | safe_set = {reverse_idx_dict[idx] for idx in safe_indices} 378 | probabilities = { 379 | reverse_idx_dict[idx]: predictions[idx] 380 | for idx in np.arange(len(all_cells))[np.logical_and(predictions > 0, predictions < 1)] 381 | } 382 | else: 383 | mine_set = set() 384 | safe_set = set() 385 | probabilities = { 386 | reverse_idx_dict[idx]: predictions[idx] 387 | for idx in range(len(all_cells)) 388 | } 389 | return mine_set, safe_set, probabilities 390 | --------------------------------------------------------------------------------