feature image

2018年12月3日 | ブログ記事

TypeScriptで役探索[アドベントカレンダー2018 40日目]

お世話になっております。18のopferです。

突然ですが、麻雀って楽しいですよね。私は大学に入ってから麻雀を始めたのですが、見事にハマってしまいました。牌を触ってると安心します。

去年はYosotsuさんが待ち牌探索をやっていたようです。大変興味深い記事でした。

さて今回は麻雀の役の探索をしてみたいと思います。

まあ僕の浅知恵で作ったものなので正しく動くか保証しませんけど(一応テストはしてます)

言語はTypeScriptを使いました。ブラウザで動く麻雀ゲームを作るならこの言語で書いておくのがいいかなと思ったからです。(絶対Pythonの方が楽だった)

TypeScriptって?

おなじみのJavaScriptを基本に、その弱点を補った言語である。クラスベースのオブジェクト指向や静的型付けができる。基本構文はJavaScriptと同じなので知ってる人ならとっつきやすく、型があるので強い。

型があるので強い

条件

成立しうる役

上の条件で成立しうる役は

こんなところでしょうか(ローカル役あったりなかったりは見逃してほしい)

愚直にひとつひとつに対して条件を当てはめていけばいいんじゃないの?

役探索クラス

yakuSearch.tsclass Yaku {
    public hansuu:  number = 0
    public yakuman: number = 0
    public yaku: string[] = []

    constructor (tehai: number[], agariHai: number, zikaze: string, kawaNum: number, tumoAgari: boolean, mentsu: number[][], isriich: number, ippatsu: boolean) {
        const hand = tehai.slice()
        hand.push(agariHai)
        hand.sort()

        this.main(tehai, agariHai, zikaze, kawaNum, tumoAgari, mentsu, hand, isriich, ippatsu)

    }


    //  探索
    private main = (tehai: number[], agariHai: number, zikaze: string, kawaNum: number, tumoAgari: boolean, mentsu: number[][], hand: number[], isriich: number, ippatsu: boolean) => {

        this.tenho(tumoAgari, kawaNum, zikaze)
        this.ryuiiso(hand)
        this.churen(tehai, hand)
        this.threeOrFourAnkoToitoi(tehai, agariHai, tumoAgari, mentsu)
        if (this.yakuman === 0) {
            this.riich(isriich, ippatsu)
            this.menzentumo(tumoAgari)
            this.sangenpai(mentsu)
            this.tanyao(hand)
            this.pinhu(hand, agariHai, mentsu)
            this.honitsuChinitsu(hand)
            this.oneOrTwoPeko(mentsu)
            this.ittsu(mentsu)
            this.chanta(hand, mentsu)
            this.chitoitsu(hand, mentsu)
        }
    }


    // 天和or地和or人和
    private tenho = (tumoAgari: boolean, kawaNum: number, zikaze: string) => {
        if (kawaNum === 0) {
            if (tumoAgari) {
                if (zikaze === '東') {
                    this.yaku.push("天和")
                    this.yakuman++
                }
                else {
                    this.yaku.push("地和")
                    this.yakuman++
                }
            }
            else {
                this.yaku.push("人和")
                this.yakuman++
            }
        }
    }


    // 緑一色
    private  ryuiiso = (hand: number[]) => {
        const notRyuiiso = [1, 5, 7, 9]
        for (var i of notRyuiiso) {
            if (hand.indexOf(i) >= 0) {
                return 0
            }
        }
        if (hand.indexOf(10) >= 0) {
            this.yaku.push("緑一色")
            this.yakuman += 1
        }
        else {
            this.yaku.push("發なし緑一色")
            this.yakuman += 2
        }
    }


    // 九蓮宝燈or純正九蓮宝燈
    private churen(tehai: number[], hand: number[]){
        if (arrcomp(tehai, [1,1,1,2,3,4,5,6,7,8,9,9,9])) {
            this.yaku.push("純正九蓮宝燈")
            this.yakuman += 2
        }
        else {
            const hand_copy = hand.slice()
            for (var i=0; i<hand_copy.length; i++) {
                if ([2,3,4,5,6,7,8].indexOf(hand_copy[i]) >= 0) {
                    if(count(hand_copy, i) == 2) {
                        hand_copy.splice(i, 1)
                        break
                    }
                }
                else {
                    if(count(hand_copy, i) == 4) {
                        hand_copy.splice(i, 1)
                        break
                    }
                }
            }
            if (arrcomp(hand_copy, [1,1,1,2,3,4,5,6,7,8,9,9,9])){
                this.yaku.push("九蓮宝燈")
                this.yakuman++
            }
        }
    }


    // 三暗刻対々和or四暗刻or四暗刻単騎
    public threeOrFourAnkoToitoi = (tehai: number[], agariHai: number, tumoAgari: boolean, mentsu: number[][]) => {
        let countkotsu = 0
        for (var i of mentsu) {
            if (count(i, i[0]) === 3) {
                countkotsu++
            }
        }
        if (countkotsu === 4) {
            if (count(tehai, agariHai) == 1) {
                this.yaku.push("四暗刻単騎")
                this.yakuman += 2
            }
            else if (tumoAgari) {
                this.yaku.push("四暗刻")
                this.yakuman += 1
            }
            else if (this.yakuman == 0) {
                this.yaku.push("三暗刻")
                this.yaku.push("対々和")
                this.hansuu += 4
            }
        }
        else if (countkotsu === 3 && this.yakuman == 0) {
            this.yaku.push("三暗刻")
            this.hansuu += 2
        }
    }


    // 立直orダブリー 一発
    private riich = (isriich: number, ippatsu: boolean) => {
        if (isriich === 1) {
            this.yaku.push("立直")
            this.hansuu++
            if (ippatsu) {
                this.yaku.push("一発")
                this.hansuu++
            }
        }
        else if (isriich === 2) {
            this.yaku.push("ダブル立直")
            this.hansuu += 2
            if (ippatsu) {
                this.yaku.push("一発")
                this.hansuu++
            }
        }
    }


    // ツモ
    private menzentumo (tumoAgari: boolean) {
        if (tumoAgari) {
            this.yaku.push("門前清自摸和")
            this.hansuu++
        }
    }


    // 三元牌
    private sangenpai = (mentsu: number[][]) => {
        for (var i of mentsu) {
            if (arrcomp(i, [10, 10, 10])) {
                this.yaku.push("三元牌")
                this.hansuu++
            }
        }
    }


    // 断么九
    private tanyao = (hand) => {
        if ((hand[0] > 1) && (hand[13] < 9)) {
            this.yaku.push("断么九")
            this.hansuu++
        }
    }


    // 平和
    private pinhu = (hand: number[], agariHai: number, mentsu: number[][]) => {
        const hand_copy = hand.slice()
        let temp: number[] = []
        // 發が入っていないか
        if (hand.indexOf(10) >= 0) {
            return false
        }
        else {
            // 面子はすべて順子か
            for (var j of mentsu) {
                if (count(j, j[0]) !== 1) {
                    return false
                }
            }
            // 両面待ちか
            for (var i of mentsu) {
                if (i.indexOf(agariHai) >= 0) {
                    const i_copy = i.slice()
                    pull(i_copy, [agariHai])
                    if ( ((i_copy[1] - i_copy[0]) === 1) && ((i_copy[0] - 1) >= 1) && ((i_copy[1] + 1) <= 9 ) ) {
                        this.yaku.push("平和")
                        this.hansuu++
                        return true
                    }
                }
            }
        }
    }


    // 混一色or清一色
    private honitsuChinitsu = (hand: number[]) => {
        if (hand.indexOf(10) >= 0) {
            this.yaku.push("混一色")
            this.hansuu += 3
        }
        else {
            this.yaku.push("清一色")
            this.hansuu += 6
        }
    }



    // 一盃口or二盃口
    private oneOrTwoPeko = (mentsu: number[][]) => {
        let temp = 0
        for (let i = 1; i <= 7; i++) {
            if (countMentsu(mentsu, [i, i+1, i+2]) === 2) {
                temp++
            }
        }
        if (temp === 1) {
            this.yaku.push("一盃口")
            this.hansuu++
        }
        else if (temp === 2) {
            this.yaku.push("二盃口")
            this.hansuu += 3
        }
    }
    

    // 一気通貫
    private ittsu = (mentsu: number[][]) => {
        for (var i of mentsu) {
            if(arrcomp(i, [1,2,3])) {
                for (var j of mentsu) {
                    if(arrcomp(j, [4,5,6])){
                        for (var k of mentsu) {
                            if(arrcomp(k, [7,8,9])) {
                                this.yaku.push("一気通貫")
                                this.hansuu += 2
                                return 0
                            }
                        }
                    }
                }
            }
        }
    }


    // チャンタor純チャンタ
    private chanta = (hand: number[], mentsu: number[][]) => {
        const tempHand = hand.slice()
        for (var i of mentsu) {
            if(!(i.indexOf(1) >= 0 || i.indexOf(9) >= 0 || i.indexOf(10) >= 0)) {
                return 0
            }
            pull(tempHand, i)
        }
        if (tempHand.length === 2) {
            if ([1,9,10].indexOf(hand[0]) >= 0) {
                if (hand.indexOf(10) >= 0) {
                    this.yaku.push("混全帯幺九")
                    this.hansuu += 2
                }
                else {
                    this.yaku.push("純全帯幺九")
                    this.hansuu += 3
                }
            }
        }
    }


    // 七対子
    private chitoitsu = (hand: number[], mentsu: number[][]) => {
        if (mentsu.length === 0) {
            for(var i of hand) {
                if (count(hand, i) !== 2) {
                    return false
                }
            }
            this.yaku.push("七対子")
            this.hansuu += 2
            return true
        }
    }
    
}

// handの中のxの個数を返す
const count = (tehai: number[], x: number) => {
    let result = 0
    for (var i of tehai) {
      if (i === x) {
        result++
      }
    }
    return result
}

// mentsuの中のxの数を返す
const countMentsu = (mentsu: number[][], x: number[]) => {
    let result = 0
    for (var i of mentsu) {
      if (arrcomp(i, x)) {
        result++
      }
    }
    return result
}

// handからpaiを抜く
const pull = (hand: number[], pai: number[]) => {
    for (var i of pai) {
        hand.splice(hand.indexOf(i), 1)
    }
    return hand
}

// 2つの配列を比較して一致すればtrue
const arrcomp = (arr: number[], target: number[]): boolean => {
    if (arr.length !== target.length) {
        return false
    }
    else {
        for (let i = 0; i < arr.length; i++) {
            if (arr[i] !== target[i]) {
                return false
            }
        }
    }
    return true
}

長いですね。ツッコミ所さん満載かと思いますがやさしくしてください。

constructorの引数について説明しときます。

tehai: number[]・・・手牌13枚(ソート済)
agariHai: number・・・アガリ牌
zikaze: string・・・自風
kawaNum: number・・・河の枚数
tumoAgari: boolean・・・ツモアガリならtrue、ロンならfalse
mentsu: number[][]・・・4面子(ex. [[1,2,3], [2,3,4], [5,5,5], [7,8,9])
isriich: number・・・2ならダブリー、1なら立直、0ならダマです
ippatsu: boolean・・・一発かどうかの真偽値

mentsuに関しては本来、tehaiとagariHaiから、考えうる組み合わせをすべて探索して、その中で最も飜数の高いものを探さなくてはならないのですが、時間が足りなかったのでそれはまた今度書きます。ここではmentsuは一意ということにしておきます。ちなみに七対子のときはmentsu = []です。

ややこしそうな役について説明しておきます

平和
難スギィ!!

// 平和
    private pinhu = (hand: number[], agariHai: number, mentsu: number[][]) => {
        const hand_copy = hand.slice()
        let temp: number[] = []
        if (hand.indexOf(10) >= 0) {
            return false
        }
        else {
            // 面子はすべて順子か
            for (var j of mentsu) {
                if (count(j, j[0]) !== 1) {
                    return false
                }
            }
            // 両面待ちか
            for (var i of mentsu) {
                if (i.indexOf(agariHai) >= 0) {
                    const i_copy = i.slice()
                    pull(i_copy, [agariHai])
                    if ( ((i_copy[1] - i_copy[0]) === 1) && ((i_copy[0] - 1) >= 1) && ((i_copy[1] + 1) <= 9 )) {
                        this.yaku.push("平和")
                        this.hansuu++
                        return true
                    }
                }
            }
        }
    }

わかりにくいのは長ったらしいif文の条件式でしょうか

まず、面子のひとつひとつを見てアガリ牌が入っているものを見つけると、そいつをdeepコピー(オブジェクトへの参照ではなくオブジェクトそのものをコピーすること)します。(後の操作でもとの配列に変化を及ばせないためです) そのコピーからアガリ牌を抜きます。
具体的にいうとアガリ牌が3sだったときに、順子[3,4,5]があったとすると、この順子のコピーから5を抜いて[4,5]にします。
ここで問題の条件式を見ると、

(i_copy[1] - i_copy[0]) === 1

これは[a,b]が連続整数になっているかの判定です。これで嵌張待ち(アガリが3sなら[2,4])を除外します。次

((i_copy[0] - 1) >= 1) && (i_copy[1] + 1) <= 9 )

これは辺張待ち(ex. [1,2]や[8,9])を除外します。これは言葉よりも例を見た方が早いでしょう。

それでは実行してみましょう

yakuSearch.ts
const tehai = [1,1,1,2,2,3,3,7,7,8,8,9,9]
const agariHai = 1
const zikaze = '東'
const kawaNum = 1
const tumoAgari = true
const mentsu: number[][] = [[1,2,3], [1,2,3], [7,8,9], [7,8,9]]
const isriich = 2
const ippatsu = true


const agari = new Yaku(tehai, agariHai, zikaze, kawaNum, tumoAgari, mentsu, isriich, ippatsu)

for (var i of agari.yaku) {
    console.log(i)
}

if (agari.yakuman > 0) {
    switch (agari.yakuman) {
        case 1: {
            console.log('役満')
            break
        }

        case 2: {
            console.log('ダブル役満')
            break
        }

        case 3: {
            console.log('トリプル役満')
            break
        }
    
        default: {
            console.log(agari.yakuman + "倍役満")
            break
        }
    }
}
else {
    if (agari.hansuu > 0) {
        console.log(agari.hansuu + "飜")
    }
    else {
        console.log("役がありません。チョンボです")
    }
}

そもそもゲーム内で情報を渡してYakuクラスのインスタンスを作る前提なので、標準入力をサボりました。ごめんなさい。
tscコマンドでコンパイル
nodeコマンドで実行します。

結果

ダブル立直
一発
門前清自摸和
平和
清一色
二盃口
純混全帯幺九
17

一生に一度でいいからリアルで見てみたいものですね

感想

要素を探すときについPythonのノリでin演算子を使いましたが、jsのin文はPythonのそれと致命的に異なっていました。変態か。

まだ混一色程度ですがいずれ萬子や筒子もいれてこういったプログラムも組んでみたいです。

おわり

型があるとちゃんとエラー吐くし、コードみながら考えをまとめやすいのでTypeScriptは結構好きです。神では?

ところで明日はチノちゃんの誕生日ですね♪♪♪チノちゃんの誕生日である明日はhukuda222さんの記事です。お楽しみに!

我慢できねえ...少し早いですがここで言わせてください

チノちゃん誕生日おめでとう!!!!

opfer icon
この記事を書いた人
opfer

永遠の駆け出しプログラマ

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】 feature image
2018年11月3日
ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】
Azon icon Azon
2018年12月23日
LogicProXでのサラウンド設定,オーケストラ用テンプレ作成,その他の小ネタ
SolunaEureka icon SolunaEureka
2018年12月16日
ICPCアジア地区横浜大会参加記【アドベントカレンダー2018 52日目】
eiya icon eiya
2018年11月30日
Flutterでスマホアプリを作ってみ(た | よう)【アドベントカレンダー2018 37日目】
Fourmsushi icon Fourmsushi
2018年12月23日
線形解読法
nari icon nari
2018年12月3日
ハル研究所プログラミングコンテスト 2018に参加しました[アドベントカレンダー2018 40日目]
ninja icon ninja
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記