点字を使った CUI での２値セル・オートマトン描画

# きっかけ

[点字を使ってぐるぐる表示ができる](https://qiita.com/tadashi9e/items/784915cded84b9f7ce7c)、ということを調べた結果
「これは２値セル・オートマトンの描画に使えるのでは？」
と思ったので実装してみました。

# 実装

ただ描画するだけなら点字キャラクタを並べて表示すればいいだけなのですが、セル・オートマトンの各セルの時間的変化を見せたいので curses を使うことにしました。

ユニコードには点字として６点のものと、漢点字とも呼ばれる８点のものとがあるので、それぞれ実装して見た目を比べてみます。

まずは６点キャラクタを使ったビットマップ描画クラス。

```python
# -*- mode:python;coding:utf-8 -*-
# bitmap6.py

import curses

class BitMapDisplay(object):
    u'''点字（６点）キャラクタを使ったビットマップ描画
    '''
    P = (
        '⠀⠁⠈⠉⠂⠃⠊⠋' +
        '⠐⠑⠘⠙⠒⠓⠚⠛' +
        '⠄⠅⠌⠍⠆⠇⠎⠏' +
        '⠔⠕⠜⠝⠖⠗⠞⠟' +
        '⠠⠡⠨⠩⠢⠣⠪⠫' +
        '⠰⠱⠸⠹⠲⠳⠺⠻' +
        '⠤⠥⠬⠭⠦⠧⠮⠯' +
        '⠴⠵⠼⠽⠶⠷⠾⠿')
    def __init__(self, scr :'curses._CursesWindow') -> None:
        h, w = scr.getmaxyx()
        self.height, self.width = 3 * (h - 1), 2 * (w - 1)
        self._scr = scr
        self._bmap = [[False for x in range(self.width)]
                      for y in range(self.height)]
        self._lines = ['' for y in range(self.height)]
        scr.clear()
    def clear(self) -> None:
        u'''ビットマップをクリアする。
        '''
        self._bmap = [[False for x in range(self.width)]
                      for y in range(self.height)]
    def set(self, y :int, x :int, b :bool) -> None:
        u'''ビットマップ上に値を設定する。
        y, x -- 画面上の位置（ドット単位）
        '''
        self._bmap[y % self.height][x % self.width] = b
    def get(self, y :int, x :int) -> bool:
        u'''ビットマップ上から値を取得する。
        y, x -- 画面上の位置（ドット単位）
        '''
        return self._bmap[y % self.height][x % self.width]
    def get_character(self, cy :int, cx :int) -> str:
        u'''画面上に表示するキャラクタを取得する。
        cy, cx -- 画面上の位置（キャラクタ単位）。
        '''
        i = sum([(1 << (2 * dy + dx) if self._bmap[3 * cy + dy][2 * cx + dx] else 0)
                 for dy in range(3) for dx in range(2)])
        return BitMapDisplay.P[i]
    def draw(self) -> None:
        u'''画面に描画する。
        '''
        for cy in range(0, self.height // 3):
            # 行の表示内容を取得（末尾の空白部分は削除）
            line = ''.join([self.get_character(cy, cx)
                            for cx in range(self.width // 2)]).rstrip('⠀')
            if self._lines[cy] == line:
                continue  # 書き換え不要
            self._scr.addstr(cy, 0, line)
            self._scr.clrtoeol()  # 行末まで削除
            self._lines[cy] = line
```

一文字ずつ表示するのは遅そうな気がしたので、行単位で前回と比較して変化があったら描画、ということをしています（実際に測定してみると、文字単位で差分チェックして文字単位で addstr するより行単位で差分チェックして一行まとめて addstr する方が微妙に速い）。

あと、通常の点字の点の並びの概念に合わせると `3 * x + y` で順序付けた方がよかったかもしれません。今回は文字コードを使ってキャラクタを生成することはせず、予め文字を並べたテーブル `BitMapDisplay.P` を準備したので `2 * y + x` にしてしまいました。

８点の場合もほとんど同じです。

```python
# -*- mode:python;coding:utf-8 -*-
# bitmap8.py

import curses

class BitMapDisplay(object):
    u'''漢点字フォント（8ドット）を使ったビットマップ表示。
    '''
    P = ('⠀⠁⠈⠉⠂⠃⠊⠋⠐⠑⠘⠙⠒⠓⠚⠛' +
         '⠄⠅⠌⠍⠆⠇⠎⠏⠔⠕⠜⠝⠖⠗⠞⠟' +
         '⠠⠡⠨⠩⠢⠣⠪⠫⠰⠱⠸⠹⠲⠳⠺⠻' +
         '⠤⠥⠬⠭⠦⠧⠮⠯⠴⠵⠼⠽⠶⠷⠾⠿' +
         '⡀⡁⡈⡉⡂⡃⡊⡋⡐⡑⡘⡙⡒⡓⡚⡛' +
         '⡄⡅⡌⡍⡆⡇⡎⡏⡔⡕⡜⡝⡖⡗⡞⡟' +
         '⡠⡡⡨⡩⡢⡣⡪⡫⡰⡱⡸⡹⡲⡳⡺⡻' +
         '⡤⡥⡬⡭⡦⡧⡮⡯⡴⡵⡼⡽⡶⡷⡾⡿' +
         '⢀⢁⢈⢉⢂⢃⢊⢋⢐⢑⢘⢙⢒⢓⢚⢛' +
         '⢄⢅⢌⢍⢆⢇⢎⢏⢔⢕⢜⢝⢖⢗⢞⢟' +
         '⢠⢡⢨⢩⢢⢣⢪⢫⢰⢱⢸⢹⢲⢳⢺⢻' +
         '⢤⢥⢬⢭⢦⢧⢮⢯⢴⢵⢼⢽⢶⢷⢾⢿' +
         '⣀⣁⣈⣉⣂⣃⣊⣋⣐⣑⣘⣙⣒⣓⣚⣛' +
         '⣄⣅⣌⣍⣆⣇⣎⣏⣔⣕⣜⣝⣖⣗⣞⣟' +
         '⣠⣡⣨⣩⣢⣣⣪⣫⣰⣱⣸⣹⣲⣳⣺⣻' +
         '⣤⣥⣬⣭⣦⣧⣮⣯⣴⣵⣼⣽⣶⣷⣾⣿')
    def __init__(self, scr :'curses._CursesWindow') -> None:
        h, w = scr.getmaxyx()
        self.height, self.width = 4 * (h - 1), 2 * (w - 1)
        self._scr = scr
        self._bmap = [[False for x in range(self.width)]
                      for y in range(self.height)]
        self._lines = ['' for y in range(self.height)]
        scr.clear()
    def clear(self) -> None:
        u'''ビットマップをクリアする。
        '''
        self._bmap = [[False for x in range(self.width)]
                      for y in range(self.height)]
    def set(self, y :int, x :int, b :bool) -> None:
        u'''ビットマップ上に値を設定する。
        y, x -- 画面上の位置（ドット単位）
        '''
        self._bmap[y % self.height][x % self.width] = b
    def get(self, y :int, x :int) -> bool:
        u'''ビットマップ上から値を取得する。
        y, x -- 画面上の位置（ドット単位）
        '''
        return self._bmap[y % self.height][x % self.width]
    def get_character(self, cy :int, cx :int) -> str:
        u'''画面上に表示するキャラクタを取得する。
        cy, cx -- 画面上の位置（キャラクタ単位）。
        '''
        i = sum([(1 << (2 * dy + dx) if self._bmap[4 * cy + dy][2 * cx + dx] else 0)
                 for dy in range(4) for dx in range(2)])
        return BitMapDisplay.P[i]
    def draw(self) -> None:
        u'''画面に描画する。
        '''
        for cy in range(0, self.height // 4):
            # 行の表示内容を取得（末尾の空白部分は削除）
            line = ''.join([self.get_character(cy, cx)
                            for cx in range(self.width // 2)]).rstrip('⠀')
            if self._lines[cy] == line:
                continue  # 書き換え不要
            self._scr.addstr(cy, 0, line)
            self._scr.clrtoeol()  # 行末まで削除
            self._lines[cy] = line
```

# まずはテストプログラム

ただ直線を引くだけだと面白くないので、螺旋を描いてみました。
螺旋だけだと面白くないので、螺旋がぐるぐる回って吸い込まれる感じを出してみました。

```python
import curses
import math
import time
from bitmap8 import BitMapDisplay # ８点で描画する場合
# from bitmap6 import BitMapDisplay # ６点で描画する場合

def main() -> None:
    stdscr = curses.initscr()
    try:
        curses.curs_set(0)
        curses.noecho()
        curses.cbreak()
        stdscr.nodelay(True)
        bmd = BitMapDisplay(stdscr)
        scale = min([bmd.height // 2, bmd.width // 2])
        step = 10
        offset = 0.0
        running = True
        start_time = datetime.datetime.now()
        count = 1
        while True:
            if running:
                offset += 0.1
                bmd.clear()
                for t in range(scale * step):
                    t1 = t / step
                    r = t1
                    a = 2 * math.pi * (t1 / scale) * 3
                    (y, x) = (r * math.sin(a + offset),
                              r * math.cos(a + offset))
                    bmd.set(round(bmd.height / 2 + y),
                            round(bmd.width / 2 + x),
                            True)
                bmd.draw()
                current_time = datetime.datetime.now()
                draw_per_sec = count / (current_time - start_time).total_seconds()
                count += 1
                stdscr.addstr(
                    stdscr.getmaxyx()[0] - 1, 1,
                    'width = {}, height = {}, draw/sec = {}'.format(
                        stdscr.getmaxyx()[1],
                        stdscr.getmaxyx()[0],
                        draw_per_sec))
                stdscr.refresh()
                #time.sleep(0.1)  # 回転速度
            else:
                time.sleep(1)
                # reset
                start_time = datetime.datetime.now()
                count = 1
            if stdscr.getch() == ord(' '):
                running = not running
    finally:
        curses.endwin()
if __name__ == '__main__':
    main()
```

実行すると、こんな感じの表示がぐるぐる回ります。

６点での表示例：

```

⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠠⠠⠄⠄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠒⠈⠁⠁⠀⠀⠀⠀⠉⠈⠂⠢
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠒⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠂⠂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠂⠄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠎⠀⠀⠀⠀⠀⠀⠀⠠⠤⠤⠒⠤⠤⠄⠀⠀⠀⠀⠀⠀⠀⠈⠂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠌⠀⠀⠀⠀⠀⠀⠔⠊⠉⠀⠀⠀⠀⠀⠈⠑⠢⠄⠀⠀⠀⠀⠀⠈⠂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠀⠀⠀⠀⠀⠠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⠀⠀⠀⠀⠀⠈⠂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠃⠀⠀⠀⠀⠠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠣⠀⠀⠀⠀⠀⠘
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠀⠀⠀⠀⠀⠜⠀⠀⠀⠀⠀⠤⠒⠒⠒⠦⠄⠀⠀⠀⠀⠀⠣⠀⠀⠀⠀⠀⠅
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠀⠀⠀⠀⠀⠇⠀⠀⠀⠀⠰⠁⠀⠀⠀⠀⠱⠄⠀⠀⠀⠀⠸⠀⠀⠀⠀⠀⠨
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠱⠀⠀⠀⠀⠀⠇⠀⠀⠀⠀⠸⠄⠰⠀⠀⠀⠀⠧⠀⠀⠀⠀⠀⠃⠀⠀⠀⠀⠐
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠀⠀⠀⠀⠀⠣⠀⠀⠀⠀⠀⠉⠉⠀⠀⠀⠀⠜⠀⠀⠀⠀⠀⠇⠀⠀⠀⠀⠘
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠄⠀⠀⠀⠀⠈⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠇⠀⠀⠀⠀⠰⠁⠀⠀⠀⠀⠨
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠢⠀⠀⠀⠀⠀⠈⠢⠀⠀⠀⠀⠀⠀⠀⠠⠊⠀⠀⠀⠀⠀⠰⠀⠀⠀⠀⠀⠐
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠃⠀⠀⠀⠀⠀⠀⠉⠒⠤⠤⠤⠔⠚⠁⠀⠀⠀⠀⠀⠠⠃⠀⠀⠀⠀⠀⠃
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠃⠀⠀⠀⠀⠀⠌
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠔⠁⠀⠀⠀⠀⠀⠨
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠄⠄⠀⠀⠀⠀⠀⠀⠀⠀⠠⠔⠁⠀⠀⠀⠀⠀⠀⠠⠂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠒⠒⠐⠒⠐⠊⠈⠁⠀⠀⠀⠀⠀⠀⠀⠔
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠈
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁

 width = 56, height = 24, draw/sec = 650.9813575689599
```


８点での表示：

```

⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⢀⡀⡀⣀⢀⢀⡀⡀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠠⠄⠒⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠈⠐⠂⢄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠐⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠠⡀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠂⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⡀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠤⠤⠤⠤⠤⢄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠀⠀⠀⠀⠀⠀⠀⠀⢠⠔⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠑⡀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡔⠀⠀⠀⠀⠀⠀⠀⢠⠜⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⣄⠀⠀⠀⠀⠀⠀⠀⠈⢄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢣⠀⠀⠀⠀⠀⠀⠀⠐⡀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡃⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⡠⠔⠒⠒⠢⢄⡀⠀⠀⠀⠀⠀⠀⠀⢣⠀⠀⠀⠀⠀⠀⠀⠡
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠁⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⡞⠀⠀⠀⠀⠀⠀⠙⣆⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠈⡂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⡄⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⢇⠀⢠⠀⠀⠀⠀⠀⠸⡀⠀⠀⠀⠀⠀⠀⢨⠀⠀⠀⠀⠀⠀⠀⡂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠆⠀⠀⠀⠀⠀⠀⠘⡄⠀⠀⠀⠀⠀⠀⠀⠉⠉⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠅
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠱⠀⠀⠀⠀⠀⠀⠀⠘⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡼⠀⠀⠀⠀⠀⠀⠀⡔⠀⠀⠀⠀⠀⠀⠀⡅
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠣⠀⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠜⠁⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀⠀⠀⢐
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠢⠤⠤⣄⠤⠤⠖⠊⠁⠀⠀⠀⠀⠀⠀⠀⢀⠅⠀⠀⠀⠀⠀⠀⠀⠆
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⡨
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠁⠀⠀⠀⠀⠀⠀⠀⡐
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⠠⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⢀⠌
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠒⠐⠢⠠⠤⠠⠔⠂⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠠⠂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠠⠂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠠⠂⠁

 width = 56, height = 24, draw/sec = 501.90098717464485
```

スペースキーを押すと描画を停止するように作ったので、適当なタイミングで止めて描画内容をテキストとしてコピーできるのが地味に便利です。

当方の環境だと、コンソールを画面いっぱいに広げても秒間 70 フレームを越えるので、描画速度が問題になることはまずないと思います。

# ライフゲーム

ビットマップの描画に問題はなさそうなので２値セル・オートマトンに使ってみることにします。
というわけでライフゲームです。

```python
# -*- mode:python;coding:utf-8 -*-
u'''
点字フォントを使って表示するライフゲーム。
'''
import collections
import curses
import time
from bitmap8 import BitMapDisplay

class Life(object):
    u'''ライフゲーム
    '''
    def __init__(self, bmd :BitMapDisplay) -> None:
        u'''初期状態では「生」状態セルはなし。
        '''
        self._bmd = bmd
        self._cells :set[tuple[int, int]] = set()
    def set(self, y :int, x :int) -> None:
        u'''「生」状態セルを置く。
        '''
        self._cells.add((y, x))
    def next_step(self) -> None:
        u'''次ステップでのセル状態を決定する。
        '''
        self._cells = {
            point
            for point, n in collections.Counter(
                    [self._clipping((y + dy, x + dx))
                     for (y, x) in self._cells
                     for dy in range(-1, 2)
                     for dx in range(-1, 2)
                     if dy != 0 or dx != 0]).items()
            if n == 3 or (n == 2 and point in self._cells)}
        self._bmd.clear()
        for (y, x) in self._cells:
            self._bmd.set(y, x, True)
    def _clipping(self, point : tuple[int, int]) -> tuple[int, int]:
        u'''境界条件処理（周期的境界条件を用いる）。'''
        (y, x) = point
        return (y % self._bmd.height, x % self._bmd.width)
def main() -> None:
    stdscr = curses.initscr()
    try:
        curses.curs_set(0)
        curses.noecho()
        curses.cbreak()
        stdscr.nodelay(True)
        bmd = BitMapDisplay(stdscr)
        life = Life(bmd)
        center_y, center_x = bmd.height // 2, bmd.width // 2
        life.set(center_y, center_x)
        life.set(center_y, center_x + 1)
        life.set(center_y - 2, center_x + 1)
        life.set(center_y - 1, center_x + 3)
        life.set(center_y, center_x + 4)
        life.set(center_y, center_x + 5)
        life.set(center_y, center_x + 6)
        running = True
        while True:
            bmd.draw()
            stdscr.refresh()
            if running:
                life.next_step()
                #time.sleep(0.01)
            else:
                time.sleep(1)
            if stdscr.getch() == ord(' '):
                running = not running
    finally:
        curses.endwin()
if __name__ == '__main__':
    main()
```


実行すると、こんな感じの表示が動きます。

６点での表示：

```
⠀⠀⠀⠀⠀⠀⠀⠠⠳
⠀⠀⠀⠀⠀⠀⠤
⠀⠤⠚⠦⠀⠘⠌⠆
⠆⠏⠀⠨⠀⠀⠁⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠴⠑⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠
⠑⠶⠋⠩⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠐⠀⠱
⠀⠀⠀⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠀⠀⠀⠜⠀⠀⠀⠀⠀⠤
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⠂⠈⠀⠀⠀⠀⠀⠀⠉

⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠄⠀⠀⠀⠀⠀⠠⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠠
⠀⠀⠀⠰⠆⠀⠀⠀⠀⠀⠀⠈⠁⠀⠀⠪⠕⠀⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠩⠜
⠀⠀⠀⠀⠀⠀⠏⠂⠀⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠨⠕
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠢⠀⠀⠀⠒⠂⠀⠀⠀⠀⠀⠀⠀⠤
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠬⠠⠑⠒⠄⠀⠀⠠⠒⠄⠀⠈⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉
⠀⠀⠀⠀⠀⠀⠀⠠⠄⠉⠊⠀⠒⠁⠀⠀⠈⠒⠁
⠀⠀⠀⠀⠀⠀⠀⠈⠁

⠀⠀⠀⠀⠀⠀⠀⠘⠃
⠀⠀⠀⠀⠀⠔⠢⠀⠀⠠⠀⠄
⠀⠀⠀⠀⠀⠈⠁⠀⠀⠼⠭⠸
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠚⠀⠀⠀⠀⠀⠀⠀⠬⠆
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠰
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠧
 generation: 333
```
８点での表示：

```
⠀⠀⠀⠀⠀⠀⠀⠀⡀
⠀⠀⠀⠀⠀⠀⠀⠐⠙
⠀⣀⠴⣄⠀⠰⡙⡄
⡆⠏⣀⣨⠀⠀⠁⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠴⠑⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠
⠈⠛⠁⡰⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠈⠈⠀⢸
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⠄⠐⠁⠀⠀⠀⠀⠀⠛

⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠰⠆⠀⠀⡠⢄⠀⠰⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠴⢰
⠀⠀⠀⠈⠁⠀⡖⠄⠀⠶⠀⠀⠀⠀⠀⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢐⡢⠀⠈⠁
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠠⡀⠀⠀⠀⠀⠀⠀⠀⠀⢠⢢⠀⠀⠀⠒⠂⠀⠀⠀⠀⠀⠀⠀⣤
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠶⡰⠈⣉⠆⠀⠀⠰⣉⠆⠀⠀⠁
⠀⠀⠀⠀⠀⠀⠀⠘⠃
⠀⠀⠀⠀⠀⠀⠀⢀⡀
⠀⠀⠀⠀⠀⢀⡀⠈⠁
⠀⠀⠀⠀⠀⠑⠊⠀⠀⣸⣒⢱
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠚⠀⠀⠀⠀⠀⠀⠀⠬⠆
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡜
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣄⡭⠂


 generation: 333
```

# まとめ

- curses と点字を組み合わせて、時間的に変化するビットマップの描画を行いました。
- GUI を持ち出すほどでもないけれどもキャラクタでの表示だとやや解像度が不足している、というときには便利です。例えば、２値セル・オートマトンの描画に使えます。
- ６点の点字と、８点の点字がありますが、自分の環境では８点の方が６点に比べてドット間隔の均一さの面で勝っていると感じました。
- 文字間隔が均一なコンソールターミナル上での表示を前提にしているので、描画内容をテキストとして他の環境に持っていくと崩れて見える可能性があります。