Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Infinite Loop with my code
#1
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.

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

.py   15-puzzle.py (Size: 7.82 KB / Downloads: 5)
Reply
#2
How are you running the code? All I see are 2 imports and 3 class definitions. There's nothing that instantiates any of the classes. When I run it, it just exits immediately without hanging.

(Mar-19-2026, 12:41 AM)TheGrado Wrote: numbers = [x for x in range(self._width * self._height)]
I would probably prefer to write this as
numbers = list(range(self._width * self._height))
Quote: @property
def width(self) -> int:
return self._width
What's the benefit of having a separate property function here? Why not just have self.width be used directly?

Looks like the initial board isn't properly reported by the history?
Reply
#3
I don't see what you describe. I thought maybe creating a valid table was an infinite loop, but that's not the case. Then I looked for some kind of solver, but I don't see anything you can call to solve the puzzle. Please provide instructions on how to make the infinite loop happen.
Reply
#4
I can't follow what your def is_solvable(self) -> bool: does. How does one find a solution?

while True will produce an endless loop if is_solvable() never returns True.

from random import shuffle

# width, height = 5, 5
def make_grid(width, height):
    numbers = [x for x in range(width * height)]
    shuffle(numbers)
    grid = [numbers[i:i + width] for i in range(0, width * height, width)]
    return grid

temp = make_grid(width, height)

# seems important to find zero
for num, row  in enumerate(temp):
    if 0 in row:
        print(f'found zero in row {num}')

# now check somehow for a solution, if none, make another grid
Reply
#5
(Mar-19-2026, 05:58 AM)Pedroski55 Wrote: I can't follow what your def is_solvable(self) -> bool: does. How does one find a solution?

while True will produce an endless loop if is_solvable() never returns True.

from random import shuffle

# width, height = 5, 5
def make_grid(width, height):
    numbers = [x for x in range(width * height)]
    shuffle(numbers)
    grid = [numbers[i:i + width] for i in range(0, width * height, width)]
    return grid

temp = make_grid(width, height)

# seems important to find zero
for num, row  in enumerate(temp):
    if 0 in row:
        print(f'found zero in row {num}')

# now check somehow for a solution, if none, make another grid


Thanks for your reply. The is_solvable() method should check the solvability of the game, but I don't understand how it loops infinitely. The project text is attached.

Attached Files

.pdf   Project 15-Puzzle (1).pdf (Size: 121.49 KB / Downloads: 4)
Reply
#6
(Mar-19-2026, 03:53 AM)deanhystad Wrote: I don't see what you describe. I thought maybe creating a valid table was an infinite loop, but that's not the case. Then I looked for some kind of solver, but I don't see anything you can call to solve the puzzle. Please provide instructions on how to make the infinite loop happen.

Sure this is the text of the project

Attached Files

.pdf   Project 15-Puzzle (1).pdf (Size: 121.49 KB / Downloads: 1)
Reply
#7
(Mar-19-2026, 02:52 AM)bowlofred Wrote: How are you running the code? All I see are 2 imports and 3 class definitions. There's nothing that instantiates any of the classes. When I run it, it just exits immediately without hanging.

(Mar-19-2026, 12:41 AM)TheGrado Wrote: numbers = [x for x in range(self._width * self._height)]
I would probably prefer to write this as
numbers = list(range(self._width * self._height))
Quote: @property
def width(self) -> int:
return self._width
What's the benefit of having a separate property function here? Why not just have self.width be used directly?

Looks like the initial board isn't properly reported by the history?

Apparently no one, but the gioco_del_15.py module must work independently, even without the gui.py module (and
even in combination with the gui.py module from another well-made project), and must have
all the type annotations to pass strict-level type checking without errors. So I assume it's for educational purposes only.
Reply
#8
What makes you think there is an infinite loop? Is it because the GUI part is frozen or doesn't appear?


There are a few places where you could have an infinite loop. The most obvious is here:
        # *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
If _Table.is_solvable() never returns True, this loop will run forever. I would test Table.is_solvable() with known good and bad tables to verify that it works correctly. I ran just the table shuffle and check part of the code, limiting how many times it attempts to create a table and reporting how many attempts it took to create a valid table. My test revealed it always took 16 tries to make a 4x4 table, which is suspicious.

Nothing else would cause the program to hang at startup.

Game.move is confusing, this swapping code in particular:
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]]
Who knew a, b = b, a could be so difficult to read.

There's no need to swap boxes. If we have 0, 1, 2, 3 and we want to slide box 2, we don't need to do this:
0, 1, 2, 3 -> 1, 0, 2, 3 -> 1, 2, 0, 3

We can do this:
0, 1, 2, 3 -> 1, 1, 2, 3 -> 1, 2, 2, 3 -> 1, 2, 0, 3

That looks like an extra step, but it is actually only 3 changes instead of 4 for the swap method. The coding is much simpler and far easier to read.

Use Table.get_box() and Table.set_box() and individual row and column indices instead of a tuple. Like this:
    def move(self, row: int, column: int) -> None:
        """Move tiles starting at [row, column] towards the empty space."""
        zero_row, zero_col = self.table.find_zero()  # New table method that returns location of zero
 
        # Check if the selected tile is empty.
        if (row == zero_row and column == zero_col):
            return
 
        # Check if the selected tile can be moved.
        if row != zero_row and column != zero_col:
            return

        # Slide the tile
        if row == zero_row:
            # Horizontal move
            if column < zero_col:
                # Slide right
                for c in range(zero_col, column+1, -1):
                    self.table.set_box(row, c-1, self.table.get_box(row, c))
            else:
                # Slide left
                for c in range(zero_col, column-1):
                    self.table.set_box(row, c+1, self.table.get_box(row, c))
        else:
            # Vertical move
            if row < zero_row:
                # Slide down
                for r in range(zero_row, row+1, -1):
                    self.table.set_box(r-1, column, self.table.get_box(r, column))
            else:
                # Slide up
                for r in range(zero_row, row-1):
                    self.table.set_box(r+1, column, self.table.get_box(r, column))
        self.table.set_box(row, column, 0)
Reply
#9
Okay, apparently I misunderstood the purpose of the is_solvable() function Doh : 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. Heart
Reply
#10
I made 8 squares from cardboard, wrote the numbers 1 to 8 on them, laid them out in a 3 by 3 grid.There is then 1 empty space. Tried moving them around. Sometimes, it is quite easy to find a solution, but I have not grasped the principal of achieving a solution yet! Complicated little game!

If you permit the numbers to go:

123
654
78

it is easier to find a solution when each row reading left to right is hard or impossible to achieve.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  How to convert while loop to for loop in my code? tatahuft 4 1,659 Dec-21-2024, 07:59 AM
Last Post: snippsat
Shocked Why this code does not go into an infinite loop? 2367409125 2 2,225 Dec-02-2022, 08:22 PM
Last Post: deanhystad
  Need help with infinite loop & making hotkeys/shortcuts Graxum 1 2,547 Aug-22-2022, 02:57 AM
Last Post: deanhystad
  Infinite loop problem Zirconyl 5 5,471 Nov-16-2020, 09:06 AM
Last Post: DeaD_EyE
  using 'while loop' output going into infinite loop... amitkb 2 3,402 Oct-05-2020, 09:18 PM
Last Post: micseydel
  Infinite loop not working pmp2 2 3,137 Aug-18-2020, 12:27 PM
Last Post: deanhystad
  Why is this code not an infinite loop? psi2kgcm 1 2,825 Dec-24-2019, 02:04 PM
Last Post: buran
  Appending to list not working and causing a infinite loop eiger23 8 7,758 Oct-10-2019, 03:41 PM
Last Post: eiger23
  Server infinite loop input from user tomislav91 1 5,717 May-23-2019, 02:18 PM
Last Post: heiner55
  Code Change Help: Infinite Loop Error bindulam 2 3,689 Mar-10-2019, 11:15 PM
Last Post: hshivaraj

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020