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