Mix.install([
{:kino, "~> 0.17.0"}
])
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()