麻雀の「待ち」を出力するプログラム

既に時間は経ってしまったが、以下のような問題が出題されていた。

麻雀の手牌が入力として与えられたとき、「待ち」を出力するプログラムを書いてください。

  • 字牌なし・萬子のみの想定、つまり、いわゆる「チンイツ」限定で結構です(プログラミングの本質的にはこの限定でまったく問題ないため)
  • 1〜9の数字13個からなる文字列を受け取り、できている順子・刻子・アタマを()、待ちの部分を[]でくくって出力してください
  • 面前かつ槓子は存在しない前提でOKです
  • ()[]の出力順は自由ですが、順序だけが違うものは同一視してください(例:111222を刻子2つで構成するとき、(111)(222)が(222)(111)に入れ替わるだけのものは同一解答とします)
  • 多面待ちのときも含めすべての待ちを出力してください
  • 待ちがないときは何も出力しないでください
http://www.itmedia.co.jp/enterprise/articles/1004/03/news002.html

麻雀は人並みに出来る。点数計算もちゃんと出来る。しかし、麻雀のルールを初心者に伝えるのは、かなりシンドイ。たぶん、言葉だけでは伝えきれず、実際に対局しながら後に付くOJTで、その状況に合った説明を繰り返すことになると思う。

だから、テンパイを判定して、そのすべての「待ち」パターンを出力するプログラムを作るのは、自分にとって相当難度が高いと思っていた。実際にやってみると、予想どおり試行錯誤の連続で、かなり手強い。それでも、どうにか出来上がったのが以下のコード。

  • 言語はRuby
  • 正規表現刻子・順子・対子を取り出して処理している。
  • 難しいのは、順子の絡み方によって、何通りものテンパイ形があるところ。
  • すべてのテンパイ形を漏れなく取り出す技が必要なのだ。
  • 結局、順子4つ分のすべての組み合わせをシラミつぶしに調べることにしてしまった。
  • まず、頭を決める。11から99まで9種類。
  • 次に、順子を調べる。123から789まで7種類、4順子分。
  • 内側のループは、外側のループカウントから開始するようにしている。
  • そうすると、全体のループカウントは意外に少なくなり、2100回で済む。
  • 無条件に1から開始すると、24010回ループするので、かなり節約できた。
  • 上がり牌を手牌で使い切っている空聴(カラテン)はチェックしていない。
  • 本来は、聴牌(テンパイ)とは認められないはず。
      • 以下コード中の半角¥は、半角\に置き換える必要あり。
class Tehai
  def initialize(input)
    @in_text = input
    @outs = []
    @count = 0
  end
  
  # 任意の刻子を取り出す
  def koutsu
    @rest.sub!(/111|222|333|444|555|666|777|888|999/, '')
    @out << "(#{$&})" if $&
  end
  
  # nで始まる順子を取り出す
  def juntsu(n)
    @rest.sub!(/#{n}#{n+1}(#{n+1}*)#{n+2}/,'\1')
    @out << "(#{n}#{n+1}#{n+2})" if $&
  end
  
  # nnな対子を取り出す(n=nilの場合、任意の対子を取り出す)
  def toitsu(n)
    case
    when n == 0
      return true
    when n.nil?
      @rest.sub!(/11|22|33|44|55|66|77|88|99/, '')
    else
      @rest.sub!(/#{n}#{n}/, '')
    end
    @out << "(#{$&})" if $&
  end
  
  # テンパイしているかどうか?
  def tenpai?
    case @rest.size
    when 1
      true
    when 2
      d = @rest[1] - @rest[0]
      true if d <= 2
    end
  end
  
  # テンパイを保存する
  def save
    @outs << (@out.sort << "[#{@rest}]").to_s if tenpai?
  end
  
  # 変数のリセット
  def reset
    @rest = '' + @in_text
    @out = []
  end
  
  # 待ちを出力する(メイン)
  def machi()
    (0..9).each do |h|
      reset; toitsu(h) && koutsu && koutsu && koutsu && koutsu; save
    (1..7).each do |i|
      reset; toitsu(h) && juntsu(i) && koutsu && koutsu && koutsu; save
    (i..7).each do |j|
      reset; toitsu(h) && juntsu(i) && juntsu(j) && koutsu && koutsu; save
    (j..7).each do |k|
      reset; toitsu(h) && juntsu(i) && juntsu(j) && juntsu(k) && koutsu; save
    (k..7).each do |l|
      @count += 1
      reset; toitsu(h) && juntsu(i) && juntsu(j) && juntsu(k) && juntsu(l); save
    end
    end
    end
    end
    end
    reset; loop {break unless toitsu(nil)}; save # 七対子の処理
    
    p @in_text
    puts @outs.uniq, "ループ回数: " + @count.to_s, ''
  end
end

# テスト
Tehai.new('1112224588899').machi
Tehai.new('1122335556799').machi
Tehai.new('1112223335559').machi
Tehai.new('1223344888999').machi
Tehai.new('1112345678999').machi # 九蓮宝燈
Tehai.new('1112223334699').machi # 嵌張待ち([46])
Tehai.new('1122334455667').machi # 七対子
Tehai.new('1112223334445').machi # 多面待ち
Tehai.new('1112223334567').machi # ノベタン

実行してみた。

$ ruby ~/Desktop/marjon.rb
"1112224588899"
(111)(222)(888)(99)[45]
ループ回数: 2100

"1122335556799"
(123)(123)(55)(567)[99]
(123)(123)(555)(99)[67]
(123)(123)(567)(99)[55]
ループ回数: 2100

"1112223335559"
(111)(222)(333)(555)[9]
(123)(123)(123)(555)[9]
ループ回数: 2100

"1223344888999"
(123)(234)(888)(999)[4]
(234)(234)(888)(999)[1]
(123)(44)(888)(999)[23]
ループ回数: 2100

"1112345678999"
(111)(234)(567)(999)[8]
(111)(234)(678)(999)[5]
(111)(345)(678)(999)[2]
(11)(123)(456)(999)[78]
(11)(123)(456)(789)[99]
(11)(123)(678)(999)[45]
(11)(345)(678)(999)[12]
(123)(456)(789)(99)[11]
(111)(234)(567)(99)[89]
(111)(234)(789)(99)[56]
(111)(456)(789)(99)[23]
ループ回数: 2100

"1112223334699"
(111)(222)(333)(99)[46]
(123)(123)(123)(99)[46]
ループ回数: 2100

"1122334455667"
(123)(123)(456)(456)[7]
(123)(123)(456)(567)[4]
(123)(234)(456)(567)[1]
(11)(234)(234)(567)[56]
(11)(234)(456)(567)[23]
(123)(123)(44)(567)[56]
(11)(22)(33)(44)(55)(66)[7]
ループ回数: 2100

"1112223334445"
(111)(222)(333)(444)[5]
(123)(123)(123)(444)[5]
(111)(234)(234)(234)[5]
(111)(234)(234)(345)[2]
(11)(123)(234)(234)[45]
(11)(123)(234)(345)[24]
(11)(234)(234)(345)[12]
(111)(22)(234)(345)[34]
(111)(222)(33)(444)[35]
(111)(222)(33)(345)[44]
(111)(222)(333)(44)[45]
(123)(123)(123)(44)[45]
(123)(123)(345)(44)[12]
(111)(222)(345)(44)[33]
ループ回数: 2100

"1112223334567"
(123)(123)(123)(456)[7]
(123)(123)(123)(567)[4]
(123)(123)(234)(567)[1]
(111)(222)(333)(456)[7]
(111)(222)(333)(567)[4]
(11)(123)(234)(567)[23]
(111)(22)(234)(567)[33]
(111)(22)(333)(567)[24]
(111)(234)(33)(567)[22]
(111)(222)(33)(345)[67]
(111)(222)(33)(567)[34]
ループ回数: 2100

果たして、これですべて網羅できているのだろうか?