DCSS client luaを触る

この記事は Roguelike Advent Calendar 2016 の12日目の記事です。

前回の記事ではLuaスクリプトのお化けのような自動探索botを紹介しましたが、そこまでやりすぎ感を出さなくてもスクリプトを普段のプレイングに役立てていくことは十分に可能です。

とはいえ、Luaの解説をやっていたら本題が進まないため、Luaの文法に関しては以下のサイトを参照してください。

オンラインでLuaスクリプトの動作を確かめられるサイトもあります。

■DCSSにおけるLua

Dungeon Crawl Stone Soup(以下DCSS)の本体はC++で書かれていますが、マップ定義などの編集のしやすさといった利点から、Luaというスクリプト言語も利用されています。

だいたいソースリポジトリツリーのdat/以下にあります。

オンライン版ではLuaファイルを直接いじることはできないため、設定ファイルを介して色々やっていくことになります。

■一行Luaの使い方

設定ファイルの行先頭に : を打つことで、一行Luaスクリプトを使うことができます。
以下のように条件分岐するのが主な使い方です。

: if you.race() == "Ogre" or you.race() == "Troll" then
# オーガとトロルは大岩を拾う
autopickup_exceptions ^= <large rock
: end

you.race()は自種族を示す文字列を返してくれる組み込みAPIです。

■複数行Luaで関数を定義する

もうちょっと複雑な処理をしたい場合、Luaで関数を書いて適宜呼び出しする必要があります。
自分の名前を呼んでくれる関数を組んでみましょう。複数行スクリプトの場合はスクリプト部分を中括弧で囲む必要*1があります。

{
function hello()
    crawl.mpr("Hello, " .. you.name() .. "!")
end
}

crawl.mpr()は文字列を渡すとゲーム中にメッセージとして表示してくれる組み込みAPIです。
you.name()は自分のキャラクター名を返してくれる組み込みAPIです。

実際にゲーム中で呼び出すにはいくつか方法があります。

デフォルトで呼び出される関数を自分で書く
                if (!clua.callfn("ready", 0, 0) && !clua.error.empty())
                    mprf(MSGCH_ERROR, "Lua error: %s", clua.error.c_str());

main.cc 1437行目ですが、コマンド入力ごとに"ready"というLua関数を呼び出すようになっています。つまり、ここを自分で書けばゲーム中でもよろしく呼んでくれるわけです。

{
-- (中略)
function ready()
    hello()
end
}

こんな感じ。
ゲーム中での動作は以下のような感じになります。
まあ、このような毎回表示を行うような関数は実用上は不向きではあります。

前回記事のqw botもready()関数を独自定義することによって「何かボタンを押したら行動」という動作を実現させています。

デフォルトで呼び出される関数を乗っ取る

ゲーム中にTabを押すと最寄りの敵を殴るようになっていますが、これはdat/clua/autofight.luaのhit_closest()関数で定義されており、設定ファイルで上書きが可能です。

{
-- (中略)
-- hit_closest関数上書き
function hit_closest()
    crawl.mpr("Tab pressed!")
    hello()
end
}

Tabを押すたびにこのように表示されるようになります。

マクロから呼び出し

マクロで===に続いて関数名を付加することによって、特定のキーから呼び出せるようになります。


■もうちょっと実用的な例

さすがにハローワールドでは実用性に乏しいので、もう少しゲームに使えそうなのを考えてみます。

ダメージ量警告

DCSSスレの過去ログを眺めていたらこのような投稿がありました。
Dungeon Crawl Stone Soupスレ part8 >>168 より

168 :名@無@し:2014/07/25(金) 22:06:04 ID:???
webtiles見てたら
You take 10 damage, and have 213/223 hp!
という風に受けたダメージ量をメッセージ欄に表示させてた人が居たのですが
こういうのはinit.txtとかを弄ればローカルの環境でも出せるんでしょうか 

今になって考えるとたぶんLuaスクリプトでなんかやってたんでしょうね。組んでみましょう。
どうせなのでついでに、10〜19ダメージなら黄文字、20ダメージ以上で赤文字で出すことにします。

{
local old_hp = -1
local old_mhp = -1

function turn_hp_warning()
    if old_hp < 0 then
       old_hp, old_mhp = you.hp()
    end

    local hp, mhp = you.hp()
    local msg = "You take " .. old_hp - hp .." damage, and have " .. hp .. "/" .. mhp .. " hp!"

    if old_hp - hp >= 20 then
        crawl.formatted_mpr(msg, "danger");
    elseif old_hp - hp >= 10 then
        crawl.formatted_mpr(msg, "warn");
    end

    old_hp, old_mhp = hp, mhp
end

function ready()
    turn_hp_warning()
end
}


WebtileにおけるCtrl-W代替コマンド

ブラウザのアップデートのせいか、最近FirefoxでCtrl+Wをフックしてくれず、「タブを閉じる」が発動してしまうようになりました。
まあゲーム中ならブラウザ側が警告してくれるのでよいのですが、中継地点の設置ができないのは困りものです。
普通のキーマクロだとCtrl+○系のコマンドは直接記述できないようなので、Luaスクリプトで関数を書きましょう。

  • [追記]: マクロでCtrl+○系を使う方法はありました。"\{^W}"を記述するといいです。
{
function set_waypoint()
    crawl.process_keys(control('w'))
end

function control(c)
    return string.char(string.byte(c) - string.byte('a') + 1)
end
}

あとは適当なキーに'===set_waypoint'を登録すればOK。

Repel Missiles自動張り直し

直近のメッセージでRepel Missilesの効果が終了した場合、(失敗率が10%以下なら)自動で張り直します。
もちろんターンは消費するので微妙かもしれません。

{
function recast_rmsl()
    spell = "Repel Missiles"
    if crawl.messages(5):find("You feel your spell is no longer protecting you from missiles.") or
       crawl.messages(5):find("You feel less protected from missiles.") then
        rmsl = spells.letter(spell)
        if rmsl and spells.fail(spell) <= 10 then
            crawl.process_keys("z" .. rmsl .. ".")
        end
    end
end
-- ready()からの呼び出し略
}

■組み込みAPI

Lua側に公開されたDCSS本体のAPIはsource/l_○○.ccで定義されています。
先程使ったyou.name()、you.hp()はl_you.ccに、crawl.mpr()、crawl.formatted_mpr()、crawl.messages()はl_crawl.ccに、spells.letter、spells.fail()はl_spells.ccに、といった感じです。

長くなりすぎるのでこの記事では詳細を省きます。気力が湧いたらなんか書きます(フラグ)

■まとめ

  • Luaスクリプトで普段のDCSSライフのかゆいところに手が届く
  • 設定ファイルをどんどん育てていこう(本末転倒)

さて次の記事はdisさんの「ZinのRecite解析してみたの巻」です!


*1:< 〜 >で囲んでもいいです