rubyで麻雀の待ちを出力

新年あけましておめでとうございます。
正月といえば(私は)麻雀なんですが、今年はやる機会がなかったので
rubyで麻雀してみました。

考え方

3時間以上かかってしまいましたが、なんとかできました。


ここで使う用語。
・面子(同一 or 連続する牌3枚)
・頭(同一牌2枚)
・待(もう一枚で面子or頭となる牌。1 or 3枚)


上がりとなるパターンは下記2種類と考えます。
・3面子(9枚)+頭(2枚)+待(2枚)
・4面子(12枚)+待(1枚)


なので、この面子となる組み合わせを再帰で探索する方向で
書いてみました。

こんな感じ

ソース
class Majang
  def mati(haipai)
    @tenpai = []
    mati_check(haipai.split(//).sort)
    @tenpai.uniq!
    @tenpai.each{|x| puts x}
  end

  private
  def append_tenpai(mentu, mati)
    @tenpai << format_tenapi(mentu, mati)
  end

  def format_tenapi(mentu, mati)
    mentu.sort.map{|x| "(#{x})"}.to_s + "[#{mati.to_s}]"
  end

  def mati_check(hais, kakutei_mentu = [])
    #最後の1枚は上がり
    if hais.size == 1
      append_tenpai(kakutei_mentu, hais) and return
    end

    #最後の4枚
    if hais.size == 4
      #全部同じ場合は、役無し
      return if all_same?(hais)
      #テンパイ確定後、終了
      return if tenpai?(hais, kakutei_mentu)
    end

    #面子候補を取得
    candidates(hais).each do |cand|
      next_hais = hais.minus(cand.split(//))
      next_kakutei_mentu = kakutei_mentu + cand.to_a
      mati_check(next_hais, next_kakutei_mentu)
    end
  end

  def all_same?(ar)
    return false if ar.size == 0
    ((ar - ar[0].to_a).size == 0)? true : false
  end

  def tenpai?(hais, kakutei_mentu)
    val = false
    hais.each_with_index do |hai, i|
      hai2  = hais[i, 2]   # 2枚ずつ取り出す
      return val unless hai2.size == 2
      atama = hai2
      mati  = hais.minus(atama)
      if atama?(atama) and mati?(mati)
        append_tenpai(kakutei_mentu + [atama.to_s], mati)
        val = true
      end
    end
    val
  end

  def atama?(hais)
    all_same?(hais)
  end

  def mati?(hais)
    (hais[1].to_i - hais[0].to_i) <= 2 ? true : false
  end

  def candidates(hais)
    work = []
    hais.each_with_index do |hai, i|
      #刻子
      work << (hai + hais[i+1] + hais[i+2]) if hai == hais[i+1] and hai == hais[i+2]
      #順子
      work << (hai + hai.succ + hai.succ.succ) if hais.include?(hai.succ) and hais.include?(hai.succ.succ)
    end
    work.uniq
  end
end

class Array
  def minus(ar)
    work = self.dup
    ar.each do |val|
      work[work.index(val)] = nil if self.include?(val)
    end
    work.compact
  end
end


m = Majang.new
m.mati("1112224588899")
puts
m.mati("1112223335559")
puts
m.mati("1223344888999")
puts
m.mati("1112345678999")
puts
結果
$ ruby majang.rb 
(111)(222)(888)(99)[45]

(111)(222)(333)(555)[9]
(123)(123)(123)(555)[9]

(123)(234)(888)(999)[4]
(123)(44)(888)(999)[23]
(234)(234)(888)(999)[1]

(111)(234)(567)(99)[89]
(111)(234)(678)(999)[5]
(111)(234)(789)(99)[56]
(111)(234)(567)(999)[8]
(111)(345)(678)(999)[2]
(111)(456)(789)(99)[23]
(11)(123)(456)(789)[99]
(123)(456)(789)(99)[11]
(11)(123)(456)(999)[78]
(11)(123)(678)(999)[45]
(11)(345)(678)(999)[12]

感想

結構手間取りました。あんまりキレイじゃないなぁ。
他のrubyistのコードも見てみたい。


しかし、麻雀というのは複雑ですね。
これに字牌、捨牌、ドラ、上がり、相手の状況などを瞬時に判断してるんだから
人間すごい!



あと、rubyの配列って引き算が可能(ステキ!)なんですが、
重複する要素は取り除かれてしまいます。

>> ar1 = [1,2,3,1]
>> ar2 = [1,2]

>> ar1 - ar2
=> [3]

今回重複は残したかったので、最後無理矢理Arrayを拡張しています。
もう少しいい書き方がありそうですが、こういうの以外と需要があるんじゃないでしょうか。

class Array
  def minus(ar)
    work = self.dup
    ar.each do |val|
      work[work.index(val)] = nil if self.include?(val)
    end
    work.compact
  end
end