makeplex salon : 麻雀プログラム

久しぶりのブログ更新。よく見ると半年近く前に「再開します」とか高らかに宣言してるけど速攻で終了してました。僕はそういう人間です。

以前チラっと見て「あー俺こんなの無理っす無理っすムリポインザーギっす」とかほざいて何も手をつけなかったのだけど id:cou929_la さんがやってるのに刺激を受けてやってみました。

あなたのスキルで飯は食えるか? 史上最大のコーディングスキル判定 (1/2) - ITmedia エンタープライズ

自分の場合は方針決めに40分程度、コーディングに3時間ちょっと、計4時間弱かかりました。制限時間は3時間とのことで余裕でオーバーです。自分の実力はこんなもんだ、ということで要修行ですね。

問題

13枚の手牌が与えられたときにテンパイなら待ちを全て出力、テンパイしてないなら何も出力しない。萬子のチンイツ限定でいいとのこと。

解き終わってから他の方の解答を見てたら七対子考慮してたりしましたけど僕はアホなので一切そういうことを考えずに実装しました。今日実装してみようと思いましたが、そのうち気が向いたらやることにします。

方針

大きな方針としては、与えられた手牌から刻子、順子になり得る牌を候補として列挙して、候補を組み合わせて待ちを絞り込んでいきます。

七対子を除外した場合、待ちは

  • 刻子or順子*4 + 単騎待ち
  • 刻子or順子*3 + アタマ + リャンメンorペンチャン待ち
  • 刻子or順子*3 + アタマ + シャボ待ち

となります。

なのでまず列挙した候補の中から刻子or順子を4つ選ぶ場合と3つ選ぶ場合に場合分けして、それぞれで手牌の数を超えないかチェックしていきます。4つの場合は手牌の数を超えなければ残りの牌を待ちとして出力。3つの場合はさらにアタマの有無をチェックして、頭があってかつリャンメンorペンチャン又はシャボ待ちになっているかを調べます。

言語はできないなりに得意な言語、ということでPerlで書きました。

あとテストデータは適当にランダムに作ってやれー!と思ってたらほとんどテンパイじゃないものばかりで何の役にも立ちませんでした。

プログラム

majong.pl

#!/usr/bin/env perl

use strict;
use warnings;

# 入力
my $input = shift;
my @tehai = (0, 0, 0, 0, 0, 0, 0, 0, 0);
my @machi = ();

# 'random'が引数にくるとランダムな手牌を生成
if ($input eq 'random') {
    my $hai_num = 0;

    while ($hai_num < 13) {
        my $hai = int(rand(9));

        if ($tehai[$hai] < 4) {
            $tehai[$hai]++;
            $hai_num++;
        }
    }
}
else {
    my @sample = split //, $input;
    for (my $i=0; $i<@sample; $i++) {
        $tehai[$sample[$i]-1]++;
    }
}

print "tehai:\t";
for (my $i=0; $i<@tehai; $i++) {
    for (my $j=0; $j<$tehai[$i]; $j++) {
        print $i+1;
    }
}
print "\n";

my @candi = &getCandidate(\@tehai);
my @tmp_tehai = (0, 0, 0, 0, 0, 0, 0, 0, 0);

for (my $i=0; $i<@candi; $i++) {
    for (my $j=$i+1; $j<@candi; $j++) {
        for (my $k=$j+1; $k<@candi; $k++) {

            # 順子・刻子が4つの場合
            for (my $l=$k+1; $l<@candi; $l++) {
                my @tmp_hai = split //, $candi[$i] . $candi[$j] . $candi[$k] . $candi[$l];

                for (my $l=0; $l<@tmp_hai; $l++) {
                    $tmp_tehai[$tmp_hai[$l]-1]++;
                }

                # 入力の牌の数を超えていないかチェック
                my $check = 1;
                my $machi;
                for (my $l=0; $l<9; $l++) {
                    $tmp_tehai[$l] = $tehai[$l] - $tmp_tehai[$l];
                    if ($tmp_tehai[$l] < 0) {
                        $check = 0;
                    }
                    elsif ($tmp_tehai[$l] > 0) {
                        $machi = $l + 1;
                    }
                }

                if ($check) {
                    print "($candi[$i])($candi[$j])($candi[$k])($candi[$l])[$machi]\n";
                }

                @tmp_tehai = (0, 0, 0, 0, 0, 0, 0, 0, 0);
            }

            # 順子・刻子が3つの場合
            my @tmp_hai = split //, $candi[$i] . $candi[$j] . $candi[$k];

            for (my $l=0; $l<@tmp_hai; $l++) {
                $tmp_tehai[$tmp_hai[$l]-1]++;
            }

            # 入力の牌の数を超えていないかチェック
            my $check = 1;
            for (my $l=0; $l<9; $l++) {
                $tmp_tehai[$l] = $tehai[$l] - $tmp_tehai[$l];
                if ($tmp_tehai[$l] < 0) {
                    $check = 0;
                }
            }
            
            if ($check) {
                my ($head, $wait) = &getHead(\@tmp_tehai);

                if ($head && $wait) {
                    for (my $l=0; $l<@$head; $l++) {
                        for (my $m=0; $m<@$wait; $m++) {
                            my @tmp_hai2 = split //, $$head[$l] . $$wait[$m];
                            my @tmp_tehai2 = (0, 0, 0, 0, 0, 0, 0, 0, 0);

                            for (my $n=0; $n<@tmp_hai2; $n++) {
                                $tmp_tehai2[$tmp_hai2[$n]-1]++;
                            }

                            # 入力の牌の数を超えていないかチェック
                            for (my $n=0; $n<9; $n++) {
                                $tmp_tehai2[$n] = $tmp_tehai[$n] - $tmp_tehai2[$n];
                                
                                if ($tmp_tehai2[$n] < 0) {
                                    $check = 0;
                                }
                            }
                            
                            print "($candi[$i])($candi[$j])($candi[$k])($$head[$l])[$$wait[$m]]\n" if ($check);
                        }
                    }
                }
                
                if (@$head == 2) {
                    print "($candi[$i])($candi[$j])($candi[$k])($$head[0])[$$head[1]]\n";
                    print "($candi[$i])($candi[$j])($candi[$k])($$head[1])[$$head[0]]\n";
                }
            }

            @tmp_tehai = (0, 0, 0, 0, 0, 0, 0, 0, 0);
        }
    }
}

# 順子、刻子候補の取得
sub getCandidate {
    my @tehai = @{shift()};
    my @candi = ();

    # 刻子
    for (my $i=0; $i<9; $i++) {
        if ($tehai[$i] >= 3) {
            push @candi, $i+1 . $i+1 . $i+1;
        }
    }

    # 順子
    for (my $i=0; $i<7; $i++) {
        if ($tehai[$i] != 0 &&
            $tehai[$i+1] != 0 &&
            $tehai[$i+2] != 0) {
            my @list = ($tehai[$i], $tehai[$i+1], $tehai[$i+2]);
            my $min = &getMin(\@list);
            for (my $j=0; $j<$min; $j++) {
                push @candi, $i+1 . $i+2 . $i+3;
            }
        }
    }

    return @candi;
}

# 対子と待ちの取得
sub getHead {
    my @tehai = @{shift()};
    my @head = ();
    my @wait = ();
    
    for (my $i=0; $i<9; $i++) {
        # 対子
        if ($tehai[$i] >= 2) {
            push @head, $i+1 . $i+1;
        }

        # 待ち        
        # リャンメンorペンチャン
        if ($i < 8 &&
            $tehai[$i] != 0 &&
            $tehai[$i+1] != 0) {
            my @list = ($tehai[$i], $tehai[$i+1]);
            my $min = &getMin(\@list);
            for (my $j=0; $j<$min; $j++) {
                push @wait, $i+1 . $i+2;
            }
        }
        # カンチャン
        elsif ($i < 7 &&
               $tehai[$i] != 0 &&
               $tehai[$i+2] != 0) {
            my @list = ($tehai[$i], $tehai[$i+2]);
            my $min = &getMin(\@list);
            for (my $j=0; $j<$min; $j++) {
                push @wait, $i+1 . $i+3;
            }
        } 
    }

    return \@head, \@wait;
}

sub getMin {
    my @list = @{shift()};

    my $min = shift @list;

    for my $v (@list) {
        $min = $v if ($min > $v);
    }

    return $min;
}

問題点

実はこのプログラムには重大なバグがありまして、それは待ちを重複して出力してしまうような入力が存在する、ということです。

perl majong.pl 1223344888999
tehai:	1223344888999
(888)(999)(123)(234)[4]
(888)(999)(123)(234)[4]
(888)(999)(123)(44)[23]
(888)(999)(234)(234)[1]

実は候補を列挙するときに重複する要素(この場合234)は別の候補として列挙しちゃってるので、こういう入力の場合は重複して出力してしまいます。昨晩および今晩の段階では有効な解決法を見出せなかったので放置してます。候補を配列じゃなくてハッシュにすればなんとかなるかもしれません。

感想

方針を考えてるときは案外いけるんじゃないかと思ってましたが、いざ実装に入ると自分にコーディング能力がないことがよーくわかりました。しかし、こういうパズル的プログラミングが自分には非常に面白かったです。ネットで色々問題見つけて挑戦したいと思います。C++を勉強してTopCoderに挑戦したいなあ。