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.
2D Component
3D Component
Memory Management
- 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);
}
}
- Disable OrbitControls during animation for better performance
- Use lower disk counts (3-4) for mobile devices
- Reduce
tweenDuration for faster gameplay
- Set
renderer.setPixelRatio(1) on low-end devices
// Conditional quality settings
if (isMobile) {
this.renderer.setPixelRatio(1);
this.setNumDisks(3);
this.setTweenDuration(400);
} else {
this.renderer.setPixelRatio(window.devicePixelRatio);
}
- Dispose Three.js resources on destroy
- Cancel animation frames
- Disconnect ResizeObserver
private cleanupScene(): void {
// Cancel animation loop
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
// Disconnect observer
this.resizeObserver?.disconnect();
// Dispose geometries and materials
this.disks.forEach(disk => {
disk.geometry.dispose();
(disk.material as Material).dispose();
});
// Dispose renderer
this.renderer.dispose();
}
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;
}