Daydreaming in Brookline, MA

DL-4; メッセージ表示ウインドウ

1 はじめに

Rogue+Wizライクなゲーム(仮称Daemon Lord)を作るプロジェクトの4回目です。今回はメッセージウインドウとその上での文字列入力を実装します。

コードのリファクタリングが進んで状況がわからなくなっていると思うので、githubでコードを 公開 しています。MacとLinuxで動作確認していますが、Windowsは未対応です。

メッセージウインドウはスペックがなかなか決まらず、どういったものを作るかを決めるのにだいぶ時間がかかりました。スクロール画面の一部に常時表示する形態としました。

2 メッセージウインドウクラス

仮想スクロール画面クラス(Vscr)の一部にするか悩んだのですが、肥大化するのも嫌なので、メッセージウインドウクラス(Meswin)として独立させました。

ウインドウ自体のサイズ(width, height)とその中のメッセージ表示エリアのサイズ(mes_width, mes_height)とを分けて管理しています。(x, y)はスクロールウインドウ内のメッセージウインドウの表示位置(左上)です。

mes_linesに表示するメッセージ(文字列)のリストを入れます。これを空にするとメッセージウインドウをクリアすることができます(cls())。

class Meswin:
    """
    Message window.  Max 40x8 at the upper center of the scroll window.
    The message area is max 36x6 and a message starts with " * ".
    """

    def __init__(self, vscr):
        self.vscr = vscr
        self.width = min(40, vscr.width)
        self.height = min(8, vscr.height)
        self.x = max(0, (vscr.width-self.width)//2)  # center
        self.y = max(0, (vscr.height-self.height)//10)  # uppper
        self.mes_width = self.width - 4  # Message area width
        self.mes_height = self.height - 2  # Message area height
        self.cur_x = 0  # cursor position in message area
        self.cur_y = 0
        self.show = False
        self.mes_lines = []
        self.cls()

    def cls(self):
        # clear message area
        self.mes_lines = []

3 メッセージ表示メソッド

print()メソッドでメッセージを表示します。昔のファミコンRPGのように、メッセージの先頭には" * "を付けます。この記号はstart引数で変更できます。

引数msgはメッセージで、この中に含まれる'\n'を改行として処理します。メソッドの最初に'\n'でsublinesリストにsplitしていますが、'\n'の情報が失われないように正規表現のre.split()を使っています。

sublineは更にウインドウの右端&単語の境界で折り返すようにtextwrap.wrap()を使います。ここで折り返した行の先頭には' * 'が付かないようにしました。

改行オンリーの行を正しく処理するように、len(ssls)がゼロの時にもmes_linesに登録する処理を入れています。

表示行数がメッセージエリアの行数よりも多い文はmes_linesから抜いています。こうすることで、メッセージウインドウがスクロールします。

def print(self, msg, start='*'):
    """
    Print a message in the message window.  Long text wraps
    to the next line.  Process '\n' in texts.
    """
    sublines = re.split('\n', msg)
    for idx, sl in enumerate(sublines):  # subline
        header = '  '
        if idx == 0:
            header = start + ' '
        ssls = textwrap.wrap(sl, width=self.mes_width)
        if len(ssls) == 0:
            self.mes_lines.append(header)
        else:
            for ssl in ssls:
                self.mes_lines.append(''.join([header, ssl]))
                header = '  '
    if len(self.mes_lines) > self.mes_height:
        self.mes_lines = self.mes_lines[len(
            self.mes_lines)-self.mes_height:]
    self.cur_y = len(self.mes_lines)-1
    self.show = True

スクリーンショット(笑)です。1歩歩くたびにメッセージ出力するようにしてみました。

^^^^^^^^^^^^^^^^^^^^----------------------------------------^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * north                               |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * north                               |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * south                               |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * west                                |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * south                               |###...#############
^^^^^^^^^^^^^^^^^^^^| * east                                |###...##...........
^^^^^^^^^^^^^^^^^^^^----------------------------------------+..+...##...........
^^^^^^^^^^^^^^^^^^^^^^^^^^^^##########..........#####.......####...##...........
^^^^^^^^^^^^^^^^^^^^^^^^^^^^##########..........########+#######...########+####
^^^^^^^^^^^^^^^^^^^^^^^^^^^^##########..........########+#######...########.####
^^^^^^^^^^^^^^^^^^^^^^^^^^^^##########..@.......*.....+.....+...+++++.......####
^^^^^^^^^^^^^^^^^^^^^^^^^^^^##########..........#######.....####.....###########
^^^^^^^^^^^^^^^^^^^^^^^^^^^^##########..........#######.....####.....###########

4 ユーザーインプットメソッド

メッセージウインドウ内でユーザーインプットできるようにしました。"* "の代わりに"> "で入力を促します。

^^^^^^^^^^^^^^^^^^^^----------------------------------------####......##########
<snip>
^^^^^^^^^^^^^^^^^^^^| * How?                                |*......#####.......
^^^^^^^^^^^^^^^^^^^^| >                                     |#####..#####.......
^^^^^^^^^^^^^^^^^^^^----------------------------------------.#####++#####.......

宝箱の罠やスペル(魔法)の名前の入力でこのメソッドを使います。

^^^^^^^^^^^^^^^^^^^^| * How?                                |.######*#####......
^^^^^^^^^^^^^^^^^^^^| > poison needle                       |.*......#####......
^^^^^^^^^^^^^^^^^^^^| * Input: poison needle                |.#####..#####......
^^^^^^^^^^^^^^^^^^^^----------------------------------------.#####++#####.......

inputメソッドです。メッセージとプロンプトを表示して、その右にカーソルを持って行き、後は普通に標準ライブラリのinput()関数を呼んでいます。こちらの関数で画面表示された文字列はメッセージウインドウに登録されておらず、次回の表示で消えてしまうために、メッセージウインドウに登録しなおしています。

def input(self, msg):
    """
    Input a string in the message window.
    """
    self.print(msg)
    self.print('', start='>')
    self.vscr.show_meswin()
    self.vscr.display()
    print(f"\033[{self.y+self.cur_y+1};{self.x+5}H", end='', flush=True)
    try:
        value = input()
        self.mes_lines[self.cur_y] = "> " + value
    except:
        pass
    return value

5 1文字入力メソッド

次は1文字入力メソッドです。Yes/Noに答える際に'y'または'n'の入力待ちをするようなケースで使用します。

^^^^^^^^^^^^^^^^^^^^----------------------------------------^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * east                                |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * east                                |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * east                                |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * Do you? (y/n) > f                   |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * Do you? (y/n) > y                   |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^| * Input char: y                       |^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^----------------------------------------####################

期待する文字をリストで渡して、それが出るまでループさせることもできます。このメソッドはnon-blocking 1文字入力のgetch()関数を使っていて、リターンキーの入力が不要です。

def input_char(self, msg, values=[]):
    """
    Input a character in the message window.
    """
    ch = ''
    while ch not in values:
        self.print(msg+' >')
        self.vscr.show_meswin()
        self.vscr.display()
        print(f"\033[{self.y+self.cur_y+1};{self.x+len(msg)+8}H",
              end='', flush=True)
        ch = getch()
        l = self.mes_lines.pop()
        self.print(''.join([l, ' ', ch])[2:])
        self.vscr.show_meswin()
        self.vscr.display()
        if not values:
            break
    return ch

6 次は、、

メッセージも出力できるようになったので、次はいよいよWizardry画面らしい、パーティーリストの画面表示にチャレンジします。