mjai-manue 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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