第1回 はじめての花映塚AI

花AI塚のチュートリアルが欲しいな、ということでチュートリアルの連載を始めました。 おおよそ隔週ペースで全4回で進めていこうかと考えています。

とりあえずサンプルを動かす

今回の記事では花AI塚に付属しているサンプルAI「random-walk」について見ていきます。 ということでまずrandom-walk AIを実際に動かしてみます。

花AI塚でAIを動かすためにはまず設定が必要です。花AI塚のフォルダにある「setting.exe」を起動して設定を行います。

setting.exeを起動する
setting.exeを起動する
setting.exeを起動するとこんな感じ
setting.exeを起動するとこんな感じ

初めて花AI塚を使う場合は東方花映塚のexeファイル(th09.exe)の指定を行う必要があるので、まずこれを設定します。

th09.exeのファイルパスを指定
th09.exeのファイルパスを指定

次に今回動かすAI(つまりrandom-walk AI)を指定します。今回は1P側でAIを動かすことにします。 花AI塚のフォルダの「sample-scripts/random-walk/main.lua」を指定します。

AIのスクリプトパスを指定
AIのスクリプトパスを指定

ひと通り設定を終えると以下の通りになります。「OK」を押して終了しましょう。

設定終了後
設定終了後

設定が終わったのでいよいよAIを動かします。 「ka_ai_duka.exe」を起動すると花映塚が立ち上がります。 とりあえずマッチモード「人 vs 式」を選択して始めましょう。

ka_ai_duka.exeを起動 対戦モード選択

対戦が始まると1Pがぶるぶる震える感じで動きます。 random-walk AIはランダムに色んな方向に動くだけのAIなので、すぐに弾や敵に激突してしまいます。 とはいえ実際にAIが動くことが確認できたと思います。

実際のプレイ
実際のプレイ

tips

今回はsetting.exeで設定を行いましたが、ka_ai_duka.iniをメモ帳などのテキストエディタで直接編集して設定することもできます。

サンプルを読み解く

先ほど動かしたrandom-walk AIの中身を見ていきます。 random-walk AIは2つのファイルからできています。

main.luaがAIの本体にあたるファイルで、keyutils.luaは自機操作まわりをラップしているライブラリです。

ここでは`main.luaを見ていきましょう。実際のmain.luaにはデバッグ用のコードが含まれていますが、ここでは省略しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
local keyutils = dofile("keyutils.lua");

local function generateCandidates()
  local candidates = {};
  local keys = { "up", "right", "down", "left", "up" };
  -- stop
  table.insert(candidates, {
    keys={}
  });
  for i=1,4 do
    table.insert(candidates, {
      keys={keys[i]},
    });
    table.insert(candidates, {
      keys={keys[i], "shift"}
    });
    table.insert(candidates, {
      keys={keys[i], keys[i+1]}
    });
    table.insert(candidates, {
      keys={keys[i], keys[i+1], "shift"}
    });
  end
  return candidates;
end

local function choice(candidates)
  return candidates[math.random(#candidates)].keys;
end

function main ()
  -- generate candidates
  local candidates = generateCandidates();
  -- choice
  local keys_to_send = choice(candidates);

  -- send keys
  local keys = keyutils.newstate()
  for i,key in ipairs(keys_to_send) do
    keys[key] = true;
  end
  keyutils.send(keys);
end

花AI塚は対戦の間毎フレームmain関数を呼び出します。よってAIの処理の起点はmain関数となります。

それではmain関数を見ていきます。 ここで行っている処理は3つです。

移動操作の列挙というのはつまり、「上に移動」、「右に移動」、「左下に低速移動」といったように移動操作を片っ端から挙げていくことです。 今回のrandom-walk AIに限らず、ゲームAIは

ということを行っています。random-walk AIの場合は、今できる操作として移動操作のみを挙げていて、選択肢から選ぶときの尺度がただのランダムといった感じになっています。

generateCandidates

3行目~25行目で定義されている関数generateCandidatesは移動操作の列挙を行う関数です。 この関数は以下のようなテーブルを戻り値として返します。

{
  { keys = {} },
  { keys = {"up"} },
  { keys = {"up", "shift"} },
  { keys = {"up", "right"} },
  { keys = {"up", "right", "shift"} },
  { keys = {"right"} },
  { keys = {"right", "shift"} },
  { keys = {"right", "down"} },
  { keys = {"right", "down", "shift"} },
  { keys = {"down"} },
  { keys = {"down", "shift"} },
  { keys = {"down", "left"} },
  { keys = {"down", "left", "shift"} },
  { keys = {"left"} },
  { keys = {"left", "shift"} },
  { keys = {"left", "up"} },
  { keys = {"left", "up", "shift"} }
}

テーブルの各要素がkeysというフィールドを持っていて、keysの中には文字列の配列が格納されている、といったものです。 このkeysの中身が移動操作のキー入力の組み合わせに対応しています。

例えば上移動なら

{ keys = {"up"} }

左下移動なら

{ keys = {"down", "left"} }

左下低速移動なら

{ keys = {"down", "left", "shift"} }

といった感じです。 まったく移動しない(停止する)操作は空の配列で表します。

{ keys = { } }

choice

27行目~29行目で定義されている関数choiceは引数candidatesで渡されたテーブルの中からランダムに1つを選んで、その要素のkeysフィールドの値を返します。 candidatesはさきほどの関数generateCandidatesの戻り値と同じものです。

例えばcandidatesの3番目の要素

  { keys = {"up", "shift"} },

を選んだとすると、関数choiceの戻り値は配列

{"up", "shift"}

となります。

つまり関数choiceの戻り値はどのキーを押すかの組み合わせを表す配列が返ってくるという訳です。

AIを書く

今度はいよいよ実際にAIを書いていきます。

最初なので、弾を避けるとか考えずに左右を往復するだけのAIを作ります。

適当なところにフォルダを作って、その中にmain.luaを用意します。

hoge
 + main.lua

AIの処理は毎フレームmain関数が呼ばれることで行われます。ということでとりあえずmain関数を書きます。

1
2
function main()
end

random-walk AIのときはmain関数の中身は

といったような流れでした。

左右に往復するAIの場合は選ぶ移動操作は

のいずれかです。毎フレーム、どちらかを選んでキー入力を送るというのが左右往復AIの場合のmain関数の流れとなります。

右移動すべきか、左移動すべきかを決めるところについて考えてみましょう。 今仮に右移動していたとして、左に折り返す必要にせまられるのはいつかというと、画面右端までたどり着いた時です。 同じように左移動していたとき、右に折り返す必要にせまられるのは画面左端にたどり着いた時です。 ということで今の考えをコードに落とすとこんな感じになります。

1
2
3
4
5
6
7
8
9
10
11
12
function main()
  if(--[[ 右に移動してる and 右端にぶつかった ]]) then
    -- 左に折り返す
  elseif (--[[ 左に移動してる and 左端にぶつかった]]) then
    -- 右に折り返す
  end
  if (--[[ 右に移動してる]] ) then
    -- 右移動のキーを送る
  else
    -- 左移動のキーを送る
  end
end

今自機は右に移動しているのか、そうでないのかを状態として持っておく必要があります。 ここではブール値の変数としてis_moving_rightを用意して、この変数で表すことにします。 そうするとさっきのコードは

1
2
3
4
5
6
7
8
9
10
11
12
13
local is_moving_right = true
function main()
  if(is_moving_right --[[ and 右端にぶつかった ]]) then
    is_moving_right = false
  elseif (not(is_moving_right) --[[ and 左端にぶつかった]]) then
    is_moving_right = true
  end
  if (is_moving_right) then
    -- 右移動のキーを送る
  else
    -- 左移動のキーを送る
  end
end

のようになります。

残るは画面端にぶつかったかどうかの判定と、キー入力の送信だけです。

花AI塚ではゲームの状態に関する様々な情報を手に入れることができます。 自機の座標が分かれば画面端にぶつかったかどうかも分かります。 ということで自機の座標を取得するコードを加えてみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local is_moving_right = true
function main()
  -- ここから自機座標の取得
  local my_side = game_sides[player_side]
  local x = my_side.player.x
  -- ここまで
  if(is_moving_right --[[ and 右端にぶつかった ]]) then
    is_moving_right = false
  elseif (not(is_moving_right) --[[ and 左端にぶつかった]]) then
    is_moving_right = true
  end
  if (is_moving_right) then
    -- 右移動のキーを送る
  else
    -- 左移動のキーを送る
  end
end

ゲームの状態に関する情報はgame_sidesという変数からアクセスできます。

game_sides[1] 1P側の情報
 + player 自機の情報
    + x
    + y
    + ...
 + enemies 敵の情報
 + bullets 弾の情報
 + exAttacks EXアタックの情報
 + items アイテムの情報
game_sides[2] 2P側の情報
 + ...

また、AIが1P側なのか2P側なのかはplayer_sideという変数から分かります(1P側なら1、2P側なら2)。 なのでgame_sides[player_side]でAIの居る側の情報が取れるという訳です。

こうして手に入った自機座標を使って画面端にぶつかったかを判定します。 x座標がある値以上になったら右端にぶつかった、ある値以下になったら左端にぶつかったと判定することにします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local X_MAX = 136
local X_MIN = -136
local is_moving_right = true
function main()
  local my_side = game_sides[player_side]
  local x = my_side.player.x
  if(is_moving_right and x >= X_MAX) then
    is_moving_right = false
  elseif (not(is_moving_right) and x <= X_MIN) then
    is_moving_right = true
  end
  if (is_moving_right) then
    -- 右移動のキーを送る
  else
    -- 左移動のキーを送る
  end
end

Q&A: X_MAXX_MINの値はどう決めた?

X_MAXX_MINの値ですが、自機座標の値のログを取って決めてます。 花AI塚ではAIの動作中も人間のプレイヤーのキー入力を受け付けています。 なので下のように座標のログを取るだけのAIを用意して、AIの動作中に手動操作で自機を動かせばその自機の座標のログを取ることができます。

1
2
3
4
5
6
fp = io.open("tmp.txt","w");
function main()
  local my_side = game_sides[player_side]
  local x = my_side.player.x
  fp:write(tostring(x).."\n")
end

あとはキー入力を送るコードを書けば完成です。 random-walk AIでは別途keyutilsというライブラリを作っていましたが、今回は花AI塚で用意されている関数を直接呼ぶことにします。

キー入力の送信にはsendKeysという関数を使います。この関数は整数値を引数に取ります。 この引数の各bitがどのキーを送るかに対応しています。 対応は以下の通りです。

例えば0x40は7bit目だけが1で他がゼロなので、「←キー」だけの入力になります。 低速で左に移動したいときは3bit目と7bit目を1にするので、sendKeys(0x44)となります。

キー入力のコードを加えるとAIのコードは下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local X_MAX = 136
local X_MIN = -136
local is_moving_right = true
function main()
  local my_side = game_sides[player_side]
  local x = my_side.player.x
  if(is_moving_right and x >= X_MAX) then
    is_moving_right = false
  elseif (not(is_moving_right) and x <= X_MIN) then
    is_moving_right = true
  end
  if (is_moving_right) then
    sendKeys(0x80)
  else
    sendKeys(0x40)
  end
end

それでは実際にAIを動かしてみましょう。setting.exeで動かすAIを指定したあとで、ka_ai_duka.exeを起動すると動きます。

作ったAIを指定する
作ったAIを指定する

ここまででAIを実際に作るところまで見てきましたが、いかがだったでしょうか。

今回作ったAIは弾を避けたりしない単純なものでしたので味気ないかもしれません。 次回は弾や敵を避けるAIを作っていきます(予定)。

もどる