【Python】Watchdogを使ってテキストゲームを作ってみる

突然ですが、皆さんは普段の作業で何かにリアクションしてもらえることはありますか？

技術者として日々成長するにつれ、何か小さな作業をする度、
リアクションしてもらえることが減ってきたのではないでしょうか。

**リアクション、ほしいですよね？** 
新人の頃はファイルを作るだけで褒められていたのに（そんなこともない）

ということで、今回はファイルの監視をしてくれる **「Watchdog」** を用いて、
簡単なテキストゲームみたいなモノを作っていきましょう。


## 準備
まずはテキストを表示する媒体を準備しましょう。
単純にコマンドプロンプトのログでもいいのですが、
ちょっと味気ないので、前回使用した **GoogleChat(Webhook)** を活用します。
（リクエスト回数が多くなる場合は、コマンドプロンプトを用いたほうが良いかもしれません。）

https://qiita.com/X23/items/58a6f77e2d93104933ad

## watchdogをインストール
例のごとくpipでインストールします。

```
pip install watchdog
```

## ファイルを監視する基盤処理を作る

watchdogには様々なイベントハンドラー（監視する方法）が用意されています。
（特定の拡張子やパターンを監視するなど…）
今回はスタンダードな **「FileSystemEventHandler」** を使いたいと思います。
他のハンドラーを確認する際は、以下のリファレンスがおすすめです。

https://pythonhosted.org/watchdog/

***

上記のリファレンスに記載されている
サンプルコードを活用して、以下のような基盤を作ってみました↓

```observe.py
# encoding: utf-8
import os
import requests
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

def SendMessage(send_text):

    # webhookのurl
    webhook_url = 'https://chat.googleapis.com/---以下省略---'

    # 送信
    response = requests.post(
        webhook_url,
        json={"text": send_text}
    )


class Handler(FileSystemEventHandler):

    # あらゆるイベント（変更・削除・作成・移動）で発火
    def on_any_event(self, event):
        SendMessage("----Any : " + event.src_path.split(os.sep)[-1])

    # 移動イベントで発火
    def on_moved(self, event):
        SendMessage("----Moved : " + event.src_path.split(os.sep)[-1])

    # 作成イベントで発火
    def on_created(self, event):
        SendMessage("----Create : " + event.src_path.split(os.sep)[-1])

    # 削除イベントで発火
    def on_deleted(self, event):
        SendMessage("----Delete : " + event.src_path.split(os.sep)[-1])

    # 変更イベントで発火
    def on_modified(self, event):
        SendMessage("----Modified : " + event.src_path.split(os.sep)[-1])


# メイン関数
def main():

    event_handler = Handler()
    observer = Observer()

    # 監視対象となるフォルダ
    path = u'\\Desktop\\syachi_kun'

    # 監視設定
    observer.schedule(event_handler, path, recursive=True)

    # 監視を開始
    observer.start()

    # 監視を続けたいのでCtrl+Cが押されるまでループ
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    
    # スレッドが終わるまで待つ
    observer.join()


if __name__ == "__main__":
    main()
```

こんな感じになりました。
ハンドラーの継承クラスを作成し、各メソッドの実装を行うような感じです。
上記以外にもdispatchというメソッドもありますが、
基本的に変更・削除・作成・移動の実装をしておけば大丈夫そうです。

早速コマンドプロンプトで実行し、
監視フォルダ内でテキストファイルを作成してみましょう。

![step_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1780415/5503f8db-4e00-961c-b70c-ddedb6c9bfe1.png)

しっかり発火しています。優秀。
一度の作成でログが2種類流れているのは、on_any_event と on_created が発火しているためです。

ゲームにするにあたり、イベントが何度も発生するのは少し都合が悪そうなので、
on_any_eventの出番はここまでになります。
（「色々なイベントを取得できるものがありますよ！」といった内容をご紹介したかっただけ）

## ゲーム部分を作る

見ていて楽しいものになるように、ゲーム部分を簡単に作っていきます。
今回はお金を貯めるゲームを作ってみました。

```obsesrve.py
# イベントデッキ
game_event = ['dungeon', 'damage', 'negirai']

DAMAGE_VALUE = 6
SANZAI_VALUE = 6
KYUYO_VALUE = 10
STEP_VALUE = 10
NEGIRAI_VALUE = 10
MAX_HP = 10

current_event = 'none'      # 現在のイベント
player_hp = MAX_HP          # HP
player_gold = 0             # 所持金
dungeon_step = STEP_VALUE   # ダンジョンの調査にかかる工数
enemy_hp = MAX_HP           # 敵HP
is_battle = False           # 交戦中か


def GameEvent():

    global current_event, player_hp, player_gold, dungeon_step, enemy_hp, is_battle

    # 前回のイベントが終わっていたら抽選
    if current_event == "none":
        current_event = game_event[random.randrange(len(game_event))]

    # 各イベント
    if current_event == "dungeon":

        if is_battle == True:
            player_atk = 1 + random.randrange(DAMAGE_VALUE)
            enemy_atk = 1 + random.randrange(DAMAGE_VALUE)

            player_hp -= enemy_atk
            enemy_hp -= player_atk

            if player_hp <= 0:
                add_gold = 1 + random.randrange(SANZAI_VALUE)
                player_gold -= add_gold
                player_hp = MAX_HP
                SendMessage("--- たたかう ---\nあなたは負けました。\n{}円の損失".format(add_gold))
                is_battle = False

            else:
                if enemy_hp <= 0:
                    add_gold = 1 + random.randrange(KYUYO_VALUE)
                    player_gold += add_gold
                    SendMessage("--- たたかう ---\nあなたは勝ちました\n{}円獲得".format(add_gold))
                    enemy_hp = MAX_HP
                    is_battle = False
                else:
                    SendMessage("--- たたかう ---\n交戦中…\nあなた：HP{}\n敵　　：HP{}".format(player_hp, enemy_hp))

        else:
            add_step = 1 + random.randrange(STEP_VALUE)
            dungeon_step -= add_step

            if dungeon_step - add_step <= 0:
                add_gold = 1 + random.randrange(KYUYO_VALUE)
                player_gold += add_gold
                SendMessage("--- ダンジョン ---\n調査完了\n{}円獲得".format(add_gold))
                current_event = "none"
                dungeon_step = STEP_VALUE

            else:
                if random.randrange(2) >= 1:
                    is_battle = True
                    SendMessage("--- ダンジョン ---\n敵に遭遇！")
                else:
                    is_battle = False
                    SendMessage("--- ダンジョン ---\n調査中")

    elif current_event == "damage":
        add_gold = 1 + random.randrange(SANZAI_VALUE)
        player_gold -= add_gold
        SendMessage("--- 突然の散財 ---\n{}円損失".format(add_gold))
        current_event = "none"

    elif current_event == "negirai":
        add_hp = 1 + random.randrange(NEGIRAI_VALUE)
        player_hp += add_hp
        if player_hp > 10:
            player_hp = 10
        SendMessage("--- 労い ---\n今日もお仕事頑張ってえらい！\nHP{}回復".format(add_hp))
        current_event = "none"
```

全コードを載せると長くなってしまうため、追加部分のみ載せます。
ファイルの変更・削除・作成・移動が行われると、上記のメソッドが実行されるといった仕組みにしました。
イベントの種類を取得することが出来るので、イベントに応じてゲームの内容を変えてみるのもいいかもしれません。

またメイン関数の observer.join() の後に、

```obsesrve.py
SendMessage("--- リザルト ---\n{}円".format(player_gold))
```

を追加して、終了時にプレイヤーの所持金を送信するようにしました。

***

それでは、とある社員がcppファイルを変更し続けたというシチュエーションで、
ゲームの様子を観察してみましょう。

![step_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1780415/1ac958f4-d40b-f8f4-f0af-2706e49b4ea0.png)

**たくさん労っていただいたのに1円の負債を抱えてしまったようです。**
ゲームバランスは要調整ですね。