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