Open6

factorioでbotを作ろう

choro3choro3

自動プレイするModを試しに作る

何かのイベントをトリガーに、あらかじめベタ打ちしておいた移動とかクラフトを実行させてみる。

自作Modをロードする

公式チュートリアルの通りに、所定の位置にLuaで書いたModとメタデータのJSONファイルを置く。
Macの場合は $HOME/Library/Application\ Support/factorio/mods/<MODNAME> 以下になる https://wiki.factorio.com/Application_directory

今回は自作のエンティティを追加する予定などはないので、Data StageのあたりやLocaleについては読み飛ばす。control.lua だけ作成して、ゲーム内チャットのechoをするコードを書いてみた。

--control.lua

script.on_event(defines.events.on_console_chat,
    function(event)
        script.raise_console_chat{player_index=1, message=event.message}
    end
)

フックするイベントはリファレンスから適当に検索。呼び出すメソッドがどのオブジェクトのものかは各ページの最上部に記載がある。(... the global object named... とか書いてある)

Luaのメソッド呼び出しの方法がよくわからない(波括弧じゃないといけないものがある?)が一旦リファレンスの見よう見真似でやっていく。

ファイルを配置したらfactorio起動。「Mod」から今書いたModがロードされていることを確認する。info.json の name に書いた名前で表示されるはず。

上で書いたコードは動かない。raise_console_chat でチャットにメッセージを送ることで on_console_chat イベントが発火するので無限ループになってしまう...
他の適当なテキスト出力の手段を探して置き換える。

--control.lua

script.on_event(defines.events.on_console_chat,
    function(event)
        game.show_message_dialog{text=event.message}
    end
)

無事出力された。

choro3choro3

自動プレイするModを試しに作る (続き)

Mod自体はインストールして動かせたので、中身を何かプレイヤーを動かすコードにしてみる。

メソッド探し

プレイヤーを歩かせてみようと思ったが、それらしいメソッドが見つからない。LuaPlayerteleport は瞬間移動になりそうだし、LuaGameScriptにあるプレイヤー系のメソッドも盤面上の操作ではなさそう...

ググったところ player.walking_state に値を書いてやればいいらしい。(関係ないけどbotを作ろうとしている人自体は他にもいるみたいだ)
https://www.reddit.com/r/factorio/comments/b4i1yx/controlling_the_player_through_a_mod/

--control.lua

remain = 30

script.on_event(defines.events.on_tick,
    function(event)
        if remain > 0 then
            game.get_player(1).walking_state = {walking = true, direction = defines.direction.north }
            remain = remain - 1
        end
    end
)

これでゲームを開始するとプレイヤーが30tick分上に動く。

choro3choro3

外部入力を受け付けるようにしてみる

最終的には「ゲームの状態を出力する」→「思考ルーチンはそれを受け取り次の行動を出力する」→「次の行動をゲームに反映する」というサイクルを繰り返す形にしたい。
一つのModとしてベタ書きしてもいいが、このサイクルに沿うようbotとゲーム本体を分離できるといい...

アプローチとしては二つ。

  • 完全に外部のプロセスとしてbotを動かして、RCONでゲームに接続して入出力
  • 思考ルーチンとそれ以外を別Modとして、remote で相互にコマンド呼び出し

Luaがわからない&botが高度になったら早い処理系で動かしたくなりそうということで、RCON経由の方を試す。

RCON 有効化

MacのGUIからfactorioを起動する場合、RCONを有効化するには $HOME/Library/Application\ Support/factorio/config/config.ini に設定を書く。内容は https://wiki.factorio.com/Console/ja にあるコマンドラインパラメータと同じ。

# コメントアウトを外して自分の環境に合わせて適宜設定
; Socket to host RCON on when lauching MP server from the menu.
local-rcon-socket=<IPADDRESS>:<PORT>

; Password for RCON when launching MP server from the menu.
local-rcon-password=<PASSWORD>

マルチプレイヤーモードでないと通信できないと思ってたが、シングルモードでも問題ない模様

RCON経由でコマンド実行

チャットから /c でコマンドを打ち込むのと同じ要領。RCONのクライアントはとりあえず https://github.com/gorcon/rcon-cli を使った。

docker run -it outdead/rcon ./rcon -a <IPADDRESS>:<PORT> -p <PASSWORD>
Waiting commands for <IPADDRESS>:<PORT> (or type :q to exit)
> 

動かしてみる。

> hello
2024-06-17 02:37:07 [CHAT] <server>: hello
> /c game.get_player(1).walking_state = {walking = true, direction = defines.direction.north}
2024-06-17 02:37:31 [COMMAND] <server> (コマンド): game.get_player(1).walking_state = {walking = true, direction = defines.direction.north}
> 
choro3choro3

外部入力を受け付けるようにしてみる (続き)

RCONから直接LuaのAPIが呼べることがわかったので、あとはプログラムからコマンドを流し込んでやればいい。https://github.com/mark9064/factorio-rcon-py を使って Python で書く

import factorio_rcon

client = factorio_rcon.RCONClient("<IPADDRESS>", <PORT>, "<PASSWORD>")

while True:
    response = client.send_command("/silent-command game.get_player(1).walking_state = {walking = true, direction = defines.direction.north}")

動きはするが、かくついていて遅い。walking_statetick単位で状態を表すとあるので、RCON経由でのコマンド実行のポーリング間隔が数tick単位か、あるいはコマンド実行のせいでtickそのものの実時間での間隔が伸びてしまっているかになっているのだと思う。

移動中フラグみたいなものを別に用意してそれが真である間は on_tickwalking_state を更新、外部コマンドからはフラグの上げ下げだけする、みたいにすると良さそう。
元々bot作成に向いているようなAPIには見えなかったが、これでラッパーAPIを作らないといけないことが(今の方針で設計する限りは)確実になった気がする...

choro3choro3

インターフェイス素案

試しに移動だけしてみる。Lua の jsonhttps://gist.github.com/tylerneylon/59f4bcf316be525b30ab を拝借した

-- control.lua

local action_queue = require("action-queue")
local json = require("json")

-- actions queue and other global states

local queue = action_queue.new()
local current = nil

-- action executors

function executor_walk(begin_tick, action)
    game.get_player(1).walking_state = {
        walking = true,
        direction = defines.direction[action.params.direction]
    }
    cont = game.tick - begin_tick < action.params.period
    return true, cont
end

-- routine

function get_executor(action_type)
    local executors = {
        walk = executor_walk,
    }
    return executors[action_type]
end

function evaluate(tick_event) 
    if current == nil then
        if queue:len() == 0 then
            return
        end
        current = {
            tick = tick_event.tick,
            action = queue:pop()
        }
        game.get_player(1).print("pop action: " .. current.action.kind .. " " .. current.action.params.direction)
    end
    executor = get_executor(current.action.kind)
    success, cont = executor(current.tick, current.action)
    if not cont then
        current = nil
    end
end

-- command defs

function command_act(command)
    local action = json.parse(command.parameter)
    queue:push(action)
    game.get_player(1).print("push action: " .. command.parameter)
end

function command_state(command)
end

function command_state_delta(command)
end


-- register handlers

script.on_event(defines.events.on_tick, evaluate)

commands.add_command("act", nil, command_act)
commands.add_command("state", nil, command_state)
commands.add_command("state_delta", nil, command_state_delta)
-- action-queue.lua
function new()
    return {
        elms = {},
        push = push,
        pop = pop,
        repr = repr,
        len = len
    }
end

function push(self, action)
    table.insert(self.elms, action)
end

function pop(self)
    return table.remove(self.elms, 1)
end

function repr(self)
    for i = 1, #self.elms do
        print('- ' .. self.elms[i])
    end
end

function len(self)
    return #self.elms
end

return {
    new = new
}

bot本体側からぐるぐる歩き回るシーケンスを送り続けてみる

import factorio_rcon
import time

client = factorio_rcon.RCONClient(<IPADDRESS>, <PORT>, <PASSWORD>)

while True:
    client.send_command('/act {"kind":"walk","params":{"period":30,"direction":"north"}}')
    client.send_command('/act {"kind":"walk","params":{"period":30,"direction":"east"}}')
    client.send_command('/act {"kind":"walk","params":{"period":30,"direction":"south"}}')
    client.send_command('/act {"kind":"walk","params":{"period":30,"direction":"west"}}')
    time.sleep(2)