麻雀問題解いてみた

4月 4th, 2010 by tune Leave a reply »

Mahjong set [04/04]
Creative Commons License photo credit: drdaeman

makeplex salon:あなたのスキルで飯は食えるか? 史上最大のコーディングスキル判定 (1/2) – ITmedia エンタープライズを解いてみました。ソースはtune’s itmedia_makeplex at master – GitHubに上げてありますのでリビジョンも見れるようになってます。最初のコミットを上げるまでに、面子の表示順の違いを抑えるところで詰まってチンイツの待ちを出力するプログラム – 107143955443560を参考にしました。参考にしましたというよりもC++のソースをRubyに移植しましたと言う方が近いほど同じ処理です。基本的なアルゴリズムは最後まで踏襲しています。

最初のコミットまでが3時間ぐらい、リファクタリングに1時間ぐらいです。ちょっと時間がかかりすぎですが、日常的に書いてないRubyで、日常的にプログラムしてない家のMacパソコン、プラグインが古いのか5分に1回落ちてしまうMacVimで時間がとられたと言い訳しておきます。ただの実力不足です、ごめんなさい。

成立する可能性がある面子を減らしながら、総当たりで探索しています。面子の順序違いによる重複表示を避けるのが結構面倒ですね。刻子を優先して探さなければならず、色々試行錯誤してみたのですが、参考にさせていただいたid:staebchenさんよりも良さそうな方法に達しませんでした。もっといいやり方があると思いますが、参考まで。

#!/usr/bin/ruby

class Array
  def sum
    s = 0
    self.each do |v|
      s += v
    end
    return s
  end
end

class Fixnum
  # 刻子
  def anko
    num = self+1
    "(#{num},#{num},#{num})"
  end

  # 順子
  def shuntsu
    num = self+1
    "(#{num},#{num+1},#{num+2})"
  end

  # アタマ
  def atama
    num = self+1
    "(#{num},#{num})"
  end

  # 単騎
  def tanki
    num = self+1
    "[#{num}]"
  end

  # リャンメン(ペンチャンも兼ねる)
  def ryanmen
    num = self+1
    "[#{num},#{num+1}]"
  end

  # カンチャン
  def kanchan
    num = self+1
    "[#{num},#{num+2}]"
  end

  # シャボ
  def shabo
    num = self+1
    "[#{num},#{num}]"
  end
end

#
# 雀牌を表す数字列を解析し、0〜8の配列に与えられた個数を入れて返す
#
def parsePai(str)
  pai = Array.new(9, 0)
  str.split(//).each do |p|
    pai[p.to_i-1] += 1
  end

  # 入力の妥当性チェック
  if pai.sum != 13 then
    # 与えられる牌は13枚
    raise RuntimeError
  else
    pai.each do |val|
      if val > 4 then
        # 1種類の牌が4枚より多いことはない
        raise RuntimeError
      end
    end
  end

  return pai
end

#
# 手牌を再帰的に解析し待ちを表示する
#
# 順子を刻子に優先して探索する。
# 順子はfromが9〜18の時に探索される
#
def analyzeTehai(pai, from=0, mentsu=[])
  shuntsu_phase = 9
  skip_anko_check = false

  if from >= 9 then
    skip_anko_check = true
    from -= shuntsu_phase
  end

  if pai.sum > 4 then
    # 面子を揃える

    # 刻子
    unless skip_anko_check then
      from.upto(8).each do |i|
        if pai[i] >= 3 then
          tmp = pai.dup
          tmp[i] -= 3
          analyzeTehai(tmp, i+1, mentsu+[i.anko])
        end
      end
    end

    # 順子
    from.upto(6).each do |i|
      if pai[i]>=1 && pai[i+1]>=1 && pai[i+2]>=1 then
        tmp = pai.dup
        tmp[i] -= 1
        tmp[i+1] -= 1
        tmp[i+2] -= 1
        analyzeTehai(tmp, i+shuntsu_phase, mentsu+[i.shuntsu])
      end
    end

  else
    # 残った4枚から待ちを絞る

    # アタマを探す
    0.upto(8).each do |i|
      if pai[i] >= 2 then
        tmp = pai.dup
        tmp[i] -= 2
        analyzeMachi(tmp, mentsu+[i.atama])
      end
    end

    # 面子(刻子 or 順子)があれば単騎待ち
    # 刻子
    unless skip_anko_check then
      from.upto(8).each do |i|
        if pai[i] >= 3 then
          tmp = pai.dup
          tmp[i] -= 3
          analyzeTanki(tmp, mentsu+[i.anko])
        end
      end
    end

    # 順子
    from.upto(6).each do |i|
      if pai[i]>=1 && pai[i+1]>=1 && pai[i+2]>=1 then
        tmp = pai.dup
        tmp[i] -= 1
        tmp[i+1] -= 1
        tmp[i+2] -= 1
        analyzeTanki(tmp, mentsu + [i.shuntsu])
      end
    end
  end
end

#
# 4枚から待ちを絞る
#
def analyzeMachi(pai, mentsu)
  0.upto(8).each do |i|
    # リャンメン(ペンチャン)
    if pai[i]==1 && pai[i+1]==1 then
      puts mentsu.sort.to_s + i.ryanmen
    end

    # カンチャン
    if pai[i]==1 && pai[i+2]==1 then
      puts mentsu.sort.to_s + i.kanchan
    end

    # シャボ
    if pai[i]==2 then
      puts mentsu.sort.to_s + i.shabo
    end
  end
end

#
# 単騎待ちの数字を調べて待ちを表示する
#
def analyzeTanki(pai, mentsu)
  # 単騎待ち
  0.upto(8).each do |i|
    if pai[i] == 1 then
      puts mentsu.sort.to_s + i.tanki
    end
  end
end

# 以下実際の処理
pai = parsePai ARGV[0] unless ARGV[0] == nil
analyzeTehai(pai)
Advertisement