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 CHANGED
@@ -18,6 +18,7 @@ module Mjai
18
18
  [:bakaze, :pai],
19
19
  [:kyoku, :number],
20
20
  [:honba, :number],
21
+ [:kyotaku, :number],
21
22
  [:oya, :player],
22
23
  [:dora_marker, :pai],
23
24
  [:uradora_markers, :pais],
@@ -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
- reach = false
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
- actions =
93
+ next_actions =
87
94
  [Action.new({:type => :tsumo, :actor => action.actor, :pai => @pipais.pop()})]
88
- # TODO Add dora.
89
- next
95
+ # TODO Handle 4 kans.
90
96
  when :reach
91
- reach = true
97
+ reach_pending = true
92
98
  end
93
- actions = choose_actions(responses)
94
- if reach && (actions.empty? || ![:dahai, :hora].include?(actions[0].type))
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,:actor => tsumo_actor,
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
- # TODO ダブロンの上家取り
123
- for action in actions
124
- hora = get_hora(action)
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
- # TODO 積み棒
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) + self.honba * 100)
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 + self.honba * 300)
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
@@ -41,6 +41,10 @@ module Mjai
41
41
  return false
42
42
  end
43
43
 
44
+ def inspect
45
+ return '#<%p:path=%p>' % [self.class, self.path]
46
+ end
47
+
44
48
  end
45
49
 
46
50
  end
@@ -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 => [], # TODO
322
+ :uradoras => uradoras,
305
323
  :reach => action.actor.reach?,
306
- :double_reach => false, # TODO
307
- :ippatsu => false, # TODO
308
- :rinshan => false, # TODO
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 => false, # TODO
311
- :chankan => false, # TODO
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, (4 + pl.id - @chicha.id) % 4] }
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)
@@ -1,6 +1,6 @@
1
1
  require "optparse"
2
2
 
3
- require "mjai/tcp_game_server"
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 = TCPGameServer.new({
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 == 7) || (@type != "t" && @number == 9)
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, :ankan
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 == :dahai && action.actor != self
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 action.consumed[1].number == action.consumed[0].number + 1
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 = action.consumed[0]
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 })
@@ -38,17 +38,10 @@ module Mjai
38
38
  return create_action({:type => :dahai, :pai => action.pai, :tsumogiri => true})
39
39
  end
40
40
 
41
- if action.type == :tsumo && self.game.num_pipais > 0
42
- for pai in self.tehais
43
- if self.tehais.select(){ |tp| tp == pai }.size >= 4
44
- return create_action({:type => :ankan, :consumed => [pai] * 4})
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
@@ -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], @params[:port]])
25
+ puts("Listening on host %s, port %d" % [@params[:host], self.port])
28
26
  puts("URL: %s" % self.server_url)
29
- puts("Waiting for 4 players...")
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 < 4
47
+ if @players.size < self.num_tcp_players
50
48
  @players.push(TCPPlayer.new(socket, message["name"]))
51
- puts("Waiting for %s more players..." % (4 - @players.size))
52
- if @players.size == 4
53
- Thread.new(){ play_game() }
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 play_game()
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
- maybe_open(mjson_path, "w") do |mjson_out|
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
- puts("game %d: %s" % [
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
- puts("game %d: Ended with error" % @num_finished_games)
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" % [@params[:port], @params[:room]]
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]
@@ -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
- scores = elem["ten"].split(/,/).map(){ |s| s.to_i() * 100 }
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
- fan = elem["yaku"].split(/,/).each_slice(2).map(){ |y, f| f.to_i() }.inject(0, :+)
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
- # TODO Fill yaku field.
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
- if on_tenhou_event(elem, elems[j + 1]) == :broken
405
- break # Something is wrong.
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
@@ -75,3 +75,7 @@
75
75
  left: 400px;
76
76
  top: 0px;
77
77
  padding: 8px; }
78
+
79
+ .log-label {
80
+ font-family: monospace;
81
+ white-space: pre; }
@@ -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...kyoku.doraMarkers.length]
315
- wanpais[i + 2] = kyoku.doraMarkers[i]
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 = kyoku.doraMarkers.length; 0 <= _ref4 ? i < _ref4 : i > _ref4; 0 <= _ref4 ? i++ : i--) {
433
- wanpais[i + 2] = kyoku.doraMarkers[i];
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.1
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: 2012-04-30 00:00:00.000000000 Z
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: &76710820 !ruby/object:Gem::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: *76710820
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: &76710120 !ruby/object:Gem::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: *76710120
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: &76709680 !ruby/object:Gem::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: *76709680
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.11
464
+ rubygems_version: 1.8.23
432
465
  signing_key:
433
466
  specification_version: 3
434
467
  summary: Game server for Japanese Mahjong AI.