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,328 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require "mjai/archive"
|
6
|
+
require "mjai/shanten_analysis"
|
7
|
+
|
8
|
+
|
9
|
+
module Mjai
|
10
|
+
|
11
|
+
module Manue
|
12
|
+
|
13
|
+
|
14
|
+
class HoraProbabilityEstimator
|
15
|
+
|
16
|
+
Criterion = Struct.new(:num_remain_turns, :shanten)
|
17
|
+
Metrics = Struct.new(:hora_prob, :quick_hora_prob, :num_samples)
|
18
|
+
|
19
|
+
class Scene
|
20
|
+
|
21
|
+
def initialize(estimator, params)
|
22
|
+
@estimator = estimator
|
23
|
+
@visible_set = params[:visible_set]
|
24
|
+
@num_invisible = 4 * (9 * 3 + 7) - @visible_set.values.inject(0, :+)
|
25
|
+
@num_remain_turns = params[:num_remain_turns]
|
26
|
+
@current_shanten = params[:current_shanten]
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader(
|
30
|
+
:estimator, :visible_set, :num_invisible,
|
31
|
+
:num_remain_turns, :current_shanten)
|
32
|
+
|
33
|
+
def get_tehais(remains)
|
34
|
+
return Tehais.new(self, remains)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class Tehais
|
40
|
+
|
41
|
+
def initialize(scene, remains)
|
42
|
+
@scene = scene
|
43
|
+
@remains = remains
|
44
|
+
@shanten_analysis = ShantenAnalysis.new(@remains, @scene.current_shanten, [:normal])
|
45
|
+
@progress_prob = get_progress_prob()
|
46
|
+
@hora_prob = get_hora_prob()
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader(:progress_prob, :hora_prob)
|
50
|
+
|
51
|
+
def get_hora_prob(num_remain_turns = @scene.num_remain_turns)
|
52
|
+
return 0.0 if @progress_prob == 0.0
|
53
|
+
return 0.0 if num_remain_turns < 0
|
54
|
+
shanten = @shanten_analysis.shanten
|
55
|
+
hora_prob_on_prog =
|
56
|
+
@scene.estimator.get_hora_prob(num_remain_turns - 2, shanten - 1)
|
57
|
+
hora_prob_on_no_prog =
|
58
|
+
get_hora_prob(num_remain_turns - 2)
|
59
|
+
hora_prob = @progress_prob * hora_prob_on_prog +
|
60
|
+
(1.0 - @progress_prob) * hora_prob_on_no_prog
|
61
|
+
#p [:hora_prob, num_remain_turns, shanten, @progress_prob,
|
62
|
+
# hora_prob, hora_prob_on_prog, hora_prob_on_no_prog]
|
63
|
+
return hora_prob
|
64
|
+
end
|
65
|
+
|
66
|
+
# Probability to decrease >= 1 shanten in 2 turns.
|
67
|
+
def get_progress_prob()
|
68
|
+
|
69
|
+
if @shanten_analysis.shanten > @scene.current_shanten
|
70
|
+
return 0.0
|
71
|
+
end
|
72
|
+
|
73
|
+
#p [:remains, @remains.join(" ")]
|
74
|
+
candidates = get_required_pais_candidates()
|
75
|
+
|
76
|
+
single_cands = Set.new()
|
77
|
+
double_cands = Set.new()
|
78
|
+
for pais in candidates
|
79
|
+
case pais.size
|
80
|
+
when 1
|
81
|
+
single_cands.add(pais[0])
|
82
|
+
when 2
|
83
|
+
double_cands.add(pais)
|
84
|
+
else
|
85
|
+
raise("should not happen")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
double_cands = double_cands.select() do |pais|
|
89
|
+
pais.all?(){ |pai| !single_cands.include?(pai) }
|
90
|
+
end
|
91
|
+
#p [:single, single_cands.sort().join(" ")]
|
92
|
+
#p [:double, double_cands]
|
93
|
+
|
94
|
+
# (p, *) or (*, p)
|
95
|
+
any_single_prob = single_cands.map(){ |pai| get_pai_prob(pai) }.inject(0.0, :+)
|
96
|
+
total_prob = 1.0 - (1.0 - any_single_prob) ** 2
|
97
|
+
|
98
|
+
#p [:single_total, total_prob]
|
99
|
+
for pai1, pai2 in double_cands
|
100
|
+
prob1 = get_pai_prob(pai1)
|
101
|
+
#p [:prob, pai1, state]
|
102
|
+
prob2 = get_pai_prob(pai2)
|
103
|
+
#p [:prob, pai2, state]
|
104
|
+
if pai1 == pai2
|
105
|
+
# (p1, p1)
|
106
|
+
total_prob += prob1 * prob2
|
107
|
+
else
|
108
|
+
# (p1, p2), (p2, p1)
|
109
|
+
total_prob += prob1 * prob2 * 2
|
110
|
+
end
|
111
|
+
end
|
112
|
+
#p [:total_prob, total_prob]
|
113
|
+
return total_prob
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
# Pais required to decrease 1 shanten.
|
118
|
+
# Can be multiple pais, but not all multi-pai cases are included.
|
119
|
+
# - included: 45m for 13m
|
120
|
+
# - not included: 2m6s for 23m5s
|
121
|
+
def get_required_pais_candidates()
|
122
|
+
result = Set.new()
|
123
|
+
for mentsus in @shanten_analysis.combinations
|
124
|
+
for janto_index in [nil] + (0...mentsus.size).to_a()
|
125
|
+
t_mentsus = mentsus.dup()
|
126
|
+
if janto_index
|
127
|
+
next if ![:toitsu, :kotsu].include?(mentsus[janto_index][0])
|
128
|
+
t_mentsus.delete_at(janto_index)
|
129
|
+
end
|
130
|
+
num_required_mentsus = @shanten_analysis.pais.size / 3
|
131
|
+
t_shanten =
|
132
|
+
-1 +
|
133
|
+
(janto_index ? 0 : 1) +
|
134
|
+
t_mentsus.
|
135
|
+
map(){ |t, ps| 3 - ps.size }.
|
136
|
+
sort()[0, num_required_mentsus].
|
137
|
+
inject(0, :+)
|
138
|
+
#p [:t_shanten, janto_index, t_shanten, @shanten_analysis.shanten]
|
139
|
+
next if t_shanten != @shanten_analysis.shanten
|
140
|
+
num_groups = t_mentsus.select(){ |t, ps| ps.size >= 2 }.size
|
141
|
+
for type, pais in t_mentsus
|
142
|
+
rnums_cands = []
|
143
|
+
if !janto_index && pais.size == 1
|
144
|
+
# 1 -> janto
|
145
|
+
rnums_cands.push([0])
|
146
|
+
end
|
147
|
+
if !janto_index && pais.size == 2 && num_groups > num_required_mentsus
|
148
|
+
# 2 -> janto
|
149
|
+
case type
|
150
|
+
when :ryanpen
|
151
|
+
rnums_cands.push([0], [1])
|
152
|
+
when :kanta
|
153
|
+
rnums_cands.push([0], [2])
|
154
|
+
end
|
155
|
+
end
|
156
|
+
if pais.size == 2
|
157
|
+
# 2 -> 3
|
158
|
+
case type
|
159
|
+
when :ryanpen
|
160
|
+
rnums_cands.push([-1], [2])
|
161
|
+
when :kanta
|
162
|
+
rnums_cands.push([1], [-2, -1], [3, 4])
|
163
|
+
when :toitsu
|
164
|
+
rnums_cands.push([0], [-2, -1], [-1, 1], [1, 2])
|
165
|
+
else
|
166
|
+
raise("should not happen")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
if pais.size == 1 && num_groups < num_required_mentsus
|
170
|
+
# 1 -> 2
|
171
|
+
rnums_cands.push([-2], [-1], [0], [1], [2])
|
172
|
+
end
|
173
|
+
if pais.size == 1
|
174
|
+
# 1 -> 3
|
175
|
+
rnums_cands.push([-2, -1], [-1, 1], [1, 2], [0, 0])
|
176
|
+
end
|
177
|
+
for rnums in rnums_cands
|
178
|
+
in_range = rnums.all?() do |rn|
|
179
|
+
(rn == 0 || pais[0].type != "t") && (1..9).include?(pais[0].number + rn)
|
180
|
+
end
|
181
|
+
if in_range
|
182
|
+
result.add(rnums.map(){ |rn| Pai.new(pais[0].type, pais[0].number + rn) })
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
return result
|
189
|
+
end
|
190
|
+
|
191
|
+
def get_pai_prob(pai)
|
192
|
+
return (4 - @scene.visible_set[pai]).to_f() / @scene.num_invisible
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.estimate(archive_paths, output_metrics_path)
|
198
|
+
freqs_map = {}
|
199
|
+
archive_paths.each_with_progress() do |path|
|
200
|
+
p [:path, path]
|
201
|
+
archive = Archive.load(path)
|
202
|
+
criteria_map = nil
|
203
|
+
winners = nil
|
204
|
+
archive.each_action() do |action|
|
205
|
+
next if action.actor && ["ASAPIN", "(≧▽≦)"].include?(action.actor.name)
|
206
|
+
archive.dump_action(action)
|
207
|
+
case action.type
|
208
|
+
when :start_kyoku
|
209
|
+
criteria_map = {}
|
210
|
+
winners = []
|
211
|
+
when :dahai
|
212
|
+
shanten_analysis = ShantenAnalysis.new(
|
213
|
+
action.actor.tehais,
|
214
|
+
nil,
|
215
|
+
ShantenAnalysis::ALL_TYPES,
|
216
|
+
action.actor.tehais.size,
|
217
|
+
false)
|
218
|
+
criterion = Criterion.new(
|
219
|
+
archive.num_pipais / 4.0,
|
220
|
+
ShantenAnalysis.new(action.actor.tehais).shanten)
|
221
|
+
p [:criterion, criterion]
|
222
|
+
criteria_map[action.actor] ||= []
|
223
|
+
criteria_map[action.actor].push(criterion)
|
224
|
+
when :hora
|
225
|
+
winners.push(action.actor)
|
226
|
+
when :end_kyoku
|
227
|
+
num_remain_turns = archive.num_pipais / 4.0
|
228
|
+
for player, criteria in criteria_map
|
229
|
+
for criterion in criteria
|
230
|
+
if winners.include?(player)
|
231
|
+
if criterion.num_remain_turns - num_remain_turns <= 2.0
|
232
|
+
result = :quick_hora
|
233
|
+
else
|
234
|
+
result = :slow_hora
|
235
|
+
end
|
236
|
+
else
|
237
|
+
result = :no_hora
|
238
|
+
end
|
239
|
+
normalized_criterion = Criterion.new(
|
240
|
+
criterion.num_remain_turns.to_i(),
|
241
|
+
criterion.shanten)
|
242
|
+
#p [player, normalized_criterion, result]
|
243
|
+
freqs_map[normalized_criterion] ||= Hash.new(0)
|
244
|
+
freqs_map[normalized_criterion][:total] += 1
|
245
|
+
freqs_map[normalized_criterion][result] += 1
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
metrics_map = {}
|
252
|
+
for criterion, freqs in freqs_map
|
253
|
+
metrics_map[criterion] = Metrics.new(
|
254
|
+
(freqs[:quick_hora] + freqs[:slow_hora]).to_f() / freqs[:total],
|
255
|
+
freqs[:quick_hora].to_f() / freqs[:total],
|
256
|
+
freqs[:total])
|
257
|
+
end
|
258
|
+
open(output_metrics_path, "wb") do |f|
|
259
|
+
Marshal.dump(metrics_map, f)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def initialize(metrics_path)
|
264
|
+
open(metrics_path, "rb") do |f|
|
265
|
+
@metrics_map = Marshal.load(f)
|
266
|
+
end
|
267
|
+
adjust()
|
268
|
+
end
|
269
|
+
|
270
|
+
def get_scene(params)
|
271
|
+
return Scene.new(self, params)
|
272
|
+
end
|
273
|
+
|
274
|
+
def get_hora_prob(num_remain_turns, shanten)
|
275
|
+
if shanten <= -1
|
276
|
+
return 1.0
|
277
|
+
elsif num_remain_turns < 0
|
278
|
+
return 0.0
|
279
|
+
else
|
280
|
+
return @metrics_map[Criterion.new(num_remain_turns, shanten)].hora_prob
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def dump_metrics_map()
|
285
|
+
puts("\#turns\tshanten\thora_p\tsamples")
|
286
|
+
for criterion, metrics in @metrics_map.sort_by(){ |c, m| [c.num_remain_turns, c.shanten] }
|
287
|
+
#for criterion, metrics in @metrics_map.sort_by(){ |c, m| [c.shanten, c.num_remain_turns] }
|
288
|
+
puts("%d\t%d\t%.3f\t%p" % [
|
289
|
+
criterion.num_remain_turns,
|
290
|
+
criterion.shanten,
|
291
|
+
metrics.hora_prob,
|
292
|
+
metrics.num_samples,
|
293
|
+
])
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def adjust()
|
298
|
+
for shanten in 0..6
|
299
|
+
adjust_for_sequence(17.downto(0).map(){ |n| Criterion.new(n, shanten) })
|
300
|
+
end
|
301
|
+
for num_remain_turns in 0..17
|
302
|
+
adjust_for_sequence((0..6).map(){ |s| Criterion.new(num_remain_turns, s) })
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def adjust_for_sequence(criteria)
|
307
|
+
prev_prob = 1.0
|
308
|
+
for criterion in criteria
|
309
|
+
metrics = @metrics_map[criterion]
|
310
|
+
if !metrics || metrics.hora_prob > prev_prob
|
311
|
+
#p [criterion, metrics && metrics.hora_prob, prev_prob]
|
312
|
+
@metrics_map[criterion] = Metrics.new(prev_prob, nil, nil)
|
313
|
+
end
|
314
|
+
prev_prob = @metrics_map[criterion].hora_prob
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
|
320
|
+
|
321
|
+
end
|
322
|
+
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
# For compatibility.
|
327
|
+
# TODO Remove this.
|
328
|
+
Mjai::HoraProbabilities = Mjai::Manue::HoraProbabilityEstimator
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
require "mjai/tcp_client_game"
|
4
|
+
require "mjai/manue/player"
|
5
|
+
|
6
|
+
|
7
|
+
module Mjai
|
8
|
+
|
9
|
+
module Manue
|
10
|
+
|
11
|
+
|
12
|
+
class MjaiManueCommand
|
13
|
+
|
14
|
+
def self.execute(argv)
|
15
|
+
Thread.abort_on_exception = true
|
16
|
+
$stdout.sync = true
|
17
|
+
opts = OptionParser.getopts(argv, "", "t:progress_prob", "name:")
|
18
|
+
url = ARGV.shift()
|
19
|
+
game = TCPClientGame.new({
|
20
|
+
:player => Mjai::Manue::Player.new({:score_type => opts["t"].intern()}),
|
21
|
+
:url => url,
|
22
|
+
:name => opts["name"] || "manue",
|
23
|
+
})
|
24
|
+
game.play()
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|