LoginSignup
9
1

More than 3 years have passed since last update.

パ○ドラの盤面欠損率を計算するWebアプリを作った話(nextjs + TypeScript + Mobx)

Posted at

アドベントカレンダーの一発目がこんなネタ記事でとても申し訳ないです。

去年のアドベントカレンダーでパズ○ラの盤面欠損率を計算するスクリプトを高速化する話を書きました。

今回は、そのときに使ったロジックを使って、「パズドラの盤面欠損率を計算するWebアプリ」を作ってみた話です。

DEMO

以下のURLから使えます。PCスマホ両対応です
https://youthful-cray-84efcb.netlify.com/pazdora-cal

また、ソースコードは例によってGitHubにあげています。

使い方

ここからはこのアプリの使い方を説明します。パズドラに興味のない人、よく知らない人は 内部の話まで飛ばすと良いかなと思います。

Webアプリ上で盤面の条件を定義するカードを作成し、その条件を満たす盤面がどの程度の確率で存在するかを計算します。
条件カードには以下の3種類が存在します。

  • ドロップ条件
  • コンボ条件
  • 多色条件

まずドロップ条件の使い方から説明します。

ドロップ条件

画面の「+ドロップ」ボタンを押すと、ドロップ条件を追加できます。ドロップ条件の中で設定できる値は以下です。
* ドロップの色: 条件に合致する色を選択します
* ドロップの個数/条件: 選択した色のドロップが何個(以上/以下)あればよいかを選択します

例えば、以下のリーダースキルを見てみてください

ファイル名

火と水の同時攻撃で攻撃力4倍と書かれていますね。
この条件を満たす盤面の出現率を計算すると以下のようになります。

image.png

こんな感じで複数カードを設定すると、それぞれの条件のアンドを取って計算します。

指定2色が存在する確率は83%ぐらいですね
5~6回に一回は欠損する計算です。意外と欠損するかも?

コンボ条件

お次はコンボ条件です。「+コンボ」のボタンを押すと追加できます。これは比較的わかりやすいんじゃないでしょうか?以下の項目を設定できます

  • 消せるドロップ数: 一コンボするのに必要なドロップ数を設定します。普通は3個です
  • コンボ数/条件: 条件を満たすコンボ数を設定します
  • 繋げるドロップがある場合は繋げるドロップを一種類だけ指定できます

image.png

56盤面で7コンボある確率を計算してみました。これをみると、ほとんどの場合で7コンボはありますね。
つまり7コンボできないのは甘えです。
7コンボ強化の覚醒スキルが如何に強いかがわかります。

繋げるドロップ条件についても見ていきましょう。
例えば、「回復を5個繋いで、なおかつ7コンボ以上ある確率」を計算したいとします。
一見、先程紹介したドロップ条件とコンボ条件の組み合わせで表現できるような気がしますが、実際には、コンボ条件は他のカードの条件とは独立して計算されてしまうため、例えば回復が盤面に6個しかない場合、本来であれば5個繋げるので1コンボ分とカウントしたいのですが、2分割して2コンボ分とカウントしてしまいます。

この問題を回避するために、コンボ条件の中で繋げるドロップを指定できるようにしています。

例えば追い打ち7コンボ以上ある確率は以下のようになります。

image.png

比較のため、ドロップ条件と組み合わせた場合の確率も計算してみます。
比べてみると、前者のほうが少し確率が下がっている事がわかりますね。

image.png

多色条件

最後は多色条件です。これが一番ややこしいです。が、設定できる幅は一番広くて、1つ目のドロップ条件の上位互換みたいな設定ができます。
順番に説明していきましょう

  • 選択する色: 多色条件に含まれる色を複数選択できます。
  • ドロップの種類: 上の選択した色の中から、N種類以上条件を満たしているかを設定します
  • ドロップの個数: これはドロップ条件と同じです。

言葉で説明してもわからないと思うので例を出します。以下のようなリーダースキルを考えてみましょう。

image.png

このリーダースキルを満たす盤面の出現率は以下になります。

image.png

7コンボ以上という条件はコンボ条件のときと同じですね。
注目すべきは一枚目のカードです。「4色以上同時攻撃」というのは正確には
「「火水木光闇」の5種類のドロップのうち、4種類以上の色が3個以上存在する」
という条件になります。これを多色条件のカードで表すと画像のようになります。

ちなみに、5色中4色存在する確率はこんな感じでかなり高いです。追い打ちとか無効貫通とか考えず、欠損率だけ考えるならかなり優秀なリーダースキルですね

もう一つ例を出しましょう。以下のようなリーダースキルを考えます。

image.png

かなりややこしそうですね。しかしこれも計算できます。

image.png

多色条件で2色選択して、ドロップの種類を2種類以上に設定すると、指定2色を計算したときと同じ条件になります。
更に、残りの4色を選択して、1種類以上が5個以上ある条件を追加することで、「火光以外を5個以上繋げて消すと~」の条件を表すことができます。

計算できない条件

さて、ここまで紹介した機能で殆どのLSの条件は計算できます。
しかし、一部正しく計算できないLSのもあります。例えば

  • 十字消しやL字消しなど、消すDropの形が定義されているLS(とくに十字消しは難しい)
  • 複雑な条件にコンボ条件が追加されたLS(コンボ数は他の消し方条件によって計算方法が変わるので正確に計算することが難しい)

具体的な例で言えば、「火か水を5個以上繋げて、7コンボ以上」とかは正確に計算できません。
ここらへんは今後の課題ですね

内部の話

Nextjs1 + TypeScript + Mobx2で作りました。
Reactでまともに開発するのはこれが初めてでしたが、すごく良かったです。

TypeScriptとの相性の良さ

まず、TypeScriptとの相性が良いです。Vueのテンプレートと違って、Reactではtsx部分はすべて型が付きます。
そして今回使用したNext.jsのver9では、特に何も設定せずともts / tsxファイルを解釈してくれる様になったので、導入コストも低いです。
フレームワークの機能に関しても殆どの部分に型がついているので、コーディングしていて型がなくて困るようなこともありませんでした。
また、TypeScript自体も良くできていて、今回は特にkeyofキーワードが活躍しました。これとジェネリックを組み合わせることで、引数に指定したオブジェクトのメンバのキーのみを引数として受け付けるような関数を書くことができます。

例を示しましょう。
以下のようなオブジェクトのプロパティを取得する関数を考えます。

/**
 * 引数に指定したオブジェクトのキーの値を所得する
 */
function propGet<T, K extends keyof T>(obj: T, key: K): T[K]{
    return obj[key]
}

これを以下のようなuserオブジェクトに適用すると


const user = {
  name: 'testuser',
  id: 1,
  obj: {
    obj2: {
      aaa: 'aaaa'
    },
    obj3:{
      ccc: 'cccc'
    },
    bbb: 'bbbb',
  }
}

このような感じで、第一引数の型(今回はuserオブジェクトの型)を使って、第2引数の型を単なるstringではなく、userオブジェクトのキーのみをstringリテラルで推論してくれます。

propGet(user,'name') // => 返り値の型がstringになる
propGet(user,'id')   // => 返り値の型がnumberになる
propGet(user,'nana') // => nanaというプロパティは存在しないのでコンパイルエラー

今回は、欠損率の計算をWebWorkerを使ってメインスレッドとは別の場所で行った関係上、条件カードの生成をFactory経由で行う必要がありました。
keyofキーワードを使っておくと、factory関数の引数に指定する値を、各条件カードクラスのメンバから推論させることができ、
仮に各カードクラスのメンバをあとから変更したとしても、factory側の引数の型を変更する必要がなくなり、常に整合性の取れた状態にすることができます。
さすがTypeScriptとついているだけありますね。
ただあまりやりすぎると、型パズル状態になってあとから読んだときに全然読めなくなってしまうので、そこは注意する必要はありそうです。

また、interface や abstract class のような機能があるので、JavaScriptよりもよりオブジェクト指向的なコードが書きやすくなっています。
特にJavaとかではおなじみの interface は似たようなクラスを複数実装していく際に適切に制限をかけていくことができるので、
特にチーム開発するときなんかでは非常に便利ではないかなと思います。

React Hooks

React Hooksは本当によくできてて感動しました。useEffect() なんかは、あのめんどくさいViewのライフサイクル周りの処理を一つのAPIだけで完結させてしまっていて、「設計した人頭良すぎでは?」ってなってます。
よくVueはeasyでReactはsimpleみたいな話を聞きますが、このReact Hooksを見てると、できるだけsimpleかつJSの標準機能だけで実現しようとしている感じがすごく伝わってきてきます。

ここらへんのReact Hooksの設計思想の話は公式サイト(日本語)にとても詳しく書かれているので興味のある人は読んで見るといいと思います。
あと、Custom Hookは無限に遊べます。
こんな感じで色々な機能をもったHookを自作できるので、ぜひ皆さんも作って遊んでみてください

MobX

MobXは、React用のStoreライブラリの一つで、Reduxに比べて、非常にシンプルな設計になっているのが特徴です。Reduxでは、一つの変数(状態)を変更するにも、actionとreducerを経由して変更を行わなければいけませんでした。これは大規模なアプリやチーム開発では、適切な秩序をもたらすことができるので良いのですが、個人開発のような小規模なアプリでは、正直大げさだなと感じる事が多いです。
MobXではそこらへんの複雑なストア周りの機能がまるっと削られていて、「結局君等がほしいのって、変更検知できるObservableなObjectなんでしょ?」と言わんばかりの簡単な設計になっています。これにより学習コストが低いのはもちろん、Storeが単純なJSのクラスなので、VuexやReduxと違い、なにも考えなくても型が付きます。これはTypeScriptを導入する上で非常に大きなメリットになります。

ただし、Reduxのように設計思想を押し付けてくるわけじゃないので、チーム開発で使うならちゃんとルールを決めないと、あっという間にカオスになっていくだろうなという感じはあります。ただそのぶん、ユースケースに合わせてとても柔軟にStoreを設計できるので、設計力のある人が使うとこれ以上ないツールになるんだろうなと思います。

例えば以下は最小設計のStoreです。actionもreducerもなく、あるのは、「変更検知可能なオブジェクト」だけです。@observable をつけたメンバが「変更検知可能なメンバ」になり、このメンバを、@actionをつけたメソッド経由で変更すれば、それだけで、このメンバを参照しているコンポーネントの再レンダリングが走ってくれるようになります。

import { observable } from "mobx"

class CounterStore {

  @observable count = 0;

  @action
  increment() {
    this.count += 1;
  }

  @action
  decrement() {
    this.count -= 1;
  }
}

使うときは、このStoreのインスタンスを以下のように作っておいて

store.ts
import { CounterStore } from "./CounterStore";
import { configure } from "mobx";
import React, { useContext } from "react";

configure({ enforceActions: "always" });

export class Store {
  counterStore = new CounterStore(this);
}

export const store = new Store(); // Storeのインスタンスを作成
export const storeContext = React.createContext(store); // storeのcontextを作成
export const useStore = () => useContext(storeContext); // コンポーネントでstoreを簡単に読み込むためのカスタムフックを作成

context経由で下の階層にstoreを渡します。

App.tsx
import React from "react";
import "./App.css";
import { Counter } from "./components/Counter";
import { storeContext, store } from "./store/store";

const App: React.FC = () => {
  return (
    <storeContext.Provider value={store}>
      <div className="App">
        <Counter></Counter>
      </div>
    </storeContext.Provider>
  );
};

export default App;

で、コンポーネントの中では以下のような感じでストアを使えます。

counter.tsx
export const Counter = observer(() =>{
  const { counterStore } = useStore();
  return (
    <div>
      カウンター: {counterStore.count}
      <p>
        <button onClick={() => counterStore.increment()}>
          カウントを増やす
        </button>
      </p>
    </div>
  )
})

以下にサンプルプロジェクトを作ったので、興味のある方は見てみてください
https://github.com/kuwabataK/my-mobx-test-prj

また、mobxをReduxやVuexのようなFLUXモデルで扱うことができるmobx-state-treeというライブラリもあるので、Reduxで型付けるのしんどいという方はこちらを試してみるのも良いかもしれません。

シンプルに使いたい人も、ガッツリ使いたい人も満足できるような柔軟性の高さがとても魅力的なライブラリです。

ロジックの話

ロジックは前回と同じで、50万個の盤面を生成し、モンテカルロ法で欠損率を計算しています。
計算はすべてフロントエンドで行っているので、サーバー側はNetlify3でhtmlとjs(とCSS)を配信しているだけです。

また、DOMの更新などを行わない単純な計算処理なので、盤面の生成から計算までをすべてWeb Worker(Service Workerじゃないよ)を使ってメインスレッドとは別のスレッドで実行し、メインスレッドをブロッキングしないようにしています。
これによって、計算中に画面がフリーズするようなことが起こらなくなっています。一昔前のスマホなどの貧弱な環境では、計算の終了までに2秒以上かかる場合もあるので、その間画面がフリーズしないというのはとても大事です。

devツールを使うと、ページを読み込んだ際に、4つのworkerスレッドが作成されているのがわかるかと思います。

tempsnip.png

4並列で計算を行っているので、環境によってはメインスレッドだけで処理するよりも高速に動作します。

ただし、JSのWebWorkerは、以下のような問題点があってあまり流行っていません。

  • スレッド間の通信時にデータのシリアライズ / デシリアライズが入るので、スレッド間で頻繁にデータをやり取りしたり、巨大なオブジェクトを送ったりすると非常に遅くなってしまう(一応バイナリデータであれば直接参照を渡すこともできるようです)
  • WorkerスレッドはDOMに直接アクセスできない
  • Workerスレッド自体の作成コストも馬鹿にならない

一応最近はworkerスレッドでcanvasを扱えるようになったらしいので、webGL周りをやる人なら使う機会があるのかもしれません。ちなみにScrapboxさんは、フロントエンドでの検索機能周りでWebWorker使ってるらしいです。
- WebWorkerをproductionで使ってる話

今後、こういうWeb Workerを使いたくなる負荷の高い計算処理なんかは、WebAssemblyが担っていくのかもしれません。JSと違って、コンパイル済みのコードをブラウザ上で実行できるようになるので、JSを並列化するよりはパフォーマンスも高くなることが期待できます(とはいえJavaScriptもスクリプト言語としては異常な速さですが)。特に最近は業界のRust熱の高まりもあるので、来年はWebAssemblyを使ったプロダクトも増えていくかもしれませんね。

パフォーマンス

Light houseのスコアも測ってみました。

image.png

特に何もしていないですが、全部90点以上になってくれました。Next.jsとNetlifyの優秀さがわかりますね。SEOはなにも考えていないので良いとして、performanceの減点は、workerを生成する処理(正確にはworker.jsをロードする処理)に時間がかかってるせいみたいです。
workerのpreloadができればよいのですが、Webpack環境でこれを行ううまい方法が見つからなかったので諦めています。
まあ初期ロードは遅くなりますが、実行時の体感性能は上がっているので良しとしましょう。
残念ながらLightHouseでは、計算実行時のパフォーマンスを測定してはくれないですが・・・

image.png

そもそもコードの規模が小さいし、外部API叩いてたり、画像読み込んだりしてるわけじゃないのでパフォーマンスが悪くなりようはないんですが、nextjs + netlifyなら、変なことしない限りは高速なページを作成できるということは言えるんじゃないでしょうか

おわりに

以上、こんなネタ記事に付き合っていただきありがとうございました。
2年連続で、パズ○ラをやったことのある人にしかわからないマイナーネタをぶっこんでしまって申し訳なさしかないです
今回で普段使う機会がないけれど使いたいなと思っていた技術(ReactとかMobxとか)はだいたい使えたので、割と満足しました。(あとはGraphQLとかも触ってみたいですが、バックエンド作るのめんどくさいなと思ってしまってやってないです・・・)

私個人はこの一年、業務でもプライベートでもほとんどWebフロントエンドしか触りませんでした。変化の激しいと言われるWebフロントエンドですが、開発標準が変わるような大きな出来事は、観測範囲だとReact Hooksが正式リリースされたぐらい(個人の感想です)なので、だいぶ落ち着いてきたかなと言う印象です。

ただ、来年はVueの3.0も出るし(TypeScriptベースになるらしい!)、JSの機能追加も続々追加されている(Optional Chainingとか)ので、まだまだ色々ありそうです。今後もこの変化を楽しんでやっていければいいなと思います。

では皆さん、来年も良い年になりますよう。m(_ _)m


  1. NextJSはReactベースのフロントエンドフレームワークで、Reactで作られたアプリをSSR(サーバーサイドレンダリング)することができます。Vueに対するNuxtjsのような立ち位置のフレームワークだと思って貰えれば良いかなと思います。 

  2. MobXはReact用のストアライブラリの一つで、フロントエンドの状態管理を行うことができます。同じようなものでは、ReactだとReduxが有名ですし、VueではVuexというライブラリを公式が提供しています 

  3. Netlifyは静的なサイトをホスティングしてくれるWebサービスです。GitHubと連携すると、masterブランチにpushするだけで最新版のサイトをデプロイすることができるようになり、かなり便利です。グローバルにCDNが整備されていて、日本からのアクセスも早いです。 

9
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1