Mar-19-2026, 12:41 AM
Hi, I'm a student developing a college project called Game of Fifteen. I can't figure out what's wrong with my code. Could someone help me?
When I run the code, it doesn't seem to give me any errors; instead, it tends to go into an infinite loop.
When I run the code, it doesn't seem to give me any errors; instead, it tends to go into an infinite loop.
from typing import Any
import random
class Game:
def __init__(self, width: int, height: int) -> None:
self._width, self._height = width, height
self._start_game = True
self._table = _Table(self)
self._history_copy: list[tuple[tuple[int, int] | None, _Table]] = [(None, self._table.copy_table(self._table))]
self._cache = []
# *shuffle numbers until a solvable board is generated*
while True:
numbers = [x for x in range(self._width * self._height)]
random.shuffle(numbers)
i = 0
for x in range(self._height):
for y in range(self._width):
self._table.set_box(x + 1, y + 1, numbers[i]) # set_box uses 1-based indexing
i += 1
if self._table.is_solvable(): # exit loop when board is solvable
break
@property
def width(self) -> int:
return self._width
@property
def height(self) -> int:
return self._height
@property
def completed(self) -> bool:
expect = 1
for x in range(self._height):
for y in range(self._width):
if x == self._height - 1 and y == self._width - 1:
return self._table._Tabellone[x][y] == 0
if self._table._Tabellone[x][y] == expect:
expect += 1
else:
return False
return True
@property
def table(self) -> list[list[int]]:
return [[self._table._Tabellone[x][y] for y in range(self._width)] for x in range(self._height)]
def move(self, row: int, column: int) -> None:
# *position of the zero tile*
pos_0: tuple | None = None
tab = self._table._Tabellone
# *check if the game has started*
if not self._start_game:
raise GridException(row, column, game=self)
# *check if the selected tile is zero*
if self.table[row - 1][column - 1] == 0:
raise GridException(row, column, game=self)
# *find position of zero in the same row or column*
if 0 in self.table[row - 1]:
pos_0 = (row - 1, self.table[row - 1].index(0))
elif 0 in [x[column - 1] for x in self.table]:
for i, x in enumerate(self.table):
if x[column - 1] == 0:
pos_0 = (i, column - 1)
# *if no zero tile in same row or column, invalid move*
if pos_0 is None:
raise GridException(row, column, game=self)
# *move zero along the row*
if pos_0[0] == row - 1:
while pos_0[1] != column - 1:
if pos_0[1] < column - 1:
tab[pos_0[0]][pos_0[1]], tab[pos_0[0]][pos_0[1] + 1] = tab[pos_0[0]][pos_0[1] + 1], tab[pos_0[0]][pos_0[1]]
pos_0 = (pos_0[0], pos_0[1] + 1)
else:
tab[pos_0[0]][pos_0[1]], tab[pos_0[0]][pos_0[1] - 1] = tab[pos_0[0]][pos_0[1] - 1], tab[pos_0[0]][pos_0[1]]
pos_0 = (pos_0[0], pos_0[1] - 1)
# *move zero along the column*
elif pos_0[1] == column - 1:
while pos_0[0] != row - 1:
if pos_0[0] < row - 1:
tab[pos_0[0]][pos_0[1]], tab[pos_0[0] + 1][pos_0[1]] = tab[pos_0[0] + 1][pos_0[1]], tab[pos_0[0]][pos_0[1]]
pos_0 = (pos_0[0] + 1, pos_0[1])
else:
tab[pos_0[0]][pos_0[1]], tab[pos_0[0] - 1][pos_0[1]] = tab[pos_0[0] - 1][pos_0[1]], tab[pos_0[0]][pos_0[1]]
pos_0 = (pos_0[0] - 1, pos_0[1])
self._history_copy.append(((row, column), self._table.copy_table(self._table)))
self._cache = []
# *undo the last move*
def undo(self) -> None:
if len(self._history_copy) <= 1:
return
self._cache.append(self._history_copy.pop())
# *redo the last undone move*
def redo(self) -> None:
if not self._cache:
return
self._history_copy.append(self._cache.pop())
def __str__(self) -> str:
final_str = ""
for m, t in self._history_copy:
if m is None:
final_str += f"Initial board:\n{t}\n"
else:
final_str += f"Board after move at row {m[0]}, column {m[1]}:\n{t}\n"
return final_str
class _Table:
def __init__(self, g: "Game"):
self._game = g
self._rows, self._columns = g.height, g.width
self._Tabellone = [[0 for _ in range(self._columns)] for _ in range(self._rows)]
@classmethod
def copy_table(cls, t: "_Table") -> "_Table":
new_table = cls(t._game)
for x in range(t._rows):
for y in range(t._columns):
new_table._Tabellone[x][y] = t._Tabellone[x][y]
return new_table
def set_box(self, row: int, column: int, value: int) -> None:
if value < 0 or value > self._rows * self._columns - 1:
raise GridException(row, column, value, table=self)
if row < 1 or column < 1:
raise GridException(row, column, value, table=self)
if row > self._rows or column > self._columns:
raise GridException(row, column, value, table=self)
self._Tabellone[row - 1][column - 1] = value
def get_box(self, row: int, column: int) -> int:
if row < 1 or column < 1 or row > self._rows or column > self._columns:
raise GridException(row, column, table=self)
return self._Tabellone[row - 1][column - 1]
# *check if board is solvable*
def is_solvable(self) -> bool:
inv_c = 0
num_columns = len(self._Tabellone[0])
num_rows = len(self._Tabellone)
# *flatten board row by row, excluding zero*
numbers = [self._Tabellone[x][y] for x in range(num_rows) for y in range(num_columns) if self._Tabellone[x][y] != 0]
# *count inversions*
for i in range(len(numbers)):
for j in range(i + 1, len(numbers)):
if numbers[i] > numbers[j]:
inv_c += 1
# *odd number of columns*
if num_columns % 2 == 1:
return inv_c % 2 == 0
# *even number of columns: find row of empty tile from bottom*
zero_row = 0
for x in range(num_rows):
for y in range(num_columns):
if self._Tabellone[x][y] == 0:
zero_row = x + 1
ind_c = num_rows - zero_row + 1
return (inv_c + ind_c) % 2 == 1
def __str__(self) -> str:
max_num = self._rows * self._columns - 1
width = len(str(max_num))
rows = []
for row in self._Tabellone:
row_str = []
for x in row:
if x == 0:
string = " " * width
else:
string = " " * (width - len(str(x))) + str(x)
row_str.append(string)
rows.append(" ".join(row_str))
return "\n".join(rows)
class GridException(Exception):
def __init__(self, row: int, column: int, value: int = 0, game: Game | None = None, table: _Table | None = None):
message = ""
if table is not None:
if value != 0:
message = f"Cannot insert value {value} at row {row}, column {column}."
else:
message = f"No cell at row {row}, column {column}."
elif game is not None:
if game.completed:
message = "The game is already completed."
elif game.table[row][column] == 0:
message = "The selected tile is empty."
else:
message = "No empty tile in the same row or column."
super().__init__(message)
Attached Files

: I didn't realize I was supposed to keep generating tables until a valid one was generated. Only when is_solvable() was false did the while loop exit, so only when an invalid table was generated. Now the code seems to work, thanks everyone for the help.