エンジニアの頭の中

知識と技術で「お金稼ぎの自動化」を実現するため、日々奮闘するエンジニアのブログです。

作って覚える!Pythonista 3を使ったPythonプログラミングのやり方

iOSPythonプログラミングができるPythonista 3 のアプリですが、Pythonista 3のモジュールの利用方法の調査のために、Pythonista 3 を使ってライフゲーム(Conway's Game Of Life)を書いてみました。

Pythonista 3 / omz:software

f:id:mitsu3204:20181016133630j:plain

Pythonista 3もPythonとしての書き方は、通常のPythonと変わりませんが、アニメーションの描画やUIの操作については、Pythonista 3 の独自モジュールの使い方を把握しておかなければなりません。

私は、Pythonista 3に同梱されているサンプルソースと、Pythonista 3の公式ドキュメントを確認しながらコードを書きました。

Pythonista 3とは

iOSバイス上でPythonプログラミングができるアプリです。

www.hacky.xyz

ライフゲームとは

ライフゲームとは、生命の誕生と死滅のシミュレーションプログラムです。

セル(細胞)の状態の変化を眺めて楽しむだけのもので、ゲームと名付けられていますが、ゲーム感はありません。

私がライフゲーム好きなのと、小さい規模で作ることができ、お試しのプログラムとしてはちょうど良いので、題材として選択しました。

ライフゲームについては、過去記事もあるので、興味あるかたはどうぞご覧ください。

www.hacky.xyz

www.hacky.xyz

 実装

大した実装量ではなく、1ファイルに収まる程度です。

ファイルを新規作成する

まずは、以下の手順に従ってファイルを新規作成します。

  1. Pythonista 3 アプリを起動します。

  2. 「New File」をタップして、ファイル新規作成画面を開きます。

  3. テンプレートの一覧の中から「Game / Animation」をタップします。

f:id:mitsu3204:20181017213002j:plain

  1. 画面丈夫のテキストフィールドにファイル名を入力します。名前は何でも良いです。

f:id:mitsu3204:20181017213039j:plain

  1. 名前を入力したら、Createボタンをタップします。

これで新規ファイルが作成されました。「Game / Animation」のテンプレートを使用したソースコードの初期状態は以下のようになっています。

f:id:mitsu3204:20181017213052j:plain

画面右上の「>」ボタンをタップすると、このPythonコードを実行できます。まだ何も具体的な処理を書いていないため、実行すると以下のようにグレーの画面が表示されるだけです。

f:id:mitsu3204:20181017213146j:plain

実行したプログラムは、右上の×ボタンをタップすると終了することができます。

初期化処理を作成

最初からテンプレートの内容として含まれているクラス名や、メイン関数などは変更する必要はありません。メソッドの中身を書き足していくのと、いくつかメソッドを追加したら完成です。

まず、空実装状態のsetupメソッドの中身を書き加えて行きます。

描画する背景の色(background_color)、セルの色(cell_color)、描画するセルの大きさ(cell_size)をインスタンスのメンバとして設定します。色の指定は、blackgreenなどのように、定義済みの色の名前でも良いですし、コードで指定することも可能です。

セルを描画する行と列の数は、セルの大きさと画面の大きさに基づいて、動的に計算します。

最後にinit_cellsメソッド(テンプレートに含まれていたものではなく自分で実装するメソッド)を呼び出して、セルの初期状態を作ります。

def setup(self):
        
    # 背景の色
    self.background_color = 'black'
        
    # セルの色
    self.cell_color = 'green'
        
    # 一つのセルの大きさ
    self.cell_size = 12
        
    # 画面のサイズとセルの大きさをから、行と列の数を計算する
    self.row_num = int(self.size.h / self.cell_size)
    self.col_num = int(self.size.w / self.cell_size)
        
    # セルを初期化
    self.cells = self.init_cells()

init_cellsメソッドの中身は、以下のとおりです。

各セルの生死の状態は、二次元の配列で管理します。それぞれのセルは「生(0)」か「死(1)」のどちらかの状態を持ちます。

初期状態は、各セルごとに擬似乱数を使って決定します。ループの中で実行しているrandom.randint(0, 1)は、0か1かどちらかの数値をランダムに返します。0なら「死」で1なら「生」を表します。

def init_cells(self):
    """全てのセルを初期化します。

  各セルの状態(ON or OFF)は、ランダムに決定します。
  """
    rows = []
    for row in range(self.row_num):
        cols = []
        for col in range(self.col_num):
            cols.append(random.randint(0, 1))
        rows.append(cols)
    return rows

セルの描画処理を実装

次は描画状態の更新処理を書きます。

このupdateメソッドは、テンプレートに含まれているSceneクラスから継承しているメソッドです。

updateメソッドは、MySceneクラスの外側から定期的に自動で呼び出されるもので、呼び出しをこちらで意識する必要はありません。呼び出された時に何を描画するかだけを実装すれば良いのです。

まず、背景の描画を行います。ライフゲームは時間の経過と共に、セルの状態を遷移させていくものなので、古い世代のセルを新しい世代のセルで塗り替える必要があります。

古い状態を一度全て消すために、updateメソッドの先頭でbackgroundメソッドを呼び出して、全体を背景色で塗りつぶしています。

背景描画によるお掃除が終わったら、現在の状態のセルを描画します。fill(self.cell_color)でセルの色を指定したあと、行と列の数の分、ループ処理を行い、各セルの状態(0 or 1)に基づいて、セルを描画します。

セルが0の場合は「死」を表しており、そこにセルは存在しなくて良いため、セルの状態が「1」の場合だけ、セルの描画処理を行います。

def update(self):
        
    # 背景色を描画
    background(self.background_color)
        
    # セルの色せをセット
    fill(self.cell_color)
        
    # 行数分のループ
    for row in range(self.row_num):
            
        # 列数分のループ
        for col in range(self.col_num):

            # セルの状態をチェックして、状態がON(1)の場合はセルを描画する。
            # 状態がOFF(0)は、何もしないため、背景が表示されたままの状態となる。
            if self.cells[row][col]:

                # セル(四角系で表現する)を描画する。
                # 縦横の長さをcell_sizeにするとセル同士が繋がって見えてしまうので、-1して境界が見えるようにする。
                rect(col * self.cell_size, row * self.cell_size, self.cell_size - 1, self.cell_size - 1)

ここまで書けたら、一度プログラムを実行してみましょう。間違っていなければ、以下のようにセルが表示されるはずです。

f:id:mitsu3204:20181018013152j:plain

現在の状態では、セルの状態遷移を実装していないため、初期表示のみ行ってそこから何も変化は起きません。

次は、このセルの状態は一定時間ごとに変化するように実装を加えていきます。

セルの次の世代を計算する

各セルの生死は、現在の周りのセルの生死の状態によって決まります。周りのセルと言っているのは、自セルの上下左右斜めの合計8つのセルです。

この8つのセルの中に「生」の状態のセルがいくつ存在するかによって、自セルが次の世代で生きるか死ぬかが決まるというのがライフゲームのルールです。

ライフゲームのルール

  • 誕生:「死」の周辺セルの生存数が3の場合、次の世代が誕生する。(0 -> 1)
  • 生存:「生」の周辺セルの生存数が2または3の場合、次の世代でも生存する。(1 -> 1)
  • 過疎:「生」の周辺セルの生存数が1の場合、次の世代は死滅する。(1 -> 0)
  • 過密:「生」の周辺セルの生存数が4以上の場合、次の世代は死滅する。(1 -> 0)

指定したセルの周辺セルの生存セル数を数えるメソッドを実装します。

def count_alive_cells(self, r: int, c: int):
    """指定したセルの周辺の生存セルの数を返します。
  """
    alive = 0
    if r > 0:
        if c > 0:
            if self.cells[r - 1][c - 1]:
                alive += 1
        if self.cells[r - 1][c]:
            alive += 1
        if c < self.col_num - 1:
            if self.cells[r - 1][c + 1]:
                alive += 1

    if c > 0:
        if self.cells[r][c - 1]:
            alive += 1
    if self.cells[r][c]:
        alive += 1
    if c < self.col_num - 1:
        if self.cells[r][c + 1]:
            alive += 1

    if r < self.row_num - 1:
        if c > 0:
            if self.cells[r + 1][c - 1]:
                alive += 1
        if self.cells[r + 1][c]:
            alive += 1
        if c < self.col_num - 1:
            if self.cells[r + 1][c + 1]:
                alive += 1
    return alive

上記のcount_alive_cellsメソッドで、現在の生の状態のセル数を計算して、ルールに基づいて、次の世代のセルを作成するのが以下のメソッドです。

def gennext_cells(self):
    """現在のセルの状態に基づいて次の世代のセルを返します。
    """
    rows = []
    for r in range(self.row_num):
        cols = []
        for c in range(self.col_num):
            alive = self.count_alive_cells(r, c)
            if alive == 2:

                # 現在の状態を引き継ぐ
                cols.append(self.cells[r][c])

            elif alive == 3:

                # 誕生する
                cols.append(1)
            else:
                cols.append(0)
            rows.append(cols)
    return rows

updateメソッドの末尾に、世代交代のメソッドの呼び出しを追加します。

また、そのまま実行すると目まぐるしくセルの状態が変化して目が追いつかないため、最後にスリープ処理を追加して、少しだけ処理を止めるようにします。timeモジュールを使うので、ファイルの冒頭にimport timeの記述も追加しましょう。

def update(self):

    〜 省略 〜

    # 次の世代のセルを取得
    self.cells = self.gen_next_cells()

    # 世代交代の速度調整のため少しスリープさせる
    time.sleep(0.05)

これで時間とともに状態が変化するライフゲームの完成です。

実行ボタンをタップして起動してみましょう。

しばらく動かしていると、セルの形が固定パターンに落ち着くか、特定のパターンを延々と繰り返す状態になると思います。(これは正常な動きです)

タップした箇所にセルを配置するように機能を追加する

せっかくなので、タップ操作に反応する機能も実装してみます。

画面をタップしたら、タップした箇所に新しいセルを追加するよう機能を追加みます。

まずは、指定した座標(x, y)に該当するセルの状態をONにするメソッドをMySceneクラスの中に作ります。

xとyは、タップした位置の座標の情報を受け取る想定です。

座標を一つのセルのサイズで割って、該当のセルの行と列のインデックスを計算しています。

位置を取得したら、その部分のセルの状態をON(生存状態)にして、描画処理を行います。

def add_cell(self, x, y):
    """指定座標にセルを追加します。
    """

    # 位置の計算
    row = int(y / self.cell_size)
    col = int(x / self.cell_size)

    # 該当のセルの状態をONに変更
    self.cells[row][col] = 1

    # セルを描画
    rect(col * self.cell_size, row * self.cell_size, self.cell_size - 1, self.cell_size - 1)

上記のメソッドは、画面をタップした時に呼び出します。画面をタップすると、touch_beganメソッドが呼び出されます。

元々このメソッドの中身の実装は空っぽでしたが、座標情報を指定してadd_cellを呼び出すように変更します。タップした箇所の座標情報は、touch_beganメソッドに渡されるtouchlocationプロパティから取得することができます。

def touch_began(self, touch):
    x, y = touch.location
    self.add_cell(x, y)

また、タップしたあとに、指をそのまま画面上で動かした際にもセルを配置するように、タップと同等の処理を行うようにします。今度はtouch_movedメソッドにtouch_beganメソッドと全く同じ内容を実装します。

def touch_moved(self, touch):
    x, y = touch.location
    self.add_cell(x, y)

これで完了です。

プログラムを実行して画面をタップすると、タップした位置に新しいセルが配置されるようになっています。

まとめ

簡単な実装のみ行いましたが、もっと凝った作りにしてみたい人や、UIコンポーネントの使い方も練習したい人は、世代交代の速度を調整する機能や、ボタンによる一時停止、再開機能、また、セルの状態のリフレッシュ機能なども実装してみると良いかもしれません。

以上です。

Pythonista 3は、App Storeからダウンロードできます。

Pythonista 3 / omz:software

f:id:mitsu3204:20181016133630j:plain