ラストのまほう　第23話『スコアランキングを実装したい』

# リーダーボードが欲しいよ

ゲームは個人で遊ぶのも面白いのですが、プレイヤーどうしの交流要素があると盛り上がったりしますよね。直接の対戦でなくとも、スコアや達成度を競ったりする仕組みも考えられます。

それで、GitHubのIssueを漁っていたところ、リーダーボード（プレイヤーのスコアランキング）のAPIが欲しいよ、というIssueがありました。

https://github.com/aduros/wasm4/issues/102

WASM-4の作者のブルーノ兄貴は、「個人的には、興味があるのはレトロなコンピューターゲームなんだ。ソーシャルな機能は別に反対じゃないけど、もっといろんなゲームが集まってからかな……」みたいな感じでした。まあそういうことなので公式APIは当分実装されないでしょうが、いろいろ**抜け道**はありそうです。

# WASM-4のセーブデータを外部から読み取る

WASM-4にはバンドルしたときのHTMLをカスタムする方法が用意されています。`w4 bundle`コマンドに`--html-template`オプションでテンプレートファイルを渡せばいいようです。

これを利用して、HTML側にネットワークアクセスのコードを仕込みます。ゲームのセーブデータはローカルストレージに書き込まれますので、これを読み取れば現在のハイスコアなどは外部から読み取ることができます。

HTML自体にもSNSログインの機能を付けて、プレイヤーを認証できるようにしたいと思います。これでツイッターのあの人がどこまでプレイしたかとかわかるようになります。

ローカルストレージは文字列しか格納できませんが、WASM-4ランタイムのコードを見ると、[z85というフォーマットでバイナリ列を文字列に変換](https://github.com/aduros/wasm4/blob/6c5cd7f27f842218fa519d5fd55de15d1314a3c0/runtimes/web/src/runtime.ts#L236)してからローカルストレージに格納しているようです。Z85というのがどのような形式なのかはよく知らないのですが、WASM-4のランタイムから[デコーダーのコード](https://github.com/aduros/wasm4/blob/6c5cd7f27f842218fa519d5fd55de15d1314a3c0/runtimes/web/src/z85.ts)をコピーしてきて、デコードしたら`DataView`でデータを読み取ります。

```html
<script async>

    // function decode (string: string, dest: number[] | Uint8Array | Uint8ClampedArray): number
    function decode (string, dest) {

      const DECODER = [
        0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00,
        0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45,
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
        0x08, 0x09, 0x40, 0x00, 0x49, 0x42, 0x4A, 0x47,
        0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
        0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
        0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
        0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00,
        0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
        0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
        0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
        0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00
      ];

      let byte_nbr = 0,
          char_nbr = 0,
          value = 0;
      const string_len = string.length,
          dest_len = dest.length;

      if ((string.length % 5) == 0) {
          while (char_nbr < string_len) {
              const idx = string.charCodeAt(char_nbr++) - 32;
              if ((idx < 0) || (idx >= DECODER.length)) {
                  return byte_nbr;
              }
              value = (value * 85) + DECODER[idx];
              if ((char_nbr % 5) == 0) {
                  let divisor = 256 * 256 * 256;
                  while (divisor >= 1) {
                      if (byte_nbr >= dest_len) {
                          return byte_nbr;
                      }
                      dest[byte_nbr++] = (value / divisor) % 256;
                      divisor /= 256;
                  }
                  value = 0;
              }
          }
      }

      return byte_nbr;
    }
  
    setInterval(() => {
      const str = localStorage.getItem("Tower Climber-disk");
      const array = new Uint8Array(1000);
      const byte_nbr = decode(str, array)

      const view = new DataView(array.buffer)
      const version = view.getUint8(0);
      const x = view.getFloat32(1, true);
      const y = view.getFloat32(5, true);      
      console.log(version, x, y);
    }, 1000)

  </script>
```

ちなみに、エンディアンに注意です。WASMではリトルエンディアンなのですが、JavaScriptの`DataView`ではビッグエンディアンがデフォルトです。クソっ！

なお、ローカルストレージに書き込まれるときのキーは、デフォルトでは`ゲーム名-disk`になるようです。これも、`--html-disk-prefix`というオプションを指定することで

これでセーブデータを外部から読み取ることができました。あとはこれを適当なデータベースに書き込めばOKです。


# 参考文献

- https://wasm4.org/docs/guides/distribution
- https://wasm4.org/docs/guides/saving-data


# 次回予告

次回はマルチプレイヤー機能を試してみます。こんなに簡単にマルチプレイヤーゲームになるなんて驚きです。

https://qiita.com/hiruberuto/items/c67fc0cd2347cabcf136