mjai-manue 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,488 @@
1
+ require "set"
2
+ require "pp"
3
+
4
+ require "mjai/pai"
5
+ require "mjai/shanten_analysis"
6
+ require "mjai/context"
7
+ require "mjai/hora"
8
+
9
+
10
+ module Mjai
11
+
12
+ module Manue
13
+
14
+
15
+ class HoraPointsEstimate
16
+
17
+ # TODO Calculate with statistics.
18
+ TSUMO_HORA_PROB = 0.5
19
+
20
+ # TODO Add ippatsu, uradora
21
+ SUPPORTED_YAKUS = [:reach, :tanyaochu, :pinfu, :iso, :fanpai, :dora, :akadora]
22
+
23
+ DORA_YAKUS = [:dora, :akadora, :uradora]
24
+
25
+ class ProbablisticFan
26
+
27
+ def self.prob_average(pfans)
28
+ return prob_weighted_average(pfans.map(){ |pf| [pf, 1.0 / pfans.size] })
29
+ end
30
+
31
+ def self.prob_weighted_average(weighted_pfans)
32
+ new_probs = Hash.new(0.0)
33
+ for pfan, weight in weighted_pfans
34
+ for fan, prob in pfan.probs
35
+ new_probs[fan] += prob * weight
36
+ end
37
+ end
38
+ return ProbablisticFan.new(new_probs)
39
+ end
40
+
41
+ def initialize(arg)
42
+ if arg.is_a?(Integer)
43
+ @probs = {arg => 1.0}
44
+ else
45
+ @probs = arg
46
+ end
47
+ end
48
+
49
+ attr_reader(:probs)
50
+
51
+ def +(other)
52
+ return apply(other){ |f1, f2| f1 + f2 }
53
+ end
54
+
55
+ def *(other)
56
+ return apply(other){ |f1, f2| f1 * f2 }
57
+ end
58
+
59
+ def max(other)
60
+ return apply(other){ |f1, f2| [f1, f2].max }
61
+ end
62
+
63
+ def expected
64
+ @probs.map(){ |f, pr| f * pr }.inject(0.0, :+)
65
+ end
66
+
67
+ def apply(other, &block)
68
+ new_probs = Hash.new(0.0)
69
+ for f1, p1 in @probs
70
+ for f2, p2 in other.probs
71
+ new_probs[block.call(f1, f2)] += p1 * p2 if p1 * p2 > 0.0
72
+ end
73
+ end
74
+ return ProbablisticFan.new(new_probs)
75
+ end
76
+
77
+ end
78
+
79
+ class HoraCombination
80
+
81
+ def initialize(used_combination, hp_est)
82
+ @used_combination = used_combination
83
+ @hp_est = hp_est
84
+ @janto_candidates = HoraPointsEstimate.janto_candidates(@used_combination.janto)
85
+ @mentsu_candidates = @used_combination.mentsus.map() do |mentsu|
86
+ HoraPointsEstimate.complete_candidates(mentsu)
87
+ end
88
+ @menzen = @used_combination.mentsus.all?(){ |m| m.visibility == :an }
89
+ end
90
+
91
+ attr_reader(:used_combination)
92
+ attr_reader(:janto_candidates)
93
+ attr_reader(:mentsu_candidates)
94
+
95
+ def yaku_pfan(yaku)
96
+ return __send__("#{yaku}_pfan")
97
+ end
98
+
99
+ def reach_pfan
100
+ return ProbablisticFan.new(@menzen ? 1 : 0)
101
+ end
102
+
103
+ def tanyaochu_pfan
104
+ prob = 1.0
105
+ for cands in [@janto_candidates] + @mentsu_candidates
106
+ prob *= get_prob(cands){ |m| !has_yaochu?(m) }
107
+ end
108
+ return ProbablisticFan.new({0 => 1.0 - prob, 1 => prob})
109
+ end
110
+
111
+ def pinfu_pfan
112
+ if @menzen
113
+ prob = 1.0
114
+ prob *= get_prob(@janto_candidates) do |m|
115
+ @hp_est.context.fanpai_fan(m.pais[0]) == 0
116
+ end
117
+ for cands in @mentsu_candidates
118
+ prob *= get_prob(cands){ |m| m.type == :shuntsu }
119
+ end
120
+ if prob > 0.0
121
+ incompletes = @used_combination.mentsus.select(){ |m| m.pais.size < 3 }
122
+ ryanmen_prob =
123
+ incompletes.map(){ |m| get_ryanmen_prob(m) }.inject(0.0, :+) / incompletes.size
124
+ prob *= ryanmen_prob
125
+ end
126
+ else
127
+ prob = 0.0
128
+ end
129
+ return ProbablisticFan.new({0 => 1.0 - prob, 1 => prob})
130
+ end
131
+
132
+ def iso_pfan
133
+ pfan = ProbablisticFan.new(0)
134
+ for type in ["m", "p", "s"]
135
+ chiniso_prob = get_iso_prob([type])
136
+ honiso_prob = get_iso_prob([type, "t"]) - chiniso_prob
137
+ type_prob = ProbablisticFan.new({
138
+ 0 => 1.0 - honiso_prob - chiniso_prob,
139
+ (@menzen ? 3 : 2) => honiso_prob,
140
+ (@menzen ? 6 : 5) => chiniso_prob,
141
+ })
142
+ pfan = pfan.max(type_prob)
143
+ end
144
+ return pfan
145
+ end
146
+
147
+ def get_iso_prob(types)
148
+ prob = 1.0
149
+ prob *= get_prob(@janto_candidates){ |m| types.include?(m.pais[0].type) }
150
+ for cands in @mentsu_candidates
151
+ prob *= get_prob(cands){ |m| types.include?(m.pais[0].type) }
152
+ end
153
+ return prob
154
+ end
155
+
156
+ def fanpai_pfan
157
+ pfan = ProbablisticFan.new(0)
158
+ for cands in @mentsu_candidates
159
+ fan1_prob = get_fanpai_prob(cands, 1)
160
+ fan2_prob = get_fanpai_prob(cands, 2)
161
+ pfan += ProbablisticFan.new({
162
+ 0 => 1.0 - fan1_prob - fan2_prob,
163
+ 1 => fan1_prob,
164
+ 2 => fan2_prob,
165
+ })
166
+ end
167
+ return pfan
168
+ end
169
+
170
+ def dora_pfan
171
+ pfan = ProbablisticFan.new(0)
172
+ for cands in [@janto_candidates] + @mentsu_candidates
173
+ probs = Hash.new(0.0)
174
+ for mentsu, prob in cands
175
+ probs[get_dora_fan(mentsu)] += prob
176
+ end
177
+ pfan += ProbablisticFan.new(probs)
178
+ end
179
+ return pfan
180
+ end
181
+
182
+ def akadora_pfan
183
+ # Note that red is removed from @mentsu_candidates etc.
184
+ red_pais = @hp_est.shanten_analysis.pais.
185
+ select(){ |pai| pai.red? }.
186
+ map(){ |pai| pai.remove_red() }
187
+ pfan = ProbablisticFan.new(0)
188
+ for red_pai in red_pais
189
+ neg_prob = 1.0
190
+ for cands in [@janto_candidates] + @mentsu_candidates
191
+ neg_prob *=
192
+ get_prob(cands){ |m| !m.pais.any?(){ |pai| red_pais.include?(pai) } }
193
+ end
194
+ pfan += ProbablisticFan.new({0 => neg_prob, 1 => 1.0 - neg_prob})
195
+ end
196
+ return pfan
197
+ end
198
+
199
+ def has_yaochu?(mentsu)
200
+ return mentsu.pais.any?(){ |pai| pai.yaochu? }
201
+ end
202
+
203
+ def get_prob(mentsu_cands, &block)
204
+ return mentsu_cands.select(){ |m, pr| yield(m) }.map(){ |m, pr| pr }.inject(0.0, :+)
205
+ end
206
+
207
+ def get_fanpai_prob(mentsu_cands, fan)
208
+ return get_prob(mentsu_cands) do |m|
209
+ m.type == :kotsu && @hp_est.context.fanpai_fan(m.pais[0]) == fan
210
+ end
211
+ end
212
+
213
+ def get_dora_fan(mentsu)
214
+ fans = mentsu.pais.map() do |pai|
215
+ @hp_est.context.doras.count(pai.remove_red())
216
+ end
217
+ return fans.inject(0, :+)
218
+ end
219
+
220
+ # Assuming the mentsu becomes shuntsu in the end, returns the probability that
221
+ # its waiting form is ryanmen.
222
+ def get_ryanmen_prob(mentsu)
223
+ case mentsu.type
224
+ when :ryanmen
225
+ return 1.0
226
+ when :kanta, :penta
227
+ return 0.0
228
+ when :single
229
+ case mentsu.pais[0].number
230
+ when 1, 9
231
+ return 0.0
232
+ when 2, 8
233
+ # [3] out of [1, 3, 4]
234
+ return 1.0 / 3.0
235
+ else
236
+ # [2, 4] out of [1, 2, 4, 5]
237
+ return 0.5
238
+ end
239
+ else
240
+ raise("should not happen: %p" % mentsu.type)
241
+ end
242
+ end
243
+
244
+ end
245
+
246
+ def initialize(params)
247
+ @shanten_analysis = params[:shanten_analysis]
248
+ @furos = params[:furos]
249
+ @context = params[:context]
250
+
251
+ @expanded_combinations = get_expanded_combinations()
252
+ @used_combinations = get_used_combinations(@expanded_combinations)
253
+ @hora_combinations = get_hora_combinations(@used_combinations)
254
+ @yaku_pfans = get_yaku_pfans(@hora_combinations)
255
+ @average_points = get_average_points(@yaku_pfans)
256
+ end
257
+
258
+ attr_reader(
259
+ :shanten_analysis, :furos, :context,
260
+ :expanded_combinations, :used_combinations, :hora_combinations,
261
+ :yaku_pfans, :average_points)
262
+
263
+ # key: [menzen, tsumo, pinfu]
264
+ FU_MAP = {
265
+ [false, false, false] => 30,
266
+ [false, false, true] => 30,
267
+ [false, true, false] => 30,
268
+ [false, true, true] => 30,
269
+ [true, false, false] => 40,
270
+ [true, false, true] => 30,
271
+ [true, true, false] => 30,
272
+ [true, true, true] => 20,
273
+ }
274
+
275
+ def get_expanded_combinations()
276
+ furo_mentsus = @furos.map(){ |f| f.to_mentsu() }
277
+ result = []
278
+ for combination in @shanten_analysis.detailed_combinations
279
+ #p combination
280
+ if combination.janto
281
+ result.push(ShantenAnalysis::DetailedCombination.new(
282
+ combination.janto, combination.mentsus + furo_mentsus))
283
+ else
284
+ num_groups = combination.mentsus.select(){ |m| m.pais.size >= 2 }.size
285
+ combination.mentsus.each_with_index() do |mentsu, i|
286
+ if mentsu.pais.size == 1
287
+ maybe_janto = true
288
+ elsif [:ryanmen, :kanchan, :penta].include?(mentsu.type) && num_groups >= 5
289
+ maybe_janto = true
290
+ else
291
+ maybe_janto = false
292
+ end
293
+ if maybe_janto
294
+ remains = combination.mentsus.dup()
295
+ remains.delete_at(i)
296
+ result.push(ShantenAnalysis::DetailedCombination.new(
297
+ mentsu, remains + furo_mentsus))
298
+ end
299
+ end
300
+ end
301
+ end
302
+ return result
303
+ end
304
+
305
+ def get_used_combinations(expanded_combinations)
306
+ result = Set.new()
307
+ for combination in expanded_combinations
308
+ completes = combination.mentsus.select(){ |m| m.pais.size >= 3 }.sort()
309
+ tatsus = combination.mentsus.select(){ |m| m.pais.size == 2 }.sort()
310
+ singles = combination.mentsus.select(){ |m| m.pais.size == 1 }.sort()
311
+ #pp [:used_exp_combi, combination]
312
+ #p [:completes, completes.size]
313
+ mentsu_combinations = []
314
+ if completes.size >= 4
315
+ mentsu_combinations.push(completes)
316
+ elsif completes.size + tatsus.size >= 4
317
+ tatsus.combination(4 - completes.size) do |t_tatsus|
318
+ mentsu_combinations.push(completes + t_tatsus)
319
+ end
320
+ else
321
+ singles.combination(4 - completes.size - tatsus.size) do |t_singles|
322
+ mentsu_combinations.push(completes + tatsus + t_singles)
323
+ end
324
+ end
325
+ #p [:mentsu_combinations, mentsu_combinations]
326
+ for mentsus in mentsu_combinations
327
+ result.add(ShantenAnalysis::DetailedCombination.new(combination.janto, mentsus))
328
+ end
329
+ end
330
+ return result
331
+ end
332
+
333
+ def get_hora_combinations(used_combinations)
334
+ return used_combinations.map(){ |c| HoraCombination.new(c, self) }
335
+ end
336
+
337
+ def get_yaku_pfans(hora_combinations)
338
+ result = {}
339
+ for yaku in SUPPORTED_YAKUS
340
+ result[yaku] = get_yaku_pfan(yaku, hora_combinations)
341
+ end
342
+ return result
343
+ end
344
+
345
+ def get_yaku_pfan(yaku, hora_combinations)
346
+ pfans = hora_combinations.map(){ |hc| hc.yaku_pfan(yaku) }
347
+ return ProbablisticFan.prob_average(pfans)
348
+ end
349
+
350
+ def get_average_points(yaku_pfans)
351
+
352
+ normal_pfan = yaku_pfans.select(){ |yk, pf| !DORA_YAKUS.include?(yk) }.
353
+ map(){ |yk, pf| pf }.
354
+ inject(ProbablisticFan.new(0), :+)
355
+ dora_pfan = yaku_pfans.select(){ |yk, pf| DORA_YAKUS.include?(yk) }.
356
+ map(){ |yk, pf| pf }.
357
+ inject(ProbablisticFan.new(0), :+)
358
+ probs = Hash.new(0.0)
359
+ for nf, np in normal_pfan.probs
360
+ for df, fp in dora_pfan.probs
361
+ probs[nf == 0 ? 0 : nf + df] += np * fp
362
+ end
363
+ end
364
+
365
+ pinfu_prob = yaku_pfans[:pinfu].probs[1]
366
+ is_menzen = @furos.empty?
367
+ result = 0.0
368
+ for is_tsumo in [false, true]
369
+ for is_pinfu in [false, true]
370
+ base_prob =
371
+ (is_tsumo ? TSUMO_HORA_PROB : 1.0 - TSUMO_HORA_PROB) *
372
+ (is_pinfu ? pinfu_prob : 1.0 - pinfu_prob)
373
+ next if base_prob == 0.0
374
+ fu = FU_MAP[[is_menzen, is_tsumo, is_pinfu]]
375
+ for fan, fan_prob in probs
376
+ fan += 1 if is_menzen && is_tsumo
377
+ if fan > 0
378
+ datum = Hora::PointsDatum.new(fu, fan, @context.oya, is_tsumo ? :tsumo : :ron)
379
+ #p [is_tsumo, is_pinfu, base_prob, fan, fan_prob, fu, datum.points]
380
+ result += datum.points * fan_prob * base_prob
381
+ end
382
+ end
383
+ end
384
+ end
385
+ return result
386
+
387
+ end
388
+
389
+ def yaku_debug_str
390
+ [:reach, :tanyaochu, :pinfu, :fanpai, :dora, :akadora]
391
+ short_names = [
392
+ [:tanyaochu, "ty"],
393
+ [:pinfu, "pf"],
394
+ [:iso, "is"],
395
+ [:fanpai, "fp"],
396
+ [:dora, "dr"],
397
+ [:akadora, "ad"],
398
+ ]
399
+ strs = short_names.map() do |yaku, short_name|
400
+ exp_fan = @yaku_pfans[yaku].expected
401
+ exp_fan < 0.001 ? nil : "%s=%.2f" % [short_name, exp_fan]
402
+ end
403
+ return strs.select(){ |s| s }.join(" ")
404
+ end
405
+
406
+ def dump()
407
+ p [:shanten, self.shanten_analysis.shanten]
408
+ p :orig
409
+ for combi in self.shanten_analysis.combinations
410
+ pp combi
411
+ end
412
+ p :detailed
413
+ for combi in self.shanten_analysis.detailed_combinations
414
+ pp combi
415
+ end
416
+ p :expanded
417
+ for combi in self.expanded_combinations
418
+ pp combi
419
+ end
420
+ p [:used, self.used_combinations.size]
421
+ for combi in self.used_combinations
422
+ pp combi
423
+ end
424
+ p [:hora, self.hora_combinations.size]
425
+ for hcombi in self.hora_combinations
426
+ p [:current_janto, hcombi.used_combination.janto.pais.join(" ")]
427
+ for mentsu in hcombi.used_combination.mentsus
428
+ p [:current_mentsu, mentsu.pais.join(" ")]
429
+ end
430
+ pp hcombi
431
+ end
432
+ for yaku, pfan in self.yaku_pfans
433
+ p [yaku, pfan.probs.reject(){ |k, v| k == 0 }.sort()] if pfan.probs[0] < 0.999
434
+ end
435
+ p [:avg_pts, self.average_points]
436
+ end
437
+
438
+ def self.complete_candidates(mentsu)
439
+ if [:shuntsu, :kotsu, :kantsu].include?(mentsu.type)
440
+ return [[mentsu, 1.0]]
441
+ end
442
+ rcands = []
443
+ case mentsu.type
444
+ when :ryanmen, :penta
445
+ rcands += [[:shuntsu, [-1, 0, 1]], [:shuntsu, [0, 1, 2]]]
446
+ when :kanta
447
+ rcands += [[:shuntsu, [0, 1, 2]]]
448
+ when :toitsu
449
+ rcands += [[:kotsu, [0, 0, 0]]]
450
+ when :single
451
+ rcands += [
452
+ [:shuntsu, [-2, -1, 0]],
453
+ [:shuntsu, [-1, 0, 1]],
454
+ [:shuntsu, [0, 1, 2]],
455
+ [:kotsu, [0, 0, 0]]
456
+ ]
457
+ else
458
+ raise("should not happen: %p" % mentsu.type)
459
+ end
460
+ cands = []
461
+ first_pai = mentsu.pais[0]
462
+ for type, rnums in rcands
463
+ in_range = rnums.all?() do |rn|
464
+ (rn == 0 || first_pai.type != "t") && (1..9).include?(first_pai.number + rn)
465
+ end
466
+ if in_range
467
+ cands.push(Mentsu.new({
468
+ :type => type,
469
+ :pais => rnums.map(){ |rn| Pai.new(first_pai.type, first_pai.number + rn) },
470
+ }))
471
+ end
472
+ end
473
+ return cands.map(){ |m| [m, 1.0 / cands.size] }
474
+ end
475
+
476
+ def self.janto_candidates(mentsu)
477
+ pai_cands = mentsu.pais.uniq()
478
+ return pai_cands.map() do |pai|
479
+ [Mentsu.new({:type => :toitsu, :pais => [pai, pai]}), 1.0 / pai_cands.size]
480
+ end
481
+ end
482
+
483
+ end
484
+
485
+
486
+ end
487
+
488
+ end