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.
- 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
|