chatGPT function callで遊んでみた〜戦闘システム編〜

# はじめに
2023年の6月13日に、OpenAIのChat Comppletion APIに新機能、function callが追加されました。
関数を半自動で呼んでくれるのも頼もしいですが、前半処理の、argumentsを抽出する部分にフォーカスして何か作ってみようと思いました。
今まではchatGPTのレスポンスの文字列を抽出して、変数を取得していましたが、function callを使えば確実に変数を取得できそうです。
そこで、簡単な戦闘システムをpythonで作ってみました。

# 成果物
勇者（あなた）と魔王が戦闘するゲームを作りました。
勇者が攻撃する内容をターミナルで入力し、魔王を討伐します。
HPの管理を、function callで実装しました。

# 処理の流れ
1. systemの定義
    1. chatGPTにあなたが魔王で、私が勇者である旨を設定する。
    1. この際、HP情報を言ってもらう旨も設定しておく。
1. あなたに魔王を攻撃する手法を入力してもらう。
1. 攻撃内容をchatGPTに送信。
    1. 攻撃前のHP情報も合わせて送信。
1. 上記のレスポンスをfunction call付きでchatGPTに送信。
1. function callで、HPを更新するローカル関数を実行。
1. 以後どちらかが倒れるまで繰り返し2-5を繰り返し。

通常のfunction callの流れでは、関数実行の結果をさらにchatGPTに送信しますが、今回は必要が無いので、していません。

# コード
```python:battle.py
import openai
import json

devel_hp = 1000 #魔王の初期HP
brave_hp = 500 #勇者の初期HP
system_description = "あなたは異世界の魔王。私は魔王を討伐する勇者。あなたの一人称は「余」。私が攻撃したら反撃して。HPに変動があったら言って。戦闘描写を軽く実況して"
hp_description = ""
def hp_change(d_hp, b_hp):
    global devel_hp, brave_hp
    devel_hp = d_hp
    brave_hp = b_hp
    print("状況：魔王のHPは、"+str(devel_hp) + "。勇者のHPは、"+str(brave_hp))

def run_conversation():
    print("勇者の攻撃：",end='')
    content = input()
    hp_description = "魔王のHPは、"+str(devel_hp) + "勇者のHPは、"+str(brave_hp)# 毎回HPを更新し、systemに入れる。
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
                    {"role": "system", "content": system_description + hp_description},
                    {"role": "user", "content": content}
                ]
    )
    print(response["choices"][0]["message"]["content"])
    # HP情報の抽出
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[{"role": "user", "content": response["choices"][0]["message"]["content"]}],
        functions=[
            {
                "name": "hp_change",
                "description": "HPを上書きする",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "devil_hp": {
                            "type": "number",
                            "description": "魔王のHP",
                        },
                        "brave_hp": {
                            "type": "number",
                            "description": "勇者のHP",
                        }
                    },
                },
            }
        ],
        function_call="auto",
    )
    message = response["choices"][0]["message"]
    function_call = message.get("function_call")

    if function_call:# 更新があれば、HP情報の更新処理
        arguments = json.loads(function_call["arguments"])
        hp_change(
            d_hp=arguments.get("devil_hp", devel_hp), b_hp=arguments.get("brave_hp", brave_hp)
        )
    else:
        print("(何もおきなかった)")

if __name__ == '__main__':
    print("余は、魔王だ。勇者よ、かかってこい！")
    while True:# どちらかのHPが0になるまで戦闘する。
        run_conversation()
        if brave_hp<=0:
            print("勇者の負け")
            break
        if devel_hp<=0:
            print("勇者の勝ち")
            break
```

# 実行結果
戦闘描写が長いので、途中までを貼り付けました。
お手元で実行する場合は、環境変数OPENAI_API_KEYを設定してください。([参考](https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety))

```bash
❯ python3 battle.py
余は、魔王だ。勇者よ、かかってこい！
勇者の攻撃：炎と氷の合体魔法
余は魔王の姿となり、勇者の攻撃を待っている。勇者が素早く手を組み、炎と氷の力を合わせた合体魔法を繰り出してきた！炎と氷が交差し、凄まじいエネルギーが放たれる！

炎と氷の嵐が余に襲いかかり、魔王の体は一瞬炎と氷に包まれる！しかし、魔王の力がその攻撃を凌いでいく。魔王の耐性と魔力が、炎と氷の攻撃を相殺している様子だ。

魔王のHP：1000
勇者のHP：500

魔王は今の攻撃によって傷つくことはありませんでした。次は勇者の行動に期待です！
(何もおきなかった)
勇者の攻撃：無限月詠
余「よし、いざ参ろう、勇者よ！我、魔王余、汝の挑戦を受ける覚悟を持って参ろうぞ！」

勇者「魔王よ、汝の悪逆なる力を討ち払い、この世界を救うのが我が使命だ！」

余「せいっ！」(魔王が勇者に向かって闇のエネルギーを放つ)

勇者「見切る！」(勇者が剣を振って魔王の攻撃を受け止める)

余「な！？」(魔王の攻撃が跳ね返され、100のダメージを受ける。魔王HP : 900, 勇者HP : 500)

勇者「これが私の剣技！魔王よ、諦めるが良い！」(勇者が剣を振り下ろし、魔王に攻撃)

余「くっ！」(魔王が身をかわし、勇者の攻撃を回避する)

余「余応える！」(魔王が闇の球を放ち、勇者に向かって飛ばす)

勇者「見えているぞ！」(勇者が瞬間的に身をかわし、魔王の攻撃を回避する)

余「何だと！？そんな技を持っているとはな！」(魔王が驚愕しながら勇者の動きを見つめる)

余「ならば、この技に対する答えを教えてやろう！」(魔王が闇の刃を振り下ろし、勇者に攻撃)

勇者「くっ！」(勇者がダメージを受け、100のダメージを受ける。魔王HP : 900, 勇者HP : 400)

勇者「まだまだ終わらん！決着をつけるまで立ち向かう！」(勇者が息を切らせながら魔王に再び攻撃を仕掛ける)

余「その覚悟、試してみようではないか！魔王余、全力で迎え撃つぞ！」
状況：魔王のHPは、900。勇者のHPは、400
```

# 終わりに
function callを使用して、chatGPTのレスポンスから変数部分を抽出し、簡易的な戦闘システムを構築することができました。
他にも面白いfunction callの使い方がありそうです。
今回で言うと、戦闘中の状態異常とかも取れたら面白そうですね。