[PYTHON] Mahjong winning judgment algorithm

Mahjong winning judgment algorithm

It is not an algorithm to find the number of listenings, which checks whether the completion of 14 Tehai is completed.

data form

ONE-HOT is used for the tile data. ONE-HOT is selected because it is easy to judge the head and engraving if the sum of the ONE-HOT array is taken in the row direction.

# ONE-Tehai of HOT expression
[
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
]

#Take the sum in the row direction
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 1, 1, 1, 4, 1, 1, 0, 0, 0, 0, 0, 0, 0]
#3 or more parts will be used to judge the engraving
# [1, 1, 1]Fold it in and use 3 or more parts to judge Junko.
#There are advantages such as (I think)

Inspection methods

  1. Inspect all patterns of the "head" (outermost loop)
  2. Inspect all patterns of "engraving" with the rest after removing the "head" (inner loop)
  3. Inspect "Junko" with the remaining "head" and "engraver" removed.

Follow the above procedure to check if the winning is completed.

Source code


import itertools
import multiprocessing
import numpy as np
import os
import sys
import time

# m1-m9, p1-p9, s1-s9, dw, dg, dr, we, ww, ws, wn
#Three-element tile = Dragon
#Wind tile = Wind
tileKeyIndex = [
    "m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", 
    "p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9", 
    "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", 
    "dw", "dg", "dr",
    "we", "ww", "ws", "wn", 
]

MTileBits = [
    1, 1, 1, 1, 1, 1, 1, 1, 1, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0,
    0, 0, 0, 0
]

PTileBits = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    1, 1, 1, 1, 1, 1, 1, 1, 1, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0,
    0, 0, 0, 0
]

STileBits = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    1, 1, 1, 1, 1, 1, 1, 1, 1, 
    0, 0, 0,
    0, 0, 0, 0
]

DTileBits = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    1, 1, 1,
    0, 0, 0, 0
]

WTileBits = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0,
    1, 1, 1, 1
]

KokusiBits = [
    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, 1, 1, 1
]

KokusiBits = np.array(KokusiBits)

# m1m2m3m4m5m6m7m8m9s1s2s3wnwn
def parseTehai(s):
    if len(s) != 28:
        print("error in {}, len(s)={}".format(sys._getframe().f_code.co_name, len(s)))
        sys.exit()
    tileMatrix = np.zeros((14, len(tileKeyIndex)))
    for i in range(14):
        pos = i * 2
        idx = tileKeyIndex.index(s[pos:pos + 2])
        tileMatrix[i][idx] = 1
    return tileMatrix

def isShuntsuCompleted(tileMatrix):
    indexes = []
    for tbits in [MTileBits, PTileBits, STileBits]:
        target = tileMatrix * tbits
        while True:
            targetB = (target != 0).astype(int) # [1, 1, 2]Convert all to 1 to avoid arrays like
            b = np.convolve(targetB, [1, 1, 1], mode="valid")
            if np.max(b) != 3:
                break
            idxs = np.where(b == 3)[0]
            idx = idxs[0]
            target[idx:idx + 3] -= 1 #Remove the inspected tiles
            indexes = indexes + list(np.arange(idx, idx + 3, 1))
    return indexes

def isCompleted(tileMatrix):
    rowSum = np.sum(tileMatrix, axis=0)
    headerIdxs = np.where(rowSum >= 2)[0]
    atama, kotsu, shuntsu = [], [], []
    
    #Cheet Toys
    if len(headerIdxs) == 7:
        return 1, list(headerIdxs) * 2, [], []
    
    #Kokushi
    kokusiCheck = (rowSum != 0).astype(int)
    if np.sum(kokusiCheck * KokusiBits) == 13 and np.sum(rowSum * KokusiBits) == 14:
        return 1, np.where(np.array(KokusiBits) == 1)[0], [], []
    
    #Fix the head
    #All patterns of engraving are put out in advance, and Junko is inspected by fixing each pattern.
    for hidx in headerIdxs:
        #Make a copy so you don't manipulate the original array
        calcBuffer = np.array(rowSum)
        
        #Get rid of the head
        calcBuffer[hidx] -= 2
        
        #Detect all possible engraving
        kotsuPos = np.where(calcBuffer >= 3)[0]
        
        #Only one of the detected engravings is valid, only two of the detected engravers are valid ... Create all patterns of all detected engravings
        kotsuPatterns = []
        for i in range(len(kotsuPos)):
            comb = list(itertools.combinations(kotsuPos, i + 1))
            kotsuPatterns = kotsuPatterns + comb
        #Add a pattern for which no engraving is valid
        kotsuPatterns.append(None)
        
        for kotsuIndexes in kotsuPatterns:
            #Make a copy so you don't manipulate the original array
            calcBuffer2 = np.array(calcBuffer)
            if isinstance(kotsuIndexes, type(None)):
                pass
            else:
                #Remove the engraving
                for kidx in kotsuIndexes:
                    calcBuffer2[kidx] -= 3
            #Junko
            shuntsuIndexes = isShuntsuCompleted(calcBuffer2)
            for idx in shuntsuIndexes:
                #Get rid of Junko
                calcBuffer2[idx] -= 1
            
            #If there are no tiles left after removing the head, engraving, and Junko, it is complete.
            #print("np.sum(calcBuffer)", np.sum(calcBuffer2))
            if np.sum(calcBuffer2) == 0:
                atama.append(np.full(2, hidx))
                kotsu.append(kotsuIndexes)
                shuntsu.append(shuntsuIndexes)
    
    return len(atama), atama, kotsu, shuntsu

def Test1():
    #2333345677778
    #2333344567888
    #2345666777888
    #3344455566777
    #2223344455677
    #1112345556677
    #4556677888999
    
    #Waiting for 1425869
    #Waiting for 14725869
    #Waiting for 1245678
    #Waiting for 36258
    #Waiting for 6257
    #Waiting for 672583
    #Waiting for 789436
    
    #tileMatrix = parseTehai("m1m2m3m4m5m6m7m8m9s1s2s3wnwn")
    #tileMatrix = parseTehai("wewewewwwwwwwswswsm9m9m9s1s1")
    #tileMatrix = parseTehai("s2s3s3s3s3s4s5s6s7s7s7s7s8s9") # s1, s2, s4, s5, s6, s8, s9
    #tileMatrix = parseTehai("m2m3m3m3m3m4m4m5m6m7m8m8m8m1") # 
    #tileMatrix = parseTehai("m2m3m4m5m6m6m6m7m7m7m8m8m8?")
    #tileMatrix = parseTehai("m3m3m4m4m4m5m5m5m6m6m7m7m7?")
    #tileMatrix = parseTehai("p2p2p2p3p3p4p4p4p5p5p6p7p7?")
    #tileMatrix = parseTehai("p1p1p1p2p3p4p5p5p5p6p6p7p7?")
    #tileMatrix = parseTehai("p4p5p5p6p6p7p7p8p8p8p9p9p9?")
    tileMatrix = parseTehai("m1m9p1p9s1s9wewswwwndwdgdrm1")
    completeCount, atama, kotsu, shuntsu = isCompleted(tileMatrix)
    if completeCount > 0:
        print("OK")
        print(atama)
        print(kotsu)
        print(shuntsu)
    else:
        print("NG")

def tileMatrixToTehaiString(tileMatrix):
    s = ""
    for r in tileMatrix:
        idx = np.where(r == 1)[0][0]
        s += tileKeyIndex[idx]
    return s

def appendFile(fileName, data):
    with open(fileName, mode="a") as f:
        f.write(data + "\n")

def TenhohTestSub(args):
    seed = time.time()
    seed = int((seed - int(seed)) * 10000000)
    np.random.seed(seed)
    instanceId, tryCount = args
    size = len(tileKeyIndex)
    allTile = []
    for i in range(size):
        tmp = [0] * size
        tmp[i] = 1
        for n in range(4):
            allTile.append(tmp)
    for i in range(tryCount):
        np.random.shuffle(allTile)
        tiles = np.array(allTile[:14])
        completeCount, atama, kotsu, shuntsu = isCompleted(tiles)
        if completeCount > 0:
            tehaiStr = tileMatrixToTehaiString(tiles)
            appendFile("tenhoh_{}.txt".format(instanceId), tehaiStr)

def TenhohTest():
    #TenhohTestSub(1, 400000)
    tryCount = 1000000
    
    args = []
    for i in range(4):
        args.append([i, tryCount])
    
    with multiprocessing.Pool(4) as p:
        p.map(TenhohTestSub, args)

def main():
    #Test1()
    TenhohTest()

if __name__ == "__main__":
    main()
# python main.py

How to use the source code

def main():
    #Test1()
    TenhohTest()

Test1 () inspects the manually prepared score in the source code. In TenhohTest (), 4 cores are used to randomly create a score 1 million times per core, and if it is a winning form, a record is taken. The record is recorded with a number for each core such as "tenhoh_0.txt". ..

Is it Tenwa by using the image conversion program added below? You can visualize the score you have made.

Imaging program for syllabary text

We will publish a program to image text, how to use it will be described later.


import PIL.Image
import os
import sys

tileKeyIndex = [
    "m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", 
    "p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9", 
    "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", 
    "dw", "dg", "dr",
    "we", "ww", "ws", "wn", 
]

haiImageNames = [
    "p_ms1_1.gif", "p_ms2_1.gif", "p_ms3_1.gif", "p_ms4_1.gif", "p_ms5_1.gif", "p_ms6_1.gif", "p_ms7_1.gif", "p_ms8_1.gif", "p_ms9_1.gif", 
    "p_ps1_1.gif", "p_ps2_1.gif", "p_ps3_1.gif", "p_ps4_1.gif", "p_ps5_1.gif", "p_ps6_1.gif", "p_ps7_1.gif", "p_ps8_1.gif", "p_ps9_1.gif", 
    "p_ss1_1.gif", "p_ss2_1.gif", "p_ss3_1.gif", "p_ss4_1.gif", "p_ss5_1.gif", "p_ss6_1.gif", "p_ss7_1.gif", "p_ss8_1.gif", "p_ss9_1.gif", 
    "p_no_1.gif", "p_ji_h_1.gif", "p_ji_c_1.gif",
    "p_ji_e_1.gif", "p_ji_w_1.gif", "p_ji_s_1.gif", "p_ji_n_1.gif", 
]

def parseTehai(s):
    if len(s) != 28:
        print("error in {}, len(s)={}".format(sys._getframe().f_code.co_name, len(s)))
        sys.exit()
    indexes, tehai = [], []
    for i in range(14):
        pos = i * 2
        idx = tileKeyIndex.index(s[pos:pos + 2])
        indexes.append(idx)
        tehai.append(s[pos:pos + 2])
    return indexes, tehai

def enumFile():
    files = []
    for v in os.listdir("./"):
        if os.path.isfile(v) and v.startswith("tenhoh_"):
            files.append(v)
    return files

def readFile(fileName):
    with open(fileName, "r") as f:
        return f.read()

def tileIndexesToImage(indexes):
    images = []
    for idx in indexes:
        imageFile = os.path.join("./images", haiImageNames[idx])
        im = PIL.Image.open(imageFile)
        images.append(im)
    imageWidth = 0
    maxHeight = 0
    for im in images:
        imageWidth += im.width
        if im.height > maxHeight:
            maxHeight = im.height
    dst = PIL.Image.new('RGB', (imageWidth, maxHeight))
    for i, im in enumerate(images):
        dst.paste(im, (im.width * i, 0))
    return dst

def main():
    files = enumFile()
    for f in files:
        lines = readFile(f).split("\n")
        basename = os.path.basename(f)
        basename, _ = os.path.splitext(basename)
        for j, l in enumerate(lines):
            if len(l) < 28:
                continue
            indexes, tehai = parseTehai(l)
            indexes = sorted(indexes)
            image = tileIndexesToImage(indexes)
            destFile = "{}_{:03d}.png ".format(basename, j)
            destFile = os.path.join("./dest", destFile)
            image.save(destFile)

if __name__ == "__main__":
    main()
# https://mj-king.net/sozai/
# python tehai_2_image.py

How to use the imaging program

The "tenhoh_???. Txt" file in the same folder is automatically read and the image is output to ./dest based on the image in ./images.

** m7s5p2p6s7p4m6p7s6p5m5p2p5p6 ** ↓ tenhoh_2_000.png Sort and convert the image in this way.

Download and expand the image of Mahjong Kingdom

Unzip the image data downloaded from "Manko 2", "Tsutsuko 2", "Ryoko 2", and "Character tile 2" to ./images.

画像フォルダ構成.PNG

The folder structure looks like this, D: \ tmp is the program folder.

Create a dest folder

フォルダ構成.PNG

Create a folder for output in advance

Run

python tehai_2_image.py

If executed normally, the imaged score will be output to ./dest.

that's all.

Recommended Posts

Mahjong winning judgment algorithm