@@ -75,8 +75,9 @@ def load_image(self):
75
75
if file_path :
76
76
# Load and resize image
77
77
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
+ )
80
81
81
82
# Split image into tiles
82
83
self .image_tiles = []
@@ -117,7 +118,7 @@ def create_board(self):
117
118
btn = tk .Button (
118
119
self .game_frame ,
119
120
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 ),
121
122
)
122
123
btn .grid (row = i , column = j , padx = 1 , pady = 1 )
123
124
row .append (btn )
@@ -129,21 +130,18 @@ def create_board(self):
129
130
def shuffle_board (self ):
130
131
# Perform random moves
131
132
for _ in range (100 ):
132
- possible_moves = self .get_possible_moves ()
133
+ possible_moves = self .get_possible_moves (self . empty_pos )
133
134
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 )
135
136
self .num_moves = 0
136
137
137
138
# Update display
138
139
self .update_display ()
139
140
140
- def get_possible_moves (self , simulate = False , empty_pos = () ):
141
+ def get_possible_moves (self , empty_pos : tuple [ int , int ] ):
141
142
moves = []
142
143
143
- if simulate :
144
- i , j = empty_pos
145
- else :
146
- i , j = self .empty_pos
144
+ i , j = empty_pos
147
145
148
146
# Check all adjacent positions
149
147
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=()):
153
151
154
152
return moves
155
153
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 )
185
159
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 ))
201
175
202
- self . empty_pos = (i ,j )
176
+ return (i ,j )
203
177
204
178
def update_display (self ):
205
179
# Update button images based on current_state
@@ -214,7 +188,7 @@ def update_display(self):
214
188
if self .debug :
215
189
print (f'Total moves: { self .num_moves } ' )
216
190
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 )} ' )
218
192
print ('-' * 50 )
219
193
220
194
def check_win (self ):
@@ -223,8 +197,8 @@ def check_win(self):
223
197
'''
224
198
225
199
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 ):
228
202
'''
229
203
Computes a graph representing every possible unique game state and the moves to reach it
230
204
'''
@@ -238,40 +212,42 @@ def precompute_search_space(self, init_game_state:tuple[tuple], init_empty_pos:t
238
212
239
213
empty_pos = init_empty_pos
240
214
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 )])
242
216
243
217
while queue :
244
218
curr_state , moves_made , empty_pos = queue .pop (0 )
245
219
246
- graph [utils . tuplify_2dmatrix ( curr_state )] = dict ()
220
+ graph [curr_state . get_space ( tuplify = True )] = dict ()
247
221
if self .debug :
248
222
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 } ' )
253
224
254
225
if n_nodes is not None and len (graph ) == n_nodes :
255
226
# this just indicates that we only want to precompute the first n nodes
256
227
if self .debug :
257
- print (f'\n Computed { n_nodes } state nodes, stopping early\n { '-' * 25 } ' )
228
+ print (f'Computed { n_nodes } state nodes, stopping early\n { '-' * 25 } ' )
258
229
break
259
230
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 }
262
233
unseen_states = set (next_states .keys ()) - seen_states
263
234
if self .debug :
264
- print (f'\n Next 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 ))
266
242
267
243
for unseen_state in unseen_states :
268
244
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
270
246
271
247
seen_states .add (unseen_state )
272
248
queue .append (
273
249
(
274
- utils . listify_2dmatrix ( unseen_state ) ,
250
+ unseen_state ,
275
251
moves_made + [move_to ],
276
252
next_states [unseen_state ]
277
253
)
@@ -303,14 +279,14 @@ def solve_game(self):
303
279
print (f'Correct sequence of { num_moves } moves:\n { moves_made } ' )
304
280
305
281
for move in moves_made :
306
- self .make_move (* move )
282
+ self .make_move (* move , self . current_state , self . empty_pos , simulate = False )
307
283
308
284
def solve_pc_bfs (self ):
309
285
'''
310
286
Solve the puzzle using a Breadth-First Search over a precomputed search space graph
311
287
'''
312
288
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 )
314
290
moves_made , num_moves = sa .precomputed_bfs (search_space , self .goal_state )
315
291
316
292
return moves_made , num_moves
@@ -320,7 +296,7 @@ def solve_pc_dfs(self):
320
296
Solve the puzzle using a Depth-First Search over a precomputed search space graph
321
297
'''
322
298
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 )
324
300
moves_made , num_moves = sa .precomputed_dfs (search_space , self .goal_state )
325
301
326
302
return moves_made , num_moves
0 commit comments