まず、wasmbotsとは
という話をしないといけません。これはwasmバイナリを作って、ダンジョン探索をするゲームです。
具体的には?
ダンジョンを探索するためのプログラムを書いて、それを特定のインタフェースを満たしたWASMファイルにコンパイルすれば、あとはそれをアップロードすることでプレイヤーに自動でダンジョン探索をさせることができる、というブラウザゲームです。
プロジェクトのサンプルダンジョンにアクセスしてみれば即イメージが掴めると思います。
アクセスしたら、いくつかプリセットでアルゴリズムが用意されていることがわかります。

適当に1つ選んだら ▶️ のボタンを押すと探索が始まります。
demo用 pic.twitter.com/SEPfMyWcH2
— Uchio Kondo💥 (@udzura) 2025年2月23日
同じように、ダンジョン探索のアルゴリズムを任意の言語で書いて、WASMにコンパイルしてアップロードしたら、自分のコードでプレイヤーにダンジョンを探索させられます。
ただ、任意の言語と言っても、以下の条件を満たしたWASMを生成できないといけません*1。
- 特定の関数をexportするよう指定しなければならない
- 以下の4つ
void clientInitialize(void)
size_t setup(size_t requestReserve)
bool receiveGameParams(size_t offset)
void tick(size_t offset)
- 以下の4つ
- 特定の関数をimportして利用することも可能である
int32_t getRandomInt(int32_t min, int32_t max)
,void logFunction(int logLevel, unsigned int msgPtr, unsigned int msgLen)
,void shutdown(void)
が用意されている
この条件では例えばGo(GOARCH=wasm などの場合)、そしてRuby.wasmではちょっと難しいかもしれません。また、WASM GCの環境に依存した言語だと動くブラウザと動かないブラウザが出てきそうに思います。
公式のサンプルでは以下の言語が用意されています。
- C
- Rust
- Zig
- AssemblyScript
- tinygo
これらのサンプルをベースにコードを少しずつ変えて遊んでみることができそうです。WASMの素振りにも良い題材ではないでしょうか。
ところで、せっかくなので...
wasmbotsをRubyで遊ぶ手順を紹介しますね...。
まず、WASM生成の環境として、先述の通りruby.wasmでは難しいですので、 mruby/edge を使いましょう。
インストールは以下のような手順です。正確にはmruby/edgeを埋め込んだWASMバイナリを作るコマンドラインツールをインストールします。前提としてRustの環境は必須になるので入れておきましょう。
$ cargo install mec
その上で以下の3つのファイルを用意します。全て同じディレクトリに置いてください。
bot.rb
$memory = nil ## wasmbots framework part; 今回使用するenumだけ用意 LOGLEVEL_INFO = 2 LOGLEVEL_WARN = 1 LOGLEVEL_ERROR = 0 MESSAGE_TYPE_ERROR = 1 MESSAGE_TYPE_INITIAL_PARAMETERS = 2 MESSAGE_TYPE_PRESENT_CIRCUMSTANCES = 3 MESSAGE_TYPE_WAIT = 4 MESSAGE_TYPE_RESIGN = 5 MESSAGE_TYPE_MOVE_TO = 6 MESSAGE_TYPE_OPEN = 7 MESSAGE_TYPE_CLOSE = 8 DIRECTION_NORTH = 0 DIRECTION_NORTHEAST = 1 DIRECTION_EAST = 2 DIRECTION_SOUTHEAST = 3 DIRECTION_SOUTH = 4 DIRECTION_SOUTHWEST = 5 DIRECTION_WEST = 6 DIRECTION_NORTHWEST = 7 class PresentCircumstances def initialize(last_tick_duration, last_move_result, hit_points, surroundings) @last_tick_duration = last_tick_duration @last_move_result = last_move_result @hit_points = hit_points @surroundings = surroundings end attr_reader :last_tick_duration, :last_move_result, :hit_points, :surroundings end def clientInitialize logFunction(LOGLEVEL_INFO, "Hello, world! This is made by #{RUBY_ENGINE}") end def setup(requested_size) logFunction(LOGLEVEL_INFO, "received setup with size: #{requested_size}") $memory = SharedMemory.new(requested_size) name = "mruby/edge wasmbot" $memory[0..17] = name # $memory[18..25] = "\0" * 8 $memory[26..31] = [0, 2, 0].pack("S S S") $memory end def receiveGameParams(offset) param = $memory[offset..(offset+10)].unpack("S S S S C C C") logFunction(LOGLEVEL_INFO, "param version: #{param[0]}") logFunction(LOGLEVEL_INFO, "param engine version: #{param[1]}.#{param[2]}.#{param[3]}") logFunction(LOGLEVEL_INFO, "param diagonal_movement: #{param[4]}") logFunction(LOGLEVEL_INFO, "param player_stride: #{param[5]}") logFunction(LOGLEVEL_INFO, "param player_open_reach: #{param[6]}") true end def tick(offset) param_pre = $memory[offset..(offset+8)].unpack("I C S S") surroundings_len = param_pre[3] surroundings = [] surroundings_len.times do |i| ptr = offset + 9 + i tile = $memory[ptr..ptr].unpack("C") surroundings.push tile[0] end curcumstances = PresentCircumstances.new(param_pre[0], param_pre[1], param_pre[2], surroundings) $brain.on_tick(curcumstances) rescue Exception => e logFunction(LOGLEVEL_ERROR, "error: #{e.message}") logFunction("stopped") $memory[0..0] = [MESSAGE_TYPE_RESIGN].pack("C") end ## main player class class Brain def initialize @turn = 0 end attr_reader :turn def write_move!(direction) logFunction(LOGLEVEL_INFO, "direction: #{direction}") $memory[0..2] = [MESSAGE_TYPE_MOVE_TO, direction, 1].pack("C C C") end def on_tick(curcumstances) if turn > 100 logFunction(LOGLEVEL_ERROR, "turn > 100, stopped") $memory[0..0] = [MESSAGE_TYPE_RESIGN].pack("C") return end mod = @turn % 4 direction = case mod when 0 DIRECTION_NORTH when 1 DIRECTION_EAST when 2 DIRECTION_SOUTH else DIRECTION_WEST end write_move!(direction) @turn += 1 end end $brain = Brain.new
bot.import.rbs
def shutdown: () -> void def logFunction: (Integer, String) -> void def getRandomInt: (Integer, Integer) -> Integer
bot.export.rbs
def clientInitialize: () -> void def setup: (Integer) -> SharedMemory def receiveGameParams: (Integer) -> bool def tick: (Integer) -> void
ここまで準備したら、mecコマンドでWASMファイルをコンパイルできます。
$ mec --no-wasi wasmbots/bot.rb running: `cp ... src/` running: `cargo build --target wasm32-unknown-unknown --release` running: `cp ./target/wasm32-unknown-unknown/release/mywasm.wasm .../bot.wasm` running: `cd .. && rm -rf work-mrubyedge-vtkixnyy5X5IYJAIZ9ikamdiUKnqm9aB` [ok] wasm file is generated: bot.wasm
作業ディレクトリに bot.wasm
ができるので、このファイルをwasmbotsのダンジョンにuploadしたら動き始めます。
mruby/edgeでwasmbotsを遊ぶdemo pic.twitter.com/iIEj87chuH
— Uchio Kondo💥 (@udzura) 2025年2月23日
もう少し解説
フレームワーク部分は置いておいて、カスタマイズする部分はここになる感じです。
class Brain def initialize @turn = 0 end attr_reader :turn def write_move!(direction) logFunction(LOGLEVEL_INFO, "direction: #{direction}") $memory[0..2] = [MESSAGE_TYPE_MOVE_TO, direction, 1].pack("C C C") end def on_tick(curcumstances) if turn > 100 logFunction(LOGLEVEL_ERROR, "turn > 100, stopped") $memory[0..0] = [MESSAGE_TYPE_RESIGN].pack("C") return end mod = @turn % 4 direction = case mod when 0 DIRECTION_NORTH when 1 DIRECTION_EAST when 2 DIRECTION_SOUTH else DIRECTION_WEST end write_move!(direction) @turn += 1 end end $brain = Brain.new
#on_tick(curcumstances)
というメソッドを実装したクラスを実装して、 $brain
というグローバル変数に格納すれば動かせるように作っています。
今回のアルゴリズムはRubyで読めばわかるよねって感じですが、1ターンごとに北→東→南→西に動いてぐるぐる回るだけです。また、100ターン動いたら終了させています。
メソッドの最後で $memory
というオブジェクトにメッセージを書き込めば、次のアクション(移動、停止...)を指定できます。この $memory
オブジェクト(SharedMemory
という、mruby/edgeの組み込みクラスのインスタンスです)はWASMの外側、ブラウザ側からもアクセスできるメモリの領域を表現したものです。メッセージはバイナリを書き込むのですが、どういうレイアウトかは Cのサンプルコード を見るのが一番速いかも...*2。
合わせて、 PresentCircumstances
のインスタンスも渡されます。どういうクラスはコードを見る感じで...。curcumstancesを見るとどの方向に何のオブジェクトがあるかわかるので、例えば扉を開く、のようなアクションを取れたりもします。
細かいところはCやRustのサンプルコードを見て頑張る感じになります(そのうちもうちょっとドキュメントを書くかも)!
RubyでWASMを触ってみたい方、ぜひ遊んでください。
ちなみにmruby/edgeは絶賛基本的な機能を実装中なので、要望やコントリビュートもお待ちしています...。
なおバージョンは今はもう 1.0.5 です。