mjai 0.0.1 → 0.0.2
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/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.
|