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,373 @@
1
+ require "mjai/pai"
2
+ require "mjai/player"
3
+ require "mjai/shanten_analysis"
4
+ require "mjai/manue/danger_estimator"
5
+ require "mjai/manue/hora_probability_estimator"
6
+ require "mjai/manue/hora_points_estimate"
7
+
8
+
9
+ module Mjai
10
+
11
+ module Manue
12
+
13
+
14
+ class Player < Mjai::Player
15
+
16
+ DahaiEval = Struct.new(:cheapness, :prob_info, :points_estimate, :expected_points, :score)
17
+
18
+ class Scene
19
+
20
+ def initialize(params)
21
+
22
+ visible_set = params[:visible_set]
23
+ context = params[:context]
24
+ hora_prob_estimator = params[:hora_prob_estimator]
25
+ num_remain_turns = params[:num_remain_turns]
26
+ current_shanten_analysis = params[:current_shanten_analysis]
27
+ furos = params[:furos]
28
+ sutehai_cands = params[:sutehai_cands]
29
+ score_type = params[:score_type]
30
+ @player = params[:player]
31
+
32
+ tehais = current_shanten_analysis.pais
33
+ scene = hora_prob_estimator.get_scene({
34
+ :visible_set => visible_set,
35
+ :num_remain_turns => num_remain_turns,
36
+ :current_shanten => current_shanten_analysis.shanten,
37
+ })
38
+
39
+ @evals = {}
40
+ for pai in sutehai_cands
41
+ #p [:pai, pai]
42
+ eval = DahaiEval.new()
43
+ if pai
44
+ idx = tehais.index(pai)
45
+ remains = tehais.dup()
46
+ remains.delete_at(idx)
47
+ shanten_analysis = ShantenAnalysis.new(
48
+ remains, current_shanten_analysis.shanten, [:normal])
49
+ eval.cheapness = pai.type == "t" ? 5 : (5 - pai.number).abs
50
+ else
51
+ remains = tehais
52
+ shanten_analysis = current_shanten_analysis
53
+ end
54
+ # TODO Reuse shanten_analysis
55
+ eval.prob_info = scene.get_tehais(remains)
56
+ eval.points_estimate = HoraPointsEstimate.new({
57
+ :shanten_analysis => shanten_analysis,
58
+ :furos => furos,
59
+ :context => context,
60
+ })
61
+ eval.expected_points =
62
+ eval.points_estimate.average_points * eval.prob_info.hora_prob
63
+ case score_type
64
+ when :expected_points
65
+ eval.score =
66
+ [eval.expected_points, eval.prob_info.progress_prob, eval.cheapness]
67
+ when :progress_prob
68
+ eval.score = [eval.prob_info.progress_prob, eval.cheapness]
69
+ else
70
+ raise("unknown score_type")
71
+ end
72
+ if eval.prob_info.progress_prob > 0.0
73
+ log("%s: ept=%d ppr=%.3f hpr=%.3f apt=%d (%s)\n" % [
74
+ pai,
75
+ eval.expected_points,
76
+ eval.prob_info.progress_prob,
77
+ eval.prob_info.hora_prob,
78
+ eval.points_estimate.average_points,
79
+ eval.points_estimate.yaku_debug_str,
80
+ ])
81
+ end
82
+ @evals[pai] = eval
83
+ end
84
+
85
+ max_score = @evals.values.map(){ |e| e.score }.max
86
+ @best_dahais = @evals.keys.select(){ |pai| @evals[pai].score == max_score }
87
+ @best_dahai = @best_dahais[rand(@best_dahais.size)]
88
+
89
+ end
90
+
91
+ attr_reader(:best_dahais, :best_dahai, :evals)
92
+
93
+ def log(text)
94
+ if @player
95
+ @player.log(text)
96
+ else
97
+ print(text)
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ def initialize(params)
104
+ super()
105
+ @score_type = params[:score_type]
106
+ data_dir = File.dirname(__FILE__) + "/../../../share"
107
+ @danger_tree = DangerEstimator::DecisionTree.new("#{data_dir}/danger.all.tree")
108
+ @hora_prob_estimator = HoraProbabilityEstimator.new("#{data_dir}/hora_prob.marshal")
109
+ end
110
+
111
+ def respond_to_action(action)
112
+
113
+ if !action.actor
114
+
115
+ case action.type
116
+ when :start_kyoku
117
+ @prereach_sutehais_map = {}
118
+ end
119
+
120
+ elsif action.actor == self
121
+
122
+ case action.type
123
+
124
+ when :tsumo, :chi, :pon, :reach
125
+
126
+ current_shanten_analysis = ShantenAnalysis.new(self.tehais, nil, [:normal])
127
+ current_shanten = current_shanten_analysis.shanten
128
+ if can_hora?(current_shanten_analysis)
129
+ return create_action({
130
+ :type => :hora,
131
+ :target => action.actor,
132
+ :pai => action.pai,
133
+ })
134
+ elsif can_reach?(current_shanten_analysis)
135
+ return create_action({:type => :reach})
136
+ elsif self.reach?
137
+ return create_action({:type => :dahai, :pai => action.pai, :tsumogiri => true})
138
+ end
139
+ #p [:shanten, current_shanten]
140
+
141
+ if current_shanten == 0
142
+ sutehai_cands = self.possible_dahais
143
+ else
144
+ safe_probs = {}
145
+ for pai in self.possible_dahais
146
+ safe_probs[pai] = 1.0
147
+ end
148
+ has_reacher = false
149
+ for player in self.game.players
150
+ if player != self && player.reach?
151
+ #p [:reacher, player, @prereach_sutehais_map[player]]
152
+ has_reacher = true
153
+ scene = DangerEstimator::Scene.new(
154
+ self.game, self, nil, player, @prereach_sutehais_map[player])
155
+ for pai in safe_probs.keys
156
+ if scene.anpai?(pai)
157
+ safe_prob = 1.0
158
+ else
159
+ safe_prob = 1.0 - @danger_tree.estimate_prob(scene, pai)
160
+ end
161
+ safe_probs[pai] *= safe_prob
162
+ end
163
+ end
164
+ end
165
+ if has_reacher
166
+ for pai, safe_prob in safe_probs
167
+ log("%s: safe_prob=%.3f\n" % [pai, safe_prob])
168
+ end
169
+ end
170
+ max_safe_prob = safe_probs.values.max
171
+ sutehai_cands = safe_probs.keys.select(){ |pai| safe_probs[pai] == max_safe_prob }
172
+ end
173
+ #p [:sutehai_cands, sutehai_cands]
174
+
175
+ scene = get_scene({
176
+ :current_shanten_analysis => current_shanten_analysis,
177
+ :sutehai_cands => sutehai_cands,
178
+ })
179
+
180
+ #p [:dahai, scene.best_dahai]
181
+
182
+ tsumogiri = [:tsumo, :reach].include?(action.type) &&
183
+ scene.best_dahai == self.tehais[-1]
184
+ return create_action({
185
+ :type => :dahai,
186
+ :pai => scene.best_dahai,
187
+ :tsumogiri => tsumogiri,
188
+ })
189
+
190
+ end
191
+
192
+ else # action.actor != self
193
+
194
+ case action.type
195
+ when :dahai
196
+ if self.can_hora?
197
+ return create_action({
198
+ :type => :hora,
199
+ :target => action.actor,
200
+ :pai => action.pai,
201
+ })
202
+ else
203
+ furo_actions = self.possible_furo_actions
204
+ if !furo_actions.empty? &&
205
+ !self.game.players.any?(){ |pl| pl != self && pl.reach_state != :none }
206
+ current_shanten_analysis = ShantenAnalysis.new(self.tehais, nil, [:normal])
207
+ current_scene = get_scene({
208
+ :current_shanten_analysis => current_shanten_analysis,
209
+ :sutehai_cands => [nil],
210
+ })
211
+ current_expected_points = current_scene.evals[nil].expected_points
212
+ for action in furo_actions
213
+ next if action.type == :daiminkan # TODO Implement later
214
+ remains = self.tehais.dup()
215
+ for pai in action.consumed
216
+ remains.delete_at(remains.index(pai))
217
+ end
218
+ furo = Furo.new({
219
+ :type => action.type,
220
+ :taken => action.pai,
221
+ :consumed => action.consumed,
222
+ :target => action.target,
223
+ })
224
+ shanten_analysis_with_furo = ShantenAnalysis.new(remains, nil, [:normal])
225
+ scene_with_furo = get_scene({
226
+ :current_shanten_analysis => shanten_analysis_with_furo,
227
+ :furos => self.furos + [furo],
228
+ :sutehai_cands => remains.uniq(),
229
+ })
230
+ best_eval =
231
+ scene_with_furo.best_dahais.
232
+ map(){ |pai| scene_with_furo.evals[pai] }.
233
+ max_by(){ |e| e.expected_points }
234
+ expected_points_with_furo = best_eval.expected_points
235
+ puts("furo_cand: %s" % action)
236
+ puts(" shanten: %d -> %d" % [
237
+ current_shanten_analysis.shanten,
238
+ shanten_analysis_with_furo.shanten,
239
+ ])
240
+ puts(" ept: %d -> %d" % [current_expected_points, expected_points_with_furo])
241
+ if expected_points_with_furo > current_expected_points
242
+ #gets() # kari
243
+ return action
244
+ end
245
+ end
246
+ end
247
+ end
248
+ when :reach_accepted
249
+ @prereach_sutehais_map[action.actor] = action.actor.sutehais.dup()
250
+ end
251
+
252
+ end
253
+
254
+ return nil
255
+ end
256
+
257
+ def get_scene(params)
258
+ visible = []
259
+ visible += self.game.doras
260
+ visible += self.tehais
261
+ for player in self.game.players
262
+ visible += player.ho + player.furos.map(){ |f| f.pais }.flatten()
263
+ end
264
+ visible_set = to_pai_set(visible)
265
+ default_params = {
266
+ :visible_set => visible_set,
267
+ :context => self.context,
268
+ :hora_prob_estimator => @hora_prob_estimator,
269
+ :num_remain_turns => self.game.num_pipais / 4,
270
+ :furos => self.furos,
271
+ :score_type => @score_type,
272
+ :player => self,
273
+ }
274
+ params = default_params.merge(params)
275
+ # pp params.reject(){ |k, v| [:visible_set, :hora_prob_estimator, :context].include?(k) }
276
+ return Scene.new(params)
277
+ end
278
+
279
+ # This is too slow but left here as most precise baseline.
280
+ def get_hora_prob_with_monte_carlo(tehais, visible_set, num_visible)
281
+ invisibles = []
282
+ for pai in self.game.all_pais.uniq
283
+ next if pai.red?
284
+ (4 - visible_set[pai]).times() do
285
+ invisibles.push(pai)
286
+ end
287
+ end
288
+ num_tsumos = game.num_pipais / 4
289
+ hora_freq = 0
290
+ num_tries = 1000
291
+ num_tries.times() do
292
+ tsumos = invisibles.sample(num_tsumos)
293
+ pais = tehais + tsumos
294
+ #p [:pais, pais.sort().join(" ")]
295
+ can_be = can_be_hora?(pais)
296
+ #p [:can_be, can_be]
297
+ next if !can_be
298
+ shanten = ShantenAnalysis.new(pais, -1, [:normal], 14, false)
299
+ #pp [:shanten, tehais, tsumos, shanten.shanten]
300
+ #if shanten.shanten == -1
301
+ # pp [:comb, shanten.combinations[0]]
302
+ #end
303
+ hora_freq += 1 if shanten.shanten == -1
304
+ end
305
+ return hora_freq.to_f() / num_tries
306
+ end
307
+
308
+ def can_be_hora?(pais)
309
+ pai_set = to_pai_set(pais)
310
+ kotsus = pai_set.select(){ |pai, c| c >= 3 }
311
+ toitsus = pai_set.select(){ |pai, c| c >= 2 }
312
+ num_cont = 1
313
+ # TODO 重複を考慮
314
+ num_shuntsus = 0
315
+ pais.map(){ |pai| pai.remove_red() }.sort().uniq().each_cons(2) do |prev_pai, pai|
316
+ if pai.type != "t" && pai.type == prev_pai.type && pai.number == prev_pai.number + 1
317
+ num_cont += 1
318
+ if num_cont >= 3
319
+ num_shuntsus += 1
320
+ num_cont = 0
321
+ end
322
+ else
323
+ num_cont = 1
324
+ end
325
+ end
326
+ return kotsus.size + num_shuntsus >= 4 && toitsus.size >= 1
327
+ end
328
+
329
+ def to_pai_set(pais)
330
+ pai_set = Hash.new(0)
331
+ for pai in pais
332
+ pai_set[pai.remove_red()] += 1
333
+ end
334
+ return pai_set
335
+ end
336
+
337
+ def random_test()
338
+ all_pais = (["m", "p", "s"].map(){ |t| (1..9).map(){ |n| Pai.new(t, n) } }.flatten() +
339
+ (1..7).map(){ |n| Pai.new("t", n) }) * 4
340
+ while true
341
+ pais = all_pais.sample(13).sort()
342
+ puts(pais.join(" "))
343
+ (nj, nm, jimp, mimp) = get_improvers(pais)
344
+ p [nj, nm]
345
+ for name, imp in [["jimp", jimp], ["mimp", mimp]]
346
+ for pais in imp.to_a().sort()
347
+ puts("%s: %s" % [name, pais.join(" ")])
348
+ end
349
+ end
350
+ gets()
351
+ end
352
+ end
353
+
354
+ end
355
+
356
+ class MockGame
357
+
358
+ def initialize()
359
+ pais = (0...4).map() do |i|
360
+ ["m", "p", "s"].map(){ |t| (1..9).map(){ |n| Pai.new(t, n, n == 5 && i == 0) } } +
361
+ (1..7).map(){ |n| Pai.new("t", n) }
362
+ end
363
+ @all_pais = pais.flatten().sort()
364
+ end
365
+
366
+ attr_reader(:all_pais)
367
+
368
+ end
369
+
370
+
371
+ end
372
+
373
+ end
Binary file
Binary file
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mjai-manue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Hiroshi Ichikawa
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mjai
16
+ requirement: &87123830 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.0.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *87123830
25
+ description: Japanese Mahjong AI.
26
+ email:
27
+ - gimite+github@gmail.com
28
+ executables:
29
+ - mjai-manue
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - bin/mjai-manue
34
+ - lib/mjai/manue/hora_probability_estimator.rb
35
+ - lib/mjai/manue/mjai_manue_command.rb
36
+ - lib/mjai/manue/player.rb
37
+ - lib/mjai/manue/hora_points_estimate.rb
38
+ - lib/mjai/manue/danger_estimator.rb
39
+ - share/hora_prob.marshal
40
+ - share/danger.all.tree
41
+ homepage: https://github.com/gimite/mjai-manue
42
+ licenses: []
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 1.8.11
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Japanese Mahjong AI.
65
+ test_files: []