mjai-manue 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,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: []
|