ハノイの塔と呼ばれるパズルゲームがある。今回は、PyScriptを使って、ハノイの塔を作ってみよう。ハノイの塔は遊ぶのも作るのも頭の体操になるので挑戦してみよう。

ハノイの塔とは

ハノイの塔は、3本の棒といくつかの円盤を使ったパズルゲームだ。円盤は大きさが異なり、最初は1本の棒に大きいものから小さいものへと積み上げられている。目的は、すべての円盤を別の棒に移動することだが、以下のルールに従う必要がある。

  • 一度に移動できるのは1枚だけ
  • 小さい円盤の上に大きい円盤を置くことはできない
  • 円盤は空の棒にのみ置くことができる

PyScriptでハノイの塔を作ろう

なお、今回は、最初に3枚の円盤を左側の棒に積んでおく。そして、この円盤を、右側の棒に積み替えるゲームを作ってみよう。簡単なように見えるが、上記2のルールのために、思ったように移動できないことが多い。頭を使って考えながら遊んでみよう。

今回は、まず、プログラムの動作を確認するために、遊んでいるところを確認してみよう。次の画面の左側のブラウザが初期状態で、右側のブラウザの状態がゲームクリアの状態だ。

  • ハノイの塔を遊んでいるところ

    ハノイの塔を遊んでいるところ

遊び方だが、左側の棒に三枚の円盤が積まれている。最初に円盤を移動したい棒をクリックして、次いで、移動先の棒をクリックすると円盤を移動できる。左側に積まれている円盤を、右側の棒に移動するのを目標としよう。

それでは、実際にPyScriptでハノイの塔を作るためのコードを見てみよう。以下のコードをhanoi.htmlという名前で保存して、ブラウザにドラッグ&ドロップして実行しよう。なお、こちらのGistにもアップロードしたので、そちらからもダウンロードできる。

<!DOCTYPE html>
<html><head><meta charset="utf-8" />
<title>ハノイの塔(PyScript版)</title>
<link rel="stylesheet" href="https://pyscript.net/releases/2025.7.3/core.css">
<script type="module" src="https://pyscript.net/releases/2025.7.3/core.js"></script>
<style>
  canvas { border: 1px solid #000; background: #f8f8f8; }
  #controls { margin: 1em; }
  #message { color: red; margin-top: 1em; }
</style>
</head><body>
<h1>ハノイの塔</h1>
<div id="controls"><button id="start_btn">開始</button></div>
<canvas id="gameCanvas" width="500" height="300"></canvas>
<div id="message"></div>
<script type="py">
# --- ここからPythonのコードを記述 --- (*1)
from js import document
from pyodide.ffi import create_proxy
# canvasなどのDOMオブジェクトを取得 --- (*2)
canvas = document.getElementById("gameCanvas")
ctx = canvas.getContext("2d") # 描画用コンテキストを取得
message = document.getElementById("message") # メッセージの表示オブジェクト
# グローバル変数の初期化 --- (*3)
NUM_DISKS = 3 # 円盤の数
pegs = [[], [], []]  # 各棒にあるディスクを管理
selected_peg = None # 選択中の棒

def draw():
    """ 画面の描画処理 """ # --- (*4)
    ctx.clearRect(0, 0, 500, 300)
    for i in range(3):
        # 棒の描画 --- (*5)
        x = 100 + i * 150
        if selected_peg == i:
            ctx.fillStyle = "red"
        else:
            ctx.fillStyle = "#000"
        ctx.fillRect(x - 5, 100, 10, 150)
        # 円盤(disk)の描画 --- (*6)
        for j, disk in enumerate(pegs[i]):
            y = 240 - j * 20
            width = 20 + disk * 30
            ctx.fillStyle = f"rgb({100+disk*50},100,200)"
            ctx.fillRect(x - width//2, y, width, 15)

def move(from_peg, to_peg):
    """ ディスクを移動する処理 """ # --- (*7)
    if not pegs[from_peg]:
        message.innerText = "ディスクがありません。"
        return
    if pegs[to_peg] and pegs[to_peg][-1] < pegs[from_peg][-1]:
        message.innerText = "大きいディスクは小さいディスクの上に置けません。"
        return
    disk = pegs[from_peg].pop()
    pegs[to_peg].append(disk)
    message.innerText = ""
    draw()

def handle_click(event):
    """ クリックイベントの処理 """ # --- (*8)
    global selected_peg
    rect = canvas.getBoundingClientRect()
    x = event.clientX - rect.left
    peg_clicked = int(x // (500 // 3))
    # 選択中の棒がなければ選択して、選択したものがあれば移動を試みる --- (*9)
    if selected_peg is None:
        selected_peg = peg_clicked
    else:
        move(selected_peg, peg_clicked)
        selected_peg = None
    draw()

# キャンバスをクリック時の処理を設定 --- (*10)
canvas.addEventListener("click", create_proxy(handle_click))

def start_game(event=None):
    """ ゲームを開始する処理 """ # --- (*11)
    global selected_peg
    selected_peg = None
    pegs[0] = list(reversed(range(NUM_DISKS)))
    pegs[1] = []
    pegs[2] = []
    message.innerText = ""
    draw()

# スタートボタンを押した時の処理を記述 --- (*12)
document.getElementById("start_btn").addEventListener(
    "click", create_proxy(start_game))
</script>
</body>
</html>

プログラムを確認してみよう。

(*1)以下の部分が、Pythonコードとなる。PyScriptがjsパッケージを提供しており、「from js import document」と書くことで、HTMLのオブジェクト構造DOMにアクセスするDocumentオブジェクトを利用できる。また、後述するが「from pyodide.ffi import create_proxy」と書くことで、JavaScriptのイベントハンドラをPythonで書くためのプロキシ機能を利用できるようになる。

(*2)では、HTML内の「<canvas>」を操作するためのDOMオブジェクトを取得する。また、同時にメッセージ表示用の要素も取得する。「getContext("2d")」と書く事で2D描画用のコンテキストを取得し、これによってキャンバスへの描画が可能になる。

(*3)では、ゲームの状態を表すグローバル変数を初期化している。変数NUM_DISKSは円盤の数で、pegsは3本の棒にある円盤をリストで管理する。また、変数selected_pegは選択中の棒の番号を管理する。

(*4)で定義したdraw関数は、キャンバスを更新して棒やディスクを描画する。(*5)の部分では、3本の棒を描画する。選択された棒は赤で強調するようにした。

(*6)では、各棒にあるディスクを描画する。円盤のサイズと色は番号に応じて変えている。

(*7)で定義しているmove関数は、選択された2つの棒の間でディスクを移動する。ルールに反する場合(大きいディスクを小さいディスクの上に乗せるなど)はメッセージを表示して無視するようにした。

(*8)の関数handle_clickでは、キャンバスのクリックイベントの処理を記述した。キャンバスを3分割して、どの棒を選択したのかを判定している。(*9)では、クリックされた棒がまだ選択されていなければ選択し、既に選択されていれば移動を試みた。(*10)では、キャンバスがクリックされた時に 関数handle_clickが呼ばれるよう設定した。

(*11)では関数start_gameを定義している。ここでは、ゲームを初期状態にリセットして開始する。すべての円盤を最初の棒(左側)に積んで描画する。

(*12)では、HTMLの「開始」ボタンが押された時に関数start_gameを実行する。

円盤を5枚に増やしてみよう

プログラムの(*3)にある変数NUM_DISKSを5に変更すると、円盤の数を5枚に増やすことができる。画面としては次のようになる。

  • 円盤を5枚に増やしたところ

    円盤を5枚に増やしたところ

ハノイの塔の解き方

なお、プログラムを作って、ハノイの塔の解を自動で求めることもできる。以下は、3枚の円盤を左から右に移動するための手順を出力するプログラムだ。再帰を利用する事で、簡潔にプログラムを書くことができる。

def hanoi(n, from_peg, to_peg, aux_peg):
    if n == 1:
        print(f"移動> 円盤1を {from_peg} から {to_peg} へ")
    else:
        hanoi(n - 1, from_peg, aux_peg, to_peg)
        print(f"移動> 円盤{n}を {from_peg} から {to_peg} へ")
        hanoi(n - 1, aux_peg, to_peg, from_peg)
hanoi(3, 'A', 'C', 'B')

上記のプログラムを「kai.py」という名前で保存してから、以下のコマンドを実行すると、ハノイの塔の解を表示することができる。

$ python kai.py

プログラムを実行すると以下のような出力が得られる。

  • ハノイの塔の解を表示するプログラム

    ハノイの塔の解を表示するプログラム

まとめ

以上、ハノイの塔はシンプルなルールながら、頭を使う秀逸なパズルゲームだ。PyScriptを使うことで、ブラウザ上で簡単にハノイの塔を実装できることがわかっただろう。さらに、Pythonの再帰を利用して、ハノイの塔の解を求めるプログラムも作ってみた。円盤の枚数を増やすと、より複雑なパズルになるので、ぜひ試してみてほしい。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。これまで50冊以上の技術書を執筆した。直近では、「大規模言語モデルを使いこなすためのプロンプトエンジニアリングの教科書(マイナビ出版)」「Pythonでつくるデスクトップアプリ(ソシム)」「実践力を身につける Pythonの教科書 第2版」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」など。