Table of Contents
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画面らしい、パーティーリストの画面表示にチャレンジします。