Skip to content

Commit 9fba2ca

Browse files
committed
Major changes
Note - really slow
1 parent 456fa44 commit 9fba2ca

File tree

2 files changed

+56
-77
lines changed

2 files changed

+56
-77
lines changed

SlidePuzzle.py

Lines changed: 50 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ def load_image(self):
7575
if file_path:
7676
# Load and resize image
7777
image = Image.open(file_path)
78-
image = image.resize((self.tile_size * self.size,
79-
self.tile_size * self.size))
78+
image = image.resize(
79+
(self.tile_size * self.size, self.tile_size * self.size),
80+
)
8081

8182
# Split image into tiles
8283
self.image_tiles = []
@@ -117,7 +118,7 @@ def create_board(self):
117118
btn = tk.Button(
118119
self.game_frame,
119120
image=self.image_tiles[number],
120-
command=lambda x=i, y=j: self.make_move(x, y),
121+
command=lambda x=i, y=j: self.make_move(x, y, self.current_state, self.empty_pos, simulate=False),
121122
)
122123
btn.grid(row=i, column=j, padx=1, pady=1)
123124
row.append(btn)
@@ -129,21 +130,18 @@ def create_board(self):
129130
def shuffle_board(self):
130131
# Perform random moves
131132
for _ in range(100):
132-
possible_moves = self.get_possible_moves()
133+
possible_moves = self.get_possible_moves(self.empty_pos)
133134
i, j = self.rand.choice(possible_moves)
134-
self.swap_tiles(i, j)
135+
self.empty_pos = self.swap_tiles(i, j, self.current_state)
135136
self.num_moves = 0
136137

137138
# Update display
138139
self.update_display()
139140

140-
def get_possible_moves(self, simulate=False, empty_pos=()):
141+
def get_possible_moves(self, empty_pos:tuple[int,int]):
141142
moves = []
142143

143-
if simulate:
144-
i, j = empty_pos
145-
else:
146-
i, j = self.empty_pos
144+
i, j = empty_pos
147145

148146
# Check all adjacent positions
149147
for di, dj in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
@@ -153,53 +151,29 @@ def get_possible_moves(self, simulate=False, empty_pos=()):
153151

154152
return moves
155153

156-
def make_move(self, i, j, simulate=False, game_state=None, empty_pos=(), possible_moves:list[tuple[int,int]]=[]) -> None|list[list[int]]:
157-
# simulate making the move, but don't actually change the board
158-
if simulate:
159-
if game_state is None or len(empty_pos) < 2:
160-
# no state provided which is necessary for simulation to work properly
161-
# also need to know empty position for simulation to work properly
162-
return None
163-
164-
if len(possible_moves) == 0:
165-
# possible moves not already supplied so calculate them
166-
possible_moves = self.get_possible_moves(simulate=True, empty_pos=empty_pos)
167-
168-
# assumes possible moves provided (or calculated) are indeed valid
169-
if (i,j) not in possible_moves:
170-
return None # invalid move, no valid board
171-
172-
# make a copy of the game state - otherwise our modifications will be done in-place
173-
# which will mess up subsequent move simulations
174-
curr_state = copy.deepcopy(game_state)
175-
176-
# move must be valid
177-
# just swap the empty tile with the tile at the i,j position
178-
empty_i, empty_j = empty_pos
179-
empty_tile = curr_state[empty_i][empty_j] # actual empty tile number
180-
swap_tile = curr_state[i][j] # actual soon-to-be-swapped tile number
181-
curr_state[empty_i][empty_j] = swap_tile
182-
curr_state[i][j] = empty_tile
183-
184-
return curr_state
154+
def make_move(self, i:int, j:int, game_state:utils.UniqueGrid, empty_pos:tuple[int,int], possible_moves:list[tuple[int,int]]=[], simulate=False):
155+
possible_moves = possible_moves if possible_moves else self.get_possible_moves(empty_pos)
156+
if (i,j) in possible_moves:
157+
temp_state = copy.deepcopy(game_state)
158+
empty_pos = self.swap_tiles(i, j, temp_state)
185159

186-
# not a simulation - carry out the move and update the board
187-
# Check if the clicked tile is adjacent to empty space
188-
if (i, j) in self.get_possible_moves():
189-
self.num_moves += 1
190-
self.swap_tiles(i, j)
191-
self.update_display()
192-
# Check if puzzle is solved
193-
if self.check_win():
194-
messagebox.showinfo(
195-
"Congratulations!", "You solved the puzzle in " + str(self.num_moves) + " moves!"
196-
)
197-
198-
def swap_tiles(self, i, j):
199-
# Swap values in current_state
200-
self.current_state.swap(self.empty_pos, (i,j))
160+
if not simulate:
161+
self.num_moves += 1
162+
self.current_state = temp_state
163+
self.empty_pos = empty_pos
164+
self.update_display()
165+
# Check if puzzle is solved
166+
if self.check_win():
167+
messagebox.showinfo(
168+
"Congratulations!", "You solved the puzzle in " + str(self.num_moves) + " moves!"
169+
)
170+
171+
return temp_state
172+
173+
def swap_tiles(self, i, j, game_state:utils.UniqueGrid):
174+
game_state.swap(self.empty_pos, (i,j))
201175

202-
self.empty_pos = (i,j)
176+
return (i,j)
203177

204178
def update_display(self):
205179
# Update button images based on current_state
@@ -214,7 +188,7 @@ def update_display(self):
214188
if self.debug:
215189
print(f'Total moves: {self.num_moves}')
216190
print(f'Current game state:\n{self.current_state}')
217-
print(f'Possible moves:\n{self.get_possible_moves()}')
191+
print(f'Possible moves:\n{self.get_possible_moves(self.empty_pos)}')
218192
print('-'*50)
219193

220194
def check_win(self):
@@ -223,8 +197,8 @@ def check_win(self):
223197
'''
224198

225199
return self.goal_state == self.current_state.get_space(tuplify=True)
226-
227-
def precompute_search_space(self, init_game_state:tuple[tuple], init_empty_pos:tuple[int,int], n_nodes:int|None=None):
200+
201+
def precompute_search_space(self, init_game_state:utils.UniqueGrid, init_empty_pos:tuple[int,int], n_nodes:int|None=None):
228202
'''
229203
Computes a graph representing every possible unique game state and the moves to reach it
230204
'''
@@ -238,40 +212,42 @@ def precompute_search_space(self, init_game_state:tuple[tuple], init_empty_pos:t
238212

239213
empty_pos = init_empty_pos
240214
queue = [(init_game_state, [], empty_pos)] # tuple of current game state, moves made to reach current state, and position of empty tile
241-
seen_states = set([utils.tuplify_2dmatrix(init_game_state)])
215+
seen_states = set([init_game_state.get_space(tuplify=True)])
242216

243217
while queue:
244218
curr_state, moves_made, empty_pos = queue.pop(0)
245219

246-
graph[utils.tuplify_2dmatrix(curr_state)] = dict()
220+
graph[curr_state.get_space(tuplify=True)] = dict()
247221
if self.debug:
248222
print(f'Unique state nodes in graph: {len(graph)}\n')
249-
print('Current game state:')
250-
# see https://stackoverflow.com/a/63496125/ (pretty printing the 2d matrix)
251-
for i in curr_state:
252-
print(' '.join(map(str, i)))
223+
print(f'Current game state:\n{curr_state}')
253224

254225
if n_nodes is not None and len(graph) == n_nodes:
255226
# this just indicates that we only want to precompute the first n nodes
256227
if self.debug:
257-
print(f'\nComputed {n_nodes} state nodes, stopping early\n{'-'*25}')
228+
print(f'Computed {n_nodes} state nodes, stopping early\n{'-'*25}')
258229
break
259230

260-
possible_moves = self.get_possible_moves(simulate=True, empty_pos=empty_pos)
261-
next_states = {utils.tuplify_2dmatrix(self.make_move(*move, simulate=True, game_state=curr_state, empty_pos=empty_pos, possible_moves=possible_moves)): move for move in possible_moves}
231+
possible_moves = self.get_possible_moves(empty_pos)
232+
next_states = {self.make_move(*move, curr_state, empty_pos, possible_moves=possible_moves, simulate=True): move for move in possible_moves}
262233
unseen_states = set(next_states.keys()) - seen_states
263234
if self.debug:
264-
print(f'\nNext possible {len(next_states)} game states:\n{pprint.pformat(next_states, indent=4, sort_dicts=False)}\n')
265-
print(f'{len(unseen_states)}/{len(next_states)} next possible game states are unseen:\n{unseen_states}')
235+
print(f'Next possible {len(next_states)} game states:')
236+
for i,next_state in enumerate(next_states.keys()):
237+
print(f'{i}:\n' + str(next_state))
238+
239+
print(f'{len(unseen_states)}/{len(next_states)} next possible game states are unseen:')
240+
for i,next_state in enumerate(next_states.keys()):
241+
print(f'{i}:\n' + str(next_state))
266242

267243
for unseen_state in unseen_states:
268244
move_to = next_states[unseen_state]
269-
graph[utils.tuplify_2dmatrix(curr_state)][unseen_state] = move_to
245+
graph[curr_state.get_space(tuplify=True)][unseen_state.get_space(tuplify=True)] = move_to
270246

271247
seen_states.add(unseen_state)
272248
queue.append(
273249
(
274-
utils.listify_2dmatrix(unseen_state),
250+
unseen_state,
275251
moves_made + [move_to],
276252
next_states[unseen_state]
277253
)
@@ -303,14 +279,14 @@ def solve_game(self):
303279
print(f'Correct sequence of {num_moves} moves:\n{moves_made}')
304280

305281
for move in moves_made:
306-
self.make_move(*move)
282+
self.make_move(*move, self.current_state, self.empty_pos, simulate=False)
307283

308284
def solve_pc_bfs(self):
309285
'''
310286
Solve the puzzle using a Breadth-First Search over a precomputed search space graph
311287
'''
312288

313-
search_space = self.precompute_search_space(self.current_state.get_space(), self.empty_pos, **self.solve_config)
289+
search_space = self.precompute_search_space(copy.deepcopy(self.current_state), self.empty_pos, **self.solve_config)
314290
moves_made, num_moves = sa.precomputed_bfs(search_space, self.goal_state)
315291

316292
return moves_made, num_moves
@@ -320,7 +296,7 @@ def solve_pc_dfs(self):
320296
Solve the puzzle using a Depth-First Search over a precomputed search space graph
321297
'''
322298

323-
search_space = self.precompute_search_space(self.current_state.get_space(), self.empty_pos, **self.solve_config)
299+
search_space = self.precompute_search_space(copy.deepcopy(self.current_state), self.empty_pos, **self.solve_config)
324300
moves_made, num_moves = sa.precomputed_dfs(search_space, self.goal_state)
325301

326302
return moves_made, num_moves

utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,6 @@ def set_space(self, space):
107107
self.space = prev_space
108108
raise e
109109

110-
return prev_space
111-
112110
def __str__(self):
113111
s = ''
114112

@@ -149,4 +147,9 @@ def __str__(self):
149147
new_pos = (1,1)
150148
g.swap(old_pos, new_pos)
151149
print(g)
152-
150+
151+
g2 = copy.deepcopy(g)
152+
g2.insert(new_pos, 123)
153+
154+
print(f'Original grid:\n{str(g)}')
155+
print(f'Copied grid:\n{g2}')

0 commit comments

Comments
 (0)