Interactive Tower of Hanoi
Bugs

Colorama Not Initialized

Colorama should be initialized, to ensure proper operation:

import colorama

colorama.init()


Clear Screen


  sh: cls: command not found


On non-Windows machines, cls is not defined

Instead, simply use colorama to clear the screen.

def clear():
    print(colorama.ansi.clear_screen())


Play again

Do you want to play again? &lt;Y&gt;es or &lt;N&gt;oY 
Do you want to play again? &lt;Y&gt;es or &lt;N&gt;oN 
Do you want to play again? &lt;Y&gt;es or &lt;N&gt;oy 
Do you want to play again? &lt;Y&gt;es or &lt;N&gt;on 
Do you want to play again? &lt;Y&gt;es or &lt;N&gt;o 


No answer seems to provide the expected behaviour, due to:

while play_again != 'y' or play_again != 'n':


always being True.  You need the and conjunction, not or.



Unused variables

toh = TowerOfHanoi(n1, 1, 2, 3)


toh is never used after this point.  So, why store it in a variable?



Global variables

a = [i for i in range(1, n1 + 1)]
b = []
c = []

toh = TowerOfHanoi(n1, 1, 2, 3)


a, b, and c are used inside the TowerOfHanoi class, but are not passed to the class.  Instead they are globals variables that must be properly initialized corresponding to the value n1 passed to the TowerOfHanoi constructor.

These lists should be created by the class itself, based on the argument passed to the constructor.  Moreover, they should be members of the class, not globals.



Code Duplication

This code:

    for i in a1:
        s = ' ' * (m - i + 1)
        h = Fore.RED + s + '██' * i + s + Style.RESET_ALL

        x.append(h)


is replicated 3 times, with a1 changing to b1 and c1, and x changing to y and z.  Perhaps you could refactor this into its own function, taking the rod list as input, and generating &amp; returning the lists internally?

    def rod_images(self, rod):
        # Fill rod with required 0's at top
        rod = [0] * (self.n - len(rod)) + rod

        image_rows = []
        for i in rod:
            spaces = ' ' * (self.n - i + 1)
            row = Fore.RED + spaces + '██' * i + spaces + Style.RESET_ALL
            image_rows.append(row)

        return image_rows


Then you could simply write:

    def parser(self, a1, b1, c1):

        x = self.rod_images(a1)
        y = self.rod_images(b1)
        z = self.rod_images(c1)

        # ... etc ...




Zero, One &amp; Many

In programming, there are 3 important numbers: zero, one, and many.  How many rods do you have?  More than one, so ... you have many rods.  Many items are stored in containers, such as lists, not individual variables.  So instead of this:

    self.r1 = r1
    self.r2 = r2
    self.r3 = r3


write something like:

    self.rods = [r1, r2, r3]


Then, instead of

    print(' ' * self.n + str(self.r1)
          + ' ' * (self.n * 2 + 1) + str(self.r2)
          + ' ' * (self.n * 2 + 1) + str(self.r3))


you can take advantage of the fact that you are doing something with each item in the list, in order.  In this case, you are converting each item to a string, and then joining them together with a bunch of spaces in-between:

    print(' ' * self.n
          + (' ' * (self.n * 2 + 1)).join(str(rod) for rod in self.rods)


But ... that still looks ugly.  What you are really doing is printing out 3 values, each centred in a n * 2 + 2 character wide field:

    width = self.n * 2 + 2
    print((f"{{:^{width}}}"*3).format(*self.rods))


With 3 disks, this creates the format string "{:^8}{:^8}{:^8}" which centres the 3 values each in 8 character wide fields.

A similar method could be used to centre the discs when printing, as long as the colour/style codes are removed from the disc "values" and moved to the format string itself, so they don't mess up the character counts for automatic centring.

Unnecessary Default Arguments

Calling solve(), with no arguments, is only done from __init__().  So the only time the default arguments are used is that one call.  So why complicate the method with default arguments which must be set via a number of if _ is None: tests?  Simply use:

self.solve(self.n, self.r1, self.r2, self.r3, 0, 1, 2)


in the __init__() method, and remove the default arguments and default argument substitution code.