multi-puzzle-solver 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl
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-1.0.2.dist-info → multi_puzzle_solver-1.0.4.dist-info}/METADATA +384 -111
- {multi_puzzle_solver-1.0.2.dist-info → multi_puzzle_solver-1.0.4.dist-info}/RECORD +13 -10
- puzzle_solver/__init__.py +7 -1
- puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py +48 -0
- puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +0 -1
- puzzle_solver/puzzles/galaxies/galaxies.py +108 -110
- puzzle_solver/puzzles/lits/lits.py +4 -4
- puzzle_solver/puzzles/norinori/norinori.py +11 -9
- puzzle_solver/puzzles/pipes/pipes.py +81 -0
- puzzle_solver/puzzles/star_battle/star_battle.py +10 -7
- puzzle_solver/puzzles/twiddle/twiddle.py +112 -0
- {multi_puzzle_solver-1.0.2.dist-info → multi_puzzle_solver-1.0.4.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.2.dist-info → multi_puzzle_solver-1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ortools.sat.python import cp_model
|
|
4
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
5
|
+
|
|
6
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_next_pos, Direction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Board:
|
|
10
|
+
def __init__(self, board: np.array, time_horizon: int = 10):
|
|
11
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
12
|
+
assert all(str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only digits'
|
|
13
|
+
self.board = board
|
|
14
|
+
self.target_state = np.sort(board, axis=None).reshape(board.shape)
|
|
15
|
+
self.V, self.H = board.shape
|
|
16
|
+
self.min_value = int(np.min(board.flatten()))
|
|
17
|
+
self.max_value = int(np.max(board.flatten()))
|
|
18
|
+
self.time_horizon = time_horizon
|
|
19
|
+
|
|
20
|
+
self.model = cp_model.CpModel()
|
|
21
|
+
self.state: dict[tuple[Pos, int], cp_model.IntVar] = {}
|
|
22
|
+
self.decision: dict[int, dict[Pos, cp_model.IntVar]] = {t: {} for t in range(self.time_horizon - 1)}
|
|
23
|
+
|
|
24
|
+
self.create_vars()
|
|
25
|
+
self.add_all_constraints()
|
|
26
|
+
self.minimize_actions()
|
|
27
|
+
self.constrain_final_state()
|
|
28
|
+
|
|
29
|
+
def create_vars(self):
|
|
30
|
+
for pos in get_all_pos(self.V, self.H):
|
|
31
|
+
for t in range(self.time_horizon):
|
|
32
|
+
self.state[pos, t] = self.model.NewIntVar(self.min_value, self.max_value, f'state:{pos}:{t}')
|
|
33
|
+
for t in range(self.time_horizon - 1):
|
|
34
|
+
self.decision[t]['NOOP'] = self.model.NewBoolVar(f'decision:NOOP:{t}')
|
|
35
|
+
for pos in get_all_pos(self.V, self.H):
|
|
36
|
+
if pos.x == self.H - 1 or pos.y == self.V - 1:
|
|
37
|
+
continue
|
|
38
|
+
self.decision[t][pos] = self.model.NewBoolVar(f'decision:{pos}:{t}')
|
|
39
|
+
|
|
40
|
+
def add_all_constraints(self):
|
|
41
|
+
# one action at most every time
|
|
42
|
+
for decision_at_t in self.decision.values():
|
|
43
|
+
self.model.AddExactlyOne(list(decision_at_t.values()))
|
|
44
|
+
# constrain the state at t=0
|
|
45
|
+
for pos in get_all_pos(self.V, self.H):
|
|
46
|
+
self.model.Add(self.state[pos, 0] == get_char(self.board, pos))
|
|
47
|
+
# constrain the state dynamics at t=1..T
|
|
48
|
+
for action_pos in get_all_pos(self.V, self.H):
|
|
49
|
+
if action_pos.x == self.H - 1 or action_pos.y == self.V - 1:
|
|
50
|
+
continue
|
|
51
|
+
self.constrain_state(action_pos)
|
|
52
|
+
# state does not change if NOOP is chosen
|
|
53
|
+
for t in range(1, self.time_horizon):
|
|
54
|
+
noop_var = self.decision[t - 1]['NOOP']
|
|
55
|
+
for pos in get_all_pos(self.V, self.H):
|
|
56
|
+
self.model.Add(self.state[pos, t] == self.state[pos, t - 1]).OnlyEnforceIf(noop_var)
|
|
57
|
+
|
|
58
|
+
def constrain_state(self, action: Pos):
|
|
59
|
+
tl = action
|
|
60
|
+
tr = get_next_pos(tl, Direction.RIGHT)
|
|
61
|
+
bl = get_next_pos(tl, Direction.DOWN)
|
|
62
|
+
br = get_next_pos(tr, Direction.DOWN)
|
|
63
|
+
two_by_two = (tl, tr, br, bl)
|
|
64
|
+
# lock state outside the two by two
|
|
65
|
+
for pos in get_all_pos(self.V, self.H):
|
|
66
|
+
if pos in two_by_two:
|
|
67
|
+
continue
|
|
68
|
+
for t in range(1, self.time_horizon):
|
|
69
|
+
self.model.Add(self.state[pos, t] == self.state[pos, t - 1]).OnlyEnforceIf(self.decision[t - 1][action])
|
|
70
|
+
# rotate clockwise inside the two by two
|
|
71
|
+
clockwise = two_by_two[-1:] + two_by_two[:-1]
|
|
72
|
+
# print('action', action)
|
|
73
|
+
# print('two_by_two', two_by_two)
|
|
74
|
+
# print('clockwise', clockwise)
|
|
75
|
+
for pre_pos, post_pos in zip(clockwise, two_by_two):
|
|
76
|
+
for t in range(1, self.time_horizon):
|
|
77
|
+
# print(f'IF self.decision[{t - 1}][{action}] THEN self.state[{post_pos}, {t}] == self.state[{pre_pos}, {t - 1}]')
|
|
78
|
+
self.model.Add(self.state[post_pos, t] == self.state[pre_pos, t - 1]).OnlyEnforceIf(self.decision[t - 1][action])
|
|
79
|
+
|
|
80
|
+
def constrain_final_state(self):
|
|
81
|
+
final_time = self.time_horizon - 1
|
|
82
|
+
for pos in get_all_pos(self.V, self.H):
|
|
83
|
+
self.model.Add(self.state[pos, final_time] == get_char(self.target_state, pos))
|
|
84
|
+
|
|
85
|
+
def minimize_actions(self):
|
|
86
|
+
flat_decisions = [(var, t+1) for t, tvs in self.decision.items() for pos, var in tvs.items() if pos != 'NOOP']
|
|
87
|
+
self.model.Minimize(lxp.weighted_sum([p[0] for p in flat_decisions], [p[1] for p in flat_decisions]))
|
|
88
|
+
|
|
89
|
+
def solve_and_print(self, verbose: bool = True):
|
|
90
|
+
solver = cp_model.CpSolver()
|
|
91
|
+
tic = time.time()
|
|
92
|
+
solver.solve(self.model)
|
|
93
|
+
assignment: dict[Pos] = [None for _ in range(self.time_horizon - 1)]
|
|
94
|
+
if solver.StatusName() in ['OPTIMAL', 'FEASIBLE']:
|
|
95
|
+
for t, tvs in self.decision.items():
|
|
96
|
+
for pos, var in tvs.items():
|
|
97
|
+
if solver.Value(var) == 1:
|
|
98
|
+
assignment[t] = (pos.x, pos.y) if pos != 'NOOP' else 'NOOP'
|
|
99
|
+
for t in range(self.time_horizon):
|
|
100
|
+
res_at_t = np.full((self.V, self.H), ' ', dtype=object)
|
|
101
|
+
for pos in get_all_pos(self.V, self.H):
|
|
102
|
+
res_at_t[pos.y][pos.x] = solver.Value(self.state[pos, t])
|
|
103
|
+
print(f't={t}')
|
|
104
|
+
print(res_at_t)
|
|
105
|
+
if verbose:
|
|
106
|
+
print("Solution found:", assignment)
|
|
107
|
+
if verbose:
|
|
108
|
+
print("status:", solver.StatusName())
|
|
109
|
+
toc = time.time()
|
|
110
|
+
print(f"Time taken: {toc - tic:.2f} seconds")
|
|
111
|
+
return assignment
|
|
112
|
+
|
|
File without changes
|
|
File without changes
|