mjai 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/mjai/action.rb +1 -0
- data/lib/mjai/active_game.rb +54 -17
- data/lib/mjai/archive.rb +4 -0
- data/lib/mjai/confidence_interval.rb +37 -0
- data/lib/mjai/game.rb +36 -10
- data/lib/mjai/mjai_command.rb +2 -2
- data/lib/mjai/pai.rb +28 -1
- data/lib/mjai/player.rb +37 -5
- data/lib/mjai/shanten_player.rb +4 -11
- data/lib/mjai/tcp_active_game_server.rb +91 -0
- data/lib/mjai/tcp_game_server.rb +19 -63
- data/lib/mjai/tenhou_archive.rb +41 -5
- data/share/html/css/style.css +4 -0
- data/share/html/css/style.scss +9 -0
- data/share/html/js/archive_player.coffee +15 -7
- data/share/html/js/archive_player.js +16 -8
- data/share/html/views/archive_player.erb +9 -1
- metadata +42 -9
data/lib/mjai/action.rb
CHANGED
data/lib/mjai/active_game.rb
CHANGED
@@ -28,10 +28,11 @@ module Mjai
|
|
28
28
|
@ag_oya = @ag_chicha = @players[0]
|
29
29
|
@ag_bakaze = Pai.new("E")
|
30
30
|
@ag_honba = 0
|
31
|
+
@ag_kyotaku = 0
|
31
32
|
while !self.game_finished?
|
32
33
|
play_kyoku()
|
33
34
|
end
|
34
|
-
do_action({:type => :end_game})
|
35
|
+
do_action({:type => :end_game, :scores => get_final_scores()})
|
35
36
|
return true
|
36
37
|
rescue ValidationError => ex
|
37
38
|
do_action({:type => :error, :message => ex.message})
|
@@ -51,6 +52,7 @@ module Mjai
|
|
51
52
|
:bakaze => @ag_bakaze,
|
52
53
|
:kyoku => (4 + @ag_oya.id - @ag_chicha.id) % 4 + 1,
|
53
54
|
:honba => @ag_honba,
|
55
|
+
:kyotaku => @ag_kyotaku,
|
54
56
|
:oya => @ag_oya,
|
55
57
|
:dora_marker => dora_marker,
|
56
58
|
:tehais => tehais,
|
@@ -67,7 +69,8 @@ module Mjai
|
|
67
69
|
|
68
70
|
# 摸打
|
69
71
|
def mota()
|
70
|
-
|
72
|
+
reach_pending = false
|
73
|
+
kandora_pending = false
|
71
74
|
tsumo_actor = @actor
|
72
75
|
actions = [Action.new({:type => :tsumo, :actor => @actor, :pai => @pipais.pop()})]
|
73
76
|
while !actions.empty?
|
@@ -79,27 +82,43 @@ module Mjai
|
|
79
82
|
raise("should not happen") if actions.size != 1
|
80
83
|
action = actions[0]
|
81
84
|
responses = do_action(action)
|
85
|
+
next_actions = nil
|
82
86
|
case action.type
|
83
87
|
when :daiminkan, :kakan, :ankan
|
88
|
+
if action.type == :ankan
|
89
|
+
add_dora()
|
90
|
+
end
|
84
91
|
# Actually takes one from wanpai and moves one pai from pipai to wanpai,
|
85
92
|
# but it's equivalent to taking from pipai.
|
86
|
-
|
93
|
+
next_actions =
|
87
94
|
[Action.new({:type => :tsumo, :actor => action.actor, :pai => @pipais.pop()})]
|
88
|
-
# TODO
|
89
|
-
next
|
95
|
+
# TODO Handle 4 kans.
|
90
96
|
when :reach
|
91
|
-
|
97
|
+
reach_pending = true
|
92
98
|
end
|
93
|
-
|
94
|
-
if
|
99
|
+
next_actions ||= choose_actions(responses)
|
100
|
+
if reach_pending &&
|
101
|
+
(next_actions.empty? || ![:dahai, :hora].include?(next_actions[0].type))
|
102
|
+
@ag_kyotaku += 1
|
95
103
|
deltas = [0, 0, 0, 0]
|
96
104
|
deltas[tsumo_actor.id] = -1000
|
97
105
|
do_action({
|
98
|
-
:type => :reach_accepted
|
106
|
+
:type => :reach_accepted,
|
107
|
+
:actor => tsumo_actor,
|
99
108
|
:deltas => deltas,
|
100
109
|
:scores => get_scores(deltas),
|
101
110
|
})
|
111
|
+
reach_pending = false
|
102
112
|
end
|
113
|
+
if kandora_pending &&
|
114
|
+
!next_actions.empty? && [:dahai, :tsumo].include?(next_actions[0].type)
|
115
|
+
add_dora()
|
116
|
+
kandora_pending = false
|
117
|
+
end
|
118
|
+
if [:daiminkan, :kakan].include?(action.type)
|
119
|
+
kandora_pending = true
|
120
|
+
end
|
121
|
+
actions = next_actions
|
103
122
|
end
|
104
123
|
end
|
105
124
|
end
|
@@ -119,22 +138,24 @@ module Mjai
|
|
119
138
|
end
|
120
139
|
|
121
140
|
def process_hora(actions)
|
122
|
-
|
123
|
-
for action in actions
|
124
|
-
|
141
|
+
tsumibo = self.honba
|
142
|
+
for action in actions.sort_by(){ |a| distance(a.actor, a.target) }
|
143
|
+
uradora_markers = action.actor.reach? ? @wanpais.pop(self.dora_markers.size) : []
|
144
|
+
hora = get_hora(action, {
|
145
|
+
:uradora_markers => uradora_markers,
|
146
|
+
:previous_action => self.previous_action,
|
147
|
+
})
|
125
148
|
raise("no yaku") if !hora.valid?
|
126
149
|
deltas = [0, 0, 0, 0]
|
127
|
-
|
128
|
-
deltas[action.actor.id] += hora.points + self.honba * 300
|
129
|
-
deltas[action.actor.id] += self.players.select(){ |pl| pl.reach? }.size * 1000
|
150
|
+
deltas[action.actor.id] += hora.points + tsumibo * 300 + @ag_kyotaku * 1000
|
130
151
|
if hora.hora_type == :tsumo
|
131
152
|
for player in self.players
|
132
153
|
next if player == action.actor
|
133
154
|
deltas[player.id] -=
|
134
|
-
((player == self.oya ? hora.oya_payment : hora.ko_payment) +
|
155
|
+
((player == self.oya ? hora.oya_payment : hora.ko_payment) + tsumibo * 100)
|
135
156
|
end
|
136
157
|
else
|
137
|
-
deltas[action.target.id] -= (hora.points +
|
158
|
+
deltas[action.target.id] -= (hora.points + tsumibo * 300)
|
138
159
|
end
|
139
160
|
do_action({
|
140
161
|
:type => action.type,
|
@@ -142,6 +163,7 @@ module Mjai
|
|
142
163
|
:target => action.target,
|
143
164
|
:pai => action.pai,
|
144
165
|
:hora_tehais => action.actor.tehais,
|
166
|
+
:uradora_markers => uradora_markers,
|
145
167
|
:yakus => hora.yakus,
|
146
168
|
:fu => hora.fu,
|
147
169
|
:fan => hora.fan,
|
@@ -149,6 +171,9 @@ module Mjai
|
|
149
171
|
:deltas => deltas,
|
150
172
|
:scores => get_scores(deltas),
|
151
173
|
})
|
174
|
+
# Only kamicha takes them in case of daburon.
|
175
|
+
tsumibo = 0
|
176
|
+
@ag_kyotaku = 0
|
152
177
|
end
|
153
178
|
update_oya(actions.any?(){ |a| a.actor == self.oya }, false)
|
154
179
|
end
|
@@ -208,6 +233,11 @@ module Mjai
|
|
208
233
|
end
|
209
234
|
end
|
210
235
|
|
236
|
+
def add_dora()
|
237
|
+
dora_marker = @wanpais.pop()
|
238
|
+
do_action({:type => :dora, :dora_marker => dora_marker})
|
239
|
+
end
|
240
|
+
|
211
241
|
def game_finished?
|
212
242
|
if @last
|
213
243
|
return true
|
@@ -217,6 +247,13 @@ module Mjai
|
|
217
247
|
end
|
218
248
|
end
|
219
249
|
|
250
|
+
def get_final_scores()
|
251
|
+
# The winner takes remaining kyotaku.
|
252
|
+
deltas = [0, 0, 0, 0]
|
253
|
+
deltas[self.ranked_players[0].id] = @ag_kyotaku * 1000
|
254
|
+
return get_scores(deltas)
|
255
|
+
end
|
256
|
+
|
220
257
|
def expect_response_from?(player)
|
221
258
|
return true
|
222
259
|
end
|
data/lib/mjai/archive.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Mjai
|
2
|
+
|
3
|
+
module ConfidenceInterval
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Uses bootstrap resampling.
|
8
|
+
def calculate(samples, params = {})
|
9
|
+
params = {:min => 0.0, :max => 1.0, :conf_level => 0.95}.merge(params)
|
10
|
+
num_tries = 1000
|
11
|
+
averages = []
|
12
|
+
num_tries.times() do
|
13
|
+
sum = 0.0
|
14
|
+
(samples.size + 2).times() do
|
15
|
+
idx = rand(samples.size + 2)
|
16
|
+
case idx
|
17
|
+
when samples.size
|
18
|
+
sum += params[:min]
|
19
|
+
when samples.size + 1
|
20
|
+
sum += params[:max]
|
21
|
+
else
|
22
|
+
sum += samples[idx]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
averages.push(sum / (samples.size + 2))
|
26
|
+
end
|
27
|
+
averages.sort!()
|
28
|
+
margin = (1.0 - params[:conf_level]) / 2
|
29
|
+
return [
|
30
|
+
averages[(num_tries * margin).to_i()],
|
31
|
+
averages[(num_tries * (1.0 - margin)).to_i()],
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/lib/mjai/game.rb
CHANGED
@@ -20,6 +20,8 @@ module Mjai
|
|
20
20
|
@current_action = nil
|
21
21
|
@previous_action = nil
|
22
22
|
@num_pipais = nil
|
23
|
+
@num_initial_pipais = nil
|
24
|
+
@first_turn = false
|
23
25
|
end
|
24
26
|
|
25
27
|
attr_reader(:players)
|
@@ -96,9 +98,15 @@ module Mjai
|
|
96
98
|
@oya = action.oya
|
97
99
|
@chicha ||= @oya
|
98
100
|
@dora_markers = [action.dora_marker]
|
99
|
-
@num_pipais = @all_pais.size - 13 * 4 - 14
|
101
|
+
@num_pipais = @num_initial_pipais = @all_pais.size - 13 * 4 - 14
|
102
|
+
@first_turn = true
|
100
103
|
when :tsumo
|
101
104
|
@num_pipais -= 1
|
105
|
+
if @num_initial_pipais - @num_pipais > 4
|
106
|
+
@first_turn = false
|
107
|
+
end
|
108
|
+
when :chi, :pon, :daiminkan, :kakan, :ankan
|
109
|
+
@first_turn = false
|
102
110
|
when :dora
|
103
111
|
@dora_markers.push(action.dora_marker)
|
104
112
|
end
|
@@ -183,6 +191,7 @@ module Mjai
|
|
183
191
|
# Actor should wait for tsumo.
|
184
192
|
valid = !response
|
185
193
|
else
|
194
|
+
# hora is for chankan.
|
186
195
|
valid = !response || response.type == :hora
|
187
196
|
end
|
188
197
|
when :log
|
@@ -204,10 +213,18 @@ module Mjai
|
|
204
213
|
case response.type
|
205
214
|
|
206
215
|
when :dahai
|
216
|
+
|
207
217
|
validate_fields_exist(response, [:pai, :tsumogiri])
|
218
|
+
if action.actor.reach?
|
219
|
+
# possible_dahais check doesn't subsume this check. Consider karagiri
|
220
|
+
# (with tsumogiri=false) after reach.
|
221
|
+
validate(response.tsumogiri, "tsumogiri must be true after reach.")
|
222
|
+
end
|
208
223
|
validate(
|
209
224
|
response.actor.possible_dahais.include?(response.pai),
|
210
|
-
"Cannot dahai this pai.")
|
225
|
+
"Cannot dahai this pai. The pai is not in the tehais, or it's kuikae.")
|
226
|
+
|
227
|
+
# Validates that pai and tsumogiri fields are consistent.
|
211
228
|
if [:tsumo, :reach].include?(action.type)
|
212
229
|
if response.tsumogiri
|
213
230
|
tsumo_pai = response.actor.tehais[-1]
|
@@ -284,7 +301,7 @@ module Mjai
|
|
284
301
|
return @dora_markers ? @dora_markers.map(){ |pai| pai.succ } : nil
|
285
302
|
end
|
286
303
|
|
287
|
-
def get_hora(action)
|
304
|
+
def get_hora(action, params = {})
|
288
305
|
raise("should not happen") if action.type != :hora
|
289
306
|
hora_type = action.actor == action.target ? :tsumo : :ron
|
290
307
|
if hora_type == :tsumo
|
@@ -292,6 +309,7 @@ module Mjai
|
|
292
309
|
else
|
293
310
|
tehais = action.actor.tehais
|
294
311
|
end
|
312
|
+
uradoras = (params[:uradora_markers] || []).map(){ |pai| pai.succ }
|
295
313
|
return Hora.new({
|
296
314
|
:tehais => tehais,
|
297
315
|
:furos => action.actor.furos,
|
@@ -301,19 +319,27 @@ module Mjai
|
|
301
319
|
:bakaze => self.bakaze,
|
302
320
|
:jikaze => action.actor.jikaze,
|
303
321
|
:doras => self.doras,
|
304
|
-
:uradoras =>
|
322
|
+
:uradoras => uradoras,
|
305
323
|
:reach => action.actor.reach?,
|
306
|
-
:double_reach =>
|
307
|
-
:ippatsu =>
|
308
|
-
:rinshan =>
|
324
|
+
:double_reach => action.actor.double_reach?,
|
325
|
+
:ippatsu => action.actor.ippatsu_chance?,
|
326
|
+
:rinshan => action.actor.rinshan?,
|
309
327
|
:haitei => self.num_pipais == 0,
|
310
|
-
:first_turn =>
|
311
|
-
:chankan =>
|
328
|
+
:first_turn => @first_turn,
|
329
|
+
:chankan => params[:previous_action].type == :kakan,
|
312
330
|
})
|
313
331
|
end
|
314
332
|
|
333
|
+
def first_turn?
|
334
|
+
return @first_turn
|
335
|
+
end
|
336
|
+
|
315
337
|
def ranked_players
|
316
|
-
return @players.sort_by(){ |pl| [-pl.score, (
|
338
|
+
return @players.sort_by(){ |pl| [-pl.score, distance(pl, @chicha)] }
|
339
|
+
end
|
340
|
+
|
341
|
+
def distance(player1, player2)
|
342
|
+
return (4 + player1.id - player2.id) % 4
|
317
343
|
end
|
318
344
|
|
319
345
|
def dump_action(action, io = $stdout)
|
data/lib/mjai/mjai_command.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require "optparse"
|
2
2
|
|
3
|
-
require "mjai/
|
3
|
+
require "mjai/tcp_active_game_server"
|
4
4
|
require "mjai/tcp_client_game"
|
5
5
|
require "mjai/tsumogiri_player"
|
6
6
|
require "mjai/shanten_player"
|
@@ -30,7 +30,7 @@ module Mjai
|
|
30
30
|
else
|
31
31
|
num_games = opts["games"].to_i()
|
32
32
|
end
|
33
|
-
server =
|
33
|
+
server = TCPActiveGameServer.new({
|
34
34
|
:host => opts["host"],
|
35
35
|
:port => opts["port"].to_i(),
|
36
36
|
:room => opts["room"],
|
data/lib/mjai/pai.rb
CHANGED
@@ -57,6 +57,17 @@ module Mjai
|
|
57
57
|
else
|
58
58
|
raise(ArgumentError, "Wrong number of args.")
|
59
59
|
end
|
60
|
+
if @type != nil || @number != nil
|
61
|
+
if !["m", "p", "s", "t"].include?(@type)
|
62
|
+
raise("Bad type: %p" % @type)
|
63
|
+
end
|
64
|
+
if !@number.is_a?(Integer)
|
65
|
+
raise("number must be Integer: %p" % @number)
|
66
|
+
end
|
67
|
+
if @red != true && @red != false
|
68
|
+
raise("red must be boolean: %p" % @red)
|
69
|
+
end
|
70
|
+
end
|
60
71
|
end
|
61
72
|
|
62
73
|
def to_s()
|
@@ -75,6 +86,16 @@ module Mjai
|
|
75
86
|
|
76
87
|
attr_reader(:type, :number)
|
77
88
|
|
89
|
+
def valid?
|
90
|
+
if @type == nil && @number == nil
|
91
|
+
return true
|
92
|
+
elsif @type == "t"
|
93
|
+
return (1..7).include?(@number)
|
94
|
+
else
|
95
|
+
return (1..9).include?(@number)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
78
99
|
def red?
|
79
100
|
return @red
|
80
101
|
end
|
@@ -91,6 +112,10 @@ module Mjai
|
|
91
112
|
return @type == "t" && (5..7).include?(@number)
|
92
113
|
end
|
93
114
|
|
115
|
+
def next(n)
|
116
|
+
return Pai.new(@type, @number + n)
|
117
|
+
end
|
118
|
+
|
94
119
|
def data
|
95
120
|
return [@type || "", @number || -1, @red ? 1 : 0]
|
96
121
|
end
|
@@ -123,8 +148,10 @@ module Mjai
|
|
123
148
|
|
124
149
|
# Next pai in terms of dora derivation.
|
125
150
|
def succ
|
126
|
-
if (@type == "t" && @number ==
|
151
|
+
if (@type == "t" && @number == 4) || (@type != "t" && @number == 9)
|
127
152
|
number = 1
|
153
|
+
elsif @type == "t" && @number == 7
|
154
|
+
number = 5
|
128
155
|
else
|
129
156
|
number = @number + 1
|
130
157
|
end
|
data/lib/mjai/player.rb
CHANGED
@@ -34,6 +34,18 @@ module Mjai
|
|
34
34
|
return @reach_state == :accepted
|
35
35
|
end
|
36
36
|
|
37
|
+
def double_reach?
|
38
|
+
return @double_reach
|
39
|
+
end
|
40
|
+
|
41
|
+
def ippatsu_chance?
|
42
|
+
return @ippatsu_chance
|
43
|
+
end
|
44
|
+
|
45
|
+
def rinshan?
|
46
|
+
return @rinshan
|
47
|
+
end
|
48
|
+
|
37
49
|
def update_state(action)
|
38
50
|
|
39
51
|
if @game.previous_action &&
|
@@ -56,6 +68,9 @@ module Mjai
|
|
56
68
|
@extra_anpais = nil
|
57
69
|
@reach_state = nil
|
58
70
|
@reach_ho_index = nil
|
71
|
+
@double_reach = false
|
72
|
+
@ippatsu_chance = false
|
73
|
+
@rinshan = false
|
59
74
|
when :start_kyoku
|
60
75
|
@tehais = action.tehais[self.id]
|
61
76
|
@furos = []
|
@@ -64,6 +79,11 @@ module Mjai
|
|
64
79
|
@extra_anpais = []
|
65
80
|
@reach_state = :none
|
66
81
|
@reach_ho_index = nil
|
82
|
+
@double_reach = false
|
83
|
+
@ippatsu_chance = false
|
84
|
+
@rinshan = false
|
85
|
+
when :chi, :pon, :daiminkan, :kakan, :ankan
|
86
|
+
@ippatsu_chance = false
|
67
87
|
end
|
68
88
|
|
69
89
|
if action.actor == self
|
@@ -75,6 +95,8 @@ module Mjai
|
|
75
95
|
@tehais.sort!()
|
76
96
|
@ho.push(action.pai)
|
77
97
|
@sutehais.push(action.pai)
|
98
|
+
@ippatsu_chance = false
|
99
|
+
@rinshan = false
|
78
100
|
@extra_anpais.clear() if !self.reach?
|
79
101
|
when :chi, :pon, :daiminkan, :ankan
|
80
102
|
for pai in action.consumed
|
@@ -86,6 +108,9 @@ module Mjai
|
|
86
108
|
:consumed => action.consumed,
|
87
109
|
:target => action.target,
|
88
110
|
}))
|
111
|
+
if [:daiminkan, :ankan].include?(action.type)
|
112
|
+
@rinshan = true
|
113
|
+
end
|
89
114
|
when :kakan
|
90
115
|
delete_tehai(action.pai)
|
91
116
|
pon_index =
|
@@ -97,17 +122,20 @@ module Mjai
|
|
97
122
|
:consumed => @furos[pon_index].consumed + [action.pai],
|
98
123
|
:target => @furos[pon_index].target,
|
99
124
|
})
|
125
|
+
@rinshan = true
|
100
126
|
when :reach
|
101
127
|
@reach_state = :declared
|
128
|
+
@double_reach = true if @game.first_turn?
|
102
129
|
when :reach_accepted
|
103
130
|
@reach_state = :accepted
|
104
131
|
@reach_ho_index = @ho.size - 1
|
132
|
+
@ippatsu_chance = true
|
105
133
|
end
|
106
134
|
end
|
107
135
|
|
108
136
|
if action.target == self
|
109
137
|
case action.type
|
110
|
-
when :chi, :pon, :daiminkan
|
138
|
+
when :chi, :pon, :daiminkan
|
111
139
|
pai = @ho.pop()
|
112
140
|
raise("should not happen") if pai != action.pai
|
113
141
|
end
|
@@ -156,7 +184,7 @@ module Mjai
|
|
156
184
|
if action.type == :tsumo && action.actor == self
|
157
185
|
hora_type = :tsumo
|
158
186
|
pais = @tehais
|
159
|
-
elsif action.type
|
187
|
+
elsif [:dahai, :kakan].include?(action.type) && action.actor != self
|
160
188
|
hora_type = :ron
|
161
189
|
pais = @tehais + [action.pai]
|
162
190
|
else
|
@@ -166,7 +194,7 @@ module Mjai
|
|
166
194
|
hora_action =
|
167
195
|
create_action({:type => :hora, :target => action.actor, :pai => pais[-1]})
|
168
196
|
return shanten_analysis.shanten == -1 &&
|
169
|
-
@game.get_hora(hora_action).valid? &&
|
197
|
+
@game.get_hora(hora_action, {:previous_action => action}).valid? &&
|
170
198
|
(hora_type == :tsumo || !self.furiten?)
|
171
199
|
end
|
172
200
|
|
@@ -259,9 +287,13 @@ module Mjai
|
|
259
287
|
end
|
260
288
|
|
261
289
|
def possible_dahais(action = @game.current_action, tehais = @tehais)
|
290
|
+
if self.reach? && action.type == :tsumo && action.actor == self
|
291
|
+
return [action.pai]
|
292
|
+
end
|
262
293
|
# Excludes kuikae.
|
294
|
+
consumed = action.consumed ? action.consumed.sort() : nil
|
263
295
|
if action.type == :chi && action.actor == self
|
264
|
-
if
|
296
|
+
if consumed[1].number == consumed[0].number + 1
|
265
297
|
forbidden_rnums = [-1, 2]
|
266
298
|
else
|
267
299
|
forbidden_rnums = [1]
|
@@ -273,7 +305,7 @@ module Mjai
|
|
273
305
|
end
|
274
306
|
cands = tehais.uniq()
|
275
307
|
if !forbidden_rnums.empty?
|
276
|
-
key_pai =
|
308
|
+
key_pai = consumed[0]
|
277
309
|
return cands.select() do |pai|
|
278
310
|
!(pai.type == key_pai.type &&
|
279
311
|
forbidden_rnums.any?(){ |rn| key_pai.number + rn == pai.number })
|
data/lib/mjai/shanten_player.rb
CHANGED
@@ -38,17 +38,10 @@ module Mjai
|
|
38
38
|
return create_action({:type => :dahai, :pai => action.pai, :tsumogiri => true})
|
39
39
|
end
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
end
|
47
|
-
pon = self.furos.find(){ |f| f.type == :pon && f.taken == action.pai }
|
48
|
-
if pon
|
49
|
-
return create_action(
|
50
|
-
{:type => :kakan, :pai => action.pai, :consumed => [action.pai] * 3})
|
51
|
-
end
|
41
|
+
# Ankan, kakan
|
42
|
+
furo_actions = self.possible_furo_actions
|
43
|
+
if !furo_actions.empty?
|
44
|
+
return furo_actions[0]
|
52
45
|
end
|
53
46
|
|
54
47
|
sutehai_cands = []
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "mjai/active_game"
|
2
|
+
require "mjai/tcp_game_server"
|
3
|
+
require "mjai/confidence_interval"
|
4
|
+
|
5
|
+
|
6
|
+
module Mjai
|
7
|
+
|
8
|
+
class TCPActiveGameServer < TCPGameServer
|
9
|
+
|
10
|
+
Statistics = Struct.new(:num_games, :total_rank, :total_score, :ranks)
|
11
|
+
|
12
|
+
def initialize(params)
|
13
|
+
super
|
14
|
+
@name_to_stat = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def num_tcp_players
|
18
|
+
return 4
|
19
|
+
end
|
20
|
+
|
21
|
+
def play_game(players)
|
22
|
+
|
23
|
+
if self.params[:log_dir]
|
24
|
+
mjson_path = "%s/%s.mjson" % [self.params[:log_dir], Time.now.strftime("%Y-%m-%d-%H%M%S")]
|
25
|
+
else
|
26
|
+
mjson_path = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
maybe_open(mjson_path, "w") do |mjson_out|
|
30
|
+
mjson_out.sync = true if mjson_out
|
31
|
+
game = ActiveGame.new(players)
|
32
|
+
game.game_type = self.params[:game_type]
|
33
|
+
game.on_action() do |action|
|
34
|
+
mjson_out.puts(action.to_json()) if mjson_out
|
35
|
+
game.dump_action(action)
|
36
|
+
end
|
37
|
+
success = game.play()
|
38
|
+
return [game, success]
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_game_succeed(game)
|
44
|
+
puts("game %d: %s" % [
|
45
|
+
self.num_finished_games,
|
46
|
+
game.ranked_players.map(){ |pl| "%s:%d" % [pl.name, pl.score] }.join(" "),
|
47
|
+
])
|
48
|
+
for player in self.players
|
49
|
+
@name_to_stat[player.name] ||= Statistics.new(0, 0, 0, [])
|
50
|
+
@name_to_stat[player.name].num_games += 1
|
51
|
+
@name_to_stat[player.name].total_score += player.score
|
52
|
+
@name_to_stat[player.name].total_rank += player.rank
|
53
|
+
@name_to_stat[player.name].ranks.push(player.rank)
|
54
|
+
end
|
55
|
+
names = self.players.map(){ |pl| pl.name }.sort().uniq()
|
56
|
+
print("Average rank:")
|
57
|
+
for name in names
|
58
|
+
stat = @name_to_stat[name]
|
59
|
+
rank_conf_interval = ConfidenceInterval.calculate(stat.ranks, :min => 1.0, :max => 4.0)
|
60
|
+
print(" %s:%.3f [%.3f, %.3f]" % [
|
61
|
+
name,
|
62
|
+
stat.total_rank.to_f() / stat.num_games,
|
63
|
+
rank_conf_interval[0],
|
64
|
+
rank_conf_interval[1],
|
65
|
+
])
|
66
|
+
end
|
67
|
+
puts()
|
68
|
+
print("Average score:")
|
69
|
+
for name in names
|
70
|
+
print(" %s:%d" % [
|
71
|
+
name,
|
72
|
+
@name_to_stat[name].total_score.to_f() / @name_to_stat[name].num_games,
|
73
|
+
])
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def on_game_fail(game)
|
78
|
+
puts("game %d: Ended with error" % self.num_finished_games)
|
79
|
+
end
|
80
|
+
|
81
|
+
def maybe_open(path, mode, &block)
|
82
|
+
if path
|
83
|
+
open(path, mode, &block)
|
84
|
+
else
|
85
|
+
yield(nil)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/lib/mjai/tcp_game_server.rb
CHANGED
@@ -4,7 +4,6 @@ require "thread"
|
|
4
4
|
require "rubygems"
|
5
5
|
require "json"
|
6
6
|
|
7
|
-
require "mjai/active_game"
|
8
7
|
require "mjai/tcp_player"
|
9
8
|
|
10
9
|
|
@@ -12,21 +11,20 @@ module Mjai
|
|
12
11
|
|
13
12
|
class TCPGameServer
|
14
13
|
|
15
|
-
Statistics = Struct.new(:num_games, :total_rank, :total_score)
|
16
|
-
|
17
14
|
def initialize(params)
|
18
15
|
@params = params
|
19
16
|
@server = TCPServer.open(params[:host], params[:port])
|
20
17
|
@players = []
|
21
18
|
@mutex = Mutex.new()
|
22
19
|
@num_finished_games = 0
|
23
|
-
@name_to_stat = {}
|
24
20
|
end
|
25
21
|
|
22
|
+
attr_reader(:params, :players, :num_finished_games)
|
23
|
+
|
26
24
|
def run()
|
27
|
-
puts("Listening on host %s, port %d" % [@params[:host],
|
25
|
+
puts("Listening on host %s, port %d" % [@params[:host], self.port])
|
28
26
|
puts("URL: %s" % self.server_url)
|
29
|
-
puts("Waiting for
|
27
|
+
puts("Waiting for %d players..." % self.num_tcp_players)
|
30
28
|
@pids = []
|
31
29
|
begin
|
32
30
|
start_default_players()
|
@@ -46,11 +44,11 @@ module Mjai
|
|
46
44
|
if message["type"] == "join" && message["name"] && message["room"]
|
47
45
|
if message["room"] == @params[:room]
|
48
46
|
@mutex.synchronize() do
|
49
|
-
if @players.size <
|
47
|
+
if @players.size < self.num_tcp_players
|
50
48
|
@players.push(TCPPlayer.new(socket, message["name"]))
|
51
|
-
puts("Waiting for %s more players..." % (
|
52
|
-
if @players.size ==
|
53
|
-
Thread.new(){
|
49
|
+
puts("Waiting for %s more players..." % (self.num_tcp_players - @players.size))
|
50
|
+
if @players.size == self.num_tcp_players
|
51
|
+
Thread.new(){ process_one_game() }
|
54
52
|
end
|
55
53
|
else
|
56
54
|
error = "The room is busy. Retry after a while."
|
@@ -84,26 +82,12 @@ module Mjai
|
|
84
82
|
end
|
85
83
|
end
|
86
84
|
|
87
|
-
def
|
88
|
-
|
89
|
-
if @params[:log_dir]
|
90
|
-
mjson_path = "%s/%s.mjson" % [@params[:log_dir], Time.now.strftime("%Y-%m-%d-%H%M%S")]
|
91
|
-
else
|
92
|
-
mjson_path = nil
|
93
|
-
end
|
85
|
+
def process_one_game()
|
94
86
|
|
87
|
+
game = nil
|
95
88
|
success = false
|
96
89
|
begin
|
97
|
-
|
98
|
-
mjson_out.sync = true if mjson_out
|
99
|
-
@game = ActiveGame.new(@players)
|
100
|
-
@game.game_type = @params[:game_type]
|
101
|
-
@game.on_action() do |action|
|
102
|
-
mjson_out.puts(action.to_json()) if mjson_out
|
103
|
-
@game.dump_action(action)
|
104
|
-
end
|
105
|
-
success = @game.play()
|
106
|
-
end
|
90
|
+
(game, success) = play_game(@players)
|
107
91
|
rescue => ex
|
108
92
|
print_backtrace(ex)
|
109
93
|
end
|
@@ -127,34 +111,9 @@ module Mjai
|
|
127
111
|
@num_finished_games += 1
|
128
112
|
|
129
113
|
if success
|
130
|
-
|
131
|
-
@num_finished_games,
|
132
|
-
@game.ranked_players.map(){ |pl| "%s:%d" % [pl.name, pl.score] }.join(" "),
|
133
|
-
])
|
134
|
-
for player in @players
|
135
|
-
@name_to_stat[player.name] ||= Statistics.new(0, 0, 0)
|
136
|
-
@name_to_stat[player.name].num_games += 1
|
137
|
-
@name_to_stat[player.name].total_score += player.score
|
138
|
-
@name_to_stat[player.name].total_rank += player.rank
|
139
|
-
end
|
140
|
-
names = @players.map(){ |pl| pl.name }.sort().uniq()
|
141
|
-
print("Average rank:")
|
142
|
-
for name in names
|
143
|
-
print(" %s:%.3f" % [
|
144
|
-
name,
|
145
|
-
@name_to_stat[name].total_rank.to_f() / @name_to_stat[name].num_games,
|
146
|
-
])
|
147
|
-
end
|
148
|
-
puts()
|
149
|
-
print("Average score:")
|
150
|
-
for name in names
|
151
|
-
print(" %s:%d" % [
|
152
|
-
name,
|
153
|
-
@name_to_stat[name].total_score.to_f() / @name_to_stat[name].num_games,
|
154
|
-
])
|
155
|
-
end
|
114
|
+
on_game_succeed(game)
|
156
115
|
else
|
157
|
-
|
116
|
+
on_game_fail(game)
|
158
117
|
end
|
159
118
|
puts()
|
160
119
|
|
@@ -165,10 +124,15 @@ module Mjai
|
|
165
124
|
else
|
166
125
|
start_default_players()
|
167
126
|
end
|
127
|
+
|
168
128
|
end
|
169
129
|
|
170
130
|
def server_url
|
171
|
-
return "mjsonp://localhost:%d/%s" % [
|
131
|
+
return "mjsonp://localhost:%d/%s" % [self.port, @params[:room]]
|
132
|
+
end
|
133
|
+
|
134
|
+
def port
|
135
|
+
return @server.addr[1]
|
172
136
|
end
|
173
137
|
|
174
138
|
def start_default_players()
|
@@ -185,14 +149,6 @@ module Mjai
|
|
185
149
|
socket.puts(line)
|
186
150
|
end
|
187
151
|
|
188
|
-
def maybe_open(path, mode, &block)
|
189
|
-
if path
|
190
|
-
open(path, mode, &block)
|
191
|
-
else
|
192
|
-
yield(nil)
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
152
|
def print_backtrace(ex, io = $stderr)
|
197
153
|
io.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
|
198
154
|
for s in ex.backtrace[1..-1]
|
data/lib/mjai/tenhou_archive.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# Reference: http://tenhou.net/1/script/tenhou.js
|
2
|
+
|
1
3
|
require "zlib"
|
2
4
|
require "uri"
|
3
5
|
require "nokogiri"
|
@@ -14,6 +16,23 @@ module Mjai
|
|
14
16
|
|
15
17
|
module Util
|
16
18
|
|
19
|
+
YAKU_ID_TO_NAME = [
|
20
|
+
:menzenchin_tsumoho, :reach, :ippatsu, :chankan, :rinshankaiho,
|
21
|
+
:haiteiraoyue, :hoteiraoyui, :pinfu, :tanyaochu, :ipeko,
|
22
|
+
:jikaze, :jikaze, :jikaze, :jikaze,
|
23
|
+
:bakaze, :bakaze, :bakaze, :bakaze,
|
24
|
+
:sangenpai, :sangenpai, :sangenpai,
|
25
|
+
:double_reach, :chitoitsu, :honchantaiyao, :ikkitsukan, :sanshokudojun,
|
26
|
+
:sanshokudoko, :sankantsu, :toitoiho, :sananko, :shosangen, :honroto,
|
27
|
+
:ryanpeko, :junchantaiyao, :honiso,
|
28
|
+
:chiniso,
|
29
|
+
:renho,
|
30
|
+
:tenho, :chiho, :daisangen, :suanko, :suanko, :tsuiso,
|
31
|
+
:ryuiso, :chinroto, :churenpoton, :churenpoton, :kokushimuso,
|
32
|
+
:kokushimuso, :daisushi, :shosushi, :sukantsu,
|
33
|
+
:dora, :uradora, :akadora,
|
34
|
+
]
|
35
|
+
|
17
36
|
def on_tenhou_event(elem, next_elem = nil)
|
18
37
|
verify_tenhou_tehais() if @first_kyoku_started
|
19
38
|
case elem.name
|
@@ -98,7 +117,10 @@ module Mjai
|
|
98
117
|
when "2"
|
99
118
|
deltas = [0, 0, 0, 0]
|
100
119
|
deltas[actor.id] = -1000
|
101
|
-
|
120
|
+
# Old Tenhou archive doesn't have "ten" attribute. Calculates it manually.
|
121
|
+
scores = (0...4).map() do |i|
|
122
|
+
self.players[i].score + deltas[i]
|
123
|
+
end
|
102
124
|
return do_action({
|
103
125
|
:type => :reach_accepted,
|
104
126
|
:actor => actor,
|
@@ -112,10 +134,18 @@ module Mjai
|
|
112
134
|
tehais = (elem["hai"].split(/,/) - [elem["machi"]]).map(){ |pid| pid_to_pai(pid) }
|
113
135
|
points_params = get_points_params(elem["sc"])
|
114
136
|
(fu, hora_points, _) = elem["ten"].split(/,/).map(&:to_i)
|
115
|
-
|
137
|
+
if elem["yakuman"]
|
138
|
+
fan = Hora::YAKUMAN_FAN
|
139
|
+
else
|
140
|
+
fan = elem["yaku"].split(/,/).each_slice(2).map(){ |y, f| f.to_i() }.inject(0, :+)
|
141
|
+
end
|
116
142
|
uradora_markers = (elem["doraHaiUra"] || "").
|
117
143
|
split(/,/).map(){ |pid| pid_to_pai(pid) }
|
118
|
-
|
144
|
+
yakus = elem["yaku"].
|
145
|
+
split(/,/).
|
146
|
+
enum_for(:each_slice, 2).
|
147
|
+
map(){ |y, f| [YAKU_ID_TO_NAME[y.to_i()], f.to_i()] }.
|
148
|
+
select(){ |y, f| f != 0 }
|
119
149
|
do_action({
|
120
150
|
:type => :hora,
|
121
151
|
:actor => self.players[elem["who"].to_i()],
|
@@ -125,6 +155,7 @@ module Mjai
|
|
125
155
|
:uradora_markers => uradora_markers,
|
126
156
|
:fu => fu,
|
127
157
|
:fan => fan,
|
158
|
+
:yakus => yakus,
|
128
159
|
:hora_points => hora_points,
|
129
160
|
:deltas => points_params[:deltas],
|
130
161
|
:scores => points_params[:scores],
|
@@ -401,8 +432,13 @@ module Mjai
|
|
401
432
|
@doc = Nokogiri.XML(@xml)
|
402
433
|
elems = @doc.root.children
|
403
434
|
elems.each_with_index() do |elem, j|
|
404
|
-
|
405
|
-
|
435
|
+
begin
|
436
|
+
if on_tenhou_event(elem, elems[j + 1]) == :broken
|
437
|
+
break # Something is wrong.
|
438
|
+
end
|
439
|
+
rescue
|
440
|
+
$stderr.puts("While interpreting element: %s" % elem)
|
441
|
+
raise
|
406
442
|
end
|
407
443
|
end
|
408
444
|
end
|
data/share/html/css/style.css
CHANGED
data/share/html/css/style.scss
CHANGED
@@ -43,24 +43,28 @@ $player-height: $pai-height * 4 + $ho-tehai-margin;
|
|
43
43
|
left: 0px;
|
44
44
|
top: $board-width - $player-height;
|
45
45
|
-webkit-transform: rotate(0deg);
|
46
|
+
transform: rotate(0deg);
|
46
47
|
}
|
47
48
|
|
48
49
|
.player-1 {
|
49
50
|
left: ($board-width - $player-height / 2) - $board-width / 2;
|
50
51
|
top: $board-width / 2 - $player-height / 2;
|
51
52
|
-webkit-transform: rotate(270deg);
|
53
|
+
transform: rotate(270deg);
|
52
54
|
}
|
53
55
|
|
54
56
|
.player-2 {
|
55
57
|
left: 0px;
|
56
58
|
top: 0px;
|
57
59
|
-webkit-transform: rotate(180deg);
|
60
|
+
transform: rotate(180deg);
|
58
61
|
}
|
59
62
|
|
60
63
|
.player-3 {
|
61
64
|
left: $player-height / 2 - $board-width / 2;
|
62
65
|
top: $board-width / 2 - $player-height / 2;
|
63
66
|
-webkit-transform: rotate(90deg);
|
67
|
+
transform: rotate(90deg);
|
64
68
|
}
|
65
69
|
|
66
70
|
.wanpais-container {
|
@@ -104,3 +108,8 @@ $player-height: $pai-height * 4 + $ho-tehai-margin;
|
|
104
108
|
top: 0px;
|
105
109
|
padding: 8px;
|
106
110
|
}
|
111
|
+
|
112
|
+
.log-label {
|
113
|
+
font-family: monospace;
|
114
|
+
white-space: pre;
|
115
|
+
}
|
@@ -22,6 +22,7 @@ BAKAZE_TO_STR =
|
|
22
22
|
kyokus = []
|
23
23
|
currentKyokuId = 0
|
24
24
|
currentActionId = 0
|
25
|
+
currentViewpoint = 0
|
25
26
|
playerInfos = [{}, {}, {}, {}]
|
26
27
|
|
27
28
|
parsePai = (pai) ->
|
@@ -128,7 +129,6 @@ loadAction = (action) ->
|
|
128
129
|
when "start_kyoku"
|
129
130
|
kyoku =
|
130
131
|
actions: []
|
131
|
-
doraMarkers: [action.dora_marker]
|
132
132
|
bakaze: action.bakaze
|
133
133
|
kyokuNum: action.kyoku
|
134
134
|
honba: action.honba
|
@@ -136,6 +136,7 @@ loadAction = (action) ->
|
|
136
136
|
prevBoard = board
|
137
137
|
board =
|
138
138
|
players: [{}, {}, {}, {}]
|
139
|
+
doraMarkers: [action.dora_marker]
|
139
140
|
initPlayers(board)
|
140
141
|
for i in [0...4]
|
141
142
|
board.players[i].tehais = action.tehais[i]
|
@@ -198,7 +199,7 @@ loadAction = (action) ->
|
|
198
199
|
|
199
200
|
if kyoku
|
200
201
|
for i in [0...4]
|
201
|
-
if i != action.actor
|
202
|
+
if action.actor != undefined && i != action.actor
|
202
203
|
ripai(board.players[i])
|
203
204
|
if action.type != "log"
|
204
205
|
action.board = board
|
@@ -265,16 +266,19 @@ renderAction = (action) ->
|
|
265
266
|
#console.log(action.type, action)
|
266
267
|
displayAction = {}
|
267
268
|
for k, v of action
|
268
|
-
if k != "board"
|
269
|
+
if k != "board" && k != "log"
|
269
270
|
displayAction[k] = v
|
270
271
|
$("#action-label").text(JSON.stringify(displayAction))
|
272
|
+
$("#log-label").text(action.log || "")
|
271
273
|
#dumpBoard(action.board)
|
272
274
|
kyoku = getCurrentKyoku()
|
273
275
|
for i in [0...4]
|
274
276
|
player = action.board.players[i]
|
275
|
-
view = Dytem.players.at(i)
|
277
|
+
view = Dytem.players.at((i - currentViewpoint + 4) % 4)
|
276
278
|
infoView = Dytem.playerInfos.at(i)
|
277
279
|
infoView.score.text(player.score)
|
280
|
+
infoView.viewpoint.text(if i == currentViewpoint then "+" else "")
|
281
|
+
|
278
282
|
if !player.tehais
|
279
283
|
renderPais([], view.tehais)
|
280
284
|
view.tsumoPai.hide()
|
@@ -311,8 +315,8 @@ renderAction = (action) ->
|
|
311
315
|
renderPais(pais, furoView.pais, poses)
|
312
316
|
--j
|
313
317
|
wanpais = ["?", "?", "?", "?", "?", "?"]
|
314
|
-
for i in [0...
|
315
|
-
wanpais[i + 2] =
|
318
|
+
for i in [0...action.board.doraMarkers.length]
|
319
|
+
wanpais[i + 2] = action.board.doraMarkers[i]
|
316
320
|
renderPais(wanpais, Dytem.wanpais)
|
317
321
|
|
318
322
|
getCurrentKyoku = ->
|
@@ -353,7 +357,11 @@ $ ->
|
|
353
357
|
currentKyokuId = parseInt($("#kyokuSelector").val())
|
354
358
|
currentActionId = 0
|
355
359
|
renderCurrentAction()
|
356
|
-
|
360
|
+
|
361
|
+
$("#viewpoint-button").click ->
|
362
|
+
currentViewpoint = (currentViewpoint + 1) % 4
|
363
|
+
renderCurrentAction()
|
364
|
+
|
357
365
|
for action in allActions
|
358
366
|
loadAction(action)
|
359
367
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
var BAKAZE_TO_STR, TSUPAIS, TSUPAI_TO_IMAGE_NAME, cloneBoard, comparePais, currentActionId, currentKyokuId, deleteTehai, dumpBoard, getCurrentKyoku, goBack, goNext, initPlayers, kyokus, loadAction, paiToImageUrl, parsePai, playerInfos, removeRed, renderAction, renderCurrentAction, renderHo, renderPai, renderPais, ripai, sortPais, _base, _base2;
|
1
|
+
var BAKAZE_TO_STR, TSUPAIS, TSUPAI_TO_IMAGE_NAME, cloneBoard, comparePais, currentActionId, currentKyokuId, currentViewpoint, deleteTehai, dumpBoard, getCurrentKyoku, goBack, goNext, initPlayers, kyokus, loadAction, paiToImageUrl, parsePai, playerInfos, removeRed, renderAction, renderCurrentAction, renderHo, renderPai, renderPais, ripai, sortPais, _base, _base2;
|
2
2
|
|
3
3
|
window.console || (window.console = {});
|
4
4
|
|
@@ -31,6 +31,8 @@ currentKyokuId = 0;
|
|
31
31
|
|
32
32
|
currentActionId = 0;
|
33
33
|
|
34
|
+
currentViewpoint = 0;
|
35
|
+
|
34
36
|
playerInfos = [{}, {}, {}, {}];
|
35
37
|
|
36
38
|
parsePai = function(pai) {
|
@@ -169,7 +171,6 @@ loadAction = function(action) {
|
|
169
171
|
case "start_kyoku":
|
170
172
|
kyoku = {
|
171
173
|
actions: [],
|
172
|
-
doraMarkers: [action.dora_marker],
|
173
174
|
bakaze: action.bakaze,
|
174
175
|
kyokuNum: action.kyoku,
|
175
176
|
honba: action.honba
|
@@ -177,7 +178,8 @@ loadAction = function(action) {
|
|
177
178
|
kyokus.push(kyoku);
|
178
179
|
prevBoard = board;
|
179
180
|
board = {
|
180
|
-
players: [{}, {}, {}, {}]
|
181
|
+
players: [{}, {}, {}, {}],
|
182
|
+
doraMarkers: [action.dora_marker]
|
181
183
|
};
|
182
184
|
initPlayers(board);
|
183
185
|
for (i = 0; i < 4; i++) {
|
@@ -271,7 +273,7 @@ loadAction = function(action) {
|
|
271
273
|
}
|
272
274
|
if (kyoku) {
|
273
275
|
for (i = 0; i < 4; i++) {
|
274
|
-
if (i !== action.actor) ripai(board.players[i]);
|
276
|
+
if (action.actor !== void 0 && i !== action.actor) ripai(board.players[i]);
|
275
277
|
}
|
276
278
|
if (action.type !== "log") {
|
277
279
|
action.board = board;
|
@@ -378,15 +380,17 @@ renderAction = function(action) {
|
|
378
380
|
displayAction = {};
|
379
381
|
for (k in action) {
|
380
382
|
v = action[k];
|
381
|
-
if (k !== "board") displayAction[k] = v;
|
383
|
+
if (k !== "board" && k !== "log") displayAction[k] = v;
|
382
384
|
}
|
383
385
|
$("#action-label").text(JSON.stringify(displayAction));
|
386
|
+
$("#log-label").text(action.log || "");
|
384
387
|
kyoku = getCurrentKyoku();
|
385
388
|
for (i = 0; i < 4; i++) {
|
386
389
|
player = action.board.players[i];
|
387
|
-
view = Dytem.players.at(i);
|
390
|
+
view = Dytem.players.at((i - currentViewpoint + 4) % 4);
|
388
391
|
infoView = Dytem.playerInfos.at(i);
|
389
392
|
infoView.score.text(player.score);
|
393
|
+
infoView.viewpoint.text(i === currentViewpoint ? "+" : "");
|
390
394
|
if (!player.tehais) {
|
391
395
|
renderPais([], view.tehais);
|
392
396
|
view.tsumoPai.hide();
|
@@ -429,8 +433,8 @@ renderAction = function(action) {
|
|
429
433
|
}
|
430
434
|
}
|
431
435
|
wanpais = ["?", "?", "?", "?", "?", "?"];
|
432
|
-
for (i = 0, _ref4 =
|
433
|
-
wanpais[i + 2] =
|
436
|
+
for (i = 0, _ref4 = action.board.doraMarkers.length; 0 <= _ref4 ? i < _ref4 : i > _ref4; 0 <= _ref4 ? i++ : i--) {
|
437
|
+
wanpais[i + 2] = action.board.doraMarkers[i];
|
434
438
|
}
|
435
439
|
return renderPais(wanpais, Dytem.wanpais);
|
436
440
|
};
|
@@ -478,6 +482,10 @@ $(function() {
|
|
478
482
|
currentActionId = 0;
|
479
483
|
return renderCurrentAction();
|
480
484
|
});
|
485
|
+
$("#viewpoint-button").click(function() {
|
486
|
+
currentViewpoint = (currentViewpoint + 1) % 4;
|
487
|
+
return renderCurrentAction();
|
488
|
+
});
|
481
489
|
for (_i = 0, _len = allActions.length; _i < _len; _i++) {
|
482
490
|
action = allActions[_i];
|
483
491
|
loadAction(action);
|
@@ -45,17 +45,25 @@
|
|
45
45
|
<div>
|
46
46
|
<button id="prev-button">Prev</button>
|
47
47
|
<button id="next-button">Next</button>
|
48
|
-
<input id="action-id-label" type="text" size="3">
|
48
|
+
<input id="action-id-label" type="text" size="3" value="0">
|
49
49
|
<button id="go-button">Go</button>
|
50
50
|
</div>
|
51
51
|
<table border="0">
|
52
|
+
<tr>
|
53
|
+
<th>#</th>
|
54
|
+
<th>Name</th>
|
55
|
+
<th>Score</th>
|
56
|
+
<th><button id="viewpoint-button">Viewpoint</button></th>
|
57
|
+
</tr>
|
52
58
|
<tr id="playerInfos" class="repeated">
|
53
59
|
<td id="playerInfos.index"></td>
|
54
60
|
<td id="playerInfos.name"></td>
|
55
61
|
<td id="playerInfos.score"></td>
|
62
|
+
<td id="playerInfos.viewpoint"></td>
|
56
63
|
</tr>
|
57
64
|
</table>
|
58
65
|
<div id="action-label"></div>
|
66
|
+
<div id="log-label" class="log-label"></div>
|
59
67
|
</div>
|
60
68
|
|
61
69
|
</body></html>
|
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.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: 1.6.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.6.0
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: nokogiri
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ! '>='
|
@@ -32,18 +37,44 @@ dependencies:
|
|
32
37
|
version: 1.5.0
|
33
38
|
type: :runtime
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.5.0
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
47
|
name: bundler
|
38
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.0.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
57
|
none: false
|
40
58
|
requirements:
|
41
59
|
- - ! '>='
|
42
60
|
- !ruby/object:Gem::Version
|
43
61
|
version: 1.0.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: sass
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 3.0.0
|
44
70
|
type: :runtime
|
45
71
|
prerelease: false
|
46
|
-
version_requirements:
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 3.0.0
|
47
78
|
description: Game server for Japanese Mahjong AI.
|
48
79
|
email:
|
49
80
|
- gimite+github@gmail.com
|
@@ -62,6 +93,7 @@ files:
|
|
62
93
|
- lib/mjai/active_game.rb
|
63
94
|
- lib/mjai/tcp_game_server.rb
|
64
95
|
- lib/mjai/mentsu.rb
|
96
|
+
- lib/mjai/tcp_active_game_server.rb
|
65
97
|
- lib/mjai/puppet_player.rb
|
66
98
|
- lib/mjai/action.rb
|
67
99
|
- lib/mjai/shanten_player.rb
|
@@ -70,6 +102,7 @@ files:
|
|
70
102
|
- lib/mjai/validation_error.rb
|
71
103
|
- lib/mjai/hora.rb
|
72
104
|
- lib/mjai/with_fields.rb
|
105
|
+
- lib/mjai/confidence_interval.rb
|
73
106
|
- lib/mjai/tcp_client_game.rb
|
74
107
|
- lib/mjai/game.rb
|
75
108
|
- lib/mjai/tcp_player.rb
|
@@ -428,7 +461,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
428
461
|
version: '0'
|
429
462
|
requirements: []
|
430
463
|
rubyforge_project:
|
431
|
-
rubygems_version: 1.8.
|
464
|
+
rubygems_version: 1.8.23
|
432
465
|
signing_key:
|
433
466
|
specification_version: 3
|
434
467
|
summary: Game server for Japanese Mahjong AI.
|