電脳麻将のプログラム構成(1) 〜 JavaScript編

電脳麻将のプログラム構成(0) ~ 全体編 - koba::blog に続き、今回はJavaScriptのプログラム構成。

電脳麻将プログラム は全てJavaScriptで記述しています。プログラムは以下に分類できます。

  1. 共通モジュール
  2. 画面系モジュール
  3. ページ毎モジュール
  4. 開発用モジュール
共通モジュール
ブラウザに依存しないコードだけで構成される。手牌の操作、シャンテン数計算、和了点計算、局進行、麻雀AI など。
画面系モジュール
手牌表示、麻雀卓表示、UIなどブラウザに依存したコード。
ページ毎モジュール
各ページのスタートアップルーチン。ページに閉じたロジックも含む。
開発用モジュール
試験用の局進行ドライバ。牌譜解析・統計用ツール、過去のAI(比較対戦用)など開発のためにだけ使用するモジュール。

ソースファイル一覧

src/js/ 配下の ファイル構成 は以下の通り。

ファイル名 クラス名 説明
entry.js Majiang 共通+画面系モジュールのエントリーポイント
game.js トップページ用ルーチン
autoplay.js 自動対戦ページ用ルーチン
paipu.js 牌譜ページ用ルーチン
paili.js 牌理ページ用ルーチン
hule.js 和了点計算ページ用ルーチン
dapai.js 何切る解答機ページ用ルーチン
paiga.js 牌画入力ページ用ルーチン
majiang.js Majiang 共通モジュールのエントリーポイント
majiang/shoupai.js Majiang.Shoupai 手牌を表現するクラス
majiang/shan.js Majiang.Shan 牌山を表現するクラス
majiang/he.js Majiang.He 河(捨て牌)を表現するクラス
majiang/game.js Majiang.Game 局進行を実現するクラス
majiang/player.js Majiang.Player 麻雀AIを実装したクラス
majiang/suanpai.js Majiang.Suanpai 読みのための牌の数え上げを実装したクラス
majiang/util.js Majiang.Util (下記2ルーチンのエントリポイント)
majiang/util/xiangting.js Majiang.Util シャンテン数計算ルーチン
majiang/util/hule.js Majiang.Util 和了点計算ルーチン
majiang/view.js Majiang.View 画面系モジュールのエントリーポイント
majiang/view/pai.js Majiang.View 牌の画像を管理するルーチン
majiang/view/audio.js Majiang.View 音声を管理するルーチン
majiang/view/shoupai.js Majiang.View.ShouPai 手牌を表示するクラス
majiang/view/shan.js Majiang.View.Shan 牌山を表示するクラス(ドラ表示のみ)
majiang/view/he.js Majiang.View.He 河(捨て牌)を表示するクラス
majiang/view/huledialog.jsMajiang.View.HuleDialog和了・流局時のダイアログを表示するクラス
majiang/view/game.js Majiang.View.Game 麻雀卓全体を表示するクラス
majiang/view/gamectl.js Majiang.View.GameCtl ゲームに関するUIを実装するクラス
majiang/view/player.js Majiang.View.Player 対戦時のUIを実装するクラス
majiang/view/paipufile.js Majiang.View.PaipuFile 牌譜一覧の表示とそのUIを実装するクラス
majiang/view/paipu.js Majiang.View.Paipu 牌譜再生のUIを実装するクラス
majiang/view/stat.js Majiang.View.Paipu 牌譜集計・表示ルーチン
majiang/view/analyzer.js Majiang.View.Analyzer 検討モードを表示するクラス
majiang/dev/game.js Majiang.Dev.Game 開発用局進行ドライバ
majiang/dev/player*.js Majiang.Player 過去のAIのバックアップ(比較対戦用)
majiang/dev/suanpai*.js Majiang SuanPai 過去のAIのバックアップ(比較対戦用)
majiang/dev/analogbase.js AnaLogBase 牌譜解析・統計用ツール
majiang/dev/analog.js AnaLog 牌譜解析・統計用ツール
共通モジュール 画面系モジュール ページ毎モジュール 開発用モジュール

プログラム・スタイルとビルド方法

ES6(ES2015) で導入された class 構文を使ってモジュール化しています。このため IE11 などの ES6 の構文を理解できない古いブラウザではそのままでは動作しません。ver.1.3 までは Babel をつかってES5に変換していましたが、次の版の ver.1.4 から古いブラウザは切り捨て、Babel の使用をやめるよう修正中です。*1

モジュールは CJS(CommonJS)require() で参照しています*2。ブラウザでも動作するよう webpack で変換し、複数のソースプログラムを1つのバンドルにまとめています。ただし、webpack で一般的な「ページごとに1つのバンドル」という方法ではなく、各ページが

  1. 全ページ共通のバンドル (上記の共通モジュール+画面系モジュール)
  2. ページ毎のバンドル (上記のページ毎モジュール、1ファイル1バンドル)

の2つのバンドルを読み込むようにしています。これを可能にするのが大域変数 Majiang です。共通+画面系モジュールのエントリーポイントの entry.js の中で、

global.Majiang = require('./majiang');            // 共通モジュール
global.Majiang.View = require('./majiang/view');  // 画面系モジュール

とし、全てのクラスを大域変数 Majiang にマップしています*3。これによりページ毎モジュールからは上記の表に示した Majiang.Shoupai などのクラス名で参照できるようになる訳です。

大域変数を使う方法に抵抗がある方もいると思いますが、ページ毎に1バンドルとする一般的な方法では、共通モジュールのコードがページ毎に異なる名前で読み込まれるという無駄な通信が発生します。電脳麻将の共通モジュールは 350KB もあり無視できないサイズです。また、全てのクラスにユニークな名前があることはプログラムの理解にも役立つと考え、この方法を採用しました。

ビルド後のバンドルは www/js/ 配下に配置されます。

ファイル名 説明
www/js/majiang.js 全ページ共通のバンドル
www/js/game.js トップページ用のバンドル
www/js/autoplay.js自動対戦ページ用のバンドル
www/js/paipu.js 牌譜ページ用のバンドル
www/js/paili.js 牌理ページ用のバンドル
www/js/hule.js 和了点計算ページ用のバンドル
www/js/dapai.js 何切る解答機ページ用のバンドル
www/js/paiga.js 牌画入力ページ用のバンドル

webpack.config.js は以下となります。

module.exports = {
    entry:  {
        majiang:   './src/js/entry.js',
        game:      './src/js/game.js',
        autoplay:  './src/js/autoplay.js',
        paipu:     './src/js/paipu.js',
        paili:     './src/js/paili.js',
        hule:      './src/js/hule.js',
        dapai:     './src/js/dapai.js',
        paiga:     './src/js/paiga.js',
    },
    output: {
        path:     __dirname + '/www/js/',
        filename: '[name].js'
    },
};

テスト方法

ブラウザに依存しない共通モジュールのみ自動試験しています。テストフレームワークには Mochaカバレッジレポートには Istanbul を使用しています。

局進行を実現するクラス Majiang.Game のテストに一工夫必要だったため、開発用局進行ドライバ Majiang.Dev.Game を用意しています。開発用局進行ドライバには牌譜から牌山と摸打を再現して局進行をしたり、一切の非同期処理をせず*4に局進行するなどの機能があります。ver.0.9 まではブラウザ上でしか動作しなかったため、思考アルゴリズムの評価のために麻雀AI同士を対戦させる場合でも非同期で処理が動いていました(そうでないとブラウザが無限ループと判定してしまうため)。ver.1.0 以降はブラウザなしでの動作が可能になったため、このような無駄がなくなり、処理速度が向上しました。また牌譜から局進行を再現することでバグの検証や思考アルゴリズムの検証がやりやすくなっています。

画面系モジュールを動作させるためにはブラウザが必要なため、自動試験は行なっていません。自動試験は可能なようなのですが、表示の乱れなどは目視でしか確認できないため、自動化のメリットは少ないと考えています。

*1:ChromeFirefox は古いOSでも ES6 対応の最新版が入手可能なため、IE11 や古い Safari への対応は不要と判断しました

*2:ESM(ES Modules) への移行はまだ時期尚早と考えています

*3:ついでに jQuery$ にマップしています

*4:本来の Majiang.Game はイベント駆動で動作するため、イベント待ちの非同期処理があります