multi-puzzle-solver 0.9.14__tar.gz → 0.9.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of multi-puzzle-solver might be problematic. Click here for more details.
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/PKG-INFO +1 -1
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/multi_puzzle_solver.egg-info/PKG-INFO +1 -1
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/multi_puzzle_solver.egg-info/SOURCES.txt +3 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/__init__.py +2 -1
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/core/utils_ortools.py +3 -1
- multi_puzzle_solver-0.9.15/src/puzzle_solver/puzzles/galaxies/galaxies.py +110 -0
- multi_puzzle_solver-0.9.15/src/puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +216 -0
- multi_puzzle_solver-0.9.15/tests/test_galaxies.py +84 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/README.md +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/pyproject.toml +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/setup.cfg +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/multi_puzzle_solver.egg-info/dependency_links.txt +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/multi_puzzle_solver.egg-info/requires.txt +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/multi_puzzle_solver.egg-info/top_level.txt +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/core/utils.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/aquarium/aquarium.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/battleships/battleships.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/black_box/black_box.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/bridges/bridges.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/chess_range/chess_melee.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/chess_range/chess_range.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/chess_range/chess_solo.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/dominosa/dominosa.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/filling/filling.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/guess/guess.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/inertia/inertia.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/inertia/parse_map/parse_map.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/inertia/tsp.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/kakurasu/kakurasu.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/keen/keen.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/light_up/light_up.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/lits/lits.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/magnets/magnets.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/map/map.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/minesweeper/minesweeper.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/mosaic/mosaic.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/nonograms/nonograms.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/pearl/pearl.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/range/range.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/signpost/signpost.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/singles/singles.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/star_battle/star_battle.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/star_battle/star_battle_shapeless.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/stitches/parse_map/parse_map.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/stitches/stitches.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/sudoku/sudoku.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/tents/tents.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/thermometers/thermometers.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/towers/towers.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/tracks/tracks.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/undead/undead.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/unruly/unruly.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/utils/visualizer.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_aquarium.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_battleships.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_black_box.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_bridges.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_chess_melee.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_chess_range.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_chess_solo.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_dominosa.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_filling.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_guess.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_inertia.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_kakurasu.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_keen.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_light_up.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_lits.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_magnets.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_map.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_minesweeper.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_mosaic.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_nonograms.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_pearl.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_range.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_signpost.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_singles.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_star_battle.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_stitches.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_sudoku.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_tents.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_thermometers.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_towers.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_tracks.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_undead.py +0 -0
- {multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/tests/test_unruly.py +0 -0
|
@@ -17,6 +17,8 @@ src/puzzle_solver/puzzles/chess_range/chess_range.py
|
|
|
17
17
|
src/puzzle_solver/puzzles/chess_range/chess_solo.py
|
|
18
18
|
src/puzzle_solver/puzzles/dominosa/dominosa.py
|
|
19
19
|
src/puzzle_solver/puzzles/filling/filling.py
|
|
20
|
+
src/puzzle_solver/puzzles/galaxies/galaxies.py
|
|
21
|
+
src/puzzle_solver/puzzles/galaxies/parse_map/parse_map.py
|
|
20
22
|
src/puzzle_solver/puzzles/guess/guess.py
|
|
21
23
|
src/puzzle_solver/puzzles/inertia/inertia.py
|
|
22
24
|
src/puzzle_solver/puzzles/inertia/tsp.py
|
|
@@ -55,6 +57,7 @@ tests/test_chess_range.py
|
|
|
55
57
|
tests/test_chess_solo.py
|
|
56
58
|
tests/test_dominosa.py
|
|
57
59
|
tests/test_filling.py
|
|
60
|
+
tests/test_galaxies.py
|
|
58
61
|
tests/test_guess.py
|
|
59
62
|
tests/test_inertia.py
|
|
60
63
|
tests/test_kakurasu.py
|
|
@@ -7,6 +7,7 @@ from puzzle_solver.puzzles.chess_range import chess_solo as chess_solo_solver
|
|
|
7
7
|
from puzzle_solver.puzzles.chess_range import chess_melee as chess_melee_solver
|
|
8
8
|
from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
|
|
9
9
|
from puzzle_solver.puzzles.filling import filling as filling_solver
|
|
10
|
+
from puzzle_solver.puzzles.galaxies import galaxies as galaxies_solver
|
|
10
11
|
from puzzle_solver.puzzles.guess import guess as guess_solver
|
|
11
12
|
from puzzle_solver.puzzles.inertia import inertia as inertia_solver
|
|
12
13
|
from puzzle_solver.puzzles.kakurasu import kakurasu as kakurasu_solver
|
|
@@ -35,4 +36,4 @@ from puzzle_solver.puzzles.unruly import unruly as unruly_solver
|
|
|
35
36
|
|
|
36
37
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
|
37
38
|
|
|
38
|
-
__version__ = '0.9.
|
|
39
|
+
__version__ = '0.9.15'
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/core/utils_ortools.py
RENAMED
|
@@ -97,6 +97,8 @@ def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any,
|
|
|
97
97
|
|
|
98
98
|
vs = vars_to_force
|
|
99
99
|
v_count = len(vs)
|
|
100
|
+
if v_count <= 2: # graph must have at least 3 nodes to possibly be disconnected
|
|
101
|
+
return {}
|
|
100
102
|
# =V model variables, one for each variable
|
|
101
103
|
is_root: dict[Pos, cp_model.IntVar] = {} # =V, defines the unique root
|
|
102
104
|
prefix_zero: dict[Pos, cp_model.IntVar] = {} # =V, used for picking the unique root
|
|
@@ -128,7 +130,7 @@ def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any,
|
|
|
128
130
|
for p in keys_in_order:
|
|
129
131
|
and_constraint(model, is_root[p], [vs[p], prefix_zero[p]])
|
|
130
132
|
# Exactly one root:
|
|
131
|
-
model.Add(sum(is_root.values())
|
|
133
|
+
model.Add(sum(is_root.values()) <= 1)
|
|
132
134
|
|
|
133
135
|
# For each node i, consider only neighbors
|
|
134
136
|
for i, pi in enumerate(keys_in_order):
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from typing import Iterable, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from ortools.sat.python import cp_model
|
|
6
|
+
|
|
7
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, Direction, get_next_pos, in_bounds, get_opposite_direction, get_pos
|
|
8
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, or_constraint, force_connected_component
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_numpy(galaxies: np.ndarray) -> list[tuple[Pos, ...]]:
|
|
12
|
+
result = defaultdict(list)
|
|
13
|
+
for pos, arr_id in np.ndenumerate(galaxies):
|
|
14
|
+
if not arr_id.strip():
|
|
15
|
+
continue
|
|
16
|
+
result[arr_id].append(get_pos(x=pos[1], y=pos[0]))
|
|
17
|
+
return [positions for _, positions in sorted(result.items(), key=lambda x: x[0])]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Board:
|
|
21
|
+
def __init__(self, galaxies: Union[list[tuple[Pos, ...]], np.ndarray], V: int = None, H: int = None):
|
|
22
|
+
if isinstance(galaxies, np.ndarray):
|
|
23
|
+
V, H = galaxies.shape
|
|
24
|
+
galaxies = parse_numpy(galaxies)
|
|
25
|
+
else:
|
|
26
|
+
assert V is not None and H is not None, 'V and H must be provided if galaxies is not a numpy array'
|
|
27
|
+
assert V >= 1 and H >= 1, 'V and H must be at least 1'
|
|
28
|
+
assert all(isinstance(galaxy, Iterable) for galaxy in galaxies), 'galaxies must be a list of Iterables'
|
|
29
|
+
assert all(len(galaxy) in [1, 2, 4] for galaxy in galaxies), 'each galaxy must be exactly 1, 2, or 4 positions'
|
|
30
|
+
self.V = V
|
|
31
|
+
self.H = H
|
|
32
|
+
self.n_galaxies = len(galaxies)
|
|
33
|
+
self.galaxies = galaxies
|
|
34
|
+
self.prelocated_positions: set[Pos] = {pos: i for i, galaxy in enumerate(galaxies) for pos in galaxy}
|
|
35
|
+
|
|
36
|
+
self.model = cp_model.CpModel()
|
|
37
|
+
self.pos_to_galaxy: dict[Pos, dict[int, cp_model.IntVar]] = {p: {} for p in get_all_pos(V, H)} # each position can be part of exactly one out of many possible galaxies
|
|
38
|
+
self.allocated_pairs: set[tuple[Pos, Pos]] = set() # each pair is allocated to exactly one galaxy
|
|
39
|
+
|
|
40
|
+
self.create_vars()
|
|
41
|
+
self.add_all_constraints()
|
|
42
|
+
|
|
43
|
+
def create_vars(self):
|
|
44
|
+
for i in range(self.n_galaxies):
|
|
45
|
+
galaxy = self.galaxies[i]
|
|
46
|
+
if len(galaxy) == 1:
|
|
47
|
+
p1, p2 = galaxy[0], galaxy[0]
|
|
48
|
+
elif len(galaxy) == 2:
|
|
49
|
+
p1, p2 = galaxy[0], galaxy[1]
|
|
50
|
+
elif len(galaxy) == 4:
|
|
51
|
+
p1, p2 = galaxy[0], galaxy[3] # [1] and [2] will be linked with symmetry
|
|
52
|
+
self.expand_galaxy(p1, p2, i)
|
|
53
|
+
|
|
54
|
+
def expand_galaxy(self, p1: Pos, p2: Pos, galaxy_idx: int):
|
|
55
|
+
if (p1, p2) in self.allocated_pairs or (p2, p1) in self.allocated_pairs:
|
|
56
|
+
return
|
|
57
|
+
if p1 in self.prelocated_positions and self.prelocated_positions[p1] != galaxy_idx:
|
|
58
|
+
return
|
|
59
|
+
if p2 in self.prelocated_positions and self.prelocated_positions[p2] != galaxy_idx:
|
|
60
|
+
return
|
|
61
|
+
if not in_bounds(p1, self.V, self.H) or not in_bounds(p2, self.V, self.H):
|
|
62
|
+
return
|
|
63
|
+
self.bind_pair(p1, p2, galaxy_idx)
|
|
64
|
+
# symmetrically expand the galaxy until illegal position is hit
|
|
65
|
+
for direction in [Direction.RIGHT, Direction.UP, Direction.DOWN, Direction.LEFT]:
|
|
66
|
+
symmetrical_direction = get_opposite_direction(direction)
|
|
67
|
+
new_p1 = get_next_pos(p1, direction)
|
|
68
|
+
new_p2 = get_next_pos(p2, symmetrical_direction)
|
|
69
|
+
self.expand_galaxy(new_p1, new_p2, galaxy_idx)
|
|
70
|
+
|
|
71
|
+
def bind_pair(self, p1: Pos, p2: Pos, galaxy_idx: int):
|
|
72
|
+
assert galaxy_idx not in self.pos_to_galaxy[p1], f'p1={p1} already has galaxy idx={galaxy_idx}'
|
|
73
|
+
assert galaxy_idx not in self.pos_to_galaxy[p2], f'p2={p2} already has galaxy idx={galaxy_idx}'
|
|
74
|
+
self.allocated_pairs.add((p1, p2))
|
|
75
|
+
v1 = self.model.NewBoolVar(f'{p1}:{galaxy_idx}')
|
|
76
|
+
v2 = self.model.NewBoolVar(f'{p2}:{galaxy_idx}')
|
|
77
|
+
self.model.Add(v1 == v2)
|
|
78
|
+
self.pos_to_galaxy[p1][galaxy_idx] = v1
|
|
79
|
+
self.pos_to_galaxy[p2][galaxy_idx] = v2
|
|
80
|
+
|
|
81
|
+
def add_all_constraints(self):
|
|
82
|
+
galaxy_vars = {}
|
|
83
|
+
for pos in get_all_pos(self.V, self.H):
|
|
84
|
+
pos_vars = list(self.pos_to_galaxy[pos].values())
|
|
85
|
+
self.model.AddExactlyOne(pos_vars)
|
|
86
|
+
for galaxy_idx, v in self.pos_to_galaxy[pos].items():
|
|
87
|
+
galaxy_vars.setdefault(galaxy_idx, {})[pos] = v
|
|
88
|
+
for galaxy_idx, pos_vars in galaxy_vars.items():
|
|
89
|
+
force_connected_component(self.model, pos_vars)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def solve_and_print(self, verbose: bool = True):
|
|
93
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
94
|
+
assignment: dict[Pos, int] = {}
|
|
95
|
+
for pos, galaxy_vars in board.pos_to_galaxy.items():
|
|
96
|
+
for galaxy_idx, var in galaxy_vars.items(): # every pos is part of exactly one galaxy
|
|
97
|
+
if solver.Value(var) == 1:
|
|
98
|
+
assignment[pos] = galaxy_idx
|
|
99
|
+
break
|
|
100
|
+
return SingleSolution(assignment=assignment)
|
|
101
|
+
def callback(single_res: SingleSolution):
|
|
102
|
+
print("Solution found")
|
|
103
|
+
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
104
|
+
for pos in get_all_pos(self.V, self.H):
|
|
105
|
+
set_char(res, pos, str(single_res.assignment[pos]).zfill(2))
|
|
106
|
+
print('[')
|
|
107
|
+
for row in range(self.V):
|
|
108
|
+
print(' ', res[row].tolist(), end=',\n')
|
|
109
|
+
print(']')
|
|
110
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file is a simple helper that parses the images from https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/inertia.html and converts them to a json file.
|
|
3
|
+
Look at the ./input_output/ directory for examples of input images and output json files.
|
|
4
|
+
The output json is used in the test_solve.py file to test the solver.
|
|
5
|
+
"""
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import numpy as np
|
|
8
|
+
cv = None
|
|
9
|
+
Image = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_lines(bw):
|
|
13
|
+
# Create the images that will use to extract the horizontal and vertical lines
|
|
14
|
+
horizontal = np.copy(bw)
|
|
15
|
+
vertical = np.copy(bw)
|
|
16
|
+
|
|
17
|
+
cols = horizontal.shape[1]
|
|
18
|
+
horizontal_size = cols // 5
|
|
19
|
+
# Create structure element for extracting horizontal lines through morphology operations
|
|
20
|
+
horizontalStructure = cv.getStructuringElement(cv.MORPH_RECT, (horizontal_size, 1))
|
|
21
|
+
horizontal = cv.erode(horizontal, horizontalStructure)
|
|
22
|
+
horizontal = cv.dilate(horizontal, horizontalStructure)
|
|
23
|
+
horizontal_means = np.mean(horizontal, axis=1)
|
|
24
|
+
horizontal_cutoff = np.percentile(horizontal_means, 50)
|
|
25
|
+
# location where the horizontal lines are
|
|
26
|
+
horizontal_idx = np.where(horizontal_means > horizontal_cutoff)[0]
|
|
27
|
+
# print(f"horizontal_idx: {horizontal_idx}")
|
|
28
|
+
height = len(horizontal_idx)
|
|
29
|
+
# show_wait_destroy("horizontal", horizontal) # this has the horizontal lines
|
|
30
|
+
|
|
31
|
+
rows = vertical.shape[0]
|
|
32
|
+
verticalsize = rows // 5
|
|
33
|
+
# Create structure element for extracting vertical lines through morphology operations
|
|
34
|
+
verticalStructure = cv.getStructuringElement(cv.MORPH_RECT, (1, verticalsize))
|
|
35
|
+
vertical = cv.erode(vertical, verticalStructure)
|
|
36
|
+
vertical = cv.dilate(vertical, verticalStructure)
|
|
37
|
+
vertical_means = np.mean(vertical, axis=0)
|
|
38
|
+
vertical_cutoff = np.percentile(vertical_means, 50)
|
|
39
|
+
vertical_idx = np.where(vertical_means > vertical_cutoff)[0]
|
|
40
|
+
# print(f"vertical_idx: {vertical_idx}")
|
|
41
|
+
width = len(vertical_idx)
|
|
42
|
+
# print(f"height: {height}, width: {width}")
|
|
43
|
+
# print(f"vertical_means: {vertical_means}")
|
|
44
|
+
# show_wait_destroy("vertical", vertical) # this has the vertical lines
|
|
45
|
+
|
|
46
|
+
vertical = cv.bitwise_not(vertical)
|
|
47
|
+
# show_wait_destroy("vertical_bit", vertical)
|
|
48
|
+
|
|
49
|
+
return horizontal_idx, vertical_idx
|
|
50
|
+
|
|
51
|
+
def show_wait_destroy(winname, img):
|
|
52
|
+
cv.imshow(winname, img)
|
|
53
|
+
cv.moveWindow(winname, 500, 0)
|
|
54
|
+
cv.waitKey(0)
|
|
55
|
+
cv.destroyWindow(winname)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def mean_consecutives(arr: np.ndarray) -> np.ndarray:
|
|
59
|
+
"""if a sequence of values is consecutive, then average the values"""
|
|
60
|
+
sums = []
|
|
61
|
+
counts = []
|
|
62
|
+
for i in range(len(arr)):
|
|
63
|
+
if i == 0:
|
|
64
|
+
sums.append(arr[i])
|
|
65
|
+
counts.append(1)
|
|
66
|
+
elif arr[i] == arr[i-1] + 1:
|
|
67
|
+
sums[-1] += arr[i]
|
|
68
|
+
counts[-1] += 1
|
|
69
|
+
else:
|
|
70
|
+
sums.append(arr[i])
|
|
71
|
+
counts.append(1)
|
|
72
|
+
return np.array(sums) // np.array(counts)
|
|
73
|
+
|
|
74
|
+
def main(image):
|
|
75
|
+
global Image
|
|
76
|
+
global cv
|
|
77
|
+
import matplotlib.pyplot as plt
|
|
78
|
+
from PIL import Image as Image_module
|
|
79
|
+
import cv2 as cv_module
|
|
80
|
+
Image = Image_module
|
|
81
|
+
cv = cv_module
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
image_path = Path(image)
|
|
85
|
+
output_path = image_path.parent / (image_path.stem + '.json')
|
|
86
|
+
src = cv.imread(image, cv.IMREAD_COLOR)
|
|
87
|
+
assert src is not None, f'Error opening image: {image}'
|
|
88
|
+
if len(src.shape) != 2:
|
|
89
|
+
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
|
|
90
|
+
else:
|
|
91
|
+
gray = src
|
|
92
|
+
# now the image is in grayscale
|
|
93
|
+
|
|
94
|
+
# Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
|
|
95
|
+
gray = cv.bitwise_not(gray)
|
|
96
|
+
bw = cv.adaptiveThreshold(gray.copy(), 255, cv.ADAPTIVE_THRESH_MEAN_C, \
|
|
97
|
+
cv.THRESH_BINARY, 15, -2)
|
|
98
|
+
# show_wait_destroy("binary", bw)
|
|
99
|
+
|
|
100
|
+
# show_wait_destroy("src", src)
|
|
101
|
+
horizontal_idx, vertical_idx = extract_lines(bw)
|
|
102
|
+
horizontal_idx = mean_consecutives(horizontal_idx)
|
|
103
|
+
vertical_idx = mean_consecutives(vertical_idx)
|
|
104
|
+
height = len(horizontal_idx)
|
|
105
|
+
width = len(vertical_idx)
|
|
106
|
+
print(f"height: {height}, width: {width}")
|
|
107
|
+
print(f"horizontal_idx: {horizontal_idx}")
|
|
108
|
+
print(f"vertical_idx: {vertical_idx}")
|
|
109
|
+
arr = np.zeros((height - 1, width - 1), dtype=object)
|
|
110
|
+
output = {(dx, dy): arr.copy() for dx in [-1, 0, 1] for dy in [-1, 0, 1]}
|
|
111
|
+
hists = {(dx, dy): {} for dx in [-1, 0, 1] for dy in [-1, 0, 1]}
|
|
112
|
+
for j in range(height - 1):
|
|
113
|
+
for i in range(width - 1):
|
|
114
|
+
hidx1, hidx2 = horizontal_idx[j], horizontal_idx[j+1]
|
|
115
|
+
vidx1, vidx2 = vertical_idx[i], vertical_idx[i+1]
|
|
116
|
+
hidx1 = max(0, hidx1 - 2)
|
|
117
|
+
hidx2 = min(src.shape[0], hidx2 + 4)
|
|
118
|
+
vidx1 = max(0, vidx1 - 2)
|
|
119
|
+
vidx2 = min(src.shape[1], vidx2 + 4)
|
|
120
|
+
cell = src[hidx1:hidx2, vidx1:vidx2]
|
|
121
|
+
mid_x = cell.shape[1] // 2
|
|
122
|
+
mid_y = cell.shape[0] // 2
|
|
123
|
+
cell = cv.bitwise_not(cell) # invert colors
|
|
124
|
+
for dx in [-1, 0, 1]:
|
|
125
|
+
for dy in [-1, 0, 1]:
|
|
126
|
+
mx = mid_x + dx*mid_x
|
|
127
|
+
my = mid_y + dy*mid_y
|
|
128
|
+
mx0 = max(0, mx - 5)
|
|
129
|
+
mx1 = min(cell.shape[1], mx + 5)
|
|
130
|
+
my0 = max(0, my - 5)
|
|
131
|
+
my1 = min(cell.shape[0], my + 5)
|
|
132
|
+
cell_part = cell[my0:my1, mx0:mx1]
|
|
133
|
+
hists[(dx, dy)][j, i] = np.sum(cell_part)
|
|
134
|
+
# top = cell[0:10, mid_y-5:mid_y+5]
|
|
135
|
+
# hists['top'][j, i] = np.sum(top)
|
|
136
|
+
# left = cell[mid_x-5:mid_x+5, 0:10]
|
|
137
|
+
# hists['left'][j, i] = np.sum(left)
|
|
138
|
+
# right = cell[mid_x-5:mid_x+5, -10:]
|
|
139
|
+
# hists['right'][j, i] = np.sum(right)
|
|
140
|
+
# bottom = cell[-10:, mid_y-5:mid_y+5]
|
|
141
|
+
# hists['bottom'][j, i] = np.sum(bottom)
|
|
142
|
+
# print(f"cell_{i}_{j}, ", [hists[(dx, dy)][j, i] for dx in [-1, 0, 1] for dy in [-1, 0, 1]])
|
|
143
|
+
# show_wait_destroy(f"cell_{i}_{j}", cell)
|
|
144
|
+
|
|
145
|
+
fig, axs = plt.subplots(3, 3)
|
|
146
|
+
target = 100
|
|
147
|
+
for dx in [-1, 0, 1]:
|
|
148
|
+
for dy in [-1, 0, 1]:
|
|
149
|
+
axs[dx+1, dy+1].hist(list(hists[(dx, dy)].values()), bins=100)
|
|
150
|
+
axs[dx+1, dy+1].set_title(f'{dx},{dy}')
|
|
151
|
+
# target = np.mean(list(hists[(dx, dy)].values()))
|
|
152
|
+
axs[dx+1, dy+1].axvline(target, color='red')
|
|
153
|
+
# plt.show()
|
|
154
|
+
# 1/0
|
|
155
|
+
for j in range(height - 1):
|
|
156
|
+
for i in range(width - 1):
|
|
157
|
+
sums_str = ''
|
|
158
|
+
out_str = ''
|
|
159
|
+
for dx in [-1, 0, 1]:
|
|
160
|
+
out_xpart = 'L' if dx == -1 else 'C' if dx == 0 else 'R'
|
|
161
|
+
for dy in [-1, 0, 1]:
|
|
162
|
+
out_ypart = 'T' if dy == -1 else 'C' if dy == 0 else 'B'
|
|
163
|
+
sums_str += str(hists[(dx, dy)][j, i]) + ' '
|
|
164
|
+
if hists[(dx, dy)][j, i] < target:
|
|
165
|
+
out_str += (out_xpart + out_ypart + ' ')
|
|
166
|
+
output[(dx, dy)][j, i] = 1
|
|
167
|
+
print(f"cell_{j}_{i}", end=': ')
|
|
168
|
+
print(out_str)
|
|
169
|
+
print(' Sums: ', sums_str)
|
|
170
|
+
|
|
171
|
+
out = np.full_like(output[(0, 0)], ' ', dtype='U2')
|
|
172
|
+
counter = 0
|
|
173
|
+
for j in range(out.shape[0]):
|
|
174
|
+
for i in range(out.shape[1]):
|
|
175
|
+
for dx in [-1, 0, 1]:
|
|
176
|
+
for dy in [-1, 0, 1]:
|
|
177
|
+
if output[(dx, dy)][j, i] == 1:
|
|
178
|
+
# out[j, i] = dxdy_to_char[(dx, dy)]
|
|
179
|
+
if dx == 0 and dy == 0: # single point
|
|
180
|
+
out[j, i] = str(counter).zfill(2)
|
|
181
|
+
counter += 1
|
|
182
|
+
elif dx == 0 and dy == 1: # vertical
|
|
183
|
+
out[j, i] = str(counter).zfill(2)
|
|
184
|
+
out[j+1, i] = str(counter).zfill(2)
|
|
185
|
+
counter += 1
|
|
186
|
+
elif dx == 1 and dy == 0: # horizontal
|
|
187
|
+
out[j, i] = str(counter).zfill(2)
|
|
188
|
+
out[j, i+1] = str(counter).zfill(2)
|
|
189
|
+
counter += 1
|
|
190
|
+
elif dx == 1 and dy == 1: # 2 by 2
|
|
191
|
+
out[j, i] = str(counter).zfill(2)
|
|
192
|
+
out[j+1, i] = str(counter).zfill(2)
|
|
193
|
+
out[j, i+1] = str(counter).zfill(2)
|
|
194
|
+
out[j+1, i+1] = str(counter).zfill(2)
|
|
195
|
+
counter += 1
|
|
196
|
+
|
|
197
|
+
# print(out)
|
|
198
|
+
with open(output_path, 'w') as f:
|
|
199
|
+
f.write('[\n')
|
|
200
|
+
for i, row in enumerate(out):
|
|
201
|
+
f.write(' ' + str(row.tolist()).replace("'", '"'))
|
|
202
|
+
if i != len(out) - 1:
|
|
203
|
+
f.write(',')
|
|
204
|
+
f.write('\n')
|
|
205
|
+
f.write(']')
|
|
206
|
+
print('output json: ', output_path)
|
|
207
|
+
|
|
208
|
+
if __name__ == '__main__':
|
|
209
|
+
# to run this script and visualize the output, in the root run:
|
|
210
|
+
# python .\src\puzzle_solver\puzzles\galaxies\parse_map\parse_map.py | python .\src\puzzle_solver\utils\visualizer.py --read_stdin
|
|
211
|
+
# main(Path(__file__).parent / 'input_output' / 'MTM6OSw4MjEsNDAx.png')
|
|
212
|
+
# main(Path(__file__).parent / 'input_output' / 'weekly_oct_3rd_2025.png')
|
|
213
|
+
# main(Path(__file__).parent / 'input_output' / 'star_battle_67f73ff90cd8cdb4b3e30f56f5261f4968f5dac940bc6.png')
|
|
214
|
+
# main(Path(__file__).parent / 'input_output' / 'LITS_MDoxNzksNzY3.png')
|
|
215
|
+
# main(Path(__file__).parent / 'input_output' / 'lits_OTo3LDMwNiwwMTU=.png')
|
|
216
|
+
main(Path(__file__).parent / 'input_output' / 'eofodowmumgzzdkopzlpzkzaezrhefoezejvdtxrzmpgozzemxjdcigcqzrk.png')
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from puzzle_solver import galaxies_solver as solver
|
|
4
|
+
from puzzle_solver.core.utils import get_pos
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_ground():
|
|
8
|
+
# 15 x 15 unreasonable
|
|
9
|
+
# https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/galaxies.html#15x15:eofodowmumgzzdkopzlpzkzaezrhefoezejvdtxrzmpgozzemxjdcigcqzrk
|
|
10
|
+
galaxies = np.array([
|
|
11
|
+
[' ', ' ', '00', ' ', ' ', '01', '01', '02', '02', '03', '03', ' ', '04', '04', ' '],
|
|
12
|
+
['05', '05', ' ', ' ', '06', '01', '01', '02', '02', ' ', ' ', ' ', '07', ' ', ' '],
|
|
13
|
+
['08', ' ', ' ', ' ', '06', ' ', '09', '09', ' ', ' ', '10', ' ', ' ', ' ', ' '],
|
|
14
|
+
[' ', ' ', ' ', ' ', ' ', ' ', '11', '11', '12', ' ', ' ', ' ', ' ', '13', '13'],
|
|
15
|
+
['14', ' ', ' ', ' ', '15', ' ', '11', '11', ' ', ' ', ' ', ' ', '16', ' ', ' '],
|
|
16
|
+
[' ', '17', ' ', ' ', '15', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '16', ' ', '18'],
|
|
17
|
+
[' ', '17', '19', ' ', ' ', ' ', ' ', ' ', ' ', '20', ' ', ' ', ' ', '21', '18'],
|
|
18
|
+
[' ', '22', ' ', ' ', '23', ' ', ' ', ' ', ' ', '20', ' ', '24', '24', '21', '25'],
|
|
19
|
+
['26', '27', '27', '28', '28', '29', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '30', '30'],
|
|
20
|
+
[' ', '27', '27', '28', '28', '31', '31', ' ', ' ', ' ', ' ', '32', ' ', '30', '30'],
|
|
21
|
+
[' ', ' ', ' ', '33', '33', '31', '31', '34', ' ', ' ', '35', ' ', ' ', ' ', ' '],
|
|
22
|
+
['36', ' ', ' ', '33', '33', ' ', ' ', '34', ' ', ' ', ' ', ' ', ' ', '37', ' '],
|
|
23
|
+
[' ', ' ', '38', '38', ' ', '39', ' ', '40', '40', '41', '41', '42', ' ', '37', ' '],
|
|
24
|
+
['43', '44', '38', '38', '45', '45', '46', '40', '40', '41', '41', '42', ' ', ' ', ' '],
|
|
25
|
+
['43', ' ', ' ', ' ', ' ', ' ', ' ', '47', ' ', ' ', ' ', ' ', '48', '48', ' ']
|
|
26
|
+
])
|
|
27
|
+
binst = solver.Board(galaxies=galaxies)
|
|
28
|
+
solutions = binst.solve_and_print()
|
|
29
|
+
assert len(solutions) == 1, f'unique solutions != 1, == {len(solutions)}'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_easy():
|
|
34
|
+
# 5x5 example
|
|
35
|
+
# https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/galaxies.html#5x5:cfjglmlcg
|
|
36
|
+
galaxies = [
|
|
37
|
+
(get_pos(x=1, y=0),),
|
|
38
|
+
(get_pos(x=4, y=0),),
|
|
39
|
+
(get_pos(x=0, y=1),),
|
|
40
|
+
(get_pos(x=3, y=1), get_pos(x=4, y=1)),
|
|
41
|
+
(get_pos(x=0, y=2), get_pos(x=1, y=2)),
|
|
42
|
+
(get_pos(x=2, y=2), get_pos(x=3, y=2), get_pos(x=2, y=3), get_pos(x=3, y=3)),
|
|
43
|
+
(get_pos(x=1, y=3), get_pos(x=1, y=4)),
|
|
44
|
+
(get_pos(x=4, y=3),),
|
|
45
|
+
(get_pos(x=0, y=4),),
|
|
46
|
+
]
|
|
47
|
+
binst = solver.Board(V=5, H=5, galaxies=galaxies)
|
|
48
|
+
solutions = binst.solve_and_print()
|
|
49
|
+
assert len(solutions) == 1, f'unique solutions != 1, == {len(solutions)}'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_dummy():
|
|
54
|
+
# 3x3 toy example
|
|
55
|
+
# https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/galaxies.html#3x3:acn
|
|
56
|
+
galaxies = [
|
|
57
|
+
(get_pos(x=0, y=0),),
|
|
58
|
+
(get_pos(x=1, y=0), get_pos(x=2, y=0)),
|
|
59
|
+
(get_pos(x=1, y=1), get_pos(x=1, y=2)),
|
|
60
|
+
]
|
|
61
|
+
binst = solver.Board(V=3, H=3, galaxies=galaxies)
|
|
62
|
+
solutions = binst.solve_and_print()
|
|
63
|
+
assert len(solutions) == 1, f'unique solutions != 1, == {len(solutions)}'
|
|
64
|
+
ground = np.array([
|
|
65
|
+
[0, 1, 1],
|
|
66
|
+
[2, 2, 2],
|
|
67
|
+
[2, 2, 2],
|
|
68
|
+
])
|
|
69
|
+
ground_assignment = {get_pos(x=x, y=y): ground[y][x] for x in range(ground.shape[1]) for y in range(ground.shape[0])}
|
|
70
|
+
solution = solutions[0].assignment
|
|
71
|
+
assert set(solution.keys()) == set(ground_assignment.keys()), f'solution keys != ground assignment keys, {set(solution.keys()) ^ set(ground_assignment.keys())} \n\n\n{solution} \n\n\n{ground_assignment}'
|
|
72
|
+
for pos in solution.keys():
|
|
73
|
+
assert solution[pos] == ground_assignment[pos], f'solution[{pos}] != ground_assignment[{pos}], {solution[pos]} != {ground_assignment[pos]}'
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_bbb():
|
|
77
|
+
solver.BBBB().solve_and_print()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == '__main__':
|
|
81
|
+
# test_bbb()
|
|
82
|
+
test_dummy()
|
|
83
|
+
# test_easy()
|
|
84
|
+
test_ground()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/guess/guess.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/inertia/tsp.py
RENAMED
|
File without changes
|
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/keen/keen.py
RENAMED
|
File without changes
|
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/lits/lits.py
RENAMED
|
File without changes
|
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/map/map.py
RENAMED
|
File without changes
|
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/mosaic/mosaic.py
RENAMED
|
File without changes
|
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/pearl/pearl.py
RENAMED
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/range/range.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/sudoku/sudoku.py
RENAMED
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/tents/tents.py
RENAMED
|
File without changes
|
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/towers/towers.py
RENAMED
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/tracks/tracks.py
RENAMED
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/undead/undead.py
RENAMED
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/puzzles/unruly/unruly.py
RENAMED
|
File without changes
|
{multi_puzzle_solver-0.9.14 → multi_puzzle_solver-0.9.15}/src/puzzle_solver/utils/visualizer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|