mjai-manue 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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