GigaProjects

← Back to tower-of-hanoi

hanoi.py

#!/usr/bin/env python3
"""
Tower of Hanoi CLI Game
A command-line implementation of the classic Tower of Hanoi puzzle.
"""

import os
import sys
import tty
import termios


class TowerOfHanoi:
    def __init__(self, num_rings):
        """Initialize the game with the specified number of rings."""
        self.num_rings = num_rings
        self.towers = {
            'A': list(range(num_rings, 0, -1)),  # Start tower with all rings
            'S': [],  # Middle tower (empty)
            'D': []   # Target tower (empty)
        }
        self.target = 'D'
        self.moves = 0
        self.min_moves = (2 ** num_rings) - 1
    
    def getch(self):
        """Read a single character from stdin without echo."""
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch
    
    def clear_screen(self):
        """Clear the terminal screen using ANSI escape codes for seamless refresh."""
        # Move cursor to top-left and clear screen
        print('\033[2J\033[H', end='', flush=True)
    
    def move_cursor(self, row, col):
        """Move cursor to specific position (1-indexed)."""
        print(f'\033[{row};{col}H', end='', flush=True)
    
    def hide_cursor(self):
        """Hide the cursor."""
        print('\033[?25l', end='', flush=True)
    
    def show_cursor(self):
        """Show the cursor."""
        print('\033[?25h', end='', flush=True)
    
    def draw_tower(self, error_msg="", selected_tower=None):
        """Draw the current state of all towers."""
        max_height = self.num_rings
        max_width = self.num_rings * 2 + 1
        
        print("\n")
        print(f"Moves: {self.moves} | Minimum possible: {self.min_moves}")
        print()
        
        # Draw towers from top to bottom
        for level in range(max_height - 1, -1, -1):
            line = "  "
            for tower_name in ['A', 'S', 'D']:
                tower = self.towers[tower_name]
                
                if level < len(tower):
                    ring_size = tower[level]
                    ring = '█' * (ring_size * 2 - 1)
                    padding = (max_width - len(ring)) // 2
                    cell = ' ' * padding + ring + ' ' * padding
                    
                    # Highlight selected ring (top ring of selected tower)
                    is_selected = (selected_tower == tower_name and 
                                   level == len(tower) - 1)
                    
                    if is_selected:
                        # Yellow color for selected ring
                        line += f"\033[93m{cell}\033[0m  "
                    elif tower_name == self.target:
                        # Green color for target tower
                        line += f"\033[92m{cell}\033[0m  "
                    else:
                        line += f"{cell}  "
                else:
                    # Just the pole
                    padding = max_width // 2
                    cell = ' ' * padding + '│' + ' ' * padding
                
                    # Highlight target tower
                    if tower_name == self.target:
                        line += f"\033[92m{cell}\033[0m  "
                    else:
                        line += f"{cell}  "
            
            print(line)
        
        # Draw base
        base_line = "  "
        for tower_name in ['A', 'S', 'D']:
            base = '═' * max_width
            if tower_name == self.target:
                base_line += f"\033[92m{base}\033[0m  "
            else:
                base_line += f"{base}  "
        print(base_line)
        
        # Draw labels
        label_line = "  "
        for tower_name in ['A', 'S', 'D']:
            padding = max_width // 2
            label = ' ' * padding + tower_name + ' ' * padding
            if tower_name == self.target:
                label_line += f"\033[92m{label}\033[0m  "
            else:
                label_line += f"{label}  "
        print(label_line)
        print()
        
        # Print error message in a fixed position if exists
        if error_msg:
            print(f"\033[91m  ⚠ {error_msg}\033[0m")  # Red color for errors
        else:
            print()  # Empty line to keep spacing consistent
    
    def is_valid_move(self, from_tower, to_tower):
        """Check if a move is valid according to Tower of Hanoi rules."""
        # Check if towers exist
        if from_tower not in self.towers or to_tower not in self.towers:
            return False, "Invalid tower selection!"
        
        # Check if moving to the same tower
        if from_tower == to_tower:
            return False, "Cannot move to the same tower!"
        
        # Check if from_tower has rings
        if not self.towers[from_tower]:
            return False, f"Tower {from_tower} is empty!"
        
        # Check if we can place the ring (smaller on larger)
        if self.towers[to_tower]:
            if self.towers[from_tower][-1] > self.towers[to_tower][-1]:
                return False, "Cannot place a larger ring on a smaller ring!"
        
        return True, ""
    
    def move_ring(self, from_tower, to_tower):
        """Move a ring from one tower to another."""
        valid, error_msg = self.is_valid_move(from_tower, to_tower)
        
        if not valid:
            return False, error_msg
        
        # Perform the move
        ring = self.towers[from_tower].pop()
        self.towers[to_tower].append(ring)
        self.moves += 1
        
        return True, f"Moved ring {ring} from {from_tower} to {to_tower}"
    
    def is_won(self):
        """Check if the game is won."""
        return len(self.towers[self.target]) == self.num_rings
    
    def play(self):
        """Main game loop."""
        error_msg = ""
        
        while True:
            self.clear_screen()
            
            # Check win condition first
            if self.is_won():
                self.draw_tower()
                print(f"CONGRATULATIONS! You won in {self.moves} moves!")
                if self.moves == self.min_moves:
                    print("PERFECT! You solved it in the minimum number of moves!")
                else:
                    print(f"The minimum possible was {self.min_moves} moves.")
                break
            
            # Initialize selected_tower for this iteration
            selected_tower = None
            
            # Draw towers with any error message
            self.draw_tower(error_msg, selected_tower)
            error_msg = ""  # Clear error for next iteration
            
            # Get user input - simplified prompt
            print("  Your move: ", end='', flush=True)
            
            # Read characters one by one
            user_input = ""
            selected_tower = None
            
            while True:
                char = self.getch()
                
                # Handle Ctrl+C
                if char == '\x03':  # Ctrl+C
                    raise KeyboardInterrupt
                
                # Handle backspace (both DEL and BACKSPACE)
                if char in ['\x7f', '\x08']:
                    if len(user_input) > 0:
                        user_input = user_input[:-1]
                        selected_tower = None
                        # Redraw without selection
                        self.clear_screen()
                        self.draw_tower(error_msg)
                        print("  Your move: " + user_input, end='', flush=True)
                    continue
                
                char = char.upper()
                
                # Handle special single-character commands
                if char in ['Q', 'R']:
                    print(char)
                    user_input = char
                    break
                
                # For two-character commands (tower moves)
                if char in ['A', 'S', 'D']:
                    print(char, end='', flush=True)
                    user_input += char
                    
                    # After first character, redraw with selection
                    if len(user_input) == 1:
                        selected_tower = char
                        # Check if tower has rings
                        if self.towers[selected_tower]:
                            self.clear_screen()
                            self.draw_tower(error_msg, selected_tower)
                            print("  Your move: " + user_input, end='', flush=True)
                    
                    if len(user_input) == 2:
                        print()  # New line after second character
                        break
            
            # Process the input
            if user_input == 'Q':
                print("\nThanks for playing! Goodbye!\n")
                break
            elif user_input == 'R':
                return True  # Signal to restart
            elif len(user_input) == 2:
                from_tower = user_input[0]
                to_tower = user_input[1]
                
                success, message = self.move_ring(from_tower, to_tower)
                
                if not success:
                    error_msg = message
            else:
                error_msg = "Invalid input!"
        
        return False  # Don't restart


def select_difficulty():
    """Let the user select the number of rings."""
    os.system('clear' if os.name != 'nt' else 'cls')    
    print(f"""
▀▛▘                ▗▀▖ ▌ ▌         ▗ 
 ▌▞▀▖▌  ▌▞▀▖▙▀▖ ▞▀▖▐   ▙▄▌▝▀▖▛▀▖▞▀▖▄ 
 ▌▌ ▌▐▐▐ ▛▀ ▌   ▌ ▌▜▀  ▌ ▌▞▀▌▌ ▌▌ ▌▐ 
 ▘▝▀  ▘▘ ▝▀▘▘   ▝▀ ▐   ▘ ▘▝▀▘▘ ▘▝▀ ▀▘
    """)   
    print("A classic puzzle game: Move all rings from Tower A to Tower D")
    print()
    print("Rules:")
    print("  • Only one ring can be moved at a time")
    print("  • A ring can only be placed on top of a larger ring")
    print("  • You can use Tower S as an auxiliary tower")
    print()
    print()
    
    while True:
        try:
            num_rings = int(input("Select number of rings (1-10): ").strip())
            if 1 <= num_rings <= 10:
                return num_rings
            else:
                print("Please enter a number between 1 and 10.")
        except ValueError:
            print("Please enter a valid number.")
        except KeyboardInterrupt:
            print("\n\nGoodbye!\n")
            sys.exit(0)


def main():
    """Main function to run the game."""
    try:
        while True:
            num_rings = select_difficulty()
            game = TowerOfHanoi(num_rings)
            restart = game.play()
            
            if not restart:
                break
    except KeyboardInterrupt:
        print("\n\nGame interrupted. Goodbye!\n")
        sys.exit(0)


if __name__ == "__main__":
    main()

Run this code