naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Pythonで、1文字ずつ入力を受け取る

プログラミング関連 Python 約13日前
2017年11月8日14:23
CUIでリアルタイムなやりとりをさせようとすると、1文字入力されたら、それをすぐにプログラム側に返したいって場合があります。つまり、エンターをいちいち押さずにキー押下ですぐに反応させたい、ということです。
組み込み関数のinputではできません。
そんな場合のTipsです。

CTRL_C = 3

try:
    from msvcrt import getch
except ImportError:
    def getch():
        import sys
        import tty
        import termios
        fd = sys.stdin.fileno()
        old = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            return sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old)

while True:
    key = ord(getch())
    if key == CTRL_C:
        break
    else:
        message = 'input, {0}'.format(chr(key))
        print(message)




使ってみると、ちゃんと動作しています。



msvcrtモジュールには文字を1文字ずつ受け取るgetchがあります。
が、このモジュールはWindowsプラットフォームのための機能です。
そこで、Windowsであればそれを使い、そうでなければgetchという関数を改めて作っています。
他人のコードをいくつか見ましたが、おおむね以下のようなコードを皆さん作っていました。
try:
    from msvcrt import getch
except ImportError:
    def getch():
        import sys
        import tty
        import termios
        fd = sys.stdin.fileno()
        old = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            return sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old)



getch()で返ってくる文字をordで変換しておきます。
CTRL_Cを押された際、これは3になります。CTRL_Cならば、処理終了ということでbreakします。
表示する際はchrで変換するようにしておきます。
CTRL_C = 3
...
...
while True:
    key = ord(getch())
    if key == CTRL_C:
        break
    else:
        message = 'input, {0}'.format(chr(key))
        print(message)




これを応用し、ちょっとしたハイ&ローめいたゲームを作ります。
エンターをいちいち入力しないので、リアルタイム感が得られるでしょう。


ソースコード
import random
import subprocess
import sys
import time

# コンソールをクリアするコマンドがプラットフォームによって違う
# windowsはcls、ほかは大体clear
# getch関数も、Windowsじゃなければ定義
if sys.platform == 'win32':
    clear_command = 'cls'
    from msvcrt import getch
else:
    clear_command = 'clear'
    def getch():
        import tty
        import termios
        fd = sys.stdin.fileno()
        old = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            return sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old)

CTRL_C = 3
HIGH = 49
LAW = 50

GAME_TEMPLATE = """\
勝ち{0[0]}:負け{0[1]}

相手:{1} あなた:{2}

相手より...{3}
1. 高い
2. 低い
"""

CARDS = [
    'A', '2', '3', '4', '5', '6', '7', '8', '9',
    '10', 'J', 'Q', 'K', 'ジョーカー'
]

# 勝ち, 負け
score = [0, 0]

# 相手の出したカードの履歴
history = []


def update(key):
    """勝敗の決定、結果の表示、次ゲームへの移行を行う"""
    # 出力をクリア
    subprocess.run([clear_command], shell=True)

    # 勝敗を決定する
    player = random.choice(CARDS)
    enemy = history[-1]
    result = ''
    your_select = ''
    if key == HIGH:
        your_select = 'High'
        if CARDS.index(player) > CARDS.index(enemy):
            score[0] += 1
            result = 'Win'
        else:
            score[1] += 1
            result = 'Lose'

    elif key == LAW:
        your_select = 'Law'
        if CARDS.index(player) < CARDS.index(enemy):
            score[0] += 1
            result = 'Win'
        else:
            score[1] += 1
            result = 'Lose'

    # 勝敗の表示
    message = GAME_TEMPLATE.format(score, enemy, player, your_select)
    print(message)
    print(result)
    time.sleep(3)

    # 次ゲームへ移行
    card_set()


def card_set():
    """敵がカードをセットし、その状態を表示する"""
    # 出力をクリア
    subprocess.run([clear_command], shell=True)

    # 敵カードを決め、履歴に追加し、そのカードを画面に表示
    enemy = random.choice(CARDS)
    history.append(enemy)
    message = GAME_TEMPLATE.format(score, enemy, '?', '')
    print(message)


def main():
    """ゲームの開始"""
    card_set()
    while True:
        key = ord(getch())
        if key == CTRL_C:
            break
        elif key == HIGH or key == LAW:
            update(key)


if __name__ == '__main__':
    main()



まるでゲーム画面の一部が更新されているように見えますが、実際はそんなことありません。
画面の内容を消去し、新たに表示し直すことで、そう見せているだけです。


Windowsのコマンドプロンプトならば「cls」で消去できますし、
それ以外ならば概ね「clear」でそれが可能です。
その部分だけをするならば、以下のようにできます。ソースコードでは、これに加えてmsvcrtやgetch関数の定義等もしていますね。
if sys.platform == 'win32':
    clear_command = 'cls'
else:
    clear_command = 'clear'



これだけで結構面白いことができるのですが、もっと面白いCUIアプリケーションも作れます。
興味があれば、cursesモジュールも調べてみましょう。