静かなる名辞

pythonとプログラミングのこと

【python】ターミナル上でCUIでライフゲーム

概要

 ANSIエスケープシーケンスを使って複数行を書き換えるテストとして書きました。洗練度は低いですがライフゲームが端末上で動きます。

www.mm2d.net

 これを動かしておくことで、なんとなくかっこいい感じがします。

実装

 基本的には以前作ったときと同じ実装です。

www.haya-programming.com

 ただし、使いやすいようにクラスとモジュールに押し込みました。iterableとして実装しています。

main.py

import sys
import time

from lifegame import LifeGame

def field_to_char(A):
    return "\n".join("".join("#" if char else " " for char in line) 
                     for line in A)

def main():
    seed = int(sys.argv[1])
    lifegame = LifeGame(60, 30, seed)
    for i, x in enumerate(lifegame):
        print(field_to_char(x))
        print("\x1b[30A", end="")
        time.sleep(0.1)

        if i > 1000:
            break

    print("\x1b[30B", end="")
    
if __name__ == "__main__":
    main()

lifegame.py

import numpy as np
import numba as nb
import matplotlib.pyplot as plt

@nb.jit(nopython=True)
def get_ijlst(x, limit):
    ret = []
    if 0 < x:
        ret.append(x-1)
    if x < limit-1:
        ret.append(x+1)
    ret.append(x)
    return ret

@nb.jit(nopython=True)
def update_cell(i, j, field, out, field_w, field_h):
    i_lst = get_ijlst(i, field_h)
    j_lst = get_ijlst(j, field_w)

    s = 0
    for ni in i_lst:
        for nj in j_lst:
            s += field[ni, nj]
    s -= field[i,j]

    if s < 2:
        out[i,j] = 0
    elif s == 2:
        out[i,j] = field[i,j]
    elif s == 3:
        out[i,j] = 1
    elif s >= 4:
        out[i,j] = 0
    else:
        raise Exception

def update_field(pair_lst, field_w, field_h):
    for i in range(field_h):
        for j in range(field_w):
            update_cell(i, j, pair_lst[0], pair_lst[1], field_w, field_h)
    pair_lst.append(pair_lst.pop(0))

class LifeGame:
    def __init__(self, field_w, field_h, seed=0):
        self.field_w = field_w
        self.field_h = field_h
        self.random = np.random.RandomState(seed)
        self.initialize()

    def initialize(self):
        field = (self.random.rand(self.field_h, self.field_w) > 0.9
        ).astype(np.int16)
        out = np.zeros(shape=(self.field_h, self.field_w)).astype(np.int16)
        self.pair_lst = [field, out]
        
    def __iter__(self):
        self.initialize()
        return self
    
    def __next__(self):
        self.update_field()
        return self.pair_lst[0]
        
    def update_field(self):
        update_field(self.pair_lst, self.field_w, self.field_h)

def main():
    lifegame = LifeGame(60, 40)
    img = plt.imshow(lifegame.pair_lst[0])
    for x in lifegame:
        img.set_data(x)
        plt.pause(0.001)
        print(x)

if __name__ == "__main__":
    main()

使い方

 上の二つのファイルを同一ディレクトリに置き、

$ python main.py 127

 のように端末から実行します(コマンドライン引数は乱数seedで省略不可)。ターミナルエミュレータの場合、できるだけ大きい画面(全画面など)にしておいてください。また、フィールドの幅と高さはmain.pyを数箇所書き換えれば調整できます。

 なお、ANSIエスケープシーケンスによる制御を受け付けない端末も中にはあるはずなので、注意が必要です。コマンドプロンプトとかはできないはず。emacsのshellも無理でした。

 うまく実行されれば、同じ位置で画面が書き換わりながらライフゲームが描画されるのを楽しめます。

 実行画面の例を以下に貼ります。動画は編集が大変なので、静止画ですが・・・。

実行例
実行例

いけてないところ

  • いかんせん文字の縦横比が1:1ではないので、表示が縦長な感じになる
  • Ctrl-Cなどで途中で止めると画面にゴミが残ることがある(clearコマンドで消してください)

 縦長になるのはともかく、後者はもう少しなんとかしてあげたいところです。

まとめ

 CUIで動かすのは思いの外簡単にできました。その気になればけっこう遊べるものも作れると思います(簡単なゲームくらいはいける?)。