mjai 0.0.5 → 0.0.6
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.
- data/lib/mjai/active_game.rb +27 -2
- data/lib/mjai/game.rb +15 -1
- data/lib/mjai/game_stats.rb +181 -7
- data/lib/mjai/player.rb +11 -1
- data/lib/mjai/tcp_game_server.rb +1 -1
- data/lib/mjai/tenhou_archive.rb +2 -2
- metadata +2 -2
data/lib/mjai/active_game.rb
CHANGED
@@ -62,7 +62,7 @@ module Mjai
|
|
62
62
|
mota()
|
63
63
|
@actor = @players[(@actor.id + 1) % 4]
|
64
64
|
end
|
65
|
-
|
65
|
+
process_fanpai()
|
66
66
|
end
|
67
67
|
do_action({:type => :end_kyoku})
|
68
68
|
end
|
@@ -78,6 +78,10 @@ module Mjai
|
|
78
78
|
# actions.size >= 2 in case of double/triple ron
|
79
79
|
process_hora(actions)
|
80
80
|
throw(:end_kyoku)
|
81
|
+
elsif actions[0].type == :ryukyoku
|
82
|
+
raise("should not happen") if actions.size != 1
|
83
|
+
process_kyushukyuhai(actions[0].actor)
|
84
|
+
throw(:end_kyoku)
|
81
85
|
else
|
82
86
|
raise("should not happen") if actions.size != 1
|
83
87
|
action = actions[0]
|
@@ -178,7 +182,28 @@ module Mjai
|
|
178
182
|
update_oya(actions.any?(){ |a| a.actor == self.oya }, false)
|
179
183
|
end
|
180
184
|
|
181
|
-
def
|
185
|
+
def process_kyushukyuhai(actor)
|
186
|
+
tehais = []
|
187
|
+
for player in players
|
188
|
+
if player == actor
|
189
|
+
tehais.push(player.tehais)
|
190
|
+
else
|
191
|
+
tehais.push([Pai::UNKNOWN] * player.tehais.size)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
do_action({
|
195
|
+
:type => :ryukyoku,
|
196
|
+
:actor => actor,
|
197
|
+
:reason => :kyushukyuhai,
|
198
|
+
:tenpais => [false, false, false, false],
|
199
|
+
:tehais => tehais,
|
200
|
+
:deltas => [0, 0, 0, 0],
|
201
|
+
:scores => players.map(){ |player| player.score }
|
202
|
+
})
|
203
|
+
update_oya(true, true)
|
204
|
+
end
|
205
|
+
|
206
|
+
def process_fanpai()
|
182
207
|
tenpais = []
|
183
208
|
tehais = []
|
184
209
|
for player in players
|
data/lib/mjai/game.rb
CHANGED
@@ -157,6 +157,15 @@ module Mjai
|
|
157
157
|
else
|
158
158
|
return action
|
159
159
|
end
|
160
|
+
when :reach
|
161
|
+
if action.actor == player
|
162
|
+
return action.merge({
|
163
|
+
:cannot_dahai =>
|
164
|
+
with_response_hint ? (player.tehais.uniq() - player.possible_dahais) : nil,
|
165
|
+
})
|
166
|
+
else
|
167
|
+
return action
|
168
|
+
end
|
160
169
|
else
|
161
170
|
return action
|
162
171
|
end
|
@@ -191,7 +200,7 @@ module Mjai
|
|
191
200
|
when :tsumo
|
192
201
|
if is_actor
|
193
202
|
valid = response &&
|
194
|
-
[:dahai, :reach, :ankan, :kakan, :hora].include?(response.type)
|
203
|
+
[:dahai, :reach, :ankan, :kakan, :hora, :ryukyoku].include?(response.type)
|
195
204
|
else
|
196
205
|
valid = !response
|
197
206
|
end
|
@@ -306,6 +315,11 @@ module Mjai
|
|
306
315
|
end
|
307
316
|
validate(response.actor.can_hora?, "Cannot hora.")
|
308
317
|
|
318
|
+
when :ryukyoku
|
319
|
+
validate_fields_exist(response, [:reason])
|
320
|
+
validate(response.reason == :kyushukyuhai, "reason must be kyushukyuhai.")
|
321
|
+
validate(response.actor.can_ryukyoku?, "Cannot ryukyoku.")
|
322
|
+
|
309
323
|
end
|
310
324
|
|
311
325
|
end
|
data/lib/mjai/game_stats.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
1
3
|
require "mjai/archive"
|
2
4
|
require "mjai/confidence_interval"
|
3
5
|
|
@@ -6,32 +8,135 @@ module Mjai
|
|
6
8
|
|
7
9
|
class GameStats
|
8
10
|
|
11
|
+
YAKU_JA_NAMES = {
|
12
|
+
:menzenchin_tsumoho => "面前清自摸和", :reach => "立直", :ippatsu => "一発",
|
13
|
+
:chankan => "槍槓", :rinshankaiho => "嶺上開花", :haiteiraoyue => "海底摸月",
|
14
|
+
:hoteiraoyui => "河底撈魚", :pinfu => "平和", :tanyaochu => "断么九",
|
15
|
+
:ipeko => "一盃口", :jikaze => "面風牌", :bakaze => "圏風牌",
|
16
|
+
:sangenpai => "三元牌", :double_reach => "ダブル立直", :chitoitsu => "七対子",
|
17
|
+
:honchantaiyao => "混全帯么九", :ikkitsukan => "一気通貫",
|
18
|
+
:sanshokudojun => "三色同順", :sanshokudoko => "三色同刻", :sankantsu => "三槓子",
|
19
|
+
:toitoiho => "対々和", :sananko => "三暗刻", :shosangen => "小三元",
|
20
|
+
:honroto => "混老頭", :ryanpeko => "二盃口", :junchantaiyao => "純全帯么九",
|
21
|
+
:honiso => "混一色", :chiniso => "清一色", :renho => "人和", :tenho => "天和",
|
22
|
+
:chiho => "地和", :daisangen => "大三元", :suanko => "四暗刻",
|
23
|
+
:tsuiso => "字一色", :ryuiso => "緑一色", :chinroto => "清老頭",
|
24
|
+
:churenpoton => "九蓮宝燈", :kokushimuso => "国士無双",
|
25
|
+
:daisushi => "大四喜", :shosushi => "小四喜", :sukantsu => "四槓子",
|
26
|
+
:dora => "ドラ", :uradora => "裏ドラ", :akadora => "赤ドラ",
|
27
|
+
}
|
28
|
+
|
9
29
|
def self.print(mjson_paths)
|
30
|
+
|
10
31
|
num_errors = 0
|
11
32
|
name_to_ranks = {}
|
33
|
+
name_to_scores = {}
|
34
|
+
name_to_kyoku_count = {}
|
35
|
+
name_to_hora_count = {}
|
36
|
+
name_to_yaku_stats = {}
|
37
|
+
name_to_dora_stats = {}
|
38
|
+
name_to_hoju_count = {}
|
39
|
+
name_to_furo_kyoku_count = {}
|
40
|
+
name_to_reach_count = {}
|
41
|
+
name_to_hora_points = {}
|
42
|
+
|
12
43
|
for path in mjson_paths
|
44
|
+
|
13
45
|
archive = Archive.load(path)
|
14
46
|
first_action = archive.raw_actions[0]
|
15
47
|
last_action = archive.raw_actions[-1]
|
16
|
-
|
17
|
-
if last_action.type != :end_game
|
48
|
+
if !last_action || last_action.type != :end_game
|
18
49
|
num_errors += 1
|
19
50
|
next
|
20
51
|
end
|
52
|
+
archive.do_action(first_action)
|
53
|
+
|
54
|
+
scores = last_action.scores
|
55
|
+
id_to_name = first_action.names
|
56
|
+
|
21
57
|
chicha_id = archive.raw_actions[1].oya.id
|
22
58
|
ranked_player_ids =
|
23
|
-
(0...4).sort_by(){ |i| [-
|
59
|
+
(0...4).sort_by(){ |i| [-scores[i], (i + 4 - chicha_id) % 4] }
|
24
60
|
for r in 0...4
|
25
|
-
name =
|
61
|
+
name = id_to_name[ranked_player_ids[r]]
|
26
62
|
name_to_ranks[name] ||= []
|
27
63
|
name_to_ranks[name].push(r + 1)
|
28
64
|
end
|
65
|
+
|
66
|
+
for p in 0...4
|
67
|
+
name = id_to_name[p]
|
68
|
+
name_to_scores[name] ||= []
|
69
|
+
name_to_scores[name].push(scores[p])
|
70
|
+
end
|
71
|
+
|
72
|
+
# Kyoku specific fields.
|
73
|
+
id_to_done_reach = {}
|
74
|
+
id_to_done_furo = {}
|
75
|
+
for raw_action in archive.raw_actions
|
76
|
+
if raw_action.type == :hora
|
77
|
+
name = id_to_name[raw_action.actor.id]
|
78
|
+
name_to_hora_count[name] ||= 0
|
79
|
+
name_to_hora_count[name] += 1
|
80
|
+
name_to_hora_points[name] ||= []
|
81
|
+
name_to_hora_points[name].push(raw_action.hora_points)
|
82
|
+
for yaku, fan in raw_action.yakus
|
83
|
+
if yaku == :dora || yaku == :akadora || yaku == :uradora
|
84
|
+
name_to_dora_stats[name] ||= {}
|
85
|
+
name_to_dora_stats[name][yaku] ||= 0
|
86
|
+
name_to_dora_stats[name][yaku] += fan
|
87
|
+
next
|
88
|
+
end
|
89
|
+
name_to_yaku_stats[name] ||= {}
|
90
|
+
name_to_yaku_stats[name][yaku] ||= 0
|
91
|
+
name_to_yaku_stats[name][yaku] += 1
|
92
|
+
end
|
93
|
+
if raw_action.actor.id != raw_action.target.id
|
94
|
+
target_name = id_to_name[raw_action.target.id]
|
95
|
+
name_to_hoju_count[target_name] ||= 0
|
96
|
+
name_to_hoju_count[target_name] += 1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
if raw_action.type == :reach_accepted
|
100
|
+
id_to_done_reach[raw_action.actor.id] = true
|
101
|
+
end
|
102
|
+
if raw_action.type == :pon
|
103
|
+
id_to_done_furo[raw_action.actor.id] = true
|
104
|
+
end
|
105
|
+
if raw_action.type == :chi
|
106
|
+
id_to_done_furo[raw_action.actor.id] = true
|
107
|
+
end
|
108
|
+
if raw_action.type == :daiminkan
|
109
|
+
id_to_done_furo[raw_action.actor.id] = true
|
110
|
+
end
|
111
|
+
if raw_action.type == :end_kyoku
|
112
|
+
for p in 0...4
|
113
|
+
name = id_to_name[p]
|
114
|
+
|
115
|
+
if id_to_done_furo[p]
|
116
|
+
name_to_furo_kyoku_count[name] ||= 0
|
117
|
+
name_to_furo_kyoku_count[name] += 1
|
118
|
+
end
|
119
|
+
if id_to_done_reach[p]
|
120
|
+
name_to_reach_count[name] ||= 0
|
121
|
+
name_to_reach_count[name] += 1
|
122
|
+
end
|
123
|
+
|
124
|
+
name_to_kyoku_count[name] ||= 0
|
125
|
+
name_to_kyoku_count[name] += 1
|
126
|
+
end
|
127
|
+
|
128
|
+
# Reset kyoku specific fields.
|
129
|
+
id_to_done_furo = {}
|
130
|
+
id_to_done_reach = {}
|
131
|
+
end
|
132
|
+
end
|
29
133
|
end
|
30
134
|
if num_errors > 0
|
31
135
|
puts("errors: %d / %d" % [num_errors, mjson_paths.size])
|
32
136
|
end
|
33
|
-
|
34
|
-
|
137
|
+
|
138
|
+
puts("Ranks:")
|
139
|
+
for name, ranks in name_to_ranks.sort
|
35
140
|
rank_conf_interval = ConfidenceInterval.calculate(ranks, :min => 1.0, :max => 4.0)
|
36
141
|
puts(" %s: %.3f [%.3f, %.3f]" % [
|
37
142
|
name,
|
@@ -40,8 +145,77 @@ module Mjai
|
|
40
145
|
rank_conf_interval[1],
|
41
146
|
])
|
42
147
|
end
|
43
|
-
end
|
44
148
|
|
149
|
+
puts("Scores:")
|
150
|
+
for name, scores in name_to_scores.sort
|
151
|
+
puts(" %s: %d" % [
|
152
|
+
name,
|
153
|
+
scores.inject(0, :+).to_i() / scores.size,
|
154
|
+
])
|
155
|
+
end
|
156
|
+
|
157
|
+
puts("Hora rates:")
|
158
|
+
for name, hora_count in name_to_hora_count.sort
|
159
|
+
puts(" %s: %.1f%%" % [
|
160
|
+
name,
|
161
|
+
100.0 * hora_count / name_to_kyoku_count[name]
|
162
|
+
])
|
163
|
+
end
|
164
|
+
|
165
|
+
puts("Hoju rates:")
|
166
|
+
for name, hoju_count in name_to_hoju_count.sort
|
167
|
+
puts(" %s: %.1f%%" % [
|
168
|
+
name,
|
169
|
+
100.0 * hoju_count / name_to_kyoku_count[name]
|
170
|
+
])
|
171
|
+
end
|
172
|
+
|
173
|
+
puts("Furo rates:")
|
174
|
+
for name, furo_kyoku_count in name_to_furo_kyoku_count.sort
|
175
|
+
puts(" %s: %.1f%%" % [
|
176
|
+
name,
|
177
|
+
100.0 * furo_kyoku_count / name_to_kyoku_count[name]
|
178
|
+
])
|
179
|
+
end
|
180
|
+
|
181
|
+
puts("Reach rates:")
|
182
|
+
for name, reach_count in name_to_reach_count.sort
|
183
|
+
puts(" %s: %.1f%%" % [
|
184
|
+
name,
|
185
|
+
100.0 * reach_count / name_to_kyoku_count[name]
|
186
|
+
])
|
187
|
+
end
|
188
|
+
|
189
|
+
puts("Average hora points:")
|
190
|
+
for name, hora_points in name_to_hora_points.sort
|
191
|
+
puts(" %s: %d" % [
|
192
|
+
name,
|
193
|
+
hora_points.inject(0, :+).to_i() / hora_points.size,
|
194
|
+
])
|
195
|
+
end
|
196
|
+
|
197
|
+
puts("Yaku stats:")
|
198
|
+
for name, yaku_stats in name_to_yaku_stats.sort
|
199
|
+
hora_count = name_to_hora_count[name]
|
200
|
+
puts(" %s (%d horas):" % [name, hora_count])
|
201
|
+
for yaku, count in yaku_stats.sort_by{|yaku, count| -count}
|
202
|
+
yaku_name = YAKU_JA_NAMES[yaku]
|
203
|
+
puts(" %s: %d (%.1f%%)" % [yaku_name, count, 100.0 * count / hora_count])
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
puts("Dora stats:")
|
208
|
+
for name, dora_stats in name_to_dora_stats.sort
|
209
|
+
hora_count = name_to_hora_count[name]
|
210
|
+
puts(" %s (%d horas):" % [name, hora_count])
|
211
|
+
for dora, count in dora_stats.sort_by{|dora, count| -count}
|
212
|
+
dora_name = YAKU_JA_NAMES[dora]
|
213
|
+
puts(" %s: %d (%.3f/hora)" % [dora_name, count, count.to_f() / hora_count])
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
45
219
|
end
|
46
220
|
|
47
221
|
end
|
data/lib/mjai/player.rb
CHANGED
@@ -192,7 +192,14 @@ module Mjai
|
|
192
192
|
@game.get_hora(hora_action, {:previous_action => action}).valid? &&
|
193
193
|
(hora_type == :tsumo || !self.furiten?)
|
194
194
|
end
|
195
|
-
|
195
|
+
|
196
|
+
def can_ryukyoku?
|
197
|
+
return @game.current_action.type == :tsumo &&
|
198
|
+
@game.current_action.actor == self &&
|
199
|
+
@game.first_turn? &&
|
200
|
+
@tehais.select(){ |pai| pai.yaochu? }.uniq().size >= 9
|
201
|
+
end
|
202
|
+
|
196
203
|
# Possible actions except for dahai.
|
197
204
|
def possible_actions
|
198
205
|
action = @game.current_action
|
@@ -209,6 +216,9 @@ module Mjai
|
|
209
216
|
if can_reach?
|
210
217
|
result.push(create_action({:type => :reach}))
|
211
218
|
end
|
219
|
+
if can_ryukyoku?
|
220
|
+
result.push(create_action({:type => :ryukyoku, :reason => :kyushukyuhai}))
|
221
|
+
end
|
212
222
|
end
|
213
223
|
result += self.possible_furo_actions
|
214
224
|
return result
|
data/lib/mjai/tcp_game_server.rb
CHANGED
data/lib/mjai/tenhou_archive.rb
CHANGED
@@ -170,7 +170,7 @@ module Mjai
|
|
170
170
|
})
|
171
171
|
if elem["owari"]
|
172
172
|
do_action({:type => :end_kyoku})
|
173
|
-
do_action({:type => :end_game})
|
173
|
+
do_action({:type => :end_game, :scores => points_params[:scores]})
|
174
174
|
end
|
175
175
|
return nil
|
176
176
|
when "RYUUKYOKU"
|
@@ -209,7 +209,7 @@ module Mjai
|
|
209
209
|
})
|
210
210
|
if elem["owari"]
|
211
211
|
do_action({:type => :end_kyoku})
|
212
|
-
do_action({:type => :end_game})
|
212
|
+
do_action({:type => :end_game, :scores => points_params[:scores]})
|
213
213
|
end
|
214
214
|
return nil
|
215
215
|
when "N"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mjai
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-12-
|
12
|
+
date: 2013-12-31 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|