toioを動かす with ChatGPT

## 本記事について

ChatGPT APIを利用してコードを生成し、それを動的に実行することでtoioを動かした記事

chatgptで動的にコード実行する参考にはなるかもしれない。

## 成果物1 toioを1台動かす。Simulation

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/6095abcd-e865-a4c1-a4b4-2df980400eb0.gif)

プロンプト

```
cube1: patrol random 3 points once
```

<details><summary><b>生成されたコードを表示(折りたたみ)</b></summary><div>

```.lua
function routine()
  cubeCommand:ShowMessage('Starting patrol')
  
  -- Define 3 random points
  local points = {}
  for i=1,3 do
    local x = math.random(100, 400)
    local y = math.random(100, 400)
    table.insert(points, {x=x, y=y})
  end
  
  -- Move to each point and wait for 1 second
  for i, point in ipairs(points) do
    coroutine1 = startCoroutine(cubeCommand:Navi2TargetCoroutine('cube1', point.x, point.y))
    coroutine.yield(coroutine1)
    coroutine.yield(CS.UnityEngine.WaitForSeconds(1))
  end
  
  cubeCommand:ShowMessage('Patrol finished')
end
```
　
</div></details>

## 成果物2 toioを2台で相撲してみる。Real

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/522c688b-b968-a6ea-a73e-bc3e85291167.gif)

```
相撲をしてください。

配置はNavi, 突撃するときはMoveを1s使ってください。

勝敗はどちらかが勝つように決めてください。
負けた方は四隅のどこかに飛ばされてください。

## Cube1
技: フェイント
少し前に進み、すぐ後ろに下がる

## Cube2
技: spin
cube2がspinすると半径50以内にいる相手は吹き飛んで負けます。

## 相撲の流れ

1. スタートポジションにつく。 cube1がtop側 y300、cube2がbottom側でy200
2. 対峙する (cube1はdeg -90、cube2はdeg 90) Use Rotate2DegCoroutine
3. 0.5秒待つ
4. 「はっけよーい」のメッセージ
5. 0.5秒待つ
6. のこった！のメッセージ
7. それぞれのCubeが正面に向かって突撃する Use 「cubeCommand:Move」両方ともspeed 70(not minus)
8. 0.2秒待つ
9. 後ろに下がる
10. 0.2秒待つ
11. それぞれ技を出してください。
12. 0.2秒待つ
13. どちらかが負けて吹き飛ばされてください。
14. 0.1秒待つ
15. 負けた方がspinしてください。
16. 勝敗宣言をしてください

突撃にはMoveを使用してください。突撃でNavi2Targetは使用しないでください。
```

<details><summary><b>生成されたコードを表示(折りたたみ)</b></summary><div>

```.lua
function routine()
  -- スタートポジションにつく
  coroutine1 = startCoroutine(cubeCommand:Navi2TargetCoroutine('cube1', 250, 300))
  coroutine2 = startCoroutine(cubeCommand:Navi2TargetCoroutine('cube2', 250, 200))
  coroutine.yield(coroutine1)
  coroutine.yield(coroutine2)
  coroutine.yield(CS.UnityEngine.WaitForSeconds(0.5))

  -- はっけよーい
  cubeCommand:ShowMessage('はっけよーい')
  coroutine.yield(CS.UnityEngine.WaitForSeconds(0.5))

  -- のこった！
  cubeCommand:ShowMessage('のこった！')
  coroutine.yield(CS.UnityEngine.WaitForSeconds(0.5))

  -- 突撃
  coroutine1 = startCoroutine(cubeCommand:Move('cube1', 100, 100, 1000))
  coroutine2 = startCoroutine(cubeCommand:Move('cube2', -100, -100, 1000))
  coroutine.yield(coroutine1)
  coroutine.yield(coroutine2)

  -- 技を出す
  coroutine1 = startCoroutine(faint, 'cube1')
  coroutine2 = startCoroutine(spin, 'cube2')
  coroutine.yield(coroutine1)
  coroutine.yield(coroutine2)

  -- 勝敗宣言
  if cubeCommand:GetCubePosY('cube1') > cubeCommand:GetCubePosY('cube2') then
    cubeCommand:ShowMessage('cube1の勝ち！')
    coroutine.yield(startCoroutine(cubeCommand:Navi2TargetCoroutine('cube2', 50, 50)))
  else
    cubeCommand:ShowMessage('cube2の勝ち！')
    coroutine.yield(startCoroutine(cubeCommand:Navi2TargetCoroutine('cube1', 450, 450)))
  end
end

function faint(id)
  -- フェイント
  coroutine.yield(CS.UnityEngine.WaitForSeconds(0.1))
  coroutine1 = startCoroutine(cubeCommand:Move(id, 100, 100, 500))
  coroutine.yield(coroutine1)
  coroutine.yield(CS.UnityEngine.WaitForSeconds(0.1))
  coroutine2 = startCoroutine(cubeCommand:Move(id, -100, -100, 500))
  coroutine.yield(coroutine2)
end

function spin(id)
  -- spin
  coroutine.yield(CS.UnityEngine.WaitForSeconds(0.1))
  coroutine1 = startCoroutine(cubeCommand:Move(id, 60, -60, 200))
  coroutine.yield(coroutine1)
  coroutine.yield(CS.UnityEngine.WaitForSeconds(0.1))
  coroutine2 = startCoroutine(cubeCommand:Move(id, -60, 60, 200))
  coroutine.yield(coroutine2)
  coroutine.yield(CS.UnityEngine.WaitForSeconds(0.1))
  coroutine3 = startCoroutine(cubeCommand:Move(id, 60, -60, 200))
  coroutine.yield(coroutine3)

  -- 判定
  if math.sqrt((cubeCommand:GetCubePosX('cube1') - cubeCommand:GetCubePosX('cube2'))^2 + (cubeCommand:GetCubePosY('cube1') - cubeCommand:GetCubePosY('cube2'))^2) < 50 then
    cubeCommand:ShowMessage(id .. 'の勝ち！')
    if id == 'cube1' then
      coroutine.yield(startCoroutine(cubeCommand:Navi2TargetCoroutine('cube2', 50, 50)))
    else
      coroutine.yield(startCoroutine(cubeCommand:Navi2TargetCoroutine('cube1', 450, 450)))
    end
  end
end
```
　
</div></details>

## リポジトリ

クライアント

https://github.com/teach310/ToioAISample

サーバー

https://github.com/teach310/ai-rails

## 仕組み

![スクリーンショット 2023-04-30 18.58.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/c972cbe4-2870-fe2b-e953-e5a398fd2e44.png)

unity側のロジックは以下

https://github.com/teach310/ToioAISample/blob/main/Assets/ToioAI/Runtime/ToioAIScene.cs

サーバーのロジックは以下みればだいたい書いてある。

https://github.com/teach310/ai-rails/blob/main/app/models/toio/lua_generator.rb

## 知見

コード以外のものを含めてくる問題
=> 「```」で囲まれたコードブロックとってくることで対処

chatgptが生成するluaのバージョンが低く、廃止されているメソッドを生成してしまう。(math.atan2)
=> 幸い置換(math.atan2 => math.atan)でなんとかなった。

while trueで無限ループしたり、実行するとフリーズするやばいコードを作ってくる時がある。
=> while true禁止を指示しても使うため防ぐのなかなか厳しい。

同期的に呼ぶ関数を非同期に呼ぼうとする。
=> 非同期な呼び方をしても大丈夫なように同期メソッドを作る。

非同期な並列処理も自動生成できるけど精度かなり落ちる
=> 複数台動かすといった処理の生成が向いてない。

chatgptが生成するコードは単語のイメージに強く依存する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/5f29cb8e-9130-d8a4-3df4-de8baa30296f.png)

toioは専用のマットの上だと自分の位置の絶対座標が取得できる。
で、その絶対座標は左上原点のため「topに行くほどYの値が小さくなる」

「top」という単語には正のイメージがあるらしく、
そのままこの座標を使うと、「go left top」でleft bottomに進むコードが
生成されがちになる。

そのため、左下原点の位置をもらうのが有効

```.cs
// NOTE: 左上原点の座標系だとchatgptの精度落ちるため左下原点の座標系での点をもらう。
double ReversePosY(double y)
{
    return 500 - y;
}
```

## 関連

xLua の使い方

https://qiita.com/Teach/items/518156d203d7c6aa42fa