Class: Mjai::ShantenAnalysis

Inherits:
Object
  • Object
show all
Defined in:
lib/mjai/shanten_analysis.rb

Defined Under Namespace

Classes: DetailedCombination

Constant Summary collapse

MENTSU_TYPES =

ryanpen = 両面 or 辺搭

[:kotsu, :shuntsu, :toitsu, :ryanpen, :kanta, :single]
MENTSU_CATEGORIES =
{
  :kotsu => :complete,
  :shuntsu => :complete,
  :toitsu => :toitsu,
  :ryanpen => :tatsu,
  :kanta => :tatsu,
  :single => :single,
}
MENTSU_SIZES =
{
  :complete => 3,
  :toitsu => 2,
  :tatsu => 2,
  :single => 1,
}
ALL_TYPES =
[:normal, :chitoitsu, :kokushimuso]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pais, max_shanten = nil, types = ALL_TYPES, num_used_pais = pais.size, need_all_combinations = true) ⇒ ShantenAnalysis

Returns a new instance of ShantenAnalysis.

Raises:

  • (ArgumentError)


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/mjai/shanten_analysis.rb', line 54

def initialize(pais, max_shanten = nil, types = ALL_TYPES,
    num_used_pais = pais.size, need_all_combinations = true)
  
  @pais = pais
  @max_shanten = max_shanten
  @num_used_pais = num_used_pais
  @need_all_combinations = need_all_combinations
  raise(ArgumentError, "invalid number of pais") if @num_used_pais % 3 == 0
  @pai_set = Hash.new(0)
  for pai in @pais
    @pai_set[pai.remove_red()] += 1
  end
  
  @cache = {}
  results = []
  results.push(count_normal(@pai_set, [])) if types.include?(:normal)
  results.push(count_chitoi(@pai_set)) if types.include?(:chitoitsu)
  results.push(count_kokushi(@pai_set)) if types.include?(:kokushimuso)
  
  @shanten = 1.0/0.0
  @combinations = []
  for shanten, combinations in results
    next if @max_shanten && shanten > @max_shanten
    if shanten < @shanten
      @shanten = shanten
      @combinations = combinations
    elsif shanten == @shanten
      @combinations += combinations
    end
  end
  
end

Instance Attribute Details

#combinationsObject (readonly)

Returns the value of attribute combinations.



87
88
89
# File 'lib/mjai/shanten_analysis.rb', line 87

def combinations
  @combinations
end

#paisObject (readonly)

Returns the value of attribute pais.



87
88
89
# File 'lib/mjai/shanten_analysis.rb', line 87

def pais
  @pais
end

#shantenObject (readonly)

Returns the value of attribute shanten.



87
88
89
# File 'lib/mjai/shanten_analysis.rb', line 87

def shanten
  @shanten
end

Class Method Details

.benchmarkObject



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/mjai/shanten_analysis.rb', line 31

def self.benchmark()
  all_pais = (["m", "p", "s"].map(){ |t| (1..9).map(){ |n| Pai.new(t, n) } }.flatten() +
      (1..7).map(){ |n| Pai.new("t", n) }) * 4
  start_time = Time.now.to_f
  100.times() do
    pais = all_pais.sample(14).sort()
    p pais.join(" ")
    shanten = ShantenAnalysis.count(pais)
    p shanten
=begin
    for i in 0...pais.size
      remains_pais = pais.dup()
      remains_pais.delete_at(i)
      if ShantenAnalysis.count(remains_pais) == shanten
        p pais[i]
      end
    end
=end
    #gets()
  end
  p Time.now.to_f - start_time
end

Instance Method Details

#convert_mentsu(mentsu) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
# File 'lib/mjai/shanten_analysis.rb', line 115

def convert_mentsu(mentsu)
  (type, pais) = mentsu
  if type == :ryanpen
    if [[1, 2], [8, 9]].include?(pais.map(){ |pai| pai.number })
      type = :penta
    else
      type = :ryanmen
    end
  end
  return Mentsu.new({:type => type, :pais => pais, :visibility => :an})
end

#count_chitoi(pai_set) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
# File 'lib/mjai/shanten_analysis.rb', line 127

def count_chitoi(pai_set)
  num_toitsus = pai_set.select(){ |pai, n| n >= 2 }.size
  num_singles = pai_set.select(){ |pai, n| n == 1 }.size
  if num_toitsus == 6 && num_singles == 0
    # toitsu * 5 + kotsu * 1 or toitsu * 5 + kantsu * 1
    shanten = 1
  else
    shanten = -1 + [7 - num_toitsus, 0].max
  end
  return [shanten, [:chitoitsu]]
end

#count_kokushi(pai_set) ⇒ Object



139
140
141
142
143
# File 'lib/mjai/shanten_analysis.rb', line 139

def count_kokushi(pai_set)
  yaochus = pai_set.select(){ |pai, n| pai.yaochu? }
  has_yaochu_toitsu = yaochus.any?(){ |pai, n| n >= 2 }
  return [(13 - yaochus.size) - (has_yaochu_toitsu ? 1 : 0), [:kokushimuso]]
end

#count_normal(pai_set, mentsus) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/mjai/shanten_analysis.rb', line 145

def count_normal(pai_set, mentsus)
  # TODO 上がり牌を全部自分が持っているケースを考慮
  key = get_key(pai_set, mentsus)
  if !@cache[key]
    if pai_set.empty?
      #p mentsus
      min_shanten = get_min_shanten_for_mentsus(mentsus)
      min_combinations = [mentsus]
    else
      shanten_lowerbound = get_min_shanten_for_mentsus(mentsus) if @max_shanten
      if @max_shanten && shanten_lowerbound > @max_shanten
        min_shanten = 1.0/0.0
        min_combinations = []
      else
        min_shanten = 1.0/0.0
        first_pai = pai_set.keys.sort()[0]
        for type in MENTSU_TYPES
          if @max_shanten == -1
            next if [:ryanpen, :kanta].include?(type)
            next if mentsus.any?(){ |t, ps| t == :toitsu } && type == :toitsu
          end
          (removed_pais, remains_set) = remove(pai_set, type, first_pai)
          if remains_set
            (shanten, combinations) =
                count_normal(remains_set, mentsus + [[type, removed_pais]])
            if shanten < min_shanten
              min_shanten = shanten
              min_combinations = combinations
              break if !@need_all_combinations && min_shanten == -1
            elsif shanten == min_shanten && shanten < 1.0/0.0
              min_combinations += combinations
            end
          end
        end
      end
    end
    @cache[key] = [min_shanten, min_combinations]
  end
  return @cache[key]
end

#detailed_combinationsObject



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/mjai/shanten_analysis.rb', line 91

def detailed_combinations
  num_required_mentsus = @pais.size / 3
  result = []
  for mentsus in @combinations.map(){ |ms| ms.map(){ |m| convert_mentsu(m) } }
    for janto_index in [nil] + (0...mentsus.size).to_a()
      t_mentsus = mentsus.dup()
      janto = nil
      if janto_index
        next if ![:toitsu, :kotsu].include?(mentsus[janto_index].type)
        janto = t_mentsus.delete_at(janto_index)
      end
      current_shanten =
          -1 +
          (janto_index ? 0 : 1) +
          t_mentsus.map(){ |m| 3 - m.pais.size }.
              sort()[0, num_required_mentsus].
              inject(0, :+)
      next if current_shanten != @shanten
      result.push(DetailedCombination.new(janto, t_mentsus))
    end
  end
  return result
end

#get_key(pai_set, mentsus) ⇒ Object



186
187
188
# File 'lib/mjai/shanten_analysis.rb', line 186

def get_key(pai_set, mentsus)
  return [pai_set, Set.new(mentsus)]
end

#get_min_shanten_for_mentsus(mentsus) ⇒ Object



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/mjai/shanten_analysis.rb', line 190

def get_min_shanten_for_mentsus(mentsus)
  
  mentsu_categories = mentsus.map(){ |t, ps| MENTSU_CATEGORIES[t] }
  num_current_pais = mentsu_categories.map(){ |m| MENTSU_SIZES[m] }.inject(0, :+)
  num_remain_pais = @pais.size - num_current_pais
  
  min_shantens = []
  if index = mentsu_categories.index(:toitsu)
    # Assumes the 対子 is 雀頭.
    mentsu_categories.delete_at(index)
    min_shantens.push(get_min_shanten_without_janto(mentsu_categories, num_remain_pais))
  else
    # Assumes 雀頭 is missing.
    min_shantens.push(get_min_shanten_without_janto(mentsu_categories, num_remain_pais) + 1)
    if num_remain_pais >= 2
      # Assumes 雀頭 is in remaining pais.
      min_shantens.push(get_min_shanten_without_janto(mentsu_categories, num_remain_pais - 2))
    end
  end
  return min_shantens.min
  
end

#get_min_shanten_without_janto(mentsu_categories, num_remain_pais) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/mjai/shanten_analysis.rb', line 213

def get_min_shanten_without_janto(mentsu_categories, num_remain_pais)
  
  # Assumes remaining pais generates best combinations.
  mentsu_categories += [:complete] * (num_remain_pais / 3)
  case num_remain_pais % 3
    when 1
      mentsu_categories.push(:single)
    when 2
      mentsu_categories.push(:toitsu)
  end
  
  sizes = mentsu_categories.map(){ |m| MENTSU_SIZES[m] }.sort_by(){ |n| -n }
  num_required_mentsus = @num_used_pais / 3
  return -1 + sizes[0...num_required_mentsus].inject(0){ |r, n| r + (3 - n) }
  
end

#inspectObject



268
269
270
# File 'lib/mjai/shanten_analysis.rb', line 268

def inspect
  return "\#<%p shanten=%d pais=<%s>>" % [self.class, @shanten, @pais.join(" ")]
end

#remove(pai_set, type, first_pai) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/mjai/shanten_analysis.rb', line 230

def remove(pai_set, type, first_pai)
  case type
    when :kotsu
      removed_pais = [first_pai] * 3
    when :shuntsu
      removed_pais = shuntsu_piece(first_pai, [0, 1, 2])
    when :toitsu
      removed_pais = [first_pai] * 2
    when :ryanpen
      removed_pais = shuntsu_piece(first_pai, [0, 1])
    when :kanta
      removed_pais = shuntsu_piece(first_pai, [0, 2])
    when :single
      removed_pais = [first_pai]
    else
      raise("should not happen")
  end
  return [nil, nil] if !removed_pais
  result_set = pai_set.dup()
  for pai in removed_pais
    if result_set[pai] > 0
      result_set[pai] -= 1
      result_set.delete(pai) if result_set[pai] == 0
    else
      return [nil, nil]
    end
  end
  return [removed_pais, result_set]
end

#shuntsu_piece(first_pai, relative_numbers) ⇒ Object



260
261
262
263
264
265
266
# File 'lib/mjai/shanten_analysis.rb', line 260

def shuntsu_piece(first_pai, relative_numbers)
  if first_pai.type == "t"
    return nil
  else
    return relative_numbers.map(){ |i| Pai.new(first_pai.type, first_pai.number + i) }
  end
end