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,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: []