ピグ麻雀のアルゴリズム | サイバーエージェント 公式エンジニアブログ

皆様初めまして。 12年度新卒のぱっとしない方のハカマタです。 アメーバピグの制作部署で、サーバサイドエンジニアとして陰ながら頑張っています。

さて、突然の告白ですが最近私は仕事中にアメーバピグで麻雀をしています。 しかし私は今まで一度たりとも叱られたことがありません。 それは、私が叱責に耐えうるメンタルが無いと先輩方に諦められているからでしょうか? 違います、私は麻雀ゲームの制作者メンバーで、デバッグという名目で、ピグで麻雀をしているためです。 本日の記事は、ピグでリリースしたピグ麻雀についてお話します。

ピグ麻雀


ピグ麻雀はエンジニア・ディベロッパー・デザイナー各1名で制作したもので、 私はエンジニアとして参加しました。 サイバーエージェントのクリエイターブログに過去の記事として、デザイナーの鈴木さんの記事『ピグ麻雀におけるデザインのポイント』と、ディベロッパーの鈴木さんの記事『ピグ麻雀がリリースされるまで』がありますが、私はサーバサイドエンジニアとしてピグ麻雀のアルゴリズムについて簡単に説明します。 また、記事の最後に麻雀の開発にあたっての個人的な感想を述べます。


※この記事は皆様が麻雀のルールを認識していることを前提に書いておりますので、 もし麻雀のルールがわからない方は途中を飛ばして最後の私の開発を終えての感想のみお読みいただければ幸いです。

麻雀の役判定アルゴリズム

ピグ麻雀の役判定アルゴリズムについてお話しする前に、麻雀の役判定の流れを説明しておきます。 麻雀の役判定は、和了型手牌(※ 一般的な名称ではなく、独自につけた名称です)を用いて行います。 和了型手牌は、七対子と国士無双を除いて、雀頭と面子4つから構成されます。 この和了型手牌を手牌から網羅的にリストアップして、 リストアップされた各々の和了型手牌で役判定を行い、最良の役を持った和了型手牌から和了役を決定します。 このアルゴリズムを簡単にまとめると以下のようになります。 


これから上記の処理の詳細を 1.和了型手牌のリストアップ 2.役判定 に分けて説明していきます。 

1.和了手牌のリストアップ

ピグ麻雀では麻雀牌をenumで定義しており、各牌のコードを連番でつけています。

/**
 * 麻雀牌
 */
public enum MahjongTile implements Transferable {
	/** 萬子1 */
	M1(0, MahjongTileType.MANZU, 1),
	/** 萬子2 */
	M2(1, MahjongTileType.MANZU, 2),
	/** 萬子3 */
	M3(2, MahjongTileType.MANZU, 3),
	/** 萬子4 */
	M4(3, MahjongTileType.MANZU, 4),
	/** 萬子5 */
	M5(4, MahjongTileType.MANZU, 5),
	/** 萬子6 */
	M6(5, MahjongTileType.MANZU, 6),

    ・・・・

	/** 白 */
	HAK(31, MahjongTileType.SANGEN, 0),
	/** 發 */
	HAT(32, MahjongTileType.SANGEN, 0),
	/** 中 */
	CHN(33, MahjongTileType.SANGEN, 0), ;

	/** 牌コード */
	private int code;
	/** 牌種別 */
	MahjongTileType type;
	/** 牌数値(役牌は0) */
	private int number;

	private MahjongTile(int code, MahjongTileType type, int number) {
		this.code = code;
		this.type = type;
		this.number = number;
	}


プレイヤーは各牌の所有枚数を配列で保持しています。


private int[] tiles = { 1,0,0,0,0,0,0,0,1, // 萬子
1,0,0,0,0,0,0,0,1, // 索子
1,0,0,0,0,0,0,0,1, // 筒子
1,1,1,1, // 風牌
2,1,1}; // 三元牌  

和了型手牌はこの配列を用いて判定します。判定のフローを以下に示します。


この処理を行うことで、手牌を網羅的に取得することができます。刻子優先取得、順子優先取得というフェーズがある理由は、刻子や順子のいずれかのみを優先して取得してしまうと問題が生じるためです。 例えば 
222 333 444 という手牌があったとして、 これを順子優先で取得すると234 234 234という手牌になります。 例えば残りの手牌が②刻子と五雀頭でフーロが無い場合では、 役満の四暗刻になりますが、順子を優先して取得された場合には四暗刻の判定ができません。


 2.役判定 

和了型手牌の結果をもとに役判定を行います。 先ほどの和了型手牌の雀頭・面子以外に、 プレイヤーが副露しているか、プレイヤーの最後の和了牌等の情報をもとに役判定を行います。 ピグ麻雀の役判定はファサードパターンを用いて実装されています。 和了型手牌一覧を窓口である和了判定処理に渡すことで、それぞれの和了型手牌の和了役を全て取得するようにしています。 インターフェースで役判定を行うメソッドを定義して、その実装クラスでそれぞれの役判定を行っているため、役を追加する場合は新たに役判定のクラスを追加するだけです。


今回は役判定のうち、麻雀の基本役であるピンフの役判定について説明します。 ピンフであるか判定する場合には以下の手順で行います、副露してないか 雀頭が役牌であるか、雀頭が場風・自風であるか、和了牌が両面待ちであったか、面子の構成要素が順子のみであるかを調べ、これらの条件をクリアしたものがピンフになります。

(訂正) 雀頭が役牌であるか→雀頭が役牌でないか


最後に


ピグ麻雀の開発期間は3ヶ月でした。 私を含め、新卒3人のチーム全員が麻雀未経験者ということもあり、ルールを把握することからプロジェクトを始めなければなりませんでした。プロジェクト開始後、麻雀入門書を手にとってルールの難しさに心が折れたことを覚えています。弱音ばかりはいていても仕方ないので入門書片手に、会議室で麻雀をしたり、雀荘に行ったりして麻雀について学んでいました。麻雀好きの先輩方からすれば麻雀の実装は相当楽そうに映っていたかもしれませんが、実際は…(以下省略)。

また、この3ヶ月のうち、最も時間を割いたのはテストでした。 動作確認では確認しづらいテストはユニットテストで動作を担保しました。実際に200以上のテストケースを作成しました。加えて、クライアントサイドとの通信テストも行う必要があり、配牌チートコマンドを作ってあらゆる処理をテストしました。 例えば、国士無双聴牌の暗カンロンなどが行えるかといったものです。

念入りにテストしたつもりでも、リリース後にバグが発見されました。 バグの原因の多くは、ルールの認識不足によるものでした。当たり前のことですが、 麻雀をはじめとして、一般的に普及しているゲームのルールを把握することは、システムの仕様を把握することと同義であり、ルールを把握していない箇所はバグとなります。 また、ゲームにはローカルルールが存在し、ユーザ間でルールの認識が異なり、開発者とユーザとの間でルールの認識のギャップが発生してしまうため、ユーザはバグであると感じます。
このため、一般的に普及したゲームの開発を行う際は、ルールを完璧に把握し、提供するゲームのルールを、改めてユーザに明示する必要性を感じました。

今後もピグ麻雀の改善をしていきますので、
引き続きピグ麻雀を宜しくお願いします。