Skip to main content

Overview

The Tower of Hanoi implementation includes both a 2D auto-solver with step-by-step visualization and a fully interactive 3D version built with Three.js. The 3D variant features real-time controls, customizable animations, and extensive scene configuration.

Components

GameHanoiAutoComponent (2D)

Selector: app-game-hanoi-auto Location: src/app/_modules/_Demos/_DemosFeatures/games/game-hanoi-auto/game-hanoi-auto.component.ts Automated solver with visual step-through playback.

GameHanoi3dComponent (3D)

Selector: app-game-hanoi3d Location: src/app/_modules/_Demos/_DemosFeatures/games/game-hanoi3d/game-hanoi3d.component.ts Interactive 3D puzzle with manual play and customizable scene.

Key features

2D auto-solver

  • Automatic solution - Recursive algorithm generates optimal move sequence
  • Step-by-step playback - Visual animation of disk movements
  • Configurable disk count - 3-4 disk options
  • Move tracking - Complete history of all moves
  • Scrollable output - Auto-scrolling step display

3D interactive

  • Three.js rendering - Hardware-accelerated 3D graphics
  • Manual play - Click towers to move disks
  • Orbit controls - Rotate, zoom, and pan camera
  • Smooth animations - Tween-based disk movements with easing
  • Live configuration - Real-time scene parameter adjustments
  • Pause/Resume - Control animation playback
  • Preset modes - Quick-load configuration presets
  • Mobile responsive - ResizeObserver for dynamic viewport

HanoiEngine (2D)

Location: src/app/_engines/hanoi-engine.ts

Signal-based state (Angular v21)

// Game state signals
towers: Signal<Disk[][]>           // Three towers with disk arrays
moves: Signal<number>               // Move counter
isWin: Signal<boolean>              // Computed win condition

// Internal state
board: number[][]                   // Legacy board representation
steps: string[]                     // Move descriptions
_steps: HanoiStep[]                // Structured move data
_stepsIndex: number                 // Current step in playback

Core methods

Manual play (3D component)

manual_selectTower(towerIndex: number): void
// Click handler for tower selection
// First click: select source tower
// Second click: select destination tower
// Validates move and updates state

manual_moveDisk(fromTower: number, toTower: number): void
// Moves disk between towers
// Validates: larger disk can't go on smaller disk
// Updates game state signals immutably
// Increments move counter
// Win condition computed automatically

manual_resetGame(): void  
// Resets to initial state:
// Tower A: [3, 2, 1] (large to small)
// Tower B: []
// Tower C: []
// Move count: 0

Auto-solver (2D component)

auto_towerOfHanoi(n: number, from: string, to: string, aux: string): void
// Recursive solution generator
// Saves each move to _steps array
// Classic divide-and-conquer algorithm

auto_printSteps(): void
// Animates step-by-step solution
// Updates visual towers
// Displays move descriptions
// Auto-scrolls to current step
// Uses configurable delay (_delayInMilliseconds)

auto_makeMove(hanoiStep: HanoiStep): void
// Executes a single step
// Updates tower Maps (towerA, towerB, towerC)
// Moves disk from source to destination

auto_startGame(): void
// Initializes new game
// Generates solution with auto_towerOfHanoi()
// Begins animated playback

Data structures

interface Disk {
  size: number;  // 1 (small) to 3 (large)
}

class HanoiStep {
  constructor(
    public n: number,      // Disk number
    public from: string,   // Source tower ('A', 'B', 'C')
    public to: string      // Destination tower
  )
}

class DiskInfo {
  constructor(
    public value: number,   // Disk size
    public graph: string    // Visual representation ("*", "**", "***")
  )
}

Usage examples

2D auto-solver

<div class="hanoi-2d">
  <!-- Disk count selector -->
  <select [(ngModel)]="hanoiEngine._diskAmt">
    <option [value]="3">3 Disks</option>
    <option [value]="4">4 Disks</option>
  </select>
  
  <!-- Start button -->
  <button (click)="hanoiEngine.auto_startGame()">
    Start Auto-Solve
  </button>
  
  <!-- Tower visualization -->
  <div class="towers">
    <div class="tower">
      <h3>Tower A</h3>
      <div *ngFor="let disk of getTowerArray(hanoiEngine.towerA)">
        {{ disk?.graph }}
      </div>
    </div>
    
    <div class="tower">
      <h3>Tower B</h3>
      <div *ngFor="let disk of getTowerArray(hanoiEngine.towerB)">
        {{ disk?.graph }}
      </div>
    </div>
    
    <div class="tower">
      <h3>Tower C</h3>
      <div *ngFor="let disk of getTowerArray(hanoiEngine.towerC)">
        {{ disk?.graph }}
      </div>
    </div>
  </div>
  
  <!-- Step history (scrollable) -->
  <div class="steps-container">
    <div *ngFor="let step of hanoiEngine.steps">
      {{ step }}
    </div>
  </div>
</div>

3D interactive

<div class="hanoi-3d">
  <!-- Three.js container -->
  <div #rendererContainer class="renderer-container"></div>
  
  <!-- Game controls -->
  <div class="controls">
    <button (click)="startPuzzle()" [disabled]="hasStarted()">
      Start
    </button>
    
    <button (click)="togglePause()" [disabled]="!hasStarted()">
      {{ isPaused() ? 'Resume' : 'Pause' }}
    </button>
    
    <button (click)="restart()">
      Restart
    </button>
    
    <button (click)="toggleControls()">
      {{ showControls() ? 'Hide' : 'Show' }} Settings
    </button>
  </div>
  
  <!-- Progress display -->
  <div class="progress">
    <p>Moves: {{ currentMove() }} / {{ totalMoves() }}</p>
    <p>Progress: {{ progressPct() }}%</p>
    <div class="progress-bar">
      <div class="fill" [style.width.%]="progressPct()"></div>
    </div>
  </div>
  
  <!-- Win message -->
  <div *ngIf="isSolved()" class="win-message">
    Puzzle Solved in {{ totalMoves() }} moves!
  </div>
  
  <!-- Settings panel (collapsible) -->
  <div *ngIf="showControls()" class="settings-panel">
    <!-- Preset modes -->
    <div class="presets">
      <button (click)="resetToDefaults()">Reset to Defaults</button>
    </div>
    
    <!-- Disk count -->
    <label>
      Disks: {{ numDisks() }}
      <input 
        type="range" 
        min="3" 
        max="7" 
        [value]="numDisks()"
        (input)="setNumDisks(+$any($event.target).value)" />
    </label>
    
    <!-- Camera controls -->
    <label>
      Camera FOV: {{ cameraFov() }}
      <input 
        type="range" 
        min="40" 
        max="120" 
        [value]="cameraFov()"
        (input)="setCameraFov(+$any($event.target).value)" />
    </label>
    
    <label>
      Camera Y: {{ cameraY() }}
      <input 
        type="range" 
        min="5" 
        max="20" 
        [value]="cameraY()"
        (input)="setCameraY(+$any($event.target).value)" />
    </label>
    
    <!-- Animation controls -->
    <label>
      Animation Speed: {{ tweenDuration() }}ms
      <input 
        type="range" 
        min="200" 
        max="1500" 
        step="50"
        [value]="tweenDuration()"
        (input)="setTweenDuration(+$any($event.target).value)" />
    </label>
    
    <label>
      Lift Height: {{ tweenLiftHeight() }}
      <input 
        type="range" 
        min="4" 
        max="12" 
        [value]="tweenLiftHeight()"
        (input)="setTweenLiftHeight(+$any($event.target).value)" />
    </label>
    
    <!-- Color controls -->
    <label>
      Background Color
      <input 
        type="color" 
        [value]="bgColor()"
        (input)="setBgColor($any($event.target).value)" />
    </label>
    
    <label>
      Tower Color
      <input 
        type="color" 
        [value]="towerColor()"
        (input)="setTowerColor($any($event.target).value)" />
    </label>
    
    <!-- Disk colors -->
    <div *ngFor="let color of diskColors(); let i = index">
      <label>
        Disk {{ i + 1 }} Color
        <input 
          type="color" 
          [value]="color"
          (input)="setDiskColor(i, $any($event.target).value)" />
      </label>
    </div>
    
    <!-- Lighting -->
    <label>
      Ambient Light: {{ ambientIntensity() }}
      <input 
        type="range" 
        min="0" 
        max="2" 
        step="0.1"
        [value]="ambientIntensity()"
        (input)="setAmbientIntensity(+$any($event.target).value)" />
    </label>
  </div>
</div>

3D scene configuration

Configuration interface

interface HanoiSceneConfig {
  // Puzzle
  numDisks: number;              // 3-7 disks
  
  // Camera
  cameraFov: number;             // 40-120 degrees
  cameraY: number;               // 5-20 units
  cameraZ: number;               // Distance from scene
  
  // Lighting  
  ambientIntensity: number;      // 0.0-2.0
  
  // Animation
  tweenDuration: number;         // 200-1500ms
  tweenLiftHeight: number;       // 4-12 units
  
  // Colors
  bgColor: string;               // Hex color
  towerColor: string;            // Hex color
  diskColors: string[];          // Array of hex colors
}

Default configuration

const DEFAULT_CONFIG: HanoiSceneConfig = {
  numDisks: 3,
  cameraFov: 75,
  cameraY: 10,
  cameraZ: 15,
  ambientIntensity: 0.8,
  tweenDuration: 600,
  tweenLiftHeight: 8,
  bgColor: '#1a1a1a',
  towerColor: '#888888',
  diskColors: [
    '#e74c3c',  // Red
    '#3498db',  // Blue
    '#2ecc71',  // Green
    '#f39c12',  // Orange
    '#9b59b6',  // Purple
    '#1abc9c',  // Teal
    '#e67e22'   // Dark orange
  ]
};

3D component signals (Angular v21)

// Configuration signal
config: Signal<HanoiSceneConfig>

// Computed config slices
numDisks: Signal<number>
cameraFov: Signal<number>
tweenDuration: Signal<number>
bgColor: Signal<string>
diskColors: Signal<string[]>
// ... and more

// Game state signals
currentMove: Signal<number>        // Current move index
isAnimating: Signal<boolean>       // Animation in progress
moves: Signal<HanoiMove[]>        // Planned move sequence
totalMoves: Signal<number>         // Total moves to solve
hasStarted: Signal<boolean>        // Game started flag
isPaused: Signal<boolean>          // Pause state
showControls: Signal<boolean>      // Settings panel visibility

// Computed signals
progressPct: Signal<number>        // Completion percentage
isSolved: Signal<boolean>          // Win condition

3D animation system

The 3D component uses Tween.js for smooth animations:
// Disk movement sequence:
// 1. Lift disk vertically
// 2. Move horizontally to target tower  
// 3. Drop disk onto target position

private _startAnimation(
  fromY: number, 
  toY: number, 
  rotationSteps: number, 
  onComplete: () => void
): void {
  // Phase 1: Lift
  new Tween(disk.position)
    .to({ y: liftHeight }, duration)
    .easing(Easing.Quadratic.Out)
    .onComplete(() => {
      // Phase 2: Horizontal move
      new Tween(disk.position)
        .to({ x: targetX }, duration)
        .easing(Easing.Quadratic.InOut)
        .onComplete(() => {
          // Phase 3: Drop
          new Tween(disk.position)
            .to({ y: targetY }, duration)
            .easing(Easing.Bounce.Out)
            .onComplete(() => {
              // Update game state
              this.currentMoveSignal.update(m => m + 1);
              this.isAnimatingSignal.set(false);
            })
            .start();
        })
        .start();
    })
    .start();
}

Tower of Hanoi algorithm

The recursive solution:
// To move n disks from source to target using auxiliary:
// 1. Move n-1 disks from source to auxiliary (using target)
// 2. Move disk n from source to target
// 3. Move n-1 disks from auxiliary to target (using source)

function towerOfHanoi(n: number, from: string, to: string, aux: string): void {
  if (n === 0) return;
  
  // Step 1: Move n-1 disks to auxiliary
  towerOfHanoi(n - 1, from, aux, to);
  
  // Step 2: Move largest disk to target
  console.log(`Move disk ${n} from ${from} to ${to}`);
  
  // Step 3: Move n-1 disks from auxiliary to target
  towerOfHanoi(n - 1, aux, to, from);
}

// Minimum moves = 2^n - 1
// 3 disks = 7 moves
// 4 disks = 15 moves  
// 5 disks = 31 moves
// 6 disks = 63 moves
// 7 disks = 127 moves

Mobile responsiveness (3D)

The 3D component uses ResizeObserver for dynamic viewport adjustments:
private onContainerResize(el: HTMLElement): void {
  if (!this.renderer || !this.camera) return;
  
  const w = el.clientWidth;
  const h = el.clientHeight;
  
  if (w === 0 || h === 0) return;
  
  // Update renderer size
  this.renderer.setSize(w, h);
  
  // Update camera aspect ratio
  this.camera.aspect = w / h;
  this.camera.updateProjectionMatrix();
}

// Observed in constructor
this.resizeObserver = new ResizeObserver(() => 
  this.onContainerResize(containerElement)
);
this.resizeObserver.observe(containerElement);
The ResizeObserver automatically handles orientation changes on mobile devices and window resizes on desktop, ensuring the 3D scene always renders at the correct aspect ratio.

Performance tips

  • Adjust _delayInMilliseconds for slower devices
  • Limit disk count to 4 for faster solutions
  • Clear timeout on component destroy
ngOnDestroy() {
  if (this.hanoiEngine._timeoutId) {
    clearTimeout(this.hanoiEngine._timeoutId);
  }
}

Styling examples

2D towers

.towers {
  display: flex;
  justify-content: space-around;
  margin: 2rem 0;
}

.tower {
  flex: 1;
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  min-height: 200px;
  border: 2px solid #333;
  padding: 1rem;
  background: #f5f5f5;
}

.tower h3 {
  margin-bottom: 1rem;
}

.steps-container {
  max-height: 300px;
  overflow-y: auto;
  border: 1px solid #ccc;
  padding: 1rem;
  font-family: monospace;
}

3D container

.renderer-container {
  width: 100%;
  height: 600px;
  position: relative;
  background: #1a1a1a;
  border-radius: 8px;
  overflow: hidden;
}

@media (max-width: 768px) {
  .renderer-container {
    height: 400px;
  }
}

.settings-panel {
  position: absolute;
  right: 1rem;
  top: 1rem;
  background: rgba(255, 255, 255, 0.95);
  padding: 1rem;
  border-radius: 8px;
  max-width: 300px;
  max-height: calc(100% - 2rem);
  overflow-y: auto;
}

.progress-bar {
  width: 100%;
  height: 8px;
  background: #e0e0e0;
  border-radius: 4px;
  overflow: hidden;
}

.progress-bar .fill {
  height: 100%;
  background: linear-gradient(90deg, #4caf50, #8bc34a);
  transition: width 0.3s ease;
}

Build docs developers (and LLMs) love