mjai-manue 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/mjai-manue +10 -0
- data/lib/mjai/manue/danger_estimator.rb +730 -0
- data/lib/mjai/manue/hora_points_estimate.rb +488 -0
- data/lib/mjai/manue/hora_probability_estimator.rb +328 -0
- data/lib/mjai/manue/mjai_manue_command.rb +32 -0
- data/lib/mjai/manue/player.rb +373 -0
- data/share/danger.all.tree +0 -0
- data/share/hora_prob.marshal +0 -0
- metadata +65 -0
@@ -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
|