Skip to content

Instantly share code, notes, and snippets.

@DeedleFake
Created July 6, 2026 17:38
Show Gist options
  • Select an option

  • Save DeedleFake/48cdc16aa94edcbb828b5c8e5f084927 to your computer and use it in GitHub Desktop.

Select an option

Save DeedleFake/48cdc16aa94edcbb828b5c8e5f084927 to your computer and use it in GitHub Desktop.
Livebook Towers of Hanoi Solver

Towers of Hanoi

Mix.install([
  {:kino, "~> 0.17.0"}
])

Section

defmodule Hanoi do
  def solve(n) when n >= 1 do
    start = :lists.seq(1, n)

    [{start, [], []}]
    |> move(n, 0, 2)
    |> Enum.reverse()
  end

  defp move([state | _] = prev, 1, src, dst) do
    [v | s] = elem(state, src)
    d = [v | elem(state, dst)]

    state =
      state
      |> put_elem(src, s)
      |> put_elem(dst, d)

    [state | prev]
  end

  defp move(state, n, src, dst) when n > 1 do
    tmp = 3 - (src + dst)

    state
    |> move(n - 1, src, tmp)
    |> move(1, src, dst)
    |> move(n - 1, tmp, dst)
  end
end
defmodule Hanoi.Live do
  use Kino.JS
  use Kino.JS.Live

  @root """
  <div class="flex flex-col gap-2">
    <form class="flex flex-row gap-1">
      <input type="number" value="5" min="1" name="size" placeholder="Size..." />
      <button type="submit">Solve</button>
      <div class="remaining"></div>
    </form>
    <canvas />
  </div>
  """

  def new(opts \\ []) do
    Kino.JS.Live.new(__MODULE__, opts)
  end

  @impl true
  def init([], ctx) do
    ctx =
      assign(ctx,
        steps: nil,
        tick: nil
      )

    {:ok, ctx}
  end

  @impl true
  def handle_connect(ctx) do
    {:ok, @root, ctx}
  end

  @impl true
  def handle_event("solve", size, ctx) do
    steps = Hanoi.solve(size)

    ctx =
      ctx
      |> assign(steps: steps)
      |> advance()

    {:noreply, ctx}
  end

  @impl true
  def handle_info(:advance, ctx) do
    ctx = advance(ctx)
    {:noreply, ctx}
  end

  asset "main.js" do
    """
    function renderColumn(cc, col, num, size, width, height) {
      cc.fillStyle = 'brown'
      cc.fillRect(
        (2 * num * width + width) / 2 - 5,
        0,
        10,
        cc.canvas.height,
      )

      col.reverse().forEach((s, i) => {
        const w = width * s / size
        const x = (2 * num * width + width) / 2 - (w / 2)
        const y = cc.canvas.height - (i * height) - height

        cc.fillStyle = `hsl(${s * 10} 100% 50%)`
        cc.fillRect(x, y, w, height)
      })
    }

    export function init(ctx, root) {
      ctx.root.innerHTML = root

      const form = ctx.root.querySelector('form')
      const rem = ctx.root.querySelector('.remaining')
      const canvas = ctx.root.querySelector('canvas')

      const cc = canvas.getContext('2d')

      form.addEventListener('submit', (ev) => {
        ev.preventDefault()
        ctx.pushEvent('solve', parseInt(form.elements['size'].value, 10))
      })

      ctx.handleEvent('render', ({step, remaining}) => {
        rem.innerText = `Steps remaining: ${remaining}`

        const size = Math.max(...step.flat())
        const width = cc.canvas.width / 3
        const height = cc.canvas.height / size

        cc.clearRect(0, 0, cc.canvas.width, cc.canvas.height)
        step.forEach((col, i) => renderColumn(cc, col, i, size, width, height))
      })
    }
    """
  end

  defp advance(%{assigns: %{steps: []}} = ctx) do
    ctx
    |> update(:tick, fn tick ->
      :timer.cancel(tick)
      nil
    end)
  end

  defp advance(%{assigns: %{steps: [cur | next]}} = ctx) do
    broadcast_event(ctx, "render", %{
      step: Tuple.to_list(cur),
      remaining: length(next)
    })

    ctx
    |> assign(steps: next)
    |> update(:tick, fn
      nil ->
        {:ok, tick} = :timer.send_interval(1000, :advance)
        tick

      tick ->
        tick
    end)
  end
end

Hanoi.Live.new()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment