@ledsun blog

無味の味は佳境に入らざればすなわち知れず

ruby.wasmのKernel#sleepをどう実装したものか?

ruby.wasmを使ってブラウザ上でKernel#sleepを呼ぶとエラーが起きます。

<html>
<body>
  <script
    src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.4.1-2024-01-05-a/dist/browser.script.iife.js"></script>
  <script type="text/ruby">
    sleep 1
  </script>
</body>
</html>

エラーのスクリーンショット

とりあえずこんなパッチを当てれば動くことはわかっています。

module Kernel
  def sleep(time)
    JS.eval("return new Promise((resolve) => setTimeout(resolve, #{time * 1000}))").await
  end
end

現状Promise#newにコールバックをブロックで渡せないので、JS.evalを使っています。 これは直せると思います。

もう一つ悩みがあります。 JavaScriptsetTimeoutを使っています。 WASIのインターフェースを使ったほうがポータブルになりそうな気がします。

Add a sleep function to the Core · Issue #77 · WebAssembly/WASI · GitHub によると

I'm the current API, the way to implement sleep is to use poll_oneoff, polling for a single __WASI_EVENTTYPE_CLOCK event.

poll_oneoffという関数があるようです。

https://github.com/newpavlov/rust/blob/1e2b711d308be714e6211c125b1a33ac1247f866/src/libstd/sys/wasi/thread.rs#L30-L63をみると、rustではWASI用のThread.sleeppoll_oneoff関数を使って実装しているようです。

これらの情報をみても、ruyb.wasmにどうやって実装したらいいのか皆目見当もつきません。

Rubyでテトリスを実装する その8

Rubyでテトリスを実装する その7 - @ledsun blog の続きです。

今回は

  • 行を消すロジックの修正
  • T字形テトリミノの追加
  • デバッグ表示の追加

です。

行を消すロジックの修正

より宣言的にならないか工夫してみました。

https://github.com/ledsun/tetoris/blob/e5bb87a3c6a24049e8b3ea973a5965c25b5d643b/lib/field.rb#L34-L44

    filled_rows = in_field_rows.select { |row| row.filled? }
    return if filled_rows.empty?

    # 埋まった行を消す
    filled_rows.each { |row| row.clear! }

    # 消した行より上の、インフィールド内のすべてのブロックを消した行分下にずらす
    # 下から順番にずらす
    # 上からずらすと、ずらしたブロックがずらす前のブロックと衝突してしまう
    y = filled_rows.last.y
    in_field_blocks_over(y).reverse.each { |block| block.down filled_rows.size, self }

T字型テトリミノの追加

他のテトリミノも実装予定です。

デバッグ表示の追加

やはりデバッグが難しいです。 例外時に盤面の情報を出せるようにしてみました。

例外時に盤面の情報を出力しているスクリーンショット

ペン習字をはじめた

私は字が汚いです。 理由はわかっています。 脳内に収納している字形の解像度が低すぎるのです。 空で字を書くと、脳内の間違ったモデルをお手本に書くので、必ず汚い字が書けます。

克服するためには、脳に正しい字形をインプットする必要があります。 そこでペン習字です。 正しい字形をなぞって、正しい字形を覚えます。 絵を描くときのデッサンに似ています。 手を動かす訓練ではなく、字がどういう形なのかを見る解像度を上げる訓練です。

ところで、kakikataという便利なアプリケーションがあります。 ledsun.github.io 入力した任意の日本語をペン習字フォーマットで印刷できます。 これを使えば、ペン習字の教材を無限に生成できます。

しかも、自分が気に入った文章で練習できるので、テンションアゲアゲです。

気に入った文章を印刷したもの

そういえばこのアプリケーションはruby.wasmでできているらしいです。 ruby.wasm便利ですね。

Rubyでテトリスを実装する その7

Rubyでテトリスを実装する その6 - @ledsun blog の続きです。

今回は、ライン消しです。 https://github.com/ledsun/tetoris/commit/fb4fd5d11f58d7353d4501124290574206a98dbf がコミットです。

行単位の処理が増えたので、FieldクラスからRowクラスを分離しました。

1行消すのは簡単です。 その後上のブロックを1段ずつ落としてくるところが難しかったです。 cursesを使っているので、デバッグプリントが出来ません。 上手く動かないときに原因を見つけるのが難しいです。 たぶん、デバッガを上手く使うともう少し効率的に進められるのだと思います。

Rubyでテトリスを実装する その6

Rubyでテトリスを実装する その5 - @ledsun blog の続きです。 左右キー下キーでテトリミノを移動出来るようにしました。

https://github.com/ledsun/tetoris/commit/bda62edefc2d13cdb6c93c242fc97b2729526313 が、コミットです。 実装は簡単でした。 落下と同様です。 移動し、衝突していたら戻します。

ここまでくると、とてもゲームっぽくなりました。 残念ながら、一行揃えても消えません。

Rubyでテトリスを実装する その4

Rubyでテトリスを実装する その3 - @ledsun blog の続きです。 今回はテトリミノの種類を増やします。 同時に、テトリミノが着地したときに新しいテトリミノを追加します。

着地しとテトリミノと新しいテトリミノが衝突する判定も必要なので、それもいれます。 実態としては、テトリミノに含まれているブロックを、フィールド(盤面)に移管します。 衝突判定ロジックは既存のままです。

https://github.com/ledsun/tetoris/commit/5e6308ecf6460991b9085c98ba7fd00c5dd9cddd がコミットです。

ゲームオーバーの判定が入っていません。 テトリミノが上まで行っても新しいテトリミノを追加しようとします。

Rubyでテトリスを実装する その3

Rubyでテトリスを動かす その2 - @ledsun blog の続きです。 前回、I字型テトリミノが落ちてくるようになりました。 今回は、上キーの入力でテトリミノを回転します。

変更は https://github.com/ledsun/tetoris/commit/f93310b1254d2b53f8ca27ac0a15a373b95d8d01 です。 Rubyで行列を回転するのは@shape_map.transpose.map(&:reverse)で終わりました。 簡単でびっくりします。

Curses.stdscr.keypad(true)を呼ばないと、上キーの入力を拾えないところで躓きました。

Rubyでテトリスを実装する その2

Rubyでテトリスを動かす その1 - @ledsun blog の続きです。 前回、枠を表示しました。 今回は、I字型テトリミノを表示して落下させます。

https://github.com/ledsun/tetoris/commit/9000667616f82486fe55c20e2c42b9c29e41bf34 の1コミットにまとめてあります。 Tetoriminoの実装と更新処理を追加しました。 Blockクラスがあると描画を共通化できることに気がついたので、追加しました。

wez.termで実行すると、カラーテーマの影響を受けるようです。 赤色を指定していますが、緑色で表示されます。 VisualStudio Codeのターミナルで実行すると、赤で表示されます。

Rubyでテトリスを実装する その1

WSL2上にRuby開発環境を構築してテトリスを作ってみた #Ruby - Qiita を見ながらテトリスを動かそうとしています。 読む分には簡単にできそうと思いました。 実際に、写経して動かそうとしてみたら結構大変でした。 特に、Cursesアプリケーションのデバッグがとても大変です。

一気に全部動かすのは諦めます。 マイルストーンを切ることにしました。 まずは、ゲームの盤面を表示します。

ゲームの盤面を表示したスクリーンショット

ここまでのソースコードです。 GitHub - ledsun/tetoris at draw_wall

次は、一種類のテトリミノが落ちてくるのを目指します。

wez.termのテーマを変えた

変更後のテーマのスクリーンショット

matrixという中2っぽいテーマにしました。

ついでに .wezterm.lua に次の設定を追加してフォントを変更しました。

config.font = wezterm.font '源ノ角ゴシック Code JP'
config.font_size = 14.0

もともとは補完途中の文字が暗い赤で表示されて見にくかったのでテーマを変えようとしました。 途中で、wez.termのテーマではなく、fish-shellのcolor-schemeの設定だと気がつきました。 fish-shellの設定をいじっているうちに直ってしまいました。 しかし、どの設定がきいたのかわかりません・・・。

明確にわかっているのは、以前、GitHub - microsoft/inshellisense: IDE style command line auto completeを試したときの設定が .config/fish/config.fishに残っているのを消しました。 でも、inshellisenseを試す前から、色が暗い赤にだったので影響するとは思えないんですよね・・・。

2023年にruby.wasmにマージできたプルリクエスト

https://github.com/ruby/ruby.wasm/pulls?q=+author%3Aledsun+is%3Amerged+created%3A%3E2023-01-01 で一覧できます。 6件でした。 時系列で見ていきます。


github.com

JavaScriptのオブジェクトを、newメソッドで作れるようにしました。 それまでは、次のようにJavaScript側でnewメソッドを呼ぶ必要がありました。

JS.eval 'return new URLSearchParams(location.search)'

次のようにRubyの中でnewメソッドを呼べるようにしました。

JS.global[:URLSearchParams].new(JS.global[:location][:search])

github.com

JavaScriptのtrue, falseとRubyのtrue, falseは異なるオブジェクトです。 ですので、次の比較は常にfalseになります。

if searchParams.has('phrase') == true
  ...
end

これを簡単に書くために、JavaScriptのtrueを定数にしました。 次のように書けます。

if searchParams.has('phrase') == JS::True
  ...
end

github.com

WebAssembly.complieStreamingというwasmバイナリのダウンロードとコンパイルを同時に実行するAPIを使うようにしました。 40ms速くなりました。 ダウンロードの方が400msぐらい掛かって支配的です。 今のところはそんなに効果はないですが、将来効果が大きくなるといいですね。


github.com

ruby.wasmはビルドの途中でzlibのソースコードをダウンロードします。 zlib 1.3がリリースされたときに、旧バージョンのソースコードがダウンロードできなくなりました。 ruby.wasmのビルドも通らなくなったので、その修正です。


github.com

Kernel#require_relativeをパッチするためのメソッドとして、RequireRemote#loadを追加しました。 自作のRubyスクリプトを読み込むだけなら次のパッチで行けます。

require 'js/require_remote'

module Kernel
  def require_relative(path) = JS::RequireRemote.instance.load(path)
end

これだけだと組み込みgemでつかっているrequirue_relativeが壊れます。 もうちょっと複雑なパッチが必要です。 ruby.wasm/packages/npm-packages/ruby-wasm-wasi/example/require_relative/index.html at 6acf40356079463b9b8d545031e8562a9b3931d2 · ruby/ruby.wasm · GitHub を見てください。

require_remoteをruby.wasmのbundled gemにする計画があります。 そうしたら、このパッチもgemに入れてもいいのかもしれません。 まあ、もう少し先の話なので、一先ずはRequireRemote#loadを組み込んだ自作のアプリケーションを作ります。


github.com

ruby.wasmのnpmパッケージの名称が変わりました。 そのときのテストコードの修正漏れの対応です。

相対URLの解決

URL: URL() コンストラクター - Web API | MDN を使うと、基準になるURLからの相対パスを解決したURLが得られます。 例えば、次のように使います。

// ベース URL:
let baseUrl = "https://developer.mozilla.org";

new URL("ja/docs", baseUrl);
// => 'https://developer.mozilla.org/ja/docs'

このコンストラクターの挙動を試しているときに、次の例を考えました。

new URL('a.rb', 'http://exapmle.com/lib').toString()
// => 'http://exapmle.com/a.rb'

このとき http://exapmle.com/lib/a.rb となって、libディレクトリの中を参照して欲しいのではないでしょうか?

これはJavaScript特有の動作なのでしょうか?

Rubyでも試してみました。 RubyではURLの結合には URI.join を使います。

require 'uri'

URI.join(URI.parse('http://exapmle.com/lib'), 'a.rb')
# => #<URI::HTTP http://exapmle.com/a.rb>

やはり lib が消えます。 統一された動作です。 もしかしてこれはどこかで決まっているのでしょうか?

るりまに以下の説明がありました。

[RFC2396] の Section 5.2 の仕様に従って連結します。

というわけでRFCを見てます。 https://datatracker.ietf.org/doc/html/rfc2396#autoid-33

6) If this step is reached, then we are resolving a relative-path reference. The relative path needs to be merged with the base URI's path. Although there are many ways to do this, we will describe a simple method using a separate string buffer.

 a) All but the last segment of the base URI's path component is
    copied to the buffer.  In other words, any characters after the
    last (right-most) slash character, if any, are excluded.

base URIの最後のスラッシュ以降に何かあれば、その部分はバッファ(解決後のURLを結合するための場所)に入れないそうです。 なるほど、JavaScriptRubyもこの動きをしていそうです。

この記事を書いている途中で気がつきました。 URLは文字列で、ファイルシステムではありません。 http://exapmle.com/lib がファイルかディレクトリかという区別は、文字列から読み取るしかありません。 すると

  • /で終わるのが、ディレクト
  • /の後ろに文字列が続いていたらファイル

みたいな、単純な方法で区別するしかない気がしてきました。 libディレクトリに見えるのは、背景知識があるから人間に判別できているっぽいです。

RubyJavaScript、ファイルとURLの間を行ったり来たりしていると、自分がどこにいるのか、よく見失います。

Playwrightでリダイレクト後のHTTPリクエストをMockできない

ruby.wasmのテストコードを書いていました。

PlaywrightでHTTPリクエストをMockしているスクリーンショット

cdn.jsdriver.netへのリクエストをMockして、レスポンスの内容をローカルファイルに置き換えています。

リダイレクトしたあとはMockできていないスクリーンショット

前述のスクリーンショットと、同じURLに対するリクエストですが、302レスポンスでリダイレクトしたあとはMockできません。

調べてたら、次のコメントを発見しました。

tests for request event and interception with redirects by tjenkinson · Pull Request #3994 · microsoft/playwright · GitHub

The network interception in Playwright is implemented on the Browser -> Network stack boundary. Once the request is in the network stack, it is going to handle the redirects and report them, but not allow intercepting them.

Playwrightが割り込んでいるのは、ブラウザとネットワークスタックの間だそうです。

fetchメソッドはデフォルトで、リダイレクトレスポンスを自動的に追いかけます。 なるほど!この動きはネットワークスタックに含まれていそうです。