factorioでbotを作ろう
参考資料
ざっと調べた限りでは先行例がほぼない。modding用の口が開けてあるのでそこから地道に作っていくしかなさそう。
github で "factorio bot" で検索をかけた結果。他にも該当はあるが、Discord向けのchatbotと建築・物流ロボット系のModがほとんど。
自動プレイする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
)
無事出力された。
自動プレイするModを試しに作る (続き)
Mod自体はインストールして動かせたので、中身を何かプレイヤーを動かすコードにしてみる。
メソッド探し
プレイヤーを歩かせてみようと思ったが、それらしいメソッドが見つからない。LuaPlayer
の teleport
は瞬間移動になりそうだし、LuaGameScript
にあるプレイヤー系のメソッドも盤面上の操作ではなさそう...
ググったところ player.walking_state
に値を書いてやればいいらしい。(関係ないけどbotを作ろうとしている人自体は他にもいるみたいだ)
--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分上に動く。
外部入力を受け付けるようにしてみる
最終的には「ゲームの状態を出力する」→「思考ルーチンはそれを受け取り次の行動を出力する」→「次の行動をゲームに反映する」というサイクルを繰り返す形にしたい。
一つの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}
>
外部入力を受け付けるようにしてみる (続き)
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_state
はtick単位で状態を表すとあるので、RCON経由でのコマンド実行のポーリング間隔が数tick単位か、あるいはコマンド実行のせいでtickそのものの実時間での間隔が伸びてしまっているかになっているのだと思う。
移動中フラグみたいなものを別に用意してそれが真である間は on_tick
で walking_state
を更新、外部コマンドからはフラグの上げ下げだけする、みたいにすると良さそう。
元々bot作成に向いているようなAPIには見えなかったが、これでラッパーAPIを作らないといけないことが(今の方針で設計する限りは)確実になった気がする...
インターフェイス素案
試しに移動だけしてみる。Lua の json
は https://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)