山傘のプログラミング勉強日記

プログラミングに関する日記とどうでもよい雑記からなるブログです。

天鳳のログから和了データを抽出する その3

和了データの抽出

前回では、INIT タグから場風と親か子であるかという情報を抜き出しました。今回は、AGARI タグから和了データを抜き出します。

データ形式

麻雀の点数計算のアルゴリズムの正当性を確かめるために和了データを使いたいので、一行で和了データを表すことを考えます。

今回は次のようなデータ形式に変換することを考えます。データはすべて数字で空白区切りであるとします。

Bakaze Jikaze Parent Tumo Hu Han Point AgariHai Hai Huuro Yaku Dara UraDora
牌の数値化

萬子は 1~9 で表し、赤五萬は 0 とします。筒子は 11~19で表し、赤⑤は 10 とします。索子は 21~29 で表し、赤Ⅴは 20 とします。東南西北は 31~34 とし、白發中は 35~36 とします。

Bakaze

風牌を表します。例えばこの値が 31 なら場風は東となります。

Jikaze

自風を表します。

Parent

この値が 1 なら親で 0 なら子を表します。

Tumo

この値が 1 なら自摸上がりで 0 ならロン上がりを表します。

Hu Han Point

それぞれ、符、翻、点数を表します。基本的に、5翻以上のときも符の値は存在しますが、国士無双のときは0となります。

AgariHai

和了した牌を表します。

Hai

手牌を表します。副露した牌はここには含まれませんが、上がり牌は含まれます。手牌の数を  n とし、 i 番目の牌の値を  h_i とすると、

 n \ h_1 \ h_2 \  \cdots \ h_n

のように表します。また、 n = 0 となることはありません。

Huuro

副露している牌を表します。副露数を  m とすると、

 m \ f_1 \ f_2 \  \cdots \ f_n

となります。 i 番目の副露した面子を  f_i とすると、

type kind id red

と表します。

type は 0, 1, 2 の値を表し、0 なら順子、1 なら刻子、2 なら槓子とします。

kind は 0 なら暗槓以外(ポン、チー、大明槓、加槓)、1 なら暗槓とします。

id は面子の中で最小の牌の値とします。例えば、678で面子を晒しているとき、id の値は 6 となります。刻子槓子はその牌の値となります。

red は副露した面子に赤牌が含まれるときは、1 であり、0 のときは含まれません。

Yaku

役に対応する値と翻数を表します。上がりにおける役の数を  l とすると、

 l \ y_1\ y_2 \ \cdots \ y_l

となります。 i 番目の役を  y_i とすると、

id han

となります。id は役に対応する値で、han はその役の翻数となります。また、役満では han の値は 1 とします。役の対応は、

www.barbaroiware.net

が詳しいです。

Dora UraDora

Dora は表ドラと槓ドラを表します。UraDora は裏ドラを表します。Dora の数を  o とすると、

 o \ h_1 \ h_2 \  \cdots \ h_o

と表します。UraDora についても同様ですが、リーチしていないときは、[tex; o = 0] となり、以降に数値はありません。

面子の解析

AGARI タグの 'm' という属性が副露した面子の情報を表しています。この情報は、16ビットで解釈されているので、上記の面子のデータ形式に変換する必要があります。

実際には、AGARI タグの副露の情報は10進数で表現されているので、その値を  n とします。

天鳳では牌にそれぞれの値がついていて、0~133までのIDが振り分けられています。赤牌は、16、52、88となります。

順子の場合

ビット 情報
1-2 誰から鳴いたか
3 必ず1
4-5 牌1のID
6-7 牌2のID
8-9 牌3のID
11-16 順子のパターン

3ビット目が1のときが順子なので、Python でこれを確認するには、

n & (1<<2) 

の値が0でなければ、3ビット目が1であると確認できます。次に順子の中で最小の牌の値を求めます。これは、11~16ビットから計算できます。順子のパターンは、123~789の7パターンあり、萬子、筒子、索子とどの牌を鳴いたかを合わせると、 7 \times 3 \times 3 = 63 通りあります。このパターンを bit とすると、次のように計算できます。

        bit = 0
        for i in range(6):
            if n & (1<<(10 + i)):
                bit += (1<<i)

この値を牌の値に変換するには、次のようになります。

        bit //= 3
        if bit <= 6: bit += 1
        elif bit <= 13: bit += 4
        else: bit += 7

3で割っているのは、どの牌を鳴いたかという情報が必要ないためです。

鳴いた牌に赤牌が含まれるかどうかは、345, 456, 567 の順子のパターンを調べれば良く、

        if bit % 10 == 3 or bit % 10 == 4 or bit % 10 == 5:
            # 345 456 567
            if (n & (1<<(7 - 2 * (bit % 10 - 3)))) == 0 and (n & (1<<(8 - 2 * (bit % 10 - 3)))) == 0:
                is_red = 1

と計算すれば良いです。

刻子

順子のときと同様に調べれば良いです。刻子槓子の場合も合わせると、次のようになります。

def get_mentu(n):
    if n & (1<<2): # 順子
        bit = 0
        for i in range(6):
            if n & (1<<(10 + i)):
                bit += (1<<i)
        
        bit //= 3
        if bit <= 6: bit += 1
        elif bit <= 13: bit += 4
        else: bit += 7
        is_red = 0
        if bit % 10 == 3 or bit % 10 == 4 or bit % 10 == 5:
            # 345 456 567
            if (n & (1<<(7 - 2 * (bit % 10 - 3)))) == 0 and (n & (1<<(8 - 2 * (bit % 10 - 3)))) == 0:
                is_red = 1
        return '%d %d %d %d' % (0, 0, bit, is_red)
    else: # 刻子
        if n & (1<<(3)) or n & (1<<(4)): # 刻子 or 加槓
            mentu_type = 1 # 刻子
            if n & (1<<(4)): mentu_type = 2 # 槓子(加槓)
            bit = 0
            for i in range(7):
                if n & (1<<(9 + i)):
                    bit += (1<<i)
            bit //= 3
            if bit <= 8: bit += 1
            elif bit <= 17: bit += 2
            elif bit <= 26: bit += 3
            else: bit += 4
            is_red = 0
            if mentu_type == 2 and bit % 10 == 5 and bit <= 25: is_red = 1
            if mentu_type == 1 and bit % 10 == 5 and bit <= 25 and (n & (1<<5) != 0 or n & (1<<6) != 0):
                is_red = 1
            return '%d %d %d %d' % (mentu_type, 0, bit, is_red)
        else: # 暗槓 or 大明槓
            is_red = 0
            mentu_type = 2
            mentu_kind = 0
            if n & 1 == 0 and n & (1<<1) == 0:
                mentu_kind = 1 # 暗槓
            bit = 0
            for i in range(8):
                if n & (1<<(i + 8)):
                    bit += 1<<i
            bit //= 4
            if bit <= 8: bit += 1
            elif bit <= 17: bit += 2
            elif bit <= 26: bit += 3
            else: bit += 4

            if bit % 10 == 5 and bit <= 25: is_red = 1
            return '%d %d %d %d' % (mentu_type, mentu_kind, bit, is_red)

INIT タグと AGARI タグからテストデータを生成

INIT タグとAGARIタグのデータから上記のデータ形式に変換するプログラムです。

import os
import re
import glob
script_dir = script_dir = os.path.abspath(os.path.dirname(__file__))

# 牌のIDを種類に変換 萬子(1~9) 筒子(11~19 索子(21~29) 東南西北(31,32,33,34) 白發中(35,36,37)
# 赤は 0 10 20
hai_to_kind = [0] * 136

def init_hai_to_kind():
    for i in range(9):
        for j in range(4):
            hai_to_kind[i * 4 + j] = i + 1
            hai_to_kind[i * 4 + j + 36] = 10 + i + 1
            hai_to_kind[i * 4 + j + 72] = 20 + i + 1
    for i in range(7):
        for j in range(4):
            hai_to_kind[i * 4 + j + 108] = 31 + i
    
    hai_to_kind[16] = 0
    hai_to_kind[52] = 10
    hai_to_kind[88] = 20

init_hai_to_kind()

def get_mentu(n):
    if n & (1<<2): # 順子
        bit = 0
        for i in range(6):
            if n & (1<<(10 + i)):
                bit += (1<<i)
        
        bit //= 3
        if bit <= 6: bit += 1
        elif bit <= 13: bit += 4
        else: bit += 7
        is_red = 0
        if bit % 10 == 3 or bit % 10 == 4 or bit % 10 == 5:
            # 345 456 567
            if (n & (1<<(7 - 2 * (bit % 10 - 3)))) == 0 and (n & (1<<(8 - 2 * (bit % 10 - 3)))) == 0:
                is_red = 1
        return '%d %d %d %d' % (0, 0, bit, is_red)
    else: # 刻子
        if n & (1<<(3)) or n & (1<<(4)): # 刻子 or 加槓
            mentu_type = 1 # 刻子
            if n & (1<<(4)): mentu_type = 2 # 槓子(加槓)
            bit = 0
            for i in range(7):
                if n & (1<<(9 + i)):
                    bit += (1<<i)
            bit //= 3
            if bit <= 8: bit += 1
            elif bit <= 17: bit += 2
            elif bit <= 26: bit += 3
            else: bit += 4
            is_red = 0
            if mentu_type == 2 and bit % 10 == 5 and bit <= 25: is_red = 1
            if mentu_type == 1 and bit % 10 == 5 and bit <= 25 and (n & (1<<5) != 0 or n & (1<<6) != 0):
                is_red = 1
            return '%d %d %d %d' % (mentu_type, 0, bit, is_red)
        else: # 暗槓 or 大明槓
            is_red = 0
            mentu_type = 2
            mentu_kind = 0
            if n & 1 == 0 and n & (1<<1) == 0:
                mentu_kind = 1 # 暗槓
            bit = 0
            for i in range(8):
                if n & (1<<(i + 8)):
                    bit += 1<<i
            bit //= 4
            if bit <= 8: bit += 1
            elif bit <= 17: bit += 2
            elif bit <= 26: bit += 3
            else: bit += 4

            if bit % 10 == 5 and bit <= 25: is_red = 1
            return '%d %d %d %d' % (mentu_type, mentu_kind, bit, is_red)

def get_testdata(path):
    test_data = []
    with open(path, 'r', encoding='utf_8') as f:
        line1 = f.readline()
        cnt = 0
        while line1:
            cnt += 1
            line2 = f.readline()
            line1 = line1.strip()
            line1 = re.sub('["<>/]', '', line1)
            data1 = line1.split(' ')
            dic1 = dict()
            for d in data1:
                v = d.split('=')
                if len(v) == 1: continue
                dic1[v[0]] = list(map(int, v[1].split(',')))
            
            line2 = line2.strip()
            line2 = re.sub('["<>/]', '', line2)
            data = line2.split(' ')
            dic = dict()
            for d in data:
                v = d.split('=')
                if len(v) == 1: continue
                if v[0] == 'owari': continue
                dic[v[0]] = list(map(int, v[1].split(',')))
            bakaze = dic1['seed'][0] / 4 + 31
            jikaze = (dic['who'][0] - dic1['oya'][0] + 4) % 4 + 31
            tumo = 0
            if dic['fromWho'] == dic['who']: tumo = 1
            parent = 0
            if dic1['oya'] == dic['who']: parent = 1
            han = 0
            hu = 0
            yaku_list = []
            if 'yaku' in dic:
                hu = dic['ten'][0]
                han = sum(dic['yaku'][1::2])
                yaku_list = dic['yaku']
                
            if 'yakuman' in dic: # 役満
                for yakuman in dic['yakuman']:
                    yaku_list.append(yakuman)
                    yaku_list.append(1)
            tmp = '%d %d %d %d %d %d %d %d' % (bakaze, jikaze, parent, tumo, hu, han, dic['ten'][1], hai_to_kind[dic['machi'][0]])
            tmp += ' ' + str(len(dic['hai']))
            for hai in dic['hai']:
                tmp += ' ' + str(hai_to_kind[hai])
            if 'm' in dic:
                tmp += ' ' + str(len(dic['m']))
                for m in dic['m']:
                    tmp += ' ' + get_mentu(m)
            else: tmp += ' 0'
            
            tmp += ' ' + str(len(yaku_list) // 2)
            for yaku in yaku_list:
                tmp += ' ' + str(yaku)

            tmp += ' ' + str(len(dic['doraHai']))
            for d in dic['doraHai']:
                tmp += ' ' + str(hai_to_kind[d])

            if 'doraHaiUra' in dic:
                tmp += ' ' + str(len(dic['doraHaiUra']))
                for d in dic['doraHaiUra']:
                    tmp += ' ' + str(hai_to_kind[d])
            else: tmp += ' 0'
            
            test_data.append(tmp)
            line1 = f.readline()
    return test_data