📥 Download raw
{-# OPTIONS --cubical-compatible #-}
module Padova2025.Recreational.Hanoi where

The Tower of Hanoi

open import Padova2025.ProgrammingBasics.Naturals.Base
open import Padova2025.ProgrammingBasics.Vectors
open import Padova2025.ProvingBasics.Negation
open import Padova2025.ProvingBasics.Equality.Base
open import Padova2025.ProvingBasics.Equality.General
open import Padova2025.ProvingBasics.Connectives.Conjunction
open import Padova2025.ProvingBasics.Connectives.Existential
open import Padova2025.ProvingBasics.Connectives.More using (Dec; yes; no)

A finite number of disks is stacked on a peg A. They should be moved to a different peg C. Temporarily, disks may also be parked on a third peg B. The catch: The disks each have a different size, and at no point may a larger disk rest on a smaller one.

This celebrated puzzle has a well-known recursive solution. To move n disks from peg A to C, do the following:

  1. Recursively move the topmost n − 1 disks from A to the auxiliary peg B. The disk which was originally the n-th disk on A is now at the top.
  2. Move that disk from A to the target peg C.
  3. Recursively move the n − 1 disks from peg B to C.

Let us formalize the puzzle and this solution in Agda. With just a tiny adaptation of this strategy, we will even be able to prove that any valid configuration of disks can be reached from any other valid configuration by a valid sequence of moves, and we can run this proof to determine such a valid sequence of moves.

We will follow a correct-by-construction approach, so the description of the strategy and the proof that the strategy is indeed working as it should are unified into a single artefact.

Preliminaries

For a vector xs and a predicate P, the type All P xs expresses that every element of the vector xs satisfies P. An inhabitant of All P xs is a list of suitable witnesses. The following definition is lifted straight from Padova2025.ProvingBasics.Connectives.More, where we discussed the same notion but for lists instead of vectors.

data All {A : Set} (P : A  Set) : {n : }  Vector A n  Set where
  []  : All P []
  _∷_ : {n : } {x : A} {xs : Vector A n}  P x  All P xs  All P (x  xs)
all-replicateV : {n : } {A : Set} {P : A  Set} {x : A}  P x  All P (replicateV n x)
all-replicateV {zero}   px = {!!}
all-replicateV {succ n} px = {!!}

Formalization of the puzzle

The three pegs are encoded by the following type with three constructors.

data Peg : Set where
  A : Peg
  B : Peg
  C : Peg

A configuration of n disks is a vector of pegs of length n, with the i-th entry telling us on which peg the i-th disk is placed. We don’t need to record the disk’s position within its peg, as it is uniquely determined by the size requirement anyway. (But we will need to impose a size-related restriction when formalizing the notion of a valid move.)

Config :   Set
Config n = Vector Peg n

A (valid) game move either relocates the largest disk from its peg to another, or leaves it untouched and is hence a (valid) move in the subgame with the largest disk imagined away. In the first case, for the move to be valid, all smaller disks need to sit on neither the source peg (else the largest disk is not exposed) nor the target peg (else the largest disk cannot be put there).

The type Move c c' is the type of (valid) moves from configuration c to configuration c'.

data Move : {n : }  Config n  Config n  Set where
  here
    : {n : }
     (source target : Peg) {c : Config n}
     source  target  All  p  p  source × p  target) c
     Move (source  c) (target  c)
  there
    : {n : } {c c' : Config n} {p : Peg}
     Move c c'
     Move (p  c) (p  c')

A (valid) play is a sequence of (valid) moves. We track the starting and ending configuration directly in the type signature.

infixr 5 _◅_
data Star {X : Set} (P : X  X  Set) : X  X  Set where
  ε   : {x : X}  Star P x x
  _◅_ : {a b c : X}  (x : P a b)  (xs : Star P b c)  Star P a c
Play : {n : }  Config n  Config n  Set
Play = Star Move

Auxiliary results

Equality of pegs is decidable, by the following boring proof where we just visit every case.

_≡?_ : (p p' : Peg)  Dec (p  p')
A ≡? A = yes refl
A ≡? B = no λ ()
A ≡? C = no λ ()
B ≡? A = no λ ()
B ≡? B = yes refl
B ≡? C = no λ ()
C ≡? A = no λ ()
C ≡? B = no λ ()
C ≡? C = yes refl

We can concatenate sequences of moves.

infixr 5 _◅◅_
_◅◅_ : {X : Set} {P : X  X  Set} {a b c : X}  Star P a b  Star P b c  Star P a c
_◅◅_ = {!!}

We can translate a play in the n-disk subgame without the largest disk to a play in the full (n + 1)-disk game. We call this operation frame in reference to the frame rule of separation logic, which likewise lets one lift a statement about a small part of the system to the whole, leaving everything else untouched.

frame : {n : } (p : Peg) {c c' : Config n}  Play c c'  Play (p  c) (p  c')
frame = {!!}

There is always a third peg.

third : (p p' : Peg)  p  p'  Σ[ q  Peg ] q  p × q  p'
third = {!!}

Formalization of the solution

Given distinct pegs source, target and auxiliary, the following function produces a play which, starting from the configuration that all disks are on source, moves them to target. No separate correctness proof is needed – correctness is already established by the types. (The strategy outlined in the comments is optimal, though this is neither stated nor proven.)

solve₀
  : {n : }  (source target auxiliary : Peg)
   source  target
   source  auxiliary
   target  auxiliary
   Play (replicateV n source) (replicateV n target)
solve₀ {zero}   source target auxiliary s≢t s≢a t≢a = {!!}
solve₀ {succ n} source target auxiliary s≢t s≢a t≢a =
  -- Step 1: Move the topmost n disks to the auxiliary peg.
  {!!} ◅◅
  -- Step 2: Move the now-exposed disk to the target peg.
  {!!} 
  -- Step 3: Move the n disks from their temporary parking position to the target peg.
  {!!}
  where
  a≢t = ≢-sym t≢a
  a≢s = ≢-sym s≢a
  t≢s = ≢-sym s≢t

With just a bit more work, we can show that every (valid) configuration is reachable from any other:

solve : {n : }  (c c' : Config n)  Play c c'
solve []      []        = {!!}
solve (p  c) (p'  c') with p ≡? p'
... | yes refl = {!!}
... | no  p≢p' =
  let (q , q≢p,p') = third p p' p≢p'
      mid          = replicateV _ q
  in
  -- Step 1: Move the topmost n disks to the auxiliary peg.
  {!!} ◅◅
  -- Step 2: Move the now-exposed (n + 1)-th disk to where it belongs.
  {!!} 
  -- Step 3: Move the parked disks to where they belong.
  {!!}

Again, correctness is ensured by the types. (But unlike solve₀, the function solve does not always compute optimal plays.)

Time to run our proofs! Put, for instance, replicateV three A and replicateV three C in the two holes in the type signature and solve _ _ into the remaining hole below. Then use C-c C-v to run example and observe the resulting sequence of moves. (The output will be a bit littered by inequality witnesses.)

example : Play {!!} {!!}
example = {!!}