PyScriptを使うと、ブラウザ上で手軽にPythonを実行できる。JavaScriptを使わなくても、大抵のことがPythonだけで実現できる。図形の描画ができるのでゲームも開発できる。ライフゲームを実装してみて、使い方を確認してみよう。

  • PyScriptでライフゲームを作ってみよう

    PyScriptでライフゲームを作ってみよう

PyScriptとは何だろう?

PyScriptとは、Pythonをブラウザ上で直接実行できるようにしたフレームワークだ。HTMLに<script type="py">…</script>と記述するだけで、Pythonをブラウザで動かすことができる。

本連載の110回目(https://news.mynavi.jp/techplus/article/zeropython-110/)でも、簡単にPyScriptについて紹介したが、その後も順調にバージョンアップを重ねている。仕組み的には、PythonをWebAssemblyとして実行する「Pyodide」をベースにして開発されている。そのため、初回実行時にはPythonランタイム(WebAssembly)の読み込みに少し時間がかかる。

PyScriptについての詳しいドキュメント(英語)は、こちら(https://docs.pyscript.net/2025.3.1/)から確認できる。

  • PyScriptのWebサイト

    PyScriptのWebサイト

PyScriptでライフゲームを作ろう

ライフゲームとは、二次元のグリッドで生物(セル)の生死を表現するものだ。本連載の9回目(https://news.mynavi.jp/techplus/article/zeropython-9/)でも紹介したが、その時は、Tkinterを利用したため、プログラムを実行するには、ローカルPCにPythonをインストールする必要があった。PyScriptを使って作り直すことで、ブラウザ上で使えるようになる。

ライフゲームと名前が付いているものの、基本的にプレイヤーは操作することなく、セルの生死を観察するシミュレーションだ。詳しいルールは以前の連載を確認してみて欲しい。

  • ライフゲームを実行したところ

    ライフゲームを実行したところ

簡単に紹介すると、「生物は過疎でも過密でも生きてはいけない」という基本的なルールに基づいたシミュレーションを行う。具体的には、生物の周囲(8方向)を調べて、何匹の生きたセルがあるかによって、次の世代の生死が決定する。

- 生きているセルが3つ → 次世代に生物が誕生
- 生きているセルが2つか3つ → 次の世代に生物は継続して生存
- 生きているセルが1つ以下(過疎状態) → 次の世代に死滅
- 生きているセルが4つ以上(過密状態) → 次の世代に死滅

PyScriptのプログラムを作ってみよう

今回、PyScriptを用いてライフゲームを作ると100行ほどになった。100行と言えど、ちょっと長いので、パーツごとに分けて紹介しよう。

プログラム全体をこちらのGist(https://gist.github.com/kujirahand/3f1acb7d148f4f9b4ac998f92b048a79)にアップロードした。全体を確認する場合はそちらで確認して欲しい。

プログラムを実行するには、まず、Gistからソースコードをコピーしてテキストエディタなどに貼り付けて、「lifegame.html」というファイル名で保存しよう。そして、このファイルをブラウザにドラッグ&ドロップしよう。「開始する」ボタンを押すと、ライフゲームが始まる。

  • ライフゲームをブラウザで実行したところ

    ライフゲームをブラウザで実行したところ

PyScriptフレームワークの読み込み

それでは、少しずつプログラムを確認してみよう。

PyScriptでは、HTMLの中にPythonのプログラムを記述する。それで、PyScriptのバージョン「2025.3.1」を利用する場合には、次のようなHTMLを記述する。これは、PyScriptのマニュアルに載っているひな形(https://docs.pyscript.net/2025.3.1/beginning-pyscript/)を微修正したものだ。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <!-- PyScriptのライブラリを取り込み -->
    <link rel="stylesheet" href="https://pyscript.net/releases/2025.3.1/core.css">
    <script type="module" src="https://pyscript.net/releases/2025.3.1/core.js"></script>
</head><body>
    <script type="py">
    # ここにPythonプログラムを記述
    </script>
</body></html>

なお、 <script type="py">…</script>の内側にPythonのプログラムを書くことになっている。

ライフゲームを描画するキャンバスや開始ボタンなどを用意

HTMLでライフゲームのような描画が必要なアプリケーションを作るには、HTMLに<canvas>タグを用意する必要がある。そこで、<body>…</body>の間に<canvas>を挿入して「game」というidを割り振ろう。また、ライフゲームを開始するボタンと初期化するボタンを用意し、それぞれに、「start-btn」と「reset-btn」というidを割り振ろう。

<body>
    <h1>ライフゲーム</h1>
    <canvas id="game" width="400" height="400" style="border: 1px solid black">
    </canvas><br />
    <button id="start-btn">▶️ 開始する</button>
    <button id="reset-btn">🔄 初期化</button>
    <script type="py">
    # ここにPythonのプログラム
    </script>
</body>

Pythonのプログラムで描画を行うには?

PyScriptのプログラムで、キャンバスに描画するためには、まず、下記の(*1)でjsパッケージのdocumentオブジェクトを利用する宣言を記述する。続いて、(*2)でdocument.getElementByIdメソッドを利用して、HTMLでid属性を割り振ったオブジェクトを取得する。ここでは、gameというidを割り振ったキャンバスオブジェクトを取得する。なお、キャンバスに描画するためには、getContext("2d")というメソッドを呼び出し、描画用のコンテキストを取得する必要がある。

<script type="py">
    # 必要なライブラリをインポートする --- (*1)
    from js import document, window
    from pyodide.ffi import create_proxy
    import random, math

    # 描画用のコンテキストを取得する --- (*2)
    canvas = document.getElementById("game")
    ctx = canvas.getContext("2d")
</script>

キャンバスに生物を描画しよう

次に、セルの初期化とグリッドを描画する処理を確認してみよう。以下の(*3)の部分では二次元リストのグリッドを初期化する。グリッドの値が1のとき生物がいる、0のとき生物がいないという状態を表す。(*4)の関数draw_gridでは、上記(*2)で取得した描画用のコンテキストを利用して、実際にキャンバスを描画する。ctx.clearRectメソッドで、画面を初期化する。(*5)以下の部分で生物を描画する。円を特定の色で描画するには、このように、beginPathメソッドを呼び出し、arcメソッドで描画し、fillメソッドで塗りつぶし、closePathメソッドでパスを閉じるという手順を踏む。

# グリッドを初期化 --- (*3)
def create_grid():
    global grid
    grid = [[random.choice([0, 0, 1]) for _ in range(cols)] for _ in range(rows)]

# グリッドを描画する --- (*4)
def draw_grid():
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    for y in range(rows):
        for x in range(cols):
            # 値が1の時生物を描画する ---- (*5)
            if grid[y][x] == 1:
                ctx.beginPath()
                ctx.arc(
                    x * cell_size + cell_size / 2,
                    y * cell_size + cell_size / 2,
                    cell_size / 2,
                    0,
                    2 * math.pi
                )
                ctx.fillStyle = "red"
                ctx.fill()
                ctx.closePath()

ライフゲームの世代を進める

次に、ライフゲームで世代を進める処理(*6)を確認しよう。以下の処理は、二次元リストの変数gridを更新する処理だ。左上から右下に向かって、セルを一つずつ確認して、次世代の生物の状態を変更する。

# グリッドの世代を進める --- (*6)
def update_grid():
    global grid
    new_grid = [[0 for _ in range(cols)] for _ in range(rows)]
    for y in range(rows):
        for x in range(cols):
            neighbors = sum(
                grid[(y + dy) % rows][(x + dx) % cols]
                for dy in [-1, 0, 1]
                for dx in [-1, 0, 1]
                if not (dy == 0 and dx == 0)
            )
            if grid[y][x] == 1 and neighbors in [2, 3]:
                new_grid[y][x] = 1
            elif grid[y][x] == 0 and neighbors == 3:
                new_grid[y][x] = 1
    grid = new_grid
    draw_grid()

開始ボタンを押した時の処理

最後に、世代を進めるタイマーを処理する部分を確認しよう。(*7)は開始ボタンを押した時に実行する処理だ。(*8)では、window.setIntervalメソッドを利用してタイマーをセットしている。(*9)では実際にHTMLのボタンをクリックした時の処理を、Pythonの関数に設定している。

# 開始・停止・リセット --- (*7)
def start(event):
    global interval_id
    if interval_id is None:
        # タイマーイベントを設定 --- (*8)
        interval_id = window.setInterval(
            create_proxy(update_grid), 300)

def stop(event):
    global interval_id
    if interval_id is not None:
        window.clearInterval(interval_id)
        interval_id = None

def reset(event):
    stop(None)
    create_grid()
    draw_grid()

# ボタン操作を指定 --- (*9)
document.getElementById("start-btn").onclick = create_proxy(start)
document.getElementById("reset-btn").onclick = create_proxy(reset)

なお、ここで注意が必要な点だが、window.setIntervalメソッドや、HTMLのonclick関数に、Pythonの関数を設定する場合、Pythonの関数をcreate_proxyメソッドでラップする必要がある。

このように記述しないと、ブラウザ側でPythonの関数オブジェクトが正しく保持されず、エラーが発生してしまう。実際に、create_proxyを削ってみると、下記のようなエラーが出る。

Uncaught Error: This borrowed proxy was automatically destroyed at the end of a function call.
Try using create_proxy or create_once_callable.

ブラウザが処理するイベントに関数オブジェクトを与えるときには、create_proxyでラップする必要があることだけ覚えておこう。

まとめ

以上、今回は、PyScriptを使ってライフゲームを作ってみた。ブラウザで気軽にPythonが動かせるPyScriptは便利だ。

JavaScriptに詳しい人であれば、ほとんどPythonとJavaScriptの差異を感じないでプログラムできるだろう。逆に、JavaScriptのことをあまり知らない人であれば、HTMLのCanvas APIについて調べてみよう。Pythonでも、ほぼ同じ名前のメソッドが使えるようになっている。

PyScriptとキャンバスの組合せで、いろいろなゲームを作ることもできるので、挑戦してみると良いだろう。

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