麻雀の点数計算ツール(JavaScript製)の解説
当サイトでは初心者向けの麻雀の点数計算ツールを提供しています。今回はその麻雀の点数計算ツールの仕組みを解説します。
目次
点数計算の方法について
世の中には様々な麻雀の点数計算ツールがあります。
- カメラで撮影してもらう
- 牌を選択してもらう
- 質問に回答してもらう
当サイトで提供しているのは、一番下の質問に回答してもらう方法です。この方法ならHTMLとCSS、JavaScript(jQuery)で制作することができます。
制作に必要な知識について
麻雀の点数計算ツールを制作する上で必要な知識は、
- HTML
- CSS
- JavaScript(jQuery)
- 麻雀の役、符数、翻数
- 麻雀の点数早見表
です。点数計算の式を知らなくても、点数早見表を知っていれば大丈夫です。ただし参考にする点数早見表に誤りがないことが前提です。
それではここからは、麻雀の点数計算を行うためのJavaScriptのコードを解説しながら紹介します。
クラスを定義する
使用する関数が多いため、クラスを定義してメソッドを作成します。
class Mahjong {
}
メインのメソッドと質問の定数を定義する
クラスを定義したら、点数計算のためのメインのメソッドと質問の定数(buttonタグのname属性の値と同じ)を定義します。
/**
* 点数計算
*/
pointCalc() {
const ITEMS = {
'cha': '', // 何家ですか?
'bakaze': '', // 何場ですか?
'honba': '', // 何本場ですか?
'daburi': '', // 一巡目でリーチ(ダブルリーチ)していますか?
'richi': '', // 2巡目以降でリーチしていますか?
'ippatsu': '', // リーチ後の最初のツモでツモアガリ、またはリーチ後に自分の番が来るまでにロンアガリしましたか?
'agari': '', // ツモアガリとロンアガリどちらですか?
'shuntsu': '', // 連続した3つの数牌(順子)はいくつありますか?
'janto': '', // 雀頭(2枚1組の形)は何ですか?
'dora': '', // ドラは何枚ありますか?
'tanyao': '', // 手牌は2~8の数牌だけですか?
'machi': '', // アガリ牌の待ちの形は何ですか?
'ipeko': '', // 同じ順序の形はいくつありますか?
'furo': '', // チー、ポン、明槓をしていますか?
'chitoitsu': '', // 同じ牌が2枚1組(対子)で7つありますか?
'sanshokudojun': '', // 3色で同じ順序の形はありますか?
'sanshokudoko': '', // 3色で3枚1組の形はありますか?
'yakuhai_haku': '', // 「白」が3枚以上ありますか?
'yakuhai_hatsu': '', // 「發」が3枚以上ありますか?
'yakuhai_chun': '', // 「中」が3枚以上ありますか?
'yakuhai_ton': '', // 「東」が3枚以上ありますか?
'yakuhai_nan': '', // 「南」が3枚以上ありますか?
'yakuhai_sha': '', // 「西」が3枚以上ありますか?
'yakuhai_pe': '', // 「北」が3枚以上ありますか?
'honitsu': '', // 同じ色の数牌と字牌だけですか?
'chinitsu': '', // 全て同じ種類ですか?
'sananko': '', // 手牌に同じ牌が3枚(暗刻)3組がありますか?
'toitoi': '', // 同じ牌が3枚1組(刻子)で4つありますか?
'ikkitsukan': '', // 同じ色の1~9の数牌が全て揃っていますか?
'chanta': '', // 1~3の順子、7~9の順子、19の数牌、字牌だけですか?
'junchan': '', // 1~3の順子、7~9の順子、19の数牌だけですか?
'haitei': '', // 山に残っている最後の牌(海底)でツモアガリしましたか?
'hotei': '', // 局の最後の捨て牌(河底)でロンアガリしましたか?
'rinshankaiho': '', // 槓してツモった牌(嶺上牌)でアガリしましたか?
'shosangen': '', // 「白」「發」「中」が2枚1組、3枚(4枚)2組ありますか?
'honroto': '', // 19の数牌と字牌だけですか?
'chankan': '', // 他の人が加槓しようとした牌でアガリましたか?
'sankantsu': '', // 槓を3回しましたか?
'minko_yaochu': '', // 1または9の数牌と字牌の明刻はいくつありますか?
'anko_yaochu': '', // 1または9の数牌と字牌の暗刻はいくつありますか?
'minko_tanyao': '', // 2~8の数牌の明刻はいくつありますか?
'anko_tanyao': '', // 2~8の数牌の暗刻はいくつありますか?
'minkan_yaochu': '', // 1または9の数牌と字牌の明槓はいくつありますか?
'ankan_yaochu': '', // 1または9の数牌と字牌の暗槓はいくつありますか?
'minkan_tanyao': '', // 2~8の数牌の明槓はいくつありますか?
'ankan_tanyao': '', // 2~8の数牌の暗槓はいくつありますか?
};
}
押されたbuttonタグのvalue属性の値を、定数の該当のキーの値として格納する
回答時に押されるbuttonタグのname属性とvalue属性には、点数計算に必要な値がそれぞれ設定されています。それを$.eachで反復処理して、定数の該当のキーの値として格納します。
$.each(ITEMS, (key, val) => {
if ($('[name="' + key + '"].selected').attr('value') !== undefined) {
ITEMS[key] = $('[name="' + key + '"].selected').attr('value');
}
});
buttonタグが押された時にselectedというHTMLのクラスが付与されるようにしておきます。解説の都合上、付与するためのコードは省略します。
参考:jQuery.each() | jQuery API Documentation
参考:.attr() | jQuery API Documentation
参考:undefined – JavaScript | MDN
点数計算に使用する変数を定義する
点数計算に使用する変数を定義します。
let result = ''; // 結果
let fu = 20; // 符数
let han = 0; // 翻数
let point = '0'; // 点数
let YAKU = []; // 役
役を判定するメソッドを定義する
役満以外の役を判定するメソッドを定義し、符数と翻数、役を先ほど定義した変数に格納します。
- ここでは「么(公に似た漢字)」は「ヤオ」と表記します。
- 副露判定のメソッドが頻出しますが、定義するのは一度だけです。
- 名前のある役満には対応していません。
リーチ、ダブルリーチ(通称ダブリー)、一発
// リーチ
if (!this.isDaburi(ITEMS) && this.isRichi(ITEMS)) {
han += 1;
YAKU.push('リーチ');
// 一発
if (this.isIppatsu(ITEMS)) {
han += 1;
YAKU.push('一発');
}
}
// ダブルリーチ
if (this.isDaburi(ITEMS)) {
han += 2;
YAKU.push('ダブルリーチ');
// 一発
if (this.isIppatsu(ITEMS)) {
han += 1;
YAKU.push('一発');
}
}
/**
* リーチ判定
*
* @param ITEMS[object]
* @return bool
*/
isRichi(ITEMS) {
if (ITEMS.richi === 'はい') {
return true;
} else {
return false;
}
}
/**
* ダブルリーチ判定
*
* @param ITEMS[object]
* @return bool
*/
isDaburi(ITEMS) {
if (ITEMS.daburi === 'はい') {
return true;
} else {
return false;
}
}
/**
* 一発判定
*
* @param ITEMS[object]
* @return bool
*/
isIppatsu(ITEMS) {
if (ITEMS.ippatsu === 'はい') {
return true;
} else {
return false;
}
}
参考:Array.prototype.push() – JavaScript | MDN
参考:this – JavaScript | MDN
門前清自摸和(メンゼンチンツモホー)(通称ツモ)
門前役のため副露判定も行います。
// 門前清自摸和
if (ITEMS.agari === 'ツモ') {
if (!this.isFuro(ITEMS)) {
han += 1;
YAKU.push('門前清自摸和');
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
平和(ピンフ)
門前役のため副露判定も行います。
// 平和
if (this.isPinfu(ITEMS)) {
han += 1;
YAKU.push('平和');
}
/**
* 平和判定
*
* @param ITEMS[object]
* @return bool
*/
isPinfu(ITEMS) {
if (ITEMS.shuntsu === '4' && ITEMS.machi === 'リャンメン' && !this.isFuro(ITEMS)) {
if (ITEMS.janto !== '' && ITEMS.janto !== '場風牌または自風牌' && ITEMS.janto !== '白または發または中') {
return true;
} else {
return false;
}
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
七対子(チートイツ)
七対子の上位の役である二盃口が優先されるように、二盃口判定も行います。選択項目を間違っている場合のために副露判定も行います。
// 七対子
if (this.isChitoitsu(ITEMS)) {
han += 2;
YAKU.push('七対子');
}
/**
* 七対子判定
*
* @param ITEMS[object]
* @return bool
*/
isChitoitsu(ITEMS) {
if (ITEMS.chitoitsu === 'はい' && ITEMS.ipeko !== '2' && !this.isFuro(ITEMS)) {
return true;
} else {
return false;
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
断ヤオ九(タンヤオチュー)(通称タンヤオ)
// 断ヤオ九
if (this.isTanyao(ITEMS)) {
han += 1;
YAKU.push('断ヤオ九');
}
/**
* 断ヤオ九判定
*
* @param ITEMS[object]
* @return bool
*/
isTanyao(ITEMS) {
if (ITEMS.tanyao === 'はい') {
return true;
} else {
return false;
}
}
一盃口(イーペーコー)
七対子と複合しないため七対子判定も行います。門前役のため副露判定も行います。
// 一盃口
if (this.isIpeko(ITEMS)) {
han += 1;
YAKU.push('一盃口');
}
/**
* 一盃口判定
*
* @param ITEMS[object]
* @return bool
*/
isIpeko(ITEMS) {
if (ITEMS.ipeko === '1' && !this.isChitoitsu(ITEMS) && !this.isFuro(ITEMS)) {
return true;
} else {
return false;
}
}
/**
* 七対子判定
*
* @param ITEMS[object]
* @return bool
*/
isChitoitsu(ITEMS) {
if (ITEMS.chitoitsu === 'はい' && ITEMS.ipeko !== '2' && !this.isFuro(ITEMS)) {
return true;
} else {
return false;
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
二盃口(リャンペーコー)
門前役のため副露判定も行います。
// 二盃口
if (this.isRyanpeko(ITEMS)) {
han += 3;
YAKU.push('二盃口');
}
/**
* 二盃口判定
*
* @param ITEMS[object]
* @return bool
*/
isRyanpeko(ITEMS) {
if (ITEMS.ipeko === '2' && !this.isFuro(ITEMS)) {
return true;
} else {
return false;
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
三色同順(サンショクドウジュン)
食い下がりによって翻数が落ちるため副露判定も行います。
// 三色同順
if (this.isSanshokudojun(ITEMS)) {
if (this.isFuro(ITEMS)) {
han += 1;
} else {
han += 2;
}
YAKU.push('三色同順');
}
/**
* 三色同順判定
*
* @param ITEMS[object]
* @return bool
*/
isSanshokudojun(ITEMS) {
if (ITEMS.sanshokudojun === 'はい') {
return true;
} else {
return false;
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
三色同刻(サンショクドウコウ)
// 三色同刻
if (this.isSanshokudoko(ITEMS)) {
han += 2;
YAKU.push('三色同刻');
}
/**
* 三色同刻判定
*
* @param ITEMS[object]
* @return bool
*/
isSanshokudoko(ITEMS) {
if (ITEMS.sanshokudoko === 'はい') {
return true;
} else {
return false;
}
}
役牌(白)(ハク)
// 役牌(白)
if (this.isYakuhaiHaku(ITEMS)) {
han += 1;
YAKU.push('役牌(白)');
}
/**
* 役牌(白)判定
*
* @param ITEMS[object]
* @return bool
*/
isYakuhaiHaku(ITEMS) {
if (ITEMS.yakuhai_haku === 'はい') {
return true;
} else {
return false;
}
}
役牌(發)(ハツ)
// 役牌(發)
if (this.isYakuhaiHatsu(ITEMS)) {
han += 1;
YAKU.push('役牌(發)');
}
/**
* 役牌(發)判定
*
* @param ITEMS[object]
* @return bool
*/
isYakuhaiHatsu(ITEMS) {
if (ITEMS.yakuhai_hatsu === 'はい') {
return true;
} else {
return false;
}
}
役牌(中)(チュン)
// 役牌(中)
if (this.isYakuhaiChun(ITEMS)) {
han += 1;
YAKU.push('役牌(中)');
}
/**
* 役牌(中)判定
*
* @param ITEMS[object]
* @return bool
*/
isYakuhaiChun(ITEMS) {
if (ITEMS.yakuhai_chun === 'はい') {
return true;
} else {
return false;
}
}
役牌(自風)(ジカゼ)
// 役牌(自風)
if (
this.isYakuhaiTon(ITEMS)
|| this.isYakuhaiNan(ITEMS)
|| this.isYakuhaiSha(ITEMS)
|| this.isYakuhaiPe(ITEMS)
) {
han += 1;
YAKU.push('役牌(自風)');
}
/**
* 役牌(東)判定
*
* @param ITEMS[object]
* @return bool
*/
isYakuhaiTon(ITEMS) {
if (
(ITEMS.yakuhai_ton === 'はい')
&& ITEMS.cha === '東'
) {
return true;
} else {
return false;
}
}
/**
* 役牌(南)判定
*
* @param ITEMS[object]
* @return bool
*/
isYakuhaiNan(ITEMS) {
if (
(ITEMS.yakuhai_nan === 'はい')
&& ITEMS.cha === '南'
) {
return true;
} else {
return false;
}
}
/**
* 役牌(西)判定
*
* @param ITEMS[object]
* @return bool
*/
isYakuhaiSha(ITEMS) {
if (
(ITEMS.yakuhai_sha === 'はい')
&& ITEMS.cha === '西'
) {
return true;
} else {
return false;
}
}
/**
* 役牌(北)判定
*
* @param ITEMS[object]
* @return bool
*/
isYakuhaiPe(ITEMS) {
if (
(ITEMS.yakuhai_pe === 'はい')
&& ITEMS.cha === '北'
) {
return true;
} else {
return false;
}
}
役牌(場風)(バカゼ)
// 役牌(場風)
if (this.isYakuhaiBa(ITEMS)) {
han += 1;
YAKU.push('役牌(場風)');
}
/**
* 場の役牌判定
*
* @param ITEMS[object]
* @return bool
*/
isYakuhaiBa(ITEMS) {
if (
(ITEMS.bakaze === '東' && ITEMS.yakuhai_ton === 'はい')
|| (ITEMS.bakaze === '南' && ITEMS.yakuhai_nan === 'はい')
|| (ITEMS.bakaze === '西' && ITEMS.yakuhai_sha === 'はい')
|| (ITEMS.bakaze === '北' && ITEMS.yakuhai_pe === 'はい')
) {
return true;
} else {
return false;
}
}
混一色(ホンイツ)
食い下がりによって翻数が落ちるため副露判定も行います。
// 混一色
if (this.isHonitsu(ITEMS)) {
if (this.isFuro(ITEMS)) {
han += 2;
} else {
han += 3;
}
YAKU.push('混一色');
}
/**
* 混一色判定
*
* @param ITEMS[object]
* @return bool
*/
isHonitsu(ITEMS) {
if (ITEMS.honitsu === 'はい') {
return true;
} else {
return false;
}
}
清一色(チンイツ)
食い下がりによって翻数が落ちるため副露判定も行います。
// 清一色
if (this.isChinitsu(ITEMS)) {
if (this.isFuro(ITEMS)) {
han += 5;
} else {
han += 6;
}
YAKU.push('清一色');
}
/**
* 清一色判定
*
* @param ITEMS[object]
* @return bool
*/
isChinitsu(ITEMS) {
if (ITEMS.chinitsu === 'はい') {
return true;
} else {
return false;
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
対々和(トイトイホー)(通称トイトイ)
// 対々和
if (this.isToitoi(ITEMS)) {
han += 2;
YAKU.push('対々和');
}
/**
* 対々和判定
*
* @param ITEMS[object]
* @return bool
*/
isToitoi(ITEMS) {
if (ITEMS.toitoi === 'はい') {
return true;
} else {
return false;
}
}
一気通貫(イッキツウカン)(通称イッツー)
食い下がりによって翻数が落ちるため副露判定も行います。
// 一気通貫
if (this.isIkkitsukan(ITEMS)) {
if (this.isFuro(ITEMS)) {
han += 1;
} else {
han += 2;
}
YAKU.push('一気通貫');
}
/**
* 一気通貫判定
*
* @param ITEMS[object]
* @return bool
*/
isIkkitsukan(ITEMS) {
if (ITEMS.ikkitsukan === 'はい') {
return true;
} else {
return false;
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
混全帯ヤオ九(ホンチャンタイヤオチュウ)(通称チャンタ)
食い下がりによって翻数が落ちるため副露判定も行います。
// 混全帯ヤオ九
if (this.isChanta(ITEMS)) {
if (this.isFuro(ITEMS)) {
han += 1;
} else {
han += 2;
}
YAKU.push('混全帯ヤオ九');
}
/**
* 混全帯ヤオ九判定
*
* @param ITEMS[object]
* @return bool
*/
isChanta(ITEMS) {
if (ITEMS.chanta === 'はい') {
return true;
} else {
return false;
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
純全帯ヤオ九(ジュンチャンタイヤオチュー)(通称ジュンチャン)
食い下がりによって翻数が落ちるため副露判定も行います。
// 純全帯ヤオ九
if (this.isJunchan(ITEMS)) {
if (this.isFuro(ITEMS)) {
han += 2;
} else {
han += 3;
}
YAKU.push('純全帯ヤオ九');
}
/**
* 純全帯ヤオ九判定
*
* @param ITEMS[object]
* @return bool
*/
isJunchan(ITEMS) {
if (ITEMS.junchan === 'はい') {
return true;
} else {
return false;
}
}
/**
* 副露判定
*
* @param ITEMS[object]
* @return bool
*/
isFuro(ITEMS) {
if (ITEMS.furo === 'はい') {
return true;
} else {
return false;
}
}
三暗刻(サンアンコウ)
// 三暗刻
if (this.isSananko(ITEMS)) {
han += 2;
YAKU.push('三暗刻');
}
/**
* 三暗刻判定
*
* @param ITEMS[object]
* @return bool
*/
isSananko(ITEMS) {
if (ITEMS.sananko === 'はい') {
return true;
} else {
return false;
}
}
海底撈月(ハイテイラオユエ)(通称ハイテイ)、河底撈魚(ホウテイラオユイ)(通称ホウテイ)、嶺上開花(リンシャンカイホウ)、搶槓(チャンカン)
アガる時はいずれか1つなのでまとめています。
// 海底撈月
if (this.isHaitei(ITEMS)) {
han += 1;
YAKU.push('海底撈月');
// 河底撈魚
} else if (this.isHotei(ITEMS)) {
han += 1;
YAKU.push('河底撈魚');
// 嶺上開花
} else if (this.isRinshankaiho(ITEMS)) {
han += 1;
YAKU.push('嶺上開花');
// 搶槓
} else if (this.isChankan(ITEMS)) {
han += 1;
YAKU.push('搶槓');
}
/**
* 海底撈月判定
*
* @param ITEMS[object]
* @return bool
*/
isHaitei(ITEMS) {
if (ITEMS.haitei === 'はい') {
return true;
} else {
return false;
}
}
/**
* 河底撈魚判定
*
* @param ITEMS[object]
* @return bool
*/
isHotei(ITEMS) {
if (ITEMS.hotei === 'はい') {
return true;
} else {
return false;
}
}
/**
* 嶺上開花判定
*
* @param ITEMS[object]
* @return bool
*/
isRinshankaiho(ITEMS) {
if (ITEMS.rinshankaiho === 'はい') {
return true;
} else {
return false;
}
}
/**
* 搶槓判定
*
* @param ITEMS[object]
* @return bool
*/
isChankan(ITEMS) {
if (ITEMS.chankan === 'はい') {
return true;
} else {
return false;
}
}
混老頭(ホンロウトウ)
// 混老頭
if (this.isHonroto(ITEMS)) {
han += 2;
YAKU.push('混老頭');
}
/**
* 混老頭判定
*
* @param ITEMS[object]
* @return bool
*/
isHonroto(ITEMS) {
if (ITEMS.honroto === 'はい') {
return true;
} else {
return false;
}
}
三槓子(サンカンツ)
// 三槓子
if (this.isSankantsu(ITEMS)) {
han += 2;
YAKU.push('三槓子');
}
/**
* 三槓子判定
*
* @param ITEMS[object]
* @return bool
*/
isSankantsu(ITEMS) {
if (ITEMS.sankantsu === 'はい') {
return true;
} else {
return false;
}
}
符を計算するメソッドを定義する
次は符計算を行うメソッドを定義します。
/**
* 符計算
*
* @param YAKU[array]
* @param ITEMS[object]
* @return int
*/
fuCalc(YAKU, ITEMS) {
let fu = 20;
// アガリ
if (ITEMS.agari === 'ツモ') {
fu += 2;
} else if (ITEMS.agari === 'ロン' && !this.isFuro(ITEMS)) {
fu += 10;
}
if (YAKU.indexOf('門前清自摸和') !== -1 && YAKU.indexOf('平和') !== -1) {
fu = 20;
} else if (YAKU.indexOf('七対子') !== -1) {
fu = 25;
} else {
if (ITEMS.machi !== '' && ITEMS.machi !== 'リャンメン' && ITEMS.machi !== 'シャンポン') {
fu += 2;
}
if (ITEMS.janto !== '' && ITEMS.janto !== '2~8の数牌') {
fu += 2;
}
if (ITEMS.minko_yaochu !== '') {
fu += (4 * parseInt(ITEMS.minko_yaochu));
}
if (ITEMS.anko_yaochu !== '') {
fu += (8 * parseInt(ITEMS.anko_yaochu));
}
if (ITEMS.minko_tanyao !== '') {
fu += (2 * parseInt(ITEMS.minko_tanyao));
}
if (ITEMS.anko_tanyao !== '') {
fu += (4 * parseInt(ITEMS.anko_tanyao));
}
if (ITEMS.minkan_yaochu !== '') {
fu += (16 * parseInt(ITEMS.minkan_yaochu));
}
if (ITEMS.ankan_yaochu !== '') {
fu += (32 * parseInt(ITEMS.ankan_yaochu));
}
if (ITEMS.minkan_tanyao !== '') {
fu += (8 * parseInt(ITEMS.minkan_tanyao));
}
if (ITEMS.ankan_tanyao !== '') {
fu += (16 * parseInt(ITEMS.ankan_tanyao));
}
fu = Math.ceil(fu / 10) * 10;
}
if (fu === 20 && ITEMS.agari === 'ロン' && this.isFuro(ITEMS)) {
fu = 30;
}
return fu;
}
コードを順番に解説します。
符数を格納する変数を定義する
符数は20から始まるため、fuという変数に20を入れておきます。これを役などに合わせて加算していきます。
let fu = 20;
アガリ方によって符数を加算する
ツモアガリの場合は符数を+2、ロンアガリで副露をしていない場合は符数を+10します。
// アガリ
if (ITEMS.agari === 'ツモ') {
fu += 2;
} else if (ITEMS.agari === 'ロン' && !this.isFuro(ITEMS)) {
fu += 10;
}
役によってベースの符数を設定する
平和と七対子は符数が固定されている役です。平和の場合は符数が20、七対子の場合は符数が25です。
if (YAKU.indexOf('門前清自摸和') !== -1 && YAKU.indexOf('平和') !== -1) {
fu = 20;
} else if (YAKU.indexOf('七対子') !== -1) {
fu = 25;
} else {
~
}
参考:String.prototype.indexOf() – JavaScript | MDN
アガリ牌の待ちや刻子の有無によって符数を加算する
上記の6行目のコードです。
アガリ牌の待ちが単騎、カンチャン、ペンチャン、ノベタンのいずれかの場合は、符数を+2します。
if (ITEMS.machi !== '' && ITEMS.machi !== 'リャンメン' && ITEMS.machi !== 'シャンポン') {
fu += 2;
}
雀頭が三元牌または連風牌の対子の場合は、符数を+2します。ルールによっては連風牌の対子は4符ですが、当サイトでは2符を採用しています。
if (ITEMS.janto !== '' && ITEMS.janto !== '2~8の数牌' && ITEMS.janto !== '1または9の数牌' && ITEMS.janto !== 'その他') {
fu += 2;
}
1または9の数牌または字牌の明刻がある場合は、選択されたボタンの数に合わせて符数を+4します。
if (ITEMS.minko_yaochu !== '') {
fu += (4 * parseInt(ITEMS.minko_yaochu));
}
参考:parseInt() – JavaScript | MD
1または9の数牌または字牌の暗刻がある場合は、選択されたボタンの数に合わせて符数を+8します。
if (ITEMS.anko_yaochu !== '') {
fu += (8 * parseInt(ITEMS.anko_yaochu));
}
2~8の数牌の明刻がある場合は、選択されたボタンの数に合わせて符数を+2します。
if (ITEMS.minko_tanyao !== '') {
fu += (2 * parseInt(ITEMS.minko_tanyao));
}
2~8の数牌の暗刻がある場合は、選択されたボタンの数に合わせて符数を+4します。
if (ITEMS.anko_tanyao !== '') {
fu += (4 * parseInt(ITEMS.anko_tanyao));
}
1または9の数牌または字牌の明槓がある場合は、選択されたボタンの数に合わせて符数を+16します。
if (ITEMS.minkan_yaochu !== '') {
fu += (16 * parseInt(ITEMS.minkan_yaochu));
}
1または9の数牌または字牌の暗槓がある場合は、選択されたボタンの数に合わせて符数を+32します。
if (ITEMS.ankan_yaochu !== '') {
fu += (32 * parseInt(ITEMS.ankan_yaochu));
}
2~8の数牌の明槓がある場合は、選択されたボタンの数に合わせて符数を+8します。
if (ITEMS.minkan_tanyao !== '') {
fu += (8 * parseInt(ITEMS.minkan_tanyao));
}
2~8の数牌の暗槓がある場合は、選択されたボタンの数に合わせて符数を+16します。
if (ITEMS.ankan_tanyao !== '') {
fu += (16 * parseInt(ITEMS.ankan_tanyao));
}
最後に符数を切り上げます。
一の位で切り上げるので、10で割って小数第一位で切り上げて、10を掛けています。
fu = Math.ceil(fu / 10) * 10;
参考:Math.ceil() – JavaScript | MDN
20符かつロンアガリの場合は30符
上記のどれにも当てはまらずに20符のままでロンアガリの場合は、符数を30に設定します。
if (fu === 20 && ITEMS.agari === 'ロン' && this.isFuro(ITEMS)) {
fu = 30;
}
計算ができたら符数を返します。
return fu;
符数と翻数から点数を計算
次に符数と翻数から点数を計算します。
符数と翻数から点数を計算するメソッドを定義する
符数と翻数から点数を計算するメソッドを定義します。このメソッドでは引数として符数、翻数、本場、アガリ方、家を使用します。
/**
* 符数と翻数から点数計算
*
* @param fu[int]
* @param han[int]
* @param honba[string]
* @param agari[string]
* @param cha[string]
* @return string
*/
pointCalcFromHanFu(fu, han, honba, agari, cha) {
~
}
使用する変数を定義する
まずは使用する変数を定義します。
let point;
let chaType = '子';
honba = honba !== '' ? honba : 0;
let kyotaku = 300 * parseInt(honba);
参考:条件 (三項) 演算子 – JavaScript | MDN
親と子の符数と翻数別の点数を設定する
次に点数早見表の、親と子の符数と翻数別の点数を定数に格納します。満貫以上は含めません。
const POINT = {
'親': {
'ツモ': {
20: {
2: 700,
3: 1300,
4: 2600
},
25: {
2: 800,
3: 1600,
4: 3200
},
30: {
1: 500,
2: 1000,
3: 2000,
4: 3900
},
40: {
1: 700,
2: 1300,
3: 2600
},
50: {
1: 800,
2: 1600,
3: 3200
},
60: {
1: 1000,
2: 2000,
3: 3900
},
70: {
1: 1200,
2: 2300
},
80: {
1: 1300,
2: 2600
},
90: {
1: 1500,
2: 2900
},
100: {
1: 1600,
2: 3200
},
110: {
1: 1800,
2: 3600
}
},
'ロン': {
25: {
2: 2400,
3: 4800,
4: 9600
},
30: {
1: 1500,
2: 2900,
3: 5800,
4: 11600
},
40: {
1: 2000,
2: 3900,
3: 7700
},
50: {
1: 2400,
2: 4800,
3: 9600
},
60: {
1: 2900,
2: 5800,
3: 11600
},
70: {
1: 3400,
2: 6800
},
80: {
1: 3900,
2: 7700
},
90: {
1: 4400,
2: 8700
},
100: {
1: 4800,
2: 9600
},
110: {
1: 5300,
2: 10600
}
}
},
'子': {
'ツモ': {
20: {
2: [400, 700],
3: [700, 1300],
4: [1300, 2600]
},
25: {
2: [400, 800],
3: [800, 1600],
4: [1600, 3200]
},
30: {
1: [300, 500],
2: [500, 1000],
3: [1000, 2000],
4: [2000, 3900]
},
40: {
1: [400, 700],
2: [700, 1300],
3: [1300, 2600]
},
50: {
1: [400, 800],
2: [800, 1600],
3: [1600, 3200]
},
60: {
1: [500, 1000],
2: [1000, 2000],
3: [2000, 3900]
},
70: {
1: [600, 1200],
2: [1200, 2300]
},
80: {
1: [700, 1300],
2: [1300, 2600]
},
90: {
1: [800, 1500],
2: [1500, 2900]
},
100: {
1: [800, 1600],
2: [1600, 3200]
},
110: {
1: [900, 1800],
2: [1800, 3600]
}
},
'ロン': {
25: {
2: 1600,
3: 3200,
4: 6400
},
30: {
1: 1000,
2: 2000,
3: 3900,
4: 7700
},
40: {
1: 1300,
2: 2600,
3: 5200
},
50: {
1: 1600,
2: 3200,
3: 6400
},
60: {
1: 2000,
2: 3900,
3: 7700
},
70: {
1: 2300,
2: 4500
},
80: {
1: 2600,
2: 5200
},
90: {
1: 2900,
2: 5800
},
100: {
1: 3200,
2: 6400
},
110: {
1: 3600,
2: 7100
}
}
}
};
家を判定して親を設定する
次に家が東の場合は親にします。
if (cha === '東') {
chaType = '親';
}
点数を変数に格納する
親か子か、アガリ方、符数、翻数の情報を使用して、変数に点数早見表の点数を格納します。
point = POINT[chaType][agari][fu][han];
満貫以上の点数を変数に格納する
上記では満貫未満までしか対応していないため、満貫、跳満、倍満、3倍満、数え役満の条件に一致するかを判定して、その点数を変数に格納します。
if (chaType === '親') {
if (
han === 5
|| (fu >= 40 && fu <= 60 && han >= 4 && han <= 5)
|| (fu >= 70 && han >= 3 && han <= 5)
) {
if (agari === 'ツモ') {
point = 4000;
} else {
point = 12000;
}
} else if (han >= 6 && han <= 7) {
if (agari === 'ツモ') {
point = 6000;
} else {
point = 18000;
}
} else if (han >= 8 && han <= 10) {
if (agari === 'ツモ') {
point = 8000;
} else {
point = 24000;
}
} else if (han >= 11 && han <= 12) {
if (agari === 'ツモ') {
point = 12000;
} else {
point = 36000;
}
} else if (han >= 13) {
if (agari === 'ツモ') {
point = 16000;
} else {
point = 48000;
}
}
} else if (chaType === '子') {
if (
han === 5
|| (fu >= 40 && fu <= 60 && han >= 4 && han <= 5)
|| (fu >= 70 && han >= 3 && han <= 5)
) {
if (agari === 'ツモ') {
point = [2000, 4000];
} else {
point = 8000;
}
} else if (han >= 6 && han <= 7) {
if (agari === 'ツモ') {
point = [3000, 6000];
} else {
point = 12000;
}
} else if (han >= 8 && han <= 10) {
if (agari === 'ツモ') {
point = [4000, 8000];
} else {
point = 16000;
}
} else if (han >= 11 && han <= 12) {
if (agari === 'ツモ') {
point = [6000, 12000];
} else {
point = 24000;
}
} else if (han >= 13) {
if (agari === 'ツモ') {
point = [8000, 16000];
} else {
point = 32000;
}
}
}
上記コードを順番に順番に順番に解説します。
下記コードは、親で5翻、または40符以上かつ60符以下かつ4翻以上かつ5翻以下の場合、または70符以上かつ3翻以上かつ5翻以下の場合に、ツモアガリなら4000(オール)、ロンアガリなら12000を変数に格納しています。
if (chaType === '親') {
if (
han === 5
|| (fu >= 40 && fu <= 60 && han >= 4 && han <= 5)
|| (fu >= 70 && han >= 3 && han <= 5)
) {
if (agari === 'ツモ') {
point = 4000;
} else {
point = 12000;
}
} ~
}
下記コードは、親で6翻以上かつ7翻以下の場合に、ツモアガリなら6000(オール)、ロンアガリなら18000を変数に格納しています。
if (chaType === '親') {
~
} else if (han >= 6 && han <= 7) {
if (agari === 'ツモ') {
point = 6000;
} else {
point = 18000;
}
} ~
}
下記コードは、親で8翻以上かつ10翻以下の場合に、ツモアガリなら8000(オール)、ロンアガリなら24000を変数に格納しています。
if (chaType === '親') {
~
} else if (han >= 8 && han <= 10) {
if (agari === 'ツモ') {
point = 8000;
} else {
point = 24000;
}
} ~
}
下記コードは、親で11翻以上かつ12翻以下の場合に、ツモアガリなら12000(オール)、ロンアガリなら36000を変数に格納しています。
if (chaType === '親') {
~
} else if (han >= 11 && han <= 12) {
if (agari === 'ツモ') {
point = 12000;
} else {
point = 36000;
}
} ~
}
下記コードは、親で13翻以上の場合に、ツモアガリなら16000(オール)、ロンアガリなら48000を変数に格納しています。
if (chaType === '親') {
~
} else if (han >= 13) {
if (agari === 'ツモ') {
point = 16000;
} else {
point = 48000;
}
}
}
下記コードは、子で5翻、または40符以上かつ60符以下かつ4翻以上かつ5翻以下の場合、または70符以上かつ3翻以上かつ5翻以下の場合に、ツモアガリなら2000/4000、ロンアガリなら8000を変数に格納しています。子のツモアガリは、親と子で支払ってもらう点数が違うため配列にして格納します。
~
} else if (chaType === '子') {
if (
han === 5
|| (fu >= 40 && fu <= 60 && han >= 4 && han <= 5)
|| (fu >= 70 && han >= 3 && han <= 5)
) {
if (agari === 'ツモ') {
point = [2000, 4000];
} else {
point = 8000;
}
} ~
}
下記コードは、子で6翻以上かつ7翻以下の場合に、ツモアガリなら3000/6000、ロンアガリなら12000を変数に格納しています。
~
} else if (chaType === '子') {
~
} else if (han >= 6 && han <= 7) {
if (agari === 'ツモ') {
point = [3000, 6000];
} else {
point = 12000;
}
} ~
}
下記コードは、子で8翻以上かつ10翻以下の場合に、ツモアガリなら4000/8000、ロンアガリなら16000を変数に格納しています。
~
} else if (chaType === '子') {
~
} else if (han >= 8 && han <= 10) {
if (agari === 'ツモ') {
point = [4000, 8000];
} else {
point = 16000;
}
} ~
}
下記コードは、子で11翻以上かつ12翻以下の場合に、ツモアガリなら6000/12000、ロンアガリなら24000を変数に格納しています。
~
} else if (chaType === '子') {
~
} else if (han >= 11 && han <= 12) {
if (agari === 'ツモ') {
point = [6000, 12000];
} else {
point = 24000;
}
} ~
}
下記コードは、子で13翻以上の場合に、ツモアガリなら8000/16000、ロンアガリなら32000を変数に格納しています。
~
} else if (chaType === '子') {
~
} else if (han >= 13) {
if (agari === 'ツモ') {
point = [8000, 16000];
} else {
point = 32000;
}
}
}
供託されているリーチ棒の点数を加算する
最後にリーチ後に局が流れて供託されているリーチ棒の点数を加算します。
if (agari === 'ツモ') {
// 供託を3等分
kyotaku = parseInt(kyotaku) / 3;
if (chaType === '親') {
// 供託がある場合
if (kyotaku !== 0) {
point = point + 'は' + (parseInt(point) + parseInt(kyotaku)) + 'オール';
} else {
point = point + 'オール';
}
} else {
// 供託がある場合
if (kyotaku !== 0) {
point = point[0] + '/' + point[1] + 'は' + (point[0] + kyotaku) + '/' + (point[1] + kyotaku);
} else {
point = point[0] + '/' + point[1];
}
}
} else {
if (kyotaku !== 0) {
point = point + 'は' + (parseInt(point) + parseInt(kyotaku));
}
}
上記コードを順番に解説します。
下記コードは、ツモアガリの場合に供託を3等分しています。これは他家の3人にそれぞれ支払ってもらうためです。
if (agari === 'ツモ') {
// 供託を3等分
kyotaku = parseInt(kyotaku) / 3;
~
} ~
下記コードは、ツモアガリで親の場合に、〇〇〇は〇〇〇(供託を加算した点数)オールを変数に格納しています。供託がない場合は、点数にオールを付けて変数に格納しています。
if (agari === 'ツモ') {
~
if (chaType === '親') {
// 供託がある場合
if (kyotaku !== 0) {
point = point + 'は' + (parseInt(point) + parseInt(kyotaku)) + 'オール';
} else {
point = point + 'オール';
}
} ~
} ~
供託がある場合は、最終的にこのように表示されます。表示させるコードは後述します。
下記コードは、ツモアガリで子の場合に、〇〇〇/〇〇〇は〇〇〇/〇〇〇(供託を加算した点数)を変数に格納しています。供託がない場合は、〇〇〇/〇〇〇を変数に格納しています。
if (agari === 'ツモ') {
~
} else {
// 供託がある場合
if (kyotaku !== 0) {
point = point[0] + '/' + point[1] + 'は' + (point[0] + kyotaku) + '/' + (point[1] + kyotaku);
} else {
point = point[0] + '/' + point[1];
}
}
} ~
供託がある場合は、最終的にこのように表示されます。
下記コードは、ロンアガリで供託がある場合に〇〇〇は〇〇〇(供託を加算した点数)を変数に格納しています。
~
} else {
if (kyotaku !== 0) {
point = point + 'は' + (parseInt(point) + parseInt(kyotaku));
}
}
最終的にこのように表示されます。
供託分を加算できたら点数を返します。
return point;
符数、翻数、点数を表示する表を出力する
ここまでで計算した符数、翻数、点数を表示する表を出力します。
pointCalcメソッドの下部に下記コードを追記します。
/**
* 点数計算
*/
pointCalc() {
~
// 符数
fu = this.fuCalc(YAKU, ITEMS);
if (ITEMS.cha !== '' && ITEMS.agari !== '' && han >= 1 && fu <= 110) {
point = this.pointCalcFromHanFu(fu, han, ITEMS.honba, ITEMS.agari, ITEMS.cha);
result += '<table class="bordered-table mahjong-result-table">';
result += '<tr>';
result += '<th>符数</th>';
result += '<th>翻数</th>';
result += '<th>点数</th>';
result += '</tr>';
result += '<tr>';
result += '<td>' + fu + '</td>';
result += '<td>' + han + '</td>';
result += '<td>' + point + '</td>';
result += '</tr>';
result += '<tr>';
result += '<th>役</th>';
result += '<td colspan="3">';
result += '<ul>';
$.each(YAKU, (key, val) => {
result += '<li>' + val + '</li>';
});
result += '</ul>';
result += '</td>';
result += '</tr>';
result += '</table>';
$('#result-inner').html(result);
} else {
$('#result-inner').html('');
$('#error').show();
$('#error').html('ノーテンです');
}
}
参考:.html() | jQuery API Documentation
参考:.show() | jQuery API Documentation
後はこのようなHTMLを用意しておけば表が表示されます。
<div id="result" class="mahjong-result">
<div id="error" class="error"></div>
<div id="result-inner" class="mahjong-result-inner">
<span class="mahjong-result-default-text">ここに点数が表示されます</span>
</div><!-- /.mahjong-result-inner -->
</div><!-- /.mahjong-result -->
クラスのインスタンスを生成する
ここまでできたら、最初に定義したクラスのインスタンスを生成します。
const mahjong = new Mahjong();
ボタンを押された時に点数計算する
選択項目のいずれかのbuttonタグが押された時に、点数計算を行うメソッドを呼び出します。
// ボタンが押されたら点数計算
$('.btn-group button').on('click', () => {
mahjong.pointCalc();
});
参考:.on() | jQuery API Documentation
以上が麻雀の点数計算ツールの解説です。
tooolsのTech Blogではこれからも役に立つ情報を発信していきますので、定期的に閲覧していただけると幸いです。